import { useEffect, useRef } from 'react'; const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), ' + 'textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; /** * Trap keyboard focus within an overlay while `active` (docs/architecture/03 → * Shell). On activation it remembers the previously focused element and moves * focus into the container; `Tab`/`Shift+Tab` cycle within it; on deactivation * focus returns to the trigger. Shared by every overlay (the confirm dialog now, * the feature-modal shell later) so accessibility is implemented once. * * `initialSelector` optionally picks which child receives focus on open * (e.g. the Cancel button for a destructive confirm); it falls back to the * first focusable child. */ export function useFocusTrap( active: boolean, initialSelector?: string, ) { const ref = useRef(null); const returnTo = useRef(null); useEffect(() => { const el = ref.current; if (!active || !el) return; returnTo.current = document.activeElement; const initial = (initialSelector ? el.querySelector(initialSelector) : null) ?? el.querySelector(FOCUSABLE); initial?.focus(); const onKey = (e: KeyboardEvent) => { if (e.key !== 'Tab') return; const f = el.querySelectorAll(FOCUSABLE); if (!f.length) return; const first = f[0]; const last = f[f.length - 1]; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } }; el.addEventListener('keydown', onKey); return () => { el.removeEventListener('keydown', onKey); (returnTo.current as HTMLElement | null)?.focus(); }; }, [active, initialSelector]); return ref; }