UNPKG

@base-ui-components/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

149 lines (148 loc) 4.58 kB
'use client'; import * as React from 'react'; import { safePolygon, useClick, useDismiss, useFloatingRootContext, useHover, useInteractions, useListNavigation, useRole, useTypeahead } from '@floating-ui/react'; import { mergeReactProps } from '../../utils/mergeReactProps.js'; import { useTransitionStatus } from '../../utils/useTransitionStatus.js'; import { useEventCallback } from '../../utils/useEventCallback.js'; import { useControlled } from '../../utils/useControlled.js'; import { TYPEAHEAD_RESET_MS } from '../../utils/constants.js'; import { useAfterExitAnimation } from '../../utils/useAfterExitAnimation.js'; import { useScrollLock } from '../../utils/useScrollLock.js'; const EMPTY_ARRAY = []; export function useMenuRoot(parameters) { const { open: openParam, defaultOpen, onOpenChange, orientation, direction, disabled, nested, closeParentOnEsc, loop, delay, openOnHover, onTypingChange, modal } = parameters; const [triggerElement, setTriggerElement] = React.useState(null); const [positionerElement, setPositionerElementUnwrapped] = React.useState(null); const popupRef = React.useRef(null); const positionerRef = React.useRef(null); const [hoverEnabled, setHoverEnabled] = React.useState(true); const [activeIndex, setActiveIndex] = React.useState(null); const [open, setOpenUnwrapped] = useControlled({ controlled: openParam, default: defaultOpen, name: 'useMenuRoot', state: 'open' }); const setPositionerElement = React.useCallback(value => { positionerRef.current = value; setPositionerElementUnwrapped(value); }, []); const allowMouseUpTriggerRef = React.useRef(false); useScrollLock(modal && open, triggerElement); const { mounted, setMounted, transitionStatus } = useTransitionStatus(open); const setOpen = useEventCallback((nextOpen, event) => { onOpenChange?.(nextOpen, event); setOpenUnwrapped(nextOpen); }); useAfterExitAnimation({ open, animatedElementRef: popupRef, onFinished: () => setMounted(false) }); const floatingRootContext = useFloatingRootContext({ elements: { reference: triggerElement, floating: positionerElement }, open, onOpenChange: setOpen }); const hover = useHover(floatingRootContext, { enabled: hoverEnabled && openOnHover && !disabled, handleClose: safePolygon({ blockPointerEvents: true }), mouseOnly: true, delay: { open: delay } }); const click = useClick(floatingRootContext, { enabled: !disabled, event: 'mousedown', toggle: !nested, ignoreMouse: nested }); const dismiss = useDismiss(floatingRootContext, { bubbles: closeParentOnEsc && nested, outsidePressEvent: 'mousedown' }); const role = useRole(floatingRootContext, { role: 'menu' }); const itemDomElements = React.useRef([]); const itemLabels = React.useRef([]); const listNavigation = useListNavigation(floatingRootContext, { enabled: !disabled, listRef: itemDomElements, activeIndex, nested, loop, orientation, rtl: direction === 'rtl', disabledIndices: EMPTY_ARRAY, onNavigate: setActiveIndex }); const typeahead = useTypeahead(floatingRootContext, { listRef: itemLabels, activeIndex, resetMs: TYPEAHEAD_RESET_MS, onMatch: index => { if (open && index !== activeIndex) { setActiveIndex(index); } }, onTypingChange }); const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([hover, click, dismiss, role, listNavigation, typeahead]); const getTriggerProps = React.useCallback(externalProps => getReferenceProps(mergeReactProps(externalProps, { onMouseEnter: () => { setHoverEnabled(true); } })), [getReferenceProps]); const getPopupProps = React.useCallback(externalProps => getFloatingProps(mergeReactProps(externalProps, { onMouseEnter: () => { setHoverEnabled(false); } })), [getFloatingProps]); return React.useMemo(() => ({ activeIndex, allowMouseUpTriggerRef, floatingRootContext, getItemProps, getPopupProps, getTriggerProps, itemDomElements, itemLabels, mounted, open, popupRef, positionerRef, setOpen, setPositionerElement, setTriggerElement, transitionStatus }), [activeIndex, floatingRootContext, getItemProps, getPopupProps, getTriggerProps, itemDomElements, itemLabels, mounted, open, positionerRef, setOpen, transitionStatus, setPositionerElement]); }