Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions src/wp-includes/block-supports/custom-css.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
/**
Comment thread
t-hamano marked this conversation as resolved.
* 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' ) );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a slight concern about this code. What happens if a consumer intentionally deregister global-styles? In that case, global-styles would be forcibly registereded only when a block with custom CSS exists on the page, which might go against the consumer's intent. Shouldn't custom CSS operate independently of global styles?

However, since this logic was not introduced in this PR, it is not a blocker. We may be able to consider a fix in a follow-up.

}
$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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@ 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() {
if ( $this->test_block_name ) {
unregister_block_type( $this->test_block_name );
}
$this->test_block_name = null;

global $wp_styles;
$wp_styles = null;

parent::tear_down();
}

Expand Down Expand Up @@ -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.'
);
}
}
Loading