diff --git a/lib/block-supports/dimensions.php b/lib/block-supports/dimensions.php index b51682a34e016..1fae233ad6484 100644 --- a/lib/block-supports/dimensions.php +++ b/lib/block-supports/dimensions.php @@ -63,6 +63,7 @@ function gutenberg_apply_spacing_support( $block_type, $block_attributes ) { $has_padding_support = gutenberg_block_has_support( $block_type, array( 'spacing', 'padding' ), false ); $has_margin_support = gutenberg_block_has_support( $block_type, array( 'spacing', 'margin' ), false ); + $has_gap_support = gutenberg_block_has_support( $block_type, array( 'spacing', 'gap' ), false ); $styles = array(); if ( $has_padding_support ) { @@ -89,6 +90,18 @@ function gutenberg_apply_spacing_support( $block_type, $block_attributes ) { } } + if ( $has_gap_support ) { + $gap_value = _wp_array_get( $block_attributes, array( 'style', 'spacing', 'gap' ), null ); + + if ( is_array( $gap_value ) ) { + foreach ( $gap_value as $key => $value ) { + $styles[] = sprintf( '--wp--theme--block-%s-gap: %s', $key, $value ); + } + } elseif ( null !== $gap_value ) { + $styles[] = sprintf( '--wp--theme--block-: %s', $gap_value ); + } + } + return empty( $styles ) ? array() : array( 'style' => implode( ' ', $styles ) ); } diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index b3ee6d1f8b9d3..266e5d19ddef0 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -61,6 +61,7 @@ class WP_Theme_JSON_Gutenberg { 'text' => null, ), 'spacing' => array( + 'gap' => null, 'margin' => null, 'padding' => null, ), @@ -98,6 +99,7 @@ class WP_Theme_JSON_Gutenberg { 'wideSize' => null, ), 'spacing' => array( + 'customGap' => null, 'customMargin' => null, 'customPadding' => null, 'units' => null, @@ -206,35 +208,38 @@ class WP_Theme_JSON_Gutenberg { * path to the value in theme.json & block attributes. */ const PROPERTIES_METADATA = array( - 'background' => array( 'color', 'gradient' ), - 'background-color' => array( 'color', 'background' ), - 'border-radius' => array( 'border', 'radius' ), - 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ), - 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ), - 'border-bottom-left-radius' => array( 'border', 'radius', 'bottomLeft' ), - 'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ), - 'border-color' => array( 'border', 'color' ), - 'border-width' => array( 'border', 'width' ), - 'border-style' => array( 'border', 'style' ), - 'color' => array( 'color', 'text' ), - 'font-family' => array( 'typography', 'fontFamily' ), - 'font-size' => array( 'typography', 'fontSize' ), - 'font-style' => array( 'typography', 'fontStyle' ), - 'font-weight' => array( 'typography', 'fontWeight' ), - 'letter-spacing' => array( 'typography', 'letterSpacing' ), - 'line-height' => array( 'typography', 'lineHeight' ), - 'margin' => array( 'spacing', 'margin' ), - 'margin-top' => array( 'spacing', 'margin', 'top' ), - 'margin-right' => array( 'spacing', 'margin', 'right' ), - 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ), - 'margin-left' => array( 'spacing', 'margin', 'left' ), - 'padding' => array( 'spacing', 'padding' ), - 'padding-top' => array( 'spacing', 'padding', 'top' ), - 'padding-right' => array( 'spacing', 'padding', 'right' ), - 'padding-bottom' => array( 'spacing', 'padding', 'bottom' ), - 'padding-left' => array( 'spacing', 'padding', 'left' ), - 'text-decoration' => array( 'typography', 'textDecoration' ), - 'text-transform' => array( 'typography', 'textTransform' ), + 'background' => array( 'color', 'gradient' ), + 'background-color' => array( 'color', 'background' ), + 'border-radius' => array( 'border', 'radius' ), + 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ), + 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ), + 'border-bottom-left-radius' => array( 'border', 'radius', 'bottomLeft' ), + 'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ), + 'border-color' => array( 'border', 'color' ), + 'border-width' => array( 'border', 'width' ), + 'border-style' => array( 'border', 'style' ), + 'color' => array( 'color', 'text' ), + 'font-family' => array( 'typography', 'fontFamily' ), + 'font-size' => array( 'typography', 'fontSize' ), + 'font-style' => array( 'typography', 'fontStyle' ), + 'font-weight' => array( 'typography', 'fontWeight' ), + '--wp--theme--block-gap' => array( 'spacing', 'gap' ), + '--wp--theme--block-column-gap' => array( 'spacing', 'gap', 'column' ), + '--wp--theme--block-row-gap' => array( 'spacing', 'gap', 'row' ), + 'letter-spacing' => array( 'typography', 'letterSpacing' ), + 'line-height' => array( 'typography', 'lineHeight' ), + 'margin' => array( 'spacing', 'margin' ), + 'margin-top' => array( 'spacing', 'margin', 'top' ), + 'margin-right' => array( 'spacing', 'margin', 'right' ), + 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ), + 'margin-left' => array( 'spacing', 'margin', 'left' ), + 'padding' => array( 'spacing', 'padding' ), + 'padding-top' => array( 'spacing', 'padding', 'top' ), + 'padding-right' => array( 'spacing', 'padding', 'right' ), + 'padding-bottom' => array( 'spacing', 'padding', 'bottom' ), + 'padding-left' => array( 'spacing', 'padding', 'left' ), + 'text-decoration' => array( 'typography', 'textDecoration' ), + 'text-transform' => array( 'typography', 'textTransform' ), ); const ELEMENTS = array( diff --git a/lib/compat.php b/lib/compat.php index 917d22fe73370..eebd9acd3f0d6 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -186,6 +186,9 @@ function gutenberg_safe_style_attrs( $attrs ) { $attrs[] = 'border-top-right-radius'; $attrs[] = 'border-bottom-right-radius'; $attrs[] = 'border-bottom-left-radius'; + $attrs[] = 'gap'; + $attrs[] = 'column-gap'; + $attrs[] = 'row-gap'; return $attrs; } diff --git a/lib/theme.json b/lib/theme.json index 9e751dc939118..4912883aecff6 100644 --- a/lib/theme.json +++ b/lib/theme.json @@ -211,6 +211,7 @@ ] }, "spacing": { + "customGap": false, "customMargin": false, "customPadding": false, "units": [ "px", "em", "rem", "vh", "vw", "%" ] diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index 8351676640b17..84315b43ef639 100644 --- a/packages/block-editor/src/hooks/dimensions.js +++ b/packages/block-editor/src/hooks/dimensions.js @@ -13,6 +13,13 @@ import { getBlockSupport } from '@wordpress/blocks'; * Internal dependencies */ import InspectorControls from '../components/inspector-controls'; +import { + GapEdit, + hasGapSupport, + hasGapValue, + resetGap, + useIsGapDisabled, +} from './gap'; import { MarginEdit, hasMarginSupport, @@ -39,6 +46,7 @@ export const SPACING_SUPPORT_KEY = 'spacing'; * @return {WPElement} Inspector controls for spacing support features. */ export function DimensionsPanel( props ) { + const isGapDisabled = useIsGapDisabled( props ); const isPaddingDisabled = useIsPaddingDisabled( props ); const isMarginDisabled = useIsMarginDisabled( props ); const isDisabled = useIsDimensionsDisabled( props ); @@ -62,6 +70,7 @@ export function DimensionsPanel( props ) { ...style, spacing: { ...style?.spacing, + gap: undefined, margin: undefined, padding: undefined, }, @@ -96,6 +105,16 @@ export function DimensionsPanel( props ) { ) } + { ! isGapDisabled && ( + hasGapValue( props ) } + label={ __( 'Gap' ) } + onDeselect={ () => resetGap( props ) } + isShownByDefault={ defaultSpacingControls?.gap } + > + + + ) } ); @@ -113,7 +132,11 @@ export function hasDimensionsSupport( blockName ) { return false; } - return hasPaddingSupport( blockName ) || hasMarginSupport( blockName ); + return ( + hasGapSupport( blockName ) || + hasPaddingSupport( blockName ) || + hasMarginSupport( blockName ) + ); } /** @@ -124,10 +147,11 @@ export function hasDimensionsSupport( blockName ) { * @return {boolean} If spacing support is completely disabled. */ const useIsDimensionsDisabled = ( props = {} ) => { + const gapDisabled = useIsGapDisabled( props ); const paddingDisabled = useIsPaddingDisabled( props ); const marginDisabled = useIsMarginDisabled( props ); - return paddingDisabled && marginDisabled; + return gapDisabled && paddingDisabled && marginDisabled; }; /** diff --git a/packages/block-editor/src/hooks/gap.js b/packages/block-editor/src/hooks/gap.js new file mode 100644 index 0000000000000..6e057f23720a8 --- /dev/null +++ b/packages/block-editor/src/hooks/gap.js @@ -0,0 +1,153 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Platform } from '@wordpress/element'; +import { getBlockSupport } from '@wordpress/blocks'; +import { + __experimentalUseCustomUnits as useCustomUnits, + __experimentalBoxControl as BoxControl, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import useSetting from '../components/use-setting'; +import { SPACING_SUPPORT_KEY, useCustomSides } from './dimensions'; +import { cleanEmptyObject } from './utils'; + +/** + * Determines if there is gap support. + * + * @param {string|Object} blockType Block name or Block Type object. + * @return {boolean} Whether there is support. + */ +export function hasGapSupport( blockType ) { + const support = getBlockSupport( blockType, SPACING_SUPPORT_KEY ); + return !! ( true === support || support?.gap ); +} + +/** + * Checks if there is a current value in the gap block support attributes. + * + * @param {Object} props Block props. + * @return {boolean} Whether or not the block has a gap value set. + */ +export function hasGapValue( props ) { + return props.attributes.style?.spacing?.gap !== undefined; +} + +/** + * Resets the gap block support attribute. This can be used when disabling + * the gap support controls for a block via a progressive discovery panel. + * + * @param {Object} props Block props. + * @param {Object} props.attributes Block's attributes. + * @param {Object} props.setAttributes Function to set block's attributes. + */ +export function resetGap( { attributes = {}, setAttributes } ) { + const { style } = attributes; + + setAttributes( { + style: { + ...style, + spacing: { + ...style?.spacing, + gap: undefined, + }, + }, + } ); +} + +/** + * Custom hook that checks if gap settings have been disabled. + * + * @param {string} name The name of the block. + * @return {boolean} Whether the gap setting is disabled. + */ +export function useIsGapDisabled( { name: blockName } = {} ) { + const isDisabled = ! useSetting( 'spacing.customGap' ); + return ! hasGapSupport( blockName ) || isDisabled; +} + +/** + * Inspector control panel containing the gap related configuration + * + * @param {Object} props + * + * @return {WPElement} Gap edit element. + */ +export function GapEdit( props ) { + const { + name: blockName, + attributes: { style }, + setAttributes, + } = props; + + const units = useCustomUnits( { + availableUnits: useSetting( 'spacing.units' ) || [ + '%', + 'px', + 'em', + 'rem', + 'vw', + ], + } ); + const sides = useCustomSides( blockName, 'gap' ); + + if ( useIsGapDisabled( props ) ) { + return null; + } + + const onChange = ( next ) => { + const newStyle = { + ...style, + spacing: { + ...style?.spacing, + gap: { row: next?.top, column: next?.left }, + }, + }; + + setAttributes( { + style: cleanEmptyObject( newStyle ), + } ); + }; + + const onChangeShowVisualizer = ( next ) => { + const newStyle = { + ...style, + visualizers: { + gap: { row: next?.top, column: next?.left }, + }, + }; + + setAttributes( { + style: cleanEmptyObject( newStyle ), + } ); + }; + + const boxValues = { + top: style?.spacing?.gap?.row, + right: style?.spacing?.gap?.column, + bottom: style?.spacing?.gap?.row, + left: style?.spacing?.gap?.column, + }; + + return Platform.select( { + web: ( + <> + + + ), + native: null, + } ); +} diff --git a/packages/block-editor/src/hooks/test/style.js b/packages/block-editor/src/hooks/test/style.js index 706d9a93ed0ba..9588bc18320ce 100644 --- a/packages/block-editor/src/hooks/test/style.js +++ b/packages/block-editor/src/hooks/test/style.js @@ -24,11 +24,13 @@ describe( 'getInlineStyles', () => { color: '#21759b', }, spacing: { + gap: { row: '1em' }, padding: { top: '10px' }, margin: { bottom: '15px' }, }, } ) ).toEqual( { + '--wp--theme--block-row-gap': '1em', backgroundColor: 'black', borderColor: '#21759b', borderRadius: '10px', @@ -66,6 +68,10 @@ describe( 'getInlineStyles', () => { expect( getInlineStyles( { spacing: { + gap: { + column: '5px', + row: '1em', + }, margin: { top: '10px', right: '0.5rem', @@ -81,6 +87,8 @@ describe( 'getInlineStyles', () => { }, } ) ).toEqual( { + '--wp--theme--block-column-gap': '5px', + '--wp--theme--block-row-gap': '1em', marginTop: '10px', marginRight: '0.5rem', marginBottom: '0.5em', @@ -96,11 +104,13 @@ describe( 'getInlineStyles', () => { expect( getInlineStyles( { spacing: { + gap: '1em', margin: '10px', padding: '20px', }, } ) ).toEqual( { + '--wp--theme--block-gap': '1em', margin: '10px', padding: '20px', } ); diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index 13d97020aca92..ccad919519278 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -72,6 +72,14 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { value: [ 'typography', 'fontWeight' ], support: [ 'typography', '__experimentalFontWeight' ], }, + '--wp--theme--block-gap': { + value: [ 'spacing', 'gap' ], + support: [ 'spacing', 'gap' ], + properties: { + '--wp--theme--block-column-gap': 'column', + '--wp--theme--block-row-gap': 'row', + }, + }, lineHeight: { value: [ 'typography', 'lineHeight' ], support: [ 'typography', 'lineHeight' ], diff --git a/packages/edit-site/src/components/editor/global-styles-renderer.js b/packages/edit-site/src/components/editor/global-styles-renderer.js index 38b46cabd8c45..07a0f3b4fbb21 100644 --- a/packages/edit-site/src/components/editor/global-styles-renderer.js +++ b/packages/edit-site/src/components/editor/global-styles-renderer.js @@ -159,7 +159,9 @@ function getStylesDeclarations( blockStyles = {} ) { return; } - const cssProperty = kebabCase( name ); + const cssProperty = name.startsWith( '--' ) + ? name + : kebabCase( name ); declarations.push( `${ cssProperty }: ${ compileStyleValue( get( styleValue, [ prop ] ) diff --git a/packages/edit-site/src/components/sidebar/dimensions-panel.js b/packages/edit-site/src/components/sidebar/dimensions-panel.js index 4f19f7e19cae6..f47af9822c00f 100644 --- a/packages/edit-site/src/components/sidebar/dimensions-panel.js +++ b/packages/edit-site/src/components/sidebar/dimensions-panel.js @@ -18,8 +18,9 @@ import { useSetting } from '../editor/utils'; export function useHasDimensionsPanel( context ) { const hasPadding = useHasPadding( context ); const hasMargin = useHasMargin( context ); + const hasGap = useHasGap( context ); - return hasPadding || hasMargin; + return hasPadding || hasMargin || hasGap; } function useHasPadding( { name, supports } ) { @@ -34,6 +35,12 @@ function useHasMargin( { name, supports } ) { return settings && supports.includes( 'margin' ); } +function useHasGap( { name, supports } ) { + const settings = useSetting( 'spacing.customGap', name ); + + return settings && supports.includes( '--wp--theme--block-gap' ); +} + function filterValuesBySides( values, sides ) { if ( ! sides ) { // If no custom side configuration all sides are opted into by default. @@ -47,6 +54,26 @@ function filterValuesBySides( values, sides ) { return filteredValues; } +function filterGapValuesBySides( values, sides ) { + if ( ! sides ) { + return { + row: values?.top, + column: values?.left, + }; + } + + const filteredValues = {}; + + sides.forEach( ( side ) => { + if ( side === 'horizontal' ) { + filteredValues.column = values?.left; + } + if ( side === 'vertical' ) { + filteredValues.row = values?.top; + } + } ); +} + function splitStyleValue( value ) { // Check for shorthand value ( a string value ). if ( value && typeof value === 'string' ) { @@ -62,10 +89,31 @@ function splitStyleValue( value ) { return value; } +function splitGapStyleValue( value ) { + // Check for shorthand value ( a string value ). + if ( value && typeof value === 'string' ) { + return { + top: value, + right: value, + bottom: value, + left: value, + }; + } + + // Convert rows and columns to individual side values. + return { + top: value?.row, + right: value?.column, + bottom: value?.row, + left: value?.column, + }; +} + export default function DimensionsPanel( { context, getStyle, setStyle } ) { const { name } = context; const showPaddingControl = useHasPadding( context ); const showMarginControl = useHasMargin( context ); + const showGapControl = useHasGap( context ); const units = useCustomUnits( { availableUnits: useSetting( 'spacing.units', name ) || [ '%', @@ -98,9 +146,22 @@ export default function DimensionsPanel( { context, getStyle, setStyle } ) { const hasMarginValue = () => marginValues && Object.keys( marginValues ).length; + const gapValues = splitGapStyleValue( + getStyle( name, '--wp--theme--block-gap' ) + ); + const gapSides = useCustomSides( name, '--wp--theme--block-gap' ); + + const setGapValues = ( newGapValues ) => { + const gap = filterGapValuesBySides( newGapValues, gapSides ); + setStyle( name, '--wp--theme--block-gap', gap ); + }; + const resetGapValue = () => setGapValues( {} ); + const hasGapValue = () => gapValues && Object.keys( gapValues ).length; + const resetAll = () => { resetPaddingValue(); resetMarginValue(); + resetGapValue(); }; return ( @@ -143,6 +204,24 @@ export default function DimensionsPanel( { context, getStyle, setStyle } ) { /> ) } + { showGapControl && ( + + + + ) } ); }