From d75884eaedea81cf1fdcd6c07025eb1610776c98 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Wed, 2 Jun 2021 16:10:55 +1000 Subject: [PATCH] Add height dimensions block support --- lib/block-supports/dimensions.php | 67 ++++++++-- lib/class-wp-theme-json-gutenberg.php | 7 + lib/experimental-default-theme.json | 3 + packages/block-editor/src/hooks/dimensions.js | 43 ++++++- packages/block-editor/src/hooks/height.js | 121 ++++++++++++++++++ packages/block-editor/src/hooks/style.js | 12 +- packages/block-editor/src/hooks/test/style.js | 4 + packages/blocks/src/api/constants.js | 4 + .../editor/global-styles-renderer.js | 8 +- .../components/sidebar/dimensions-panel.js | 31 ++++- phpunit/class-wp-theme-json-test.php | 13 +- 11 files changed, 288 insertions(+), 25 deletions(-) create mode 100644 packages/block-editor/src/hooks/height.js diff --git a/lib/block-supports/dimensions.php b/lib/block-supports/dimensions.php index b51682a34e0161..0c380c18feb46f 100644 --- a/lib/block-supports/dimensions.php +++ b/lib/block-supports/dimensions.php @@ -21,10 +21,10 @@ function gutenberg_register_dimensions_support( $block_type ) { return; } - $has_spacing_support = gutenberg_block_has_support( $block_type, array( 'spacing' ), false ); - // Future block supports such as height & width will be added here. + $has_dimensions_support = gutenberg_block_has_support( $block_type, array( '__experimentalDimensions' ), false ); + $has_spacing_support = gutenberg_block_has_support( $block_type, array( 'spacing' ), false ); - if ( $has_spacing_support ) { + if ( $has_dimensions_support || $has_spacing_support ) { $block_type->attributes['style'] = array( 'type' => 'object', ); @@ -41,22 +41,53 @@ function gutenberg_register_dimensions_support( $block_type ) { * @return array Block spacing CSS classes and inline styles. */ function gutenberg_apply_dimensions_support( $block_type, $block_attributes ) { - $spacing_styles = gutenberg_apply_spacing_support( $block_type, $block_attributes ); - // Future block supports such as height and width will be added here. + $dimensions_styles = gutenberg_get_dimensions_styles( $block_type, $block_attributes ); + $spacing_styles = gutenberg_get_spacing_styles( $block_type, $block_attributes ); + $styles = $dimensions_styles . $spacing_styles; - return $spacing_styles; + return empty( $styles ) ? array() : array( 'style' => $styles ); } /** - * Add CSS classes for block spacing to the incoming attributes array. + * Add inline styles for block dimensions to the incoming attributes array. * This will be applied to the block markup in the front-end. * * @param WP_Block_Type $block_type Block Type. * @param array $block_attributes Block attributes. * - * @return array Block spacing CSS classes and inline styles. + * @return array Block dimensions inline styles. */ -function gutenberg_apply_spacing_support( $block_type, $block_attributes ) { +function gutenberg_get_dimensions_styles( $block_type, $block_attributes ) { + if ( gutenberg_skip_dimensions_serialization( $block_type ) ) { + return array(); + } + + $has_height_support = gutenberg_block_has_support( $block_type, array( '__experimentalDimensions', 'height' ), false ); + $styles = array(); + + if ( $has_height_support ) { + $height_value = _wp_array_get( $block_attributes, array( 'style', 'dimensions', 'height' ), null ); + + if ( null !== $height_value ) { + $styles[] = sprintf( 'height: %s;', $height_value ); + } + } + + // Width support to be added in near future. + + return empty( $styles ) ? null : implode( ' ', $styles ); +} + +/** + * Add inline styles for block spacing to the incoming attributes array. + * This will be applied to the block markup in the front-end. + * + * @param WP_Block_Type $block_type Block Type. + * @param array $block_attributes Block attributes. + * + * @return array Block spacing inline styles. + */ +function gutenberg_get_spacing_styles( $block_type, $block_attributes ) { if ( gutenberg_skip_spacing_serialization( $block_type ) ) { return array(); } @@ -89,7 +120,23 @@ function gutenberg_apply_spacing_support( $block_type, $block_attributes ) { } } - return empty( $styles ) ? array() : array( 'style' => implode( ' ', $styles ) ); + return empty( $styles ) ? null : implode( ' ', $styles ); +} + +/** + * Checks whether serialization of the current block's dimensions properties + * should occur. + * + * @param WP_Block_type $block_type Block type. + * + * @return boolean Whether to serialize dimensions support styles & classes. + */ +function gutenberg_skip_dimensions_serialization( $block_type ) { + $dimensions_support = _wp_array_get( $block_type->supports, array( '__experimentalDimensions' ), false ); + + return is_array( $dimensions_support ) && + array_key_exists( '__experimentalSkipSerialization', $dimensions_support ) && + $dimensions_support['__experimentalSkipSerialization']; } /** diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index b3ee6d1f8b9d3e..af0716477c9fbf 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -60,6 +60,9 @@ class WP_Theme_JSON_Gutenberg { 'gradient' => null, 'text' => null, ), + 'dimensions' => array( + 'height' => null, + ), 'spacing' => array( 'margin' => null, 'padding' => null, @@ -93,6 +96,9 @@ class WP_Theme_JSON_Gutenberg { 'palette' => null, ), 'custom' => null, + 'dimensions' => array( + 'customHeight' => null, + ), 'layout' => array( 'contentSize' => null, 'wideSize' => null, @@ -221,6 +227,7 @@ class WP_Theme_JSON_Gutenberg { 'font-size' => array( 'typography', 'fontSize' ), 'font-style' => array( 'typography', 'fontStyle' ), 'font-weight' => array( 'typography', 'fontWeight' ), + 'height' => array( 'dimensions', 'height' ), 'letter-spacing' => array( 'typography', 'letterSpacing' ), 'line-height' => array( 'typography', 'lineHeight' ), 'margin' => array( 'spacing', 'margin' ), diff --git a/lib/experimental-default-theme.json b/lib/experimental-default-theme.json index 9e751dc939118b..90c2c3e175ce19 100644 --- a/lib/experimental-default-theme.json +++ b/lib/experimental-default-theme.json @@ -210,6 +210,9 @@ } ] }, + "dimensions": { + "customHeight": false + }, "spacing": { "customMargin": false, "customPadding": false, diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index 57ce3a764b31e7..7b473837faf384 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 { + HeightEdit, + hasHeightSupport, + hasHeightValue, + resetHeight, + useIsHeightDisabled, +} from './height'; import { MarginEdit, hasMarginSupport, @@ -28,18 +35,19 @@ import { useIsPaddingDisabled, } from './padding'; +export const DIMENSIONS_SUPPORT_KEY = '__experimentalDimensions'; export const SPACING_SUPPORT_KEY = 'spacing'; /** * Inspector controls for dimensions support. * * @param {Object} props Block props. - * - * @return {WPElement} Inspector controls for spacing support features. + * @return {WPElement} Inspector controls for dimensions support features. */ export function DimensionsPanel( props ) { const isPaddingDisabled = useIsPaddingDisabled( props ); const isMarginDisabled = useIsMarginDisabled( props ); + const isHeightDisabled = useIsHeightDisabled( props ); const isDisabled = useIsDimensionsDisabled( props ); const isSupported = hasDimensionsSupport( props.name ); @@ -47,6 +55,11 @@ export function DimensionsPanel( props ) { return null; } + const defaultDimensionsControls = getBlockSupport( props.name, [ + DIMENSIONS_SUPPORT_KEY, + '__experimentalDefaultControls', + ] ); + const defaultSpacingControls = getBlockSupport( props.name, [ SPACING_SUPPORT_KEY, '__experimentalDefaultControls', @@ -59,6 +72,10 @@ export function DimensionsPanel( props ) { props.setAttributes( { style: { ...style, + dimensions: { + ...style?.dimensions, + height: undefined, + }, spacing: { ...style?.spacing, margin: undefined, @@ -75,6 +92,16 @@ export function DimensionsPanel( props ) { title={ __( 'Dimensions' ) } resetAll={ resetAll } > + { ! isHeightDisabled && ( + hasHeightValue( props ) } + label={ __( 'Height' ) } + onDeselect={ () => resetHeight( props ) } + isShownByDefault={ defaultDimensionsControls?.height } + > + + + ) } { ! isPaddingDisabled && ( hasPaddingValue( props ) } @@ -112,21 +139,25 @@ export function hasDimensionsSupport( blockName ) { return false; } - return hasPaddingSupport( blockName ) || hasMarginSupport( blockName ); + return ( + hasHeightSupport( blockName ) || + hasPaddingSupport( blockName ) || + hasMarginSupport( blockName ) + ); } /** * Determines whether dimensions support has been disabled. * * @param {Object} props Block properties. - * - * @return {boolean} If spacing support is completely disabled. + * @return {boolean} If dimensions support is completely disabled. */ const useIsDimensionsDisabled = ( props = {} ) => { + const heightDisabled = useIsHeightDisabled( props ); const paddingDisabled = useIsPaddingDisabled( props ); const marginDisabled = useIsMarginDisabled( props ); - return paddingDisabled && marginDisabled; + return heightDisabled && paddingDisabled && marginDisabled; }; /** diff --git a/packages/block-editor/src/hooks/height.js b/packages/block-editor/src/hooks/height.js new file mode 100644 index 00000000000000..302556d231a7b6 --- /dev/null +++ b/packages/block-editor/src/hooks/height.js @@ -0,0 +1,121 @@ +/** + * WordPress dependencies + */ +import { getBlockSupport } from '@wordpress/blocks'; +import { + __experimentalUseCustomUnits as useCustomUnits, + __experimentalUnitControl as UnitControl, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import useSetting from '../components/use-setting'; +import { DIMENSIONS_SUPPORT_KEY } from './dimensions'; +import { cleanEmptyObject } from './utils'; + +/** + * Determines if there is height support. + * + * @param {string|Object} blockType Block name or Block Type object. + * @return {boolean} Whether there is support. + */ +export function hasHeightSupport( blockType ) { + const support = getBlockSupport( blockType, DIMENSIONS_SUPPORT_KEY ); + return !! ( true === support || support?.height ); +} + +/** + * Checks if there is a current value in the height block support attributes. + * + * @param {Object} props Block props. + * @return {boolean} Whether or not the block has a height value set. + */ +export function hasHeightValue( props ) { + return props.attributes.style?.dimensions?.height !== undefined; +} + +/** + * Resets the height block support attributes. This can be used when + * disabling the height 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 resetHeight( { attributes = {}, setAttributes } ) { + const { style } = attributes; + + setAttributes( { + style: { + ...style, + dimensions: { + ...style?.dimensions, + height: undefined, + }, + }, + } ); +} + +/** + * Custom hook that checks if height controls have been disabled. + * + * @param {string} name The name of the block. + * @return {boolean} Whether height control is disabled. + */ +export function useIsHeightDisabled( { name: blockName } = {} ) { + const isDisabled = ! useSetting( 'dimensions.customHeight' ); + return ! hasHeightSupport( blockName ) || isDisabled; +} + +/** + * Inspector control panel containing the height related configuration. + * + * @param {Object} props Block props. + * @return {WPElement} Edit component for height. + */ +export function HeightEdit( props ) { + const { + attributes: { style }, + setAttributes, + } = props; + + const units = useCustomUnits( { + availableUnits: useSetting( 'dimensions.units' ) || [ + '%', + 'px', + 'em', + 'rem', + 'vh', + 'vw', + ], + } ); + + if ( useIsHeightDisabled( props ) ) { + return null; + } + + const onChange = ( next ) => { + const newStyle = { + ...style, + dimensions: { + ...style?.dimensions, + height: next, + }, + }; + + setAttributes( { style: cleanEmptyObject( newStyle ) } ); + }; + + return ( + + ); +} diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 87bf4685d50d42..2c3fff303ec402 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -37,13 +37,18 @@ import { TYPOGRAPHY_SUPPORT_KEY, TYPOGRAPHY_SUPPORT_KEYS, } from './typography'; -import { SPACING_SUPPORT_KEY, DimensionsPanel } from './dimensions'; +import { + DIMENSIONS_SUPPORT_KEY, + SPACING_SUPPORT_KEY, + DimensionsPanel, +} from './dimensions'; import useDisplayBlockControls from '../components/use-display-block-controls'; const styleSupportKeys = [ ...TYPOGRAPHY_SUPPORT_KEYS, BORDER_SUPPORT_KEY, COLOR_SUPPORT_KEY, + DIMENSIONS_SUPPORT_KEY, SPACING_SUPPORT_KEY, ]; @@ -152,8 +157,11 @@ const skipSerializationPaths = { [ `${ TYPOGRAPHY_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [ TYPOGRAPHY_SUPPORT_KEY, ], + [ `${ DIMENSIONS_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [ + DIMENSIONS_SUPPORT_KEY, + ], [ `${ SPACING_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [ - 'spacing', + SPACING_SUPPORT_KEY, ], }; diff --git a/packages/block-editor/src/hooks/test/style.js b/packages/block-editor/src/hooks/test/style.js index 706d9a93ed0ba4..12e369ceb592f7 100644 --- a/packages/block-editor/src/hooks/test/style.js +++ b/packages/block-editor/src/hooks/test/style.js @@ -23,6 +23,9 @@ describe( 'getInlineStyles', () => { style: 'dotted', color: '#21759b', }, + dimensions: { + height: '500px', + }, spacing: { padding: { top: '10px' }, margin: { bottom: '15px' }, @@ -37,6 +40,7 @@ describe( 'getInlineStyles', () => { color: 'red', lineHeight: 1.5, fontSize: 10, + height: '500px', marginBottom: '15px', paddingTop: '10px', } ); diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index 13d97020aca924..2da96b88da54ab 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -72,6 +72,10 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { value: [ 'typography', 'fontWeight' ], support: [ 'typography', '__experimentalFontWeight' ], }, + height: { + value: [ 'dimensions', 'height' ], + support: [ '__experimentalDimensions', 'height' ], + }, 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 5d64dfabac0ad0..bb1ed4720f544a 100644 --- a/packages/edit-site/src/components/editor/global-styles-renderer.js +++ b/packages/edit-site/src/components/editor/global-styles-renderer.js @@ -186,7 +186,13 @@ export const getNodesWithStyles = ( tree, blockSelectors ) => { const pickStyleKeys = ( treeToPickFrom ) => pickBy( treeToPickFrom, ( value, key ) => - [ 'border', 'color', 'spacing', 'typography' ].includes( key ) + [ + 'border', + 'color', + 'dimensions', + 'spacing', + 'typography', + ].includes( key ) ); // Top-level. diff --git a/packages/edit-site/src/components/sidebar/dimensions-panel.js b/packages/edit-site/src/components/sidebar/dimensions-panel.js index abea542bacd082..b69bd6e8e2fa82 100644 --- a/packages/edit-site/src/components/sidebar/dimensions-panel.js +++ b/packages/edit-site/src/components/sidebar/dimensions-panel.js @@ -7,6 +7,7 @@ import { __experimentalProgressiveDisclosurePanelItem as ProgressiveDisclosurePanelItem, __experimentalBoxControl as BoxControl, __experimentalUseCustomUnits as useCustomUnits, + __experimentalUnitControl as UnitControl, } from '@wordpress/components'; import { __experimentalUseCustomSides as useCustomSides } from '@wordpress/block-editor'; @@ -16,10 +17,17 @@ import { __experimentalUseCustomSides as useCustomSides } from '@wordpress/block import { useSetting } from '../editor/utils'; export function useHasDimensionsPanel( context ) { + const hasHeight = useHasHeight( context ); const hasPadding = useHasPadding( context ); const hasMargin = useHasMargin( context ); - return hasPadding || hasMargin; + return hasHeight || hasPadding || hasMargin; +} + +function useHasHeight( { name, supports } ) { + const settings = useSetting( 'dimensions.customHeight', name ); + + return settings && supports.includes( 'height' ); } function useHasPadding( { name, supports } ) { @@ -64,6 +72,7 @@ function splitStyleValue( value ) { export default function DimensionsPanel( { context, getStyle, setStyle } ) { const { name } = context; + const showHeightControl = useHasHeight( context ); const showPaddingControl = useHasPadding( context ); const showMarginControl = useHasMargin( context ); const units = useCustomUnits( { @@ -76,6 +85,13 @@ export default function DimensionsPanel( { context, getStyle, setStyle } ) { ], } ); + // Height. + const heightValue = getStyle( name, 'height' ); + const setHeightValue = ( next ) => setStyle( name, 'height', next ); + const resetHeightValue = () => setHeightValue( undefined ); + const hasHeightValue = () => !! heightValue; + + // Padding. const paddingValues = splitStyleValue( getStyle( name, 'padding' ) ); const paddingSides = useCustomSides( name, 'padding' ); @@ -87,6 +103,7 @@ export default function DimensionsPanel( { context, getStyle, setStyle } ) { const hasPaddingValue = () => paddingValues && Object.keys( paddingValues ).length; + // Margin. const marginValues = splitStyleValue( getStyle( name, 'margin' ) ); const marginSides = useCustomSides( name, 'margin' ); @@ -109,6 +126,18 @@ export default function DimensionsPanel( { context, getStyle, setStyle } ) { title={ __( 'Dimensions' ) } resetAll={ resetAll } > + { showHeightControl && ( + + ) } { showPaddingControl && ( array( 'core/group' => array( - 'border' => array( + 'border' => array( 'radius' => '10px', ), - 'elements' => array( + 'elements' => array( 'link' => array( 'color' => array( 'text' => '#111', ), ), ), - 'spacing' => array( + 'dimensions' => array( + 'height' => '200px', + ), + 'spacing' => array( 'padding' => '24px', ), ), @@ -352,11 +355,11 @@ function test_get_stylesheet() { ); $this->assertEquals( - 'body{--wp--preset--color--grey: grey;--wp--preset--font-family--small: 14px;--wp--preset--font-family--big: 41px;}.wp-block-group{--wp--custom--base-font: 16;--wp--custom--line-height--small: 1.2;--wp--custom--line-height--medium: 1.4;--wp--custom--line-height--large: 1.8;}body{color: var(--wp--preset--color--grey);}a{background-color: #333;color: #111;}.wp-block-group{border-radius: 10px;padding: 24px;}.wp-block-group a{color: #111;}h1,h2,h3,h4,h5,h6{color: #123456;}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{background-color: #333;color: #111;font-size: 60px;}.wp-block-post-date{color: #123456;}.wp-block-post-date a{background-color: #777;color: #555;}.wp-block-image{border-top-left-radius: 10px;border-bottom-right-radius: 1em;margin-bottom: 30px;}.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}', + 'body{--wp--preset--color--grey: grey;--wp--preset--font-family--small: 14px;--wp--preset--font-family--big: 41px;}.wp-block-group{--wp--custom--base-font: 16;--wp--custom--line-height--small: 1.2;--wp--custom--line-height--medium: 1.4;--wp--custom--line-height--large: 1.8;}body{color: var(--wp--preset--color--grey);}a{background-color: #333;color: #111;}.wp-block-group{border-radius: 10px;height: 200px;padding: 24px;}.wp-block-group a{color: #111;}h1,h2,h3,h4,h5,h6{color: #123456;}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{background-color: #333;color: #111;font-size: 60px;}.wp-block-post-date{color: #123456;}.wp-block-post-date a{background-color: #777;color: #555;}.wp-block-image{border-top-left-radius: 10px;border-bottom-right-radius: 1em;margin-bottom: 30px;}.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}', $theme_json->get_stylesheet() ); $this->assertEquals( - 'body{color: var(--wp--preset--color--grey);}a{background-color: #333;color: #111;}.wp-block-group{border-radius: 10px;padding: 24px;}.wp-block-group a{color: #111;}h1,h2,h3,h4,h5,h6{color: #123456;}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{background-color: #333;color: #111;font-size: 60px;}.wp-block-post-date{color: #123456;}.wp-block-post-date a{background-color: #777;color: #555;}.wp-block-image{border-top-left-radius: 10px;border-bottom-right-radius: 1em;margin-bottom: 30px;}.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}', + 'body{color: var(--wp--preset--color--grey);}a{background-color: #333;color: #111;}.wp-block-group{border-radius: 10px;height: 200px;padding: 24px;}.wp-block-group a{color: #111;}h1,h2,h3,h4,h5,h6{color: #123456;}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{background-color: #333;color: #111;font-size: 60px;}.wp-block-post-date{color: #123456;}.wp-block-post-date a{background-color: #777;color: #555;}.wp-block-image{border-top-left-radius: 10px;border-bottom-right-radius: 1em;margin-bottom: 30px;}.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}', $theme_json->get_stylesheet( 'block_styles' ) ); $this->assertEquals(