ConfirmModal never called useModalLock, so the page scrolled behind every confirmation dialog (8+ usages). Add the ref-counted scroll lock and guard backdrop-click against the loading state, matching FormModal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
172 lines
4.8 KiB
TypeScript
172 lines
4.8 KiB
TypeScript
import { useEffect, type ReactNode } from "react";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import useReducedMotion from "../hooks/useReducedMotion";
|
|
import useModalLock from "../hooks/useModalLock";
|
|
|
|
interface ConfirmModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onConfirm: () => void;
|
|
title: string;
|
|
message: ReactNode;
|
|
confirmText?: string;
|
|
cancelText?: string;
|
|
type?: "danger" | "warning" | "default" | "info";
|
|
confirmVariant?: "danger" | "primary";
|
|
loading?: boolean;
|
|
}
|
|
|
|
function ConfirmIcon({ type }: { type: string }) {
|
|
switch (type) {
|
|
case "danger":
|
|
return (
|
|
<svg
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<line x1="15" y1="9" x2="9" y2="15" />
|
|
<line x1="9" y1="9" x2="15" y2="15" />
|
|
</svg>
|
|
);
|
|
case "info":
|
|
return (
|
|
<svg
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<line x1="12" y1="16" x2="12" y2="12" />
|
|
<line x1="12" y1="8" x2="12.01" y2="8" />
|
|
</svg>
|
|
);
|
|
case "warning":
|
|
return (
|
|
<svg
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
<line x1="12" y1="9" x2="12" y2="13" />
|
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
</svg>
|
|
);
|
|
default:
|
|
return (
|
|
<svg
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<line x1="12" y1="16" x2="12" y2="12" />
|
|
<line x1="12" y1="8" x2="12.01" y2="8" />
|
|
</svg>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default function ConfirmModal({
|
|
isOpen,
|
|
onClose,
|
|
onConfirm,
|
|
title,
|
|
message,
|
|
confirmText = "Potvrdit",
|
|
cancelText = "Zrušit",
|
|
type = "default",
|
|
confirmVariant,
|
|
loading,
|
|
}: ConfirmModalProps) {
|
|
const reducedMotion = useReducedMotion();
|
|
// Lock body scroll while open (ref-counted, nesting-safe) — matches FormModal.
|
|
useModalLock(isOpen);
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
|
|
function handleKeyDown(e: KeyboardEvent) {
|
|
if (e.key === "Escape" && !loading) {
|
|
onClose();
|
|
}
|
|
}
|
|
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
}, [isOpen, loading, onClose]);
|
|
|
|
const duration = reducedMotion ? 0 : 0.22;
|
|
const ease = [0.16, 1, 0.3, 1] as const;
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.div
|
|
className="admin-modal-overlay"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: reducedMotion ? 0 : 0.18 }}
|
|
>
|
|
<div
|
|
className="admin-modal-backdrop"
|
|
onClick={() => !loading && onClose()}
|
|
/>
|
|
<motion.div
|
|
className="admin-modal admin-confirm-modal"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="confirm-modal-title"
|
|
initial={{ opacity: 0, scale: 0.96, y: 16 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.97, y: 8 }}
|
|
transition={{ duration, ease }}
|
|
>
|
|
<div className="admin-modal-body admin-confirm-content">
|
|
<div className={`admin-confirm-icon admin-confirm-icon-${type}`}>
|
|
<ConfirmIcon type={type} />
|
|
</div>
|
|
<h2 id="confirm-modal-title" className="admin-confirm-title">
|
|
{title}
|
|
</h2>
|
|
<p className="admin-confirm-message">{message}</p>
|
|
</div>
|
|
<div className="admin-modal-footer">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="admin-btn admin-btn-secondary"
|
|
disabled={loading}
|
|
>
|
|
{cancelText}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={onConfirm}
|
|
className="admin-btn admin-btn-primary"
|
|
disabled={loading}
|
|
>
|
|
{loading ? "Zpracování..." : confirmText}
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|