Commit 714ddea58d3 for woocommerce
commit 714ddea58d335cdb2db6556f74a28670a7d3cdb8
Author: Luigi Teschio <gigitux@gmail.com>
Date: Tue Oct 20 14:41:56 2026 +0200
Scope Product Collection context to collection descendants (#69802)
* Scope Product Collection context to collection descendants
* Add changelog entry for Product Collection context scoping
* improve guard
* improve logic
* improve code
diff --git a/plugins/woocommerce/changelog/fix-product-collection-inner-block-context b/plugins/woocommerce/changelog/fix-product-collection-inner-block-context
new file mode 100644
index 00000000000..3ca3155819d
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-product-collection-inner-block-context
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Prevent Product Collection behavior from affecting blocks outside the collection.
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Renderer.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Renderer.php
index 39ea66d826b..7973e7913ff 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Renderer.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Renderer.php
@@ -87,7 +87,7 @@ class Renderer {
1
);
add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 );
- add_filter( 'render_block_context', array( $this, 'extend_context_for_inner_blocks' ), 11, 2 );
+ add_filter( 'render_block_context', array( $this, 'handle_render_block_context' ), 11, 3 );
}
/**
@@ -402,6 +402,33 @@ class Renderer {
return $block_content;
}
+ /**
+ * Extend context only for Product Collection blocks and their descendants.
+ *
+ * @since 11.3.0
+ * @internal
+ *
+ * @param array $context The block context.
+ * @param array $parsed_block The parsed block being rendered.
+ * @param \WP_Block|null $parent_block The parent block instance, or null for a top-level block.
+ * @return array
+ */
+ public function handle_render_block_context( $context, $parsed_block, $parent_block = null ) {
+ $block_name = $parsed_block['blockName'] ?? '';
+
+ if (
+ 'woocommerce/product-collection' !== $block_name &&
+ (
+ ! $parent_block instanceof \WP_Block ||
+ ( 'woocommerce/product-collection' !== $parent_block->name && ( ! isset( $parent_block->context['isProductCollectionInnerBlock'] ) || true !== ( $parent_block->context['isProductCollectionInnerBlock'] ) ) )
+ )
+ ) {
+ return $context;
+ }
+
+ return $this->extend_context_for_inner_blocks( $context, $parsed_block );
+ }
+
/**
* Provides the location context to each inner block of the product collection block.
* Hint: Only blocks using the 'query' context will be affected.
@@ -431,7 +458,8 @@ class Renderer {
*/
public function extend_context_for_inner_blocks( $context, $parsed_block = array() ) {
// Add iapi/provider to inner blocks so they can run this store's Interactivity API actions.
- $context['iapi/provider'] = 'woocommerce/product-collection';
+ $context['iapi/provider'] = 'woocommerce/product-collection';
+ $context['isProductCollectionInnerBlock'] = true;
$block_name = $parsed_block['blockName'] ?? '';
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductTemplate.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductTemplate.php
index 354e7825c61..6df400ac8b1 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductTemplate.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductTemplate.php
@@ -107,14 +107,13 @@ class ProductTemplate extends AbstractBlock {
// Use an early priority so that other 'render_block_context' filters have access to the values.
add_filter( 'render_block_context', $filter_block_context, 1 );
- // Render the inner blocks of the Post Template block with `dynamic` set to `false` to prevent calling
- // `render_callback` and ensure that no wrapper markup is included.
- $block_content = (
- new WP_Block(
- $block_instance,
- $block->context
- )
- )->render( array( 'dynamic' => false ) );
+
+ $inner_block = new WP_Block( $block_instance, $block->context );
+ // core/null has no uses_context, so preserve the collection marker for descendant checks.
+ $inner_block->context['isProductCollectionInnerBlock'] = $block->context['isProductCollectionInnerBlock'] ?? false;
+
+ // Render only the inner blocks, without the Product Template callback or wrapper.
+ $block_content = $inner_block->render( array( 'dynamic' => false ) );
remove_filter( 'render_block_context', $filter_block_context, 1 );
// Load product into the shared products store.
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/RendererTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/RendererTest.php
index c99a3cd7dea..f70f9aefec4 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/RendererTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/RendererTest.php
@@ -4,6 +4,7 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes\ProductCollection;
use Automattic\WooCommerce\Blocks\BlockTypes\ProductCollection\Renderer;
+use WC_Helper_Product;
use WC_Unit_Test_Case;
/**
@@ -24,6 +25,61 @@ class RendererTest extends WC_Unit_Test_Case {
'render_block_context',
);
+ /**
+ * @testdox Should extend context only for Product Collection and its descendants, including through Group and Product Template blocks.
+ */
+ public function test_scopes_context_to_collection_descendants(): void {
+ remove_all_filters( 'render_block_context' );
+ new Renderer();
+ $product = WC_Helper_Product::create_simple_product();
+ $contexts = array();
+ add_filter(
+ 'render_block_context',
+ function ( $context, $parsed_block ) use ( &$contexts ) {
+ $anchor = $parsed_block['attrs']['anchor'] ?? '';
+ if ( $anchor ) {
+ $contexts[ $anchor ] = $context;
+ }
+ return $context;
+ },
+ 12,
+ 2
+ );
+
+ $markup = '<!-- wp:group {"anchor":"outside-group"} --><div class="wp-block-group">'
+ . '<!-- wp:paragraph {"anchor":"outside-child"} --><p>Outside</p><!-- /wp:paragraph -->'
+ . '<!-- wp:woocommerce/product-collection {"anchor":"collection","query":{"isProductCollectionBlock":false,"postType":"product","perPage":1,"inherit":false}} -->'
+ . '<div class="wp-block-woocommerce-product-collection">'
+ . '<!-- wp:group {"anchor":"inside-group"} --><div class="wp-block-group">'
+ . '<!-- wp:group {"anchor":"nested-group"} --><div class="wp-block-group">'
+ . '<!-- wp:paragraph {"anchor":"inside-child"} --><p>Inside</p><!-- /wp:paragraph -->'
+ . '</div><!-- /wp:group --></div><!-- /wp:group -->'
+ . '<!-- wp:woocommerce/product-template -->'
+ . '<!-- wp:group {"anchor":"inside-template-group"} --><div class="wp-block-group">'
+ . '<!-- wp:post-title {"anchor":"inside-product-title"} /-->'
+ . '</div><!-- /wp:group -->'
+ . '<!-- /wp:woocommerce/product-template -->'
+ . '</div><!-- /wp:woocommerce/product-collection -->'
+ . '<!-- wp:paragraph {"anchor":"outside-sibling"} --><p>After</p><!-- /wp:paragraph -->'
+ . '</div><!-- /wp:group -->';
+
+ render_block( parse_blocks( $markup )[0] );
+
+ foreach ( array( 'collection', 'inside-group', 'nested-group', 'inside-child', 'inside-template-group', 'inside-product-title' ) as $anchor ) {
+ $this->assertArrayHasKey( $anchor, $contexts, "The {$anchor} block must render." );
+ $this->assertSame( 'woocommerce/product-collection', $contexts[ $anchor ]['iapi/provider'] ?? null, "The {$anchor} block should use Product Collection actions." );
+ $this->assertTrue( $contexts[ $anchor ]['isProductCollectionInnerBlock'] ?? false, "The {$anchor} block should be marked as a collection descendant." );
+ }
+ $this->assertSame( $product->get_id(), $contexts['inside-product-title']['postId'] ?? null, 'The title must render for the product in the Product Template loop.' );
+
+ foreach ( array( 'outside-group', 'outside-child', 'outside-sibling' ) as $anchor ) {
+ $this->assertArrayHasKey( $anchor, $contexts, "The {$anchor} block must render." );
+ $this->assertArrayNotHasKey( 'iapi/provider', $contexts[ $anchor ], "The {$anchor} block must not receive a Product Collection provider." );
+ $this->assertArrayNotHasKey( 'isProductCollectionInnerBlock', $contexts[ $anchor ], "The {$anchor} block must not be marked as a collection descendant." );
+ $this->assertArrayNotHasKey( 'productCollectionLocation', $contexts[ $anchor ], "The {$anchor} block must not receive a Product Collection location." );
+ }
+ }
+
/**
* @testdox Should reset result and No Results state between Product Collection renders.
*/