Skip to content

Commit

Permalink
Block Support: Add gap block support feature (#33991)
Browse files Browse the repository at this point in the history
* Add gap block support feature based on #32571

* Server-render the gap block support, and skip serialization on save in the editor

* Add PHP tests for spacing gap support server-side rendering

* Rename support from customBlockGap to blockGap

* Align whitespace
  • Loading branch information
andrewserong authored Sep 2, 2021
1 parent 32a370f commit f0ee759
Show file tree
Hide file tree
Showing 10 changed files with 435 additions and 10 deletions.
53 changes: 53 additions & 0 deletions lib/block-supports/spacing.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,57 @@ function gutenberg_skip_spacing_serialization( $block_type ) {
$spacing_support['__experimentalSkipSerialization'];
}


/**
* Renders the spacing gap support to the block wrapper, to ensure
* that the CSS variable is rendered in all environments.
*
* @param string $block_content Rendered block content.
* @param array $block Block object.
* @return string Filtered block content.
*/
function gutenberg_render_spacing_gap_support( $block_content, $block ) {
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
$has_gap_support = gutenberg_block_has_support( $block_type, array( 'spacing', 'blockGap' ), false );
if ( ! $has_gap_support || ! isset( $block['attrs']['style']['spacing']['blockGap'] ) ) {
return $block_content;
}

$gap_value = $block['attrs']['style']['spacing']['blockGap'];

// Skip if gap value contains unsupported characters.
// Regex for CSS value borrowed from `safecss_filter_attr`, and used here
// because we only want to match against the value, not the CSS attribute.
if ( preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ) {
return $block_content;
}

$style = sprintf(
'--wp--style--block-gap: %s',
esc_attr( $gap_value )
);

// Attempt to update an existing style attribute on the wrapper element.
$injected_style = preg_replace(
'/^([^>.]+?)(' . preg_quote( 'style="', '/' ) . ')(?=.+?>)/',
'$1$2' . $style . '; ',
$block_content,
1
);

// If there is no existing style attribute, add one to the wrapper element.
if ( $injected_style === $block_content ) {
$injected_style = preg_replace(
'/<([a-zA-Z0-9]+)([ >])/',
'<$1 style="' . $style . '"$2',
$block_content,
1
);
};

return $injected_style;
}

// Register the block support.
WP_Block_Supports::get_instance()->register(
'spacing',
Expand All @@ -98,3 +149,5 @@ function gutenberg_skip_spacing_serialization( $block_type ) {
'apply' => 'gutenberg_apply_spacing_support',
)
);

add_filter( 'render_block', 'gutenberg_render_spacing_gap_support', 10, 2 );
1 change: 1 addition & 0 deletions lib/class-wp-theme-json-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class WP_Theme_JSON_Gutenberg {
'wideSize' => null,
),
'spacing' => array(
'blockGap' => null,
'customMargin' => null,
'customPadding' => null,
'units' => null,
Expand Down
1 change: 1 addition & 0 deletions lib/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@
]
},
"spacing": {
"blockGap": false,
"customMargin": false,
"customPadding": false,
"units": [ "px", "em", "rem", "vh", "vw", "%" ]
Expand Down
29 changes: 27 additions & 2 deletions packages/block-editor/src/hooks/dimensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 );
Expand All @@ -64,6 +72,7 @@ export function DimensionsPanel( props ) {
...style,
spacing: {
...style?.spacing,
blockGap: undefined,
margin: undefined,
padding: undefined,
},
Expand Down Expand Up @@ -98,6 +107,17 @@ export function DimensionsPanel( props ) {
<MarginEdit { ...props } />
</ToolsPanelItem>
) }
{ ! isGapDisabled && (
<ToolsPanelItem
className="single-column"
hasValue={ () => hasGapValue( props ) }
label={ __( 'Block gap' ) }
onDeselect={ () => resetGap( props ) }
isShownByDefault={ defaultSpacingControls?.blockGap }
>
<GapEdit { ...props } />
</ToolsPanelItem>
) }
</ToolsPanel>
</InspectorControls>
);
Expand All @@ -115,7 +135,11 @@ export function hasDimensionsSupport( blockName ) {
return false;
}

return hasPaddingSupport( blockName ) || hasMarginSupport( blockName );
return (
hasGapSupport( blockName ) ||
hasPaddingSupport( blockName ) ||
hasMarginSupport( blockName )
);
}

