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