From a3bf1e30644c18e38c0c0bf50f3b09d9346016aa Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Wed, 11 Aug 2021 10:49:03 +1000
Subject: [PATCH] Add gap block support feature based on #32571
---
lib/block-supports/dimensions.php | 12 ++
lib/class-wp-theme-json-gutenberg.php | 1 +
lib/compat.php | 3 +
lib/theme.json | 1 +
packages/block-editor/src/hooks/dimensions.js | 28 +++-
packages/block-editor/src/hooks/gap.js | 153 ++++++++++++++++++
packages/block-editor/src/hooks/test/style.js | 10 ++
packages/blocks/src/api/constants.js | 8 +
.../editor/global-styles-renderer.js | 4 +-
.../components/sidebar/dimensions-panel.js | 81 +++++++++-
10 files changed, 297 insertions(+), 4 deletions(-)
create mode 100644 packages/block-editor/src/hooks/gap.js
diff --git a/lib/block-supports/dimensions.php b/lib/block-supports/dimensions.php
index 2692b316d4db17..0802d9e77b222a 100644
--- a/lib/block-supports/dimensions.php
+++ b/lib/block-supports/dimensions.php
@@ -54,6 +54,18 @@ function gutenberg_apply_dimensions_support( $block_type, $block_attributes ) {
// Height support to be added in near future.
// Width support to be added in near future.
+ 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 cf713529b4f70d..80fab8d38a37b0 100644
--- a/lib/class-wp-theme-json-gutenberg.php
+++ b/lib/class-wp-theme-json-gutenberg.php
@@ -99,6 +99,7 @@ class WP_Theme_JSON_Gutenberg {
'wideSize' => null,
),
'spacing' => array(
+ 'customGap' => null,
'customMargin' => null,
'customPadding' => null,
'units' => null,
diff --git a/lib/compat.php b/lib/compat.php
index 917d22fe733704..eebd9acd3f0d61 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 6b52969009beed..e94f3263ee61f1 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 697955670b497e..41450e692ed742 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,
@@ -41,6 +48,7 @@ export const AXIAL_SIDES = [ 'vertical', 'horizontal' ];
* @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 );
@@ -64,6 +72,7 @@ export function DimensionsPanel( props ) {
...style,
spacing: {
...style?.spacing,
+ gap: undefined,
margin: undefined,
padding: undefined,
},
@@ -98,6 +107,16 @@ export function DimensionsPanel( props ) {
) }
+ { ! isGapDisabled && (
+ hasGapValue( props ) }
+ label={ __( 'Gap' ) }
+ onDeselect={ () => resetGap( props ) }
+ isShownByDefault={ defaultSpacingControls?.gap }
+ >
+
+
+ ) }
);
@@ -115,7 +134,11 @@ export function hasDimensionsSupport( blockName ) {
return false;
}
- return hasPaddingSupport( blockName ) || hasMarginSupport( blockName );
+ return (
+ hasGapSupport( blockName ) ||
+ hasPaddingSupport( blockName ) ||
+ hasMarginSupport( blockName )
+ );
}
/**
@@ -126,10 +149,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 00000000000000..6e057f23720a88
--- /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 706d9a93ed0ba4..9588bc18320ce6 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 18342aa9162565..c1c84157a4658f 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 fdec33c3fc0230..4c8147af120888 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 05b4aadd2d5bf0..e4db1c965f1884 100644
--- a/packages/edit-site/src/components/sidebar/dimensions-panel.js
+++ b/packages/edit-site/src/components/sidebar/dimensions-panel.js
@@ -20,8 +20,9 @@ const AXIAL_SIDES = [ 'horizontal', 'vertical' ];
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 } ) {
@@ -36,6 +37,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.
@@ -59,6 +66,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' ) {
@@ -74,10 +101,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 ) || [
'%',
@@ -116,9 +164,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 (
@@ -163,6 +224,24 @@ export default function DimensionsPanel( { context, getStyle, setStyle } ) {
/>
) }
+ { showGapControl && (
+
+
+
+ ) }
);
}