import { useEffect, useId, useLayoutEffect, useRef } from "react";
interface ConfirmDialogProps {
title: string;
message: string;
confirmLabel: string;
variant?: "destructive" | "primary";
onConfirm: () => void;
onCancel: () => void;
}
const FOCUSABLE = 'button:not([disabled]),[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
export function ConfirmDialog({
title,
message,
confirmLabel,
variant = "destructive",
onConfirm,
onCancel,
}: ConfirmDialogProps) {
const titleId = useId();
const dialogRef = useRef<HTMLDivElement>(null);
const onCancelRef = useRef(onCancel);
useLayoutEffect(() => { onCancelRef.current = onCancel; });
// Close on Escape
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
onCancelRef.current();
}
};
document.addEventListener("keydown", handleKey);
return () => document.removeEventListener("keydown", handleKey);
}, []);
// Initial focus + Tab trap
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
const focusable = () => Array.from(dialog.querySelectorAll<HTMLElement>(FOCUSABLE));
focusable()[0]?.focus();
const handleTab = (e: KeyboardEvent) => {
if (e.key !== "Tab") return;
const els = focusable();
if (els.length === 0) return;
const first = els[0];
const last = els[els.length - 1];
if (e.shiftKey) {
if (document.activeElement === first) { e.preventDefault(); last.focus(); }
} else {
if (document.activeElement === last) { e.preventDefault(); first.focus(); }
}
};
dialog.addEventListener("keydown", handleTab);
return () => dialog.removeEventListener("keydown", handleTab);
}, []);
const confirmClass =
variant === "primary"
? "px-4 py-2 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 disabled:opacity-50"
: "px-4 py-2 bg-red-600 text-white rounded text-sm hover:bg-red-700 disabled:opacity-50";
return (
<div
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50"
onClick={onCancel}
>
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
className="bg-white dark:bg-gray-900 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 p-6 max-w-sm w-full space-y-4"
onClick={(e) => e.stopPropagation()}
>
<h2 id={titleId} className="text-lg font-semibold text-gray-900 dark:text-gray-100">{title}</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">{message}</p>
<div className="flex gap-2 justify-end">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 border border-gray-300 dark:border-gray-700 text-gray-500 dark:text-gray-400 rounded text-sm hover:bg-gray-200 dark:hover:bg-gray-800"
>
Cancel
</button>
<button
type="button"
onClick={onConfirm}
className={confirmClass}
>
{confirmLabel}
</button>
</div>
</div>
</div>
);
}