From 081b8438b5f975f7395bc40e359a5a137daed5e8 Mon Sep 17 00:00:00 2001 From: ramonjd Date: Wed, 27 Apr 2022 15:50:25 +1000 Subject: [PATCH] Building on work in #40082 and #40332 by adding border support. Allowing for border side styles, e.g., border.top.width et. al. --- lib/block-supports/border.php | 116 ++------ .../style-engine/class-wp-style-engine.php | 254 ++++++++++++++---- .../phpunit/class-wp-style-engine-test.php | 81 +++++- phpunit/block-supports/border-test.php | 8 +- 4 files changed, 307 insertions(+), 152 deletions(-) diff --git a/lib/block-supports/border.php b/lib/block-supports/border.php index d4d18fd6cb687e..9f9053cf970052 100644 --- a/lib/block-supports/border.php +++ b/lib/block-supports/border.php @@ -48,9 +48,8 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) { return array(); } - $classes = array(); - $styles = array(); - $sides = array( 'top', 'right', 'bottom', 'left' ); + $sides = array( 'top', 'right', 'bottom', 'left' ); + $border_block_styles = array(); $has_border_color_support = gutenberg_has_border_feature_support( $block_type, 'color' ); $has_border_width_support = gutenberg_has_border_feature_support( $block_type, 'width' ); @@ -63,21 +62,11 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) { ) { $border_radius = $block_attributes['style']['border']['radius']; - if ( is_array( $border_radius ) ) { - // We have individual border radius corner values. - foreach ( $border_radius as $key => $radius ) { - // Convert CamelCase corner name to kebab-case. - $corner = strtolower( preg_replace( '/(? isset( $border['width'] ) && ! gutenberg_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'width' ) ? $border['width'] : null, + 'color' => isset( $border['color'] ) && ! gutenberg_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'color' ) ? $border['color'] : null, + 'style' => isset( $border['style'] ) && ! gutenberg_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'style' ) ? $border['style'] : null, + ); + $border_block_styles[ $side ] = $border_side_values; } } // Collect classes and styles. $attributes = array(); + $styles = gutenberg_style_engine_generate( array( 'border' => $border_block_styles ) ); - if ( ! empty( $classes ) ) { - $attributes['class'] = implode( ' ', $classes ); + if ( ! empty( $styles['classnames'] ) ) { + $attributes['class'] = $styles['classnames']; } - if ( ! empty( $styles ) ) { - $attributes['style'] = implode( ' ', $styles ); + if ( ! empty( $styles['css'] ) ) { + $attributes['style'] = $styles['css']; } return $attributes; } -/** - * Generates longhand CSS styles for an individual side border. - * - * If some values are omitted from the border configuration, using shorthand - * styles would lead to `initial` values being used instead of the more - * desirable inherited values. This could also lead to browser inconsistencies. - * - * @param string $side The side the styles are being generated for. - * @param array $border Array containing border color, style, and width values. - * @param WP_Block_Type $block_type Block type. - * - * @return array Longhand CSS border styles for a single side. - */ -function gutenberg_generate_individual_border_classes_and_styles( $side, $border, $block_type ) { - $styles = array(); - - if ( - isset( $border['width'] ) && - null !== $border['width'] && - ! gutenberg_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'width' ) - ) { - $styles[] = sprintf( 'border-%s-width: %s;', $side, $border['width'] ); - } - - if ( - isset( $border['style'] ) && - null !== $border['style'] && - ! gutenberg_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'style' ) - ) { - $styles[] = sprintf( 'border-%s-style: %s;', $side, $border['style'] ); - } - - $border_color = _wp_array_get( $border, array( 'color' ), null ); - - if ( - $border_color && - ! gutenberg_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'color' ) - ) { - $has_color_preset = strpos( $border_color, 'var:preset|color|' ) !== false; - if ( $has_color_preset ) { - $named_color_slug = substr( $border_color, strrpos( $border_color, '|' ) + 1 ); - $styles [] = sprintf( 'border-%s-color: var(--wp--preset--color--%s);', $side, $named_color_slug ); - } else { - $styles [] = sprintf( 'border-%s-color: %s;', $side, $border['color'] ); - } - } - - return $styles; -} - /** * Checks whether the current block type supports the border feature requested. * diff --git a/packages/style-engine/class-wp-style-engine.php b/packages/style-engine/class-wp-style-engine.php index 5687a2f422186e..4090fb65d44dae 100644 --- a/packages/style-engine/class-wp-style-engine.php +++ b/packages/style-engine/class-wp-style-engine.php @@ -37,84 +37,177 @@ class WP_Style_Engine { * - classnames => an array of classnames to be returned for block styles. The key is a classname or pattern. * A value of `true` means the classname should be applied always. Otherwise a valid CSS property * to match the incoming value, e.g., "color" to match var:preset|color|somePresetName. - * - property_key => the key that represents a valid CSS property, e.g., "margin" or "border". + * - properties => an array of `key => value` pairs that represents valid CSS properties for the style, e.g., "margin" or "border". + * A 'default' property, corresponding to a single or shorthand value, is required. * - path => a path that accesses the corresponding style value in the block style object. + * - value_func => a function to generate an array of valid CSS rules for a particular style object. + * For example, `'padding' => 'array( 'top' => '1em' )` will return `array( 'padding-top' => '1em' )` */ const BLOCK_STYLE_DEFINITIONS_METADATA = array( 'color' => array( 'text' => array( - 'property_key' => 'color', - 'path' => array( 'color', 'text' ), - 'classnames' => array( - 'has-text-color' => true, - 'has-%s-color' => 'color', + 'properties' => array( + 'default' => 'color', + ), + 'path' => array( 'color', 'text' ), + 'classnames' => array( + 'has-text-color' => true, + 'has-$slug-color' => 'color', ), ), 'background' => array( - 'property_key' => 'background-color', - 'path' => array( 'color', 'background' ), - 'classnames' => array( - 'has-background' => true, - 'has-%s-background-color' => 'color', + 'properties' => array( + 'default' => 'background-color', + ), + 'path' => array( 'color', 'background' ), + 'classnames' => array( + 'has-background' => true, + 'has-$slug-background-color' => 'color', ), ), 'gradient' => array( - 'property_key' => 'background', - 'path' => array( 'color', 'gradient' ), - 'classnames' => array( - 'has-background' => true, - 'has-%s-gradient-background' => 'gradient', + 'properties' => array( + 'default' => 'background', + ), + 'path' => array( 'color', 'gradient' ), + 'classnames' => array( + 'has-background' => true, + 'has-$slug-gradient-background' => 'gradient', + ), + ), + ), + 'border' => array( + 'color' => array( + 'properties' => array( + 'default' => 'border-color', + 'sides' => 'border-$side-color', + ), + 'path' => array( 'border', 'color' ), + 'classnames' => array( + 'has-border-color' => true, + 'has-$slug-border-color' => 'color', + ), + ), + 'radius' => array( + 'properties' => array( + 'default' => 'border-radius', + 'sides' => 'border-$side-radius', + ), + 'path' => array( 'border', 'radius' ), + ), + 'style' => array( + 'properties' => array( + 'default' => 'border-style', + 'sides' => 'border-$side-style', + ), + 'path' => array( 'border', 'style' ), + ), + 'width' => array( + 'properties' => array( + 'default' => 'border-width', + 'sides' => 'border-$side-width', + ), + 'path' => array( 'border', 'width' ), + ), + 'top' => array( + 'value_func' => 'static::get_css_side_rules', + 'path' => array( 'border', 'top' ), + 'css_vars' => array( + 'color' => '--wp--preset--color--$slug', + ), + ), + 'right' => array( + 'value_func' => 'static::get_css_side_rules', + 'path' => array( 'border', 'right' ), + 'css_vars' => array( + 'color' => '--wp--preset--color--$slug', + ), + ), + 'bottom' => array( + 'value_func' => 'static::get_css_side_rules', + 'path' => array( 'border', 'bottom' ), + 'css_vars' => array( + 'color' => '--wp--preset--color--$slug', + ), + ), + 'left' => array( + 'value_func' => 'static::get_css_side_rules', + 'path' => array( 'border', 'left' ), + 'css_vars' => array( + 'color' => '--wp--preset--color--$slug', ), ), ), 'spacing' => array( 'padding' => array( - 'property_key' => 'padding', - 'path' => array( 'spacing', 'padding' ), + 'properties' => array( + 'default' => 'padding', + 'sides' => 'padding-$side', + ), + 'path' => array( 'spacing', 'padding' ), ), 'margin' => array( - 'property_key' => 'margin', - 'path' => array( 'spacing', 'margin' ), + 'properties' => array( + 'default' => 'margin', + 'sides' => 'margin-$side', + ), + 'path' => array( 'spacing', 'margin' ), ), ), 'typography' => array( 'fontSize' => array( - 'property_key' => 'font-size', - 'path' => array( 'typography', 'fontSize' ), - 'classnames' => array( - 'has-%s-font-size' => 'font-size', + 'properties' => array( + 'default' => 'font-size', + ), + 'path' => array( 'typography', 'fontSize' ), + 'classnames' => array( + 'has-$slug-font-size' => 'font-size', ), ), 'fontFamily' => array( - 'property_key' => 'font-family', - 'path' => array( 'typography', 'fontFamily' ), - 'classnames' => array( - 'has-%s-font-family' => 'font-family', + 'properties' => array( + 'default' => 'font-family', + ), + 'path' => array( 'typography', 'fontFamily' ), + 'classnames' => array( + 'has-$slug-font-family' => 'font-family', ), ), 'fontStyle' => array( - 'property_key' => 'font-style', - 'path' => array( 'typography', 'fontStyle' ), + 'properties' => array( + 'default' => 'font-style', + ), + 'path' => array( 'typography', 'fontStyle' ), ), 'fontWeight' => array( - 'property_key' => 'font-weight', - 'path' => array( 'typography', 'fontWeight' ), + 'properties' => array( + 'default' => 'font-weight', + ), + 'path' => array( 'typography', 'fontWeight' ), ), 'lineHeight' => array( - 'property_key' => 'line-height', - 'path' => array( 'typography', 'lineHeight' ), + 'properties' => array( + 'default' => 'line-height', + ), + 'path' => array( 'typography', 'lineHeight' ), ), 'textDecoration' => array( - 'property_key' => 'text-decoration', - 'path' => array( 'typography', 'textDecoration' ), + 'properties' => array( + 'default' => 'text-decoration', + ), + 'path' => array( 'typography', 'textDecoration' ), ), 'textTransform' => array( - 'property_key' => 'text-transform', - 'path' => array( 'typography', 'textTransform' ), + 'properties' => array( + 'default' => 'text-transform', + ), + 'path' => array( 'typography', 'textTransform' ), ), 'letterSpacing' => array( - 'property_key' => 'letter-spacing', - 'path' => array( 'typography', 'letterSpacing' ), + 'properties' => array( + 'default' => 'letter-spacing', + ), + 'path' => array( 'typography', 'letterSpacing' ), ), ), ); @@ -173,7 +266,7 @@ protected static function get_classnames( $style_value, $style_definition ) { // One day, if there are no stored schemata, we could allow custom patterns or // generate classnames based on other properties // such as a path or a value or a prefix passed in options. - $classnames[] = sprintf( $classname, $slug ); + $classnames[] = strtr( $classname, array( '$slug' => $slug ) ); } } } @@ -190,13 +283,21 @@ protected static function get_classnames( $style_value, $style_definition ) { * @return array An array of CSS rules. */ protected static function get_css( $style_value, $style_definition ) { + $css = array(); + + if ( + isset( $style_definition['value_func'] ) && + is_callable( $style_definition['value_func'] ) + ) { + return call_user_func( $style_definition['value_func'], $style_value, $style_definition ); + } + // Low-specificity check to see if the value is a CSS preset. if ( is_string( $style_value ) && strpos( $style_value, 'var:' ) !== false ) { - return array(); + return $css; } - // If required in the future, style definitions could define a callable `value_func` to generate custom CSS rules. - return static::get_css_rules( $style_value, $style_definition['property_key'] ); + return static::get_css_rules( $style_value, $style_definition ); } /** @@ -220,8 +321,11 @@ public function generate( $block_styles ) { $styles_output = array(); // Collect CSS and classnames. - foreach ( self::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group ) { - foreach ( $definition_group as $style_definition ) { + foreach ( self::BLOCK_STYLE_DEFINITIONS_METADATA as $definition_group_key => $definition_group_style ) { + if ( empty( $block_styles[ $definition_group_key ] ) ) { + continue; + } + foreach ( $definition_group_style as $style_definition ) { $style_value = _wp_array_get( $block_styles, $style_definition['path'], null ); if ( empty( $style_value ) ) { @@ -261,29 +365,75 @@ public function generate( $block_styles ) { /** * Default style value parser that returns a CSS ruleset. * If the input contains an array, it will be treated like a box model - * for styles such as margins and padding + * for styles such as margins and padding. * - * @param string|array $style_value A single raw Gutenberg style attributes value for a CSS property. - * @param string $style_property The CSS property for which we're creating a rule. + * @param string|array $style_value A single raw Gutenberg style attributes value for a CSS property. + * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. * * @return array The class name for the added style. */ - protected static function get_css_rules( $style_value, $style_property ) { + protected static function get_css_rules( $style_value, $style_definition ) { $rules = array(); if ( ! $style_value ) { return $rules; } + $style_properties = $style_definition['properties']; + // We assume box model-like properties. if ( is_array( $style_value ) ) { foreach ( $style_value as $key => $value ) { - $rules[ "$style_property-$key" ] = $value; + $side_property = strtr( $style_properties['sides'], array( '$side' => _wp_to_kebab_case( $key ) ) ); + $rules[ $side_property ] = $value; } } else { - $rules[ $style_property ] = $style_value; + $rules[ $style_properties['default'] ] = $style_value; + } + + return $rules; + } + + /** + * Style value parser that returns a CSS ruleset for style groups that have 'top', 'right', 'bottom', 'left' keys. + * E.g., `border.top{color|width|style}. + * + * @param array $style_value A single raw Gutenberg style attributes value for a CSS property. + * @param array $style_definition A single style definition from BLOCK_STYLE_DEFINITIONS_METADATA. + * + * @return array The class name for the added style. + */ + protected static function get_css_side_rules( $style_value, $style_definition ) { + $rules = array(); + + if ( ! is_array( $style_value ) || empty( $style_value ) ) { + return $rules; } + foreach ( $style_value as $css_property => $value ) { + if ( empty( $value ) ) { + continue; + } + // The first item in the style definition path array tells us the style property, e.g., "border". + // We use this to get a corresponding CSS style definition such as "color" or "width" from the same group. + $side_style_definition_path = array( $style_definition['path'][0], $css_property ); + $side_style_definition = _wp_array_get( self::BLOCK_STYLE_DEFINITIONS_METADATA, $side_style_definition_path, null ); + + if ( $side_style_definition && isset( $side_style_definition['properties']['sides'] ) ) { + // Set a CSS var if there is a valid preset value. + $slug = isset( $style_definition['css_vars'][ $css_property ] ) ? static::get_slug_from_preset_value( $value, $css_property ) : null; + if ( $slug ) { + $css_var = strtr( + $style_definition['css_vars'][ $css_property ], + array( '$slug' => $slug ) + ); + $value = "var($css_var)"; + } + // The second item in the style definition path array refers to the side property, e.g., "top". + $side_css_property = strtr( $side_style_definition['properties']['sides'], array( '$side' => $style_definition['path'][1] ) ); + $rules[ $side_css_property ] = $value; + } + } return $rules; } } diff --git a/packages/style-engine/phpunit/class-wp-style-engine-test.php b/packages/style-engine/phpunit/class-wp-style-engine-test.php index f9830e173e11a7..0a5d19a2c8f474 100644 --- a/packages/style-engine/phpunit/class-wp-style-engine-test.php +++ b/packages/style-engine/phpunit/class-wp-style-engine-test.php @@ -70,10 +70,15 @@ public function data_generate_styles_fixtures() { 'spacing' => array( 'margin' => '111px', ), + 'border' => array( + 'color' => 'var:preset|color|cool-caramel', + 'width' => '2rem', + 'style' => 'dotted', + ), ), 'expected_output' => array( - 'css' => 'margin: 111px;', - 'classnames' => 'has-text-color has-texas-flood-color', + 'css' => 'border-style: dotted; border-width: 2rem; margin: 111px;', + 'classnames' => 'has-text-color has-texas-flood-color has-border-color has-cool-caramel-border-color', ), ), @@ -93,9 +98,17 @@ public function data_generate_styles_fixtures() { 'right' => '10em', ), ), + 'border' => array( + 'radius' => array( + 'topLeft' => '99px', + 'topRight' => '98px', + 'bottomLeft' => '97px', + 'bottomRight' => '96px', + ), + ), ), 'expected_output' => array( - 'css' => 'padding-top: 42px; padding-left: 2%; padding-bottom: 44px; padding-right: 5rem; margin-top: 12rem; margin-left: 2vh; margin-bottom: 2px; margin-right: 10em;', + 'css' => 'border-top-left-radius: 99px; border-top-right-radius: 98px; border-bottom-left-radius: 97px; border-bottom-right-radius: 96px; padding-top: 42px; padding-left: 2%; padding-bottom: 44px; padding-right: 5rem; margin-top: 12rem; margin-left: 2vh; margin-bottom: 2px; margin-right: 10em;', ), ), @@ -116,6 +129,7 @@ public function data_generate_styles_fixtures() { 'css' => 'font-family: Roboto,Oxygen-Sans,Ubuntu,sans-serif; font-style: italic; font-weight: 800; line-height: 1.3; text-decoration: underline; text-transform: uppercase; letter-spacing: 2;', ), ), + 'valid_classnames_deduped' => array( 'block_styles' => array( 'color' => array( @@ -132,6 +146,7 @@ public function data_generate_styles_fixtures() { 'classnames' => 'has-text-color has-copper-socks-color has-background has-splendid-carrot-background-color has-like-wow-dude-gradient-background has-fantastic-font-size has-totally-awesome-font-family', ), ), + 'valid_classnames_with_null_style_values' => array( 'block_styles' => array( 'color' => array( @@ -144,6 +159,7 @@ public function data_generate_styles_fixtures() { 'classnames' => 'has-text-color', ), ), + 'invalid_classnames_preset_value' => array( 'block_styles' => array( 'color' => array( @@ -159,6 +175,7 @@ public function data_generate_styles_fixtures() { 'classnames' => 'has-text-color has-background', ), ), + 'invalid_classnames_options' => array( 'block_styles' => array( 'typography' => array( @@ -172,6 +189,64 @@ public function data_generate_styles_fixtures() { ), 'expected_output' => array(), ), + + 'inline_valid_box_model_style_with_sides' => array( + 'block_styles' => array( + 'border' => array( + 'top' => array( + 'color' => '#fe1', + 'width' => '1.5rem', + 'style' => 'dashed', + ), + 'right' => array( + 'color' => '#fe2', + 'width' => '1.4rem', + 'style' => 'solid', + ), + 'bottom' => array( + 'color' => '#fe3', + 'width' => '1.3rem', + ), + 'left' => array( + 'color' => 'var:preset|color|swampy-yellow', + 'width' => '0.5rem', + 'style' => 'dotted', + ), + ), + ), + 'expected_output' => array( + 'css' => 'border-top-color: #fe1; border-top-width: 1.5rem; border-top-style: dashed; border-right-color: #fe2; border-right-width: 1.4rem; border-right-style: solid; border-bottom-color: #fe3; border-bottom-width: 1.3rem; border-left-color: var(--wp--preset--color--swampy-yellow); border-left-width: 0.5rem; border-left-style: dotted;', + ), + ), + + 'inline_invalid_box_model_style_with_sides' => array( + 'block_styles' => array( + 'border' => array( + 'top' => array( + 'top' => '#fe1', + 'right' => '1.5rem', + 'cheese' => 'dashed', + ), + 'right' => array( + 'right' => '#fe2', + 'top' => '1.4rem', + 'bacon' => 'solid', + ), + 'bottom' => array( + 'color' => 'var:preset|color|terrible-lizard', + 'bottom' => '1.3rem', + ), + 'left' => array( + 'left' => null, + 'width' => null, + 'top' => 'dotted', + ), + ), + ), + 'expected_output' => array( + 'css' => 'border-bottom-color: var(--wp--preset--color--terrible-lizard);', + ), + ), ); } } diff --git a/phpunit/block-supports/border-test.php b/phpunit/block-supports/border-test.php index 3ec9bbe4f53874..b139f83452a0a3 100644 --- a/phpunit/block-supports/border-test.php +++ b/phpunit/block-supports/border-test.php @@ -237,7 +237,7 @@ function test_flat_border_with_custom_color() { $actual = gutenberg_apply_border_support( $block_type, $block_attrs ); $expected = array( 'class' => 'has-border-color', - 'style' => 'border-style: dashed; border-width: 2px; border-color: #72aee6;', + 'style' => 'border-color: #72aee6; border-style: dashed; border-width: 2px;', ); $this->assertSame( $expected, $actual ); @@ -282,7 +282,7 @@ function test_split_borders_with_custom_colors() { ); $actual = gutenberg_apply_border_support( $block_type, $block_attrs ); $expected = array( - 'style' => 'border-top-width: 2px; border-top-style: dashed; border-top-color: #72aee6; border-right-width: 0.25rem; border-right-style: dotted; border-right-color: #e65054; border-bottom-width: 0.5em; border-bottom-style: solid; border-bottom-color: #007017; border-left-width: 1px; border-left-style: solid; border-left-color: #f6f7f7;', + 'style' => 'border-top-width: 2px; border-top-color: #72aee6; border-top-style: dashed; border-right-width: 0.25rem; border-right-color: #e65054; border-right-style: dotted; border-bottom-width: 0.5em; border-bottom-color: #007017; border-bottom-style: solid; border-left-width: 1px; border-left-color: #f6f7f7; border-left-style: solid;', ); $this->assertSame( $expected, $actual ); @@ -409,7 +409,7 @@ function test_partial_split_borders() { ); $actual = gutenberg_apply_border_support( $block_type, $block_attrs ); $expected = array( - 'style' => 'border-top-width: 2px; border-top-style: dashed; border-top-color: #72aee6; border-right-width: 0.25rem; border-right-color: #e65054; border-left-style: solid;', + 'style' => 'border-top-width: 2px; border-top-color: #72aee6; border-top-style: dashed; border-right-width: 0.25rem; border-right-color: #e65054; border-left-style: solid;', ); $this->assertSame( $expected, $actual ); @@ -454,7 +454,7 @@ function test_split_borders_with_named_colors() { ); $actual = gutenberg_apply_border_support( $block_type, $block_attrs ); $expected = array( - 'style' => 'border-top-width: 2px; border-top-style: dashed; border-top-color: var(--wp--preset--color--red); border-right-width: 0.25rem; border-right-style: dotted; border-right-color: var(--wp--preset--color--green); border-bottom-width: 0.5em; border-bottom-style: solid; border-bottom-color: var(--wp--preset--color--blue); border-left-width: 1px; border-left-style: solid; border-left-color: var(--wp--preset--color--yellow);', + 'style' => 'border-top-width: 2px; border-top-color: var(--wp--preset--color--red); border-top-style: dashed; border-right-width: 0.25rem; border-right-color: var(--wp--preset--color--green); border-right-style: dotted; border-bottom-width: 0.5em; border-bottom-color: var(--wp--preset--color--blue); border-bottom-style: solid; border-left-width: 1px; border-left-color: var(--wp--preset--color--yellow); border-left-style: solid;', ); $this->assertSame( $expected, $actual );