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.
 	 */