diff --git a/etc/hooks.api.md b/etc/hooks.api.md index dfd9f0a..55362de 100644 --- a/etc/hooks.api.md +++ b/etc/hooks.api.md @@ -54,6 +54,9 @@ export interface UseCounterResult { set: (value: number) => void; } +// @public +export function useFocusTrap(firstRef: RefObject, lastRef: RefObject): void; + // @public export function useHover(ref: RefObject, options?: UseHoverOptions): boolean; diff --git a/src/index.ts b/src/index.ts index 4c5ec4b..7d53164 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ export { useClickOutside } from "./use-click-outside"; export { useConst } from "./use-const"; export { useConstFn } from "./use-const-fn"; export { useCounter, type UseCounterResult } from "./use-counter"; +export { useFocusTrap } from "./use-focus-trap"; export { useHover, type UseHoverOptions } from "./use-hover"; export { useKeydown, type ModifierKeys } from "./use-keydown"; export { diff --git a/src/use-focus-trap.ts b/src/use-focus-trap.ts new file mode 100644 index 0000000..1fe8efc --- /dev/null +++ b/src/use-focus-trap.ts @@ -0,0 +1,29 @@ +import { useEffect, type RefObject } from "react"; + +/** + * Trap the tab focus between two elements. + * @param firstRef - A ref object of the element at the start of the trap. + * @param lastRef - A ref object of the element at the end of the trap. + */ +export function useFocusTrap( + firstRef: RefObject, + lastRef: RefObject +) { + const handleTab = (e: KeyboardEvent) => { + if (e.code !== "Tab") return; + if (e.shiftKey && document.activeElement === firstRef.current) { + e.preventDefault(); + lastRef.current?.focus(); + } else if (!e.shiftKey && document.activeElement === lastRef.current) { + e.preventDefault(); + firstRef.current?.focus(); + } + }; + + useEffect(() => { + document.addEventListener("keydown", handleTab); + return () => document.removeEventListener("keydown", handleTab); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); +}