Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions packages/components/src/tools-panel/test/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,32 @@ describe( 'ToolsPanel', () => {
expect( controlRerendered ).toBeInTheDocument();
} );

it( 'should render optional item on first render when isShownOnFirstRender is true', () => {
const { rerender } = render(
<ToolsPanel { ...defaultProps }>
<ToolsPanelItem { ...altControlProps }>
<div>Optional control</div>
</ToolsPanelItem>
</ToolsPanel>
);

expect(
screen.queryByText( 'Optional control' )
).not.toBeInTheDocument();

rerender(
<ToolsPanel { ...defaultProps }>
<ToolsPanelItem { ...altControlProps } isShownOnFirstRender>
<div>Optional control</div>
</ToolsPanelItem>
</ToolsPanel>
);

expect(
screen.getByText( 'Optional control' )
).toBeInTheDocument();
} );

it( 'should continue to render shown by default item after it is toggled off via menu item', async () => {
render(
<ToolsPanel { ...defaultProps }>
Expand Down Expand Up @@ -749,6 +775,23 @@ describe( 'ToolsPanel', () => {
expect( altControlProps.onDeselect ).not.toHaveBeenCalled();
} );

it( 'should call onDeselect when optional item with no value is hidden', async () => {
// Covers the case where an optional item is shown via the menu but
// never receives a value. Previously, onDeselect would silently not
// fire because it was gated on hasValue() being true.
renderPanel();

await openDropdownMenu();
await selectMenuItem( altControlProps.label );

expect( altControlProps.onSelect ).toHaveBeenCalledTimes( 1 );
expect( altControlProps.onDeselect ).not.toHaveBeenCalled();

await selectMenuItem( altControlProps.label );

expect( altControlProps.onDeselect ).toHaveBeenCalledTimes( 1 );
} );

it( 'should call resetAll callback when its menu item is selected', async () => {
renderPanel();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ panel's menu.
- Required: No
- Default: `false`

### `isShownOnFirstRender`: `boolean`

For optional controls only, this determines whether the item should be visible
when it first renders, even if `hasValue()` is `false`.

- Required: No

### `label`: `string`

The supplied label is dual purpose.
Expand Down
17 changes: 15 additions & 2 deletions packages/components/src/tools-panel/tools-panel-item/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
useEffect,
useLayoutEffect,
useMemo,
useRef,
} from '@wordpress/element';

/**
Expand All @@ -28,6 +29,7 @@ export function useToolsPanelItem(
className,
hasValue,
isShownByDefault = false,
isShownOnFirstRender,
label,
panelId,
resetAllFilter = noop,
Expand Down Expand Up @@ -74,6 +76,7 @@ export function useToolsPanelItem(
registerPanelItem( {
hasValue: hasValueCallback,
isShownByDefault,
isShownOnFirstRender,
label,
panelId,
} );
Expand All @@ -91,6 +94,7 @@ export function useToolsPanelItem(
currentPanelId,
hasMatchingPanel,
isShownByDefault,
isShownOnFirstRender,
label,
hasValueCallback,
panelId,
Expand Down Expand Up @@ -139,6 +143,11 @@ export function useToolsPanelItem(
isShownByDefault,
] );

// Tracks whether the item was explicitly shown by a user menu action.
// This allows onDeselect to fire even when hasValue() is false, but only
// for items the user intentionally toggled on.
const wasShownByUserRef = useRef( false );

// Determine if the panel item's corresponding menu is being toggled and
// trigger appropriate callback if it is.
useEffect( () => {
Expand All @@ -150,11 +159,15 @@ export function useToolsPanelItem(
}

if ( isMenuItemChecked && ! isValueSet && ! wasMenuItemChecked ) {
wasShownByUserRef.current = true;
onSelect?.();
}

if ( ! isMenuItemChecked && isValueSet && wasMenuItemChecked ) {
onDeselect?.();
if ( ! isMenuItemChecked && wasMenuItemChecked ) {
if ( isValueSet || wasShownByUserRef.current ) {
onDeselect?.();
}
wasShownByUserRef.current = false;
}
}, [
hasMatchingPanel,
Expand Down
28 changes: 17 additions & 11 deletions packages/components/src/tools-panel/tools-panel/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,23 @@ const generateMenuItems = ( {
const newMenuItems: ToolsPanelMenuItems = emptyMenuItems();
const menuItems: ToolsPanelMenuItems = emptyMenuItems();

panelItems.forEach( ( { hasValue, isShownByDefault, label } ) => {
const group = isShownByDefault ? 'default' : 'optional';

// If a menu item for this label has already been flagged as customized
// (for default controls), or toggled on (for optional controls), do not
// overwrite its value as those controls would lose that state.
const existingItemValue = currentMenuItems?.[ group ]?.[ label ];
const value = existingItemValue ? existingItemValue : hasValue();

newMenuItems[ group ][ label ] = shouldReset ? false : value;
} );
panelItems.forEach(
( { hasValue, isShownByDefault, isShownOnFirstRender, label } ) => {
const group = isShownByDefault ? 'default' : 'optional';

// If a menu item for this label has already been flagged as customized
// (for default controls), or toggled on (for optional controls), do not
// overwrite its value as those controls would lose that state.
const existingItemValue = currentMenuItems?.[ group ]?.[ label ];
const initialValue =
! isShownByDefault && isShownOnFirstRender !== undefined
? isShownOnFirstRender
: hasValue();
const value = existingItemValue ?? initialValue;

newMenuItems[ group ][ label ] = shouldReset ? false : value;
}
);

// Loop the known, previously registered items first to maintain menu order.
menuItemOrder.forEach( ( key ) => {
Expand Down
5 changes: 5 additions & 0 deletions packages/components/src/tools-panel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ export type ToolsPanelItem = {
* @default false
*/
isShownByDefault?: boolean;
/**
* For optional items only, determines whether the item should be visible on
* first render even when `hasValue()` is false.
*/
isShownOnFirstRender?: boolean;
/**
* The supplied label is dual purpose. It is used as:
* 1. the human-readable label for the panel's dropdown menu
Expand Down
Loading