From 660171a3f2d1211f0a86ecd62f55b597d33467f7 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 10 Nov 2023 16:12:19 +0100 Subject: [PATCH 01/23] Expose help text subcomponent (uses truncate under the hood) --- .../src/dropdown-menu-v2-ariakit/index.tsx | 30 +++++- .../stories/index.story.tsx | 97 ++++++++++++------- .../src/dropdown-menu-v2-ariakit/styles.ts | 23 +++++ 3 files changed, 112 insertions(+), 38 deletions(-) diff --git a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx index 10b93d8c552c13..9580b7957134ef 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx @@ -61,7 +61,11 @@ export const DropdownMenuItem = forwardRef< { prefix && ( { prefix } ) } - { children } + + + { children } + + { suffix && ( { suffix } ) } @@ -92,7 +96,10 @@ export const DropdownMenuCheckboxItem = forwardRef< - { children } + + { children } + + { suffix && ( { suffix } ) } @@ -128,7 +135,11 @@ export const DropdownMenuRadioItem = forwardRef< > - { children } + + + { children } + + { suffix } ); @@ -332,3 +343,16 @@ export const DropdownMenuSeparator = forwardRef< /> ); } ); + +export const DropdownMenuItemHelpText = forwardRef< + HTMLHRElement, + WordPressComponentProps< { children: React.ReactNode }, 'span', true > +>( function DropdownMenuItemHelpText( props, ref ) { + return ( + + ); +} ); diff --git a/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx b/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx index a6319c6cfdc932..8c9eb81e07f86a 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx @@ -24,6 +24,7 @@ import { DropdownMenuSeparator, DropdownMenuContext, DropdownMenuRadioItem, + DropdownMenuItemHelpText, } from '..'; import Icon from '../../icon'; import Button from '../../button'; @@ -65,35 +66,25 @@ const meta: Meta< typeof DropdownMenu > = { }; export default meta; -const ItemHelpText = styled.span` - font-size: 12px; - color: ${ COLORS.gray[ '700' ] }; - - /* when the immediate parent item is hovered / focused */ - [data-active-item] > * > &, - /* when the parent item is a submenu trigger and the submenu is open */ - [aria-expanded='true'] > &, - /* when the parent item is disabled */ - [aria-disabled='true'] > & { - color: inherit; - } -`; - export const Default: StoryFn< typeof DropdownMenu > = ( props ) => ( - Default item + Label + + Label + Help text + + + Label + + Help text is automatically truncated when there are more than + two lines of text. + + -
- Other item - - Won't close the menu when clicked - -
+ Label + + This item doesn't close the menu on click +
Disabled item @@ -174,14 +165,20 @@ export const WithCheckboxes: StoryFn< typeof DropdownMenu > = ( props ) => { name="checkbox-individual-uncontrolled-a" value="a" > - Checkbox item A (initially unchecked) + Checkbox item A + + Uncontrolled + - Checkbox item B (initially checked) + Checkbox item B + + Uncontrolled, initially checked + @@ -196,6 +193,9 @@ export const WithCheckboxes: StoryFn< typeof DropdownMenu > = ( props ) => { onChange={ ( e ) => setAChecked( e.target.checked ) } > Checkbox item A + + Controlled + = ( props ) => { checked={ isBChecked } onChange={ ( e ) => setBChecked( e.target.checked ) } > - Checkbox item B (initially checked) + Checkbox item B + + Controlled, initially checked + @@ -215,14 +218,20 @@ export const WithCheckboxes: StoryFn< typeof DropdownMenu > = ( props ) => { name="checkbox-multiple-uncontrolled" value="a" > - Checkbox item A (initially unchecked) + Checkbox item A + + Uncontrolled, multiple selection + - Checkbox item B (initially checked) + Checkbox item B + + Uncontrolled, multiple selection, initially checked + @@ -236,7 +245,10 @@ export const WithCheckboxes: StoryFn< typeof DropdownMenu > = ( props ) => { checked={ multipleCheckboxesValue.includes( 'a' ) } onChange={ onMultipleCheckboxesCheckedChange } > - Checkbox item A (initially unchecked) + Checkbox item A + + Controlled, multiple selection + = ( props ) => { checked={ multipleCheckboxesValue.includes( 'b' ) } onChange={ onMultipleCheckboxesCheckedChange } > - Checkbox item B (initially checked) + Checkbox item B + + Controlled, multiple selection, initially checked +
@@ -268,13 +283,19 @@ export const WithRadios: StoryFn< typeof DropdownMenu > = ( props ) => { Radio item 1 + + Uncontrolled + - Radio item 2 (initially checked) + Radio item 2 + + Uncontrolled, initially checked + @@ -289,6 +310,9 @@ export const WithRadios: StoryFn< typeof DropdownMenu > = ( props ) => { onChange={ onRadioChange } > Radio item 1 + + Controlled + = ( props ) => { checked={ radioValue === 'two' } onChange={ onRadioChange } > - Radio item 2 (initially checked) + Radio item 2 + + Controlled, initially checked + diff --git a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts index 0858bba4289efd..929f4620097315 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts +++ b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts @@ -12,6 +12,7 @@ import styled from '@emotion/styled'; import { COLORS, font, rtl, CONFIG } from '../utils'; import { space } from '../utils/space'; import Icon from '../icon'; +import { Truncate } from '../truncate'; import type { DropdownMenuContext } from './types'; const ANIMATION_PARAMS = { @@ -248,6 +249,12 @@ export const DropdownMenuRadioItem = styled( Ariakit.MenuItemRadio )` ${ baseItem } `; +export const DropdownMenuItemContentWrapper = styled.div` + display: inline-flex; + flex-direction: column; + pointer-events: none; +`; + export const DropdownMenuGroup = styled( Ariakit.MenuGroup )``; export const DropdownMenuGroupLabel = styled( Ariakit.MenuGroupLabel )` @@ -295,3 +302,19 @@ export const SubmenuChevronIcon = styled( Icon )` } ) } `; + +export const DropdownMenuItemHelpText = styled( Truncate )` + font-size: 12px; + color: ${ COLORS.gray[ '700' ] }; +`; + +// /* when the immediate parent item is hovered / focused */ +// [data-active-item] > ${ DropdownMenuItem }[data-active-item] > &, +// [data-active-item] > ${ DropdownMenuRadioItem }[data-active-item] > &, +// [data-active-item] > ${ DropdownMenuCheckboxItem }[data-active-item] > &, +// /* when the parent item is a submenu trigger and the submenu is open */ +// [aria-expanded='true'] > &, +// /* when the parent item is disabled */ +// [aria-disabled='true'] > & { +// color: inherit; +// } From 7811b8dd44193ad227bcbb2e4c743ac94c7a04fd Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 10 Nov 2023 16:25:02 +0100 Subject: [PATCH 02/23] Remove DropdownMenuGroupLabel --- .../src/dropdown-menu-v2-ariakit/README.md | 14 ----------- .../src/dropdown-menu-v2-ariakit/index.tsx | 15 ------------ .../stories/index.story.tsx | 23 +------------------ .../src/dropdown-menu-v2-ariakit/styles.ts | 18 --------------- .../dropdown-menu-v2-ariakit/test/index.tsx | 7 ------ .../src/dropdown-menu-v2-ariakit/types.ts | 7 ------ packages/components/src/private-apis.ts | 2 -- 7 files changed, 1 insertion(+), 85 deletions(-) diff --git a/packages/components/src/dropdown-menu-v2-ariakit/README.md b/packages/components/src/dropdown-menu-v2-ariakit/README.md index f74098efff4103..19a28870e1ef1e 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/README.md +++ b/packages/components/src/dropdown-menu-v2-ariakit/README.md @@ -298,20 +298,6 @@ The contents of the group. - Required: yes -### `DropdownMenuGroupLabel` - -Used to render a group label. - -#### Props - -The component accepts the following props: - -##### `children`: `React.ReactNode` - -The contents of the group. - -- Required: yes - ### `DropdownMenuSeparatorProps` Used to render a visual separator. diff --git a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx index 9580b7957134ef..52bc9251dbd96c 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx @@ -30,7 +30,6 @@ import type { DropdownMenuContext as DropdownMenuContextType, DropdownMenuProps, DropdownMenuGroupProps, - DropdownMenuGroupLabelProps, DropdownMenuItemProps, DropdownMenuCheckboxItemProps, DropdownMenuRadioItemProps, @@ -159,20 +158,6 @@ export const DropdownMenuGroup = forwardRef< ); } ); -export const DropdownMenuGroupLabel = forwardRef< - HTMLDivElement, - WordPressComponentProps< DropdownMenuGroupLabelProps, 'div', false > ->( function DropdownMenuGroupLabel( props, ref ) { - const dropdownMenuContext = useContext( DropdownMenuContext ); - return ( - - ); -} ); - const UnconnectedDropdownMenu = ( props: WordPressComponentProps< DropdownMenuProps, 'div', false >, ref: React.ForwardedRef< HTMLDivElement > diff --git a/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx b/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx index 8c9eb81e07f86a..2eba5a6bf6d0e3 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx @@ -2,7 +2,6 @@ * External dependencies */ import type { Meta, StoryFn } from '@storybook/react'; -import styled from '@emotion/styled'; import { css } from '@emotion/react'; /** @@ -14,13 +13,12 @@ import { useState, useMemo, useContext } from '@wordpress/element'; /** * Internal dependencies */ -import { COLORS, useCx } from '../../utils'; +import { useCx } from '../../utils'; import { DropdownMenu, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuGroup, - DropdownMenuGroupLabel, DropdownMenuSeparator, DropdownMenuContext, DropdownMenuRadioItem, @@ -89,7 +87,6 @@ export const Default: StoryFn< typeof DropdownMenu > = ( props ) => ( Disabled item - Prefix and suffix } > @@ -158,9 +155,6 @@ export const WithCheckboxes: StoryFn< typeof DropdownMenu > = ( props ) => { return ( - - Individual, uncontrolled checkboxes - = ( props ) => { - - Individual, controlled checkboxes - = ( props ) => { - - Multiple, uncontrolled checkboxes - = ( props ) => { - - Multiple, controlled checkboxes - = ( props ) => { return ( - - Uncontrolled radios - Radio item 1 @@ -300,9 +282,6 @@ export const WithRadios: StoryFn< typeof DropdownMenu > = ( props ) => { - - Controlled radios - >` diff --git a/packages/components/src/dropdown-menu-v2-ariakit/test/index.tsx b/packages/components/src/dropdown-menu-v2-ariakit/test/index.tsx index 297bc0dec9390f..4c7fd12fcc98ce 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/test/index.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/test/index.tsx @@ -16,7 +16,6 @@ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuItem, - DropdownMenuGroupLabel, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuGroup, @@ -452,9 +451,6 @@ describe( 'DropdownMenu', () => { return ( Open dropdown }> - - Radio group label - { render( Open dropdown }> - - Radio group label - Date: Sat, 11 Nov 2023 22:49:52 +0100 Subject: [PATCH 03/23] Submenu trigger always display chevron next to suffix --- .../src/dropdown-menu-v2-ariakit/index.tsx | 15 +++++++++------ .../stories/index.story.tsx | 6 +++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx index 52bc9251dbd96c..3597326b4e9d6b 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx @@ -276,12 +276,15 @@ const UnconnectedDropdownMenu = ( dropdownMenuStore.parent ? cloneElement( trigger, { // Add submenu arrow, unless a `suffix` is explicitly specified - suffix: trigger.props.suffix ?? ( - } + trigger={ + + Submenu trigger + + } > Level 2 item Level 2 item From fb2a46dbe8d7114effe8939c202e4d568b0f5f32 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Sat, 11 Nov 2023 22:53:37 +0100 Subject: [PATCH 04/23] Tweak gutter so that submenus overlap by 8px --- packages/components/src/dropdown-menu-v2-ariakit/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx index 3597326b4e9d6b..e3941b0241d3b6 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx @@ -296,7 +296,9 @@ const UnconnectedDropdownMenu = ( { ...otherProps } modal={ modal } store={ dropdownMenuStore } - gutter={ gutter ?? ( dropdownMenuStore.parent ? 16 : 8 ) } + // Nested menus overlap by 8px + gutter={ gutter ?? ( dropdownMenuStore.parent ? 0 : 8 ) } + // Nested menus have their items aligned horizontally shift={ shift ?? ( dropdownMenuStore.parent ? -8 : 0 ) } hideOnHoverOutside={ false } data-side={ appliedPlacementSide } From c92dbc2e4c9b788982c602458061ffd23f3d0787 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Sun, 12 Nov 2023 08:36:33 +0100 Subject: [PATCH 05/23] Implement auto-indentation via context --- .../src/dropdown-menu-v2-ariakit/index.tsx | 79 ++++++++++++++++++- .../stories/index.story.tsx | 6 +- .../src/dropdown-menu-v2-ariakit/styles.ts | 14 ++-- .../src/dropdown-menu-v2-ariakit/types.ts | 14 ++++ 4 files changed, 105 insertions(+), 8 deletions(-) diff --git a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx index e3941b0241d3b6..da6242fcf70c83 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx @@ -15,6 +15,10 @@ import { cloneElement, isValidElement, useCallback, + useState, + useEffect, + useRef, + useId, } from '@wordpress/element'; import { isRTL } from '@wordpress/i18n'; import { check, chevronRightSmall } from '@wordpress/icons'; @@ -48,14 +52,26 @@ export const DropdownMenuItem = forwardRef< { prefix, suffix, children, hideOnClick = true, ...props }, ref ) { + const id = useId(); const dropdownMenuContext = useContext( DropdownMenuContext ); + const { registerHasPrefix, unregisterHasPrefix } = + dropdownMenuContext ?? {}; + const hasPrefix = !! prefix; + useEffect( () => { + registerHasPrefix?.( id, hasPrefix ); + return () => unregisterHasPrefix?.( id ); + }, [ id, registerHasPrefix, unregisterHasPrefix, hasPrefix ] ); + return ( { prefix && ( { prefix } @@ -79,8 +95,17 @@ export const DropdownMenuCheckboxItem = forwardRef< { suffix, children, hideOnClick = false, ...props }, ref ) { + const id = useId(); const dropdownMenuContext = useContext( DropdownMenuContext ); + const { registerHasPrefix, unregisterHasPrefix } = + dropdownMenuContext ?? {}; + useEffect( () => { + // Check items always have a prefix (the check icon). + registerHasPrefix?.( id, true ); + return () => unregisterHasPrefix?.( id ); + }, [ id, registerHasPrefix, unregisterHasPrefix ] ); + return ( { + // Radio items always have a prefix (the check icon). + registerHasPrefix?.( id, true ); + return () => unregisterHasPrefix?.( id ); + }, [ id, registerHasPrefix, unregisterHasPrefix ] ); + return ( { + let atLeastOnePrefix = false; + for ( const value of hasPrefixMap.current.values() ) { + atLeastOnePrefix ||= value; + } + + setShouldIndent( atLeastOnePrefix ); + }, [] ); + + const hasPrefixMap = useRef( new Map< string, boolean >() ); + + const registerHasPrefix = useCallback( + ( id: string, hasPrefix: boolean ) => { + hasPrefixMap.current.set( id, hasPrefix ); + updateShouldIndent(); + }, + [ updateShouldIndent ] + ); + + const unregisterHasPrefix = useCallback( + ( id: string ) => { + hasPrefixMap.current.delete( id ); + updateShouldIndent(); + }, + [ updateShouldIndent ] + ); + const contextValue = useMemo( - () => ( { store: dropdownMenuStore, variant } ), - [ dropdownMenuStore, variant ] + () => ( { + store: dropdownMenuStore, + variant, + shouldIndent, + registerHasPrefix, + unregisterHasPrefix, + } ), + [ + dropdownMenuStore, + variant, + shouldIndent, + registerHasPrefix, + unregisterHasPrefix, + ] ); // Extract the side from the applied placement — useful for animations. diff --git a/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx b/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx index 216eace5396971..7f69a18fb9ef47 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx @@ -123,7 +123,11 @@ export const WithSubmenu: StoryFn< typeof DropdownMenu > = ( props ) => ( } > - Level 2 item + } + > + Level 2 item + Level 2 item Submenu trigger } diff --git a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts index c47e7747fcc93f..d072d7b07cdcb8 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts +++ b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts @@ -231,14 +231,18 @@ const baseItem = css` svg { fill: currentColor; } - - &:not( :has( ${ ItemPrefixWrapper } ) ) { - padding-inline-start: ${ ITEM_PREFIX_WIDTH }; - } `; -export const DropdownMenuItem = styled( Ariakit.MenuItem )` +export const DropdownMenuItem = styled( Ariakit.MenuItem )< { + shouldIndent?: boolean; +} >` ${ baseItem } + + ${ ( props ) => + props.shouldIndent && + ` + padding-inline-start: ${ ITEM_PREFIX_WIDTH }; + ` } `; export const DropdownMenuCheckboxItem = styled( Ariakit.MenuItemCheckbox )` diff --git a/packages/components/src/dropdown-menu-v2-ariakit/types.ts b/packages/components/src/dropdown-menu-v2-ariakit/types.ts index 478b89c67f136a..08f66fd52862e9 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/types.ts +++ b/packages/components/src/dropdown-menu-v2-ariakit/types.ts @@ -14,6 +14,20 @@ export interface DropdownMenuContext { * The variant used by the underlying menu popover. */ variant?: 'toolbar'; + /** + * Whether items in this menu should be indented, ie. they should show + * extra white space even when they don't have a prefix. + */ + shouldIndent: boolean; + /** + * Function used by menu items to let the parent dropdown menu know + * whether they are rendering a prefix or not. + */ + registerHasPrefix: ( itemId: string, hasPrefix: boolean ) => void; + /** + * Function used by menu items to unregister a previously set entry. + */ + unregisterHasPrefix: ( itemId: string ) => void; } export interface DropdownMenuProps { From 33db29be602b8e8f53a596cb61927947d7acf55f Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 14 Nov 2023 09:43:10 +0100 Subject: [PATCH 06/23] Revert "Implement auto-indentation via context" This reverts commit a1e2d3af0285f1f28d1677d41005bcdb25fd0659. --- .../src/dropdown-menu-v2-ariakit/index.tsx | 79 +------------------ .../stories/index.story.tsx | 6 +- .../src/dropdown-menu-v2-ariakit/styles.ts | 14 ++-- .../src/dropdown-menu-v2-ariakit/types.ts | 14 ---- 4 files changed, 8 insertions(+), 105 deletions(-) diff --git a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx index da6242fcf70c83..e3941b0241d3b6 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx @@ -15,10 +15,6 @@ import { cloneElement, isValidElement, useCallback, - useState, - useEffect, - useRef, - useId, } from '@wordpress/element'; import { isRTL } from '@wordpress/i18n'; import { check, chevronRightSmall } from '@wordpress/icons'; @@ -52,26 +48,14 @@ export const DropdownMenuItem = forwardRef< { prefix, suffix, children, hideOnClick = true, ...props }, ref ) { - const id = useId(); const dropdownMenuContext = useContext( DropdownMenuContext ); - const { registerHasPrefix, unregisterHasPrefix } = - dropdownMenuContext ?? {}; - const hasPrefix = !! prefix; - useEffect( () => { - registerHasPrefix?.( id, hasPrefix ); - return () => unregisterHasPrefix?.( id ); - }, [ id, registerHasPrefix, unregisterHasPrefix, hasPrefix ] ); - return ( { prefix && ( { prefix } @@ -95,17 +79,8 @@ export const DropdownMenuCheckboxItem = forwardRef< { suffix, children, hideOnClick = false, ...props }, ref ) { - const id = useId(); const dropdownMenuContext = useContext( DropdownMenuContext ); - const { registerHasPrefix, unregisterHasPrefix } = - dropdownMenuContext ?? {}; - useEffect( () => { - // Check items always have a prefix (the check icon). - registerHasPrefix?.( id, true ); - return () => unregisterHasPrefix?.( id ); - }, [ id, registerHasPrefix, unregisterHasPrefix ] ); - return ( { - // Radio items always have a prefix (the check icon). - registerHasPrefix?.( id, true ); - return () => unregisterHasPrefix?.( id ); - }, [ id, registerHasPrefix, unregisterHasPrefix ] ); - return ( { - let atLeastOnePrefix = false; - for ( const value of hasPrefixMap.current.values() ) { - atLeastOnePrefix ||= value; - } - - setShouldIndent( atLeastOnePrefix ); - }, [] ); - - const hasPrefixMap = useRef( new Map< string, boolean >() ); - - const registerHasPrefix = useCallback( - ( id: string, hasPrefix: boolean ) => { - hasPrefixMap.current.set( id, hasPrefix ); - updateShouldIndent(); - }, - [ updateShouldIndent ] - ); - - const unregisterHasPrefix = useCallback( - ( id: string ) => { - hasPrefixMap.current.delete( id ); - updateShouldIndent(); - }, - [ updateShouldIndent ] - ); - const contextValue = useMemo( - () => ( { - store: dropdownMenuStore, - variant, - shouldIndent, - registerHasPrefix, - unregisterHasPrefix, - } ), - [ - dropdownMenuStore, - variant, - shouldIndent, - registerHasPrefix, - unregisterHasPrefix, - ] + () => ( { store: dropdownMenuStore, variant } ), + [ dropdownMenuStore, variant ] ); // Extract the side from the applied placement — useful for animations. diff --git a/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx b/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx index 7f69a18fb9ef47..216eace5396971 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx @@ -123,11 +123,7 @@ export const WithSubmenu: StoryFn< typeof DropdownMenu > = ( props ) => ( } > - } - > - Level 2 item - + Level 2 item Level 2 item Submenu trigger } diff --git a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts index d072d7b07cdcb8..c47e7747fcc93f 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts +++ b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts @@ -231,18 +231,14 @@ const baseItem = css` svg { fill: currentColor; } + + &:not( :has( ${ ItemPrefixWrapper } ) ) { + padding-inline-start: ${ ITEM_PREFIX_WIDTH }; + } `; -export const DropdownMenuItem = styled( Ariakit.MenuItem )< { - shouldIndent?: boolean; -} >` +export const DropdownMenuItem = styled( Ariakit.MenuItem )` ${ baseItem } - - ${ ( props ) => - props.shouldIndent && - ` - padding-inline-start: ${ ITEM_PREFIX_WIDTH }; - ` } `; export const DropdownMenuCheckboxItem = styled( Ariakit.MenuItemCheckbox )` diff --git a/packages/components/src/dropdown-menu-v2-ariakit/types.ts b/packages/components/src/dropdown-menu-v2-ariakit/types.ts index 08f66fd52862e9..478b89c67f136a 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/types.ts +++ b/packages/components/src/dropdown-menu-v2-ariakit/types.ts @@ -14,20 +14,6 @@ export interface DropdownMenuContext { * The variant used by the underlying menu popover. */ variant?: 'toolbar'; - /** - * Whether items in this menu should be indented, ie. they should show - * extra white space even when they don't have a prefix. - */ - shouldIndent: boolean; - /** - * Function used by menu items to let the parent dropdown menu know - * whether they are rendering a prefix or not. - */ - registerHasPrefix: ( itemId: string, hasPrefix: boolean ) => void; - /** - * Function used by menu items to unregister a previously set entry. - */ - unregisterHasPrefix: ( itemId: string ) => void; } export interface DropdownMenuProps { From dcb1bdfe093942b338def01bb1ea536cc5f6831b Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 14 Nov 2023 10:46:30 +0100 Subject: [PATCH 07/23] Use CSS grid/subgrid to impement auto-indentation --- .../src/dropdown-menu-v2-ariakit/index.tsx | 40 ++++++++++----- .../src/dropdown-menu-v2-ariakit/styles.ts | 51 ++++++++++++++++--- 2 files changed, 70 insertions(+), 21 deletions(-) diff --git a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx index e3941b0241d3b6..c1b6869e633347 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx @@ -62,12 +62,16 @@ export const DropdownMenuItem = forwardRef< ) } - { children } - + + { children } + - { suffix && ( - { suffix } - ) } + { suffix && ( + + { suffix } + + ) } + ); } ); @@ -96,12 +100,16 @@ export const DropdownMenuCheckboxItem = forwardRef< - { children } - + + { children } + - { suffix && ( - { suffix } - ) } + { suffix && ( + + { suffix } + + ) } + ); } ); @@ -136,10 +144,16 @@ export const DropdownMenuRadioItem = forwardRef< - { children } - + + { children } + - { suffix } + { suffix && ( + + { suffix } + + ) } + ); } ); diff --git a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts index c47e7747fcc93f..546b98703e9629 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts +++ b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts @@ -72,6 +72,10 @@ export const DropdownMenu = styled( Ariakit.Menu )< /* TODO: is there a way to read the sass variable? */ z-index: 1000000; + display: grid; + grid-template-columns: minmax( 0, max-content ) 1fr; + grid-template-rows: auto; + min-width: 220px; max-height: var( --popover-available-height ); padding: ${ CONTENT_WRAPPER_PADDING }; @@ -112,6 +116,9 @@ export const DropdownMenu = styled( Ariakit.Menu )< `; const itemPrefix = css` + /* Always occupy the first column, even when auto-collapsing /* + grid-column: 1; + /* !important is to override some inline styles set by Ariakit */ width: ${ ITEM_PREFIX_WIDTH } !important; /* !important is to override some inline styles set by Ariakit */ @@ -131,6 +138,7 @@ const itemPrefix = css` `; const itemSuffix = css` + flex: 0; width: max-content; display: inline-flex; align-items: center; @@ -179,17 +187,31 @@ export const ItemSuffixWrapper = styled.span` const baseItem = css` all: unset; + + position: relative; + + /* Occupy the width of all grid columns (ie. full width) */ + grid-column: 1 / -1; + + /* + * Define a grid layout which inherits the same columns configuration + * from the parent layout (ie. subgrid). + */ + display: grid; + grid-template-columns: subgrid; + align-items: center; + font-size: ${ font( 'default.fontSize' ) }; font-family: inherit; font-weight: normal; line-height: 20px; + color: ${ COLORS.gray[ 900 ] }; border-radius: ${ CONFIG.radiusBlockUi }; - display: flex; - align-items: center; + padding: ${ space( 2 ) } ${ ITEM_PADDING_INLINE_END } ${ space( 2 ) } ${ ITEM_PADDING_INLINE_START }; - position: relative; + user-select: none; outline: none; @@ -231,10 +253,6 @@ const baseItem = css` svg { fill: currentColor; } - - &:not( :has( ${ ItemPrefixWrapper } ) ) { - padding-inline-start: ${ ITEM_PREFIX_WIDTH }; - } `; export const DropdownMenuItem = styled( Ariakit.MenuItem )` @@ -250,16 +268,33 @@ export const DropdownMenuRadioItem = styled( Ariakit.MenuItemRadio )` `; export const DropdownMenuItemContentWrapper = styled.div` + /* + * Always occupy the second column, since the first column + * is taken by the prefix wrapper (when displayed). + */ + grid-column: 2; + + display: flex; +`; + +export const DropdownMenuItemChildrenWrapper = styled.div` + flex: 1; display: inline-flex; flex-direction: column; pointer-events: none; `; -export const DropdownMenuGroup = styled( Ariakit.MenuGroup )``; +export const DropdownMenuGroup = styled( Ariakit.MenuGroup )` + /* Ignore this element when calculating the layout. Useful for subgrid */ + display: contents; +`; export const DropdownMenuSeparator = styled( Ariakit.MenuSeparator )< Pick< DropdownMenuContext, 'variant' > >` + /* Occupy the width of all grid columns (ie. full width) */ + grid-column: 1 / -1; + border: none; height: ${ CONFIG.borderWidth }; /* TODO: doesn't match border color from variables */ From e37a4f94294941b003a3adcc84bd37fc900646ed Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 14 Nov 2023 10:46:45 +0100 Subject: [PATCH 08/23] Fix slotfill example --- .../src/dropdown-menu-v2-ariakit/stories/index.story.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx b/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx index 216eace5396971..1b7728b1e4ea99 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx @@ -393,7 +393,13 @@ const Slot = () => { [ dropdownMenuContext ] ); - return ; + return ( + + ); }; type ForwardedContextTuple< P = {} > = [ From 6c0377a05f134998b40bd116082ab34b18a2a926 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 14 Nov 2023 19:57:36 +0100 Subject: [PATCH 09/23] Tweak sizes, spacing, and colors. Tidy up styles. --- .../src/dropdown-menu-v2-ariakit/index.tsx | 5 +- .../src/dropdown-menu-v2-ariakit/styles.ts | 210 ++++++++---------- 2 files changed, 100 insertions(+), 115 deletions(-) diff --git a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx index c1b6869e633347..47192a6ee2bae8 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx @@ -57,9 +57,7 @@ export const DropdownMenuItem = forwardRef< hideOnClick={ hideOnClick } store={ dropdownMenuContext?.store } > - { prefix && ( - { prefix } - ) } + { prefix } @@ -297,6 +295,7 @@ const UnconnectedDropdownMenu = ( aria-hidden="true" icon={ chevronRightSmall } size={ 24 } + preserveAspectRatio="xMidYMid slice" /> ), diff --git a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts index 546b98703e9629..13884b1388f043 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts +++ b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts @@ -22,12 +22,15 @@ const ANIMATION_PARAMS = { }; const CONTENT_WRAPPER_PADDING = space( 2 ); -const ITEM_PREFIX_WIDTH = space( 7 ); -const ITEM_PADDING_INLINE_START = space( 2 ); -const ITEM_PADDING_INLINE_END = space( 2.5 ); - -// TODO: should bring this into the config, and make themeable -const DEFAULT_BORDER_COLOR = COLORS.ui.borderDisabled; +const ITEM_PADDING_BLOCK = space( 2 ); +const ITEM_PADDING_INLINE = space( 3 ); + +// TODO: +// - those values are different from saved variables? +// - should bring this into the config, and make themeable +// - border color and divider color are different? +const DEFAULT_BORDER_COLOR = COLORS.gray[ 300 ]; +const DIVIDER_COLOR = COLORS.gray[ 200 ]; const TOOLBAR_VARIANT_BORDER_COLOR = COLORS.gray[ '900' ]; const DEFAULT_BOX_SHADOW = `0 0 0 ${ CONFIG.borderWidth } ${ DEFAULT_BORDER_COLOR }, ${ CONFIG.popoverShadow }`; const TOOLBAR_VARIANT_BOX_SHADOW = `0 0 0 ${ CONFIG.borderWidth } ${ TOOLBAR_VARIANT_BORDER_COLOR }`; @@ -76,7 +79,9 @@ export const DropdownMenu = styled( Ariakit.Menu )< grid-template-columns: minmax( 0, max-content ) 1fr; grid-template-rows: auto; - min-width: 220px; + box-sizing: border-box; + min-width: 160px; + max-width: 320px; max-height: var( --popover-available-height ); padding: ${ CONTENT_WRAPPER_PADDING }; @@ -115,80 +120,12 @@ export const DropdownMenu = styled( Ariakit.Menu )< } `; -const itemPrefix = css` - /* Always occupy the first column, even when auto-collapsing /* - grid-column: 1; - - /* !important is to override some inline styles set by Ariakit */ - width: ${ ITEM_PREFIX_WIDTH } !important; - /* !important is to override some inline styles set by Ariakit */ - height: auto !important; - display: inline-flex; - align-items: center; - justify-content: center; - /* Prefixes don't get affected by the item's inline start padding */ - margin-inline-start: calc( -1 * ${ ITEM_PADDING_INLINE_START } ); - /* - Negative margin allows the suffix to be as tall as the whole item - (incl. padding) before increasing the items' height. This can be useful, - e.g., when using icons that are bigger than 20x20 px - */ - margin-top: ${ space( -2 ) }; - margin-bottom: ${ space( -2 ) }; -`; - -const itemSuffix = css` - flex: 0; - width: max-content; - display: inline-flex; - align-items: center; - justify-content: center; - /* Push prefix to the inline-end of the item */ - margin-inline-start: auto; - /* Minimum space between the item's content and suffix */ - padding-inline-start: ${ space( 6 ) }; - /* - Negative margin allows the suffix to be as tall as the whole item - (incl. padding) before increasing the items' height. This can be useful, - e.g., when using icons that are bigger than 20x20 px - */ - margin-top: ${ space( -2 ) }; - margin-bottom: ${ space( -2 ) }; - - /* - Override color in normal conditions, but inherit the item's color - for altered conditions. - - TODO: - - For now, used opacity like for disabled item, which makes it work - regardless of the theme - - how do we translate this for themes? Should we have a new variable - for "secondary" text? - */ - opacity: 0.6; - - /* when the parent item is hovered / focused */ - [data-active-item] > &, - /* when the parent item is a submenu trigger and the submenu is open */ - [aria-expanded='true'] > &, - /* when the parent item is disabled */ - [aria-disabled='true'] > & { - opacity: 1; - } -`; - -export const ItemPrefixWrapper = styled.span` - ${ itemPrefix } -`; - -export const ItemSuffixWrapper = styled.span` - ${ itemSuffix } -`; - const baseItem = css` all: unset; position: relative; + min-height: ${ space( 10 ) }; + box-sizing: border-box; /* Occupy the width of all grid columns (ie. full width) */ grid-column: 1 / -1; @@ -209,26 +146,21 @@ const baseItem = css` color: ${ COLORS.gray[ 900 ] }; border-radius: ${ CONFIG.radiusBlockUi }; - padding: ${ space( 2 ) } ${ ITEM_PADDING_INLINE_END } ${ space( 2 ) } - ${ ITEM_PADDING_INLINE_START }; + padding-block: ${ ITEM_PADDING_BLOCK }; + padding-inline: ${ ITEM_PADDING_INLINE }; user-select: none; outline: none; &[aria-disabled='true'] { - /* - TODO: - - we need a disabled color in the Theme variables - - design specs use opacity instead of setting a new text color - */ - opacity: 0.5; + color: ${ COLORS.ui.textDisabled }; pointer-events: none; } /* Hover */ - &[data-active-item] { - /* TODO: reconcile with global focus styles */ - background-color: ${ COLORS.gray[ '100' ] }; + &[data-active-item]:not( [data-focus-visible] ) { + background-color: ${ COLORS.theme.accent }; + color: ${ COLORS.white }; } /* Keyboard focus (focus-visible) */ @@ -247,7 +179,8 @@ const baseItem = css` /* When the item is the trigger of an open submenu */ ${ DropdownMenu }:not(:focus) &:not(:focus)[aria-expanded="true"] { - /* TODO: should we style submenu triggers any different? */ + background-color: ${ COLORS.gray[ 100 ] }; + color: ${ COLORS.gray[ 900 ] }; } svg { @@ -256,15 +189,44 @@ const baseItem = css` `; export const DropdownMenuItem = styled( Ariakit.MenuItem )` - ${ baseItem } + ${ baseItem }; `; export const DropdownMenuCheckboxItem = styled( Ariakit.MenuItemCheckbox )` - ${ baseItem } + ${ baseItem }; `; export const DropdownMenuRadioItem = styled( Ariakit.MenuItemRadio )` - ${ baseItem } + ${ baseItem }; +`; + +export const ItemPrefixWrapper = styled.span` + /* Always occupy the first column, even when auto-collapsing */ + grid-column: 1; + + &:not( :empty ) { + margin-inline-end: ${ space( 2 ) }; + } + + display: flex; + align-items: center; + justify-content: center; + + /* Override inline styles applied by ariakit */ + width: auto !important; + height: auto !important; + + color: ${ COLORS.gray[ '700' ] }; + + /* + * When the parent menu item is active, except when it's a non-focused/hovered + * submenu trigger (in that case, color should not be inherited) + */ + [data-active-item]:not( [data-focus-visible] ) > &, + /* When the parent menu item is disabled */ + [aria-disabled='true'] > & { + color: inherit; + } `; export const DropdownMenuItemContentWrapper = styled.div` @@ -275,13 +237,41 @@ export const DropdownMenuItemContentWrapper = styled.div` grid-column: 2; display: flex; + align-items: center; + justify-content: space-between; + gap: ${ space( 3 ) }; + + pointer-events: none; `; export const DropdownMenuItemChildrenWrapper = styled.div` flex: 1; + display: inline-flex; flex-direction: column; - pointer-events: none; + gap: ${ space( 1 ) }; +`; + +export const ItemSuffixWrapper = styled.span` + flex: 0; + width: max-content; + + display: flex; + align-items: center; + justify-content: center; + gap: ${ space( 3 ) }; + + color: ${ COLORS.gray[ '700' ] }; + + /* + * When the parent menu item is active, except when it's a non-focused/hovered + * submenu trigger (in that case, color should not be inherited) + */ + [data-active-item]:not( [data-focus-visible] ) *:not(${ DropdownMenu }) &, + /* When the parent menu item is disabled */ + [aria-disabled='true'] *:not(${ DropdownMenu }) & { + color: inherit; + } `; export const DropdownMenuGroup = styled( Ariakit.MenuGroup )` @@ -297,41 +287,37 @@ export const DropdownMenuSeparator = styled( Ariakit.MenuSeparator )< border: none; height: ${ CONFIG.borderWidth }; - /* TODO: doesn't match border color from variables */ background-color: ${ ( props ) => props.variant === 'toolbar' ? TOOLBAR_VARIANT_BORDER_COLOR - : DEFAULT_BORDER_COLOR }; - /* Negative horizontal margin to make separator go from side to side */ - margin: ${ space( 2 ) } calc( -1 * ${ CONTENT_WRAPPER_PADDING } ); + : DIVIDER_COLOR }; + /* Align with menu items' content */ + margin-block: ${ space( 2 ) }; + margin-inline: ${ ITEM_PADDING_INLINE }; /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; `; export const SubmenuChevronIcon = styled( Icon )` + width: ${ space( 1.5 ) }; ${ rtl( { - transform: `scaleX(1) translateX(${ space( 2 ) })`, + transform: `scaleX(1)`, }, { - transform: `scaleX(-1) translateX(${ space( 2 ) })`, + transform: `scaleX(-1)`, } - ) } + ) }; `; export const DropdownMenuItemHelpText = styled( Truncate )` - font-size: 12px; + font-size: ${ font( 'helpText.fontSize' ) }; + line-height: 16px; color: ${ COLORS.gray[ '700' ] }; -`; -// /* when the immediate parent item is hovered / focused */ -// [data-active-item] > ${ DropdownMenuItem }[data-active-item] > &, -// [data-active-item] > ${ DropdownMenuRadioItem }[data-active-item] > &, -// [data-active-item] > ${ DropdownMenuCheckboxItem }[data-active-item] > &, -// /* when the parent item is a submenu trigger and the submenu is open */ -// [aria-expanded='true'] > &, -// /* when the parent item is disabled */ -// [aria-disabled='true'] > & { -// color: inherit; -// } + [data-active-item]:not( [data-focus-visible] ) *:not( ${ DropdownMenu } ) &, + [aria-disabled='true'] *:not( ${ DropdownMenu } ) & { + color: inherit; + } +`; From 7dd75880903eb2075b537aca0fd187321b044611 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 14 Nov 2023 22:22:11 +0100 Subject: [PATCH 10/23] Add label subcomponent --- .../src/dropdown-menu-v2-ariakit/index.tsx | 15 ++++++++++++++- .../src/dropdown-menu-v2-ariakit/styles.ts | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx index 47192a6ee2bae8..aac8a8844f6ed6 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx @@ -347,8 +347,21 @@ export const DropdownMenuSeparator = forwardRef< ); } ); +export const DropdownMenuItemLabel = forwardRef< + HTMLSpanElement, + WordPressComponentProps< { children: React.ReactNode }, 'span', true > +>( function DropdownMenuItemLabel( props, ref ) { + return ( + + ); +} ); + export const DropdownMenuItemHelpText = forwardRef< - HTMLHRElement, + HTMLSpanElement, WordPressComponentProps< { children: React.ReactNode }, 'span', true > >( function DropdownMenuItemHelpText( props, ref ) { return ( diff --git a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts index 13884b1388f043..55d8ab5a40de91 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts +++ b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts @@ -311,6 +311,12 @@ export const SubmenuChevronIcon = styled( Icon )` ) }; `; +export const DropdownMenuItemLabel = styled( Truncate )` + font-size: ${ font( 'default.fontSize' ) }; + line-height: 20px; + color: inherit; +`; + export const DropdownMenuItemHelpText = styled( Truncate )` font-size: ${ font( 'helpText.fontSize' ) }; line-height: 16px; From 96ed0800a35b98d1fc99edcda5b033fe32157a74 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 14 Nov 2023 22:22:24 +0100 Subject: [PATCH 11/23] Update storybook examples --- .../stories/index.story.tsx | 192 +++++++++++++----- 1 file changed, 145 insertions(+), 47 deletions(-) diff --git a/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx b/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx index 1b7728b1e4ea99..fcc8f76294bc6b 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx @@ -7,7 +7,7 @@ import { css } from '@emotion/react'; /** * WordPress dependencies */ -import { wordpress } from '@wordpress/icons'; +import { customLink, formatCapitalize } from '@wordpress/icons'; import { useState, useMemo, useContext } from '@wordpress/element'; /** @@ -22,6 +22,7 @@ import { DropdownMenuSeparator, DropdownMenuContext, DropdownMenuRadioItem, + DropdownMenuItemLabel, DropdownMenuItemHelpText, } from '..'; import Icon from '../../icon'; @@ -36,6 +37,20 @@ const meta: Meta< typeof DropdownMenu > = { subcomponents: { // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 DropdownMenuItem, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownMenuCheckboxItem, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownMenuGroup, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownMenuSeparator, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownMenuContext, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownMenuRadioItem, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownMenuItemLabel, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownMenuItemHelpText, }, argTypes: { children: { control: { type: null } }, @@ -66,20 +81,22 @@ export default meta; export const Default: StoryFn< typeof DropdownMenu > = ( props ) => ( - Label - Label + Label + + + Label Help text - Label + Label - Help text is automatically truncated when there are more than - two lines of text. + The menu item help text is automatically truncated when there + are more than two lines of text - Label + Label This item doesn't close the menu on click @@ -88,19 +105,22 @@ export const Default: StoryFn< typeof DropdownMenu > = ( props ) => ( } + prefix={ } > - With prefix - - ⌘S }> - With suffix + With prefix + With suffix } - suffix={ ⌥⌘T } + prefix={ } + suffix="⌥⌘T" > - Disabled with prefix and suffix + + Disabled with prefix and suffix + + + And help text + @@ -119,17 +139,33 @@ export const WithSubmenu: StoryFn< typeof DropdownMenu > = ( props ) => ( - Submenu trigger + + Submenu trigger item with a long label + } > - Level 2 item - Level 2 item + + Level 2 item + + + Level 2 item + Submenu trigger } + trigger={ + + + Submenu trigger + + + } > - Level 3 item - Level 3 item + + Level 3 item + + + Level 3 item + @@ -162,8 +198,11 @@ export const WithCheckboxes: StoryFn< typeof DropdownMenu > = ( props ) => { - Checkbox item A + + Checkbox item A + Uncontrolled @@ -173,7 +212,9 @@ export const WithCheckboxes: StoryFn< typeof DropdownMenu > = ( props ) => { value="b" defaultChecked > - Checkbox item B + + Checkbox item B + Uncontrolled, initially checked @@ -187,7 +228,9 @@ export const WithCheckboxes: StoryFn< typeof DropdownMenu > = ( props ) => { checked={ isAChecked } onChange={ ( e ) => setAChecked( e.target.checked ) } > - Checkbox item A + + Checkbox item A + Controlled @@ -198,7 +241,9 @@ export const WithCheckboxes: StoryFn< typeof DropdownMenu > = ( props ) => { checked={ isBChecked } onChange={ ( e ) => setBChecked( e.target.checked ) } > - Checkbox item B + + Checkbox item B + Controlled, initially checked @@ -210,7 +255,9 @@ export const WithCheckboxes: StoryFn< typeof DropdownMenu > = ( props ) => { name="checkbox-multiple-uncontrolled" value="a" > - Checkbox item A + + Checkbox item A + Uncontrolled, multiple selection @@ -220,7 +267,9 @@ export const WithCheckboxes: StoryFn< typeof DropdownMenu > = ( props ) => { value="b" defaultChecked > - Checkbox item B + + Checkbox item B + Uncontrolled, multiple selection, initially checked @@ -234,7 +283,9 @@ export const WithCheckboxes: StoryFn< typeof DropdownMenu > = ( props ) => { checked={ multipleCheckboxesValue.includes( 'a' ) } onChange={ onMultipleCheckboxesCheckedChange } > - Checkbox item A + + Checkbox item A + Controlled, multiple selection @@ -245,7 +296,9 @@ export const WithCheckboxes: StoryFn< typeof DropdownMenu > = ( props ) => { checked={ multipleCheckboxesValue.includes( 'b' ) } onChange={ onMultipleCheckboxesCheckedChange } > - Checkbox item B + + Checkbox item B + Controlled, multiple selection, initially checked @@ -268,7 +321,7 @@ export const WithRadios: StoryFn< typeof DropdownMenu > = ( props ) => { - Radio item 1 + Radio item 1 Uncontrolled @@ -278,7 +331,7 @@ export const WithRadios: StoryFn< typeof DropdownMenu > = ( props ) => { value="two" defaultChecked > - Radio item 2 + Radio item 2 Uncontrolled, initially checked @@ -292,7 +345,7 @@ export const WithRadios: StoryFn< typeof DropdownMenu > = ( props ) => { checked={ radioValue === 'one' } onChange={ onRadioChange } > - Radio item 1 + Radio item 1 Controlled @@ -303,7 +356,7 @@ export const WithRadios: StoryFn< typeof DropdownMenu > = ( props ) => { checked={ radioValue === 'two' } onChange={ onRadioChange } > - Radio item 2 + Radio item 2 Controlled, initially checked @@ -337,13 +390,17 @@ export const WithModals: StoryFn< typeof DropdownMenu > = ( props ) => { onClick={ () => setOuterModalOpen( true ) } hideOnClick={ false } > - Open outer modal + + Open outer modal + setInnerModalOpen( true ) } hideOnClick={ false } > - Open inner modal + + Open inner modal + { isInnerModalOpen && ( = ( props ) => { return ( - Item + + Item + - Item from fill + + + Item from fill + + Submenu from fill + + + Submenu from fill + + } > - Submenu item from fill + + + Submenu item from fill + + @@ -457,15 +528,28 @@ const toolbarVariantContextValue = { }, }; export const ToolbarVariant: StoryFn< typeof DropdownMenu > = ( props ) => ( + // TODO: add toolbar - Level 1 item - Level 1 item + + Level 1 item + + + Level 1 item + Submenu trigger } + trigger={ + + + Submenu trigger + + + } > - Level 2 item + + Level 2 item + @@ -488,17 +572,31 @@ export const InsideModal: StoryFn< typeof DropdownMenu > = ( props ) => { { isModalOpen && ( setModalOpen( false ) }> - Level 1 item - Level 1 item + + + Level 1 item + + + + + Level 1 item + + - Submenu trigger + + Submenu trigger + } > - Level 2 item + + + Level 2 item + +