57 lines
2.1 KiB
TypeScript
57 lines
2.1 KiB
TypeScript
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<HTMLElement>(focusableSelector)].filter((element) => !element.hidden && element.getClientRects().length > 0);
|
|
}
|
|
|
|
export function useDialogFocus(onClose: () => void): RefObject<HTMLElement | null> {
|
|
const dialogRef = useRef<HTMLElement>(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<HTMLElement>("[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;
|
|
}
|