diff --git a/lib/block-supports/dimensions.php b/lib/block-supports/dimensions.php new file mode 100644 index 0000000000000..0a54063445469 --- /dev/null +++ b/lib/block-supports/dimensions.php @@ -0,0 +1,114 @@ +attributes ) { + $block_type->attributes = array(); + } + + if ( $has_width_support && ! array_key_exists( 'style', $block_type->attributes ) ) { + $block_type->attributes['style'] = array( + 'type' => 'object', + ); + } +} + +/** + * Add CSS classes and inline styles for dimension features 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 Dimensions CSS classes and inline styles. + */ +function gutenberg_apply_dimensions_support( $block_type, $block_attributes ) { + $has_width_support = gutenberg_has_dimensions_support( $block_type, 'width' ); + $styles = array(); + + if ( $has_width_support ) { + $width_style = gutenberg_dimensions_get_css_variable_inline_style( $block_attributes, 'width', 'width' ); + if ( $width_style ) { + $styles[] = $width_style; + } + } + + $attributes = array(); + if ( ! empty( $styles ) ) { + $attributes['style'] = implode( ' ', $styles ); + } + return $attributes; +} + +/** + * Generates an inline style for a dimension feature e.g. width, height. + * + * @param array $attributes Block's attributes. + * @param string $feature Key for the feature within the dimensions styles. + * @param string $css_property Slug for the CSS property the inline style sets. + * + * @return string CSS inline style. + */ +function gutenberg_dimensions_get_css_variable_inline_style( $attributes, $feature, $css_property ) { + // Retrieve current attribute value or skip if not found. + $style_value = gutenberg_experimental_get( $attributes, array( 'style', 'dimensions', $feature ), false ); + if ( ! $style_value ) { + return; + } + + // If we don't have a preset CSS variable, we'll assume it's a regular CSS value. + if ( strpos( $style_value, "var:preset|{$css_property}|" ) === false ) { + return sprintf( '%s: %s;', $css_property, $style_value ); + } + + // We have a preset CSS variable as the style. + // Get the style value from the string and return CSS style. + $index_to_splice = strrpos( $style_value, '|' ) + 1; + $slug = substr( $style_value, $index_to_splice ); + + // Return the actual CSS inline style e.g. `text-decoration:var(--wp--preset--text-decoration--underline);`. + return sprintf( '%s: var(--wp--preset--%s--%s);', $css_property, $css_property, $slug ); +} + + +/** + * Checks whether the current block type supports the experimental feature + * requested. + * + * @param WP_Block_Type $block_type Block type to check for support. + * @param string $feature Name of the feature to check support for. + * @param mixed $default Fallback value for feature support, defaults to false. + * + * @return boolean Whether or not the feature is supported. + */ +function gutenberg_has_dimensions_support( $block_type, $feature, $default = false ) { + $block_support = false; + if ( property_exists( $block_type, 'supports' ) ) { + $block_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalDimensions' ), $default ); + } + + return true === $block_support || ( is_array( $block_support ) && gutenberg_experimental_get( $block_support, array( $feature ), false ) ); +} + +// Register the block support. +WP_Block_Supports::get_instance()->register( + 'dimensions', + array( + 'register_attribute' => 'gutenberg_register_dimensions_support', + 'apply' => 'gutenberg_apply_dimensions_support', + ) +); diff --git a/lib/experimental-default-theme.json b/lib/experimental-default-theme.json index 28a20292ff3f8..069e5a1c436b2 100644 --- a/lib/experimental-default-theme.json +++ b/lib/experimental-default-theme.json @@ -165,6 +165,30 @@ "spacing": { "customPadding": false, "units": [ "px", "em", "rem", "vh", "vw" ] + }, + "dimensions": { + "width": [ + { + "name": "25%", + "slug": "25", + "value": "25%" + }, + { + "name": "50%", + "slug": "50", + "value": "50%" + }, + { + "name": "75%", + "slug": "75", + "value": "75%" + }, + { + "name": "100%", + "slug": "100", + "value": "100%" + } + ] } } } diff --git a/lib/global-styles.php b/lib/global-styles.php index 2b4173f58d9e2..592adaf8f662d 100644 --- a/lib/global-styles.php +++ b/lib/global-styles.php @@ -407,6 +407,7 @@ function gutenberg_experimental_global_styles_get_style_property() { 'color' => array( 'color', 'text' ), 'fontSize' => array( 'typography', 'fontSize' ), 'lineHeight' => array( 'typography', 'lineHeight' ), + 'width' => array( 'dimensions', 'width' ), ); } @@ -423,6 +424,7 @@ function gutenberg_experimental_global_styles_get_support_keys() { 'color' => array( 'color' ), 'fontSize' => array( 'fontSize' ), 'lineHeight' => array( 'lineHeight' ), + 'width' => array( '__experimentalDimensions', 'width' ), ); } @@ -445,6 +447,10 @@ function gutenberg_experimental_global_styles_get_presets_structure() { 'path' => array( 'typography', 'fontSizes' ), 'key' => 'size', ), + 'width' => array( + 'path' => array( 'dimensions', 'width' ), + 'key' => 'value', + ), ); } @@ -819,6 +825,7 @@ function gutenberg_experimental_global_styles_normalize_schema( $tree ) { 'custom' => array(), 'typography' => array(), 'spacing' => array(), + 'dimensions' => array(), ), ); diff --git a/lib/load.php b/lib/load.php index 42ebfe7bb5733..6b13fd47e2935 100644 --- a/lib/load.php +++ b/lib/load.php @@ -129,3 +129,4 @@ function gutenberg_is_experiment_enabled( $name ) { require dirname( __FILE__ ) . '/block-supports/align.php'; require dirname( __FILE__ ) . '/block-supports/typography.php'; require dirname( __FILE__ ) . '/block-supports/custom-classname.php'; +require dirname( __FILE__ ) . '/block-supports/dimensions.php'; 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 0000000000000..f98e9951054c0 --- /dev/null +++ b/packages/block-editor/src/components/width-control/index.js @@ -0,0 +1,62 @@ +/** + * WordPress dependencies + */ +import { Button, ButtonGroup } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Control to facilitate width selections. + * + * @param {Object} props Component props. + * @param {string} props.value Currently selected width. + * @param {Array} props.widthOptions Width options available for selection. + * @param {Function} props.onChange Handles change in width selection. + * @return {WPElement} Width control. + */ +export default function WidthControl( { + value: selectedWidth, + widthOptions, + onChange, +} ) { + /** + * Determines the new width as a result of user interaction with + * the control. Then passes this to the supplied onChange handler. + * + * @param {string} newWidth Slug for selected width + */ + const handleChange = ( newWidth ) => { + // Check if we are toggling the width off + const width = selectedWidth === newWidth ? undefined : newWidth; + + // Ensure only predefined width options are allowed + const presetWidth = widthOptions.find( ( { slug } ) => slug === width ); + + // Create string that will be turned into custom CSS property + const customWidthProperty = presetWidth + ? `var:preset|width|${ presetWidth.slug }` + : undefined; + + // Pass on to the supplied handler. + onChange( customWidthProperty ); + }; + + return ( + <> +

{ __( 'Width' ) }

+ + { widthOptions.map( ( widthOption ) => { + return ( + + ); + } ) } + + + ); +} diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js new file mode 100644 index 0000000000000..34dc43a68fca3 --- /dev/null +++ b/packages/block-editor/src/hooks/dimensions.js @@ -0,0 +1,111 @@ +/** + * WordPress dependencies + */ +import { hasBlockSupport } from '@wordpress/blocks'; +import { PanelBody } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { cleanEmptyObject } from './utils'; +import InspectorControls from '../components/inspector-controls'; +import useEditorFeature from '../components/use-editor-feature'; +import WidthControl from '../components/width-control'; + +/** + * Key within block settings' supports array indicating support for + * dimensions including width, e.g. settings found in 'block.json'. + */ +export const DIMENSIONS_SUPPORT_KEY = '__experimentalDimensions'; + +export function DimensionsPanel( props ) { + const { + attributes: { style }, + setAttributes, + } = props; + + const widthOptions = useEditorFeature( 'dimensions.width' ); + const isEnabled = useIsWidthEnabled( props ); + + if ( ! isEnabled ) { + return null; + } + + const selectedWidth = getWidthFromAttributeValue( + widthOptions, + style?.dimensions?.width + ); + + function onChange( newWidth ) { + setAttributes( { + style: cleanEmptyObject( { + ...style, + dimensions: { + ...style?.dimensions, + width: newWidth, + }, + } ), + } ); + } + + return ( + + + + + + ); +} + +/** + * Checks if there is block support for width. + * + * @param {string|Object} blockType Block name or Block Type object. + * @return {boolean} Whether there is support. + */ +export function hasWidthSupport( blockName ) { + const support = hasBlockSupport( blockName, DIMENSIONS_SUPPORT_KEY ); + + // Further dimension properties to be added in future iterations. + // e.g. support && ( support.width || support.height ) + return true === support || ( support && support.width ); +} + +/** + * Checks if width is supported and has not been disabled. + * + * @param {string|Object} blockType Block name or Block Type object. + * @return {boolean} Whether there is support. + */ +export function useIsWidthEnabled( { name: blockName } = {} ) { + const supported = hasWidthSupport( blockName ); + const widthOptions = useEditorFeature( 'dimensions.width' ); + const hasWidthOptions = !! widthOptions?.length; + + return supported && hasWidthOptions; +} + +/** + * Extracts the current width selection, if available, from the CSS variable + * set as the 'styles.width' attribute. + * + * @param {Array} widthOptions Available width options as defined in theme.json + * @param {string} value Attribute value in `styles.width` + * @return {string} Actual width value + */ +const getWidthFromAttributeValue = ( widthOptions, value ) => { + const attributeParsed = /var:preset\|width\|(.+)/.exec( value ); + + if ( attributeParsed && attributeParsed[ 1 ] ) { + return widthOptions.find( + ( { slug } ) => slug === attributeParsed[ 1 ] + )?.slug; + } + + return value; +}; diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index adec7bb0d02df..7f3bb76e3825c 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -20,11 +20,13 @@ import { COLOR_SUPPORT_KEY, ColorEdit } from './color'; import { TypographyPanel, TYPOGRAPHY_SUPPORT_KEYS } from './typography'; import { SPACING_SUPPORT_KEY, PaddingEdit } from './padding'; import SpacingPanelControl from '../components/spacing-panel-control'; +import { DIMENSIONS_SUPPORT_KEY, DimensionsPanel } from './dimensions'; const styleSupportKeys = [ ...TYPOGRAPHY_SUPPORT_KEYS, COLOR_SUPPORT_KEY, SPACING_SUPPORT_KEY, + DIMENSIONS_SUPPORT_KEY, ]; const hasStyleSupport = ( blockType ) => @@ -156,6 +158,7 @@ export const withBlockControls = createHigherOrderComponent( return [ , , + , , hasSpacingSupport && ( diff --git a/packages/block-library/src/button/block.json b/packages/block-library/src/button/block.json index b0cff94836069..5d719856f7619 100644 --- a/packages/block-library/src/button/block.json +++ b/packages/block-library/src/button/block.json @@ -59,6 +59,12 @@ "align": true, "alignWide": false, "reusable": false, - "__experimentalSelector": ".wp-block-button > a" + "__experimentalSelector": ".wp-block-button > a", + "__experimentalDimensions": { + "width": true + }, + "color": { + "gradients": true + } } } diff --git a/packages/block-library/src/button/deprecated.js b/packages/block-library/src/button/deprecated.js index 110807907fc3c..db1e72e303257 100644 --- a/packages/block-library/src/button/deprecated.js +++ b/packages/block-library/src/button/deprecated.js @@ -10,9 +10,15 @@ import classnames from 'classnames'; import { RichText, getColorClassName, + useBlockProps, __experimentalGetGradientClass, } from '@wordpress/block-editor'; +/** + * Internal dependencies + */ +import getColorAndStyleProps from './color-props'; + const migrateCustomColorsAndGradients = ( attributes ) => { if ( ! attributes.customTextColor && @@ -81,6 +87,85 @@ const blockAttributes = { }; const deprecated = [ + { + supports: { + anchor: true, + align: true, + alignWide: false, + reusable: false, + __experimentalSelector: '.wp-block-button > a', + }, + attributes: { + ...blockAttributes, + linkTarget: { + type: 'string', + source: 'attribute', + selector: 'a', + attribute: 'target', + }, + rel: { + type: 'string', + source: 'attribute', + selector: 'a', + attribute: 'rel', + }, + placeholder: { + type: 'string', + }, + borderRadius: { + type: 'number', + }, + backgroundColor: { + type: 'string', + }, + textColor: { + type: 'string', + }, + gradient: { + type: 'string', + }, + style: { + type: 'object', + }, + }, + save( { attributes } ) { + const { + borderRadius, + linkTarget, + rel, + text, + title, + url, + } = attributes; + const colorProps = getColorAndStyleProps( attributes ); + const buttonClasses = classnames( + 'wp-block-button__link', + colorProps.className, + { + 'no-border-radius': borderRadius === 0, + } + ); + const buttonStyle = { + borderRadius: borderRadius ? borderRadius + 'px' : undefined, + ...colorProps.style, + }; + + return ( +
+ +
+ ); + }, + }, { supports: { align: true, diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index 690913ea01632..95a2bfc2e9e21 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -24,25 +24,16 @@ import { RichText, useBlockProps, __experimentalLinkControl as LinkControl, - __experimentalUseEditorFeature as useEditorFeature, } from '@wordpress/block-editor'; import { rawShortcut, displayShortcut } from '@wordpress/keycodes'; import { link, linkOff } from '@wordpress/icons'; import { createBlock } from '@wordpress/blocks'; -/** - * Internal dependencies - */ -import ColorEdit from './color-edit'; -import getColorAndStyleProps from './color-props'; - const NEW_TAB_REL = 'noreferrer noopener'; const MIN_BORDER_RADIUS_VALUE = 0; const MAX_BORDER_RADIUS_VALUE = 50; const INITIAL_BORDER_RADIUS_POSITION = 5; -const EMPTY_ARRAY = []; - function BorderPanel( { borderRadius = '', setAttributes } ) { const initialBorderRadius = borderRadius; const setBorderRadius = useCallback( @@ -175,7 +166,6 @@ function ButtonEdit( props ) { }, [ setAttributes ] ); - const colors = useEditorFeature( 'color.palette' ) || EMPTY_ARRAY; const onToggleOpenInNewTab = useCallback( ( value ) => { @@ -196,12 +186,18 @@ function ButtonEdit( props ) { [ rel, setAttributes ] ); - const colorProps = getColorAndStyleProps( attributes, colors, true ); const blockProps = useBlockProps(); + // Temporarily, we need to add the border radius to the blockProps so + // that it is applied at the block level. This can be replaced by using + // the border radius block support. + blockProps.style = { + ...blockProps.style, + borderRadius: borderRadius ? borderRadius + 'px' : undefined, + }; + return ( <> -
createBlock( 'core/button', { ...attributes, diff --git a/packages/block-library/src/button/save.js b/packages/block-library/src/button/save.js index 2a49d4e5b87aa..e169e1a81e693 100644 --- a/packages/block-library/src/button/save.js +++ b/packages/block-library/src/button/save.js @@ -8,24 +8,18 @@ import classnames from 'classnames'; */ import { RichText, useBlockProps } from '@wordpress/block-editor'; -/** - * Internal dependencies - */ -import getColorAndStyleProps from './color-props'; - export default function save( { attributes } ) { const { borderRadius, linkTarget, rel, text, title, url } = attributes; - const colorProps = getColorAndStyleProps( attributes ); - const buttonClasses = classnames( - 'wp-block-button__link', - colorProps.className, - { - 'no-border-radius': borderRadius === 0, - } - ); - const buttonStyle = { + const buttonClasses = classnames( 'wp-block-button__link', { + 'no-border-radius': borderRadius === 0, + } ); + const blockProps = useBlockProps.save(); + // Temporarily, we need to add the border radius to the blockProps so + // that it is applied at the block level. This can be replaced by using + // the border radius block support. + blockProps.style = { + ...blockProps.style, borderRadius: borderRadius ? borderRadius + 'px' : undefined, - ...colorProps.style, }; // The use of a `title` attribute here is soft-deprecated, but still applied @@ -33,13 +27,12 @@ export default function save( { attributes } ) { // A title will no longer be assigned for new or updated button block links. return ( -
+
.wp-block-button { + &.has-text-color .wp-block-button__link { + color: inherit; + } + + &.has-background { + display: inline-block; + .wp-block-button__link { + background-color: inherit; + } + } + + &[style*="border-radius:"] .wp-block-button__link { + border-radius: inherit; + } +} + // Prefer the link selector instead of the regular button classname // to support the previous markup in addition to the new one. .wp-block-button__link { @@ -53,3 +72,11 @@ $blocks-button__height: 3.1em; background-color: transparent; border: 2px solid; } + +// Set the inner button to 100% width when a custom width +// has been set on the parent +div[style*="--wp--preset--width"] { + .wp-block-button__link { + width: 100%; + } +} diff --git a/packages/block-library/src/buttons/editor.scss b/packages/block-library/src/buttons/editor.scss index 121ba5f5af5cf..fdc225489f7b7 100644 --- a/packages/block-library/src/buttons/editor.scss +++ b/packages/block-library/src/buttons/editor.scss @@ -3,10 +3,13 @@ margin-left: 0; } -.wp-block[data-align="center"] > .wp-block-buttons { +.wp-block > .wp-block-buttons { display: flex; - align-items: center; flex-wrap: wrap; +} + +.wp-block[data-align="center"] > .wp-block-buttons { + align-items: center; justify-content: center; } diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index 18488d1db55f7..7bbfc0f451d69 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -23,4 +23,5 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { paddingLeft: [ 'spacing', 'padding', 'left' ], paddingRight: [ 'spacing', 'padding', 'right' ], paddingTop: [ 'spacing', 'padding', 'top' ], + width: [ 'dimensions', 'width' ], }; diff --git a/packages/edit-site/src/components/editor/utils.js b/packages/edit-site/src/components/editor/utils.js index 2655b5f86af48..0bc1b60bb5af3 100644 --- a/packages/edit-site/src/components/editor/utils.js +++ b/packages/edit-site/src/components/editor/utils.js @@ -4,6 +4,7 @@ export const PRESET_CATEGORIES = { color: { path: [ 'color', 'palette' ], key: 'color' }, gradient: { path: [ 'color', 'gradients' ], key: 'gradient' }, fontSize: { path: [ 'typography', 'fontSizes' ], key: 'size' }, + width: { path: [ 'dimensions', 'width' ], key: 'value' }, }; export const LINK_COLOR = '--wp--style--color--link'; export const LINK_COLOR_DECLARATION = `a { color: var(${ LINK_COLOR }, #00e); }`; diff --git a/phpunit/class-block-supported-styles-test.php b/phpunit/class-block-supported-styles-test.php index 19765f6a2b661..4c84296f969b2 100644 --- a/phpunit/class-block-supported-styles-test.php +++ b/phpunit/class-block-supported-styles-test.php @@ -572,6 +572,64 @@ function test_block_alignment_unsupported() { $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } + /** + * Tests width support + */ + function test_width() { + $block_type_settings = array( + 'attributes' => array(), + 'supports' => array( + '__experimentalDimensions' => array( 'width' => true ), + ), + 'render_callback' => true, + ); + $this->register_block_type( 'core/example', $block_type_settings ); + + $block = array( + 'blockName' => 'core/example', + 'attrs' => array( + 'style' => array( + 'dimensions' => array( 'width' => 'var:preset|width|75' ), + ), + ), + 'innerBlock' => array(), + 'innerContent' => array(), + 'innerHTML' => array(), + ); + + $expected_classes = 'foo-bar-class wp-block-example'; + $expected_styles = 'test: style; width: var(--wp--preset--width--75);'; + + $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); + } + + /** + * Tests width not applied without support flag. + */ + function test_width_unsupported() { + $block_type_settings = array( + 'attributes' => array(), + 'supports' => array(), + 'render_callback' => true, + ); + $this->register_block_type( 'core/example', $block_type_settings ); + + $block = array( + 'blockName' => 'core/example', + 'attrs' => array( + 'style' => array( 'dimensions' => array( 'width' => 'var:preset|width|75' ) ), + ), + 'innerBlock' => array(), + 'innerContent' => array(), + 'innerHTML' => array(), + ); + + $expected_classes = 'foo-bar-class wp-block-example'; + $expected_styles = 'test: style;'; + + $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); + } + /** * Tests all support flags together to ensure they work together as expected. */ @@ -579,13 +637,16 @@ function test_all_supported() { $block_type_settings = array( 'attributes' => array(), 'supports' => array( - 'color' => array( + 'color' => array( 'gradients' => true, 'link' => true, ), - 'fontSize' => true, - 'lineHeight' => true, - 'align' => true, + 'fontSize' => true, + 'lineHeight' => true, + 'align' => true, + '__experimentalDimensions' => array( + 'width' => true, + ), ), ); $this->register_block_type( 'core/example', $block_type_settings ); @@ -604,6 +665,7 @@ function test_all_supported() { 'lineHeight' => '20', 'fontSize' => '10', ), + 'dimensions' => array( 'width' => 'var:preset|width|75' ), ), ), 'innerBlock' => array(), @@ -612,7 +674,7 @@ function test_all_supported() { ); $expected_classes = 'foo-bar-class wp-block-example has-text-color has-background alignwide'; - $expected_styles = 'test: style; color: #000; background-color: #fff; font-size: 10px; line-height: 20;'; + $expected_styles = 'test: style; color: #000; background-color: #fff; font-size: 10px; line-height: 20; width: var(--wp--preset--width--75);'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); }