diff --git a/packages/components/src/tools-panel/context.ts b/packages/components/src/tools-panel/context.ts index d0104db26e6b7..7b13e59a92acd 100644 --- a/packages/components/src/tools-panel/context.ts +++ b/packages/components/src/tools-panel/context.ts @@ -18,6 +18,7 @@ export const ToolsPanelContext = createContext< ToolsPanelContextType >( { registerPanelItem: noop, deregisterPanelItem: noop, flagItemCustomization: noop, + areAllOptionalControlsHidden: true, } ); export const useToolsPanelContext = () => diff --git a/packages/components/src/tools-panel/stories/index.js b/packages/components/src/tools-panel/stories/index.js index 230a407bff6fc..95a33301da56c 100644 --- a/packages/components/src/tools-panel/stories/index.js +++ b/packages/components/src/tools-panel/stories/index.js @@ -35,7 +35,10 @@ export const _default = () => { return ( - + !! width } @@ -66,6 +69,7 @@ export const _default = () => { hasValue={ () => !! minHeight } label="Minimum height" onDeselect={ () => setMinHeight( undefined ) } + isShownByDefault={ true } > { ); }; +export const WithOptionalItemsPlusIcon = () => { + const [ height, setHeight ] = useState(); + const [ width, setWidth ] = useState(); + + const resetAll = () => { + setHeight( undefined ); + setWidth( undefined ); + }; + + return ( + + + + !! width } + label="Width" + onDeselect={ () => setWidth( undefined ) } + isShownByDefault={ false } + > + setWidth( next ) } + /> + + !! height } + label="Height" + onDeselect={ () => setHeight( undefined ) } + isShownByDefault={ false } + > + setHeight( next ) } + /> + + + + + ); +}; + const { Fill: ToolsPanelItems, Slot } = createSlotFill( 'ToolsPanelSlot' ); const panelId = 'unique-tools-panel-id'; diff --git a/packages/components/src/tools-panel/test/index.js b/packages/components/src/tools-panel/test/index.js index b8aaba1914972..f8379fe4d551d 100644 --- a/packages/components/src/tools-panel/test/index.js +++ b/packages/components/src/tools-panel/test/index.js @@ -139,10 +139,17 @@ const renderPanel = () => { ); }; -// Helper to find the menu button and simulate a user click. +/** + * Helper to find the menu button and simulate a user click. + * + * @return {HTMLElement} The menuButton. + */ const openDropdownMenu = () => { - const menuButton = screen.getByLabelText( defaultProps.label ); + const menuButton = screen.getByRole( 'button', { + name: /view([\w\s]+)options/i, + } ); fireEvent.click( menuButton ); + return menuButton; }; // Opens dropdown then selects the menu item by label before simulating a click. @@ -212,7 +219,7 @@ describe( 'ToolsPanel', () => { it( 'should render panel menu when at least one panel item', () => { renderPanel(); - const menuButton = screen.getByLabelText( defaultProps.label ); + const menuButton = openDropdownMenu(); expect( menuButton ).toBeInTheDocument(); } ); @@ -509,4 +516,47 @@ describe( 'ToolsPanel', () => { expect( items[ 1 ] ).toHaveTextContent( 'Item 2' ); } ); } ); + + describe( 'panel header icon toggle', () => { + const optionalControls = { + attributes: { value: false }, + hasValue: jest.fn().mockImplementation( () => { + return !! optionalControls.attributes.value; + } ), + label: 'Optional', + onDeselect: jest.fn(), + onSelect: jest.fn(), + isShownByDefault: false, + }; + + it( 'should render appropriate icons for the dropdown menu', async () => { + render( + + +
Optional control
+
+
+ ); + + // There are unactivated, optional menu items in the Tools Panel dropdown. + const optionsHiddenIcon = screen.getByRole( 'button', { + name: 'View and add options', + } ); + + expect( optionsHiddenIcon ).toBeInTheDocument(); + + await selectMenuItem( optionalControls.label ); + + // There are now NO unactivated, optional menu items in the Tools Panel dropdown. + expect( + screen.queryByRole( 'button', { name: 'View and add options' } ) + ).not.toBeInTheDocument(); + + const optionsDisplayedIcon = screen.getByRole( 'button', { + name: 'View options', + } ); + + expect( optionsDisplayedIcon ).toBeInTheDocument(); + } ); + } ); } ); diff --git a/packages/components/src/tools-panel/tools-panel-header/component.tsx b/packages/components/src/tools-panel/tools-panel-header/component.tsx index 3204615434b4b..537578c93da0c 100644 --- a/packages/components/src/tools-panel/tools-panel-header/component.tsx +++ b/packages/components/src/tools-panel/tools-panel-header/component.tsx @@ -7,8 +7,8 @@ import type { Ref } from 'react'; /** * WordPress dependencies */ -import { check, reset, moreVertical } from '@wordpress/icons'; -import { __, sprintf } from '@wordpress/i18n'; +import { check, reset, moreVertical, plus } from '@wordpress/icons'; +import { __, _x, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -118,6 +118,7 @@ const ToolsPanelHeader = ( const { dropdownMenuClassName, hasMenuItems, + areAllOptionalControlsHidden, label: labelText, menuItems, resetAll, @@ -131,14 +132,21 @@ const ToolsPanelHeader = ( const defaultItems = Object.entries( menuItems?.default || {} ); const optionalItems = Object.entries( menuItems?.optional || {} ); + const dropDownMenuIcon = areAllOptionalControlsHidden ? plus : moreVertical; + const dropDownMenuLabelText = areAllOptionalControlsHidden + ? _x( + 'View and add options', + 'Button label to reveal tool panel options' + ) + : _x( 'View options', 'Button label to reveal tool panel options' ); return (

