diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index e3e9c9660c518..a215b14cd8008 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -129,6 +129,10 @@ const BlockInspectorSingleBlock = ( { ) } +
diff --git a/packages/block-editor/src/components/inspector-controls/block-support-tools-panel.js b/packages/block-editor/src/components/inspector-controls/block-support-tools-panel.js new file mode 100644 index 0000000000000..84db563a51acd --- /dev/null +++ b/packages/block-editor/src/components/inspector-controls/block-support-tools-panel.js @@ -0,0 +1,76 @@ +/** + * WordPress dependencies + */ +import { __experimentalToolsPanel as ToolsPanel } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { useDispatch, useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +import { cleanEmptyObject } from '../../hooks/utils'; + +const labels = { + border: { + label: __( 'Border options' ), + header: __( 'Border' ), + }, + color: { + label: __( 'Color options' ), + header: __( 'Color' ), + }, + dimensions: { + label: __( 'Dimensions options' ), + header: __( 'Dimensions' ), + }, + typography: { + label: __( 'Typography options' ), + header: __( 'Typography' ), + }, +}; + +export default function BlockSupportToolsPanel( { group, children } ) { + const { clientId, attributes } = useSelect( ( select ) => { + const { getBlockAttributes, getSelectedBlockClientId } = select( + blockEditorStore + ); + const selectedBlockClientId = getSelectedBlockClientId(); + + return { + clientId: selectedBlockClientId, + attributes: getBlockAttributes( selectedBlockClientId ), + }; + } ); + const { updateBlockAttributes } = useDispatch( blockEditorStore ); + + const resetAll = ( resetFilters = [] ) => { + const { style } = attributes; + let newAttributes = { style }; + + resetFilters.forEach( ( resetFilter ) => { + newAttributes = { + ...newAttributes, + ...resetFilter( newAttributes ), + }; + } ); + + // Enforce a cleaned style object. + newAttributes = { + ...newAttributes, + style: cleanEmptyObject( newAttributes.style ), + }; + + updateBlockAttributes( clientId, newAttributes ); + }; + + return ( + + { children } + + ); +} diff --git a/packages/block-editor/src/components/inspector-controls/groups.js b/packages/block-editor/src/components/inspector-controls/groups.js index 6021389ccf56b..441c3a7a4c9dc 100644 --- a/packages/block-editor/src/components/inspector-controls/groups.js +++ b/packages/block-editor/src/components/inspector-controls/groups.js @@ -6,11 +6,15 @@ import { createSlotFill } from '@wordpress/components'; const InspectorControlsDefault = createSlotFill( 'InspectorControls' ); const InspectorControlsBlock = createSlotFill( 'InspectorControlsBlock' ); const InspectorControlsAdvanced = createSlotFill( 'InspectorAdvancedControls' ); +const InspectorControlsDimensions = createSlotFill( + 'InspectorDimensionsControls' +); const groups = { default: InspectorControlsDefault, block: InspectorControlsBlock, advanced: InspectorControlsAdvanced, + dimensions: InspectorControlsDimensions, }; export default groups; diff --git a/packages/block-editor/src/components/inspector-controls/slot.js b/packages/block-editor/src/components/inspector-controls/slot.js index 8fe2a711b9e3e..8092f91875a34 100644 --- a/packages/block-editor/src/components/inspector-controls/slot.js +++ b/packages/block-editor/src/components/inspector-controls/slot.js @@ -7,8 +7,11 @@ import warning from '@wordpress/warning'; /** * Internal dependencies */ +import BlockSupportToolsPanel from './block-support-tools-panel'; import groups from './groups'; +const blockSupportGroups = [ 'border', 'color', 'dimensions', 'typography' ]; + export default function InspectorControlsSlot( { group = 'default', bubblesVirtually = true, @@ -26,5 +29,13 @@ export default function InspectorControlsSlot( { return null; } + if ( blockSupportGroups.includes( group ) ) { + return ( + + + + ); + } + return ; } diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index 697955670b497..b3b57272621b7 100644 --- a/packages/block-editor/src/hooks/dimensions.js +++ b/packages/block-editor/src/hooks/dimensions.js @@ -1,10 +1,7 @@ /** * WordPress dependencies */ -import { - __experimentalToolsPanel as ToolsPanel, - __experimentalToolsPanelItem as ToolsPanelItem, -} from '@wordpress/components'; +import { __experimentalToolsPanelItem as ToolsPanelItem } from '@wordpress/components'; import { Platform } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { getBlockSupport } from '@wordpress/blocks'; @@ -27,7 +24,6 @@ import { resetPadding, useIsPaddingDisabled, } from './padding'; -import { cleanEmptyObject } from './utils'; export const SPACING_SUPPORT_KEY = 'spacing'; export const ALL_SIDES = [ 'top', 'right', 'bottom', 'left' ]; @@ -55,50 +51,48 @@ export function DimensionsPanel( props ) { '__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 } - > - - - ) } - + + { ! isPaddingDisabled && ( + hasPaddingValue( props ) } + label={ __( 'Padding' ) } + onDeselect={ () => resetPadding( props ) } + resetAllFilter={ ( newAttributes ) => ( { + ...newAttributes, + style: { + ...newAttributes.style, + spacing: { + ...newAttributes.style?.spacing, + padding: undefined, + }, + }, + } ) } + isShownByDefault={ defaultSpacingControls?.padding } + > + + + ) } + { ! isMarginDisabled && ( + hasMarginValue( props ) } + label={ __( 'Margin' ) } + onDeselect={ () => resetMargin( props ) } + resetAllFilter={ ( newAttributes ) => ( { + ...newAttributes, + style: { + ...newAttributes.style, + spacing: { + ...newAttributes.style?.spacing, + margin: undefined, + }, + }, + } ) } + isShownByDefault={ defaultSpacingControls?.margin } + > + + + ) } ); } diff --git a/packages/components/src/tools-panel/test/index.js b/packages/components/src/tools-panel/test/index.js index d371edabc8be4..d18a95dc26cba 100644 --- a/packages/components/src/tools-panel/test/index.js +++ b/packages/components/src/tools-panel/test/index.js @@ -316,6 +316,10 @@ describe( 'ToolsPanel', () => { await selectMenuItem( 'Reset all' ); expect( resetAll ).toHaveBeenCalledTimes( 1 ); + expect( controlProps.onSelect ).not.toHaveBeenCalled(); + expect( controlProps.onDeselect ).not.toHaveBeenCalled(); + expect( altControlProps.onSelect ).not.toHaveBeenCalled(); + expect( altControlProps.onDeselect ).not.toHaveBeenCalled(); } ); } ); diff --git a/packages/components/src/tools-panel/tools-panel-item/hook.js b/packages/components/src/tools-panel/tools-panel-item/hook.js index fe9050ab91db9..194e4f6228cbb 100644 --- a/packages/components/src/tools-panel/tools-panel-item/hook.js +++ b/packages/components/src/tools-panel/tools-panel-item/hook.js @@ -18,6 +18,7 @@ export function useToolsPanelItem( props ) { hasValue, isShownByDefault, label, + resetAllFilter, onDeselect = () => undefined, onSelect = () => undefined, ...otherProps @@ -28,7 +29,12 @@ export function useToolsPanelItem( props ) { return cx( styles.ToolsPanelItem, className ); } ); - const { menuItems, registerPanelItem } = useToolsPanelContext(); + const { + menuItems, + registerPanelItem, + deregisterPanelItem, + isResetting, + } = useToolsPanelContext(); // Registering the panel item allows the panel to include it in its // automatically generated menu and determine its initial checked status. @@ -37,7 +43,10 @@ export function useToolsPanelItem( props ) { hasValue, isShownByDefault, label, + resetAllFilter, } ); + + return () => deregisterPanelItem( label ); }, [] ); const isValueSet = hasValue(); @@ -50,6 +59,10 @@ export function useToolsPanelItem( props ) { // Determine if the panel item's corresponding menu is being toggled and // trigger appropriate callback if it is. useEffect( () => { + if ( isResetting ) { + return; + } + if ( isMenuItemChecked && ! isValueSet && ! wasMenuItemChecked ) { onSelect(); } diff --git a/packages/components/src/tools-panel/tools-panel/hook.js b/packages/components/src/tools-panel/tools-panel/hook.js index 898d1c5e04333..57b41b6c8cc1f 100644 --- a/packages/components/src/tools-panel/tools-panel/hook.js +++ b/packages/components/src/tools-panel/tools-panel/hook.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useEffect, useMemo, useState } from '@wordpress/element'; +import { useEffect, useMemo, useRef, useState } from '@wordpress/element'; /** * Internal dependencies @@ -21,6 +21,14 @@ export function useToolsPanel( props ) { return cx( styles.ToolsPanel, className ); }, [ className ] ); + const isResetting = useRef( false ); + + useEffect( () => { + if ( isResetting.current ) { + isResetting.current = false; + } + } ); + // Allow panel items to register themselves. const [ panelItems, setPanelItems ] = useState( [] ); @@ -28,9 +36,29 @@ export function useToolsPanel( props ) { setPanelItems( ( items ) => [ ...items, item ] ); }; + // Panels need to deregister on unmount to avoid orphans in menu state. + // This is an issue when panel items are being injected via SlotFills. + const deregisterPanelItem = ( label ) => { + setPanelItems( ( items ) => + items.filter( ( item ) => item.label !== label ) + ); + }; + // Manage and share display state of menu items representing child controls. const [ menuItems, setMenuItems ] = useState( {} ); + const getResetAllFilters = () => { + const filters = []; + + panelItems.forEach( ( item ) => { + if ( item.resetAllFilter ) { + filters.push( item.resetAllFilter ); + } + } ); + + return filters; + }; + // Setup menuItems state as panel items register themselves. useEffect( () => { const items = {}; @@ -54,7 +82,8 @@ export function useToolsPanel( props ) { // Resets display of children and executes resetAll callback if available. const resetAllItems = () => { if ( typeof resetAll === 'function' ) { - resetAll(); + isResetting.current = true; + resetAll( getResetAllFilters() ); } // Turn off display of all non-default items. @@ -67,7 +96,12 @@ export function useToolsPanel( props ) { setMenuItems( resetMenuItems ); }; - const panelContext = { menuItems, registerPanelItem }; + const panelContext = { + menuItems, + registerPanelItem, + deregisterPanelItem, + isResetting: isResetting.current, + }; return { ...otherProps,