/**
Expand All @@ -126,10 +150,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;
};

/**
Expand Down
128 changes: 128 additions & 0 deletions packages/block-editor/src/hooks/gap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { Platform } from '@wordpress/element';
import { getBlockSupport } from '@wordpress/blocks';
import {
__experimentalUseCustomUnits as useCustomUnits,
__experimentalUnitControl as UnitControl,
} from '@wordpress/components';

/**
* Internal dependencies
*/
import useSetting from '../components/use-setting';
import { SPACING_SUPPORT_KEY } 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?.blockGap );
}

/**
* 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?.blockGap !== 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,
blockGap: 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.blockGap' );
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 {
attributes: { style },
setAttributes,
} = props;

const units = useCustomUnits( {
availableUnits: useSetting( 'spacing.units' ) || [
'%',
'px',
'em',
'rem',
'vw',
],
} );

if ( useIsGapDisabled( props ) ) {
return null;
}

const onChange = ( next ) => {
const newStyle = {
...style,
spacing: {
...style?.spacing,
blockGap: next,
},
};

setAttributes( {
style: cleanEmptyObject( newStyle ),
} );
};

return Platform.select( {
web: (
<>
<UnitControl
label={ __( 'Block gap' ) }
min={ 0 }
onChange={ onChange }
units={ units }
value={ style?.spacing?.blockGap }
/>
</>
),
native: null,
} );
}
49 changes: 42 additions & 7 deletions packages/block-editor/src/hooks/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,14 @@ function addAttribute( settings ) {
return settings;
}

const skipSerializationPaths = {
/**
* A dictionary of paths to flag skipping block support serialization as the key,
* with values providing the style paths to be omitted from serialization.
*
* @constant
* @type {Record<string, string[]>}
*/
const skipSerializationPathsEdit = {
[ `${ BORDER_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [ 'border' ],
[ `${ COLOR_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [
COLOR_SUPPORT_KEY,
Expand All @@ -157,23 +164,46 @@ const skipSerializationPaths = {
],
};

/**
* A dictionary of paths to flag skipping block support serialization as the key,
* with values providing the style paths to be omitted from serialization.
*
* Extends the Edit skip paths to enable skipping additional paths in just
* the Save component. This allows a block support to be serialized within the
* editor, while using an alternate approach, such as server-side rendering, when
* the support is saved.
*
* @constant
* @type {Record<string, string[]>}
*/
const skipSerializationPathsSave = {
...skipSerializationPathsEdit,
[ `${ SPACING_SUPPORT_KEY }` ]: [ 'spacing.blockGap' ],
};

/**
* Override props assigned to save component to inject the CSS variables definition.
*
* @param {Object} props Additional props applied to save element.
* @param {Object} blockType Block type.
* @param {Object} attributes Block attributes.
* @param {Object} props Additional props applied to save element.
* @param {Object} blockType Block type.
* @param {Object} attributes Block attributes.
* @param {?Record<string, string[]>} skipPaths An object of keys and paths to skip serialization.
*
* @return {Object} Filtered props applied to save element.
*/
export function addSaveProps( props, blockType, attributes ) {
export function addSaveProps(
props,
blockType,
attributes,
skipPaths = skipSerializationPathsSave
) {
if ( ! hasStyleSupport( blockType ) ) {
return props;
}

let { style } = attributes;

forEach( skipSerializationPaths, ( path, indicator ) => {
forEach( skipPaths, ( path, indicator ) => {
if ( getBlockSupport( blockType, indicator ) ) {
style = omit( style, path );
}
Expand Down Expand Up @@ -207,7 +237,12 @@ export function addEditProps( settings ) {
props = existingGetEditWrapperProps( attributes );
}

return addSaveProps( props, settings, attributes );
return addSaveProps(
props,
settings,
attributes,
skipSerializationPathsEdit
);
};

return settings;
Expand Down
4 changes: 4 additions & 0 deletions packages/block-editor/src/hooks/test/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ describe( 'getInlineStyles', () => {
color: '#21759b',
},
spacing: {
blockGap: '1em',
padding: { top: '10px' },
margin: { bottom: '15px' },
},
} )
).toEqual( {
'--wp--style--block-gap': '1em',
backgroundColor: 'black',
borderColor: '#21759b',
borderRadius: '10px',
Expand Down Expand Up @@ -96,11 +98,13 @@ describe( 'getInlineStyles', () => {
expect(
getInlineStyles( {
spacing: {
blockGap: '1em',
margin: '10px',
padding: '20px',
},
} )
).toEqual( {
'--wp--style--block-gap': '1em',
margin: '10px',
padding: '20px',
} );
Expand Down
Loading

0 comments on commit f0ee759

Please sign in to comment.