UNPKG

@itwin/itwinui-react

Version:

A react component library for iTwinUI

236 lines (235 loc) 6.45 kB
import * as React from 'react'; import cx from 'classnames'; import { useMergedRefs, Box, Portal, useControlledState, cloneElementWithRef, mergeRefs, useSyncExternalStore, mergeEventHandlers, useFocusableElements, } from '../../utils/index.js'; import { PopoverOpenContext, usePopover } from '../Popover/Popover.js'; import { FloatingNode, useFloatingNodeId, useFloatingParentNodeId, useFloatingTree, useListNavigation, useInteractions, } from '@floating-ui/react'; export const Menu = React.forwardRef((props, ref) => { let { className, trigger, positionReference, portal: portalProp, popoverProps: popoverPropsProp, children, ...rest } = props; let menuPortalContext = React.useContext(MenuPortalContext); let portal = portalProp ?? menuPortalContext; let tree = useFloatingTree(); let nodeId = useFloatingNodeId(); let parentId = useFloatingParentNodeId(); let { interactions: interactionsProp, visible: visibleProp, onVisibleChange: onVisibleChangeProp, ...restPopoverProps } = popoverPropsProp ?? {}; let { listNavigation: listNavigationPropsProp, hover: hoverProp, ...restInteractionsProps } = interactionsProp ?? {}; let [visible, setVisible] = useControlledState( false, visibleProp, onVisibleChangeProp, ); let [hasFocusedNodeInSubmenu, setHasFocusedNodeInSubmenu] = React.useState(false); let [menuElement, setMenuElement] = React.useState(null); let { focusableElementsRef, focusableElements } = useFocusableElements( menuElement, { filter: (allElements) => allElements.filter( (i) => !allElements?.some((p) => p.contains(i.parentElement)), ), }, ); let [activeIndex, setActiveIndex] = React.useState(null); let popover = usePopover({ nodeId, visible, onVisibleChange: (open) => (open ? setVisible(true) : close()), interactions: { hover: null == tree ? hoverProp : { enabled: !!hoverProp && !hasFocusedNodeInSubmenu, ...hoverProp, }, ...restInteractionsProps, }, ...restPopoverProps, middleware: { size: { maxHeight: 'var(--iui-menu-max-height)', }, ...restPopoverProps.middleware, }, }); let { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([ useListNavigation(popover.context, { activeIndex, focusItemOnHover: false, listRef: focusableElementsRef, onNavigate: setActiveIndex, ...listNavigationPropsProp, }), ]); React.useEffect(() => { if (void 0 !== positionReference) popover.refs.setPositionReference(positionReference); }, [popover.refs, positionReference]); let refs = useMergedRefs(setMenuElement, ref, popover.refs.setFloating); let triggerRef = React.useRef(null); let close = React.useCallback(() => { setVisible(false); if (null == parentId) triggerRef.current?.focus({ preventScroll: true, }); }, [parentId, setVisible]); useSyncExternalStore( React.useCallback(() => { let closeUnrelatedMenus = (event) => { if ( (parentId === event.parentId && nodeId !== event.nodeId) || parentId === event.nodeId ) { setVisible(false); setHasFocusedNodeInSubmenu(false); } }; tree?.events.on('onNodeFocused', closeUnrelatedMenus); return () => { tree?.events.off('onNodeFocused', closeUnrelatedMenus); }; }, [nodeId, parentId, tree?.events, setVisible]), () => void 0, () => void 0, ); let popoverGetItemProps = React.useCallback( ({ focusableItemIndex, userProps }) => getItemProps({ ...userProps, tabIndex: null != activeIndex && activeIndex >= 0 && null != focusableItemIndex && focusableItemIndex >= 0 && activeIndex === focusableItemIndex ? 0 : -1, onFocus: mergeEventHandlers(userProps?.onFocus, () => { queueMicrotask(() => { setHasFocusedNodeInSubmenu(true); }); tree?.events.emit('onNodeFocused', { nodeId: nodeId, parentId: parentId, }); }), onMouseEnter: mergeEventHandlers(userProps?.onMouseEnter, (event) => { if (null != focusableItemIndex && focusableItemIndex >= 0) setActiveIndex(focusableItemIndex); if (event.target === event.currentTarget) event.currentTarget.focus({ focusVisible: false, }); }), }), [activeIndex, getItemProps, nodeId, parentId, tree?.events], ); let reference = cloneElementWithRef(trigger, (triggerChild) => getReferenceProps( popover.getReferenceProps({ 'aria-haspopup': 'menu', ...triggerChild.props, 'aria-expanded': popover.open, ref: mergeRefs(triggerRef, popover.refs.setReference), }), ), ); let floating = popover.open && React.createElement( Portal, { portal: portal, }, React.createElement( Box, { as: 'div', className: cx('iui-menu', className), ref: refs, ...getFloatingProps( popover.getFloatingProps({ role: 'menu', ...rest, }), ), }, children, ), ); return React.createElement( React.Fragment, null, React.createElement( MenuContext.Provider, { value: React.useMemo( () => ({ popoverGetItemProps, focusableElements, }), [focusableElements, popoverGetItemProps], ), }, React.createElement( MenuPortalContext.Provider, { value: portal, }, React.createElement( PopoverOpenContext.Provider, { value: popover.open, }, reference, ), null != tree ? React.createElement( FloatingNode, { id: nodeId, }, floating, ) : floating, ), ), ); }); export const MenuContext = React.createContext(void 0); export const MenuPortalContext = React.createContext(void 0);