Make WordPress Core

Changeset 62068


Ignore:
Timestamp:
03/19/2026 02:39:42 PM (9 days ago)
Author:
wildworks
Message:

Blocks: Fix wrapper attribute merging in get_block_wrapper_attributes().

Replace the previous generic concatenation logic in get_block_wrapper_attributes() with attribute-specific merge behavior:

  • Add explicit merge callbacks for each attribute.
  • Merge style values safely, normalize trailing semicolons, and sanitize the result.
  • Merge class values with deduplication .
  • Treat id and aria-label as override attributes, giving precedence to explicitly passed extra attributes.

Props adrock42, mamaduka, r1k0, westonruter, wildworks.

Fixes #64603.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-block-supports.php

    r59925 r62068  
    180180    }
    181181
    182     // This is hardcoded on purpose.
    183     // We only support a fixed list of attributes.
    184     $attributes_to_merge = array( 'style', 'class', 'id', 'aria-label' );
    185     $attributes          = array();
    186     foreach ( $attributes_to_merge as $attribute_name ) {
    187         if ( empty( $new_attributes[ $attribute_name ] ) && empty( $extra_attributes[ $attribute_name ] ) ) {
     182    // Attribute values are concatenated or overridden depending on the attribute type.
     183    // This is hardcoded on purpose, as we only support a fixed list of attributes.
     184    $attribute_merge_callbacks = array(
     185        'style'      => static function ( $new_attribute, $extra_attribute ) {
     186            $styles = array_filter(
     187                array(
     188                    rtrim( trim( $new_attribute ), ';' ),
     189                    rtrim( trim( $extra_attribute ), ';' ),
     190                )
     191            );
     192            return safecss_filter_attr( implode( ';', array_filter( $styles ) ) );
     193        },
     194        'class'      => static function ( $new_attribute, $extra_attribute ) {
     195            $classes = array_merge(
     196                (array) preg_split( '/\s+/', $extra_attribute, -1, PREG_SPLIT_NO_EMPTY ),
     197                (array) preg_split( '/\s+/', $new_attribute, -1, PREG_SPLIT_NO_EMPTY )
     198            );
     199            $classes = array_unique( array_filter( $classes ) );
     200            return implode( ' ', $classes );
     201        },
     202        'id'         => static function ( $new_attribute, $extra_attribute ) {
     203            return '' !== $extra_attribute ? $extra_attribute : $new_attribute;
     204        },
     205        'aria-label' => static function ( $new_attribute, $extra_attribute ) {
     206            return '' !== $extra_attribute ? $extra_attribute : $new_attribute;
     207        },
     208    );
     209
     210    $attributes = array();
     211    foreach ( $attribute_merge_callbacks as $attribute_name => $merge_callback ) {
     212        $new_attribute   = $new_attributes[ $attribute_name ] ?? '';
     213        $extra_attribute = $extra_attributes[ $attribute_name ] ?? '';
     214        $new_attribute   = is_string( $new_attribute ) ? $new_attribute : '';
     215        $extra_attribute = is_string( $extra_attribute ) ? $extra_attribute : '';
     216
     217        if ( '' === $new_attribute && '' === $extra_attribute ) {
    188218            continue;
    189219        }
    190220
    191         if ( empty( $new_attributes[ $attribute_name ] ) ) {
    192             $attributes[ $attribute_name ] = $extra_attributes[ $attribute_name ];
    193             continue;
    194         }
    195 
    196         if ( empty( $extra_attributes[ $attribute_name ] ) ) {
    197             $attributes[ $attribute_name ] = $new_attributes[ $attribute_name ];
    198             continue;
    199         }
    200 
    201         $attributes[ $attribute_name ] = $extra_attributes[ $attribute_name ] . ' ' . $new_attributes[ $attribute_name ];
     221        $attributes[ $attribute_name ] = $merge_callback( $new_attribute, $extra_attribute );
    202222    }
    203223
    204224    foreach ( $extra_attributes as $attribute_name => $value ) {
    205         if ( ! in_array( $attribute_name, $attributes_to_merge, true ) ) {
     225        if ( ! isset( $attribute_merge_callbacks[ $attribute_name ] ) ) {
    206226            $attributes[ $attribute_name ] = $value;
    207227        }
  • trunk/tests/phpunit/tests/blocks/supportedStyles.php

    r59925 r62068  
    100100            array(
    101101                'class' => 'foo-bar-class',
    102                 'style' => 'test: style;',
     102                'style' => 'margin-top: 2px;',
    103103            )
    104104        );
    105105        return '<div ' . $wrapper_attributes . '>' . self::BLOCK_CONTENT . '</div>';
    106     }
    107 
    108     /**
    109      * Runs assertions that the rendered output has expected class/style attrs.
    110      *
    111      * @param array  $block            Block to render.
    112      * @param string $expected_classes Expected output class attr string.
    113      * @param string $expected_styles  Expected output styles attr string.
    114      */
    115     private function assert_styles_and_classes_match( $block, $expected_classes, $expected_styles ) {
    116         $styled_block = $this->render_example_block( $block );
    117         $class_list   = $this->get_attribute_from_block( 'class', $styled_block );
    118         $style_list   = $this->get_attribute_from_block( 'style', $styled_block );
    119 
    120         $this->assertSame( $expected_classes, $class_list, 'Class list does not match expected classes' );
    121         $this->assertSame( $expected_styles, $style_list, 'Style list does not match expected styles' );
    122106    }
    123107
     
    197181
    198182        $expected_classes = 'foo-bar-class wp-block-example has-text-color has-red-color has-background has-black-background-color';
    199         $expected_styles  = 'test: style;';
     183        $expected_styles  = 'margin-top: 2px';
    200184
    201185        $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    233217        );
    234218
    235         $expected_styles  = 'test: style;color:#000;background-color:#fff;';
     219        $expected_styles  = 'color:#000;background-color:#fff;margin-top: 2px';
    236220        $expected_classes = 'foo-bar-class wp-block-example has-text-color has-background';
    237221
     
    265249
    266250        $expected_classes = 'foo-bar-class wp-block-example has-background has-red-gradient-background';
    267         $expected_styles  = 'test: style;';
     251        $expected_styles  = 'margin-top: 2px';
    268252
    269253        $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    296280
    297281        $expected_classes = 'foo-bar-class wp-block-example has-background';
    298         $expected_styles  = 'test: style; background:some-gradient-style;';
     282        $expected_styles  = 'background:some-gradient-style;margin-top: 2px';
    299283
    300284        $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    332316
    333317        $expected_classes = 'foo-bar-class wp-block-example';
    334         $expected_styles  = 'test: style;';
     318        $expected_styles  = 'margin-top: 2px';
    335319
    336320        $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    362346
    363347        $expected_classes = 'foo-bar-class wp-block-example has-large-font-size';
    364         $expected_styles  = 'test: style;';
     348        $expected_styles  = 'margin-top: 2px';
    365349
    366350        $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    392376
    393377        $expected_classes = 'foo-bar-class wp-block-example';
    394         $expected_styles  = 'test: style; font-size:10px;';
     378        $expected_styles  = 'font-size:10px;margin-top: 2px';
    395379
    396380        $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    419403
    420404        $expected_classes = 'foo-bar-class wp-block-example';
    421         $expected_styles  = 'test: style;';
     405        $expected_styles  = 'margin-top: 2px';
    422406
    423407        $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    449433
    450434        $expected_classes = 'foo-bar-class wp-block-example';
    451         $expected_styles  = 'test: style; line-height:10;';
     435        $expected_styles  = 'line-height:10;margin-top: 2px';
    452436
    453437        $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    475459
    476460        $expected_classes = 'foo-bar-class wp-block-example';
    477         $expected_styles  = 'test: style;';
     461        $expected_styles  = 'margin-top: 2px';
    478462
    479463        $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    503487
    504488        $expected_classes = 'foo-bar-class wp-block-example alignwide';
    505         $expected_styles  = 'test: style;';
     489        $expected_styles  = 'margin-top: 2px';
    506490
    507491        $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    529513
    530514        $expected_classes = 'foo-bar-class wp-block-example';
    531         $expected_styles  = 'test: style;';
     515        $expected_styles  = 'margin-top: 2px';
    532516
    533517        $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    576560
    577561        $expected_classes = 'foo-bar-class wp-block-example has-text-color has-background alignwide';
    578         $expected_styles  = 'test: style; color:#000; background-color:#fff; font-size:10px; line-height:20;';
     562        $expected_styles  = 'color:#000;background-color:#fff;font-size:10px;line-height:20;margin-top: 2px';
    579563
    580564        $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    619603
    620604        $expected_classes = 'foo-bar-class wp-block-example';
    621         $expected_styles  = 'test: style; font-size:10px;';
     605        $expected_styles  = 'font-size:10px;margin-top: 2px';
    622606
    623607        $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles );
     
    644628        );
    645629
    646         $expected_styles  = 'test: style;';
     630        $expected_styles  = 'margin-top: 2px';
    647631        $expected_classes = 'foo-bar-class wp-block-example my-custom-classname';
    648632
     
    672656        );
    673657
    674         $expected_styles  = 'test: style;';
     658        $expected_styles  = 'margin-top: 2px';
    675659        $expected_classes = 'foo-bar-class wp-block-example';
    676660
     
    698682        );
    699683
    700         $expected_styles  = 'test: style;';
     684        $expected_styles  = 'margin-top: 2px';
    701685        $expected_classes = 'foo-bar-class';
    702686
     
    765749        $this->assertEmpty( $errors, 'Libxml errors should be dropped.' );
    766750    }
     751
     752    /**
     753     * Ensures that style, class, id, and aria-label attributes are correctly merged or overridden
     754     * in get_block_wrapper_attributes().
     755     *
     756     * @ticket 64603
     757     * @covers ::get_block_wrapper_attributes
     758     *
     759     * @dataProvider data_get_block_wrapper_attributes_merge_or_override
     760     *
     761     * @param array<string, mixed>  $block_type_settings
     762     * @param array<string, mixed>  $block_attrs
     763     * @param array<string, string> $extra_attributes
     764     * @param string                $expected_attribute
     765     */
     766    public function test_get_block_wrapper_attributes_merge_and_override( array $block_type_settings, array $block_attrs, array $extra_attributes, string $expected_attribute ): void {
     767        $block_name          = 'core/example';
     768        $block_type_settings = array_merge(
     769            array(
     770                'attributes'      => array(),
     771                'render_callback' => true,
     772            ),
     773            $block_type_settings
     774        );
     775        $this->register_block_type( $block_name, $block_type_settings );
     776
     777        $block = array(
     778            'blockName'    => $block_name,
     779            'attrs'        => $block_attrs,
     780            'innerBlock'   => array(),
     781            'innerContent' => array(),
     782            'innerHTML'    => array(),
     783        );
     784
     785        WP_Block_Supports::init();
     786        WP_Block_Supports::$block_to_render = $block;
     787
     788        $wrapper_attributes = get_block_wrapper_attributes( $extra_attributes );
     789
     790        $this->assertSame( $expected_attribute, $wrapper_attributes );
     791    }
     792
     793    /**
     794     * Data provider for test_get_block_wrapper_attributes_merge_and_override.
     795     *
     796     * @return array<string, array{
     797     *      block_type_settings: array<string, mixed>,
     798     *      block_attrs: array<string, mixed>,
     799     *      extra_attributes: array<string, string>,
     800     *      expected_attribute: string
     801     *  }> Array of test cases.
     802     */
     803    public function data_get_block_wrapper_attributes_merge_or_override(): array {
     804        return array(
     805            'extra style attributes are merged with block values' => array(
     806                'block_type_settings' => array(
     807                    'supports' => array(
     808                        'color' => true,
     809                    ),
     810                ),
     811                'block_attrs'         => array(
     812                    'style' => array(
     813                        'color' => array(
     814                            'text' => '#000',
     815                        ),
     816                    ),
     817                ),
     818                'extra_attributes'    => array(
     819                    // Redundant trailing semicolons should be stripped
     820                    'style' => 'margin-top: 2px;;;',
     821                ),
     822                'expected_attribute'  => 'style="color:#000;margin-top: 2px" class="wp-block-example has-text-color"',
     823            ),
     824            'extra class attributes are merged with block values' => array(
     825                'block_type_settings' => array(
     826                    'supports' => array(
     827                        'color' => true,
     828                    ),
     829                ),
     830                'block_attrs'         => array(
     831                    'style' => array(
     832                        'color' => array(
     833                            'text' => '#000',
     834                        ),
     835                    ),
     836                ),
     837                'extra_attributes'    => array(
     838                    // Duplicate class names should be merged, and commas should be preserved.
     839                    'class' => 'extra-class extra,class has-text-color',
     840                ),
     841                'expected_attribute'  => 'style="color:#000" class="extra-class extra,class has-text-color wp-block-example"',
     842            ),
     843            'extra attributes override block-generated id' => array(
     844                'block_type_settings' => array(
     845                    'supports' => array(
     846                        'anchor' => true,
     847                    ),
     848                ),
     849                'block_attrs'         => array(
     850                    'anchor' => 'block-id',
     851                ),
     852                'extra_attributes'    => array(
     853                    'id' => 'user-id',
     854                ),
     855                'expected_attribute'  => 'class="wp-block-example" id="user-id"',
     856            ),
     857            'block-generated id is used when no extra provided' => array(
     858                'block_type_settings' => array(
     859                    'supports' => array(
     860                        'anchor' => true,
     861                    ),
     862                ),
     863                'block_attrs'         => array(
     864                    'anchor' => 'block-id',
     865                ),
     866                'extra_attributes'    => array(),
     867                'expected_attribute'  => 'class="wp-block-example" id="block-id"',
     868            ),
     869            'extra attributes override block-generated aria-label' => array(
     870                'block_type_settings' => array(
     871                    'supports' => array(
     872                        'ariaLabel' => true,
     873                    ),
     874                ),
     875                'block_attrs'         => array(
     876                    'ariaLabel' => 'Block aria-label',
     877                ),
     878                'extra_attributes'    => array(
     879                    'aria-label' => 'User aria-label',
     880                ),
     881                'expected_attribute'  => 'class="wp-block-example" aria-label="User aria-label"',
     882            ),
     883            'block-generated aria-label is used when no extra provided' => array(
     884                'block_type_settings' => array(
     885                    'supports' => array(
     886                        'ariaLabel' => true,
     887                    ),
     888                ),
     889                'block_attrs'         => array(
     890                    'ariaLabel' => 'Block aria-label',
     891                ),
     892                'extra_attributes'    => array(),
     893                'expected_attribute'  => 'class="wp-block-example" aria-label="Block aria-label"',
     894            ),
     895        );
     896    }
    767897}
Note: See TracChangeset for help on using the changeset viewer.