// @flow strict import * as React from 'react'; import { // $FlowFixMe[untyped-import] FloatingFocusManager, // $FlowFixMe[untyped-import] useFloating, // $FlowFixMe[untyped-import] useInteractions, // $FlowFixMe[untyped-import] useListNavigation, } from '@floating-ui/react'; import classify from '../../utils/classify'; import type {FocusManagerProps} from '../FocusManager'; import css from './FocusManagerWithArrowKeyNavigation.module.css'; const SKIP_ELEMENT_DISPLAY_NAME = 'SkipElementFromNavigation'; export type SkipElementFromNavigationProps = { children: React.Node, className?: string, ... }; export const SkipElementFromNavigation: React$AbstractComponent< SkipElementFromNavigationProps, HTMLDivElement, > = React.forwardRef( ( {children, className, ...restProps}: SkipElementFromNavigationProps, ref, ) => (
{children}
), ); SkipElementFromNavigation.displayName = SKIP_ELEMENT_DISPLAY_NAME; export type FocusManagerWithArrowKeyNavigationProps = { ...FocusManagerProps, cols?: number, orientation?: 'horizontal' | 'vertical', focusItemOnOpen?: 'auto' | boolean, loop?: boolean, listReference?: {current: Array}, }; export const FocusManagerWithArrowKeyNavigation = ( props: FocusManagerWithArrowKeyNavigationProps, ): React.Node => { const { classNames, children, initialFocus = -1, orientation = 'vertical', modal = false, cols = 1, focusItemOnOpen = 'auto', loop = false, listReference, ...restFloatingFocusManagerProps } = props; const {refs, context} = useFloating({open: true}); const [activeIndex, setActiveIndex] = React.useState(null); const listRef = React.useRef([]); const childrenArray = React.Children.toArray(children).filter(Boolean); // Note(Nishant): This is to correctly call the onClick handler which could have been on child // we also need to set the active index correctly on click for list navigation to work const childOnClickPassthrough = (childOnClickHandler, index, ...args) => { if (childOnClickHandler) { childOnClickHandler(...args); } setActiveIndex(index); }; // Add childOnClickPassthrough for the list of references passed in case of custom nodes passed if (listReference) { listReference.current.map((element, index) => { const childClickHandler = element.onclick; element.onclick = (...args) => { childOnClickPassthrough(childClickHandler, index, ...args); }; }); } // Note(Nishant): Force the list navigation props onto the children // while using this component make sure your all the passed children have a focus state let skippedChildrenCount = 0; const clonedChildren = listReference ? children : childrenArray.map((child, index) => { const {onClick: childClickHandler} = child.props; let adjustedIndex = index - skippedChildrenCount; if (child?.type?.displayName === SKIP_ELEMENT_DISPLAY_NAME) { skippedChildrenCount++; adjustedIndex = null; } return React.cloneElement(child, { ...child.props, tabIndex: activeIndex === index ? 0 : -1, ref: (node) => { if (adjustedIndex !== null) { listRef.current[adjustedIndex] = node; } }, onClick: (...args) => { childOnClickPassthrough(childClickHandler, adjustedIndex, ...args); }, }); }); const listNavigation = useListNavigation(context, { orientation, cols, listRef: listReference ? listReference : listRef, activeIndex, onNavigate: setActiveIndex, focusItemOnOpen, loop, }); const {getFloatingProps} = useInteractions([listNavigation]); return (
{clonedChildren}
); };