Commit a4b00c7fb3c for woocommerce

commit a4b00c7fb3c9a31a58e832d7d6a03be2da812d4c
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date:   Wed Apr 1 21:23:45 2026 +0200

    Fix issues when Product Gallery is used inside the Product block (#63934)

    * Fix issues when Product Gallery was used inside the Product block

    * Add changelog

    * Make code more resilient

    * PHPStan fixes

    * Add missing context in usesContext

    * Remove check for Product block when setting Product Gallery image as loading eagerly

    * Remove unnecessary test

    * Remove unnecessary usesContext key

diff --git a/plugins/woocommerce/changelog/fix-product-gallery-single-product b/plugins/woocommerce/changelog/fix-product-gallery-single-product
new file mode 100644
index 00000000000..7178d10edf2
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-product-gallery-single-product
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix issues when Product Gallery was used inside the Product block
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImage.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImage.php
index 4c9170335fd..2b750105ae0 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImage.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductGalleryLargeImage.php
@@ -165,6 +165,7 @@ class ProductGalleryLargeImage extends AbstractBlock {

 		if ( 0 === $index ) {
 			$p->set_attribute( 'fetchpriority', 'high' );
+			$p->set_attribute( 'loading', 'eager' );
 		} else {
 			$p->set_attribute( 'fetchpriority', 'low' );
 			$p->set_attribute( 'loading', 'lazy' );
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductTemplate.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductTemplate.php
index 73625086454..0b8002fe6e1 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductTemplate.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductTemplate.php
@@ -194,6 +194,6 @@ class ProductTemplate extends AbstractBlock {
 		if ( ! empty( $metadata['name'] ) && 'woocommerce/product-template' === $metadata['name'] ) {
 			$settings['skip_inner_blocks'] = true;
 		}
-			return $settings;
+		return $settings;
 	}
 }
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/SingleProduct.php b/plugins/woocommerce/src/Blocks/BlockTypes/SingleProduct.php
index 3a684f5beae..df22b72d27f 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/SingleProduct.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/SingleProduct.php
@@ -133,10 +133,20 @@ class SingleProduct extends AbstractBlock {
 	 */
 	protected function replace_post_for_single_product_inner_block( $block, &$context ) {
 		if ( $this->single_product_inner_blocks_names ) {
-			$block_name = end( $this->single_product_inner_blocks_names );
+			// Find the block index in $this->single_product_inner_blocks_names
+			// starting from the end.
+			$block_index_reversed = array_search( $block['blockName'], array_reverse( $this->single_product_inner_blocks_names ), true );
+
+			if ( false !== $block_index_reversed ) {
+				$block_index = count( $this->single_product_inner_blocks_names ) - (int) $block_index_reversed - 1;
+
+				$block_name = $block['blockName'];
+
+				// Remove all blocks after the current one. In some cases, like
+				// in the Product Gallery block, inner blocks are rendered
+				// directly by the parent block, so we need to skip them.
+				$this->single_product_inner_blocks_names = array_slice( $this->single_product_inner_blocks_names, 0, $block_index );

-			if ( $block_name === $block['blockName'] ) {
-				array_pop( $this->single_product_inner_blocks_names );
 				/**
 				 * This is a temporary fix to ensure the Post Title and Excerpt blocks work as expected
 				 * until Gutenberg versions 15.2 and 15.6 are included in the core of WordPress.
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/SingleProduct.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/SingleProduct.php
new file mode 100644
index 00000000000..548eaf58200
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/SingleProduct.php
@@ -0,0 +1,150 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
+
+use WC_Helper_Product;
+
+/**
+ * Tests for the SingleProduct block type.
+ */
+class SingleProduct extends \WP_UnitTestCase {
+	/**
+	 * Creates a simple product with a featured image and gallery images.
+	 *
+	 * @param int   $gallery_count Number of gallery-only attachments (in addition to the featured image).
+	 * @param array $product_props Optional props merged into {@see WC_Helper_Product::create_simple_product()} (e.g. `name`).
+	 * @return array{product: \WC_Product, main_image_id: int, gallery_image_ids: int[]}
+	 */
+	private function create_product_with_gallery( $gallery_count = 2, array $product_props = array() ) {
+		$product = WC_Helper_Product::create_simple_product( true, $product_props );
+
+		$main_image_id = $this->create_test_jpeg_attachment( 'Main Product Image' );
+		$product->set_image_id( $main_image_id );
+
+		$gallery_image_ids = array();
+		for ( $i = 0; $i < $gallery_count; $i++ ) {
+			$gallery_image_ids[] = $this->create_test_jpeg_attachment( 'Gallery Image ' . ( $i + 1 ) );
+		}
+		$product->set_gallery_image_ids( $gallery_image_ids );
+		$product->save();
+
+		return array(
+			'product'           => $product,
+			'main_image_id'     => $main_image_id,
+			'gallery_image_ids' => $gallery_image_ids,
+		);
+	}
+
+	/**
+	 * Creates a JPEG attachment on disk so `wp_get_attachment_image` returns markup (required for the gallery viewer).
+	 *
+	 * @param string $title Attachment title.
+	 * @return int Attachment ID.
+	 */
+	private function create_test_jpeg_attachment( $title ) {
+		$file = wp_tempnam( 'wc-test-gallery-' . sanitize_title( $title ) . '.jpg' );
+
+		$attachment_id = wp_insert_attachment(
+			array(
+				'post_title'     => $title,
+				'post_type'      => 'attachment',
+				'post_mime_type' => 'image/jpeg',
+			),
+			$file
+		);
+
+		return $attachment_id;
+	}
+
+	/**
+	 * Deletes a product created by {@see create_product_with_gallery()} and its image attachments.
+	 *
+	 * @param array{product: \WC_Product, main_image_id: int, gallery_image_ids: int[]} $data Product data from create_product_with_gallery().
+	 */
+	private function delete_product_with_gallery_attachments( array $data ) {
+		WC_Helper_Product::delete_product( $data['product']->get_id() );
+		wp_delete_attachment( $data['main_image_id'], true );
+		foreach ( $data['gallery_image_ids'] as $gallery_image_id ) {
+			wp_delete_attachment( $gallery_image_id, true );
+		}
+	}
+
+	/**
+	 * Renders the Single Product block with the default-style layout.
+	 *
+	 * @param int $product_id Product ID.
+	 * @return string Rendered HTML.
+	 */
+	private function render_single_product_with_gallery_columns_and_title( $product_id ) {
+		return do_blocks(
+			sprintf(
+				'<!-- wp:woocommerce/single-product {"productId":%d} -->
+<div class="wp-block-woocommerce-single-product woocommerce">
+<!-- wp:columns -->
+<div class="wp-block-columns">
+<!-- wp:column -->
+<div class="wp-block-column">
+<!-- wp:woocommerce/product-gallery -->
+<div class="wp-block-woocommerce-product-gallery wc-block-product-gallery">
+<!-- wp:woocommerce/product-gallery-thumbnails /-->
+
+<!-- wp:woocommerce/product-gallery-large-image -->
+<div class="wp-block-woocommerce-product-gallery-large-image wc-block-product-gallery-large-image__inner-blocks">
+<!-- wp:woocommerce/product-image {"showProductLink":false,"showSaleBadge":false,"isDescendentOfSingleProductBlock":true} /-->
+
+<!-- wp:woocommerce/product-sale-badge {"align":"right"} /-->
+
+<!-- wp:woocommerce/product-gallery-large-image-next-previous -->
+<div class="wp-block-woocommerce-product-gallery-large-image-next-previous"></div>
+<!-- /wp:woocommerce/product-gallery-large-image-next-previous --></div>
+<!-- /wp:woocommerce/product-gallery-large-image --></div>
+<!-- /wp:woocommerce/product-gallery --></div>
+<!-- /wp:column -->
+
+<!-- wp:column -->
+<div class="wp-block-column">
+<!-- wp:post-title {"isLink":true,"__woocommerceNamespace":"woocommerce/product-query/product-title"} /-->
+
+<!-- wp:woocommerce/product-rating {"isDescendentOfSingleProductBlock":true} /-->
+
+<!-- wp:woocommerce/product-price {"isDescendentOfSingleProductBlock":true} /-->
+
+<!-- wp:woocommerce/product-summary {"isDescendentOfSingleProductBlock":true} /-->
+
+<!-- wp:woocommerce/product-meta -->
+<div class="wp-block-woocommerce-product-meta"></div>
+<!-- /wp:woocommerce/product-meta --></div>
+<!-- /wp:column --></div>
+<!-- /wp:columns --></div>
+<!-- /wp:woocommerce/single-product -->',
+				$product_id
+			)
+		);
+	}
+
+	/**
+	 * @testdox Post title outputs the product name when Product Gallery and viewer blocks are present in an earlier column.
+	 */
+	public function test_post_title_renders_correct_product_title_with_product_gallery_layout() {
+		$product_title = 'Product 123';
+
+		$data       = $this->create_product_with_gallery(
+			2,
+			array(
+				'name' => $product_title,
+			)
+		);
+		$product_id = $data['product']->get_id();
+
+		try {
+			$markup = $this->render_single_product_with_gallery_columns_and_title( $product_id );
+
+			$this->assertStringContainsString( 'wp-block-post-title', $markup, 'The core Post Title block should render inside the Single Product block.' );
+			$this->assertStringContainsString( $product_title, $markup, 'The visible product title should match the product post title, not the global post.' );
+		} finally {
+			$this->delete_product_with_gallery_attachments( $data );
+		}
+	}
+}