Make WordPress Core

Changeset 64676


Ignore:
Timestamp:
10/22/2026 12:56:22 AM (21 hours ago)
Author:
ramonopoly
Message:

Editor: Generate custom CSS for blocks rendered by their parent block

This commit generates per-block custom, "Additional CSS" for blocks that a parent block parses and renders itself, such as the links in a navigation menu.

Previously, the frontend did not render generate CSS for these blocks, the major case being Navigation Link blocks.

Developed in: ​https://github.com/WordPress/wordpress-develop/pull/14538

Props mikachan, mrleemon, scruffian.

Fixes #66345.

Location:
trunk
Files:
2 edited

Legend:

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

    r64353 r64676  
    122122
    123123        return $processed_css;
     124}
     125
     126/**
     127 * Generates the custom CSS class name for a block and registers its stylesheet.
     128 *
     129 * @since 7.2.0
     130 * @access private
     131 *
     132 * @param array $parsed_block The parsed block.
     133 * @return string|null The generated class name, or null if the block has no custom CSS to render.
     134 *
     135 * @phpstan-param array{
     136 *     blockName: string|null,
     137 *     attrs: array{
     138 *         className?: string,
     139 *         style?: array{
     140 *             css?: string,
     141 *             ...
     142 *         },
     143 *         ...
     144 *     },
     145 *     ...
     146 * } $parsed_block
     147 */
     148function wp_generate_custom_css_class_name( $parsed_block ) {
     149        $style = $parsed_block['attrs']['style'] ?? null;
     150        if ( empty( $style ) || ! is_array( $style ) ) {
     151                return null;
     152        }
     153
     154        $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $parsed_block['blockName'] );
     155        if ( ! block_has_support( $block_type, 'customCSS', true ) ) {
     156                return null;
     157        }
     158
     159        $viewport_settings        = wp_get_global_settings( array( 'viewport' ) );
     160        $responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings );
     161        $state_entries            = wp_get_custom_css_state_entries( $style, $parsed_block['blockName'], $responsive_media_queries );
     162        if ( empty( $state_entries ) ) {
     163                return null;
     164        }
     165
     166        // Generate a unique class name for this block instance.
     167        $class_name    = wp_unique_id_from_values( $parsed_block, 'wp-custom-css-' );
     168        $processed_css = wp_process_custom_css_state_entries( $state_entries, '.' . $class_name );
     169        if ( '' === $processed_css ) {
     170                return null;
     171        }
     172
     173        /**
     174         * Reuse one handle so identical custom CSS is enqueued only once via
     175         * {@see wp_unique_id_from_values()}. Explicitly declare the `wp-block-library`
     176         * dependency so `global-styles` is guaranteed to print after it, preventing
     177         * block default styles from unintentionally overriding global styles.
     178         */
     179        $handle = 'wp-block-custom-css';
     180        if ( ! wp_style_is( $handle, 'registered' ) ) {
     181                wp_register_style( $handle, false, array( 'wp-block-library', 'global-styles' ) );
     182        }
     183        $after_styles = wp_styles()->get_data( $handle, 'after' );
     184        if ( ! is_array( $after_styles ) ) {
     185                $after_styles = array();
     186        }
     187        if ( ! in_array( $processed_css, $after_styles, true ) ) {
     188                wp_add_inline_style( $handle, $processed_css );
     189        }
     190
     191        return $class_name;
    124192}
    125193
    … …  
    158226 */
    159227function wp_render_custom_css_support_styles( $parsed_block ) {
    160         $style = $parsed_block['attrs']['style'] ?? null;
    161         if ( empty( $style ) || ! is_array( $style ) ) {
    162                 return $parsed_block;
    163         }
    164 
    165         $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $parsed_block['blockName'] );
    166         if ( ! block_has_support( $block_type, 'customCSS', true ) ) {
    167                 return $parsed_block;
    168         }
    169 
    170         $viewport_settings        = wp_get_global_settings( array( 'viewport' ) );
    171         $responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings );
    172         $state_entries            = wp_get_custom_css_state_entries( $style, $parsed_block['blockName'], $responsive_media_queries );
    173         if ( empty( $state_entries ) ) {
    174                 return $parsed_block;
    175         }
    176 
    177         // Generate a unique class name for this block instance.
    178         $class_name    = wp_unique_id_from_values( $parsed_block, 'wp-custom-css-' );
    179         $processed_css = wp_process_custom_css_state_entries( $state_entries, '.' . $class_name );
    180         if ( '' === $processed_css ) {
     228        $class_name = wp_generate_custom_css_class_name( $parsed_block );
     229        if ( null === $class_name ) {
    181230                return $parsed_block;
    182231        }
    … …  
    189238        $parsed_block['attrs']['className'] = $updated_class_name;
    190239
    191         /**
    192          * Reuse one handle so identical custom CSS is enqueued only once via
    193          * {@see wp_unique_id_from_values()}. Explicitly declare the `wp-block-library`
    194          * dependency so `global-styles` is guaranteed to print after it, preventing
    195          * block default styles from unintentionally overriding global styles.
    196          */
    197         $handle = 'wp-block-custom-css';
    198         if ( ! wp_style_is( $handle, 'registered' ) ) {
    199                 wp_register_style( $handle, false, array( 'wp-block-library', 'global-styles' ) );
    200         }
    201         $after_styles = wp_styles()->get_data( $handle, 'after' );
    202         if ( ! is_array( $after_styles ) ) {
    203                 $after_styles = array();
    204         }
    205         if ( ! in_array( $processed_css, $after_styles, true ) ) {
    206                 wp_add_inline_style( $handle, $processed_css );
    207         }
    208 
    209240        return $parsed_block;
    210241}
    … …  
    222253 * Applies the custom CSS class name to the block's rendered HTML.
    223254 *
    224  * The class name is generated in {@see wp_render_custom_css_support_styles()}
    225  * and stored in block attributes. This filter adds it to the actual markup.
    226  *
    227  * @since 7.0.0
     255 * Uses the class name that {@see wp_render_custom_css_support_styles()}
     256 * stored in the block attributes. Blocks that a parent parsed and rendered
     257 * itself never reach that filter, so the class name is generated here instead
     258 * via {@see wp_generate_custom_css_class_name()}.
     259 *
     260 * @since 7.0.0
     261 * @since 7.2.0 Generates the class name when the block attributes lack one.
    228262 *
    229263 * @param string $block_content Rendered block content.
    … …  
    232266 *
    233267 * @phpstan-param array{
     268 *     blockName: string|null,
    234269 *     attrs: array{
    235270 *         className?: string,
     271 *         style?: array{
     272 *             css?: string,
     273 *             ...
     274 *         },
    236275 *         ...
    237276 *     },
    … …  
    240279 */
    241280function wp_render_custom_css_class_name( $block_content, $block ) {
    242         $class_name_attr   = $block['attrs']['className'] ?? null;
    243         $class_name_prefix = 'wp-custom-css-';
    244         if ( ! is_string( $class_name_attr ) || ! str_contains( $class_name_attr, $class_name_prefix ) ) {
    245                 return $block_content;
    246         }
     281        $class_name_attr    = $block['attrs']['className'] ?? null;
     282        $class_name_prefix  = 'wp-custom-css-';
     283        $matched_class_name = null;
    247284
    248285        // Parse out the 'wp-custom-css-*' class name added by wp_render_custom_css_support_styles().
    249         $matched_class_name = null;
    250         $token_delimiter    = " \t\f\r\n";
    251         $class_token        = strtok( $class_name_attr, $token_delimiter );
    252         while ( false !== $class_token ) {
    253                 if ( str_starts_with( $class_token, $class_name_prefix ) ) {
    254                         $matched_class_name = $class_token;
    255                         break;
    256                 }
    257                 $class_token = strtok( $token_delimiter );
    258         }
     286        if ( is_string( $class_name_attr ) && str_contains( $class_name_attr, $class_name_prefix ) ) {
     287                $token_delimiter = " \t\f\r\n";
     288                $class_token     = strtok( $class_name_attr, $token_delimiter );
     289                while ( false !== $class_token ) {
     290                        if ( str_starts_with( $class_token, $class_name_prefix ) ) {
     291                                $matched_class_name = $class_token;
     292                                break;
     293                        }
     294                        $class_token = strtok( $token_delimiter );
     295                }
     296        }
     297
     298        // Direct WP_Block::render() calls skip render_block_data for the block itself.
     299        if ( null === $matched_class_name ) {
     300                $matched_class_name = wp_generate_custom_css_class_name( $block );
     301        }
     302
    259303        if ( null === $matched_class_name ) {
    260304                return $block_content;
  • trunk/tests/phpunit/tests/block-supports/wpRenderCustomCssClassName.php

    r64449 r64676  
    77 */
    88class Tests_Block_Supports_WpRenderCustomCssClassName extends WP_UnitTestCase {
     9        /**
     10         * @var string|null
     11         */
     12        private $test_block_name;
     13
     14        public function set_up() {
     15                parent::set_up();
     16                $this->test_block_name = null;
     17
     18                global $wp_styles, $wp_script_modules;
     19                $wp_styles         = null;
     20                $wp_script_modules = null;
     21        }
     22
     23        public function tear_down() {
     24                if ( $this->test_block_name ) {
     25                        unregister_block_type( $this->test_block_name );
     26                }
     27                $this->test_block_name = null;
     28
     29                global $wp_styles, $wp_script_modules;
     30                $wp_styles         = null;
     31                $wp_script_modules = null;
     32
     33                parent::tear_down();
     34        }
    935
    1036        /**
    … …  
    160186                );
    161187        }
     188
     189        /**
     190         * Returns the custom CSS class name found in a string of rendered markup.
     191         *
     192         * @param string $markup Rendered block markup.
     193         * @return string|null The custom CSS class name, or null if there is none.
     194         */
     195        private function get_custom_css_class_name_from_markup( $markup ) {
     196                return preg_match( '/\bwp-custom-css-[0-9a-f]+\b/', $markup, $matches ) ? $matches[0] : null;
     197        }
     198
     199        /**
     200         * Returns the custom CSS rules currently attached to the custom CSS stylesheet.
     201         *
     202         * @return string The inline custom CSS.
     203         */
     204        private function get_enqueued_custom_css() {
     205                $after = wp_styles()->get_data( 'wp-block-custom-css', 'after' );
     206
     207                return is_array( $after ) ? implode( "\n", $after ) : '';
     208        }
     209
     210        /**
     211         * Tests that the class name and stylesheet are still generated for blocks that
     212         * are rendered without the `render_block_data` filter running.
     213         *
     214         * Direct `WP_Block::render()` calls skip `render_block_data` for the block
     215         * itself, as when `core/navigation` renders a `wp_navigation` post's blocks.
     216         *
     217         * @ticket 66345
     218         *
     219         * @covers ::wp_render_custom_css_class_name
     220         */
     221        public function test_generates_class_when_render_block_data_skipped() {
     222                $this->test_block_name = 'test/custom-css-skipped-block-data';
     223                register_block_type(
     224                        $this->test_block_name,
     225                        array(
     226                                'api_version' => 3,
     227                                'attributes'  => array(
     228                                        'style' => array(
     229                                                'type' => 'object',
     230                                        ),
     231                                ),
     232                                'supports'    => array( 'customCSS' => true ),
     233                        )
     234                );
     235
     236                $block = array(
     237                        'blockName' => 'test/custom-css-skipped-block-data',
     238                        'attrs'     => array(
     239                                'style' => array(
     240                                        'css' => 'padding: 0;',
     241                                ),
     242                        ),
     243                );
     244
     245                $result     = wp_render_custom_css_class_name( '<li class="wp-block-navigation-item">Home</li>', $block );
     246                $class_name = $this->get_custom_css_class_name_from_markup( $result );
     247
     248                $this->assertNotNull( $class_name, 'A custom CSS class should be added even though render_block_data did not run.' );
     249                $this->assertStringContainsString( 'has-custom-css', $result, 'The has-custom-css class should be added.' );
     250                $this->assertStringContainsString( 'wp-block-navigation-item', $result, 'Existing classes should be preserved.' );
     251                $this->assertStringContainsString(
     252                        '.' . $class_name,
     253                        $this->get_enqueued_custom_css(),
     254                        'The custom CSS stylesheet should contain a rule for the generated class.'
     255                );
     256        }
     257
     258        /**
     259         * Tests that a block is not given a second class name when `render_block_data`
     260         * has already generated one.
     261         *
     262         * @ticket 66345
     263         *
     264         * @covers ::wp_render_custom_css_class_name
     265         */
     266        public function test_does_not_regenerate_existing_class() {
     267                $this->test_block_name = 'test/custom-css-existing-class';
     268                register_block_type(
     269                        $this->test_block_name,
     270                        array(
     271                                'api_version' => 3,
     272                                'attributes'  => array(
     273                                        'style' => array(
     274                                                'type' => 'object',
     275                                        ),
     276                                ),
     277                                'supports'    => array( 'customCSS' => true ),
     278                        )
     279                );
     280
     281                $parsed_block = array(
     282                        'blockName' => 'test/custom-css-existing-class',
     283                        'attrs'     => array(
     284                                'style' => array(
     285                                        'css' => 'padding: 0;',
     286                                ),
     287                        ),
     288                );
     289
     290                $filtered = wp_render_custom_css_support_styles( $parsed_block );
     291                $result   = wp_render_custom_css_class_name( '<p>Test content</p>', $filtered );
     292
     293                $this->assertSame(
     294                        1,
     295                        preg_match_all( '/wp-custom-css-[0-9a-f]+/', $result ),
     296                        'Only the class name generated by render_block_data should be added.'
     297                );
     298                $this->assertStringContainsString(
     299                        $filtered['attrs']['className'],
     300                        $result,
     301                        'The class name generated by render_block_data should be the one added to the markup.'
     302                );
     303        }
     304
     305        /**
     306         * Tests that blocks inside a navigation menu get their custom CSS on the front end.
     307         *
     308         * @ticket 66345
     309         *
     310         * @covers ::wp_render_custom_css_class_name
     311         */
     312        public function test_custom_css_is_rendered_for_blocks_inside_a_navigation_menu() {
     313                /*
     314                 * Custom CSS is stripped on save for users without the `edit_css` capability,
     315                 * which the default test user does not have. Hooks are restored on tear down.
     316                 */
     317                remove_filter( 'content_save_pre', 'wp_strip_custom_css_from_blocks', 8 );
     318
     319                $navigation_id = self::factory()->post->create(
     320                        array(
     321                                'post_type'    => 'wp_navigation',
     322                                'post_title'   => 'Custom CSS Menu',
     323                                'post_status'  => 'publish',
     324                                'post_content' => '<!-- wp:navigation-link {"label":"Home","url":"/","kind":"custom","isTopLevelLink":true,"style":{"css":"padding: 0;"}} /-->',
     325                        )
     326                );
     327
     328                $rendered = do_blocks( '<!-- wp:navigation {"ref":' . $navigation_id . '} /-->' );
     329
     330                $class_name = $this->get_custom_css_class_name_from_markup( $rendered );
     331
     332                $this->assertNotNull( $class_name, 'The navigation link should be given a custom CSS class.' );
     333                $this->assertStringContainsString(
     334                        '.' . $class_name,
     335                        $this->get_enqueued_custom_css(),
     336                        'The custom CSS stylesheet should contain a rule for the navigation link.'
     337                );
     338        }
    162339}
Note: See TracChangeset for help on using the changeset viewer.