diff --git a/docs/manifest.json b/docs/manifest.json
index 654c6037674e94..5099d81ab31f6b 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -1265,6 +1265,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 592949f0473b86..b51682a34e0161 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 d6cf4449b2168b..3e9292efbf57be 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 00000000000000..8351676640b177
--- /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 c0e5c1b5f8bb85..e8a976277f9702 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 c0cb5c5d4ef5d9..7e89049b40846b 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 6a29ad7fd7f001..e481946e3defc0 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 f356a211248f79..00000000000000
--- 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 8c5d6ce1872f97..87bf4685d50d42 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 86d40d1db38fb9..d155278d854706 100644
--- a/packages/components/src/index.js
+++ b/packages/components/src/index.js
@@ -133,6 +133,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 00000000000000..e74e08708db9f9
--- /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 00000000000000..e557e232330b5e
--- /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 00000000000000..3f40051135857a
--- /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 00000000000000..d9c7a985c011fc
--- /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 00000000000000..d371edabc8be4b
--- /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 (
+
+ );
+};
+
+// 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 00000000000000..da6840b8a2a1fa
--- /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 00000000000000..6fa9391e20e0e2
--- /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 (
+
+ );
+ }
+ ) }
+
+
+
+
+ >
+ ) }
+
+ ) }
+
+ );
+};
+
+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 00000000000000..a3c2644ba7475e
--- /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 00000000000000..b404d7fd44a81a
--- /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 00000000000000..2384b32f7be41e
--- /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 00000000000000..92cca8a626022f
--- /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 00000000000000..fe9050ab91db91
--- /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 00000000000000..8b4b163fe0f222
--- /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 00000000000000..4424ef880af9b8
--- /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 00000000000000..99ac9baebec1fa
--- /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 00000000000000..898d1c5e043336
--- /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 00000000000000..a0c01e54d06911
--- /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 cf224ee63fc25d..4f19f7e19cae6d 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 2186eb4edc3187..a031917c71fde5 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 && (
-