From f448b2092e52bba457d30ca328eab00a2843d753 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 29 Nov 2024 07:49:28 +0100 Subject: [PATCH 01/22] MenuItem: add render and store props --- packages/components/src/menu/item.tsx | 6 ++++-- packages/components/src/menu/types.ts | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/components/src/menu/item.tsx b/packages/components/src/menu/item.tsx index 6d09bdf3d0f591..90c193049a01bb 100644 --- a/packages/components/src/menu/item.tsx +++ b/packages/components/src/menu/item.tsx @@ -15,7 +15,7 @@ export const MenuItem = forwardRef< HTMLDivElement, WordPressComponentProps< MenuItemProps, 'div', false > >( function MenuItem( - { prefix, suffix, children, hideOnClick = true, ...props }, + { prefix, suffix, children, hideOnClick = true, store, ...props }, ref ) { const menuContext = useContext( MenuContext ); @@ -26,13 +26,15 @@ export const MenuItem = forwardRef< ); } + const computedStore = store ?? menuContext.store; + return ( { prefix } diff --git a/packages/components/src/menu/types.ts b/packages/components/src/menu/types.ts index 7b58cef241743e..80e71cd2872f38 100644 --- a/packages/components/src/menu/types.ts +++ b/packages/components/src/menu/types.ts @@ -118,6 +118,11 @@ export interface MenuItemProps { * Determines if the element is disabled. */ disabled?: boolean; + render?: Ariakit.MenuItemProps[ 'render' ]; + /** + * @ignore + */ + store?: Ariakit.MenuItemProps[ 'store' ]; } export interface MenuCheckboxItemProps From ed4f816ca46a6ac9eaf7a6ec98e3232c3d2aa885 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 29 Nov 2024 08:26:32 +0100 Subject: [PATCH 02/22] Extract sub-components: popover, trigger button, submenu trigger item --- packages/components/src/menu/index.tsx | 225 +++++------------- packages/components/src/menu/popover.tsx | 103 ++++++++ .../src/menu/submenu-trigger-item.tsx | 58 +++++ .../components/src/menu/trigger-button.tsx | 46 ++++ packages/components/src/menu/types.ts | 76 +++++- 5 files changed, 334 insertions(+), 174 deletions(-) create mode 100644 packages/components/src/menu/popover.tsx create mode 100644 packages/components/src/menu/submenu-trigger-item.tsx create mode 100644 packages/components/src/menu/trigger-button.tsx diff --git a/packages/components/src/menu/index.tsx b/packages/components/src/menu/index.tsx index 9886f324823212..2e0fc91cfbc34f 100644 --- a/packages/components/src/menu/index.tsx +++ b/packages/components/src/menu/index.tsx @@ -6,23 +6,14 @@ import * as Ariakit from '@ariakit/react'; /** * WordPress dependencies */ -import { - useContext, - useMemo, - cloneElement, - isValidElement, - useCallback, -} from '@wordpress/element'; -import { isRTL } from '@wordpress/i18n'; -import { chevronRightSmall } from '@wordpress/icons'; +import { useContext, useMemo } from '@wordpress/element'; +import { isRTL as isRTLFn } from '@wordpress/i18n'; /** * Internal dependencies */ -import { useContextSystem, contextConnect } from '../context'; -import type { WordPressComponentProps } from '../context'; +import { useContextSystem, contextConnectWithoutRef } from '../context'; import type { MenuContext as MenuContextType, MenuProps } from './types'; -import * as Styled from './styles'; import { MenuContext } from './context'; import { MenuItem } from './item'; import { MenuCheckboxItem } from './checkbox-item'; @@ -32,49 +23,36 @@ import { MenuGroupLabel } from './group-label'; import { MenuSeparator } from './separator'; import { MenuItemLabel } from './item-label'; import { MenuItemHelpText } from './item-help-text'; +import { MenuTriggerButton } from './trigger-button'; +import { MenuSubmenuTriggerItem } from './submenu-trigger-item'; +import { MenuPopover } from './popover'; -const UnconnectedMenu = ( - props: WordPressComponentProps< MenuProps, 'div', false >, - ref: React.ForwardedRef< HTMLDivElement > -) => { +const UnconnectedMenu = ( props: MenuProps ) => { const { - // Store props - open, + children, defaultOpen = false, + open, onOpenChange, placement, - // Menu trigger props - trigger, - - // Menu props - gutter, - children, - shift, - modal = true, - // From internal components context variant, - - // Rest - ...otherProps - } = useContextSystem< typeof props & Pick< MenuContextType, 'variant' > >( - props, - 'Menu' - ); + } = useContextSystem< + // @ts-expect-error TODO: missing 'className' in MenuProps + typeof props & Pick< MenuContextType, 'variant' > + >( props, 'Menu' ); const parentContext = useContext( MenuContext ); - const computedDirection = isRTL() ? 'rtl' : 'ltr'; + const rtl = isRTLFn(); // 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 ?? - ( parentContext?.store ? 'right-start' : 'bottom-start' ); + placement ?? ( parentContext?.store ? 'right-start' : 'bottom-start' ); // Swap left/right in case of RTL direction - if ( computedDirection === 'rtl' ) { + if ( rtl ) { if ( /right/.test( computedPlacement ) ) { computedPlacement = computedPlacement.replace( 'right', @@ -97,7 +75,7 @@ const UnconnectedMenu = ( setOpen( willBeOpen ) { onOpenChange?.( willBeOpen ); }, - rtl: computedDirection === 'rtl', + rtl, } ); const contextValue = useMemo( @@ -105,134 +83,53 @@ const UnconnectedMenu = ( [ 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 = Ariakit.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: React.KeyboardEvent< Element > ) => { - // 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 as React.CSSProperties[ 'direction' ], - }, - } ), - [ computedDirection ] - ); - return ( - <> - { /* Menu trigger */ } - - { trigger.props.suffix } -