Commit 8a72c04528c for woocommerce

commit 8a72c04528c5675a1333842d5ef933709fb4c90e
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date:   Tue Sep 8 10:08:48 2026 +0200

    Add support for typography and border radius styles to the Chips block (#68105)

    * Add changelog

    * Add support for typography and border radius styles to the Chips block

    * Update PHPStan

    * Add tests

    * Make sure swatches are not squared with border radius set to 0

    * Add padding support

    * Remove redundants CSS properties

    * Fix useMemo() dependencies

    * Reduce diff

    * Fix tests

    * Remove random widths

    * refactor: use style engine to parse block support

    * fix: only generate style when chip is not color swatches

    ---------

    Co-authored-by: Tung Du <dinhtungdu@gmail.com>

diff --git a/docs/block-development/reference/block-references.md b/docs/block-development/reference/block-references.md
index 2c2cfab7551..94524b66a08 100644
--- a/docs/block-development/reference/block-references.md
+++ b/docs/block-development/reference/block-references.md
@@ -1278,7 +1278,7 @@ Display filter options as chips.
 - **Name:** woocommerce/product-filter-chips
 - **Category:** woocommerce
 - **Ancestor:** woocommerce/product-filter-attribute, woocommerce/product-filter-taxonomy, woocommerce/product-filter-status, woocommerce/add-to-cart-with-options-variation-selector-attribute
-- **Supports:** interactivity, woocommerce (innerBlockDisplayStyle)
+- **Supports:** interactivity, spacing (padding), typography (fontSize), woocommerce (innerBlockDisplayStyle)
 - **Attributes:** chipBackground, chipBorder, chipText, customChipBackground, customChipBorder, customChipText, customSelectedChipBackground, customSelectedChipBorder, customSelectedChipText, selectedChipBackground, selectedChipBorder, selectedChipText

 ## Clear filters - woocommerce/product-filter-clear-button
diff --git a/plugins/woocommerce/changelog/add-filter-chips-styles b/plugins/woocommerce/changelog/add-filter-chips-styles
new file mode 100644
index 00000000000..f6d2a44f760
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-filter-chips-styles
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add support for typography, padding, and border radius styles to the Chips block
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json
index c3a446d6b44..a67f6c0d6f5 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json
@@ -17,6 +17,25 @@
 		"interactivity": true,
 		"woocommerce": {
 			"innerBlockDisplayStyle": true
+		},
+		"typography": {
+			"fontSize": true,
+			"__experimentalLetterSpacing": true,
+			"__experimentalTextTransform": true
+		},
+		"__experimentalBorder": {
+			"radius": true,
+			"__experimentalSkipSerialization": true
+		},
+		"spacing": {
+			"padding": true,
+			"__experimentalSkipSerialization": true
+		}
+	},
+	"selectors": {
+		"border": ".wp-block-woocommerce-product-filter-chips:not(.is-style-swatch) .wc-block-product-filter-chips__item",
+		"spacing": {
+			"padding": ".wp-block-woocommerce-product-filter-chips:not(.is-style-swatch) .wc-block-product-filter-chips__item"
 		}
 	},
 	"usesContext": [ "woocommerce/selectableItems", "woocommerce/attributeId" ],
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx
index f071dc82596..5a19a006e6f 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx
@@ -2,7 +2,6 @@
  * External dependencies
  */
 import { __ } from '@wordpress/i18n';
-import { useMemo } from '@wordpress/element';
 import { Disabled } from '@wordpress/components';
 import clsx from 'clsx';
 import { decodeHtmlEntities } from '@woocommerce/utils';
