@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.
124 lines (122 loc) • 4.07 kB
JavaScript
'use client';
import * as React from 'react';
import { AnimationFrame } from '@base-ui-components/utils/useAnimationFrame';
import { FloatingNode, FloatingTree, useFloatingNodeId, useFloatingTree } from "../floating-ui-react/index.js";
import { MenubarContext, useMenubarContext } from "./MenubarContext.js";
import { useScrollLock } from "../utils/useScrollLock.js";
import { useOpenInteractionType } from "../utils/useOpenInteractionType.js";
import { CompositeRoot } from "../composite/root/CompositeRoot.js";
import { useBaseUiId } from "../utils/useBaseUiId.js";
/**
* The container for menus.
*
* Documentation: [Base UI Menubar](https://base-ui.com/react/components/menubar)
*/
import { jsx as _jsx } from "react/jsx-runtime";
export const Menubar = /*#__PURE__*/React.forwardRef(function Menubar(props, forwardedRef) {
const {
orientation = 'horizontal',
loop = true,
render,
className,
modal = true,
id: idProp,
...elementProps
} = props;
const [contentElement, setContentElement] = React.useState(null);
const [hasSubmenuOpen, setHasSubmenuOpen] = React.useState(false);
const {
openMethod,
triggerProps: interactionTypeProps,
reset: resetOpenInteractionType
} = useOpenInteractionType(hasSubmenuOpen);
React.useEffect(() => {
if (!hasSubmenuOpen) {
resetOpenInteractionType();
}
}, [hasSubmenuOpen, resetOpenInteractionType]);
useScrollLock({
enabled: modal && hasSubmenuOpen && openMethod !== 'touch',
open: hasSubmenuOpen,
mounted: hasSubmenuOpen,
referenceElement: contentElement
});
const id = useBaseUiId(idProp);
const state = React.useMemo(() => ({
orientation,
modal
}), [orientation, modal]);
const contentRef = React.useRef(null);
const allowMouseUpTriggerRef = React.useRef(false);
const context = React.useMemo(() => ({
contentElement,
setContentElement,
setHasSubmenuOpen,
hasSubmenuOpen,
modal,
orientation,
allowMouseUpTriggerRef,
rootId: id
}), [contentElement, hasSubmenuOpen, modal, orientation, id]);
return /*#__PURE__*/_jsx(MenubarContext.Provider, {
value: context,
children: /*#__PURE__*/_jsx(FloatingTree, {
children: /*#__PURE__*/_jsx(MenubarContent, {
children: /*#__PURE__*/_jsx(CompositeRoot, {
render: render,
className: className,
state: state,
refs: [forwardedRef, setContentElement, contentRef],
props: [{
role: 'menubar',
id
}, interactionTypeProps, elementProps],
orientation: orientation,
loop: loop,
highlightItemOnHover: hasSubmenuOpen
})
})
})
});
});
if (process.env.NODE_ENV !== "production") Menubar.displayName = "Menubar";
function MenubarContent(props) {
const nodeId = useFloatingNodeId();
const {
events: menuEvents
} = useFloatingTree();
const openSubmenusRef = React.useRef(new Set());
const rootContext = useMenubarContext();
React.useEffect(() => {
function onSubmenuOpenChange(event) {
if (event.parentNodeId !== nodeId) {
return;
}
if (event.open) {
openSubmenusRef.current.add(event.nodeId);
} else {
openSubmenusRef.current.delete(event.nodeId);
}
const isAnyOpen = openSubmenusRef.current.size > 0;
if (isAnyOpen) {
rootContext.setHasSubmenuOpen(true);
} else if (rootContext.hasSubmenuOpen) {
// wait for the next frame to set the state to make sure another menu doesn't open
// immediately after the previous one is closed
AnimationFrame.request(() => {
if (openSubmenusRef.current.size === 0) {
rootContext.setHasSubmenuOpen(false);
}
});
}
}
menuEvents.on('openchange', onSubmenuOpenChange);
return () => {
menuEvents.off('openchange', onSubmenuOpenChange);
};
}, [menuEvents, nodeId, rootContext]);
return /*#__PURE__*/_jsx(FloatingNode, {
id: nodeId,
children: props.children
});
}