Commit 4a9ee9b5416 for woocommerce
commit 4a9ee9b5416304ca81ebad3e92ad0655a05cb985
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date: Thu May 21 15:39:39 2026 +0200
Update Variation Selector block so it displays swatches for wc-visual attributes (#65180)
* Update Variation Selector block so it displays swatches for wc-visual attributes
* Add changelog
* Make default term colors default setting an empty object
* Minor fixes
* Clean up
* Don't save empty colors unnecessarily
diff --git a/plugins/woocommerce/changelog/update-variation-selector-color-swatches b/plugins/woocommerce/changelog/update-variation-selector-color-swatches
new file mode 100644
index 00000000000..3259db137cd
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-variation-selector-color-swatches
@@ -0,0 +1,5 @@
+Significance: patch
+Type: update
+Comment: Update Variation Selector block so it displays swatches for wc-visual attributes
+
+
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/constants.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/constants.ts
index c643193027a..20917f21a71 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/constants.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/constants.ts
@@ -48,9 +48,9 @@ export const DEFAULT_ATTRIBUTES = [
name: __( 'Color', 'woocommerce' ),
has_variations: true,
terms: [
- { id: 1, slug: 'blue', name: __( 'Blue', 'woocommerce' ) },
- { id: 2, slug: 'red', name: __( 'Red', 'woocommerce' ) },
- { id: 3, slug: 'green', name: __( 'Green', 'woocommerce' ) },
+ { id: -1, slug: 'blue', name: __( 'Blue', 'woocommerce' ) },
+ { id: -2, slug: 'red', name: __( 'Red', 'woocommerce' ) },
+ { id: -3, slug: 'green', name: __( 'Green', 'woocommerce' ) },
],
},
{
@@ -65,3 +65,9 @@ export const DEFAULT_ATTRIBUTES = [
],
},
] as const;
+
+export const EMPTY_TERM_COLORS: Record< string, string > = {
+ '-1': '#0000ff',
+ '-2': '#e10000',
+ '-3': '#009b00',
+};
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/edit.tsx
index 17bed069750..d442058dd77 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute/edit.tsx
@@ -20,6 +20,7 @@ import {
import { isProductResponseItem } from '@woocommerce/entities';
import type { ProductResponseAttributeItem } from '@woocommerce/types';
import { __ } from '@wordpress/i18n';
+import { getSetting } from '@woocommerce/settings';
import {
ToggleControl,
__experimentalToggleGroupControl as ToggleGroupControl,
@@ -31,12 +32,15 @@ import {
/**
* Internal dependencies
*/
-import { DEFAULT_ATTRIBUTES } from './constants';
+import { DEFAULT_ATTRIBUTES, EMPTY_TERM_COLORS } from './constants';
import {
DisplayStyleSwitcher,
resetDisplayStyleBlock,
} from '../../../product-filters/components/display-style-switcher';
-import type { SelectableItemsContext } from '../../../../types/type-defs/selectable-items';
+import type {
+ SelectableItem,
+ SelectableItemsContext,
+} from '../../../../types/type-defs/selectable-items';
const INNER_CHIPS = 'woocommerce/product-filter-chips';
@@ -57,19 +61,36 @@ function AttributeItem( { blocks, isSelected, onSelect }: AttributeItemProps ) {
const { data: attribute } =
useCustomDataContext< ProductResponseAttributeItem >( 'attribute' );
+ const termColors = getSetting< Record< string, string > >(
+ 'variationSelectorTermColors',
+ {} as Record< string, string >
+ );
+
const selectableContext = useMemo( () => {
- let items = [];
+ let items: SelectableItem< {
+ label: string;
+ ariaLabel: string;
+ } >[] = [];
if (
attribute &&
Array.isArray( attribute?.terms ) &&
attribute.terms.length > 0
) {
- items = attribute.terms.map( ( term ) => ( {
- id: `${ attribute.taxonomy }-${ term.slug }`,
- label: term.name,
- value: term.slug,
- ariaLabel: term.name,
- } ) );
+ items = attribute.terms.map( ( term ) => {
+ let color: string | null = null;
+ if ( term.id in termColors ) {
+ color = termColors[ term.id ];
+ } else if ( term.id in EMPTY_TERM_COLORS ) {
+ color = EMPTY_TERM_COLORS[ term.id ];
+ }
+ return {
+ id: `${ attribute.taxonomy }-${ term.slug }`,
+ label: term.name,
+ value: term.slug,
+ ariaLabel: term.name,
+ ...( color !== null ? { color } : {} ),
+ };
+ } );
}
return {
@@ -81,7 +102,7 @@ function AttributeItem( { blocks, isSelected, onSelect }: AttributeItemProps ) {
label: string;
ariaLabel: string;
} >;
- }, [ attribute ] );
+ }, [ attribute, termColors ] );
const blockPreviewProps = useBlockPreview( {
blocks,
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/frontend.ts
index 2037075c6f7..e187dfa1982 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/frontend.ts
@@ -31,6 +31,7 @@ type VariationOptionItem = {
label: string;
value: string;
ariaLabel?: string;
+ color?: string;
};
type Context = AddToCartWithOptionsStoreContext & {
@@ -248,6 +249,7 @@ const { actions, state } = store< VariableProductAddToCartWithOptionsStore >(
selected,
disabled,
hidden: hideInvalid && disabled,
+ ...( row.color !== undefined && { color: row.color } ),
};
} );
},
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttribute.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttribute.php
index 819dce6a4a9..d44bfefc528 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttribute.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttribute.php
@@ -21,6 +21,60 @@ class VariationSelectorAttribute extends AbstractBlock {
*/
protected $block_name = 'add-to-cart-with-options-variation-selector-attribute';
+ /**
+ * Extra data passed through from server to client for block.
+ *
+ * @param array $attributes Any attributes that currently are available from the block.
+ * @return void
+ */
+ protected function enqueue_data( array $attributes = array() ) {
+ parent::enqueue_data( $attributes );
+
+ if ( is_admin() ) {
+ $this->asset_data_registry->add( 'variationSelectorTermColors', $this->get_visual_attribute_term_colors() );
+ }
+ }
+
+ /**
+ * Get color values for all wc-visual attribute terms.
+ *
+ * @param string|null $attribute_name Optional product attribute taxonomy name (e.g. `pa_color`). When omitted, colors for every wc-visual attribute are loaded.
+ * @return array<int, string|null> Map of term ID to hex color.
+ */
+ private function get_visual_attribute_term_colors( ?string $attribute_name = null ): array {
+ $colors = array();
+ $attributes = wc_get_attribute_taxonomies();
+
+ foreach ( $attributes as $attribute ) {
+ if ( 'wc-visual' !== $attribute->attribute_type ) {
+ continue;
+ }
+ if ( $attribute_name && 'pa_' . $attribute->attribute_name !== $attribute_name ) {
+ continue;
+ }
+
+ $terms = get_terms(
+ array(
+ 'taxonomy' => 'pa_' . $attribute->attribute_name,
+ 'hide_empty' => false,
+ )
+ );
+
+ if ( is_wp_error( $terms ) ) {
+ continue;
+ }
+
+ foreach ( $terms as $term ) {
+ $color = sanitize_hex_color( get_term_meta( $term->term_id, 'color', true ) );
+ if ( ! empty( $color ) ) {
+ $colors[ $term->term_id ] = $color;
+ }
+ }
+ }
+
+ return $colors;
+ }
+
/**
* Render the block.
*
@@ -77,7 +131,7 @@ class VariationSelectorAttribute extends AbstractBlock {
}
$default_selected = $this->get_default_selected_attribute( $attribute_slug, $attribute_terms );
- $variation_items = $this->build_variation_selectable_items( $attribute_slug, $attribute_terms, $default_selected );
+ $variation_items = $this->build_variation_selectable_items( $attribute_name, $attribute_slug, $attribute_terms, $default_selected );
$attribute_label = wc_attribute_label( $attribute_name );
$attribute_id = 'wc_product_attribute_' . uniqid();
$context = array(
@@ -247,28 +301,36 @@ class VariationSelectorAttribute extends AbstractBlock {
/**
* Build selectable items for the inner block protocol and client context.
*
+ * @param string $attribute_name Product attribute name.
* @param string $attribute_slug Attribute slug.
* @param array $attribute_terms Terms from context.
* @param string|null $default_selected Default selected attribute value.
* @return array
*/
- private function build_variation_selectable_items( string $attribute_slug, array $attribute_terms, ?string $default_selected ): array {
- $id_prefix = sanitize_title( $attribute_slug );
- $items = array();
+ private function build_variation_selectable_items( string $attribute_name, string $attribute_slug, array $attribute_terms, ?string $default_selected ): array {
+ $id_prefix = sanitize_title( $attribute_slug );
+ $items = array();
+ $term_colors = $this->get_visual_attribute_term_colors( $attribute_name );
foreach ( $attribute_terms as $attribute_term ) {
if ( ! is_array( $attribute_term ) || ! isset( $attribute_term['value'], $attribute_term['label'] ) ) {
continue;
}
- $value = (string) $attribute_term['value'];
- $slug = sanitize_title( $value );
- $items[] = array(
+ $value = (string) $attribute_term['value'];
+ $slug = sanitize_title( $value );
+ $item = array(
'id' => $id_prefix . '-' . $slug,
'label' => (string) $attribute_term['label'],
'value' => $value,
'ariaLabel' => (string) $attribute_term['label'],
'selected' => $default_selected === $value,
);
+
+ if ( ! empty( $term_colors ) && isset( $attribute_term['term_id'], $term_colors[ $attribute_term['term_id'] ] ) ) {
+ $item['color'] = $term_colors[ $attribute_term['term_id'] ];
+ }
+
+ $items[] = $item;
}
return $items;
@@ -296,7 +358,7 @@ class VariationSelectorAttribute extends AbstractBlock {
return array();
}
- return array(
+ $option = array(
'value' => $value,
/**
* Filter the variation option name.
@@ -317,5 +379,11 @@ class VariationSelectorAttribute extends AbstractBlock {
),
'isSelected' => $selected_attribute === $value,
);
+
+ if ( $term instanceof \WP_Term ) {
+ $option['term_id'] = $term->term_id;
+ }
+
+ return $option;
}
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/VariationSelectorAttribute.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/VariationSelectorAttribute.php
index 3e2885b9068..51dafe8b1aa 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/VariationSelectorAttribute.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/VariationSelectorAttribute.php
@@ -146,6 +146,94 @@ class VariationSelectorAttribute extends WC_Unit_Test_Case {
$this->assertStringContainsString( '"disabledAttributesAction":"hide"', $markup, 'Legacy disabledAttributesAction should be applied to the interactivity context.' );
}
+ /**
+ * Tests that wc-visual attribute terms render chips with swatch markup and classes.
+ *
+ * @testdox VariationSelectorAttribute renders wc-visual attribute options with swatch classes and colors.
+ * @covers \Automattic\WooCommerce\Blocks\BlockTypes\AddToCartWithOptions\VariationSelectorAttribute::build_variation_selectable_items
+ */
+ public function test_renders_wc_visual_attribute_with_swatch_classes(): void {
+ global $wpdb;
+
+ $fixtures = new FixtureData();
+ $attribute = FixtureData::get_product_attribute(
+ 'vswatch',
+ array( 'Tone A', 'Tone B' )
+ );
+ $attribute_id = $attribute['attribute_id'];
+ $taxonomy = $attribute['attribute_taxonomy'];
+ $term_ids = $attribute['term_ids'];
+ $term_a = get_term( $term_ids[0] );
+ $term_b = get_term( $term_ids[1] );
+ $this->assertInstanceOf( \WP_Term::class, $term_a );
+ $this->assertInstanceOf( \WP_Term::class, $term_b );
+
+ $wpdb->update(
+ $wpdb->prefix . 'woocommerce_attribute_taxonomies',
+ array( 'attribute_type' => 'wc-visual' ),
+ array( 'attribute_id' => $attribute_id ),
+ array( '%s' ),
+ array( '%d' )
+ );
+ delete_transient( 'wc_attribute_taxonomies' );
+ \WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );
+
+ update_term_meta( $term_a->term_id, 'color', '#aa0000' );
+ update_term_meta( $term_b->term_id, 'color', '#0000aa' );
+
+ try {
+ $variable_product = $fixtures->get_variable_product(
+ array(),
+ array( $attribute )
+ );
+
+ $product_id = $variable_product->get_id();
+
+ $fixtures->get_variation_product(
+ $product_id,
+ array( $taxonomy => $term_a->slug ),
+ array(
+ 'regular_price' => 10,
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ )
+ );
+
+ $fixtures->get_variation_product(
+ $product_id,
+ array( $taxonomy => $term_b->slug ),
+ array(
+ 'regular_price' => 12,
+ 'stock_status' => ProductStockStatus::IN_STOCK,
+ )
+ );
+
+ \WC_Product_Variable::sync( $product_id );
+
+ $variable_product = wc_get_product( $product_id );
+ $this->assertInstanceOf( \WC_Product_Variable::class, $variable_product );
+
+ $inner_blocks = $this->get_attribute_name_block_markup() . $this->get_chips_block_markup();
+ $markup = $this->render_variation_selector_attribute( $variable_product, $inner_blocks );
+
+ $this->assertStringContainsString( 'is-style-swatch', $markup, 'Chips wrapper should use swatch style when colors are present.' );
+ $this->assertStringContainsString( 'wc-block-product-filter-chips__swatch', $markup, 'Swatch elements should be rendered for wc-visual terms.' );
+ $this->assertStringContainsString( 'background-color: #aa0000', $markup, 'First term swatch should use its term color meta.' );
+ $this->assertStringContainsString( 'background-color: #0000aa', $markup, 'Second term swatch should use its term color meta.' );
+ } finally {
+ delete_term_meta( $term_a->term_id, 'color' );
+ delete_term_meta( $term_b->term_id, 'color' );
+ $wpdb->update(
+ $wpdb->prefix . 'woocommerce_attribute_taxonomies',
+ array( 'attribute_type' => 'select' ),
+ array( 'attribute_id' => $attribute_id ),
+ array( '%s' ),
+ array( '%d' )
+ );
+ delete_transient( 'wc_attribute_taxonomies' );
+ \WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );
+ }
+ }
+
/**
* Tests that legacy Attribute Options blocks nested in a group are replaced when rendered.
*
@@ -249,6 +337,15 @@ class VariationSelectorAttribute extends WC_Unit_Test_Case {
return '<!-- wp:woocommerce/add-to-cart-with-options-variation-selector-attribute-name /-->';
}
+ /**
+ * Block markup for the Chips inner block (variation selector default).
+ *
+ * @return string
+ */
+ private function get_chips_block_markup(): string {
+ return '<!-- wp:woocommerce/product-filter-chips /-->';
+ }
+
/**
* Get block markup for the legacy Attribute Options inner block.
*