import { useEffect, useRef, type ReactNode } from 'react'; import { X } from 'lucide-react'; import { cn } from '@/lib/cn'; import { Button } from './Button'; interface ModalProps { open: boolean; onClose: () => void; title: string; children: ReactNode; footer?: ReactNode; className?: string; } /** Accessible dialog: Escape to close, focus trapping, restores focus on close. */ export function Modal({ open, onClose, title, children, footer, className }: ModalProps) { const panelRef = useRef(null); const previouslyFocused = useRef(null); const onCloseRef = useRef(onClose); onCloseRef.current = onClose; // Focus into the dialog ONCE when it opens (not on every parent re-render — // that was stealing focus to the close button on each keystroke). Prefer the // first real field over the X button so composers land in their input. useEffect(() => { if (!open) return; previouslyFocused.current = document.activeElement as HTMLElement | null; const panel = panelRef.current; const target = panel?.querySelector('[data-autofocus]') ?? panel?.querySelector('input:not([type="hidden"]), textarea, select') ?? panel?.querySelector('a[href], button:not([disabled])'); target?.focus(); return () => { previouslyFocused.current?.focus(); }; }, [open]); // Escape to close + focus trap. Reads onClose via a ref so a changing // onClose identity never re-binds (and never re-triggers the focus effect). useEffect(() => { if (!open) return; const panel = panelRef.current; function onKey(e: KeyboardEvent) { if (e.key === 'Escape') { e.preventDefault(); onCloseRef.current(); return; } if (e.key === 'Tab' && panel) { const focusable = panel.querySelectorAll( 'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])', ); if (focusable.length === 0) return; const first = focusable[0]!; const last = focusable[focusable.length - 1]!; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } } } document.addEventListener('keydown', onKey); return () => document.removeEventListener('keydown', onKey); }, [open]); if (!open) return null; return (

{title}

{children}
{footer &&
{footer}
}
); }