From 5a610d4727cdbba5f1802f933f591c3d25e94671 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Wed, 11 Aug 2021 09:44:00 +1000 Subject: [PATCH] Dimensions Panel: Add new ToolsPanel component and update spacing supports (#32392) - Adds new ToolsPanel components - Uses the new components to refactor spacing block supports into a "Dimensions" panel. --- docs/manifest.json | 18 + .../{spacing.php => dimensions.php} | 38 +- lib/load.php | 2 +- packages/block-editor/src/hooks/dimensions.js | 154 +++++++ packages/block-editor/src/hooks/index.js | 2 +- packages/block-editor/src/hooks/margin.js | 35 +- packages/block-editor/src/hooks/padding.js | 35 +- packages/block-editor/src/hooks/spacing.js | 97 ----- packages/block-editor/src/hooks/style.js | 4 +- packages/components/src/index.js | 4 + .../components/src/tools-panel/context.js | 7 + packages/components/src/tools-panel/index.js | 2 + .../src/tools-panel/stories/index.js | 73 ++++ packages/components/src/tools-panel/styles.js | 64 +++ .../components/src/tools-panel/test/index.js | 385 ++++++++++++++++++ .../tools-panel/tools-panel-header/README.md | 47 +++ .../tools-panel-header/component.js | 81 ++++ .../tools-panel/tools-panel-header/hook.js | 34 ++ .../tools-panel/tools-panel-header/index.js | 1 + .../tools-panel/tools-panel-item/README.md | 39 ++ .../tools-panel/tools-panel-item/component.js | 31 ++ .../src/tools-panel/tools-panel-item/hook.js | 67 +++ .../src/tools-panel/tools-panel-item/index.js | 2 + .../src/tools-panel/tools-panel/README.md | 91 +++++ .../src/tools-panel/tools-panel/component.js | 38 ++ .../src/tools-panel/tools-panel/hook.js | 79 ++++ .../src/tools-panel/tools-panel/index.js | 2 + .../{spacing-panel.js => dimensions-panel.js} | 66 ++- .../sidebar/global-styles-sidebar.js | 13 +- 29 files changed, 1378 insertions(+), 133 deletions(-) rename lib/block-supports/{spacing.php => dimensions.php} (71%) create mode 100644 packages/block-editor/src/hooks/dimensions.js delete mode 100644 packages/block-editor/src/hooks/spacing.js create mode 100644 packages/components/src/tools-panel/context.js create mode 100644 packages/components/src/tools-panel/index.js create mode 100644 packages/components/src/tools-panel/stories/index.js create mode 100644 packages/components/src/tools-panel/styles.js create mode 100644 packages/components/src/tools-panel/test/index.js create mode 100644 packages/components/src/tools-panel/tools-panel-header/README.md create mode 100644 packages/components/src/tools-panel/tools-panel-header/component.js create mode 100644 packages/components/src/tools-panel/tools-panel-header/hook.js create mode 100644 packages/components/src/tools-panel/tools-panel-header/index.js create mode 100644 packages/components/src/tools-panel/tools-panel-item/README.md create mode 100644 packages/components/src/tools-panel/tools-panel-item/component.js create mode 100644 packages/components/src/tools-panel/tools-panel-item/hook.js create mode 100644 packages/components/src/tools-panel/tools-panel-item/index.js create mode 100644 packages/components/src/tools-panel/tools-panel/README.md create mode 100644 packages/components/src/tools-panel/tools-panel/component.js create mode 100644 packages/components/src/tools-panel/tools-panel/hook.js create mode 100644 packages/components/src/tools-panel/tools-panel/index.js rename packages/edit-site/src/components/sidebar/{spacing-panel.js => dimensions-panel.js} (63%) diff --git a/docs/manifest.json b/docs/manifest.json index 35117c66d264a..a5b52ae4872d1 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1277,6 +1277,24 @@ "markdown_source": "../packages/components/src/toolbar/README.md", "parent": "components" }, + { + "title": "ToolsPanelHeader", + "slug": "tools-panel-header", + "markdown_source": "../packages/components/src/tools-panel/tools-panel-header/README.md", + "parent": "components" + }, + { + "title": "ToolsPanelItem", + "slug": "tools-panel-item", + "markdown_source": "../packages/components/src/tools-panel/tools-panel-item/README.md", + "parent": "components" + }, + { + "title": "ToolsPanel", + "slug": "tools-panel", + "markdown_source": "../packages/components/src/tools-panel/tools-panel/README.md", + "parent": "components" + }, { "title": "Tooltip", "slug": "tooltip", diff --git a/lib/block-supports/spacing.php b/lib/block-supports/dimensions.php similarity index 71% rename from lib/block-supports/spacing.php rename to lib/block-supports/dimensions.php index 592949f0473b8..b51682a34e016 100644 --- a/lib/block-supports/spacing.php +++ b/lib/block-supports/dimensions.php @@ -1,6 +1,6 @@ attributes ) { $block_type->attributes = array(); } - if ( $has_spacing_support && ! array_key_exists( 'style', $block_type->attributes ) ) { + // Check for existing style attribute definition e.g. from block.json. + if ( array_key_exists( 'style', $block_type->attributes ) ) { + return; + } + + $has_spacing_support = gutenberg_block_has_support( $block_type, array( 'spacing' ), false ); + // Future block supports such as height & width will be added here. + + if ( $has_spacing_support ) { $block_type->attributes['style'] = array( 'type' => 'object', ); } } +/** + * Add CSS classes for block dimensions to the incoming attributes array. + * This will be applied to the block markup in the front-end. + * + * @param WP_Block_Type $block_type Block Type. + * @param array $block_attributes Block attributes. + * + * @return array Block spacing CSS classes and inline styles. + */ +function gutenberg_apply_dimensions_support( $block_type, $block_attributes ) { + $spacing_styles = gutenberg_apply_spacing_support( $block_type, $block_attributes ); + // Future block supports such as height and width will be added here. + + return $spacing_styles; +} + /** * Add CSS classes for block spacing to the incoming attributes array. * This will be applied to the block markup in the front-end. @@ -88,9 +110,9 @@ function gutenberg_skip_spacing_serialization( $block_type ) { // Register the block support. WP_Block_Supports::get_instance()->register( - 'spacing', + 'dimensions', array( - 'register_attribute' => 'gutenberg_register_spacing_support', - 'apply' => 'gutenberg_apply_spacing_support', + 'register_attribute' => 'gutenberg_register_dimensions_support', + 'apply' => 'gutenberg_apply_dimensions_support', ) ); diff --git a/lib/load.php b/lib/load.php index d6cf4449b2168..3e9292efbf57b 100644 --- a/lib/load.php +++ b/lib/load.php @@ -128,5 +128,5 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/block-supports/custom-classname.php'; require __DIR__ . '/block-supports/border.php'; require __DIR__ . '/block-supports/layout.php'; -require __DIR__ . '/block-supports/spacing.php'; +require __DIR__ . '/block-supports/dimensions.php'; require __DIR__ . '/block-supports/duotone.php'; diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js new file mode 100644 index 0000000000000..8351676640b17 --- /dev/null +++ b/packages/block-editor/src/hooks/dimensions.js @@ -0,0 +1,154 @@ +/** + * WordPress dependencies + */ +import { + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, +} from '@wordpress/components'; +import { Platform } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { getBlockSupport } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import InspectorControls from '../components/inspector-controls'; +import { + MarginEdit, + hasMarginSupport, + hasMarginValue, + resetMargin, + useIsMarginDisabled, +} from './margin'; +import { + PaddingEdit, + hasPaddingSupport, + hasPaddingValue, + resetPadding, + useIsPaddingDisabled, +} from './padding'; +import { cleanEmptyObject } from './utils'; + +export const SPACING_SUPPORT_KEY = 'spacing'; + +/** + * Inspector controls for dimensions support. + * + * @param {Object} props Block props. + * + * @return {WPElement} Inspector controls for spacing support features. + */ +export function DimensionsPanel( props ) { + const isPaddingDisabled = useIsPaddingDisabled( props ); + const isMarginDisabled = useIsMarginDisabled( props ); + const isDisabled = useIsDimensionsDisabled( props ); + const isSupported = hasDimensionsSupport( props.name ); + + if ( isDisabled || ! isSupported ) { + return null; + } + + const defaultSpacingControls = getBlockSupport( props.name, [ + SPACING_SUPPORT_KEY, + '__experimentalDefaultControls', + ] ); + + // Callback to reset all block support attributes controlled via this panel. + const resetAll = () => { + const { style } = props.attributes; + + props.setAttributes( { + style: cleanEmptyObject( { + ...style, + spacing: { + ...style?.spacing, + margin: undefined, + padding: undefined, + }, + } ), + } ); + }; + + return ( + + + { ! isPaddingDisabled && ( + hasPaddingValue( props ) } + label={ __( 'Padding' ) } + onDeselect={ () => resetPadding( props ) } + isShownByDefault={ defaultSpacingControls?.padding } + > + + + ) } + { ! isMarginDisabled && ( + hasMarginValue( props ) } + label={ __( 'Margin' ) } + onDeselect={ () => resetMargin( props ) } + isShownByDefault={ defaultSpacingControls?.margin } + > + + + ) } + + + ); +} + +/** + * Determine whether there is dimensions related block support. + * + * @param {string} blockName Block name. + * + * @return {boolean} Whether there is support. + */ +export function hasDimensionsSupport( blockName ) { + if ( Platform.OS !== 'web' ) { + return false; + } + + return hasPaddingSupport( blockName ) || hasMarginSupport( blockName ); +} + +/** + * Determines whether dimensions support has been disabled. + * + * @param {Object} props Block properties. + * + * @return {boolean} If spacing support is completely disabled. + */ +const useIsDimensionsDisabled = ( props = {} ) => { + const paddingDisabled = useIsPaddingDisabled( props ); + const marginDisabled = useIsMarginDisabled( props ); + + return paddingDisabled && marginDisabled; +}; + +/** + * Custom hook to retrieve which padding/margin is supported + * e.g. top, right, bottom or left. + * + * Sides are opted into by default. It is only if a specific side is set to + * false that it is omitted. + * + * @param {string} blockName Block name. + * @param {string} feature The feature custom sides relate to e.g. padding or margins. + * + * @return {Object} Sides supporting custom margin. + */ +export function useCustomSides( blockName, feature ) { + const support = getBlockSupport( blockName, SPACING_SUPPORT_KEY ); + + // Skip when setting is boolean as theme isn't setting arbitrary sides. + if ( typeof support[ feature ] === 'boolean' ) { + return; + } + + return support[ feature ]; +} diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index c0e5c1b5f8bb8..e8a976277f970 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -12,7 +12,7 @@ import './font-size'; import './border-color'; import './layout'; -export { useCustomSides } from './spacing'; +export { useCustomSides } from './dimensions'; export { getBorderClassesAndStyles, useBorderProps } from './use-border-props'; export { getColorClassesAndStyles, useColorProps } from './use-color-props'; export { getSpacingClassesAndStyles } from './use-spacing-props'; diff --git a/packages/block-editor/src/hooks/margin.js b/packages/block-editor/src/hooks/margin.js index c0cb5c5d4ef5d..7e89049b40846 100644 --- a/packages/block-editor/src/hooks/margin.js +++ b/packages/block-editor/src/hooks/margin.js @@ -13,7 +13,7 @@ import { * Internal dependencies */ import useSetting from '../components/use-setting'; -import { SPACING_SUPPORT_KEY, useCustomSides } from './spacing'; +import { SPACING_SUPPORT_KEY, useCustomSides } from './dimensions'; import { cleanEmptyObject } from './utils'; /** @@ -28,6 +28,38 @@ export function hasMarginSupport( blockType ) { return !! ( true === support || support?.margin ); } +/** + * Checks if there is a current value in the margin block support attributes. + * + * @param {Object} props Block props. + * @return {boolean} Whether or not the block has a margin value set. + */ +export function hasMarginValue( props ) { + return props.attributes.style?.spacing?.margin !== undefined; +} + +/** + * Resets the margin block support attributes. This can be used when disabling + * the margin support controls for a block via a `ToolsPanel`. + * + * @param {Object} props Block props. + * @param {Object} props.attributes Block's attributes. + * @param {Object} props.setAttributes Function to set block's attributes. + */ +export function resetMargin( { attributes = {}, setAttributes } ) { + const { style } = attributes; + + setAttributes( { + style: cleanEmptyObject( { + ...style, + spacing: { + ...style?.spacing, + margin: undefined, + }, + } ), + } ); +} + /** * Custom hook that checks if margin settings have been disabled. * @@ -106,6 +138,7 @@ export function MarginEdit( props ) { label={ __( 'Margin' ) } sides={ sides } units={ units } + allowReset={ false } /> ), diff --git a/packages/block-editor/src/hooks/padding.js b/packages/block-editor/src/hooks/padding.js index 6a29ad7fd7f00..e481946e3defc 100644 --- a/packages/block-editor/src/hooks/padding.js +++ b/packages/block-editor/src/hooks/padding.js @@ -13,7 +13,7 @@ import { * Internal dependencies */ import useSetting from '../components/use-setting'; -import { SPACING_SUPPORT_KEY, useCustomSides } from './spacing'; +import { SPACING_SUPPORT_KEY, useCustomSides } from './dimensions'; import { cleanEmptyObject } from './utils'; /** @@ -28,6 +28,38 @@ export function hasPaddingSupport( blockType ) { return !! ( true === support || support?.padding ); } +/** + * Checks if there is a current value in the padding block support attributes. + * + * @param {Object} props Block props. + * @return {boolean} Whether or not the block has a padding value set. + */ +export function hasPaddingValue( props ) { + return props.attributes.style?.spacing?.padding !== undefined; +} + +/** + * Resets the padding block support attributes. This can be used when disabling + * the padding support controls for a block via a `ToolsPanel`. + * + * @param {Object} props Block props. + * @param {Object} props.attributes Block's attributes. + * @param {Object} props.setAttributes Function to set block's attributes. + */ +export function resetPadding( { attributes = {}, setAttributes } ) { + const { style } = attributes; + + setAttributes( { + style: cleanEmptyObject( { + ...style, + spacing: { + ...style?.spacing, + padding: undefined, + }, + } ), + } ); +} + /** * Custom hook that checks if padding settings have been disabled. * @@ -106,6 +138,7 @@ export function PaddingEdit( props ) { label={ __( 'Padding' ) } sides={ sides } units={ units } + allowReset={ false } /> ), diff --git a/packages/block-editor/src/hooks/spacing.js b/packages/block-editor/src/hooks/spacing.js deleted file mode 100644 index f356a211248f7..0000000000000 --- a/packages/block-editor/src/hooks/spacing.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * WordPress dependencies - */ -import { PanelBody } from '@wordpress/components'; -import { Platform } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { getBlockSupport } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import InspectorControls from '../components/inspector-controls'; -import { MarginEdit, hasMarginSupport, useIsMarginDisabled } from './margin'; -import { - PaddingEdit, - hasPaddingSupport, - useIsPaddingDisabled, -} from './padding'; - -export const SPACING_SUPPORT_KEY = 'spacing'; - -/** - * Inspector controls for spacing support. - * - * @param {Object} props Block props. - * - * @return {WPElement} Inspector controls for spacing support features. - */ -export function SpacingPanel( props ) { - const isDisabled = useIsSpacingDisabled( props ); - const isSupported = hasSpacingSupport( props.name ); - - if ( isDisabled || ! isSupported ) { - return null; - } - - return ( - - - - - - - ); -} - -/** - * Determine whether there is block support for padding or margins. - * - * @param {string} blockName Block name. - * - * @return {boolean} Whether there is support. - */ -export function hasSpacingSupport( blockName ) { - if ( Platform.OS !== 'web' ) { - return false; - } - - return hasPaddingSupport( blockName ) || hasMarginSupport( blockName ); -} - -/** - * Determines whether spacing support has been disabled. - * - * @param {Object} props Block properties. - * - * @return {boolean} If spacing support is completely disabled. - */ -const useIsSpacingDisabled = ( props = {} ) => { - const paddingDisabled = useIsPaddingDisabled( props ); - const marginDisabled = useIsMarginDisabled( props ); - - return paddingDisabled && marginDisabled; -}; - -/** - * Custom hook to retrieve which padding/margin is supported - * e.g. top, right, bottom or left. - * - * Sides are opted into by default. It is only if a specific side is set to - * false that it is omitted. - * - * @param {string} blockName Block name. - * @param {string} feature The feature custom sides relate to e.g. padding or margins. - * - * @return {Object} Sides supporting custom margin. - */ -export function useCustomSides( blockName, feature ) { - const support = getBlockSupport( blockName, SPACING_SUPPORT_KEY ); - - // Skip when setting is boolean as theme isn't setting arbitrary sides. - if ( typeof support[ feature ] === 'boolean' ) { - return; - } - - return support[ feature ]; -} diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 8c5d6ce1872f9..87bf4685d50d4 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -37,7 +37,7 @@ import { TYPOGRAPHY_SUPPORT_KEY, TYPOGRAPHY_SUPPORT_KEYS, } from './typography'; -import { SPACING_SUPPORT_KEY, SpacingPanel } from './spacing'; +import { SPACING_SUPPORT_KEY, DimensionsPanel } from './dimensions'; import useDisplayBlockControls from '../components/use-display-block-controls'; const styleSupportKeys = [ @@ -232,7 +232,7 @@ export const withBlockControls = createHigherOrderComponent( - + ) } diff --git a/packages/components/src/index.js b/packages/components/src/index.js index 59550b082e7d6..f054c43e6d7dd 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -138,6 +138,10 @@ export { default as ToolbarDropdownMenu } from './toolbar-dropdown-menu'; export { default as __experimentalToolbarContext } from './toolbar-context'; export { default as ToolbarGroup } from './toolbar-group'; export { default as ToolbarItem } from './toolbar-item'; +export { + ToolsPanel as __experimentalToolsPanel, + ToolsPanelItem as __experimentalToolsPanelItem, +} from './tools-panel'; export { default as Tooltip } from './tooltip'; export { default as __experimentalTreeGrid, diff --git a/packages/components/src/tools-panel/context.js b/packages/components/src/tools-panel/context.js new file mode 100644 index 0000000000000..e74e08708db9f --- /dev/null +++ b/packages/components/src/tools-panel/context.js @@ -0,0 +1,7 @@ +/** + * WordPress dependencies + */ +import { createContext, useContext } from '@wordpress/element'; + +export const ToolsPanelContext = createContext( {} ); +export const useToolsPanelContext = () => useContext( ToolsPanelContext ); diff --git a/packages/components/src/tools-panel/index.js b/packages/components/src/tools-panel/index.js new file mode 100644 index 0000000000000..e557e232330b5 --- /dev/null +++ b/packages/components/src/tools-panel/index.js @@ -0,0 +1,2 @@ +export { default as ToolsPanel } from './tools-panel'; +export { default as ToolsPanelItem } from './tools-panel-item'; diff --git a/packages/components/src/tools-panel/stories/index.js b/packages/components/src/tools-panel/stories/index.js new file mode 100644 index 0000000000000..3f40051135857 --- /dev/null +++ b/packages/components/src/tools-panel/stories/index.js @@ -0,0 +1,73 @@ +/** + * External dependencies + */ +import styled from '@emotion/styled'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { ToolsPanel, ToolsPanelItem } from '../'; +import Panel from '../../panel'; +import UnitControl from '../../unit-control'; + +export default { + title: 'Components (Experimental)/ToolsPanel', + component: ToolsPanel, +}; + +export const _default = () => { + const [ height, setHeight ] = useState(); + const [ width, setWidth ] = useState(); + + const resetAll = () => { + setHeight( undefined ); + setWidth( undefined ); + }; + + return ( + + + + !! height } + label="Height" + onDeselect={ () => setHeight( undefined ) } + > + setHeight( next ) } + /> + + !! width } + label="Width" + onDeselect={ () => setWidth( undefined ) } + > + setWidth( next ) } + /> + + + + + ); +}; + +const PanelWrapperView = styled.div` + max-width: 250px; + font-size: 13px; +`; diff --git a/packages/components/src/tools-panel/styles.js b/packages/components/src/tools-panel/styles.js new file mode 100644 index 0000000000000..d9c7a985c011f --- /dev/null +++ b/packages/components/src/tools-panel/styles.js @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import { css } from '@emotion/react'; + +/** + * Internal dependencies + */ +import { COLORS, CONFIG } from '../utils'; +import { space } from '../ui/utils/space'; + +export const ToolsPanel = css` + border-top: ${ CONFIG.borderWidth } solid ${ COLORS.gray[ 200 ] }; + column-gap: ${ space( 4 ) }; + display: grid; + grid-template-columns: 1fr 1fr; + margin-top: -1px; + padding: ${ space( 4 ) }; + row-gap: ${ space( 6 ) }; +`; + +export const ToolsPanelHeader = css` + align-items: center; + display: flex; + font-size: inherit; + font-weight: 500; + grid-column: span 2; + justify-content: space-between; + line-height: normal; + + .components-tools-panel & { + margin: 0; + } + + .components-dropdown-menu { + margin-top: ${ space( -1 ) }; + margin-bottom: ${ space( -1 ) }; + height: ${ space( 6 ) }; + } + + .components-dropdown-menu__toggle { + padding: 0; + height: ${ space( 6 ) }; + min-width: ${ space( 6 ) }; + width: ${ space( 6 ) }; + } +`; + +export const ToolsPanelItem = css` + grid-column: span 2; + + &.single-column { + grid-column: span 1; + } + + /* Clear spacing in and around controls added as panel items. */ + /* Remove when they can be addressed via context system. */ + & > div, + & > fieldset { + padding-bottom: 0; + margin-bottom: 0; + max-width: 100%; + } +`; diff --git a/packages/components/src/tools-panel/test/index.js b/packages/components/src/tools-panel/test/index.js new file mode 100644 index 0000000000000..d371edabc8be4 --- /dev/null +++ b/packages/components/src/tools-panel/test/index.js @@ -0,0 +1,385 @@ +/** + * External dependencies + */ +import { render, screen, fireEvent } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import { ToolsPanel, ToolsPanelItem } from '../'; + +const resetAll = jest.fn(); + +// Default props for the tools panel. +const defaultProps = { + header: 'Panel header', + label: 'Display options', + resetAll, +}; + +// Default props for an enabled control to be rendered within panel. +const controlProps = { + attributes: { value: true }, + hasValue: jest.fn().mockImplementation( () => { + return !! controlProps.attributes.value; + } ), + label: 'Example', + onDeselect: jest.fn().mockImplementation( () => { + controlProps.attributes.value = undefined; + } ), + onSelect: jest.fn(), +}; + +// Default props without a value for an alternate control to be rendered within +// the panel. +const altControlProps = { + attributes: { value: false }, + hasValue: jest.fn().mockImplementation( () => { + return !! altControlProps.attributes.value; + } ), + label: 'Alt', + onDeselect: jest.fn(), + onSelect: jest.fn(), +}; + +// Default props for wrapped or grouped panel items. +const nestedControlProps = { + attributes: { value: true }, + hasValue: jest.fn().mockImplementation( () => { + return !! nestedControlProps.attributes.value; + } ), + label: 'Nested Control 1', + onDeselect: jest.fn().mockImplementation( () => { + nestedControlProps.attributes.value = undefined; + } ), + onSelect: jest.fn(), + isShownByDefault: true, +}; + +// Alternative props for wrapped or grouped panel items. +const altNestedControlProps = { + attributes: { value: false }, + hasValue: jest.fn().mockImplementation( () => { + return !! altNestedControlProps.attributes.value; + } ), + label: 'Nested Control 2', + onDeselect: jest.fn(), + onSelect: jest.fn(), +}; + +// Simple custom component grouping panel items. Used to test panel item +// registration and display when not an immediate child of `ToolsPanel`. +const GroupedItems = ( { + defaultGroupedProps = nestedControlProps, + altGroupedProps = altNestedControlProps, +} ) => { + return ( + <> + +
Grouped control
+
+ +
Alt grouped control
+
+ + ); +}; + +// Renders a tools panel including panel items that have been grouped within +// a custom component. +const renderGroupedItemsInPanel = () => { + return render( + + + + ); +}; + +// Custom component rendering a panel item within a wrapping element. Also used +// to test panel item registration and rendering. +const WrappedItem = ( { text, ...props } ) => { + return ( +
+ +
{ text }
+
+
+ ); +}; + +// Renders a `ToolsPanel` with single wrapped panel item via a custom component. +const renderWrappedItemInPanel = () => { + return render( + + + + + ); +}; + +// Attempts to find the tools panel via its CSS class. +const getPanel = ( container ) => + container.querySelector( '.components-tools-panel' ); + +// Renders a default tools panel including children that are +// not to be represented within the panel's menu. +const renderPanel = () => { + return render( + + { false &&
Hidden
} + +
Example control
+
+ +
Alt control
+
+ Visible +
+ ); +}; + +// Helper to find the menu button and simulate a user click. +const openDropdownMenu = () => { + const menuButton = screen.getByLabelText( defaultProps.label ); + fireEvent.click( menuButton ); +}; + +// Opens dropdown then selects the menu item by label before simulating a click. +const selectMenuItem = async ( label ) => { + openDropdownMenu(); + const menuItem = await screen.findByText( label ); + fireEvent.click( menuItem ); +}; + +describe( 'ToolsPanel', () => { + describe( 'basic rendering', () => { + it( 'should render panel', () => { + const { container } = renderPanel(); + + expect( getPanel( container ) ).toBeInTheDocument(); + } ); + + it( 'should render non panel item child', () => { + renderPanel(); + + const nonPanelItem = screen.queryByText( 'Visible' ); + + expect( nonPanelItem ).toBeInTheDocument(); + } ); + + it( 'should render panel item flagged as default control even without value', () => { + render( + + +
Example control
+
+ +
Alt control
+
+
+ ); + + const altControl = screen.getByText( 'Alt control' ); + + expect( altControl ).toBeInTheDocument(); + } ); + + it( 'should not render panel menu when there are no panel items', () => { + render( + + { false && ( + Should not show + ) } + { false && ( + Not shown either + ) } + Visible but insignificant + + ); + + const menu = screen.queryByLabelText( defaultProps.label ); + expect( menu ).not.toBeInTheDocument(); + } ); + + it( 'should render panel menu when at least one panel item', () => { + renderPanel(); + + const menuButton = screen.getByLabelText( defaultProps.label ); + expect( menuButton ).toBeInTheDocument(); + } ); + + it( 'should render reset all item in menu', async () => { + renderPanel(); + openDropdownMenu(); + + const resetAllItem = await screen.findByRole( 'menuitem' ); + + expect( resetAllItem ).toBeInTheDocument(); + } ); + + it( 'should render panel menu items correctly', async () => { + renderPanel(); + openDropdownMenu(); + + const menuItems = await screen.findAllByRole( 'menuitemcheckbox' ); + + expect( menuItems.length ).toEqual( 2 ); + expect( menuItems[ 0 ] ).toHaveAttribute( 'aria-checked', 'true' ); + expect( menuItems[ 1 ] ).toHaveAttribute( 'aria-checked', 'false' ); + } ); + + it( 'should render panel header', () => { + renderPanel(); + const header = screen.getByText( defaultProps.header ); + + expect( header ).toBeInTheDocument(); + } ); + } ); + + describe( 'conditional rendering of panel items', () => { + it( 'should render panel item when it has a value', () => { + renderPanel(); + + const exampleControl = screen.getByText( 'Example control' ); + const altControl = screen.queryByText( 'Alt control' ); + + expect( exampleControl ).toBeInTheDocument(); + expect( altControl ).not.toBeInTheDocument(); + } ); + + it( 'should render panel item when corresponding menu item is selected', async () => { + renderPanel(); + await selectMenuItem( altControlProps.label ); + const control = await screen.findByText( 'Alt control' ); + + expect( control ).toBeInTheDocument(); + } ); + + it( 'should prevent panel item rendering when toggled off via menu item', async () => { + renderPanel(); + await selectMenuItem( controlProps.label ); + const control = screen.queryByText( 'Example control' ); + + expect( control ).not.toBeInTheDocument(); + } ); + + it( 'should prevent shown by default item rendering when toggled off via menu item', async () => { + render( + + +
Default control
+
+
+ ); + + const control = screen.getByText( 'Default control' ); + + expect( control ).toBeInTheDocument(); + + await selectMenuItem( controlProps.label ); + const resetControl = screen.queryByText( 'Default control' ); + + expect( resetControl ).not.toBeInTheDocument(); + } ); + } ); + + describe( 'callbacks on menu item selection', () => { + beforeEach( () => { + jest.clearAllMocks(); + controlProps.attributes.value = true; + } ); + + it( 'should call onDeselect callback when menu item is toggled off', async () => { + renderPanel(); + await selectMenuItem( controlProps.label ); + + expect( controlProps.onSelect ).not.toHaveBeenCalled(); + expect( controlProps.onDeselect ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'should call onSelect callback when menu item is toggled on', async () => { + renderPanel(); + await selectMenuItem( altControlProps.label ); + + expect( altControlProps.onSelect ).toHaveBeenCalledTimes( 1 ); + expect( altControlProps.onDeselect ).not.toHaveBeenCalled(); + } ); + + it( 'should call resetAll callback when its menu item is selected', async () => { + renderPanel(); + await selectMenuItem( 'Reset all' ); + + expect( resetAll ).toHaveBeenCalledTimes( 1 ); + } ); + } ); + + describe( 'grouped panel items within custom components', () => { + it( 'should render grouped items correctly', () => { + renderGroupedItemsInPanel(); + + const defaultItem = screen.getByText( 'Grouped control' ); + const altItem = screen.queryByText( 'Alt grouped control' ); + + expect( defaultItem ).toBeInTheDocument(); + expect( altItem ).not.toBeInTheDocument(); + } ); + + it( 'should render grouped items within the menu', async () => { + renderGroupedItemsInPanel(); + openDropdownMenu(); + + const defaultItem = screen.getByText( 'Nested Control 1' ); + const defaultMenuItem = defaultItem.parentNode; + + const altItem = screen.getByText( 'Nested Control 2' ); + const altMenuItem = altItem.parentNode; + + expect( defaultItem ).toBeInTheDocument(); + expect( defaultMenuItem ).toHaveAttribute( 'aria-checked', 'true' ); + + expect( altItem ).toBeInTheDocument(); + expect( altMenuItem ).toHaveAttribute( 'aria-checked', 'false' ); + } ); + } ); + + describe( 'wrapped panel items within custom components', () => { + it( 'should render wrapped items correctly', () => { + const { container } = renderWrappedItemInPanel(); + + const wrappers = container.querySelectorAll( + '.wrapped-panel-item-container' + ); + const defaultItem = screen.getByText( 'Wrapped 1' ); + const altItem = screen.queryByText( 'Wrapped 2' ); + + // Both wrappers should be rendered but only the panel item + // displayed by default should be within the document. + expect( wrappers.length ).toEqual( 2 ); + expect( defaultItem ).toBeInTheDocument(); + expect( altItem ).not.toBeInTheDocument(); + } ); + + it( 'should render wrapped items within the menu', () => { + renderWrappedItemInPanel(); + openDropdownMenu(); + + const defaultItem = screen.getByText( 'Nested Control 1' ); + const defaultMenuItem = defaultItem.parentNode; + + const altItem = screen.getByText( 'Nested Control 2' ); + const altMenuItem = altItem.parentNode; + + expect( defaultItem ).toBeInTheDocument(); + expect( defaultMenuItem ).toHaveAttribute( 'aria-checked', 'true' ); + + expect( altItem ).toBeInTheDocument(); + expect( altMenuItem ).toHaveAttribute( 'aria-checked', 'false' ); + } ); + } ); +} ); diff --git a/packages/components/src/tools-panel/tools-panel-header/README.md b/packages/components/src/tools-panel/tools-panel-header/README.md new file mode 100644 index 0000000000000..da6840b8a2a1f --- /dev/null +++ b/packages/components/src/tools-panel/tools-panel-header/README.md @@ -0,0 +1,47 @@ +# ToolsPanelHeader + +
+This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. +
+
+ +This component renders a tools panel's header including a menu. + +## Usage + +This component is generated automatically by its parent +`ToolsPanel`. + +
+In general, this should not be used directly. +
+ +## Props + +### `header`: `string` + +Text to be displayed within the panel header. + +- 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 +for items contained within this header's panel. + +- Required: Yes + +### `toggleItem`: `function` + +This is executed when an individual control's menu item is toggled. It +will update the panel's menu item state and call the panel item's `onSelect` or +`onDeselect` callbacks as appropriate. + +- Required: Yes diff --git a/packages/components/src/tools-panel/tools-panel-header/component.js b/packages/components/src/tools-panel/tools-panel-header/component.js new file mode 100644 index 0000000000000..6fa9391e20e0e --- /dev/null +++ b/packages/components/src/tools-panel/tools-panel-header/component.js @@ -0,0 +1,81 @@ +/** + * WordPress dependencies + */ +import { check, moreHorizontal } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import DropdownMenu from '../../dropdown-menu'; +import MenuGroup from '../../menu-group'; +import MenuItem from '../../menu-item'; +import { useToolsPanelHeader } from './hook'; +import { contextConnect } from '../../ui/context'; + +const ToolsPanelHeader = ( props, forwardedRef ) => { + const { + hasMenuItems, + header, + menuItems, + menuLabel, + resetAll, + toggleItem, + ...headerProps + } = useToolsPanelHeader( props ); + + if ( ! header ) { + return null; + } + + return ( +

+ { header } + { hasMenuItems && ( + + { ( { onClose } ) => ( + <> + + { Object.entries( menuItems ).map( + ( [ label, isSelected ] ) => { + return ( + { + toggleItem( label ); + onClose(); + } } + role="menuitemcheckbox" + > + { label } + + ); + } + ) } + + + { + resetAll(); + onClose(); + } } + > + { __( 'Reset all' ) } + + + + ) } + + ) } +

+ ); +}; + +const ConnectedToolsPanelHeader = contextConnect( + ToolsPanelHeader, + 'ToolsPanelHeader' +); + +export default ConnectedToolsPanelHeader; diff --git a/packages/components/src/tools-panel/tools-panel-header/hook.js b/packages/components/src/tools-panel/tools-panel-header/hook.js new file mode 100644 index 0000000000000..a3c2644ba7475 --- /dev/null +++ b/packages/components/src/tools-panel/tools-panel-header/hook.js @@ -0,0 +1,34 @@ +/** + * WordPress dependencies + */ +import { useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import * as styles from '../styles'; +import { useToolsPanelContext } from '../context'; +import { useContextSystem } from '../../ui/context'; +import { useCx } from '../../utils/hooks/use-cx'; + +export function useToolsPanelHeader( props ) { + const { className, ...otherProps } = useContextSystem( + props, + 'ToolsPanelHeader' + ); + + const cx = useCx(); + const classes = useMemo( () => { + return cx( styles.ToolsPanelHeader, className ); + }, [ className ] ); + + const { menuItems } = useToolsPanelContext(); + const hasMenuItems = !! Object.entries( menuItems ).length; + + return { + ...otherProps, + hasMenuItems, + menuItems, + className: classes, + }; +} diff --git a/packages/components/src/tools-panel/tools-panel-header/index.js b/packages/components/src/tools-panel/tools-panel-header/index.js new file mode 100644 index 0000000000000..b404d7fd44a81 --- /dev/null +++ b/packages/components/src/tools-panel/tools-panel-header/index.js @@ -0,0 +1 @@ +export { default } from './component'; diff --git a/packages/components/src/tools-panel/tools-panel-item/README.md b/packages/components/src/tools-panel/tools-panel-item/README.md new file mode 100644 index 0000000000000..2384b32f7be41 --- /dev/null +++ b/packages/components/src/tools-panel/tools-panel-item/README.md @@ -0,0 +1,39 @@ +# ToolsPanelItem + +
+This feature is still experimental. “Experimental” means this is an early +implementation subject to drastic and breaking changes. +
+
+ +This component acts 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. + +## Usage + +See [`tools-panel/README.md#usage`](/packages/components/src/tools-panel/tools-panel/) +for how to use `ToolsPanelItem`. + +## Props + +### `isShownByDefault`: `boolean` + +This prop identifies the current item as being displayed by default. This means +it will show regardless of whether it has a value set or is toggled on in the +panel's menu. + +- Required: Yes + +### `label`: `string` + +The supplied label is dual purpose. +It is used as: +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 diff --git a/packages/components/src/tools-panel/tools-panel-item/component.js b/packages/components/src/tools-panel/tools-panel-item/component.js new file mode 100644 index 0000000000000..92cca8a626022 --- /dev/null +++ b/packages/components/src/tools-panel/tools-panel-item/component.js @@ -0,0 +1,31 @@ +/** + * Internal dependencies + */ +import { useToolsPanelItem } from './hook'; +import { View } from '../../view'; +import { contextConnect } from '../../ui/context'; + +// This wraps controls to be conditionally displayed within a tools panel. It +// prevents props being applied to HTML elements that would make them invalid. +const ToolsPanelItem = ( props, forwardedRef ) => { + const { children, isShown, ...toolsPanelItemProps } = useToolsPanelItem( + props + ); + + if ( ! isShown ) { + return null; + } + + return ( + + { children } + + ); +}; + +const ConnectedToolsPanelItem = contextConnect( + ToolsPanelItem, + 'ToolsPanelItem' +); + +export default ConnectedToolsPanelItem; diff --git a/packages/components/src/tools-panel/tools-panel-item/hook.js b/packages/components/src/tools-panel/tools-panel-item/hook.js new file mode 100644 index 0000000000000..fe9050ab91db9 --- /dev/null +++ b/packages/components/src/tools-panel/tools-panel-item/hook.js @@ -0,0 +1,67 @@ +/** + * WordPress dependencies + */ +import { usePrevious } from '@wordpress/compose'; +import { useEffect, useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import * as styles from '../styles'; +import { useToolsPanelContext } from '../context'; +import { useContextSystem } from '../../ui/context'; +import { useCx } from '../../utils/hooks/use-cx'; + +export function useToolsPanelItem( props ) { + const { + className, + hasValue, + isShownByDefault, + label, + onDeselect = () => undefined, + onSelect = () => undefined, + ...otherProps + } = useContextSystem( props, 'ToolsPanelItem' ); + + const cx = useCx(); + const classes = useMemo( () => { + return cx( styles.ToolsPanelItem, className ); + } ); + + const { menuItems, registerPanelItem } = 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, + } ); + }, [] ); + + const isValueSet = hasValue(); + + // Note: `label` is used as a key when building menu item state in + // `ToolsPanel`. + const isMenuItemChecked = menuItems[ label ]; + const wasMenuItemChecked = usePrevious( isMenuItemChecked ); + + // Determine if the panel item's corresponding menu is being toggled and + // trigger appropriate callback if it is. + useEffect( () => { + if ( isMenuItemChecked && ! isValueSet && ! wasMenuItemChecked ) { + onSelect(); + } + + if ( ! isMenuItemChecked && wasMenuItemChecked ) { + onDeselect(); + } + }, [ isMenuItemChecked, wasMenuItemChecked, isValueSet ] ); + + return { + ...otherProps, + isShown: isMenuItemChecked, + className: classes, + }; +} diff --git a/packages/components/src/tools-panel/tools-panel-item/index.js b/packages/components/src/tools-panel/tools-panel-item/index.js new file mode 100644 index 0000000000000..8b4b163fe0f22 --- /dev/null +++ b/packages/components/src/tools-panel/tools-panel-item/index.js @@ -0,0 +1,2 @@ +export { default } from './component'; +export { useToolsPanelItem } from './hook'; diff --git a/packages/components/src/tools-panel/tools-panel/README.md b/packages/components/src/tools-panel/tools-panel/README.md new file mode 100644 index 0000000000000..4424ef880af9b --- /dev/null +++ b/packages/components/src/tools-panel/tools-panel/README.md @@ -0,0 +1,91 @@ +# ToolsPanel + +
+This feature is still experimental. “Experimental” means this is an early +implementation subject to drastic and breaking changes. +
+
+These panels provide progressive discovery options for their children. For +example the controls provided via block supports. + +## Development guidelines + +The `ToolsPanel` creates a container with a header including a +dropdown menu. The menu is generated automatically from the panel's children +matching the `ToolsPanelItem` component type. + +Each menu item allows for the display of the corresponding child to be +toggled on or off. The control's `onSelect` and `onDeselect` callbacks are fired +allowing for greater control over the child e.g. resetting block attributes when +a block support control is toggled off. + +Whether a child control is initially displayed or not is dependent upon +if there has previously been a value set or the child has been flagged as +displaying by default through the `isShownByDefault` prop. Determining whether a +child has a value is done via the `hasValue` function provided through the +child's props. + +## Usage + +```jsx +import { + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +import { + PaddingEdit, + hasPaddingValue, + resetPadding, + useIsPaddingDisabled, +} from './padding'; + + +export function DimensionPanel( props ) { + const isPaddingDisabled = useIsPaddingDisabled( props ); + + const resetAll = () => { + // Reset attributes for all block support features in this panel. + }; + + return ( + + { ! isPaddingDisabled && ( + hasPaddingValue( props ) } + label={ __( 'Padding' ) } + onDeselect={ () => resetPadding( props ) } + > + + + ) } + + ); +} +``` + +## Props + +### `label`: `string` + +The label for the panel's dropdown menu. + +- Required: Yes + +### `resetAll`: `function` + +A function to call when the `Reset all` menu option is selected. This is passed +through to the panel's header component. + +- Required: Yes + +### `header`: `string` + +Text to be displayed within the panel's header. + +- Required: Yes diff --git a/packages/components/src/tools-panel/tools-panel/component.js b/packages/components/src/tools-panel/tools-panel/component.js new file mode 100644 index 0000000000000..99ac9baebec1f --- /dev/null +++ b/packages/components/src/tools-panel/tools-panel/component.js @@ -0,0 +1,38 @@ +/** + * Internal dependencies + */ +import ToolsPanelHeader from '../tools-panel-header'; +import { ToolsPanelContext } from '../context'; +import { useToolsPanel } from './hook'; +import { View } from '../../view'; +import { contextConnect } from '../../ui/context'; + +const ToolsPanel = ( props, forwardedRef ) => { + const { + children, + header, + label, + panelContext, + resetAllItems, + toggleItem, + ...toolsPanelProps + } = useToolsPanel( props ); + + return ( + + + + { children } + + + ); +}; + +const ConnectedToolsPanel = contextConnect( ToolsPanel, 'ToolsPanel' ); + +export default ConnectedToolsPanel; diff --git a/packages/components/src/tools-panel/tools-panel/hook.js b/packages/components/src/tools-panel/tools-panel/hook.js new file mode 100644 index 0000000000000..898d1c5e04333 --- /dev/null +++ b/packages/components/src/tools-panel/tools-panel/hook.js @@ -0,0 +1,79 @@ +/** + * WordPress dependencies + */ +import { useEffect, useMemo, useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import * as styles from '../styles'; +import { useContextSystem } from '../../ui/context'; +import { useCx } from '../../utils/hooks/use-cx'; + +export function useToolsPanel( props ) { + const { className, resetAll, ...otherProps } = useContextSystem( + props, + 'ToolsPanel' + ); + + const cx = useCx(); + const classes = useMemo( () => { + return cx( styles.ToolsPanel, className ); + }, [ className ] ); + + // Allow panel items to register themselves. + const [ panelItems, setPanelItems ] = useState( [] ); + + const registerPanelItem = ( item ) => { + setPanelItems( ( items ) => [ ...items, item ] ); + }; + + // Manage and share display state of menu items representing child controls. + const [ menuItems, setMenuItems ] = useState( {} ); + + // Setup menuItems state as panel items register themselves. + useEffect( () => { + const items = {}; + + panelItems.forEach( ( { hasValue, isShownByDefault, label } ) => { + items[ label ] = isShownByDefault || hasValue(); + } ); + + setMenuItems( items ); + }, [ panelItems ] ); + + // Toggle the checked state of a menu item which is then used to determine + // display of the item within the panel. + const toggleItem = ( label ) => { + setMenuItems( { + ...menuItems, + [ label ]: ! menuItems[ label ], + } ); + }; + + // Resets display of children and executes resetAll callback if available. + const resetAllItems = () => { + if ( typeof resetAll === 'function' ) { + resetAll(); + } + + // Turn off display of all non-default items. + const resetMenuItems = {}; + + panelItems.forEach( ( { label, isShownByDefault } ) => { + resetMenuItems[ label ] = !! isShownByDefault; + } ); + + setMenuItems( resetMenuItems ); + }; + + const panelContext = { menuItems, registerPanelItem }; + + return { + ...otherProps, + panelContext, + resetAllItems, + toggleItem, + className: classes, + }; +} diff --git a/packages/components/src/tools-panel/tools-panel/index.js b/packages/components/src/tools-panel/tools-panel/index.js new file mode 100644 index 0000000000000..a0c01e54d0691 --- /dev/null +++ b/packages/components/src/tools-panel/tools-panel/index.js @@ -0,0 +1,2 @@ +export { default } from './component'; +export { useToolsPanel } from './hook'; diff --git a/packages/edit-site/src/components/sidebar/spacing-panel.js b/packages/edit-site/src/components/sidebar/dimensions-panel.js similarity index 63% rename from packages/edit-site/src/components/sidebar/spacing-panel.js rename to packages/edit-site/src/components/sidebar/dimensions-panel.js index cf224ee63fc25..4f19f7e19cae6 100644 --- a/packages/edit-site/src/components/sidebar/spacing-panel.js +++ b/packages/edit-site/src/components/sidebar/dimensions-panel.js @@ -3,8 +3,9 @@ */ import { __ } from '@wordpress/i18n'; import { + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, __experimentalBoxControl as BoxControl, - PanelBody, __experimentalUseCustomUnits as useCustomUnits, } from '@wordpress/components'; import { __experimentalUseCustomSides as useCustomSides } from '@wordpress/block-editor'; @@ -14,7 +15,7 @@ import { __experimentalUseCustomSides as useCustomSides } from '@wordpress/block */ import { useSetting } from '../editor/utils'; -export function useHasSpacingPanel( context ) { +export function useHasDimensionsPanel( context ) { const hasPadding = useHasPadding( context ); const hasMargin = useHasMargin( context ); @@ -61,7 +62,7 @@ function splitStyleValue( value ) { return value; } -export default function SpacingPanel( { context, getStyle, setStyle } ) { +export default function DimensionsPanel( { context, getStyle, setStyle } ) { const { name } = context; const showPaddingControl = useHasPadding( context ); const showMarginControl = useHasMargin( context ); @@ -82,6 +83,9 @@ export default function SpacingPanel( { context, getStyle, setStyle } ) { const padding = filterValuesBySides( newPaddingValues, paddingSides ); setStyle( name, 'padding', padding ); }; + const resetPaddingValue = () => setPaddingValues( {} ); + const hasPaddingValue = () => + paddingValues && Object.keys( paddingValues ).length; const marginValues = splitStyleValue( getStyle( name, 'margin' ) ); const marginSides = useCustomSides( name, 'margin' ); @@ -90,27 +94,55 @@ export default function SpacingPanel( { context, getStyle, setStyle } ) { const margin = filterValuesBySides( newMarginValues, marginSides ); setStyle( name, 'margin', margin ); }; + const resetMarginValue = () => setMarginValues( {} ); + const hasMarginValue = () => + marginValues && Object.keys( marginValues ).length; + + const resetAll = () => { + resetPaddingValue(); + resetMarginValue(); + }; return ( - + { showPaddingControl && ( - + onDeselect={ resetPaddingValue } + isShownByDefault={ true } + > + + ) } { showMarginControl && ( - + onDeselect={ resetMarginValue } + isShownByDefault={ true } + > + + ) } - + ); } diff --git a/packages/edit-site/src/components/sidebar/global-styles-sidebar.js b/packages/edit-site/src/components/sidebar/global-styles-sidebar.js index 2186eb4edc318..a031917c71fde 100644 --- a/packages/edit-site/src/components/sidebar/global-styles-sidebar.js +++ b/packages/edit-site/src/components/sidebar/global-styles-sidebar.js @@ -25,7 +25,10 @@ import { } from './typography-panel'; import { default as BorderPanel, useHasBorderPanel } from './border-panel'; import { default as ColorPanel, useHasColorPanel } from './color-panel'; -import { default as SpacingPanel, useHasSpacingPanel } from './spacing-panel'; +import { + default as DimensionsPanel, + useHasDimensionsPanel, +} from './dimensions-panel'; function GlobalStylesPanel( { wrapperPanelTitle, @@ -38,9 +41,9 @@ function GlobalStylesPanel( { const hasBorderPanel = useHasBorderPanel( context ); const hasColorPanel = useHasColorPanel( context ); const hasTypographyPanel = useHasTypographyPanel( context ); - const hasSpacingPanel = useHasSpacingPanel( context ); + const hasDimensionsPanel = useHasDimensionsPanel( context ); - if ( ! hasColorPanel && ! hasTypographyPanel && ! hasSpacingPanel ) { + if ( ! hasColorPanel && ! hasTypographyPanel && ! hasDimensionsPanel ) { return null; } @@ -62,8 +65,8 @@ function GlobalStylesPanel( { setSetting={ setSetting } /> ) } - { hasSpacingPanel && ( -