@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
JavaScript
'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]);
}