From 1399bd8e265968742ebefd5d5de94dccff478236 Mon Sep 17 00:00:00 2001
From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com>
Date: Tue, 8 Jun 2021 15:38:21 +1000
Subject: [PATCH] Add width block support
---
lib/block-supports/dimensions.php | 14 +-
.../class-wp-theme-json-gutenberg.php | 5 +-
lib/compat/wordpress-5.9/theme.json | 3 +-
.../src/components/width-control/index.js | 130 +++++++++++++++++
.../src/components/width-control/style.scss | 34 +++++
packages/block-editor/src/hooks/dimensions.js | 33 ++++-
packages/block-editor/src/hooks/test/style.js | 2 +
packages/block-editor/src/hooks/width.js | 131 ++++++++++++++++++
packages/block-editor/src/style.scss | 1 +
packages/blocks/src/api/constants.js | 4 +
packages/components/src/box-control/index.js | 8 +-
.../global-styles/dimensions-panel.js | 35 ++++-
12 files changed, 394 insertions(+), 6 deletions(-)
create mode 100644 packages/block-editor/src/components/width-control/index.js
create mode 100644 packages/block-editor/src/components/width-control/style.scss
create mode 100644 packages/block-editor/src/hooks/width.js
diff --git a/lib/block-supports/dimensions.php b/lib/block-supports/dimensions.php
index f96567dad0b12e..c20f5cdd14ae2b 100644
--- a/lib/block-supports/dimensions.php
+++ b/lib/block-supports/dimensions.php
@@ -61,7 +61,19 @@ function gutenberg_apply_dimensions_support( $block_type, $block_attributes ) {
}
}
- // Width support to be added in near future.
+ // Width.
+
+ // Width support flag can be true|false|"segmented" cannot use
+ // `gutenberg_block_has_support` which checked for boolean true or array.
+ $has_width_support = _wp_array_get( $block_type->supports, array( '__experimentalDimensions', 'width' ), false );
+
+ if ( $has_width_support ) {
+ $width_value = _wp_array_get( $block_attributes, array( 'style', 'dimensions', 'width' ), null );
+
+ if ( null !== $width_value ) {
+ $styles[] = sprintf( 'width: %s;', $width_value );
+ }
+ }
return empty( $styles ) ? array() : array( 'style' => implode( ' ', $styles ) );
}
diff --git a/lib/compat/wordpress-5.9/class-wp-theme-json-gutenberg.php b/lib/compat/wordpress-5.9/class-wp-theme-json-gutenberg.php
index 618d7e3c8c9e26..2db9e62149e39c 100644
--- a/lib/compat/wordpress-5.9/class-wp-theme-json-gutenberg.php
+++ b/lib/compat/wordpress-5.9/class-wp-theme-json-gutenberg.php
@@ -167,7 +167,6 @@ 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' ),
@@ -184,6 +183,8 @@ class WP_Theme_JSON_Gutenberg {
'text-decoration' => array( 'typography', 'textDecoration' ),
'text-transform' => array( 'typography', 'textTransform' ),
'filter' => array( 'filter', 'duotone' ),
+ 'height' => array( 'dimensions', 'height' ),
+ 'width' => array( 'dimensions', 'width' ),
);
/**
@@ -241,6 +242,7 @@ class WP_Theme_JSON_Gutenberg {
'custom' => null,
'dimensions' => array(
'height' => null,
+ 'width' => null,
),
'layout' => array(
'contentSize' => null,
@@ -280,6 +282,7 @@ class WP_Theme_JSON_Gutenberg {
),
'dimensions' => array(
'height' => null,
+ 'width' => null,
),
'color' => array(
'background' => null,
diff --git a/lib/compat/wordpress-5.9/theme.json b/lib/compat/wordpress-5.9/theme.json
index 6444465e35bc74..153aeca538d5e0 100644
--- a/lib/compat/wordpress-5.9/theme.json
+++ b/lib/compat/wordpress-5.9/theme.json
@@ -185,7 +185,8 @@
"text": true
},
"dimensions": {
- "height": false
+ "height": false,
+ "width": false
},
"spacing": {
"blockGap": null,
diff --git a/packages/block-editor/src/components/width-control/index.js b/packages/block-editor/src/components/width-control/index.js
new file mode 100644
index 00000000000000..b59828b728737d
--- /dev/null
+++ b/packages/block-editor/src/components/width-control/index.js
@@ -0,0 +1,130 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ Button,
+ ButtonGroup,
+ __experimentalUnitControl as UnitControl,
+} from '@wordpress/components';
+import { useEffect, useRef, useState } from '@wordpress/element';
+import { edit } from '@wordpress/icons';
+import { __ } from '@wordpress/i18n';
+
+const DEFAULT_WIDTHS = [ '25%', '50%', '75%', '100%' ];
+const DEFAULT_UNIT = '%';
+const MIN_WIDTH = 0;
+
+/**
+ * Determines the CSS unit within the supplied width value.
+ *
+ * @param {string} value Value including CSS unit.
+ * @param {Array} units Available CSS units to validate against.
+ *
+ * @return {string} CSS unit extracted from supplied value.
+ */
+const parseUnit = ( value, units ) => {
+ let unit = String( value )
+ .trim()
+ .match( /[\d.\-\+]*\s*(.*)/ )[ 1 ];
+
+ if ( ! unit ) {
+ return DEFAULT_UNIT;
+ }
+
+ unit = unit.toLowerCase();
+ unit = units.find( ( item ) => item.value === unit );
+
+ return unit?.value || DEFAULT_UNIT;
+};
+
+/**
+ * Width control that will display as either a simple `UnitControl` or a
+ * segmented control containing preset percentage widths. The segmented version
+ * contains a toggle to switch to a UnitControl and Slider for explicit control.
+ *
+ * @param {Object} props Component props.
+ * @return {WPElement} Width control.
+ */
+export default function WidthControl( props ) {
+ const {
+ label = __( 'Width' ),
+ onChange,
+ units,
+ value,
+ isSegmentedControl = false,
+ min = MIN_WIDTH,
+ presetWidths = DEFAULT_WIDTHS,
+ } = props;
+
+ const ref = useRef();
+ const hasCustomValue = value && ! presetWidths.includes( value );
+ const [ customView, setCustomView ] = useState( hasCustomValue );
+ const currentUnit = parseUnit( value, units );
+
+ // When switching to the custom view, move focus to the UnitControl.
+ useEffect( () => {
+ if ( customView && ref.current ) {
+ ref.current.focus();
+ }
+ }, [ customView ] );
+
+ // Unless segmented control is desired return a normal UnitControl.
+ if ( ! isSegmentedControl ) {
+ return (
+
+ );
+ }
+
+ const toggleCustomView = () => {
+ setCustomView( ! customView );
+ };
+
+ const handlePresetChange = ( selectedValue ) => {
+ const newWidth = selectedValue === value ? undefined : selectedValue;
+ onChange( newWidth );
+ };
+
+ const renderCustomView = () => (
+
+ );
+
+ const renderPresetView = () => (
+
+ { presetWidths.map( ( width ) => (
+
+ ) ) }
+
+ );
+
+ return (
+
+ );
+}
diff --git a/packages/block-editor/src/components/width-control/style.scss b/packages/block-editor/src/components/width-control/style.scss
new file mode 100644
index 00000000000000..1d2c04619e3837
--- /dev/null
+++ b/packages/block-editor/src/components/width-control/style.scss
@@ -0,0 +1,34 @@
+.components-width-control.is-segmented {
+ legend {
+ margin-bottom: $grid-unit-10;
+ }
+
+ .components-width-control__wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .components-unit-control-wrapper {
+ flex: 1;
+ margin-right: $grid-unit-10;
+ max-width: 80px;
+ }
+
+ .components-range-control {
+ flex: 1;
+ margin-bottom: 0;
+
+ .components-base-control__field {
+ margin-bottom: 0;
+ height: 30px;
+ }
+ }
+
+ .components-button.is-small.has-icon:not(.has-text) {
+ margin-left: $grid-unit-20;
+ min-width: 30px;
+ height: 30px;
+ padding: 0 4px;
+ }
+}
diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js
index bcf738d19975cf..af363b1eb966d7 100644
--- a/packages/block-editor/src/hooks/dimensions.js
+++ b/packages/block-editor/src/hooks/dimensions.js
@@ -38,6 +38,13 @@ import {
resetPadding,
useIsPaddingDisabled,
} from './padding';
+import {
+ WidthEdit,
+ hasWidthSupport,
+ hasWidthValue,
+ resetWidth,
+ useIsWidthDisabled,
+} from './width';
export const DIMENSIONS_SUPPORT_KEY = '__experimentalDimensions';
export const SPACING_SUPPORT_KEY = 'spacing';
@@ -55,6 +62,7 @@ export function DimensionsPanel( props ) {
const isPaddingDisabled = useIsPaddingDisabled( props );
const isMarginDisabled = useIsMarginDisabled( props );
const isHeightDisabled = useIsHeightDisabled( props );
+ const isWidthDisabled = useIsWidthDisabled( props );
const isDisabled = useIsDimensionsDisabled( props );
const isSupported = hasDimensionsSupport( props.name );
@@ -103,6 +111,21 @@ export function DimensionsPanel( props ) {
) }
+ { ! isWidthDisabled && (
+ hasWidthValue( props ) }
+ label={ __( 'Width' ) }
+ onDeselect={ () => resetWidth( props ) }
+ resetAllFilter={ createResetAllFilter(
+ 'width',
+ 'dimensions'
+ ) }
+ isShownByDefault={ defaultDimensionsControls?.width }
+ panelId={ props.clientId }
+ >
+
+
+ ) }
{ ! isPaddingDisabled && (
hasPaddingValue( props ) }
@@ -167,6 +190,7 @@ export function hasDimensionsSupport( blockName ) {
return (
hasGapSupport( blockName ) ||
hasHeightSupport( blockName ) ||
+ hasWidthSupport( blockName ) ||
hasPaddingSupport( blockName ) ||
hasMarginSupport( blockName )
);
@@ -181,10 +205,17 @@ export function hasDimensionsSupport( blockName ) {
const useIsDimensionsDisabled = ( props = {} ) => {
const gapDisabled = useIsGapDisabled( props );
const heightDisabled = useIsHeightDisabled( props );
+ const widthDisabled = useIsWidthDisabled( props );
const paddingDisabled = useIsPaddingDisabled( props );
const marginDisabled = useIsMarginDisabled( props );
- return gapDisabled && heightDisabled && paddingDisabled && marginDisabled;
+ return (
+ gapDisabled &&
+ heightDisabled &&
+ widthDisabled &&
+ paddingDisabled &&
+ marginDisabled
+ );
};
/**
diff --git a/packages/block-editor/src/hooks/test/style.js b/packages/block-editor/src/hooks/test/style.js
index 121fc114d7dd05..edbcd589a12328 100644
--- a/packages/block-editor/src/hooks/test/style.js
+++ b/packages/block-editor/src/hooks/test/style.js
@@ -25,6 +25,7 @@ describe( 'getInlineStyles', () => {
},
dimensions: {
height: '500px',
+ width: '100%',
},
spacing: {
blockGap: '1em',
@@ -44,6 +45,7 @@ describe( 'getInlineStyles', () => {
height: '500px',
marginBottom: '15px',
paddingTop: '10px',
+ width: '100%',
} );
} );
diff --git a/packages/block-editor/src/hooks/width.js b/packages/block-editor/src/hooks/width.js
new file mode 100644
index 00000000000000..309b5bb8355452
--- /dev/null
+++ b/packages/block-editor/src/hooks/width.js
@@ -0,0 +1,131 @@
+/**
+ * WordPress dependencies
+ */
+import { getBlockSupport } from '@wordpress/blocks';
+import { __experimentalUseCustomUnits as useCustomUnits } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import WidthControl from '../components/width-control';
+import useSetting from '../components/use-setting';
+import { DIMENSIONS_SUPPORT_KEY } from './dimensions';
+import { cleanEmptyObject } from './utils';
+
+/**
+ * Determines if there is width support.
+ *
+ * @param {string|Object} blockType Block name or Block Type object.
+ * @return {boolean} Whether there is support.
+ */
+export function hasWidthSupport( blockType ) {
+ const support = getBlockSupport( blockType, DIMENSIONS_SUPPORT_KEY );
+ return !! ( true === support || support?.width );
+}
+
+/**
+ * Checks if there is a current value in the width block support attributes.
+ *
+ * @param {Object} props Block props.
+ * @return {boolean} Whether or not the block has a width value set.
+ */
+export function hasWidthValue( props ) {
+ return props.attributes.style?.dimensions?.width !== undefined;
+}
+
+/**
+ * Checks whether the segmented width control was opted into via the block's
+ * support configuration.
+ *
+ * @param {string|Object} blockType Block name or Block Type object.
+ * @return {boolean} Whether width control should display as segmented control.
+ */
+export function useIsSegmentedControl( blockType ) {
+ const support = getBlockSupport( blockType, DIMENSIONS_SUPPORT_KEY );
+ return support?.width === 'segmented';
+}
+
+/**
+ * Resets the width block support attributes. This can be used when
+ * disabling the width 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 resetWidth( { attributes = {}, setAttributes } ) {
+ const { style } = attributes;
+
+ setAttributes( {
+ style: {
+ ...style,
+ dimensions: {
+ ...style?.dimensions,
+ width: undefined,
+ },
+ },
+ } );
+}
+
+/**
+ * Custom hook that checks if width controls have been disabled.
+ *
+ * @param {string} name The name of the block.
+ * @return {boolean} Whether width control is disabled.
+ */
+export function useIsWidthDisabled( { name: blockName } = {} ) {
+ const isDisabled = ! useSetting( 'dimensions.width' );
+ return ! hasWidthSupport( blockName ) || isDisabled;
+}
+
+/**
+ * Inspector control panel containing the width related configuration.
+ *
+ * @param {Object} props Block props.
+ * @return {WPElement} Edit component for width.
+ */
+export function WidthEdit( props ) {
+ const {
+ attributes: { style },
+ name,
+ setAttributes,
+ } = props;
+
+ const isSegmentedControl = useIsSegmentedControl( name );
+ const units = useCustomUnits( {
+ availableUnits: useSetting( 'dimensions.units' ) || [
+ '%',
+ 'px',
+ 'em',
+ 'rem',
+ 'vh',
+ 'vw',
+ ],
+ } );
+
+ if ( useIsWidthDisabled( props ) ) {
+ return null;
+ }
+
+ const onChange = ( next ) => {
+ const newStyle = {
+ ...style,
+ dimensions: {
+ ...style?.dimensions,
+ width: next,
+ },
+ };
+
+ setAttributes( { style: cleanEmptyObject( newStyle ) } );
+ };
+
+ return (
+
+ );
+}
diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss
index 6f20a3b1b29012..6a5909bdfe9178 100644
--- a/packages/block-editor/src/style.scss
+++ b/packages/block-editor/src/style.scss
@@ -52,6 +52,7 @@
@import "./components/url-input/style.scss";
@import "./components/url-popover/style.scss";
@import "./components/warning/style.scss";
+@import "./components/width-control/style.scss";
@import "./hooks/anchor.scss";
@import "./hooks/layout.scss";
@import "./hooks/border.scss";
diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js
index 170d3a7e7cdf04..bd9d8dd1095684 100644
--- a/packages/blocks/src/api/constants.js
+++ b/packages/blocks/src/api/constants.js
@@ -116,6 +116,10 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = {
value: [ 'typography', 'letterSpacing' ],
support: [ 'typography', '__experimentalLetterSpacing' ],
},
+ width: {
+ value: [ 'dimensions', 'width' ],
+ support: [ '__experimentalDimensions', 'width' ],
+ },
'--wp--style--block-gap': {
value: [ 'spacing', 'blockGap' ],
support: [ 'spacing', 'blockGap' ],
diff --git a/packages/components/src/box-control/index.js b/packages/components/src/box-control/index.js
index 5779b4a975b184..1b62b6bb29a75f 100644
--- a/packages/components/src/box-control/index.js
+++ b/packages/components/src/box-control/index.js
@@ -47,6 +47,7 @@ function useUniqueId( idProp ) {
return idProp || instanceId;
}
export default function BoxControl( {
+ className,
id: idProp,
inputProps = defaultInputProps,
onChange = noop,
@@ -133,7 +134,12 @@ export default function BoxControl( {
};
return (
-
+
setHeightValue( undefined );
const hasHeightValue = () => !! heightValue;
+ // Width.
+ const [ widthValue, setWidthValue ] = useStyle( 'dimensions.width', name );
+ const resetWidthValue = () => setWidthValue( undefined );
+ const hasWidthValue = () => !! widthValue;
+
const resetAll = () => {
resetHeightValue();
+ resetWidthValue();
resetPaddingValue();
resetMarginValue();
resetGapValue();
@@ -179,6 +195,23 @@ export default function DimensionsPanel( { name } ) {
/>
) }
+ { showWidthControl && (
+
+
+
+ ) }
{ showPaddingControl && (