Commit d92a5402862 for woocommerce

commit d92a5402862147ff4d14573efde2eaa25f9a805f
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date:   Tue May 5 09:14:13 2026 +0200

    Add color input to 'Create value' modal (#64493)

    * Add color input to 'Create value' modal

    * Add changelog

    * Add spacing between inputs

    * Remove unnecessary phpcs:ignore comment

    * Test code cleanup

    * Linting

    * Test code cleanup (II)

    * Create wc_taxonomy_is_attribute_type() util

    * Add strict types declaration to wc-attribute-functions-test.php

    * Revert "Create wc_taxonomy_is_attribute_type() util"

    This reverts commit 5a4f6a0a8c489efb64598b03fcdbcb47183835e3.

diff --git a/plugins/woocommerce/changelog/fix-add-color-input-to-create-value b/plugins/woocommerce/changelog/fix-add-color-input-to-create-value
new file mode 100644
index 00000000000..e267b30e511
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-add-color-input-to-create-value
@@ -0,0 +1,5 @@
+Significance: patch
+Type: update
+Comment: Add color input to 'Create value' modal
+
+
diff --git a/plugins/woocommerce/client/legacy/css/admin.scss b/plugins/woocommerce/client/legacy/css/admin.scss
index c64db9ea5d9..e1e3aee2e6a 100644
--- a/plugins/woocommerce/client/legacy/css/admin.scss
+++ b/plugins/woocommerce/client/legacy/css/admin.scss
@@ -992,6 +992,11 @@ $font-sf-pro-display: "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe U
 		padding-bottom: 2em;
 	}

+	label:not(:first-child) {
+		display: block;
+		margin-top: 1.5em;
+	}
+
 	input[type="text"] {
 		display: block;
 		font-size: 13px;
@@ -1000,6 +1005,12 @@ $font-sf-pro-display: "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe U
 		padding: 12px;
 		width: 100%;
 	}
