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.

134 lines (132 loc) 5.03 kB
'use client'; import _formatErrorMessage from "@base-ui-components/utils/formatErrorMessage"; import * as React from 'react'; import { safePolygon, useClick, useHoverReferenceInteraction, useInteractions } from "../../floating-ui-react/index.js"; import { useMenuRootContext } from "../root/MenuRootContext.js"; import { useBaseUiId } from "../../utils/useBaseUiId.js"; import { triggerOpenStateMapping } from "../../utils/popupStateMapping.js"; import { useCompositeListItem } from "../../composite/list/useCompositeListItem.js"; import { useMenuItem } from "../item/useMenuItem.js"; import { useRenderElement } from "../../utils/useRenderElement.js"; import { useMenuPositionerContext } from "../positioner/MenuPositionerContext.js"; import { useTriggerRegistration } from "../../utils/popups/index.js"; import { useMenuSubmenuRootContext } from "../submenu-root/MenuSubmenuRootContext.js"; /** * A menu item that opens a submenu. * Renders a `<div>` element. * * Documentation: [Base UI Menu](https://base-ui.com/react/components/menu) */ export const MenuSubmenuTrigger = /*#__PURE__*/React.forwardRef(function SubmenuTriggerComponent(componentProps, forwardedRef) { const { render, className, label, id: idProp, nativeButton = false, openOnHover = true, delay = 100, closeDelay = 0, disabled: disabledProp = false, ...elementProps } = componentProps; const listItem = useCompositeListItem(); const menuPositionerContext = useMenuPositionerContext(); const { store } = useMenuRootContext(); const thisTriggerId = useBaseUiId(idProp); const open = store.useState('open'); const floatingRootContext = store.useState('floatingRootContext'); const floatingTreeRoot = store.useState('floatingTreeRoot'); const baseRegisterTrigger = useTriggerRegistration(thisTriggerId, store); const registerTrigger = React.useCallback(element => { const cleanup = baseRegisterTrigger(element); if (element !== null && store.select('open') && store.select('activeTriggerId') == null) { store.update({ activeTriggerId: thisTriggerId, activeTriggerElement: element, closeDelay }); } return cleanup; }, [baseRegisterTrigger, closeDelay, store, thisTriggerId]); const [triggerElement, setTriggerElement] = React.useState(null); const submenuRootContext = useMenuSubmenuRootContext(); if (!submenuRootContext?.parentMenu) { throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: <Menu.SubmenuTrigger> must be placed in <Menu.SubmenuRoot>.' : _formatErrorMessage(37)); } store.useSyncedValues({ closeDelay, activeTriggerElement: triggerElement }); const parentMenuStore = submenuRootContext.parentMenu; const itemProps = parentMenuStore.useState('itemProps'); const highlighted = parentMenuStore.useState('isActive', listItem.index); const itemMetadata = React.useMemo(() => ({ type: 'submenu-trigger', setActive: () => parentMenuStore.set('activeIndex', listItem.index) }), [parentMenuStore, listItem.index]); const rootDisabled = store.useState('disabled'); const disabled = disabledProp || rootDisabled; const { getItemProps, itemRef } = useMenuItem({ closeOnClick: false, disabled, highlighted, id: thisTriggerId, store, nativeButton, itemMetadata, nodeId: menuPositionerContext?.nodeId }); const hoverEnabled = store.useState('hoverEnabled'); const allowMouseEnter = store.useState('allowMouseEnter'); const hoverProps = useHoverReferenceInteraction(floatingRootContext, { enabled: hoverEnabled && openOnHover && !disabled, handleClose: safePolygon({ blockPointerEvents: true }), mouseOnly: true, move: true, restMs: allowMouseEnter ? delay : undefined, delay: { open: allowMouseEnter ? delay : 10 ** 10, close: closeDelay }, triggerElement, externalTree: floatingTreeRoot }); const click = useClick(floatingRootContext, { enabled: !disabled, event: 'mousedown', toggle: !openOnHover, ignoreMouse: openOnHover, stickIfOpen: false }); const localInteractionProps = useInteractions([click]); const rootTriggerProps = store.useState('triggerProps', true); delete rootTriggerProps.id; const state = React.useMemo(() => ({ disabled, highlighted, open }), [disabled, highlighted, open]); const element = useRenderElement('div', componentProps, { state, stateAttributesMapping: triggerOpenStateMapping, props: [localInteractionProps.getReferenceProps(), hoverProps, rootTriggerProps, itemProps, { tabIndex: open || highlighted ? 0 : -1, onBlur() { if (highlighted) { parentMenuStore.set('activeIndex', null); } } }, elementProps, getItemProps], ref: [forwardedRef, listItem.ref, itemRef, registerTrigger, setTriggerElement] }); return element; }); if (process.env.NODE_ENV !== "production") MenuSubmenuTrigger.displayName = "MenuSubmenuTrigger";