import { useLayoutEffect, useRef, type RefObject } from "react"; const focusableSelector = [ "a[href]", "button:not([disabled])", "input:not([disabled])", "select:not([disabled])", "textarea:not([disabled])", '[tabindex]:not([tabindex="-1"])', ].join(","); function focusableElements(dialog: HTMLElement) { return [...dialog.querySelectorAll(focusableSelector)].filter((element) => !element.hidden && element.getClientRects().length > 0); } export function useDialogFocus(onClose: () => void): RefObject { const dialogRef = useRef(null); const onCloseRef = useRef(onClose); onCloseRef.current = onClose; useLayoutEffect(() => { const dialog = dialogRef.current; if (!dialog) return undefined; const returnTarget = document.activeElement instanceof HTMLElement ? document.activeElement : null; const initial = dialog.querySelector("[data-dialog-initial-focus]") ?? focusableElements(dialog)[0] ?? dialog; initial.focus(); const handleKeyDown = (event: globalThis.KeyboardEvent) => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); onCloseRef.current(); return; } if (event.key !== "Tab") return; const focusable = focusableElements(dialog); if (focusable.length === 0) { event.preventDefault(); dialog.focus(); return; } const first = focusable[0]!; const last = focusable.at(-1)!; 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, true); return () => { document.removeEventListener("keydown", handleKeyDown, true); if (returnTarget?.isConnected) returnTarget.focus(); }; }, []); return dialogRef; }