+
+	input[type="color"] {
+		display: block;
+		margin: 6px 0;
+		width: 100%;
+	}
 }

 /**
diff --git a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js
index 4ec93fa91c4..08fd9d47c38 100644
--- a/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js
+++ b/plugins/woocommerce/client/legacy/js/admin/meta-boxes-product.js
@@ -992,6 +992,14 @@ jQuery( function ( $ ) {
 				security: woocommerce_admin_meta_boxes.add_attribute_nonce,
 			};

+			if (
+				currentAttributeTermCreationContext.isVisualAttribute &&
+				postedData &&
+				postedData.term_color
+			) {
+				data.term_color = postedData.term_color;
+			}
+
 			$.post(
 				woocommerce_admin_meta_boxes.ajax_url,
 				data,
@@ -1044,14 +1052,20 @@ jQuery( function ( $ ) {

 			const wrapper = this.closest( '.woocommerce_attribute' );
 			const attribute = wrapper ? wrapper.dataset.taxonomy : '';
+			const isVisualAttribute =
+				this.dataset.isVisualAttribute === 'yes';

 			currentAttributeTermCreationContext = {
 				wrapper,
 				attribute,
+				isVisualAttribute,
 			};

 			$( this ).WCBackboneModal( {
 				template: 'wc-modal-add-attribute-term',
+				variable: {
+					isVisualAttribute,
+				},
 			} );
 		}
 	);
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-taxonomies.php b/plugins/woocommerce/includes/admin/class-wc-admin-taxonomies.php
index 1ccbce52456..71da485fca6 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-taxonomies.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-taxonomies.php
@@ -324,15 +324,14 @@ class WC_Admin_Taxonomies {
 			return false;
 		}

-		$attribute_slug = wc_attribute_taxonomy_slug( $taxonomy );
-
-		foreach ( wc_get_attribute_taxonomies() as $attribute_taxonomy ) {
-			if ( $attribute_slug === $attribute_taxonomy->attribute_name ) {
-				return 'wc-visual' === $attribute_taxonomy->attribute_type;
-			}
+		if ( ! array_key_exists( 'wc-visual', wc_get_attribute_types() ) ) {
+			return false;
 		}

-		return false;
+		$attribute_id = wc_attribute_taxonomy_id_by_name( $taxonomy );
+		$attribute    = $attribute_id ? wc_get_attribute( $attribute_id ) : null;
+
+		return $attribute && 'wc-visual' === $attribute->type;
 	}

 	/**
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php
index 0b9fca21e01..163d8921209 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-attribute-inner.php
@@ -74,7 +74,7 @@ if ( ! defined( 'ABSPATH' ) ) {
 					</select>
 					<button class="button plus select_all_attributes"><?php esc_html_e( 'Select all', 'woocommerce' ); ?></button>
 					<button class="button minus select_no_attributes"><?php esc_html_e( 'Select none', 'woocommerce' ); ?></button>
-					<button class="button fr plus add_new_attribute"><?php esc_html_e( 'Create value', 'woocommerce' ); ?></button>
+					<button class="button fr plus add_new_attribute" data-is-visual-attribute="<?php echo esc_attr( wc_bool_to_string( 'wc-visual' === $attribute_taxonomy->attribute_type ) ); ?>"><?php esc_html_e( 'Create value', 'woocommerce' ); ?></button>
 					<?php
 				}

diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-attributes.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-attributes.php
index cf948ccaddb..1c1300ede84 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-attributes.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-attributes.php
@@ -77,6 +77,10 @@ $product_attributes = $product_object->get_attributes( 'edit' );
 					<form class="wc-add-attribute-term-fields" action="" method="post">
 						<label for="wc-modal-add-attribute-term-input"><?php esc_html_e( 'Name', 'woocommerce' ); ?></label>
 						<input id="wc-modal-add-attribute-term-input" type="text" name="term" value="" />
+						<# if ( data.isVisualAttribute ) { #>
+							<label for="wc-modal-add-attribute-term-color"><?php esc_html_e( 'Color value', 'woocommerce' ); ?></label>
+							<input id="wc-modal-add-attribute-term-color" type="color" name="term_color" value="" />
+						<# } #>
 					</form>
 				</article>
 				<footer>
diff --git a/plugins/woocommerce/includes/class-wc-ajax.php b/plugins/woocommerce/includes/class-wc-ajax.php
index 55369cc6c64..fe87de8f76d 100644
--- a/plugins/woocommerce/includes/class-wc-ajax.php
+++ b/plugins/woocommerce/includes/class-wc-ajax.php
@@ -740,6 +740,29 @@ class WC_AJAX {
 		wp_die();
 	}

+	/**
+	 * Check if a product attribute taxonomy supports visual term colors.
+	 *
+	 * @param string $taxonomy Taxonomy slug.
+	 * @return bool
+	 *
+	 * @internal
+	 */
+	private static function is_visual_product_attribute_taxonomy( $taxonomy ) {
+		if ( ! taxonomy_exists( $taxonomy ) || ! taxonomy_is_product_attribute( $taxonomy ) ) {
+			return false;
+		}
+
+		if ( ! array_key_exists( 'wc-visual', wc_get_attribute_types() ) ) {
+			return false;
+		}
+
+		$attribute_id = wc_attribute_taxonomy_id_by_name( $taxonomy );
+		$attribute    = $attribute_id ? wc_get_attribute( $attribute_id ) : null;
+
+		return $attribute && 'wc-visual' === $attribute->type;
+	}
+
 	/**
 	 * Add a new attribute via ajax function.
 	 *
@@ -763,6 +786,14 @@ class WC_AJAX {
 						)
 					);
 				} else {
+					if ( self::is_visual_product_attribute_taxonomy( $taxonomy ) && isset( $_POST['term_color'] ) ) {
+						$color_value = sanitize_hex_color( wp_unslash( $_POST['term_color'] ) );
+
+						if ( $color_value ) {
+							update_term_meta( $result['term_id'], 'color', $color_value );
+						}
+					}
+
 					$term = get_term_by( 'id', $result['term_id'], $taxonomy );
 					wp_send_json(
 						array(
@@ -771,9 +802,9 @@ class WC_AJAX {
 							'slug'    => $term->slug,
 						)
 					);
-				}
-			}
-		}
+				}//end if
+			}//end if
+		}//end if
 		wp_die( -1 );
 	}

diff --git a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
index 9372fb92be5..9240b998ca2 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
@@ -130,6 +130,135 @@ class WC_AJAX_Test extends \WP_Ajax_UnitTestCase {
 		}
 	}

+	/**
+	 * Test to verify that term color is saved in AJAX calls, but only for terms belonging to a visual attribute.
+	 *
+	 * @testdox Should save term color only when adding visual attribute terms via AJAX.
+	 */
+	public function test_add_new_attribute_saves_color_only_for_visual_attributes(): void {
+		$original_theme      = wp_get_theme()->get_stylesheet();
+		$visual_attribute_id = null;
+		$text_attribute_id   = null;
+		$visual_taxonomy     = null;
+		$text_taxonomy       = null;
+		$visual_term_id      = 0;
+		$text_term_id        = 0;
+		$suffix              = (string) wp_rand( 1000, 9999 );
+
+		$enable_visual_attribute_feature = function ( $features ) {
+			$features[] = 'wc-visual-attribute';
+			return array_unique( $features );
+		};
+
+		add_filter( 'woocommerce_admin_features', $enable_visual_attribute_feature );
+
+		try {
+			switch_theme( 'twentytwentyfour' );
+
+			$visual_attribute_id = wc_create_attribute(
+				array(
+					'name' => 'Visual AJAX ' . $suffix,
+					'type' => 'wc-visual',
+				)
+			);
+			$text_attribute_id   = wc_create_attribute(
+				array(
+					'name' => 'Text AJAX ' . $suffix,
+					'type' => 'select',
+				)
+			);
+
+			$this->assertIsInt( $visual_attribute_id, 'The visual attribute should be created.' );
+			$this->assertIsInt( $text_attribute_id, 'The text attribute should be created.' );
+
+			$visual_taxonomy = $this->register_attribute_taxonomy_for_test( $visual_attribute_id );
+			$text_taxonomy   = $this->register_attribute_taxonomy_for_test( $text_attribute_id );
+
+			$this->_setRole( 'administrator' );
+
+			$_POST['security']   = wp_create_nonce( 'add-attribute' );
+			$_POST['taxonomy']   = $visual_taxonomy;
+			$_POST['term']       = 'Cerulean ' . $suffix;
+			$_POST['term_color'] = '#336699';
+
+			$visual_response = $this->do_ajax( 'woocommerce_add_new_attribute' );
+			$visual_term_id  = isset( $visual_response['term_id'] ) ? absint( $visual_response['term_id'] ) : 0;
+
+			$this->assertNotEmpty( $visual_term_id, 'The visual attribute term should be created.' );
+			$this->assertSame( '#336699', get_term_meta( $visual_term_id, 'color', true ), 'Visual attribute terms should store the posted color.' );
+
+			$_POST['security']   = wp_create_nonce( 'add-attribute' );
+			$_POST['taxonomy']   = $text_taxonomy;
+			$_POST['term']       = 'Plain ' . $suffix;
+			$_POST['term_color'] = '#abcdef';
+
+			$text_response = $this->do_ajax( 'woocommerce_add_new_attribute' );
+			$text_term_id  = isset( $text_response['term_id'] ) ? absint( $text_response['term_id'] ) : 0;
+
+			$this->assertNotEmpty( $text_term_id, 'The text attribute term should be created.' );
+			$this->assertSame( '', get_term_meta( $text_term_id, 'color', true ), 'Text attribute terms should ignore posted colors.' );
+		} finally {
+			unset( $_POST['security'], $_POST['taxonomy'], $_POST['term'], $_POST['term_color'] );
+
+			if ( $visual_term_id && taxonomy_exists( $visual_taxonomy ) ) {
+				wp_delete_term( $visual_term_id, $visual_taxonomy );
+			}
+
+			if ( $text_term_id && taxonomy_exists( $text_taxonomy ) ) {
+				wp_delete_term( $text_term_id, $text_taxonomy );
+			}
+
+			if ( is_int( $visual_attribute_id ) ) {
+				wc_delete_attribute( $visual_attribute_id );
+			}
+
+			if ( is_int( $text_attribute_id ) ) {
+				wc_delete_attribute( $text_attribute_id );
+			}
+
+			global $wc_product_attributes;
+			foreach ( array_filter( array( $visual_taxonomy, $text_taxonomy ) ) as $taxonomy ) {
+				if ( taxonomy_exists( $taxonomy ) ) {
+					unregister_taxonomy( $taxonomy );
+				}
+				unset( $wc_product_attributes[ $taxonomy ] );
+			}
+
+			remove_filter( 'woocommerce_admin_features', $enable_visual_attribute_feature );
+			switch_theme( $original_theme );
+		}//end try
+	}
+
+	/**
+	 * Register a product attribute taxonomy created inside a test.
+	 *
+	 * @param int $attribute_id Attribute ID.
+	 * @return string
+	 */
+	private function register_attribute_taxonomy_for_test( int $attribute_id ): string {
+		global $wc_product_attributes;
+
+		$taxonomy             = wc_attribute_taxonomy_name_by_id( $attribute_id );
+		$attribute_taxonomies = wc_get_attribute_taxonomies();
+
+		$wc_product_attributes[ $taxonomy ] = $attribute_taxonomies[ 'id:' . $attribute_id ];
+
+		register_taxonomy(
+			$taxonomy,
+			array( 'product' ),
+			array(
+				'capabilities' => array(
+					'manage_terms' => 'manage_product_terms',
+					'edit_terms'   => 'edit_product_terms',
+					'delete_terms' => 'delete_product_terms',
+					'assign_terms' => 'assign_product_terms',
+				),
+			)
+		);
+
+		return $taxonomy;
+	}
+
 	/**
 	 * Test coupon and recalculation of totals sequences when product prices are tax inclusive.
 	 */
diff --git a/plugins/woocommerce/tests/php/includes/wc-attribute-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-attribute-functions-test.php
index 1b509fd30bd..fefa46bbcb4 100644
--- a/plugins/woocommerce/tests/php/includes/wc-attribute-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-attribute-functions-test.php
@@ -5,6 +5,8 @@
  * @package WooCommerce\Tests\Functions.
  */

+declare( strict_types=1 );
+
 use PHPUnit\Framework\MockObject\Matcher\InvokedRecorder;

 /**