@wordpress/components
Version:
UI components for WordPress.
182 lines (178 loc) • 6.32 kB
JavaScript
/**
* 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