import React, { forwardRef, useCallback, useEffect, useRef } from "react"; import { composeEventHandlers, ownerDocument } from "../../../helpers"; import type { DescendantsManager } from "../../../hooks"; import { useEventCallback, useMergeRefs } from "../../../hooks"; import { Slot } from "../../slot/Slot"; interface RovingFocusProps extends Omit< React.HTMLAttributes, "tabIndex" > { asChild?: boolean; descendants: DescendantsManager; onEntryFocus?: (event: Event) => void; } const ENTRY_FOCUS = "rovingFocusGroup.onEntryFocus"; const EVENT_OPTIONS = { bubbles: false, cancelable: true }; const RovingFocus = forwardRef( ( { children, asChild, descendants, onKeyDown, onEntryFocus, onMouseDown, onFocus, ...rest }, ref: React.Ref, ) => { const _ref = React.useRef(null); const composedRefs = useMergeRefs(ref, _ref); const handleEntryFocus = useEventCallback(onEntryFocus); const isMouseFocusRef = useRef(false); useEffect(() => { const node = _ref.current; if (node) { node.addEventListener(ENTRY_FOCUS, handleEntryFocus); return () => node.removeEventListener(ENTRY_FOCUS, handleEntryFocus); } }, [handleEntryFocus]); const handleKeyDown = useCallback( (event: React.KeyboardEvent) => { const loop = false; const ownerDoc = ownerDocument(_ref?.current); const idx = descendants .values() .findIndex((x) => x.node.isSameNode(ownerDoc.activeElement)); const nextItem = () => { const next = descendants.nextEnabled(idx, loop); next?.node?.focus(); }; const prevItem = () => { const prev = descendants.prevEnabled(idx, loop); prev?.node?.focus(); }; const firstItem = () => { const first = descendants.firstEnabled(); first?.node?.focus(); }; const lastItem = () => { const last = descendants.lastEnabled(); last?.node?.focus(); }; const keyMap: Record = { ArrowUp: prevItem, ArrowDown: nextItem, Home: firstItem, End: lastItem, }; const action = keyMap[event.key]; if (action) { event.preventDefault(); action(event); } }, [descendants], ); const Comp = asChild ? Slot : "div"; return ( { isMouseFocusRef.current = true; })} onFocus={composeEventHandlers(onFocus, (event) => { if (event.target === event.currentTarget) { const entryFocusEvent = new CustomEvent(ENTRY_FOCUS, EVENT_OPTIONS); event.currentTarget.dispatchEvent(entryFocusEvent); if (!entryFocusEvent.defaultPrevented) { descendants.firstEnabled()?.node.focus({ preventScroll: true }); } } isMouseFocusRef.current = false; })} > {children} ); }, ); export { RovingFocus, type RovingFocusProps };