import { useEffect, useRef, type RefObject } from 'react'; const FOCUSABLE_SELECTOR = [ 'button:not([disabled])', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'a[href]', '[tabindex]:not([tabindex="-1"])', ].join(','); interface UseModalFocusOptions { dialogRef: RefObject; initialFocusRef: RefObject; dismissDisabled?: boolean; onDismiss: () => void; } export function useModalFocus({ dialogRef, initialFocusRef, dismissDisabled = false, onDismiss, }: UseModalFocusOptions): void { const dismissDisabledRef = useRef(dismissDisabled); const onDismissRef = useRef(onDismiss); dismissDisabledRef.current = dismissDisabled; onDismissRef.current = onDismiss; useEffect(() => { const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null; initialFocusRef.current?.focus(); const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { if (!dismissDisabledRef.current) { event.preventDefault(); onDismissRef.current(); } return; } if (event.key !== 'Tab') return; const dialog = dialogRef.current; if (!dialog) return; const focusable = [...dialog.querySelectorAll(FOCUSABLE_SELECTOR)]; const first = focusable[0]; const last = focusable.at(-1); if (!first || !last) return; if ( event.shiftKey && (document.activeElement === first || !dialog.contains(document.activeElement)) ) { event.preventDefault(); last.focus(); } else if ( !event.shiftKey && (document.activeElement === last || !dialog.contains(document.activeElement)) ) { event.preventDefault(); first.focus(); } }; document.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); previouslyFocused?.focus(); }; }, [dialogRef, initialFocusRef]); }