32 lines
1.2 KiB
TypeScript
32 lines
1.2 KiB
TypeScript
|
|
import { useEffect, type RefObject } from "react";
|
||
|
|
|
||
|
|
const FOCUSABLE_SELECTOR =
|
||
|
|
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||
|
|
|
||
|
|
export function useFocusTrap(ref: RefObject<HTMLElement | null>, active: boolean) {
|
||
|
|
useEffect(() => {
|
||
|
|
if (!active) return;
|
||
|
|
const el = ref.current;
|
||
|
|
if (!el) return;
|
||
|
|
|
||
|
|
const first = el.querySelector<HTMLElement>(FOCUSABLE_SELECTOR) ?? el;
|
||
|
|
first.focus();
|
||
|
|
|
||
|
|
function onKeyDown(e: KeyboardEvent) {
|
||
|
|
if (e.key !== "Tab") return;
|
||
|
|
const focusables = Array.from(el?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR) ?? []);
|
||
|
|
if (focusables.length === 0) return;
|
||
|
|
const firstEl = focusables[0];
|
||
|
|
const lastEl = focusables[focusables.length - 1];
|
||
|
|
if (e.shiftKey) {
|
||
|
|
if (document.activeElement === firstEl) { e.preventDefault(); lastEl.focus(); }
|
||
|
|
} else {
|
||
|
|
if (document.activeElement === lastEl) { e.preventDefault(); firstEl.focus(); }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
document.addEventListener("keydown", onKeyDown);
|
||
|
|
return () => document.removeEventListener("keydown", onKeyDown);
|
||
|
|
}, [ref, active]);
|
||
|
|
}
|