From c141c00caa52b0205c566afddec547d626b4bf13 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Thu, 12 Nov 2026 20:53:54 +0530 Subject: [PATCH] Fix element styles across Query Loop navigation --- src/wp-includes/block-supports/elements.php | 70 ++++++++++++++- .../wpRenderElementsSupport.php | 87 +++++++++++++++++-- .../wpRenderElementsSupportStyles.php | 24 ++--- 3 files changed, 160 insertions(+), 21 deletions(-) diff --git a/src/wp-includes/block-supports/elements.php b/src/wp-includes/block-supports/elements.php index e8076cc1bd3ba..10f4b6b020451 100644 --- a/src/wp-includes/block-supports/elements.php +++ b/src/wp-includes/block-supports/elements.php @@ -12,12 +12,74 @@ * @since 6.0.0 * @access private * + * @param array $block Parsed block data. * @return string The unique class name. * * @phpstan-return lowercase-string&non-falsy-string */ -function wp_get_elements_class_name(): string { - return wp_unique_prefixed_id( 'wp-elements-' ); +function wp_get_elements_class_name( $block ): string { + $hash = md5( serialize( $block ) ); + $scope = wp_elements_class_name_scope(); + + return wp_unique_prefixed_id( 'wp-elements-' . $hash . ( '' !== $scope ? '-q' . $scope . '-' : '' ) ); +} + +/** + * Gets or updates the current Query block scope for elements class names. + * + * @since 7.2.0 + * @access private + * + * @param string $operation Operation to perform: 'get', 'push', or 'pop'. + * @param string $id Query block ID to push onto the scope stack. + * @return string The current Query block ID, or an empty string. + */ +function wp_elements_class_name_scope( $operation = 'get', $id = '' ) { + static $stack = array(); + + if ( 'push' === $operation ) { + $stack[] = sanitize_html_class( $id ); + } elseif ( 'pop' === $operation && ! empty( $stack ) ) { + array_pop( $stack ); + } + + return empty( $stack ) ? '' : (string) end( $stack ); +} + +/** + * Tracks the Query block scope when rendering its inner blocks. + * + * @since 7.2.0 + * @access private + * + * @param array $parsed_block The block being rendered. + * @return array The parsed block, unmodified. + */ +function wp_elements_track_query_scope( $parsed_block ) { + if ( isset( $parsed_block['blockName'] ) && 'core/query' === $parsed_block['blockName'] ) { + $query_id = $parsed_block['attrs']['queryId'] ?? ''; + wp_elements_class_name_scope( 'push', is_scalar( $query_id ) ? (string) $query_id : '' ); + } + + return $parsed_block; +} + +/** + * Restores the previous Query block scope after rendering a Query block. + * + * @since 7.2.0 + * @access private + * + * @param string $block_content The block content. + * @param array $block The full block, including name and attributes. + * @return string The block content, unmodified. + */ +function wp_elements_untrack_query_scope( $block_content, $block ) { + if ( isset( $block['blockName'] ) && 'core/query' === $block['blockName'] ) { + wp_elements_class_name_scope( 'pop' ); + } + + return $block_content; } /** @@ -184,7 +246,7 @@ function wp_render_elements_support_styles( $parsed_block ) { return $parsed_block; } - $class_name = wp_get_elements_class_name(); + $class_name = wp_get_elements_class_name( $parsed_block ); $updated_class_name = isset( $parsed_block['attrs']['className'] ) ? $parsed_block['attrs']['className'] . " $class_name" : $class_name; _wp_array_set( $parsed_block, array( 'attrs', 'className' ), $updated_class_name ); @@ -308,3 +370,5 @@ function wp_render_elements_class_name( $block_content, $block ) { add_filter( 'render_block', 'wp_render_elements_class_name', 10, 2 ); add_filter( 'render_block_data', 'wp_render_elements_support_styles', 10, 1 ); +add_filter( 'render_block_data', 'wp_elements_track_query_scope', 9, 1 ); +add_filter( 'render_block', 'wp_elements_untrack_query_scope', 1000, 2 ); diff --git a/tests/phpunit/tests/block-supports/wpRenderElementsSupport.php b/tests/phpunit/tests/block-supports/wpRenderElementsSupport.php index aae61805ab5f0..acf8649900163 100644 --- a/tests/phpunit/tests/block-supports/wpRenderElementsSupport.php +++ b/tests/phpunit/tests/block-supports/wpRenderElementsSupport.php @@ -213,7 +213,7 @@ public function test_elements_block_support_class_with_duplicate_blocks(): void $elements_class_name = array_first( array_filter( iterator_to_array( $processor->class_list() ), - fn( string $class_name ) => (bool) preg_match( '/^wp-elements-\d+$/', $class_name ) + fn( string $class_name ) => (bool) preg_match( '/^wp-elements-[a-f0-9]{32}\d+$/', $class_name ) ) ); $this->assertIsString( $elements_class_name, "Expected wp-elements class in block #$i." ); @@ -223,6 +223,81 @@ public function test_elements_block_support_class_with_duplicate_blocks(): void $this->assertSame( $count, count( array_unique( $elements_class_names ) ), 'Expected each rendered block to have a unique wp-elements class name.' ); } + /** + * Tests that elements class name counters are scoped to their Query block. + * + * @ticket 66633 + * + * @covers ::wp_get_elements_class_name + * @covers ::wp_elements_class_name_scope + * @covers ::wp_elements_track_query_scope + * @covers ::wp_elements_untrack_query_scope + */ + public function test_elements_block_support_class_names_are_scoped_to_query_blocks(): void { + $this->test_block_name = 'test/element-block-supports'; + + register_block_type( + $this->test_block_name, + array( + 'api_version' => 3, + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( + 'color' => array( + 'link' => true, + ), + ), + ) + ); + + $block = array( + 'blockName' => $this->test_block_name, + 'attrs' => array( + 'style' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'ticket-66633-' . wp_rand(), + ), + ), + ), + ), + ), + ); + + $first_query = array( + 'blockName' => 'core/query', + 'attrs' => array( + 'queryId' => 6663301, + ), + ); + $first_query = apply_filters( 'render_block_data', $first_query, $first_query, null ); + $first_query_instance = new WP_Block( $first_query ); + $first_query_child = apply_filters( 'render_block_data', $block, $block, $first_query_instance ); + $second_query_child = apply_filters( 'render_block_data', $block, $block, $first_query_instance ); + + $second_query = array( + 'blockName' => 'core/query', + 'attrs' => array( + 'queryId' => 6663302, + ), + ); + $second_query = apply_filters( 'render_block_data', $second_query, $second_query, $first_query_instance ); + $second_query_instance = new WP_Block( $second_query, $first_query_instance->context ); + $nested_query_child = apply_filters( 'render_block_data', $block, $block, $second_query_instance ); + apply_filters( 'render_block', '', $second_query, $second_query_instance ); + $restored_query_child = apply_filters( 'render_block_data', $block, $block, $first_query_instance ); + apply_filters( 'render_block', '', $first_query, $first_query_instance ); + + $this->assertMatchesRegularExpression( '/-q' . $first_query['attrs']['queryId'] . '-1$/', $first_query_child['attrs']['className'] ); + $this->assertMatchesRegularExpression( '/-q' . $first_query['attrs']['queryId'] . '-2$/', $second_query_child['attrs']['className'] ); + $this->assertMatchesRegularExpression( '/-q' . $second_query['attrs']['queryId'] . '-1$/', $nested_query_child['attrs']['className'] ); + $this->assertMatchesRegularExpression( '/-q' . $first_query['attrs']['queryId'] . '-3$/', $restored_query_child['attrs']['className'] ); + } + /** * Data provider. * @@ -302,7 +377,7 @@ public static function data_elements_block_support_class() { 'button' => array( 'color' => $color_styles ), ), 'block_markup' => '

Hello WordPress!

', - 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', + 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', ), 'link element styles apply class to wrapper' => array( 'color_settings' => array( 'link' => true ), @@ -310,7 +385,7 @@ public static function data_elements_block_support_class() { 'link' => array( 'color' => $color_styles ), ), 'block_markup' => '

Hello WordPress!

', - 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', + 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', ), 'heading element styles apply class to wrapper' => array( 'color_settings' => array( 'heading' => true ), @@ -318,7 +393,7 @@ public static function data_elements_block_support_class() { 'heading' => array( 'color' => $color_styles ), ), 'block_markup' => '

Hello WordPress!

', - 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', + 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', ), 'element styles apply class to wrapper when it has other classes' => array( 'color_settings' => array( 'link' => true ), @@ -326,7 +401,7 @@ public static function data_elements_block_support_class() { 'link' => array( 'color' => $color_styles ), ), 'block_markup' => '

Hello WordPress!

', - 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', + 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', ), 'element styles apply class to wrapper when it has other attributes' => array( 'color_settings' => array( 'link' => true ), @@ -334,7 +409,7 @@ public static function data_elements_block_support_class() { 'link' => array( 'color' => $color_styles ), ), 'block_markup' => '

Hello WordPress!

', - 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', + 'expected_markup' => '/^

Hello WordPress<\/a>!<\/p>$/', ), ); } diff --git a/tests/phpunit/tests/block-supports/wpRenderElementsSupportStyles.php b/tests/phpunit/tests/block-supports/wpRenderElementsSupportStyles.php index cc36a4ee52cf0..d23d7e71f0c22 100644 --- a/tests/phpunit/tests/block-supports/wpRenderElementsSupportStyles.php +++ b/tests/phpunit/tests/block-supports/wpRenderElementsSupportStyles.php @@ -120,7 +120,7 @@ public function test_elements_block_support_styles_with_duplicate_blocks(): void $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); // Count the number of distinct class names to confirm uniqueness. - $this->assertSame( $count, preg_match_all( '/\.wp-elements-(\d+)/', $actual_stylesheet, $matches ) ); + $this->assertSame( $count, preg_match_all( '/\.wp-elements-([a-f0-9]{32}\d+)/', $actual_stylesheet, $matches ) ); $unique_classes = array_unique( $matches[1] ); $this->assertCount( $count, $unique_classes, 'Both blocks should produce distinct class names' ); } @@ -184,7 +184,7 @@ public static function data_elements_block_support_styles() { 'elements_styles' => array( 'button' => array( 'color' => $color_styles ), ), - 'expected_styles' => '/^.wp-elements-\d+ .wp-element-button, .wp-elements-\d+ .wp-block-button__link' . $color_css_rules . '$/', + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32}\d+ .wp-element-button, .wp-elements-[a-f0-9]{32}\d+ .wp-block-button__link' . $color_css_rules . '$/', ), 'button hover styles are skipped without a hover selector' => array( 'color_settings' => array( 'button' => true ), @@ -200,7 +200,7 @@ public static function data_elements_block_support_styles() { * type has no `hover_selector`, so the `:hover` object must be * ignored rather than triggering an undefined array key warning. */ - 'expected_styles' => '/^.wp-elements-\d+ .wp-element-button, .wp-elements-\d+ .wp-block-button__link' . $color_css_rules . '$/', + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32}\d+ .wp-element-button, .wp-elements-[a-f0-9]{32}\d+ .wp-block-button__link' . $color_css_rules . '$/', ), 'link element styles are applied' => array( 'color_settings' => array( 'link' => true ), @@ -212,15 +212,15 @@ public static function data_elements_block_support_styles() { ), ), ), - 'expected_styles' => '/^.wp-elements-\d+ a:where\(:not\(.wp-element-button\)\)' . $color_css_rules . - '.wp-elements-\d+ a:where\(:not\(.wp-element-button\)\):hover' . $color_css_rules . '$/', + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32}\d+ a:where\(:not\(.wp-element-button\)\)' . $color_css_rules . + '.wp-elements-[a-f0-9]{32}\d+ a:where\(:not\(.wp-element-button\)\):hover' . $color_css_rules . '$/', ), 'generic heading element styles are applied' => array( 'color_settings' => array( 'heading' => true ), 'elements_styles' => array( 'heading' => array( 'color' => $color_styles ), ), - 'expected_styles' => '/^.wp-elements-\d+ h1, .wp-elements-\d+ h2, .wp-elements-\d+ h3, .wp-elements-\d+ h4, .wp-elements-\d+ h5, .wp-elements-\d+ h6' . $color_css_rules . '$/', + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32}\d+ h1, .wp-elements-[a-f0-9]{32}\d+ h2, .wp-elements-[a-f0-9]{32}\d+ h3, .wp-elements-[a-f0-9]{32}\d+ h4, .wp-elements-[a-f0-9]{32}\d+ h5, .wp-elements-[a-f0-9]{32}\d+ h6' . $color_css_rules . '$/', ), 'individual heading element styles are applied' => array( 'color_settings' => array( 'heading' => true ), @@ -232,12 +232,12 @@ public static function data_elements_block_support_styles() { 'h5' => array( 'color' => $color_styles ), 'h6' => array( 'color' => $color_styles ), ), - 'expected_styles' => '/^.wp-elements-\d+ h1' . $color_css_rules . - '.wp-elements-\d+ h2' . $color_css_rules . - '.wp-elements-\d+ h3' . $color_css_rules . - '.wp-elements-\d+ h4' . $color_css_rules . - '.wp-elements-\d+ h5' . $color_css_rules . - '.wp-elements-\d+ h6' . $color_css_rules . '$/', + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32}\d+ h1' . $color_css_rules . + '.wp-elements-[a-f0-9]{32}\d+ h2' . $color_css_rules . + '.wp-elements-[a-f0-9]{32}\d+ h3' . $color_css_rules . + '.wp-elements-[a-f0-9]{32}\d+ h4' . $color_css_rules . + '.wp-elements-[a-f0-9]{32}\d+ h5' . $color_css_rules . + '.wp-elements-[a-f0-9]{32}\d+ h6' . $color_css_rules . '$/', ), ); }