Skip to content

Commit

Permalink
ToolsPanel: Add CSS classes to first and last displayed ToolsPanelIte…
Browse files Browse the repository at this point in the history
…ms (#37546)
  • Loading branch information
aaronrobertshaw committed Jan 21, 2022
1 parent fa82344 commit 5062a1e
Show file tree
Hide file tree
Showing 8 changed files with 580 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- `ToggleGroupControl`: Avoid calling `onChange` if radio state changed from an incoming value ([#37224](https://github.com/WordPress/gutenberg/pull/37224/)).
- `ToggleGroupControl`: fix the computation of the backdrop dimensions when rendered in a Popover ([#37067](https://github.com/WordPress/gutenberg/pull/37067)).
- Add `__experimentalIsRenderedInSidebar` property to the `GradientPicker`and `CustomGradientPicker`. The property changes the color popover behavior to have a special placement behavior appropriate for sidebar UI's.
- Add `first` and `last` classes to displayed `ToolsPanelItem` group within a `ToolsPanel` ([#37546](https://github.com/WordPress/gutenberg/pull/37546))

### Bug Fix

Expand Down
1 change: 1 addition & 0 deletions packages/components/src/tools-panel/stories/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ export const WithConditionallyRenderedControl = () => {
};

export { TypographyPanel } from './typography-panel';
export { ToolsPanelWithItemGroupSlot } from './tools-panel-with-item-group-slot';

const PanelWrapperView = styled.div`
max-width: 280px;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/**
* External dependencies
*/
import styled from '@emotion/styled';
import { css } from '@emotion/react';

/**
* WordPress dependencies
*/
import { useContext, useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import Button from '../../button';
import ColorIndicator from '../../color-indicator';
import ColorPalette from '../../color-palette';
import Dropdown from '../../dropdown';
import Panel from '../../panel';
import { FlexItem } from '../../flex';
import { HStack } from '../../h-stack';
import { Item, ItemGroup } from '../../item-group';
import { ToolsPanel, ToolsPanelItem, ToolsPanelContext } from '..';
import { createSlotFill, Provider as SlotFillProvider } from '../../slot-fill';
import { useCx } from '../../utils';

// Available border colors.
const colors = [
{ name: 'Gray 0', color: '#f6f7f7' },
{ name: 'Gray 5', color: '#dcdcde' },
{ name: 'Gray 20', color: '#a7aaad' },
{ name: 'Gray 70', color: '#3c434a' },
{ name: 'Gray 100', color: '#101517' },
{ name: 'Blue 20', color: '#72aee6' },
{ name: 'Blue 40', color: '#3582c4' },
{ name: 'Blue 70', color: '#0a4b78' },
{ name: 'Red 40', color: '#e65054' },
{ name: 'Red 70', color: '#8a2424' },
{ name: 'Green 10', color: '#68de7c' },
{ name: 'Green 40', color: '#00a32a' },
{ name: 'Green 60', color: '#007017' },
{ name: 'Yellow 10', color: '#f2d675' },
{ name: 'Yellow 40', color: '#bd8600' },
];
const panelId = 'unique-tools-panel-id';

const { Fill, Slot } = createSlotFill( 'ToolsPanelSlot' );

// This storybook example aims to replicate a virtual bubbling SlotFill use case
// for the `ToolsPanel` when the Slot itself is an `ItemGroup`.

// In this scenario the `ToolsPanel` has to render item placeholders so fills
// maintain their order in the DOM. These placeholders in the DOM prevent the
// normal styling of the `ItemGroup` in particular the border radii on the first
// and last items. In case consumers of the ItemGroup and ToolsPanel are
// applying their own styles to these components, the ToolsPanel needs to assist
// consumers in identifying which of its visible items are first and last.

// This custom fill is required to re-establish the ToolsPanelContext for
// injected ToolsPanelItem components as they will not have access to the React
// Context as the Provider is part of the ToolsPanelItems.Slot tree.
const ToolsPanelItems = ( { children } ) => {
return (
<Fill>
{ ( fillProps ) => (
<ToolsPanelContext.Provider value={ fillProps }>
{ children }
</ToolsPanelContext.Provider>
) }
</Fill>
);
};

// This fetches the ToolsPanelContext and passes it through `fillProps` so that
// rendered fills can re-establish the `ToolsPanelContext.Provider`.
const SlotContainer = ( { Slot: ToolsPanelSlot, ...props } ) => {
const toolsPanelContext = useContext( ToolsPanelContext );

return (
<ToolsPanelSlot
{ ...props }
fillProps={ toolsPanelContext }
bubblesVirtually
/>
);
};

// This wraps the slot with a `ToolsPanel` mimicking a real-world use case from
// the block editor.
ToolsPanelItems.Slot = ( { resetAll, ...props } ) => (
<ToolsPanel
label="Tools Panel with Item Group"
resetAll={ resetAll }
panelId={ panelId }
hasInnerWrapper={ true }
shouldRenderPlaceholderItems={ true }
__experimentalFirstVisibleItemClass="first"
__experimentalLastVisibleItemClass="last"
>
<SlotContainer { ...props } Slot={ Slot } />
</ToolsPanel>
);

export const ToolsPanelWithItemGroupSlot = () => {
const [ attributes, setAttributes ] = useState( {} );
const { text, background, link } = attributes;

const cx = useCx();
const slotWrapperClassName = cx( SlotWrapper );
const itemClassName = cx( ToolsPanelItemClass );

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

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

setAttributes( newAttributes );
};

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

const ToolsPanelColorDropdown = ( { attribute, label, value } ) => {
return (
<ToolsPanelItem
className={ itemClassName }
hasValue={ () => !! value }
label={ label }
onDeselect={ () => updateAttribute( attribute, undefined ) }
resetAllFilter={ () => ( { [ attribute ]: undefined } ) }
panelId={ panelId }
as={ Item }
>
<Dropdown
renderToggle={ ( { onToggle } ) => (
<Button onClick={ onToggle }>
<HStack justify="flex-start">
<ColorIndicator colorValue={ value } />
<FlexItem>{ label }</FlexItem>
</HStack>
</Button>
) }
renderContent={ () => (
<ColorPalette
value={ value }
colors={ colors }
onChange={ ( newColor ) =>
updateAttribute( attribute, newColor )
}
/>
) }
/>
</ToolsPanelItem>
);
};

// ToolsPanelItems are rendered via two different fills to simulate
// injection from multiple locations.
return (
<SlotFillProvider>
<PanelWrapperView>
<Panel>
<ToolsPanelItems.Slot
as={ ItemGroup }
isBordered
isSeparated
isRounded={ false }
className={ slotWrapperClassName }
resetAll={ resetAll }
/>
</Panel>
</PanelWrapperView>
<ToolsPanelItems>
<ToolsPanelColorDropdown
attribute="text"
label="Text"
value={ text }
/>
<ToolsPanelColorDropdown
attribute="background"
label="Background"
value={ background }
/>
</ToolsPanelItems>
<ToolsPanelItems>
<ToolsPanelColorDropdown
attribute="link"
label="Link"
value={ link }
/>
</ToolsPanelItems>
</SlotFillProvider>
);
};

const PanelWrapperView = styled.div`
max-width: 280px;
font-size: 13px;
.components-dropdown-menu__menu {
max-width: 220px;
}
`;

const SlotWrapper = css`
&&& {
row-gap: 0;
border-radius: 20px;
}
> div {
grid-column: span 2;
border-radius: inherit;
}
`;

const ToolsPanelItemClass = css`
padding: 0;
&&.first {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}
&.last {
border-bottom-left-radius: inherit;
border-bottom-right-radius: inherit;
border-bottom-color: transparent;
}
&& > div,
&& > div > button {
width: 100%;
border-radius: inherit;
}
`;

0 comments on commit 5062a1e

Please sign in to comment.