@@ -17,6 +16,12 @@ import {
 	// @ts-expect-error - no types.
 	// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
 	__experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients,
+	// @ts-expect-error - no types.
+	// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
+	__experimentalUseBorderProps as useBorderProps,
+	// @ts-expect-error - no types.
+	// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
+	__experimentalGetSpacingClassesAndStyles as useSpacingProps,
 } from '@wordpress/block-editor';

 /**
@@ -30,6 +35,17 @@ import {
 	isVisualAttributeTermEmpty,
 } from '../../../../base/utils/visual-attribute-terms';

+const LOADING_WIDTHS = [
+	'42%',
+	'67%',
+	'31%',
+	'55%',
+	'73%',
+	'28%',
+	'48%',
+	'61%',
+];
+
 const Edit = ( props: EditProps ): JSX.Element => {
 	const colorGradientSettings = useMultipleOriginColorsAndGradients();
 	const {
@@ -68,6 +84,8 @@ const Edit = ( props: EditProps ): JSX.Element => {
 		{}
 	);
 	const colorVars = getColorVars( attributes );
+	const borderProps = useBorderProps( attributes );
+	const spacingProps = useSpacingProps( attributes );

 	const blockProps = useBlockProps( {
 		className: clsx( 'wc-block-product-filter-chips', {
@@ -88,21 +106,6 @@ const Edit = ( props: EditProps ): JSX.Element => {
 		},
 	} );

-	const loadingState = useMemo( () => {
-		return [ ...Array( 10 ) ].map( ( _, i ) => (
-			<div
-				className="wc-block-product-filter-chips__item"
-				key={ i }
-				style={ {
-					/* stylelint-disable */
-					width: Math.floor( Math.random() * ( 100 - 25 ) ) + '%',
-				} }
-			>
-				&nbsp;
-			</div>
-		) );
-	}, [] );
-
 	if ( ! items ) {
 		return <></>;
 	}
