"use client"; import * as React from "react"; const FOCUSABLE_SELECTOR = [ "a[href]", "button:not(:disabled)", "input:not(:disabled)", "select:not(:disabled)", "textarea:not(:disabled)", '[tabindex]:not([tabindex="-1"])', "[contenteditable]", ].join(", "); /** * Represents the focus movement helpers returned by {@link useFocusManager}. */ interface FocusManager { /** * Focuses the first focusable element in the container. */ focusFirst: () => void; /** * Focuses the last focusable element in the container. */ focusLast: () => void; /** * Focuses the next focusable element after the currently focused one. */ focusNext: () => void; /** * Focuses the previous focusable element before the currently focused one. */ focusPrevious: () => void; } /** * Provides programmatic focus movement helpers for a container element. * * @param containerRef - Ref pointing to the container whose focusable descendants should be managed. * @returns Focus helpers for moving to the first, last, next, or previous focusable descendant. * * @example * ```tsx * const containerRef = React.useRef(null); * const {focusNext, focusPrevious} = useFocusManager(containerRef); * ``` */ export function useFocusManager(containerRef: React.RefObject): FocusManager { const getFocusable = React.useCallback((): HTMLElement[] => { if (!containerRef.current) { return []; } return [...containerRef.current.querySelectorAll(FOCUSABLE_SELECTOR)].filter( (element) => !element.hasAttribute("disabled") && element.tabIndex >= 0, ); }, [containerRef]); const focusFirst = React.useCallback(() => { getFocusable()[0]?.focus(); }, [getFocusable]); const focusLast = React.useCallback(() => { const items = getFocusable(); items.at(-1)?.focus(); }, [getFocusable]); const focusNext = React.useCallback(() => { const items = getFocusable(); const current = document.activeElement; const index = items.indexOf(current as HTMLElement); const next = items[index + 1] ?? items[0]; next?.focus(); }, [getFocusable]); const focusPrevious = React.useCallback(() => { const items = getFocusable(); const current = document.activeElement; const index = items.indexOf(current as HTMLElement); const previous = items[index - 1] ?? items.at(-1); previous?.focus(); }, [getFocusable]); return {focusFirst, focusLast, focusNext, focusPrevious}; }