From 7192eb860db872405d071afb43ba6aaf30726b00 Mon Sep 17 00:00:00 2001 From: Mukesh Panchal Date: Mon, 28 Sep 2026 10:30:44 +0530 Subject: [PATCH 1/2] Deduplicate position block support container classes --- src/wp-includes/block-supports/position.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/block-supports/position.php b/src/wp-includes/block-supports/position.php index 18968bb8be578..095e18e0da685 100644 --- a/src/wp-includes/block-supports/position.php +++ b/src/wp-includes/block-supports/position.php @@ -148,12 +148,26 @@ function wp_render_position_support( $block_content, $block ) { $viewport_settings = $global_settings['viewport'] ?? null; $responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings ); - $class_name = wp_unique_id( 'wp-container-' ); - $selector = ".$class_name"; $position_styles = array(); $wrapper_classes = array(); $base_position = $style_attribute['position'] ?? null; + /* + * Derive the container class from the position styles rather than from a + * counter, so that blocks resolving to the same CSS share a single rule in + * the style engine store instead of emitting one rule per block instance. + * + * The hash covers the default state and every viewport state, since each + * contributes rules scoped to this class. + */ + $position_hash_input = array( $base_position ); + foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) { + $position_hash_input[ $breakpoint ] = $style_attribute[ $breakpoint ]['position'] ?? null; + } + + $class_name = wp_unique_id_from_values( $position_hash_input, 'wp-container-' ); + $selector = ".$class_name"; + // Default viewport (base) position styles. $base_styles = wp_get_position_support_styles( $selector, From 6d4fae4a00156942eb21128f6982316d9c5af039 Mon Sep 17 00:00:00 2001 From: Mukesh Panchal Date: Mon, 28 Sep 2026 10:51:30 +0530 Subject: [PATCH 2/2] Add tests for block position style sharing behaviour --- .../wpRenderPositionSupport.php | 201 +++++++++++++++++- 1 file changed, 193 insertions(+), 8 deletions(-) diff --git a/tests/phpunit/tests/block-supports/wpRenderPositionSupport.php b/tests/phpunit/tests/block-supports/wpRenderPositionSupport.php index 94032a7b024a3..58030544b9c26 100644 --- a/tests/phpunit/tests/block-supports/wpRenderPositionSupport.php +++ b/tests/phpunit/tests/block-supports/wpRenderPositionSupport.php @@ -127,6 +127,191 @@ public function test_position_block_support( $theme_name, $block_name, $position ); } + /** + * Tests that blocks resolving to the same position styles share a single CSS rule. + * + * The container class is derived from the position styles, so repeating a block + * with the same position does not add a rule per instance to the style engine store. + * + * @ticket 66200 + * + * @covers ::wp_render_position_support + */ + public function test_identical_position_styles_share_one_rule() { + switch_theme( 'block-theme-child-with-fluid-typography' ); + $this->test_block_name = 'test/position-rules-are-deduplicated'; + + register_block_type( + $this->test_block_name, + array( + 'api_version' => 2, + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( + 'position' => true, + ), + ) + ); + + $block = array( + 'blockName' => $this->test_block_name, + 'attrs' => array( + 'style' => array( + 'position' => array( + 'type' => 'sticky', + 'top' => '0px', + ), + ), + ), + ); + + $first = wp_render_position_support( '
One
', $block ); + $second = wp_render_position_support( '
Two
', $block ); + + $this->assertSame( + 1, + preg_match( '/class="(wp-container-[0-9a-f]{8}) is-position-sticky"/', $first, $first_match ), + 'First block should receive a hashed position container class.' + ); + $this->assertSame( + 1, + preg_match( '/class="(wp-container-[0-9a-f]{8}) is-position-sticky"/', $second, $second_match ), + 'Second block should receive a hashed position container class.' + ); + $this->assertSame( + $first_match[1], + $second_match[1], + 'Blocks with identical position styles should share a container class.' + ); + + $stylesheet = wp_style_engine_get_stylesheet_from_context( + 'block-supports', + array( + 'prettify' => false, + ) + ); + + $this->assertSame( + 1, + substr_count( $stylesheet, '.' . $first_match[1] . '{' ), + 'Identical position styles should produce a single CSS rule.' + ); + } + + /** + * Tests that blocks with different position styles do not share a CSS rule. + * + * @ticket 66200 + * + * @covers ::wp_render_position_support + */ + public function test_differing_position_styles_do_not_share_a_rule() { + switch_theme( 'block-theme-child-with-fluid-typography' ); + $this->test_block_name = 'test/position-rules-differ'; + + register_block_type( + $this->test_block_name, + array( + 'api_version' => 2, + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( + 'position' => true, + ), + ) + ); + + $make_block = static function ( $top ) { + return array( + 'blockName' => 'test/position-rules-differ', + 'attrs' => array( + 'style' => array( + 'position' => array( + 'type' => 'sticky', + 'top' => $top, + ), + ), + ), + ); + }; + + $first = wp_render_position_support( '
One
', $make_block( '0px' ) ); + $second = wp_render_position_support( '
Two
', $make_block( '10px' ) ); + + preg_match( '/class="(wp-container-[0-9a-f]{8}) /', $first, $first_match ); + preg_match( '/class="(wp-container-[0-9a-f]{8}) /', $second, $second_match ); + + $this->assertNotSame( + $first_match[1], + $second_match[1], + 'Blocks with different position styles should not share a container class.' + ); + } + + /** + * Tests that blocks differing only in a viewport state position do not share a CSS rule. + * + * @ticket 66200 + * + * @covers ::wp_render_position_support + */ + public function test_differing_viewport_position_styles_do_not_share_a_rule() { + switch_theme( 'block-theme-child-with-fluid-typography' ); + $this->test_block_name = 'test/position-viewport-rules-differ'; + + register_block_type( + $this->test_block_name, + array( + 'api_version' => 2, + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( + 'position' => true, + ), + ) + ); + + $make_block = static function ( $tablet_top ) { + return array( + 'blockName' => 'test/position-viewport-rules-differ', + 'attrs' => array( + 'style' => array( + 'position' => array( + 'type' => 'sticky', + 'top' => '0px', + ), + '@tablet' => array( + 'position' => array( + 'top' => $tablet_top, + ), + ), + ), + ), + ); + }; + + $first = wp_render_position_support( '
One
', $make_block( '1rem' ) ); + $second = wp_render_position_support( '
Two
', $make_block( '2rem' ) ); + + preg_match( '/class="(wp-container-[0-9a-f]{8}) /', $first, $first_match ); + preg_match( '/class="(wp-container-[0-9a-f]{8}) /', $second, $second_match ); + + $this->assertNotSame( + $first_match[1], + $second_match[1], + 'Blocks with different viewport position styles should not share a container class.' + ); + } + /** * Data provider. * @@ -144,8 +329,8 @@ public function data_position_block_support() { 'top' => '0px', ), ), - 'expected_wrapper' => '/^
Content<\/div>$/', - 'expected_styles' => '/^.wp-container-\d+' . preg_quote( '{top:calc(0px + var(--wp-admin--admin-bar--position-offset, 0px));position:sticky;z-index:10;}' ) . '$/', + 'expected_wrapper' => '/^
Content<\/div>$/', + 'expected_styles' => '/^.wp-container-[0-9a-f]{8}' . preg_quote( '{top:calc(0px + var(--wp-admin--admin-bar--position-offset, 0px));position:sticky;z-index:10;}' ) . '$/', ), 'sticky position style is not applied if theme does not support it' => array( 'theme_name' => 'default', @@ -198,8 +383,8 @@ public function data_position_block_support() { ), ), ), - 'expected_wrapper' => '/^
Content<\/div>$/', - 'expected_styles' => '/^' . preg_quote( '@media (width <= 480px){.wp-container-' ) . '\d+' . preg_quote( '{top:calc(0px + var(--wp-admin--admin-bar--position-offset, 0px));position:sticky;z-index:10;}}' ) . '$/', + 'expected_wrapper' => '/^
Content<\/div>$/', + 'expected_styles' => '/^' . preg_quote( '@media (width <= 480px){.wp-container-' ) . '[0-9a-f]{8}' . preg_quote( '{top:calc(0px + var(--wp-admin--admin-bar--position-offset, 0px));position:sticky;z-index:10;}}' ) . '$/', ), 'viewport position style inherits the default state position type' => array( 'theme_name' => 'block-theme-child-with-fluid-typography', @@ -216,8 +401,8 @@ public function data_position_block_support() { ), ), ), - 'expected_wrapper' => '/^
Content<\/div>$/', - 'expected_styles' => '/^' . preg_quote( '.wp-container-' ) . '\d+' . preg_quote( '{top:calc(0px + var(--wp-admin--admin-bar--position-offset, 0px));position:sticky;z-index:10;}' ) . preg_quote( '@media (480px < width <= 782px){.wp-container-' ) . '\d+' . preg_quote( '{top:calc(2rem + var(--wp-admin--admin-bar--position-offset, 0px));position:sticky;z-index:10;}}' ) . '$/', + 'expected_wrapper' => '/^
Content<\/div>$/', + 'expected_styles' => '/^' . preg_quote( '.wp-container-' ) . '[0-9a-f]{8}' . preg_quote( '{top:calc(0px + var(--wp-admin--admin-bar--position-offset, 0px));position:sticky;z-index:10;}' ) . preg_quote( '@media (480px < width <= 782px){.wp-container-' ) . '[0-9a-f]{8}' . preg_quote( '{top:calc(2rem + var(--wp-admin--admin-bar--position-offset, 0px));position:sticky;z-index:10;}}' ) . '$/', ), 'viewport position style is not applied if theme does not support it' => array( 'theme_name' => 'default', @@ -249,8 +434,8 @@ public function data_position_block_support() { ), ), ), - 'expected_wrapper' => '/^
Content<\/div>$/', - 'expected_styles' => '/^' . preg_quote( '.wp-container-' ) . '\d+' . preg_quote( '{top:calc(0px + var(--wp-admin--admin-bar--position-offset, 0px));position:sticky;z-index:10;}' ) . preg_quote( '@media (width <= 480px){.wp-container-' ) . '\d+' . preg_quote( '{position:static;}}' ) . '$/', + 'expected_wrapper' => '/^
Content<\/div>$/', + 'expected_styles' => '/^' . preg_quote( '.wp-container-' ) . '[0-9a-f]{8}' . preg_quote( '{top:calc(0px + var(--wp-admin--admin-bar--position-offset, 0px));position:sticky;z-index:10;}' ) . preg_quote( '@media (width <= 480px){.wp-container-' ) . '[0-9a-f]{8}' . preg_quote( '{position:static;}}' ) . '$/', ), ); }