"use client"; import { useEffect, useRef, type ReactNode } from "react"; import { createPortal } from "react-dom"; const FOCUSABLE = 'a[href],button:not([disabled]),input,select,textarea,[tabindex]:not([tabindex="-1"])'; /** Accessible modal: portal to , ESC + backdrop close, focus trap, scroll lock, * focus restore on close. Renders nothing when closed. */ export function Modal({ open, onClose, title, children, wide, }: { open: boolean; onClose: () => void; title?: ReactNode; children: ReactNode; wide?: boolean; }) { const panelRef = useRef(null); const lastFocus = useRef(null); useEffect(() => { if (!open) return; lastFocus.current = document.activeElement as HTMLElement | null; const prevOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; panelRef.current?.focus(); function onKey(e: KeyboardEvent) { if (e.key === "Escape") { onClose(); return; } if (e.key !== "Tab") return; const nodes = panelRef.current?.querySelectorAll(FOCUSABLE); if (!nodes || nodes.length === 0) return; const first = nodes[0]; const last = nodes[nodes.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); document.body.style.overflow = prevOverflow; lastFocus.current?.focus(); }; }, [open, onClose]); if (!open || typeof document === "undefined") return null; return createPortal(
e.target === e.currentTarget && onClose()}>
{title}
{children}
, document.body, ); }