diff --git a/src/wp-includes/class-wp-block-supports.php b/src/wp-includes/class-wp-block-supports.php index ec5bc9c8d6846..c5df0b9fbd241 100644 --- a/src/wp-includes/class-wp-block-supports.php +++ b/src/wp-includes/class-wp-block-supports.php @@ -179,30 +179,50 @@ function get_block_wrapper_attributes( $extra_attributes = array() ) { return ''; } - // This is hardcoded on purpose. - // We only support a fixed list of attributes. - $attributes_to_merge = array( 'style', 'class', 'id', 'aria-label' ); - $attributes = array(); - foreach ( $attributes_to_merge as $attribute_name ) { - if ( empty( $new_attributes[ $attribute_name ] ) && empty( $extra_attributes[ $attribute_name ] ) ) { - continue; - } - - if ( empty( $new_attributes[ $attribute_name ] ) ) { - $attributes[ $attribute_name ] = $extra_attributes[ $attribute_name ]; - continue; - } - - if ( empty( $extra_attributes[ $attribute_name ] ) ) { - $attributes[ $attribute_name ] = $new_attributes[ $attribute_name ]; + // Attribute values are concatenated or overridden depending on the attribute type. + // This is hardcoded on purpose, as we only support a fixed list of attributes. + $attribute_merge_callbacks = array( + 'style' => static function ( $new_attribute, $extra_attribute ) { + $styles = array_filter( + array( + rtrim( trim( $extra_attribute ), ';' ), + rtrim( trim( $new_attribute ), ';' ), + ) + ); + return safecss_filter_attr( implode( ';', array_filter( $styles ) ) ); + }, + 'class' => static function ( $new_attribute, $extra_attribute ) { + $classes = array_merge( + preg_split( '/\s+/', $extra_attribute, -1, PREG_SPLIT_NO_EMPTY ), + preg_split( '/\s+/', $new_attribute, -1, PREG_SPLIT_NO_EMPTY ) + ); + $classes = array_unique( array_filter( $classes ) ); + return implode( ' ', $classes ); + }, + 'id' => static function ( $new_attribute, $extra_attribute ) { + return '' !== $extra_attribute ? $extra_attribute : $new_attribute; + }, + 'aria-label' => static function ( $new_attribute, $extra_attribute ) { + return '' !== $extra_attribute ? $extra_attribute : $new_attribute; + }, + ); + + $attributes = array(); + foreach ( $attribute_merge_callbacks as $attribute_name => $merge_callback ) { + $new_attribute = $new_attributes[ $attribute_name ] ?? ''; + $extra_attribute = $extra_attributes[ $attribute_name ] ?? ''; + $new_attribute = is_string( $new_attribute ) ? $new_attribute : ''; + $extra_attribute = is_string( $extra_attribute ) ? $extra_attribute : ''; + + if ( '' === $new_attribute && '' === $extra_attribute ) { continue; } - $attributes[ $attribute_name ] = $extra_attributes[ $attribute_name ] . ' ' . $new_attributes[ $attribute_name ]; + $attributes[ $attribute_name ] = $merge_callback( $new_attribute, $extra_attribute ); } foreach ( $extra_attributes as $attribute_name => $value ) { - if ( ! in_array( $attribute_name, $attributes_to_merge, true ) ) { + if ( ! isset( $attribute_merge_callbacks[ $attribute_name ] ) ) { $attributes[ $attribute_name ] = $value; } } diff --git a/tests/phpunit/tests/blocks/supportedStyles.php b/tests/phpunit/tests/blocks/supportedStyles.php index c733b7218d8c5..771c8f6089d15 100644 --- a/tests/phpunit/tests/blocks/supportedStyles.php +++ b/tests/phpunit/tests/blocks/supportedStyles.php @@ -99,28 +99,12 @@ private function render_example_block( $block ) { $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => 'foo-bar-class', - 'style' => 'test: style;', + 'style' => 'margin-top: 2px;', ) ); return '
' . self::BLOCK_CONTENT . '
'; } - /** - * Runs assertions that the rendered output has expected class/style attrs. - * - * @param array $block Block to render. - * @param string $expected_classes Expected output class attr string. - * @param string $expected_styles Expected output styles attr string. - */ - private function assert_styles_and_classes_match( $block, $expected_classes, $expected_styles ) { - $styled_block = $this->render_example_block( $block ); - $class_list = $this->get_attribute_from_block( 'class', $styled_block ); - $style_list = $this->get_attribute_from_block( 'style', $styled_block ); - - $this->assertSame( $expected_classes, $class_list, 'Class list does not match expected classes' ); - $this->assertSame( $expected_styles, $style_list, 'Style list does not match expected styles' ); - } - /** * Runs assertions that the rendered output has expected content and class/style attrs. * @@ -196,7 +180,7 @@ public function test_named_color_support() { ); $expected_classes = 'foo-bar-class wp-block-example has-text-color has-red-color has-background has-black-background-color'; - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -232,7 +216,7 @@ public function test_custom_color_support() { 'innerHTML' => array(), ); - $expected_styles = 'test: style;color:#000;background-color:#fff;'; + $expected_styles = 'margin-top: 2px;color:#000;background-color:#fff'; $expected_classes = 'foo-bar-class wp-block-example has-text-color has-background'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); @@ -264,7 +248,7 @@ public function test_named_gradient_support() { ); $expected_classes = 'foo-bar-class wp-block-example has-background has-red-gradient-background'; - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -295,7 +279,7 @@ public function test_custom_gradient_support() { ); $expected_classes = 'foo-bar-class wp-block-example has-background'; - $expected_styles = 'test: style; background:some-gradient-style;'; + $expected_styles = 'margin-top: 2px;background:some-gradient-style'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -331,7 +315,7 @@ public function test_color_unsupported() { ); $expected_classes = 'foo-bar-class wp-block-example'; - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -361,7 +345,7 @@ public function test_named_font_size() { ); $expected_classes = 'foo-bar-class wp-block-example has-large-font-size'; - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -391,7 +375,7 @@ public function test_custom_font_size() { ); $expected_classes = 'foo-bar-class wp-block-example'; - $expected_styles = 'test: style; font-size:10px;'; + $expected_styles = 'margin-top: 2px;font-size:10px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -418,7 +402,7 @@ public function test_font_size_unsupported() { ); $expected_classes = 'foo-bar-class wp-block-example'; - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -448,7 +432,7 @@ public function test_line_height() { ); $expected_classes = 'foo-bar-class wp-block-example'; - $expected_styles = 'test: style; line-height:10;'; + $expected_styles = 'margin-top: 2px;line-height:10'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -474,7 +458,7 @@ public function test_line_height_unsupported() { ); $expected_classes = 'foo-bar-class wp-block-example'; - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -502,7 +486,7 @@ public function test_block_alignment() { ); $expected_classes = 'foo-bar-class wp-block-example alignwide'; - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -528,7 +512,7 @@ public function test_block_alignment_unsupported() { ); $expected_classes = 'foo-bar-class wp-block-example'; - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -575,7 +559,7 @@ public 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 = 'margin-top: 2px;color:#000;background-color:#fff;font-size:10px;line-height:20'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -618,7 +602,7 @@ public function test_one_supported() { ); $expected_classes = 'foo-bar-class wp-block-example'; - $expected_styles = 'test: style; font-size:10px;'; + $expected_styles = 'margin-top: 2px;font-size:10px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -643,7 +627,7 @@ public function test_custom_classnames_support() { 'innerHTML' => array(), ); - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $expected_classes = 'foo-bar-class wp-block-example my-custom-classname'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); @@ -671,7 +655,7 @@ public function test_custom_classnames_support_opt_out() { 'innerHTML' => array(), ); - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $expected_classes = 'foo-bar-class wp-block-example'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); @@ -697,7 +681,7 @@ public function test_generated_classnames_support_opt_out() { 'innerHTML' => array(), ); - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $expected_classes = 'foo-bar-class'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); @@ -764,4 +748,161 @@ static function ( $errno = 0, $errstr = '' ) use ( &$errors ) { $this->assertEmpty( $errors, 'Libxml errors should be dropped.' ); } + + /** + * Ensures that style, class, id, and aria-label attributes are correctly merged or overridden + * in get_block_wrapper_attributes(). + * + * @ticket 64603 + * @covers ::get_block_wrapper_attributes + * + * @dataProvider data_get_block_wrapper_attributes_merge_or_override + * + * @param array $data { + * Data from the provider. + * + * @type array $block_type_settings Block type settings. + * @type array $block_attrs Block attributes. + * @type array $extra_attributes Extra attributes passed to get_block_wrapper_attributes(). + * @type string $expected_attribute Expected attribute string (e.g. 'id="user-id"'). + * } + */ + public function test_get_block_wrapper_attributes_merge_and_override( $data ) { + $block_name = 'core/example'; + $block_type_settings = array_merge( + array( + 'attributes' => array(), + 'render_callback' => true, + ), + $data['block_type_settings'] + ); + $this->register_block_type( $block_name, $block_type_settings ); + + $block = array( + 'blockName' => $block_name, + 'attrs' => $data['block_attrs'], + 'innerBlock' => array(), + 'innerContent' => array(), + 'innerHTML' => array(), + ); + + WP_Block_Supports::init(); + WP_Block_Supports::$block_to_render = $block; + + $wrapper_attributes = get_block_wrapper_attributes( $data['extra_attributes'] ); + + $this->assertStringContainsString( $data['expected_attribute'], $wrapper_attributes ); + } + + /** + * Data provider for test_get_block_wrapper_attributes_merge_and_override. + * + * @return array[] Array of test cases. + */ + public function data_get_block_wrapper_attributes_merge_or_override() { + return array( + 'extra style attributes are merged with block values' => array( + array( + 'block_type_settings' => array( + 'supports' => array( + 'color' => true, + ), + ), + 'block_attrs' => array( + 'style' => array( + 'color' => array( + 'text' => '#000', + ), + ), + ), + 'extra_attributes' => array( + // Redundant trailing semicolons should be stripped + 'style' => 'margin-top: 2px;;;', + ), + 'expected_attribute' => 'style="margin-top: 2px;color:#000"', + ), + ), + 'extra class attributes are merged with block values' => array( + array( + 'block_type_settings' => array( + 'supports' => array( + 'color' => true, + ), + ), + 'block_attrs' => array( + 'style' => array( + 'color' => array( + 'text' => '#000', + ), + ), + ), + 'extra_attributes' => array( + // Duplicate class names should be merged, and commas should be preserved. + 'class' => 'extra-class extra,class has-text-color', + ), + 'expected_attribute' => 'class="extra-class extra,class has-text-color wp-block-example"', + ), + ), + 'extra attributes override block-generated id' => array( + array( + 'block_type_settings' => array( + 'supports' => array( + 'anchor' => true, + ), + ), + 'block_attrs' => array( + 'anchor' => 'block-id', + ), + 'extra_attributes' => array( + 'id' => 'user-id', + ), + 'expected_attribute' => 'id="user-id"', + ), + ), + 'block-generated id is used when no extra provided' => array( + array( + 'block_type_settings' => array( + 'supports' => array( + 'anchor' => true, + ), + ), + 'block_attrs' => array( + 'anchor' => 'block-id', + ), + 'extra_attributes' => array(), + 'expected_attribute' => 'id="block-id"', + ), + ), + 'extra attributes override block-generated aria-label' => array( + array( + 'block_type_settings' => array( + 'supports' => array( + 'ariaLabel' => true, + ), + ), + 'block_attrs' => array( + 'ariaLabel' => 'Block aria-label', + ), + 'extra_attributes' => array( + 'aria-label' => 'User aria-label', + ), + 'expected_attribute' => 'aria-label="User aria-label"', + ), + ), + 'block-generated aria-label is used when no extra provided' => array( + array( + 'block_type_settings' => array( + 'supports' => array( + 'ariaLabel' => true, + ), + ), + 'block_attrs' => array( + 'ariaLabel' => 'Block aria-label', + ), + 'extra_attributes' => array(), + 'expected_attribute' => 'aria-label="Block aria-label"', + ), + ), + ); + } }