File
Blob: src/client/hooks/use-focus-trap.ts
| 1 | import { type RefObject, useEffect } from "react"; |
| 2 | |
| 3 | const FOCUSABLE = [ |
| 4 | "a[href]", |
| 5 | "button:not(:disabled)", |
| 6 | "input:not(:disabled)", |
| 7 | "select:not(:disabled)", |
| 8 | "textarea:not(:disabled)", |
| 9 | '[tabindex]:not([tabindex="-1"])', |
| 10 | ].join(", "); |
| 11 | |
| 12 | export function useFocusTrap(containerRef: RefObject<HTMLElement | null>): void { |
| 13 | useEffect(() => { |
| 14 | const el = containerRef.current; |
| 15 | if (!el) return; |
| 16 | |
| 17 | const focusable = () => Array.from(el.querySelectorAll<HTMLElement>(FOCUSABLE)); |
| 18 | focusable()[0]?.focus(); |
| 19 | |
| 20 | const onKeyDown = (e: KeyboardEvent) => { |
| 21 | if (e.key !== "Tab") return; |
| 22 | const items = focusable(); |
| 23 | if (items.length === 0) return; |
| 24 | const first = items[0]; |
| 25 | const last = items[items.length - 1]; |
| 26 | |
| 27 | if (e.shiftKey && document.activeElement === first) { |
| 28 | e.preventDefault(); |
| 29 | last.focus(); |
| 30 | } else if (!e.shiftKey && document.activeElement === last) { |
| 31 | e.preventDefault(); |
| 32 | first.focus(); |
| 33 | } |
| 34 | }; |
| 35 | |
| 36 | el.addEventListener("keydown", onKeyDown); |
| 37 | return () => el.removeEventListener("keydown", onKeyDown); |
| 38 | }, [containerRef]); |
| 39 | } |