import { useEffect, useRef, type ReactNode } from 'react' import { createPortal } from 'react-dom' type ModalProps = { open: boolean title?: string onClose: () => void children: ReactNode size?: 'md' | 'lg' } function focusable(root: HTMLElement | null): HTMLElement[] { if (!root) return [] const sel = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' return Array.from(root.querySelectorAll(sel)).filter( (el) => !el.hasAttribute('disabled') && el.getAttribute('aria-hidden') !== 'true', ) } // Reusable modal shell: portal to , ESC to close, Tab focus-trap, // focus-on-open (prefers [data-modal-autofocus]), focus restore on close, // click-on-overlay to close, and body scroll lock while open. export function Modal({ open, title, onClose, children, size = 'md' }: ModalProps) { const dialogRef = useRef(null) const prevFocus = useRef(null) const onCloseRef = useRef(onClose) onCloseRef.current = onClose useEffect(() => { if (!open) return prevFocus.current = document.activeElement as HTMLElement | null const auto = dialogRef.current?.querySelector('[data-modal-autofocus]') ;(auto ?? focusable(dialogRef.current)[0] ?? dialogRef.current)?.focus() function onKeyDown(e: KeyboardEvent) { if (e.key === 'Escape') { e.preventDefault() onCloseRef.current() return } if (e.key !== 'Tab') return const items = focusable(dialogRef.current) if (items.length === 0) { e.preventDefault() return } const first = items[0] const last = items[items.length - 1] const active = document.activeElement as HTMLElement | null const inside = dialogRef.current?.contains(active) if (e.shiftKey) { if (active === first || !inside) { e.preventDefault() last.focus() } } else if (active === last || !inside) { e.preventDefault() first.focus() } } document.addEventListener('keydown', onKeyDown, true) const prevOverflow = document.body.style.overflow document.body.style.overflow = 'hidden' return () => { document.removeEventListener('keydown', onKeyDown, true) document.body.style.overflow = prevOverflow prevFocus.current?.focus?.() } }, [open]) if (!open) return null return createPortal(
{ if (e.target === e.currentTarget) onClose() }} >
{title &&
{title}
} {children}
, document.body, ) }