{ labelText } { hasMenuItems && ( { ( { onClose = noop } ) => ( diff --git a/packages/components/src/tools-panel/tools-panel-header/hook.ts b/packages/components/src/tools-panel/tools-panel-header/hook.ts index f31e8cb6372df..064f324c163be 100644 --- a/packages/components/src/tools-panel/tools-panel-header/hook.ts +++ b/packages/components/src/tools-panel/tools-panel-header/hook.ts @@ -29,12 +29,17 @@ export function useToolsPanelHeader( return cx( styles.DropdownMenu ); }, [] ); - const { menuItems, hasMenuItems } = useToolsPanelContext(); + const { + menuItems, + hasMenuItems, + areAllOptionalControlsHidden, + } = useToolsPanelContext(); return { ...otherProps, dropdownMenuClassName, hasMenuItems, + areAllOptionalControlsHidden, menuItems, className: classes, }; diff --git a/packages/components/src/tools-panel/tools-panel/hook.ts b/packages/components/src/tools-panel/tools-panel/hook.ts index afbf3174fae90..ab0f29c034830 100644 --- a/packages/components/src/tools-panel/tools-panel/hook.ts +++ b/packages/components/src/tools-panel/tools-panel/hook.ts @@ -109,17 +109,15 @@ export function useToolsPanel( } ); }; - // Track whether all optional controls are displayed or not. - // If no optional controls are present, then none are hidden and this will - // be `false`. + // Whether all optional menu items are hidden or not must be tracked + // in order to later determine if the panel display is empty and handle + // conditional display of a plus icon to indicate the presence of further + // menu items. const [ areAllOptionalControlsHidden, setAreAllOptionalControlsHidden, ] = useState( false ); - // We need to track whether any optional menu items are active to later - // determine whether the panel is currently empty and any inner wrapper - // should be hidden. useEffect( () => { if ( menuItems.optional ) { const optionalItems = Object.entries( menuItems.optional ); @@ -203,6 +201,7 @@ export function useToolsPanel( registerPanelItem, deregisterPanelItem, flagItemCustomization, + areAllOptionalControlsHidden, hasMenuItems: !! panelItems.length, isResetting: isResetting.current, shouldRenderPlaceholderItems, diff --git a/packages/components/src/tools-panel/types.ts b/packages/components/src/tools-panel/types.ts index 1617f32f313bc..374c98495c8c7 100644 --- a/packages/components/src/tools-panel/types.ts +++ b/packages/components/src/tools-panel/types.ts @@ -127,6 +127,7 @@ export type ToolsPanelContext = { flagItemCustomization: ( label: string ) => void; isResetting: boolean; shouldRenderPlaceholderItems: boolean; + areAllOptionalControlsHidden: boolean; }; export type ToolsPanelControlsGroupProps = {