@spaced-out/ui-design-system
Version:
Sense UI components library
153 lines (132 loc) • 4.31 kB
Flow
// @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<SkipElementFromNavigationProps, HTMLDivElement>(
(
{children, className, ...restProps}: SkipElementFromNavigationProps,
ref,
) => (
<div {...restProps} className={className} tabIndex={-1} ref={ref}>
{children}
</div>
),
);
SkipElementFromNavigation.displayName = SKIP_ELEMENT_DISPLAY_NAME;
export type FocusManagerWithArrowKeyNavigationProps = {
...FocusManagerProps,
cols?: number,
orientation?: 'horizontal' | 'vertical',
focusItemOnOpen?: 'auto' | boolean,
loop?: boolean,
listReference?: {current: Array<HTMLElement>},
};
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 (
<FloatingFocusManager
context={context}
modal={modal}
initialFocus={initialFocus}
{...restFloatingFocusManagerProps}
>
<div
ref={refs.setFloating}
data-testid="FocusManagerWithArrowKeyNavigation"
{...getFloatingProps()}
className={classify(css.wrapper, classNames?.wrapper)}
>
{clonedChildren}
</div>
</FloatingFocusManager>
);
};