diff --git a/src/wp-includes/block-supports/custom-css.php b/src/wp-includes/block-supports/custom-css.php index c947609a35698..e952f2f22d88a 100644 --- a/src/wp-includes/block-supports/custom-css.php +++ b/src/wp-includes/block-supports/custom-css.php @@ -25,6 +25,18 @@ * }, * ... * } $parsed_block + * @phpstan-return array{ + * blockName: string|null, + * attrs: array{ + * className?: string, + * style?: array{ + * css?: string, + * ... + * }, + * ... + * }, + * ... + * } */ function wp_render_custom_css_support_styles( $parsed_block ) { $custom_css = $parsed_block['attrs']['style']['css'] ?? null; @@ -49,20 +61,30 @@ function wp_render_custom_css_support_styles( $parsed_block ) { ? "$existing_class_name $class_name" : $class_name; - _wp_array_set( $parsed_block, array( 'attrs', 'className' ), $updated_class_name ); + $parsed_block['attrs']['className'] = $updated_class_name; // Process the custom CSS using the same method as global styles. $selector = '.' . $class_name; $processed_css = WP_Theme_JSON::process_blocks_custom_css( $custom_css, $selector ); if ( ! empty( $processed_css ) ) { - /* - * Register and add inline style for block custom CSS. - * The style depends on global-styles to ensure custom CSS loads after - * and can override global styles. + /** + * Skip CSS that has already been added. Blocks with identical attributes + * share the same class name and processed CSS via {@see wp_unique_id_from_values()}, + * so the same style would otherwise be enqueued more than once (e.g. inside + * a Query Loop or when blocks share identical custom CSS). */ - wp_register_style( 'wp-block-custom-css', false, array( 'global-styles' ) ); - wp_add_inline_style( 'wp-block-custom-css', $processed_css ); + $handle = 'wp-block-custom-css'; + if ( ! wp_style_is( $handle, 'registered' ) ) { + wp_register_style( $handle, false, array( 'global-styles' ) ); + } + $after_styles = wp_styles()->get_data( $handle, 'after' ); + if ( ! is_array( $after_styles ) ) { + $after_styles = array(); + } + if ( ! in_array( $processed_css, $after_styles, true ) ) { + wp_add_inline_style( $handle, $processed_css ); + } } return $parsed_block; diff --git a/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php b/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php index 0048b379f8f33..e9556254311b8 100644 --- a/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php +++ b/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php @@ -14,6 +14,9 @@ class Tests_Block_Supports_WpRenderCustomCssSupportStyles extends WP_UnitTestCas public function set_up() { parent::set_up(); $this->test_block_name = null; + + global $wp_styles; + $wp_styles = null; } public function tear_down() { @@ -21,6 +24,10 @@ public function tear_down() { unregister_block_type( $this->test_block_name ); } $this->test_block_name = null; + + global $wp_styles; + $wp_styles = null; + parent::tear_down(); } @@ -264,4 +271,60 @@ public function data_does_not_add_class_name() { ), ); } + + /** + * Tests that CSS is enqueued only once when the same block is rendered + * multiple times, as happens inside a Query Loop. + * + * @ticket 65268 + * + * @covers ::wp_render_custom_css_support_styles + */ + public function test_css_not_duplicated_on_repeated_renders(): void { + $this->test_block_name = 'test/custom-css-query-loop-dedup'; + register_block_type( + $this->test_block_name, + array( + 'api_version' => 3, + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( 'customCSS' => true ), + ) + ); + + $parsed_block = array( + 'blockName' => 'test/custom-css-query-loop-dedup', + 'attrs' => array( + 'style' => array( + 'css' => 'font-size: 2em; /* query-loop-dedup-test */', + ), + ), + ); + + // Simulate the same block being rendered multiple times inside a Query Loop. + $result = wp_render_custom_css_support_styles( $parsed_block ); + wp_render_custom_css_support_styles( $parsed_block ); + wp_render_custom_css_support_styles( $parsed_block ); + + // Extract the generated class name from the first render's result. + $this->assertSame( 1, preg_match( '/(?:^|\s)(wp-custom-css-\S+)/', $result['attrs']['className'] ?? '', $matches ) ); + $class_name = $matches[1]; + + // Count how many times the CSS selector for this block appears in the enqueued inline styles. + $inline_styles = (array) wp_styles()->get_data( 'wp-block-custom-css', 'after' ); + $occurrences = 0; + foreach ( $inline_styles as $style ) { + $this->assertIsString( $style ); + $occurrences += substr_count( $style, '.' . $class_name ); + } + + $this->assertSame( + 1, + $occurrences, + 'CSS should be enqueued exactly once even when the same block renders multiple times.' + ); + } }