import React, { ReactNode, useEffect, useId, useRef } from 'react'; import { X } from 'lucide-react'; import IconButton from './IconButton'; import { cn } from '../../utils/classNames'; interface ModalShellProps { open: boolean; title: string; children: ReactNode; footer?: ReactNode; maxWidthClass?: string; onClose: () => void; } const FOCUSABLE = 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])'; const ModalShell: React.FC = ({ open, title, children, footer, maxWidthClass = 'max-w-3xl', onClose, }) => { const dialogRef = useRef(null); const titleId = useId(); // Hold onClose in a ref so the focus-trap effect below depends only on `open`. // Otherwise a parent that passes a fresh onClose each render (common) would // re-run the effect on every keystroke, and its cleanup would steal focus from // whatever input the user is typing in. const onCloseRef = useRef(onClose); onCloseRef.current = onClose; // Escape to close, focus trap, and focus restore — applies to every modal that // uses ModalShell. Works inside the WordPress Shadow DOM (reads activeElement // from the dialog's root node). useEffect(() => { if (!open) return; const dialog = dialogRef.current; const root = (dialog?.getRootNode() as Document | ShadowRoot) ?? document; const previouslyFocused = root.activeElement as HTMLElement | null; const focusables = () => dialog ? Array.from(dialog.querySelectorAll(FOCUSABLE)) : []; // Move focus into the dialog on open. (focusables()[0] ?? dialog)?.focus(); const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { event.stopPropagation(); onCloseRef.current(); return; } if (event.key !== 'Tab') return; const items = focusables(); if (items.length === 0) { event.preventDefault(); dialog?.focus(); return; } const active = root.activeElement as HTMLElement | null; const index = active ? items.indexOf(active) : -1; if (event.shiftKey && (index <= 0)) { event.preventDefault(); items[items.length - 1].focus(); } else if (!event.shiftKey && index === items.length - 1) { event.preventDefault(); items[0].focus(); } }; document.addEventListener('keydown', onKeyDown, true); return () => { document.removeEventListener('keydown', onKeyDown, true); previouslyFocused?.focus?.(); }; }, [open]); if (!open) return null; return (
); }; export default ModalShell;