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 );
+ }
+ }
+}