diff --git a/packages/block-editor/src/components/global-styles/dimensions-panel.js b/packages/block-editor/src/components/global-styles/dimensions-panel.js index 4d493bdaacdf9f..fbd92f88ec07e3 100644 --- a/packages/block-editor/src/components/global-styles/dimensions-panel.js +++ b/packages/block-editor/src/components/global-styles/dimensions-panel.js @@ -23,7 +23,7 @@ import { useCallback, Platform } from '@wordpress/element'; /** * Internal dependencies */ -import { getValueFromVariable } from './utils'; +import { getValueFromVariable, normalizeFalsyValue } from './utils'; import SpacingSizesControl from '../spacing-sizes-control'; import HeightControl from '../height-control'; import ChildLayoutControl from '../child-layout-control'; @@ -226,7 +226,11 @@ export default function DimensionsPanel( { const contentSizeValue = decodeValue( inheritedValue?.layout?.contentSize ); const setContentSizeValue = ( newValue ) => { onChange( - immutableSet( value, [ 'layout', 'contentSize' ], newValue ) + immutableSet( + value, + [ 'layout', 'contentSize' ], + normalizeFalsyValue( newValue ) + ) ); }; const hasUserSetContentSizeValue = () => !! value?.layout?.contentSize; @@ -237,7 +241,13 @@ export default function DimensionsPanel( { useHasWideSize( settings ) && includeLayoutControls; const wideSizeValue = decodeValue( inheritedValue?.layout?.wideSize ); const setWideSizeValue = ( newValue ) => { - onChange( immutableSet( value, [ 'layout', 'wideSize' ], newValue ) ); + onChange( + immutableSet( + value, + [ 'layout', 'wideSize' ], + normalizeFalsyValue( newValue ) + ) + ); }; const hasUserSetWideSizeValue = () => !! value?.layout?.wideSize; const resetWideSizeValue = () => setWideSizeValue( undefined ); diff --git a/packages/block-editor/src/components/global-styles/typography-panel.js b/packages/block-editor/src/components/global-styles/typography-panel.js index cdaa6826ddd431..4ba3d777cb2b94 100644 --- a/packages/block-editor/src/components/global-styles/typography-panel.js +++ b/packages/block-editor/src/components/global-styles/typography-panel.js @@ -19,7 +19,7 @@ import LineHeightControl from '../line-height-control'; import LetterSpacingControl from '../letter-spacing-control'; import TextTransformControl from '../text-transform-control'; import TextDecorationControl from '../text-decoration-control'; -import { getValueFromVariable } from './utils'; +import { getValueFromVariable, normalizeFalsyValue } from './utils'; import { immutableSet } from '../../utils/object'; const MIN_TEXT_COLUMNS = 1; @@ -166,7 +166,9 @@ export default function TypographyPanel( { immutableSet( value, [ 'typography', 'fontFamily' ], - slug ? `var:preset|font-family|${ slug }` : newValue + slug + ? `var:preset|font-family|${ slug }` + : normalizeFalsyValue( newValue ) ) ); }; @@ -188,7 +190,11 @@ export default function TypographyPanel( { : newValue; onChange( - immutableSet( value, [ 'typography', 'fontSize' ], actualValue ) + immutableSet( + value, + [ 'typography', 'fontSize' ], + normalizeFalsyValue( actualValue ) + ) ); }; const hasFontSize = () => !! value?.typography?.fontSize; @@ -209,8 +215,8 @@ export default function TypographyPanel( { ...value, typography: { ...value?.typography, - fontStyle: newFontStyle, - fontWeight: newFontWeight, + fontStyle: normalizeFalsyValue( newFontStyle ), + fontWeight: normalizeFalsyValue( newFontWeight ), }, } ); }; @@ -225,7 +231,11 @@ export default function TypographyPanel( { const lineHeight = decodeValue( inheritedValue?.typography?.lineHeight ); const setLineHeight = ( newValue ) => { onChange( - immutableSet( value, [ 'typography', 'lineHeight' ], newValue ) + immutableSet( + value, + [ 'typography', 'lineHeight' ], + normalizeFalsyValue( newValue ) + ) ); }; const hasLineHeight = () => !! value?.typography?.lineHeight; @@ -238,7 +248,11 @@ export default function TypographyPanel( { ); const setLetterSpacing = ( newValue ) => { onChange( - immutableSet( value, [ 'typography', 'letterSpacing' ], newValue ) + immutableSet( + value, + [ 'typography', 'letterSpacing' ], + normalizeFalsyValue( newValue ) + ) ); }; const hasLetterSpacing = () => !! value?.typography?.letterSpacing; @@ -249,7 +263,11 @@ export default function TypographyPanel( { const textColumns = decodeValue( inheritedValue?.typography?.textColumns ); const setTextColumns = ( newValue ) => { onChange( - immutableSet( value, [ 'typography', 'textColumns' ], newValue ) + immutableSet( + value, + [ 'typography', 'textColumns' ], + normalizeFalsyValue( newValue ) + ) ); }; const hasTextColumns = () => !! value?.typography?.textColumns; @@ -262,7 +280,11 @@ export default function TypographyPanel( { ); const setTextTransform = ( newValue ) => { onChange( - immutableSet( value, [ 'typography', 'textTransform' ], newValue ) + immutableSet( + value, + [ 'typography', 'textTransform' ], + normalizeFalsyValue( newValue ) + ) ); }; const hasTextTransform = () => !! value?.typography?.textTransform; @@ -275,7 +297,11 @@ export default function TypographyPanel( { ); const setTextDecoration = ( newValue ) => { onChange( - immutableSet( value, [ 'typography', 'textDecoration' ], newValue ) + immutableSet( + value, + [ 'typography', 'textDecoration' ], + normalizeFalsyValue( newValue ) + ) ); }; const hasTextDecoration = () => !! value?.typography?.textDecoration; diff --git a/packages/block-editor/src/components/global-styles/utils.js b/packages/block-editor/src/components/global-styles/utils.js index d1abd0a57dbc2d..92b1c76ca62c0f 100644 --- a/packages/block-editor/src/components/global-styles/utils.js +++ b/packages/block-editor/src/components/global-styles/utils.js @@ -376,3 +376,16 @@ export function scopeSelector( scope, selector ) { return selectorsScoped.join( ', ' ); } + +/** + * Some Global Styles UI components (font size for instance) relies on the fact that emptying inputs + * (empty strings) should fallback to the parent value (theme.json value). + * Ideally, there should be a dedicated UI element to "revert to theme" for each input instead. + * But until we do, this function is used to transform falsy values to undefined values for these components + * which allows the global styles merge algorithm to revert to the theme.json value. + * + * @param {*} value Value to normalize. + * + * @return {undefined|*} normalized value. + */ +export const normalizeFalsyValue = ( value ) => value || undefined; diff --git a/packages/block-editor/src/hooks/test/anchor.js b/packages/block-editor/src/hooks/test/anchor.js new file mode 100644 index 00000000000000..a919fad575312e --- /dev/null +++ b/packages/block-editor/src/hooks/test/anchor.js @@ -0,0 +1,113 @@ +/** + * WordPress dependencies + */ +import { applyFilters } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import '../anchor'; + +const noop = () => {}; + +describe( 'anchor', () => { + const blockSettings = { + save: noop, + category: 'text', + title: 'block title', + }; + + describe( 'addAttribute()', () => { + const registerBlockType = applyFilters.bind( + null, + 'blocks.registerBlockType' + ); + + it( 'should do nothing if the block settings do not define anchor support', () => { + const settings = registerBlockType( blockSettings ); + + expect( settings.attributes ).toBe( undefined ); + } ); + + it( 'should assign a new anchor attribute', () => { + const settings = registerBlockType( { + ...blockSettings, + supports: { + anchor: true, + }, + } ); + + expect( settings.attributes ).toHaveProperty( 'anchor' ); + } ); + + it( 'should not override attributes defined in settings', () => { + const settings = registerBlockType( { + ...blockSettings, + supports: { + anchor: true, + }, + attributes: { + anchor: { + type: 'string', + default: 'testAnchor', + }, + }, + } ); + + expect( settings.attributes.anchor ).toEqual( { + type: 'string', + default: 'testAnchor', + } ); + } ); + } ); + + describe( 'addSaveProps', () => { + const getSaveContentExtraProps = applyFilters.bind( + null, + 'blocks.getSaveContent.extraProps' + ); + + it( 'should do nothing if the block settings do not define anchor support', () => { + const attributes = { anchor: 'foo' }; + const extraProps = getSaveContentExtraProps( + {}, + blockSettings, + attributes + ); + + expect( extraProps ).not.toHaveProperty( 'id' ); + } ); + + it( 'should inject anchor attribute ID', () => { + const attributes = { anchor: 'foo' }; + const extraProps = getSaveContentExtraProps( + {}, + { + ...blockSettings, + supports: { + anchor: true, + }, + }, + attributes + ); + + expect( extraProps.id ).toBe( 'foo' ); + } ); + + it( 'should remove an anchor attribute ID when field is cleared', () => { + const attributes = { anchor: '' }; + const extraProps = getSaveContentExtraProps( + {}, + { + ...blockSettings, + supports: { + anchor: true, + }, + }, + attributes + ); + + expect( extraProps.id ).toBe( null ); + } ); + } ); +} ); diff --git a/packages/block-editor/src/hooks/test/color.js b/packages/block-editor/src/hooks/test/color.js index 29710d30a9c390..179ad21f913df4 100644 --- a/packages/block-editor/src/hooks/test/color.js +++ b/packages/block-editor/src/hooks/test/color.js @@ -12,17 +12,8 @@ import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; * Internal dependencies */ import BlockEditorProvider from '../../components/provider'; -import { cleanEmptyObject } from '../utils'; import { withColorPaletteStyles } from '../color'; -describe( 'cleanEmptyObject', () => { - it( 'should remove nested keys', () => { - expect( cleanEmptyObject( { color: { text: undefined } } ) ).toEqual( - undefined - ); - } ); -} ); - describe( 'withColorPaletteStyles', () => { const settings = { __experimentalFeatures: { diff --git a/packages/block-editor/src/hooks/test/utils.js b/packages/block-editor/src/hooks/test/utils.js index a919fad575312e..801ed0878df47b 100644 --- a/packages/block-editor/src/hooks/test/utils.js +++ b/packages/block-editor/src/hooks/test/utils.js @@ -1,113 +1,32 @@ -/** - * WordPress dependencies - */ -import { applyFilters } from '@wordpress/hooks'; - /** * Internal dependencies */ -import '../anchor'; +import { cleanEmptyObject } from '../utils'; -const noop = () => {}; - -describe( 'anchor', () => { - const blockSettings = { - save: noop, - category: 'text', - title: 'block title', - }; - - describe( 'addAttribute()', () => { - const registerBlockType = applyFilters.bind( - null, - 'blocks.registerBlockType' +describe( 'cleanEmptyObject', () => { + it( 'should remove nested keys', () => { + expect( cleanEmptyObject( { color: { text: undefined } } ) ).toEqual( + undefined ); + } ); - it( 'should do nothing if the block settings do not define anchor support', () => { - const settings = registerBlockType( blockSettings ); - - expect( settings.attributes ).toBe( undefined ); - } ); - - it( 'should assign a new anchor attribute', () => { - const settings = registerBlockType( { - ...blockSettings, - supports: { - anchor: true, - }, - } ); - - expect( settings.attributes ).toHaveProperty( 'anchor' ); - } ); - - it( 'should not override attributes defined in settings', () => { - const settings = registerBlockType( { - ...blockSettings, - supports: { - anchor: true, - }, - attributes: { - anchor: { - type: 'string', - default: 'testAnchor', - }, - }, - } ); - - expect( settings.attributes.anchor ).toEqual( { - type: 'string', - default: 'testAnchor', - } ); + it( 'should remove partial nested keys', () => { + expect( + cleanEmptyObject( { + color: { text: undefined }, + typography: { fontSize: '10px' }, + } ) + ).toEqual( { + typography: { fontSize: '10px' }, } ); } ); - describe( 'addSaveProps', () => { - const getSaveContentExtraProps = applyFilters.bind( - null, - 'blocks.getSaveContent.extraProps' + it( 'should not remove falsy nested keys', () => { + expect( cleanEmptyObject( { color: { text: false } } ) ).not.toEqual( + undefined + ); + expect( cleanEmptyObject( { color: { text: '' } } ) ).not.toEqual( + undefined ); - - it( 'should do nothing if the block settings do not define anchor support', () => { - const attributes = { anchor: 'foo' }; - const extraProps = getSaveContentExtraProps( - {}, - blockSettings, - attributes - ); - - expect( extraProps ).not.toHaveProperty( 'id' ); - } ); - - it( 'should inject anchor attribute ID', () => { - const attributes = { anchor: 'foo' }; - const extraProps = getSaveContentExtraProps( - {}, - { - ...blockSettings, - supports: { - anchor: true, - }, - }, - attributes - ); - - expect( extraProps.id ).toBe( 'foo' ); - } ); - - it( 'should remove an anchor attribute ID when field is cleared', () => { - const attributes = { anchor: '' }; - const extraProps = getSaveContentExtraProps( - {}, - { - ...blockSettings, - supports: { - anchor: true, - }, - }, - attributes - ); - - expect( extraProps.id ).toBe( null ); - } ); } ); } ); diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index 6db0bad2eb64f0..1a4e0726033baf 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -33,7 +33,7 @@ export const cleanEmptyObject = ( object ) => { const cleanedNestedObjects = Object.fromEntries( Object.entries( object ) .map( ( [ key, value ] ) => [ key, cleanEmptyObject( value ) ] ) - .filter( ( [ , value ] ) => Boolean( value ) ) + .filter( ( [ , value ] ) => value !== undefined ) ); return isEmpty( cleanedNestedObjects ) ? undefined : cleanedNestedObjects; }; diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index 95681bb1943538..0b7e8b2511aa6b 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -11,6 +11,7 @@ import { ComposedPrivateInserter as PrivateInserter } from './components/inserte import { PrivateListView } from './components/list-view'; import BlockInfo from './components/block-info-slot-fill'; import { useShouldContextualToolbarShow } from './utils/use-should-contextual-toolbar-show'; +import { cleanEmptyObject } from './hooks/utils'; /** * Private @wordpress/block-editor APIs. @@ -26,4 +27,5 @@ lock( privateApis, { ResizableBoxPopover, BlockInfo, useShouldContextualToolbarShow, + cleanEmptyObject, } ); diff --git a/packages/block-library/src/cover/transforms.js b/packages/block-library/src/cover/transforms.js index ce02e91518b4ec..adf7bfe0997e3d 100644 --- a/packages/block-library/src/cover/transforms.js +++ b/packages/block-library/src/cover/transforms.js @@ -2,12 +2,15 @@ * WordPress dependencies */ import { createBlock } from '@wordpress/blocks'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; /** * Internal dependencies */ import { IMAGE_BACKGROUND_TYPE, VIDEO_BACKGROUND_TYPE } from './shared'; -import cleanEmptyObject from '../utils/clean-empty-object'; +import { unlock } from '../private-apis'; + +const { cleanEmptyObject } = unlock( blockEditorPrivateApis ); const transforms = { from: [ diff --git a/packages/block-library/src/query/deprecated.js b/packages/block-library/src/query/deprecated.js index 89a8c38b204dce..05ee6217a288d6 100644 --- a/packages/block-library/src/query/deprecated.js +++ b/packages/block-library/src/query/deprecated.js @@ -6,12 +6,15 @@ import { InnerBlocks, useInnerBlocksProps, useBlockProps, + privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; /** * Internal dependencies */ -import cleanEmptyObject from '../utils/clean-empty-object'; +import { unlock } from '../private-apis'; + +const { cleanEmptyObject } = unlock( blockEditorPrivateApis ); const migrateToTaxQuery = ( attributes ) => { const { query } = attributes; diff --git a/packages/block-library/src/utils/clean-empty-object.js b/packages/block-library/src/utils/clean-empty-object.js deleted file mode 100644 index e0e55c659bf575..00000000000000 --- a/packages/block-library/src/utils/clean-empty-object.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * External dependencies - */ -import { isEmpty } from 'lodash'; - -/** - * Removed empty nodes from nested objects. - * - * @param {Object} object - * @return {Object} Object cleaned from empty nodes. - */ -const cleanEmptyObject = ( object ) => { - if ( - object === null || - typeof object !== 'object' || - Array.isArray( object ) - ) { - return object; - } - const cleanedNestedObjects = Object.fromEntries( - Object.entries( object ) - .map( ( [ key, value ] ) => [ key, cleanEmptyObject( value ) ] ) - .filter( ( [ , value ] ) => Boolean( value ) ) - ); - return isEmpty( cleanedNestedObjects ) ? undefined : cleanedNestedObjects; -}; - -export default cleanEmptyObject; diff --git a/packages/block-library/src/utils/migrate-font-family.js b/packages/block-library/src/utils/migrate-font-family.js index cd15c74802ad02..7edec513e94a95 100644 --- a/packages/block-library/src/utils/migrate-font-family.js +++ b/packages/block-library/src/utils/migrate-font-family.js @@ -1,7 +1,14 @@ +/** + * WordPress dependencies + */ +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; + /** * Internal dependencies */ -import cleanEmptyObject from './clean-empty-object'; +import { unlock } from '../private-apis'; + +const { cleanEmptyObject } = unlock( blockEditorPrivateApis ); /** * Migrates the current style.typography.fontFamily attribute, diff --git a/packages/edit-site/src/components/global-styles/global-styles-provider.js b/packages/edit-site/src/components/global-styles/global-styles-provider.js index b194873916a9fc..7ddd518020569a 100644 --- a/packages/edit-site/src/components/global-styles/global-styles-provider.js +++ b/packages/edit-site/src/components/global-styles/global-styles-provider.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { mergeWith, isEmpty } from 'lodash'; +import { mergeWith } from 'lodash'; /** * WordPress dependencies @@ -17,7 +17,9 @@ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import CanvasSpinner from '../canvas-spinner'; import { unlock } from '../../private-apis'; -const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); +const { GlobalStylesContext, cleanEmptyObject } = unlock( + blockEditorPrivateApis +); function mergeTreesCustomizer( _, srcValue ) { // We only pass as arrays the presets, @@ -32,22 +34,6 @@ export function mergeBaseAndUserConfigs( base, user ) { return mergeWith( {}, base, user, mergeTreesCustomizer ); } -const cleanEmptyObject = ( object ) => { - if ( - object === null || - typeof object !== 'object' || - Array.isArray( object ) - ) { - return object; - } - const cleanedNestedObjects = Object.fromEntries( - Object.entries( object ) - .map( ( [ key, value ] ) => [ key, cleanEmptyObject( value ) ] ) - .filter( ( [ , value ] ) => Boolean( value ) ) - ); - return isEmpty( cleanedNestedObjects ) ? undefined : cleanedNestedObjects; -}; - function useGlobalStylesUserConfig() { const { globalStylesId, isReady, settings, styles } = useSelect( ( select ) => {