Skip to content

Commit

Permalink
ToolsPanel: Allow SlotFill injection of panel items (#34632)
Browse files Browse the repository at this point in the history
* Update ToolsPanel to allow SlotFill injection of panel items
* Add SlotFill demo for ToolsPanel story
* Improve ToolsPanel readmes

Co-authored-by: Marco Ciampini <marco.ciampo@gmail.com>
  • Loading branch information
aaronrobertshaw and ciampo committed Sep 10, 2021
1 parent 43b591e commit 3b14a92
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 54 deletions.
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';
100 changes: 94 additions & 6 deletions packages/components/src/tools-panel/stories/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useState } from '@wordpress/element';
import { ToolsPanel, ToolsPanelItem } from '../';
import Panel from '../../panel';
import UnitControl from '../../unit-control';
import { createSlotFill, Provider as SlotFillProvider } from '../../slot-fill';

export default {
title: 'Components (Experimental)/ToolsPanel',
Expand All @@ -28,16 +29,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 Expand Up @@ -79,7 +77,97 @@ export const _default = () => {
);
};

const { Fill: ToolsPanelItems, Slot } = createSlotFill( 'ToolsPanelSlot' );
const panelId = 'unique-tools-panel-id';

export const WithSlotFillItems = () => {
const [ attributes, setAttributes ] = useState( {} );
const { width, height } = attributes;

const resetAll = ( resetFilters = [] ) => {
let newAttributes = {};

resetFilters.forEach( ( resetFilter ) => {
newAttributes = {
...newAttributes,
...resetFilter( newAttributes ),
};
} );

setAttributes( newAttributes );
};

const updateAttribute = ( name, value ) => {
setAttributes( {
...attributes,
[ name ]: value,
} );
};

return (
<SlotFillProvider>
<ToolsPanelItems>
<ToolsPanelItem
className="single-column"
hasValue={ () => !! width }
label="Injected Width"
onDeselect={ () => updateAttribute( 'width', undefined ) }
resetAllFilter={ () => ( { width: undefined } ) }
panelId={ panelId }
>
<UnitControl
label="Injected Width"
value={ width }
onChange={ ( next ) =>
updateAttribute( 'width', next )
}
/>
</ToolsPanelItem>
<ToolsPanelItem
className="single-column"
hasValue={ () => !! height }
label="Injected Height"
onDeselect={ () => updateAttribute( 'height', undefined ) }
resetAllFilter={ () => ( { height: undefined } ) }
panelId={ panelId }
>
<UnitControl
label="Injected Height"
value={ height }
onChange={ ( next ) =>
updateAttribute( 'height', next )
}
/>
</ToolsPanelItem>
<ToolsPanelItem
hasValue={ () => true }
label="Item for alternate panel"
onDeselect={ () => undefined }
resetAllFilter={ () => undefined }
panelId={ 'intended-for-another-panel-via-shared-slot' }
>
<p>
This panel item will not be displayed in the demo as its
panelId does not match the panel being rendered.
</p>
</ToolsPanelItem>
</ToolsPanelItems>
<PanelWrapperView>
<Panel>
<ToolsPanel
label="Tools Panel With SlotFill Items"
resetAll={ resetAll }
panelId={ panelId }
>
<Slot />
</ToolsPanel>
</Panel>
</PanelWrapperView>
</SlotFillProvider>
);
};

const PanelWrapperView = styled.div`
max-width: 250px;
max-width: 260px;
font-size: 13px;
`;
27 changes: 23 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,26 @@ 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();
} );

// This confirms the internal `isResetting` state when resetting all
// controls does not prevent subsequent individual reset requests.
// i.e. onDeselect callbacks are called correctly after a resetAll.
it( 'should call onDeselect after previous reset all', async () => {
renderPanel();

await selectMenuItem( 'Reset all' ); // Initial control is displayed by default.
await selectMenuItem( controlProps.label ); // Re-display control.

expect( controlProps.onDeselect ).not.toHaveBeenCalled();

await selectMenuItem( controlProps.label ); // Reset control.

expect( controlProps.onDeselect ).toHaveBeenCalled();
} );
} );

Expand Down
13 changes: 4 additions & 9 deletions packages/components/src/tools-panel/tools-panel-header/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,17 @@ 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
item is selected. It's purpose is to facilitate resetting any control values
item is selected. Its purpose is to facilitate resetting any control values
for items contained within this header's panel.

- Required: Yes
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
40 changes: 38 additions & 2 deletions packages/components/src/tools-panel/tools-panel-item/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ implementation subject to drastic and breaking changes.
</div>
<br />

This component acts a wrapper and controls the display of items to be contained
This component acts as a wrapper and controls the display of items to be contained
within a ToolsPanel. An item is displayed if it is flagged as a default control
or the corresponding panel menu item, provided via context, is toggled on for
this item.
Expand All @@ -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 @@ -30,10 +37,39 @@ panel's menu.

The supplied label is dual purpose.
It is used as:
1. the human readable label for the panel's dropdown menu
1. the human-readable label for the panel's dropdown menu
2. a key to locate the corresponding item in the panel's menu context to
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`

Panel items will ensure they are only registering with their intended panel by
comparing the `panelId` props set on both the item and the panel itself. This
allows items to be injected from a shared source.

- Required: No

### `resetAllFilter`: `function`

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

- 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

0 comments on commit 3b14a92

Please sign in to comment.