diff --git a/packages/block-editor/src/components/spacing-sizes-control/spacing-input-control.js b/packages/block-editor/src/components/spacing-sizes-control/spacing-input-control.js index 2a9ed938e043c4..c0d2573b42e807 100644 --- a/packages/block-editor/src/components/spacing-sizes-control/spacing-input-control.js +++ b/packages/block-editor/src/components/spacing-sizes-control/spacing-input-control.js @@ -30,6 +30,7 @@ import { LABELS, getSliderValueFromPreset, getCustomValueFromPreset, + getPresetValueFromCustomValue, isValueSpacingPreset, } from './utils'; @@ -42,6 +43,9 @@ export default function SpacingInputControl( { type, minimumCustomValue, } ) { + // Treat value as a preset value if the passed in value matches the value of one of the spacingSizes. + value = getPresetValueFromCustomValue( value, spacingSizes ); + let selectListSizes = spacingSizes; const showRangeControl = spacingSizes.length <= 8; @@ -216,6 +220,7 @@ export default function SpacingInputControl( { label={ ariaLabel } hideLabelFromVision={ true } className="components-spacing-sizes-control__custom-value-input" + style={ { gridColumn: '1' } } /> { } ); } ); +describe( 'getPresetValueFromCustomValue', () => { + const spacingSizes = [ { name: 'Small', slug: 20, size: '8px' } ]; + it( 'should return original value if a string in spacing presets var format', () => { + expect( + getPresetValueFromCustomValue( + 'var:preset|spacing|80', + spacingSizes + ) + ).toBe( 'var:preset|spacing|80' ); + } ); + it( 'should return value constructed from matching spacingSizes array entry if value matches sizes', () => { + expect( getPresetValueFromCustomValue( '8px', spacingSizes ) ).toBe( + 'var:preset|spacing|20' + ); + } ); + it( 'should return values as-is if no matching preset in spacingSizes array', () => { + expect( + getPresetValueFromCustomValue( '1.125rem', spacingSizes ) + ).toBe( '1.125rem' ); + } ); +} ); + describe( 'getSpacingPresetCssVar', () => { it( 'should return original value if not a string in spacing presets var format', () => { expect( getSpacingPresetCssVar( '20px' ) ).toBe( '20px' ); @@ -144,6 +167,9 @@ describe( 'isValuesDefined', () => { it( 'should return false if values are not defined', () => { expect( isValuesDefined( undefinedValues ) ).toBe( false ); } ); + it( 'should return false if values is passed in as null', () => { + expect( isValuesDefined( null ) ).toBe( false ); + } ); const definedValues = { top: 'var:preset|spacing|30', bottom: 'var:preset|spacing|20', diff --git a/packages/block-editor/src/components/spacing-sizes-control/utils.js b/packages/block-editor/src/components/spacing-sizes-control/utils.js index 2f824e25cacf67..8236d743ab32f2 100644 --- a/packages/block-editor/src/components/spacing-sizes-control/utils.js +++ b/packages/block-editor/src/components/spacing-sizes-control/utils.js @@ -43,6 +43,33 @@ export function getCustomValueFromPreset( value, spacingSizes ) { return spacingSize?.size; } +/** + * Converts a custom value to preset value if one can be found. + * + * Returns value as-is if no match is found. + * + * @param {string} value Value to convert + * @param {Array} spacingSizes Array of the current spacing preset objects + * + * @return {string} The preset value if it can be found. + */ +export function getPresetValueFromCustomValue( value, spacingSizes ) { + // Return value as-is if it is already a preset; + if ( isValueSpacingPreset( value ) ) { + return value; + } + + const spacingMatch = spacingSizes.find( + ( size ) => String( size.size ) === String( value ) + ); + + if ( spacingMatch?.slug ) { + return `var:preset|spacing|${ spacingMatch.slug }`; + } + + return value; +} + /** * Converts a spacing preset into a custom value. * @@ -181,15 +208,15 @@ export function isValuesMixed( values = {}, sides = ALL_SIDES ) { * @return {boolean} Whether values are defined. */ export function isValuesDefined( values ) { - return ( - values !== undefined && - ! isEmpty( - Object.values( values ).filter( - // Switching units when input is empty causes values only - // containing units. This gives false positive on mixed values - // unless filtered. - ( value ) => !! value && /\d/.test( value ) - ) + if ( values === undefined || values === null ) { + return false; + } + return ! isEmpty( + Object.values( values ).filter( + // Switching units when input is empty causes values only + // containing units. This gives false positive on mixed values + // unless filtered. + ( value ) => !! value && /\d/.test( value ) ) ); } diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index 6140e756c1d799..3eded40fc4a0ae 100644 --- a/packages/block-editor/src/hooks/dimensions.js +++ b/packages/block-editor/src/hooks/dimensions.js @@ -79,15 +79,16 @@ export function DimensionsPanel( props ) { }, } ); + const spacingClassnames = classnames( { + 'tools-panel-item-spacing': spacingSizes && spacingSizes.length > 0, + } ); + return ( <> { ! isPaddingDisabled && ( 0, - } ) } + className={ spacingClassnames } hasValue={ () => hasPaddingValue( props ) } label={ __( 'Padding' ) } onDeselect={ () => resetPadding( props ) } @@ -100,10 +101,7 @@ export function DimensionsPanel( props ) { ) } { ! isMarginDisabled && ( 0, - } ) } + className={ spacingClassnames } hasValue={ () => hasMarginValue( props ) } label={ __( 'Margin' ) } onDeselect={ () => resetMargin( props ) } @@ -116,6 +114,7 @@ export function DimensionsPanel( props ) { ) } { ! isGapDisabled && ( hasGapValue( props ) } label={ __( 'Block spacing' ) } onDeselect={ () => resetGap( props ) } diff --git a/packages/block-editor/src/hooks/gap.js b/packages/block-editor/src/hooks/gap.js index 9bb30261cd3150..018872eb840329 100644 --- a/packages/block-editor/src/hooks/gap.js +++ b/packages/block-editor/src/hooks/gap.js @@ -15,6 +15,7 @@ import { */ import { __unstableUseBlockRef as useBlockRef } from '../components/block-list/use-block-props/use-block-refs'; import { getSpacingPresetCssVar } from '../components/spacing-sizes-control/utils'; +import SpacingSizesControl from '../components/spacing-sizes-control'; import useSetting from '../components/use-setting'; import { AXIAL_SIDES, SPACING_SUPPORT_KEY, useCustomSides } from './dimensions'; import { cleanEmptyObject } from './utils'; @@ -55,12 +56,8 @@ export function getGapBoxControlValueFromStyle( blockGapValue ) { const isValueString = typeof blockGapValue === 'string'; return { - top: isValueString - ? getSpacingPresetCssVar( blockGapValue ) - : getSpacingPresetCssVar( blockGapValue?.top ), - left: isValueString - ? getSpacingPresetCssVar( blockGapValue ) - : getSpacingPresetCssVar( blockGapValue?.left ), + top: isValueString ? blockGapValue : blockGapValue?.top, + left: isValueString ? blockGapValue : blockGapValue?.left, }; } @@ -78,8 +75,10 @@ export function getGapCSSValue( blockGapValue, defaultValue = '0' ) { return null; } - const row = blockGapBoxControlValue?.top || defaultValue; - const column = blockGapBoxControlValue?.left || defaultValue; + const row = + getSpacingPresetCssVar( blockGapBoxControlValue?.top ) || defaultValue; + const column = + getSpacingPresetCssVar( blockGapBoxControlValue?.left ) || defaultValue; return row === column ? row : `${ row } ${ column }`; } @@ -132,6 +131,8 @@ export function GapEdit( props ) { setAttributes, } = props; + const spacingSizes = useSetting( 'spacing.spacingSizes' ); + const units = useCustomUnits( { availableUnits: useSetting( 'spacing.units' ) || [ '%', @@ -157,6 +158,9 @@ export function GapEdit( props ) { // If splitOnAxis activated we need to return a BoxControl object to the BoxControl component. if ( !! next && splitOnAxis ) { blockGap = { ...getGapBoxControlValueFromStyle( next ) }; + } else if ( next?.hasOwnProperty( 'top' ) ) { + // If splitOnAxis is not enabled, treat the 'top' value as the shorthand gap value. + blockGap = next.top; } const newStyle = { @@ -195,32 +199,46 @@ export function GapEdit( props ) { right: gapValue?.left, bottom: gapValue?.top, } - : gapValue?.top; + : { + top: gapValue?.top, + }; return Platform.select( { web: ( <> - { splitOnAxis ? ( - + ) : ( + + ) ) } + { spacingSizes?.length > 0 && ( + - ) : ( - ) } ), diff --git a/packages/block-editor/src/hooks/test/gap.js b/packages/block-editor/src/hooks/test/gap.js index 1e9d76079cb00e..a08e5a6759b2ab 100644 --- a/packages/block-editor/src/hooks/test/gap.js +++ b/packages/block-editor/src/hooks/test/gap.js @@ -27,28 +27,6 @@ describe( 'gap', () => { ...blockGapValue, } ); } ); - it( 'should unwrap var: values from a string into a CSS var() function', () => { - const expectedValue = { - top: 'var(--wp--preset--spacing--60)', - left: 'var(--wp--preset--spacing--60)', - }; - expect( - getGapBoxControlValueFromStyle( 'var:preset|spacing|60' ) - ).toEqual( expectedValue ); - } ); - it( 'should unwrap var: values from an object into a CSS var() function', () => { - const expectedValue = { - top: 'var(--wp--preset--spacing--20)', - left: 'var(--wp--preset--spacing--60)', - }; - const blockGapValue = { - top: 'var:preset|spacing|20', - left: 'var:preset|spacing|60', - }; - expect( getGapBoxControlValueFromStyle( blockGapValue ) ).toEqual( - expectedValue - ); - } ); } ); describe( 'getGapCSSValue()', () => { it( 'should return `null` if argument is falsey', () => { @@ -84,5 +62,21 @@ describe( 'gap', () => { '88px 1px' ); } ); + + it( 'should unwrap var: values from a string into a CSS var() function', () => { + expect( getGapCSSValue( 'var:preset|spacing|60' ) ).toEqual( + 'var(--wp--preset--spacing--60)' + ); + } ); + + it( 'should unwrap var: values from an object into a CSS var() function and return shorthand values', () => { + const blockGapValue = { + top: 'var:preset|spacing|20', + left: 'var:preset|spacing|60', + }; + expect( getGapCSSValue( blockGapValue ) ).toEqual( + 'var(--wp--preset--spacing--20) var(--wp--preset--spacing--60)' + ); + } ); } ); } ); diff --git a/packages/block-editor/src/layouts/constrained.js b/packages/block-editor/src/layouts/constrained.js index 8517382defcf89..8e6e5fd264f3fb 100644 --- a/packages/block-editor/src/layouts/constrained.js +++ b/packages/block-editor/src/layouts/constrained.js @@ -15,7 +15,7 @@ import { getCSSRules } from '@wordpress/style-engine'; */ import useSetting from '../components/use-setting'; import { appendSelectors, getBlockGapCSS, getAlignmentsInfo } from './utils'; -import { getGapBoxControlValueFromStyle } from '../hooks/gap'; +import { getGapCSSValue } from '../hooks/gap'; import { shouldSkipSerialization } from '../hooks/utils'; export default { @@ -117,16 +117,19 @@ export default { layoutDefinitions, } ) { const { contentSize, wideSize } = layout; - const blockGapStyleValue = getGapBoxControlValueFromStyle( - style?.spacing?.blockGap - ); + const blockGapStyleValue = getGapCSSValue( style?.spacing?.blockGap ); + // If a block's block.json skips serialization for spacing or // spacing.blockGap, don't apply the user-defined value to the styles. - const blockGapValue = - blockGapStyleValue?.top && - ! shouldSkipSerialization( blockName, 'spacing', 'blockGap' ) - ? blockGapStyleValue?.top - : ''; + let blockGapValue = ''; + if ( ! shouldSkipSerialization( blockName, 'spacing', 'blockGap' ) ) { + // If an object is provided only use the 'top' value for this kind of gap. + if ( blockGapStyleValue?.top ) { + blockGapValue = getGapCSSValue( blockGapStyleValue?.top ); + } else if ( typeof blockGapStyleValue === 'string' ) { + blockGapValue = getGapCSSValue( blockGapStyleValue ); + } + } let output = !! contentSize || !! wideSize diff --git a/packages/block-editor/src/layouts/flow.js b/packages/block-editor/src/layouts/flow.js index fb0eb5ecde093f..ecc6779b9128dd 100644 --- a/packages/block-editor/src/layouts/flow.js +++ b/packages/block-editor/src/layouts/flow.js @@ -6,9 +6,8 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ - import { getBlockGapCSS, getAlignmentsInfo } from './utils'; -import { getGapBoxControlValueFromStyle } from '../hooks/gap'; +import { getGapCSSValue } from '../hooks/gap'; import { shouldSkipSerialization } from '../hooks/utils'; export default { @@ -27,16 +26,19 @@ export default { hasBlockGapSupport, layoutDefinitions, } ) { - const blockGapStyleValue = getGapBoxControlValueFromStyle( - style?.spacing?.blockGap - ); + const blockGapStyleValue = getGapCSSValue( style?.spacing?.blockGap ); + // If a block's block.json skips serialization for spacing or // spacing.blockGap, don't apply the user-defined value to the styles. - const blockGapValue = - blockGapStyleValue?.top && - ! shouldSkipSerialization( blockName, 'spacing', 'blockGap' ) - ? blockGapStyleValue?.top - : ''; + let blockGapValue = ''; + if ( ! shouldSkipSerialization( blockName, 'spacing', 'blockGap' ) ) { + // If an object is provided only use the 'top' value for this kind of gap. + if ( blockGapStyleValue?.top ) { + blockGapValue = getGapCSSValue( blockGapStyleValue?.top ); + } else if ( typeof blockGapStyleValue === 'string' ) { + blockGapValue = getGapCSSValue( blockGapStyleValue ); + } + } let output = ''; diff --git a/packages/block-library/src/gallery/gap-styles.js b/packages/block-library/src/gallery/gap-styles.js index 9c5b217d18ba94..7bd0932d62f5b8 100644 --- a/packages/block-library/src/gallery/gap-styles.js +++ b/packages/block-library/src/gallery/gap-styles.js @@ -1,7 +1,10 @@ /** * WordPress dependencies */ -import { BlockList } from '@wordpress/block-editor'; +import { + BlockList, + __experimentalGetGapCSSValue as getGapCSSValue, +} from '@wordpress/block-editor'; import { useContext, createPortal } from '@wordpress/element'; export default function GapStyles( { blockGap, clientId } ) { @@ -17,17 +20,18 @@ export default function GapStyles( { blockGap, clientId } ) { if ( !! blockGap ) { row = typeof blockGap === 'string' - ? blockGap - : blockGap?.top || fallbackValue; + ? getGapCSSValue( blockGap ) + : getGapCSSValue( blockGap?.top ) || fallbackValue; column = typeof blockGap === 'string' - ? blockGap - : blockGap?.left || fallbackValue; + ? getGapCSSValue( blockGap ) + : getGapCSSValue( blockGap?.left ) || fallbackValue; gapValue = row === column ? row : `${ row } ${ column }`; } + // The unstable gallery gap calculation requires a real value (such as `0px`) and not `0`. const gap = `#block-${ clientId } { - --wp--style--unstable-gallery-gap: ${ column }; + --wp--style--unstable-gallery-gap: ${ column === '0' ? '0px' : column }; gap: ${ gapValue } }`; diff --git a/packages/block-library/src/gallery/index.php b/packages/block-library/src/gallery/index.php index e6eecb7dda4122..9f29184ffed07a 100644 --- a/packages/block-library/src/gallery/index.php +++ b/packages/block-library/src/gallery/index.php @@ -51,13 +51,29 @@ function block_core_gallery_render( $attributes, $content ) { if ( is_array( $gap ) ) { foreach ( $gap as $key => $value ) { // Make sure $value is a string to avoid PHP 8.1 deprecation error in preg_match() when the value is null. - $value = is_string( $value ) ? $value : ''; - $gap[ $key ] = $value && preg_match( '%[\\\(&=}]|/\*%', $value ) ? null : $value; + $value = is_string( $value ) ? $value : ''; + $value = $value && preg_match( '%[\\\(&=}]|/\*%', $value ) ? null : $value; + + // Get spacing CSS variable from preset value if provided. + if ( is_string( $value ) && str_contains( $value, 'var:preset|spacing|' ) ) { + $index_to_splice = strrpos( $value, '|' ) + 1; + $slug = _wp_to_kebab_case( substr( $value, $index_to_splice ) ); + $value = "var(--wp--preset--spacing--$slug)"; + } + + $gap[ $key ] = $value; } } else { // Make sure $gap is a string to avoid PHP 8.1 deprecation error in preg_match() when the value is null. $gap = is_string( $gap ) ? $gap : ''; $gap = $gap && preg_match( '%[\\\(&=}]|/\*%', $gap ) ? null : $gap; + + // Get spacing CSS variable from preset value if provided. + if ( is_string( $gap ) && str_contains( $gap, 'var:preset|spacing|' ) ) { + $index_to_splice = strrpos( $gap, '|' ) + 1; + $slug = _wp_to_kebab_case( substr( $gap, $index_to_splice ) ); + $gap = "var(--wp--preset--spacing--$slug)"; + } } $class = wp_unique_id( 'wp-block-gallery-' ); @@ -80,6 +96,11 @@ function block_core_gallery_render( $attributes, $content ) { $gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column; } + // The unstable gallery gap calculation requires a real value (such as `0px`) and not `0`. + if ( '0' === $gap_column ) { + $gap_column = '0px'; + } + // Set the CSS variable to the column value, and the `gap` property to the combined gap value. $style = '.wp-block-gallery.' . $class . '{ --wp--style--unstable-gallery-gap: ' . $gap_column . '; gap: ' . $gap_value . '}'; diff --git a/packages/edit-site/src/components/global-styles/dimensions-panel.js b/packages/edit-site/src/components/global-styles/dimensions-panel.js index cca4fdd0298282..583a43f3411497 100644 --- a/packages/edit-site/src/components/global-styles/dimensions-panel.js +++ b/packages/edit-site/src/components/global-styles/dimensions-panel.js @@ -121,20 +121,21 @@ function splitStyleValue( value ) { function splitGapValue( value ) { // Check for shorthand value (a string value). if ( value && typeof value === 'string' ) { - // Convert to value for individual sides for BoxControl. + // If the value is a string, treat it as a single side (top) for the spacing controls. return { top: value, - right: value, - bottom: value, - left: value, }; } - return { - ...value, - right: value?.left, - bottom: value?.top, - }; + if ( value ) { + return { + ...value, + right: value?.left, + bottom: value?.top, + }; + } + + return value; } // Props for managing `layout.contentSize`. @@ -238,21 +239,26 @@ function useMarginProps( name ) { function useBlockGapProps( name ) { const [ gapValue, setGapValue ] = useStyle( 'spacing.blockGap', name ); const gapValues = splitGapValue( gapValue ); - const setGapValues = ( nextBoxGapValue ) => { - if ( ! nextBoxGapValue ) { - setGapValue( null ); - } - setGapValue( { - top: nextBoxGapValue?.top, - left: nextBoxGapValue?.left, - } ); - }; const gapSides = useCustomSides( name, 'blockGap' ); const isAxialGap = gapSides && gapSides.some( ( side ) => AXIAL_SIDES.includes( side ) ); const resetGapValue = () => setGapValue( undefined ); const [ userSetGapValue ] = useStyle( 'spacing.blockGap', name, 'user' ); const hasGapValue = () => !! userSetGapValue; + const setGapValues = ( nextBoxGapValue ) => { + if ( ! nextBoxGapValue ) { + setGapValue( null ); + } + // If axial gap is not enabled, treat the 'top' value as the shorthand gap value. + if ( ! isAxialGap && nextBoxGapValue?.hasOwnProperty( 'top' ) ) { + setGapValue( nextBoxGapValue.top ); + } else { + setGapValue( { + top: nextBoxGapValue?.top, + left: nextBoxGapValue?.left, + } ); + } + }; return { gapValue, gapValues, @@ -469,27 +475,42 @@ export default function DimensionsPanel( { name } ) { label={ __( 'Block spacing' ) } onDeselect={ resetGapValue } isShownByDefault={ true } + className={ classnames( { + 'tools-panel-item-spacing': showSpacingPresetsControl, + } ) } > - { isAxialGap ? ( - + ) : ( + + ) ) } + { showSpacingPresetsControl && ( + - ) : ( - ) } ) } diff --git a/packages/edit-site/src/components/global-styles/utils.js b/packages/edit-site/src/components/global-styles/utils.js index 81d39355752735..4c9d7bae19f7a1 100644 --- a/packages/edit-site/src/components/global-styles/utils.js +++ b/packages/edit-site/src/components/global-styles/utils.js @@ -79,7 +79,7 @@ export const PRESET_METADATA = [ }, { path: [ 'spacing', 'spacingSizes' ], - valueKey: 'spacingSizes', + valueKey: 'size', cssVarInfix: 'spacing', valueFunc: ( { size } ) => size, classes: [],