@@ -110,6 +113,28 @@ const Edit = ( props: EditProps ): JSX.Element => {
 	const threshold = 15;
 	const isLongList = items.length > threshold;

+	const chipItemClassName = clsx(
+		'wc-block-product-filter-chips__item',
+		! hasVisualSwatches && borderProps.className,
+		! hasVisualSwatches && spacingProps.className
+	);
+	const chipItemStyle = hasVisualSwatches
+		? undefined
+		: { ...borderProps.style, ...spacingProps.style };
+	const loadingState = LOADING_WIDTHS.map( ( width, i ) => (
+		<div
+			className={ chipItemClassName }
+			key={ i }
+			style={ {
+				...chipItemStyle,
+				/* stylelint-disable */
+				width,
+			} }
+		>
+			&nbsp;
+		</div>
+	) );
+
 	return (
 		<>
 			<div { ...blockProps }>
@@ -123,7 +148,8 @@ const Edit = ( props: EditProps ): JSX.Element => {
 							).map( ( item, index ) => (
 								<div
 									key={ index }
-									className="wc-block-product-filter-chips__item"
+									className={ chipItemClassName }
+									style={ chipItemStyle }
 									aria-checked={ !! item.selected }
 								>
 									<span className="wc-block-product-filter-chips__label">
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss
index cdd0d1a403f..c85cd2578a3 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss
@@ -1,3 +1,7 @@
+:where(.wp-block-woocommerce-product-filter-chips) {
+	font-size: 0.8em;
+}
+
 :where(.wc-block-product-filter-chips__fieldset) {
 	display: contents;
 }
@@ -14,8 +18,10 @@
 	appearance: none;
 	background: transparent;
 	border-radius: 2px;
-	font-size: 0.8em;
 	font-family: inherit;
+	font-size: inherit;
+	letter-spacing: inherit;
+	text-transform: inherit;
 	cursor: pointer;
 	color: inherit;

@@ -94,7 +100,6 @@
 	border: 0;
 	padding: 0;
 	font: inherit;
-	font-size: 0.875em;
 	color: inherit;
 	text-decoration: underline;
 	cursor: pointer;
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 6df50b7b695..e515c8ab6cf 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -51309,18 +51309,6 @@ parameters:
 			count: 1
 			path: src/Blocks/BlockTypes/ProductFilterChips.php

-		-
-			message: '#^Parameter \#1 \$text of function esc_attr expects string, string\|true given\.$#'
-			identifier: argument.type
-			count: 1
-			path: src/Blocks/BlockTypes/ProductFilterChips.php
-
-		-
-			message: '#^Parameter \#1 \$text of function esc_attr expects string, string\|true\|null given\.$#'
-			identifier: argument.type
-			count: 1
-			path: src/Blocks/BlockTypes/ProductFilterChips.php
-
 		-
 			message: '#^Access to property \$context on an unknown class Automattic\\WooCommerce\\Blocks\\BlockTypes\\WP_Block\.$#'
 			identifier: class.notFound
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterChips.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterChips.php
index c45119fa9b9..eff9bfa08cb 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterChips.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterChips.php
@@ -52,15 +52,35 @@ final class ProductFilterChips extends AbstractBlock {
 		$items           = is_array( $block_context['items'] ?? null ) ? $block_context['items'] : array();
 		$store_namespace = $block_context['storeNamespace'] ?? 'woocommerce/product-filters';
 		$display_limit   = 'woocommerce/product-filters' === $store_namespace ? 15 : 30;
-		$classes         = '';
-		$style           = '';

-		$tags = new \WP_HTML_Tag_Processor( $content );
+		$classes = '';
+		$style   = '';
+		$tags    = new \WP_HTML_Tag_Processor( $content );
 		if ( $tags->next_tag( array( 'class_name' => 'wc-block-product-filter-chips' ) ) ) {
-			$classes = $tags->get_attribute( 'class' );
-			$style   = $tags->get_attribute( 'style' );
+			$saved_classes = $tags->get_attribute( 'class' );
+			$saved_style   = $tags->get_attribute( 'style' );
+			$classes       = is_string( $saved_classes ) ? $saved_classes : '';
+			$style         = is_string( $saved_style ) ? $saved_style : '';
 		}

+		$has_visual_swatches     = self::has_visual_swatches( $items );
+		$item_classes_and_styles = wp_parse_args(
+			$has_visual_swatches ? array() : wp_style_engine_get_styles(
+				array(
+					'border'  => array(
+						'radius' => $attributes['style']['border']['radius'] ?? null,
+					),
+					'spacing' => array(
+						'padding' => $attributes['style']['spacing']['padding'] ?? null,
+					),
+				)
+			),
+			array(
+				'css'        => '',
+				'classnames' => '',
+			)
+		);
+
 		$wrapper_attributes = array(
 			'data-wp-interactive'  => 'woocommerce/product-filter-chips',
 			'data-wp-init--colors' => 'callbacks.initColors',
@@ -94,12 +114,11 @@ final class ProductFilterChips extends AbstractBlock {
 		$visible_items           = array_merge( $first_items, $overflow_selected_items );
 		$hidden_count            = count( $items ) - count( $visible_items );

-		$first_item          = reset( $items );
-		$show_counts         = is_array( $first_item ) && array_key_exists( 'count', $first_item );
-		$has_visual_swatches = self::has_visual_swatches( $items );
-		$button_role         = 'single' === $block_context['selectionMode'] ? 'radio' : 'checkbox';
+		$first_item  = reset( $items );
+		$show_counts = is_array( $first_item ) && array_key_exists( 'count', $first_item );
+		$button_role = 'single' === $block_context['selectionMode'] ? 'radio' : 'checkbox';

-		if ( $has_visual_swatches && is_string( $classes ) && ! str_contains( $classes, 'is-style-swatch' ) ) {
+		if ( $has_visual_swatches && ! str_contains( $classes, 'is-style-swatch' ) ) {
 			$classes                    .= ' is-style-swatch';
 			$wrapper_attributes['class'] = esc_attr( $classes );
 		}
@@ -116,7 +135,10 @@ final class ProductFilterChips extends AbstractBlock {
 					foreach ( $visible_items as $item ) :
 						?>
 						<button
-							class="wc-block-product-filter-chips__item"
+							class="wc-block-product-filter-chips__item <?php echo esc_attr( $item_classes_and_styles['classnames'] ); ?>"
+							<?php if ( $item_classes_and_styles['css'] ) : ?>
+								style="<?php echo esc_attr( $item_classes_and_styles['css'] ); ?>"
+							<?php endif; ?>
 							type="button"
 							role="<?php echo esc_attr( $button_role ); ?>"
 							id="<?php echo esc_attr( $item['id'] ); ?>"
@@ -166,7 +188,10 @@ final class ProductFilterChips extends AbstractBlock {
 						data-wp-each-key="context.item.id"
 					>
 						<button
-							class="wc-block-product-filter-chips__item"
+							class="wc-block-product-filter-chips__item <?php echo esc_attr( $item_classes_and_styles['classnames'] ); ?>"
+							<?php if ( $item_classes_and_styles['css'] ) : ?>
+								style="<?php echo esc_attr( $item_classes_and_styles['css'] ); ?>"
+							<?php endif; ?>
 							type="button"
 							role="<?php echo esc_attr( $button_role ); ?>"
 							data-wp-bind--id="context.item.id"
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilterChipsTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilterChipsTest.php
new file mode 100644
index 00000000000..196bf2ab516
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilterChipsTest.php
@@ -0,0 +1,130 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
+
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the Product Filter Chips block type.
+ */
+class ProductFilterChipsTest extends WC_Unit_Test_Case {
+
+	/**
+	 * @testdox Typography is applied to the wrapper and border radius to chip items.
+	 * @covers \Automattic\WooCommerce\Blocks\BlockTypes\ProductFilterChips::render
+	 */
+	public function test_renders_typography_on_wrapper_and_border_radius_on_items(): void {
+		$markup = $this->render_chips(
+			array(
+				'style' => array(
+					'border'     => array(
+						'radius' => '12px',
+					),
+					'typography' => array(
+						'textTransform' => 'uppercase',
+					),
+				),
+			),
+			array(
+				array(
+					'id'       => 'item-red',
+					'label'    => 'Red',
+					'value'    => 'red',
+					'selected' => false,
+				),
+			)
+		);
+
+		$wrapper_style = $this->get_style( $markup, 'wc-block-product-filter-chips' );
+		$item_style    = $this->get_style( $markup, 'wc-block-product-filter-chips__item' );
+
+		$this->assertStringContainsString( 'text-transform:uppercase', $wrapper_style, 'Wrapper should have text-transform: uppercase.' );
+		$this->assertStringContainsString( 'border-radius:12px', $item_style, 'Item should have border-radius: 12px.' );
+		$this->assertStringNotContainsString( 'border-radius', $wrapper_style, 'Wrapper should not have border-radius.' );
+	}
+
+	/**
+	 * @testdox Padding and border radius are not applied to visual swatch chip items.
+	 * @covers \Automattic\WooCommerce\Blocks\BlockTypes\ProductFilterChips::render
+	 */
+	public function test_does_not_apply_padding_or_border_radius_to_swatch_items(): void {
+		$markup = $this->render_chips(
+			array(
+				'style' => array(
+					'border'  => array(
+						'radius' => '0px',
+					),
+					'spacing' => array(
+						'padding' => array(
+							'top'    => '8px',
+							'right'  => '16px',
+							'bottom' => '8px',
+							'left'   => '16px',
+						),
+					),
+				),
+			),
+			array(
+				array(
+					'id'       => 'item-red',
+					'label'    => 'Red',
+					'value'    => 'red',
+					'selected' => false,
+					'visual'   => array(
+						'type'  => 'color',
+						'color' => '#ff0000',
+					),
+				),
+			)
+		);
+
+		$item_style = $this->get_style( $markup, 'wc-block-product-filter-chips__item' );
+
+		$this->assertStringContainsString( 'is-style-swatch', $markup, 'Visual items should use the swatch style.' );
+		$this->assertStringNotContainsString( 'border-radius', $item_style, 'Swatch items should not get an inline border radius.' );
+		$this->assertStringNotContainsString( 'padding-top', $item_style, 'Swatch items should not get inline padding.' );
+	}
+
+	/**
+	 * Render the Chips block with the given attributes.
+	 *
+	 * @param array $attributes Block attributes.
+	 * @param array $items      Selectable items.
+	 * @return string Rendered markup.
+	 */
+	private function render_chips( array $attributes, array $items ): string {
+		$block = new \WP_Block(
+			array(
+				'blockName'    => 'woocommerce/product-filter-chips',
+				'attrs'        => array_merge( $attributes, array( 'className' => 'wc-block-product-filter-chips' ) ),
+				'innerContent' => array(),
+			),
+			array(
+				'woocommerce/selectableItems' => array(
+					'items'          => $items,
+					'selectionMode'  => 'multiple',
+					'storeNamespace' => 'woocommerce/product-filters',
+				),
+			)
+		);
+
+		return $block->render();
+	}
+
+	/**
+	 * Get the style attribute of the first element with the given class.
+	 *
+	 * @param string $markup     Rendered markup.
+	 * @param string $class_name Class name to find.
+	 * @return string
+	 */
+	private function get_style( string $markup, string $class_name ): string {
+		$processor = new \WP_HTML_Tag_Processor( $markup );
+		$this->assertTrue( $processor->next_tag( array( 'class_name' => $class_name ) ), 'Should find the first element with the given class name.' );
+
+		$style = $processor->get_attribute( 'style' );
+
+		return is_string( $style ) ? $style : '';
+	}
+}