/** * Component: Confirm Dialog * Documentation: documentation/frontend/components.md * * Reusable confirmation dialog for destructive actions. * Features: backdrop blur, smooth enter animation, Escape to close, focus trap, ARIA. */ 'use client'; import React, { useEffect, useRef } from 'react'; export interface ConfirmDialogProps { isOpen: boolean; title: string; message: string | React.ReactNode; confirmLabel?: string; cancelLabel?: string; confirmVariant?: 'danger' | 'primary'; onConfirm: () => void; onCancel: () => void; } export function ConfirmDialog({ isOpen, title, message, confirmLabel = 'Confirm', cancelLabel = 'Cancel', confirmVariant = 'danger', onConfirm, onCancel, }: ConfirmDialogProps) { const cancelRef = useRef(null); const confirmRef = useRef(null); const dialogRef = useRef(null); // Focus the cancel button on open (safer default for destructive dialogs) useEffect(() => { if (isOpen) { // Small delay to let animation start before stealing focus const t = setTimeout(() => cancelRef.current?.focus(), 50); return () => clearTimeout(t); } }, [isOpen]); // Escape to close + focus trap useEffect(() => { if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault(); onCancel(); return; } // Focus trap: tab cycles only within dialog if (e.key === 'Tab') { const focusable = dialogRef.current?.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); if (!focusable || focusable.length === 0) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault(); last.focus(); } } else { if (document.activeElement === last) { e.preventDefault(); first.focus(); } } } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [isOpen, onCancel]); // Prevent body scroll while open useEffect(() => { if (isOpen) { document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = ''; }; } }, [isOpen]); if (!isOpen) return null; const isDestructive = confirmVariant === 'danger'; return (
{/* Backdrop */} ); }