Commit b58c5035665 for woocommerce

commit b58c5035665ae35b8bfa53b0ff4016eec0c14fb9
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date:   Fri Jul 17 16:02:58 2026 +0200

    Make images in Featured Product and Featured Category blocks responsive (#66466)

    * Add changelog

    * Make images in Featured Product and Featured Category blocks responsive

    * Update PHPStan baseline

    * Simplify code

    * Make get_item_image_id() concrete

    * Override responsive image sizes when Image Fit is None

    * Add fall back in case get_item_image_id is not implemented

diff --git a/plugins/woocommerce/changelog/fix-make-featured-product-category-images-responsive b/plugins/woocommerce/changelog/fix-make-featured-product-category-images-responsive
new file mode 100644
index 00000000000..677f5100ff4
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-make-featured-product-category-images-responsive
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Make images in Featured Product and Featured Category blocks responsive
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 8265f01ccb0..a6847dfae7e 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -50859,12 +50859,6 @@ parameters:
 			count: 1
 			path: src/Blocks/BlockTypes/FeaturedProduct.php

-		-
-			message: '#^Parameter \#1 \$attachment_id of function wp_get_attachment_image_url expects int, string given\.$#'
-			identifier: argument.type
-			count: 2
-			path: src/Blocks/BlockTypes/FeaturedProduct.php
-
 		-
 			message: '#^Parameter \#1 \$variation of function wc_get_formatted_variation expects array\|WC_Product_Variation, WC_Product given\.$#'
 			identifier: argument.type
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedCategory.php b/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedCategory.php
index 325a07f3c63..9cef5d7fff1 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedCategory.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedCategory.php
@@ -57,6 +57,16 @@ class FeaturedCategory extends FeaturedItem {
 		return $category->name;
 	}

+	/**
+	 * Returns the featured category image attachment ID.
+	 *
+	 * @param \WP_Term $category Term object.
+	 * @return int
+	 */
+	protected function get_item_image_id( $category ) {
+		return (int) get_term_meta( $category->term_id, 'thumbnail_id', true );
+	}
+
 	/**
 	 * Returns the featured category image URL.
 	 *
@@ -65,14 +75,13 @@ class FeaturedCategory extends FeaturedItem {
 	 * @return string
 	 */
 	protected function get_item_image( $category, $size = 'full' ) {
-		$image    = '';
-		$image_id = get_term_meta( $category->term_id, 'thumbnail_id', true );
+		$image_id = $this->get_item_image_id( $category );

 		if ( $image_id ) {
-			$image = wp_get_attachment_image_url( $image_id, $size );
+			return wp_get_attachment_image_url( $image_id, $size );
 		}

-		return $image;
+		return '';
 	}

 	/**
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedItem.php b/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedItem.php
index 166fc2c212e..295d6d40fd0 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedItem.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedItem.php
@@ -203,6 +203,22 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
 	 */
 	abstract protected function get_item_image( $item, $size = 'full' );

+	/**
+	 * Returns the featured item image attachment ID.
+	 *
+	 * Note: Ideally, this method would be declared as abstract.
+	 * However, it remains a concrete method returning 0 to preserve legacy
+	 * compatibility with existing child classes that may not implement it.
+	 * See:
+	 * https://github.com/woocommerce/woocommerce/pull/66466#discussion_r3559124282
+	 *
+	 * @param \WP_Term|\WC_Product $item Item object.
+	 * @return int
+	 */
+	protected function get_item_image_id( $item ) {
+		return 0;
+	}
+
 	/**
 	 * Renders the featured item attributes.
 	 *
@@ -231,8 +247,6 @@ abstract class FeaturedItem extends AbstractDynamicBlock {

 		$attributes['height'] = $attributes['height'] ?? wc_get_theme_support( 'featured_block::default_height', 500 );

-		$image_url = esc_url( $this->get_image_url( $attributes, $item ) );
-
 		$styles  = $this->get_styles( $attributes );
 		$classes = $this->get_classes( $attributes );

@@ -241,9 +255,9 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
 		$output .= $this->render_overlay( $attributes );

 		if ( ! $attributes['isRepeated'] && ! $attributes['hasParallax'] ) {
-			$output .= $this->render_image( $attributes, $item, $image_url );
+			$output .= $this->render_image( $attributes, $item );
 		} else {
-			$output .= $this->render_bg_image( $attributes, $image_url );
+			$output .= $this->render_bg_image( $attributes, $item );
 		}

 		if ( isset( $aria_label ) && ! empty( $aria_label ) ) {
@@ -268,6 +282,21 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
 		return $output;
 	}

+	/**
+	 * Returns the image size slug for the featured item image.
+	 *
+	 * @param array $attributes Block attributes. Default empty array.
+	 * @return string
+	 */
+	private function get_image_size( $attributes ) {
+		$image_size = 'large';
+		if ( 'none' !== $attributes['align'] || $attributes['height'] > 800 ) {
+			$image_size = 'full';
+		}
+
+		return $image_size;
+	}
+
 	/**
 	 * Returns the url the item's image
 	 *
@@ -277,10 +306,7 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
 	 * @return string
 	 */
 	private function get_image_url( $attributes, $item ) {
-		$image_size = 'large';
-		if ( 'none' !== $attributes['align'] || $attributes['height'] > 800 ) {
-			$image_size = 'full';
-		}
+		$image_size = $this->get_image_size( $attributes );

 		if ( $attributes['mediaId'] ) {
 			return wp_get_attachment_image_url( $attributes['mediaId'], $image_size );
@@ -292,13 +318,14 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
 	/**
 	 * Renders the featured image as a div background.
 	 *
-	 * @param array  $attributes Block attributes. Default empty array.
-	 * @param string $image_url  Item image url.
+	 * @param array                $attributes Block attributes. Default empty array.
+	 * @param \WC_Product|\WP_Term $item       Item object.
 	 *
 	 * @return string
 	 */
-	private function render_bg_image( $attributes, $image_url ) {
-		$styles = $this->get_bg_styles( $attributes, $image_url );
+	private function render_bg_image( $attributes, $item ) {
+		$image_url = $this->get_image_url( $attributes, $item );
+		$styles    = $this->get_bg_styles( $attributes, $image_url );

 		$classes = [ "wc-block-{$this->block_name}__background-image" ];

@@ -350,12 +377,11 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
 	 *
 	 * @param array                $attributes Block attributes. Default empty array.
 	 * @param \WC_Product|\WP_Term $item       Item object.
-	 * @param string               $image_url  Item image url.
 	 *
 	 * @return string
 	 */
-	private function render_image( $attributes, $item, string $image_url ) {
-		$style   = sprintf( 'object-fit: %s;', esc_attr( $attributes['imageFit'] ) );
+	private function render_image( $attributes, $item ) {
+		$style   = sprintf( 'object-fit: %s;', $attributes['imageFit'] );
 		$img_alt = $attributes['alt'] ?: $this->get_item_title( $item );

 		if ( $this->hasFocalPoint( $attributes ) ) {
@@ -366,6 +392,34 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
 			);
 		}

+		$image_size = $this->get_image_size( $attributes );
+
+		// When imageFit is not "cover", the image uses object-fit: none, so it is
+		// not scaled to the block — it renders at its natural pixel size. Before
+		// this block used responsive images, a single large URL was always loaded,
+		// which was typically bigger than the block and got clipped by overflow:
+		// hidden, making the block look filled. wp_get_attachment_image() picks a
+		// smaller srcset candidate on narrow viewports by default, leaving empty
+		// space around the image. Override sizes so the browser still selects the
+		// full image width and the previous appearance is preserved.
+		if ( 'cover' === $attributes['imageFit'] ) {
+			$image_id = empty( $attributes['mediaId'] ) ?
+				$this->get_item_image_id( $item ) :
+				absint( $attributes['mediaId'] );
+
+			if ( $image_id ) {
+				$attr = array(
+					'alt'   => $img_alt,
+					'class' => "wc-block-{$this->block_name}__background-image",
+					'style' => $style,
+				);
+
+				return wp_get_attachment_image( $image_id, $image_size, false, $attr );
+			}
+		}
+
+		$image_url = $this->get_item_image( $item, $image_size );
+
 		if ( ! empty( $image_url ) ) {
 			return sprintf(
 				'<img alt="%1$s" class="wc-block-%2$s__background-image" src="%3$s" style="%4$s" />',
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedProduct.php b/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedProduct.php
index dd2a4075183..ec91d0adc20 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedProduct.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/FeaturedProduct.php
@@ -42,6 +42,28 @@ class FeaturedProduct extends FeaturedItem {
 		return $product->get_title();
 	}

+	/**
+	 * Returns the featured product image attachment ID.
+	 *
+	 * @param \WC_Product $product Product object.
+	 * @return int
+	 */
+	protected function get_item_image_id( $product ) {
+		$image_id = $product->get_image_id();
+		if ( $image_id ) {
+			return (int) $image_id;
+		}
+
+		if ( $product->get_parent_id() ) {
+			$parent_product = wc_get_product( $product->get_parent_id() );
+			if ( $parent_product ) {
+				return (int) $parent_product->get_image_id();
+			}
+		}
+
+		return 0;
+	}
+
 	/**
 	 * Returns the featured product image URL.
 	 *
@@ -50,17 +72,13 @@ class FeaturedProduct extends FeaturedItem {
 	 * @return string
 	 */
 	protected function get_item_image( $product, $size = 'full' ) {
-		$image = '';
-		if ( $product->get_image_id() ) {
-			$image = wp_get_attachment_image_url( $product->get_image_id(), $size );
-		} elseif ( $product->get_parent_id() ) {
-			$parent_product = wc_get_product( $product->get_parent_id() );
-			if ( $parent_product ) {
-				$image = wp_get_attachment_image_url( $parent_product->get_image_id(), $size );
-			}
+		$image_id = $this->get_item_image_id( $product );
+
+		if ( $image_id ) {
+			return wp_get_attachment_image_url( $image_id, $size );
 		}

-		return $image;
+		return '';
 	}

 	/**