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 && (
+
+
+
+ ) }
);
}