import { useEffect, useRef, type ReactNode } from 'react'; interface ModalPanelProps { children: ReactNode; className: string; labelledBy: string; onClose: () => void; } export function ModalPanel({ children, className, labelledBy, onClose, }: ModalPanelProps) { const panel = useRef(null); useEffect(() => { const returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null; const dialog = panel.current; const focusable = () => dialog ? [...dialog.querySelectorAll(FOCUSABLE_SELECTOR)].filter( (element) => !element.hasAttribute('disabled') ) : []; (focusable()[0] ?? dialog)?.focus(); const previousOverflow = document.documentElement.style.overflow; document.documentElement.style.overflow = 'hidden'; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { event.preventDefault(); onClose(); return; } if (event.key !== 'Tab') return; const elements = focusable(); const first = elements[0]; const last = elements.at(-1); if (!first || !last) { event.preventDefault(); dialog?.focus(); return; } const active = document.activeElement; if (!dialog?.contains(active)) { event.preventDefault(); (event.shiftKey ? last : first).focus(); } else if (event.shiftKey && active === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && active === last) { event.preventDefault(); first.focus(); } }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); document.documentElement.style.overflow = previousOverflow; if (returnFocus?.isConnected) returnFocus.focus(); }; }, [onClose]); return (
event.target === event.currentTarget && onClose() } >
{children}
); } const FOCUSABLE_SELECTOR = [ 'a[href]', 'button:not([disabled])', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', '[tabindex]:not([tabindex="-1"])', ].join(',');