UNPKG

@wordpress/components

Version:
182 lines (178 loc) 6.32 kB
/** * External dependencies */ import * as Ariakit from '@ariakit/react'; import { useStoreState } from '@ariakit/react'; /** * WordPress dependencies */ import { useContext, useMemo, cloneElement, isValidElement, useCallback } from '@wordpress/element'; import { isRTL } from '@wordpress/i18n'; import { chevronRightSmall } from '@wordpress/icons'; /** * Internal dependencies */ import { useContextSystem, contextConnect } from '../context'; import * as Styled from './styles'; import { MenuContext } from './context'; import { MenuItem } from './item'; import { MenuCheckboxItem } from './checkbox-item'; import { MenuRadioItem } from './radio-item'; import { MenuGroup } from './group'; import { MenuGroupLabel } from './group-label'; import { MenuSeparator } from './separator'; import { MenuItemLabel } from './item-label'; import { MenuItemHelpText } from './item-help-text'; import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; const UnconnectedMenu = (props, ref) => { var _props$placement; const { // Store props open, defaultOpen = false, onOpenChange, placement, // Menu trigger props trigger, // Menu props gutter, children, shift, modal = true, // From internal components context variant, // Rest ...otherProps } = useContextSystem(props, 'Menu'); const parentContext = useContext(MenuContext); const computedDirection = isRTL() ? 'rtl' : 'ltr'; // If an explicit value for the `placement` prop is not passed, // apply a default placement of `bottom-start` for the root menu popover, // and of `right-start` for nested menu popovers. let computedPlacement = (_props$placement = props.placement) !== null && _props$placement !== void 0 ? _props$placement : parentContext?.store ? 'right-start' : 'bottom-start'; // Swap left/right in case of RTL direction if (computedDirection === 'rtl') { if (/right/.test(computedPlacement)) { computedPlacement = computedPlacement.replace('right', 'left'); } else if (/left/.test(computedPlacement)) { computedPlacement = computedPlacement.replace('left', 'right'); } } const menuStore = Ariakit.useMenuStore({ parent: parentContext?.store, open, defaultOpen, placement: computedPlacement, focusLoop: true, setOpen(willBeOpen) { onOpenChange?.(willBeOpen); }, rtl: computedDirection === 'rtl' }); const contextValue = useMemo(() => ({ store: menuStore, variant }), [menuStore, variant]); // Extract the side from the applied placement — useful for animations. // Using `currentPlacement` instead of `placement` to make sure that we // use the final computed placement (including "flips" etc). const appliedPlacementSide = useStoreState(menuStore, 'currentPlacement').split('-')[0]; if (menuStore.parent && !(isValidElement(trigger) && MenuItem === trigger.type)) { // eslint-disable-next-line no-console console.warn('For nested Menus, the `trigger` should always be a `MenuItem`.'); } const hideOnEscape = useCallback(event => { // Pressing Escape can cause unexpected consequences (ie. exiting // full screen mode on MacOs, close parent modals...). event.preventDefault(); // Returning `true` causes the menu to hide. return true; }, []); const wrapperProps = useMemo(() => ({ dir: computedDirection, style: { direction: computedDirection } }), [computedDirection]); return /*#__PURE__*/_jsxs(_Fragment, { children: [/*#__PURE__*/_jsx(Ariakit.MenuButton, { ref: ref, store: menuStore, render: menuStore.parent ? cloneElement(trigger, { // Add submenu arrow, unless a `suffix` is explicitly specified suffix: /*#__PURE__*/_jsxs(_Fragment, { children: [trigger.props.suffix, /*#__PURE__*/_jsx(Styled.SubmenuChevronIcon, { "aria-hidden": "true", icon: chevronRightSmall, size: 24, preserveAspectRatio: "xMidYMid slice" })] }) }) : trigger }), /*#__PURE__*/_jsx(Ariakit.Menu, { ...otherProps, modal: modal, store: menuStore // Root menu has an 8px distance from its trigger, // otherwise 0 (which causes the submenu to slightly overlap) , gutter: gutter !== null && gutter !== void 0 ? gutter : menuStore.parent ? 0 : 8 // Align nested menu by the same (but opposite) amount // as the menu container's padding. , shift: shift !== null && shift !== void 0 ? shift : menuStore.parent ? -4 : 0, hideOnHoverOutside: false, "data-side": appliedPlacementSide, wrapperProps: wrapperProps, hideOnEscape: hideOnEscape, unmountOnHide: true, render: renderProps => /*#__PURE__*/ // Two wrappers are needed for the entry animation, where the menu // container scales with a different factor than its contents. // The {...renderProps} are passed to the inner wrapper, so that the // menu element is the direct parent of the menu item elements. _jsx(Styled.MenuPopoverOuterWrapper, { variant: variant, children: /*#__PURE__*/_jsx(Styled.MenuPopoverInnerWrapper, { ...renderProps }) }), children: /*#__PURE__*/_jsx(MenuContext.Provider, { value: contextValue, children: children }) })] }); }; export const Menu = Object.assign(contextConnect(UnconnectedMenu, 'Menu'), { Context: Object.assign(MenuContext, { displayName: 'Menu.Context' }), Item: Object.assign(MenuItem, { displayName: 'Menu.Item' }), RadioItem: Object.assign(MenuRadioItem, { displayName: 'Menu.RadioItem' }), CheckboxItem: Object.assign(MenuCheckboxItem, { displayName: 'Menu.CheckboxItem' }), Group: Object.assign(MenuGroup, { displayName: 'Menu.Group' }), GroupLabel: Object.assign(MenuGroupLabel, { displayName: 'Menu.GroupLabel' }), Separator: Object.assign(MenuSeparator, { displayName: 'Menu.Separator' }), ItemLabel: Object.assign(MenuItemLabel, { displayName: 'Menu.ItemLabel' }), ItemHelpText: Object.assign(MenuItemHelpText, { displayName: 'Menu.ItemHelpText' }) }); export default Menu; //# sourceMappingURL=index.js.map