Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ToolsPanel: Allow SlotFill injection of panel items #34632

Merged
merged 6 commits into from
Sep 10, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export { default as ToolbarItem } from './toolbar-item';
export {
ToolsPanel as __experimentalToolsPanel,
ToolsPanelItem as __experimentalToolsPanelItem,
ToolsPanelContext as __experimentalToolsPanelContext,
} from './tools-panel';
export { default as Tooltip } from './tooltip';
export {
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/tools-panel/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as ToolsPanel } from './tools-panel';
export { default as ToolsPanelItem } from './tools-panel-item';
export { ToolsPanelContext } from './context';
7 changes: 2 additions & 5 deletions packages/components/src/tools-panel/stories/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,13 @@ export const _default = () => {
const resetAll = () => {
setHeight( undefined );
setWidth( undefined );
setMinHeight( undefined );
};

return (
<PanelWrapperView>
<Panel>
<ToolsPanel
header="Tools Panel"
label="Display options"
resetAll={ resetAll }
>
<ToolsPanel label="Tools Panel" resetAll={ resetAll }>
<ToolsPanelItem
className="single-column"
hasValue={ () => !! width }
Expand Down
11 changes: 7 additions & 4 deletions packages/components/src/tools-panel/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ const resetAll = jest.fn();

// Default props for the tools panel.
const defaultProps = {
header: 'Panel header',
label: 'Display options',
label: 'Panel header',
resetAll,
};

Expand Down Expand Up @@ -231,9 +230,9 @@ describe( 'ToolsPanel', () => {
expect( menuItems[ 1 ] ).toHaveAttribute( 'aria-checked', 'false' );
} );

it( 'should render panel header', () => {
it( 'should render panel label as header text', () => {
renderPanel();
const header = screen.getByText( defaultProps.header );
const header = screen.getByText( defaultProps.label );

expect( header ).toBeInTheDocument();
} );
Expand Down Expand Up @@ -316,6 +315,10 @@ describe( 'ToolsPanel', () => {
await selectMenuItem( 'Reset all' );

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,13 @@ This component is generated automatically by its parent

## Props

### `header`: `string`
### `label`: `string`

Text to be displayed within the panel header.
Text to be displayed within the panel header. It is also passed along as the
`label` for the panel header's `DropdownMenu`.

- Required: Yes

### `menuLabel`: `string`

This is passed along as the `label` for the panel header's `DropdownMenu`.

- Required: No

### `resetAll`: `function`

The `resetAll` prop provides the callback to execute when the "Reset all" menu
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,22 @@ import { contextConnect } from '../../ui/context';
const ToolsPanelHeader = ( props, forwardedRef ) => {
const {
hasMenuItems,
header,
label: labelText,
menuItems,
menuLabel,
resetAll,
toggleItem,
...headerProps
} = useToolsPanelHeader( props );

if ( ! header ) {
if ( ! labelText ) {
return null;
}

return (
<h2 { ...headerProps } ref={ forwardedRef }>
{ header }
{ labelText }
{ hasMenuItems && (
<DropdownMenu icon={ moreVertical } label={ menuLabel }>
<DropdownMenu icon={ moreVertical } label={ labelText }>
{ ( { onClose } ) => (
<>
<MenuGroup label={ __( 'Display options' ) }>
Expand Down
37 changes: 37 additions & 0 deletions packages/components/src/tools-panel/tools-panel-item/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ for how to use `ToolsPanelItem`.

## Props

### `hasValue`: `function`

This is called when building the `ToolsPanel` menu to determine the item's
initial checked state.

- Required: Yes

### `isShownByDefault`: `boolean`

This prop identifies the current item as being displayed by default. This means
Expand All @@ -37,3 +44,33 @@ determine if the panel item should be displayed.
A panel item's `label` should be unique among all items within a single panel.

- Required: Yes

### `onDeselect`: `function`

Called when this item is deselected in the `ToolsPanel` menu. This is normally
used to reset the panel item control's value.

- Required: No

### `onSelect`: `function`

A callback to take action when this item is selected in the `ToolsPanel` menu.

- Required: No

### `panelId`: `string`

This prop can be set to a ID representing a unique `ToolsPanel`. Before
attempting to register with a panel, each item will ensure that it belongs to
the current panel. This avoids issues when sharing SlotFills to inject items
into a panel.

- Required: No

### `resetAllFilter`: `function`

A `ToolsPanel` will collect each item's `resetAllFilter` and pass an array of
these function through to the panel's `resetAll` callback. They can then be
iterated over to perform additional tasks for items injected via SlotFills.

- Required: No
24 changes: 18 additions & 6 deletions packages/components/src/tools-panel/tools-panel-item/hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export function useToolsPanelItem( props ) {
hasValue,
isShownByDefault,
label,
panelId,
resetAllFilter,
onDeselect = () => undefined,
onSelect = () => undefined,
...otherProps
Expand All @@ -29,19 +31,25 @@ export function useToolsPanelItem( props ) {
} );

const {
panelId: currentPanelId,
menuItems,
registerPanelItem,
deregisterPanelItem,
isResetting,
} = useToolsPanelContext();

// Registering the panel item allows the panel to include it in its
// automatically generated menu and determine its initial checked status.
useEffect( () => {
registerPanelItem( {
hasValue,
isShownByDefault,
label,
} );
if ( currentPanelId === panelId ) {
registerPanelItem( {
hasValue,
isShownByDefault,
label,
resetAllFilter,
panelId,
} );
}

return () => deregisterPanelItem( label );
}, [] );
Expand All @@ -56,14 +64,18 @@ export function useToolsPanelItem( props ) {
// Determine if the panel item's corresponding menu is being toggled and
// trigger appropriate callback if it is.
useEffect( () => {
if ( isResetting ) {
return;
}

if ( isMenuItemChecked && ! isValueSet && ! wasMenuItemChecked ) {
onSelect();
}

if ( ! isMenuItemChecked && wasMenuItemChecked ) {
onDeselect();
}
}, [ isMenuItemChecked, wasMenuItemChecked, isValueSet ] );
}, [ isMenuItemChecked, wasMenuItemChecked, isValueSet, isResetting ] );

return {
...otherProps,
Expand Down
23 changes: 11 additions & 12 deletions packages/components/src/tools-panel/tools-panel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,7 @@ export function DimensionPanel( props ) {
};

return (
<ToolsPanel
header={ __( 'Dimensions' ) }
label={ __( 'Dimensions options' ) }
resetAll={ resetAll }
>
<ToolsPanel label={ __( 'Dimensions' ) } resetAll={ resetAll }>
{ ! isPaddingDisabled && (
<ToolsPanelItem
hasValue={ () => hasPaddingValue( props ) }
Expand All @@ -73,19 +69,22 @@ export function DimensionPanel( props ) {

### `label`: `string`

The label for the panel's dropdown menu.
Text to be displayed within the panel's header and as the aria label for the
panel's dropdown menu.

- Required: Yes

### `resetAll`: `function`
### `panelId`: `function`
aaronrobertshaw marked this conversation as resolved.
Show resolved Hide resolved

A function to call when the `Reset all` menu option is selected. This is passed
through to the panel's header component.
The `panelId` is passed through the `ToolsPanelContext` to panel items. This is
be used to ensure items injected via SlotFills are only registered for their
intended panels.

- Required: Yes
- Required: No

### `header`: `string`
### `resetAll`: `function`

Text to be displayed within the panel's header.
A function to call when the `Reset all` menu option is selected. This is passed
through to the panel's header component.

- Required: Yes
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { contextConnect } from '../../ui/context';
const ToolsPanel = ( props, forwardedRef ) => {
const {
children,
header,
label,
panelContext,
resetAllItems,
Expand All @@ -22,8 +21,7 @@ const ToolsPanel = ( props, forwardedRef ) => {
<View { ...toolsPanelProps } ref={ forwardedRef }>
<ToolsPanelContext.Provider value={ panelContext }>
<ToolsPanelHeader
header={ header }
menuLabel={ label }
label={ label }
resetAll={ resetAllItems }
toggleItem={ toggleItem }
/>
Expand Down
47 changes: 40 additions & 7 deletions packages/components/src/tools-panel/tools-panel/hook.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
import { useEffect, useMemo, useState } from '@wordpress/element';
import { useEffect, useMemo, useRef, useState } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -11,7 +11,7 @@ import { useContextSystem } from '../../ui/context';
import { useCx } from '../../utils/hooks/use-cx';

export function useToolsPanel( props ) {
const { className, resetAll, ...otherProps } = useContextSystem(
const { className, resetAll, panelId, ...otherProps } = useContextSystem(
props,
'ToolsPanel'
);
Expand All @@ -21,6 +21,14 @@ export function useToolsPanel( props ) {
return cx( styles.ToolsPanel, className );
}, [ className ] );

const isResetting = useRef( false );

useEffect( () => {
if ( isResetting.current ) {
isResetting.current = false;
}
}, [ isResetting ] );

// Allow panel items to register themselves.
const [ panelItems, setPanelItems ] = useState( [] );

Expand All @@ -31,14 +39,32 @@ export function useToolsPanel( props ) {
// Panels need to deregister on unmount to avoid orphans in menu state.
// This is an issue when panel items are being injected via SlotFills.
const deregisterPanelItem = ( label ) => {
setPanelItems( ( items ) =>
items.filter( ( item ) => item.label !== label )
);
// When switching selections between components injecting matching
// controls, e.g. both panels have a "padding" control, the
// deregistration of the first panel doesn't occur until after the
// registration of the next.
const index = panelItems.findIndex( ( item ) => item.label === label );

if ( index !== -1 ) {
setPanelItems( ( items ) => items.splice( index, 1 ) );
}
};

// Manage and share display state of menu items representing child controls.
const [ menuItems, setMenuItems ] = useState( {} );

const getResetAllFilters = () => {
const filters = [];

panelItems.forEach( ( item ) => {
if ( item.resetAllFilter ) {
filters.push( item.resetAllFilter );
}
} );

return filters;
};

// Setup menuItems state as panel items register themselves.
useEffect( () => {
const items = {};
Expand All @@ -62,7 +88,8 @@ export function useToolsPanel( props ) {
// Resets display of children and executes resetAll callback if available.
const resetAllItems = () => {
if ( typeof resetAll === 'function' ) {
resetAll();
isResetting.current = true;
resetAll( getResetAllFilters() );
}

// Turn off display of all non-default items.
Expand All @@ -75,7 +102,13 @@ export function useToolsPanel( props ) {
setMenuItems( resetMenuItems );
};

const panelContext = { menuItems, registerPanelItem, deregisterPanelItem };
const panelContext = {
panelId,
menuItems,
registerPanelItem,
deregisterPanelItem,
isResetting: isResetting.current,
};

return {
...otherProps,
Expand Down