import { useCallback, useRef } from "react"; import getFocusableElements from "./getFocusableElements"; interface Options { /** * Boolean if the focus wrap behavior should be disabled. */ disabled?: boolean; /** * Boolean if the list of focusable elements should not be cached after the * first tab key press. This should only be set to `true` if you have a lot of * dynamic content whin your element and the first and last elements change. */ disableFocusCache?: boolean; /** * An optional keydown event handler to merge with the focus wrap behavior. */ onKeyDown?: React.KeyboardEventHandler; } /** * Creates an `onKeyDown` event handler to trap keyboard focus within a * container element. * * @typeparam E The HTMLElement type that has the keydown event listener * attached. * @param options All the options for handling tab focus wrapping. * @return The kedown event handler to enforce focus wrapping or the onKeyDown * prop if this functionality is disabled. */ export default function useTabFocusWrap({ disabled = false, disableFocusCache = false, onKeyDown, }: Options): React.KeyboardEventHandler | undefined { const focusables = useRef([]); const handleKeyDown = useCallback>( (event): void => { if (onKeyDown) { onKeyDown(event); } if (event.key !== "Tab") { return; } if (disableFocusCache || !focusables.current.length) { focusables.current = getFocusableElements(event.currentTarget); } const elements = focusables.current; const l = elements.length; if (l === 0) { return; } if (l === 1) { event.preventDefault(); } else if (elements[0] === event.target && event.shiftKey) { event.preventDefault(); elements[l - 1].focus(); } else if (elements[l - 1] === event.target && !event.shiftKey) { event.preventDefault(); elements[0].focus(); } }, [onKeyDown, disableFocusCache] ); return disabled ? onKeyDown : handleKeyDown; }