Commit 424b4c902fd for woocommerce
commit 424b4c902fd5a457be8ba6a99a37a0db37f9d778
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date: Fri Oct 9 19:38:46 2026 +0200
Product Image: constrain padding to inner blocks (#69329)
* Add changelog
* Product Image: constrain padding to inner blocks
* Use wp_style_engine_get_styles() instead of StyleAttributesUtils::get_classes_and_styles_by_attributes()
* Fix Product Badge when its not an inner block
* Expand tests
* Simplify selectors
* Prevent padding specific rules overriding the shorthand
* Linting
diff --git a/plugins/woocommerce/changelog/fix-69279-product-image-padding b/plugins/woocommerce/changelog/fix-69279-product-image-padding
new file mode 100644
index 00000000000..4e48228b6c5
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-69279-product-image-padding
@@ -0,0 +1,4 @@
+Significance: patch
+Type: update
+
+Product Image: constrain padding to inner blocks
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-elements-blocks/image/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/product-elements-blocks/image/block.json
index f7475a39103..31ae61b2642 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-elements-blocks/image/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-elements-blocks/image/block.json
@@ -32,7 +32,8 @@
},
"spacing": {
"margin": true,
- "padding": true
+ "padding": true,
+ "__experimentalSkipSerialization": [ "padding" ]
},
"dimensions": {
"aspectRatio": true,
@@ -41,7 +42,10 @@
"email": true
},
"selectors": {
- "root": ".wp-block-woocommerce-product-image.wc-block-components-product-image, .wp-block-woocommerce-product-image .wc-block-components-product-image"
+ "root": ".wp-block-woocommerce-product-image.wc-block-components-product-image, .wp-block-woocommerce-product-image .wc-block-components-product-image",
+ "spacing": {
+ "padding": ".wp-block-woocommerce-product-image .wc-block-components-product-image__inner-container"
+ }
},
"ancestor": [
"woocommerce/all-products",
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-elements-blocks/image/block.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-elements-blocks/image/block.tsx
index 1b81f87a5f8..f3b58b96862 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-elements-blocks/image/block.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-elements-blocks/image/block.tsx
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { Fragment } from '@wordpress/element';
+import { Fragment, cloneElement, isValidElement } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import clsx from 'clsx';
import { PLACEHOLDER_IMG_SRC, getSetting } from '@woocommerce/settings';
@@ -12,7 +12,7 @@ import {
import { useStyleProps } from '@woocommerce/base-hooks';
import { withProductDataContext } from '@woocommerce/shared-hocs';
import { useStoreEvents } from '@woocommerce/base-context/hooks';
-import type { HTMLAttributes } from 'react';
+import type { CSSProperties, HTMLAttributes } from 'react';
import { decodeEntities } from '@wordpress/html-entities';
import { isEmpty, ProductResponseItem } from '@woocommerce/types';
import { ProductEntityResponse } from '@woocommerce/entities';
@@ -35,6 +35,38 @@ const buildStyles = ( props: Partial< ImageProps > ) => {
};
};
+/**
+ * Unset sides are omitted on purpose: assigning them as undefined clears a
+ * shorthand `padding` value when React writes the style.
+ */
+const getDefinedPaddingStyles = ( {
+ padding,
+ paddingTop,
+ paddingRight,
+ paddingBottom,
+ paddingLeft,
+}: CSSProperties ): CSSProperties => {
+ const paddingStyles: CSSProperties = {};
+
+ if ( padding !== undefined ) {
+ paddingStyles.padding = padding;
+ }
+ if ( paddingTop !== undefined ) {
+ paddingStyles.paddingTop = paddingTop;
+ }
+ if ( paddingRight !== undefined ) {
+ paddingStyles.paddingRight = paddingRight;
+ }
+ if ( paddingBottom !== undefined ) {
+ paddingStyles.paddingBottom = paddingBottom;
+ }
+ if ( paddingLeft !== undefined ) {
+ paddingStyles.paddingLeft = paddingLeft;
+ }
+
+ return paddingStyles;
+};
+
const chooseImage = ( product: ProductResponseItem, imageId?: number ) => {
// Default to placeholder image if no product images are available.
if ( ! product.images.length ) {
@@ -170,6 +202,28 @@ export const Block = ( props: Props ): JSX.Element | null => {
} = props;
const styleProps = useStyleProps( props );
+ const {
+ padding,
+ paddingTop,
+ paddingRight,
+ paddingBottom,
+ paddingLeft,
+ ...imageStyle
+ } = styleProps.style;
+ const innerBlocks = isValidElement< { style?: CSSProperties } >( children )
+ ? cloneElement( children, {
+ style: {
+ ...children.props.style,
+ ...getDefinedPaddingStyles( {
+ padding,
+ paddingTop,
+ paddingRight,
+ paddingBottom,
+ paddingLeft,
+ } ),
+ },
+ } )
+ : children;
const { parentClassName } = useInnerBlockLayoutContext();
const { product, isLoading } = useProductDataContext( {
isAdmin,
@@ -213,11 +267,11 @@ export const Block = ( props: Props ): JSX.Element | null => {
},
styleProps.className
) }
- style={ styleProps.style }
+ style={ imageStyle }
>
<ImagePlaceholder style={ imageStyles } />
</div>
- { children }
+ { innerBlocks }
</>
);
}
@@ -258,7 +312,7 @@ export const Block = ( props: Props ): JSX.Element | null => {
},
styleProps.className
) }
- style={ styleProps.style }
+ style={ imageStyle }
>
{ /* For backwards compatibility in All Products blocks. */ }
{ displayLegacySaleBadge( props ) && (
@@ -281,7 +335,7 @@ export const Block = ( props: Props ): JSX.Element | null => {
/>
</ParentComponent>
</div>
- { children }
+ { innerBlocks }
</>
);
};
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-elements-blocks/image/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/product-elements-blocks/image/style.scss
index 63bbba957be..996ec3e5fa0 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-elements-blocks/image/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-elements-blocks/image/style.scss
@@ -26,15 +26,17 @@
}
}
- .wc-block-components-product-sale-badge {
- &--align-left {
+ // Legacy badge added by the Product Image block. This selectors don't
+ // target the Product Badge when used as an inner block.
+ :where(.wc-block-components-product-sale-badge:not(.wc-block-components-product-image__inner-container *)) {
+ &.wc-block-components-product-sale-badge--align-left {
position: absolute;
left: $gap-smaller * 0.5;
top: $gap-smaller * 0.5;
right: auto;
margin: 0;
}
- &--align-center {
+ &.wc-block-components-product-sale-badge--align-center {
position: absolute;
top: $gap-smaller * 0.5;
left: 50%;
@@ -42,7 +44,7 @@
transform: translateX(-50%);
margin: 0;
}
- &--align-right {
+ &.wc-block-components-product-sale-badge--align-right {
position: absolute;
right: $gap-smaller * 0.5;
top: $gap-smaller * 0.5;
@@ -65,7 +67,7 @@
&__inner-container {
display: flex;
flex-direction: column;
- padding: $gap-small;
+ padding: $gap-smaller * 0.5;
position: absolute;
bottom: 0;
left: 0;
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductImage.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductImage.php
index ed24b14043f..ec3c337aa1f 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductImage.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductImage.php
@@ -103,8 +103,17 @@ class ProductImage extends AbstractBlock {
$is_link = isset( $attributes['showProductLink'] ) ? $attributes['showProductLink'] : true;
+ $padding_styles = wp_style_engine_get_styles(
+ array(
+ 'spacing' => array(
+ 'padding' => $attributes['style']['spacing']['padding'] ?? null,
+ ),
+ )
+ );
+
$inner_blocks_container = sprintf(
- '<div class="wc-block-components-product-image__inner-container">%s</div>',
+ '<div class="wc-block-components-product-image__inner-container" style="%1$s">%2$s</div>',
+ esc_attr( isset( $padding_styles['css'] ) ? $padding_styles['css'] : '' ),
$inner_blocks_content
);
@@ -348,7 +357,7 @@ class ProductImage extends AbstractBlock {
*/
protected function render( $attributes, $content, $block ) {
$parsed_attributes = $this->parse_attributes( $attributes );
- $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'extra_classes' ) );
+ $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'extra_classes', 'padding' ) );
$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
$image_id = isset( $block->context['imageId'] ) ? (int) $block->context['imageId'] : null;
$product = wc_get_product( $post_id );
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductImage.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductImage.php
index 1b4fefddd9c..1ee078b9daf 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductImage.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductImage.php
@@ -320,13 +320,18 @@ class ProductImage extends \WP_UnitTestCase {
public function test_product_image_render_with_inner_blocks() {
$data = $this->create_product_with_image();
- $markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image --><div class="custom-inner-block">Custom content</div><!-- /wp:woocommerce/product-image --><!-- /wp:woocommerce/single-product -->' );
+ $markup = do_blocks( '<!-- wp:woocommerce/single-product {"productId":' . $data['product']->get_id() . '} --><!-- wp:woocommerce/product-image {"style":{"spacing":{"padding":{"top":"22px"}}}} --><div class="custom-inner-block">Custom content</div><!-- /wp:woocommerce/product-image --><!-- /wp:woocommerce/single-product -->' );
$this->assertStringContainsString( 'wc-block-components-product-image', $markup );
- $this->assertStringContainsString( 'wc-block-components-product-image__inner-container', $markup );
+ $this->assertStringContainsString( 'wc-block-components-product-image__inner-container" style="padding-top:22px;"', $markup );
$this->assertStringContainsString( 'custom-inner-block', $markup );
$this->assertStringContainsString( 'Custom content', $markup );
+ $markup_before_inner_container = strstr( $markup, 'wc-block-components-product-image__inner-container', true );
+
+ // Verify padding is not added to the wrapper.
+ $this->assertStringNotContainsString( 'padding-top:22px', $markup_before_inner_container );
+
// Clean up.
$data['product']->delete( true );
wp_delete_attachment( $data['image_id'], true );