Commit e4655663de for woocommerce

commit e4655663de685747215de89a51e347de9f8cdc7d
Author: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com>
Date:   Tue Nov 25 11:05:28 2025 +0100

    Add to Cart with Options refactor: Pass `inputElement` as context in quantity selector (#62045)

    * Store `inputElement` ref

    * Remove `event` from quantity selector actions

    * Remove options from `setQuantity`

    * Remove default namespace

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Remove destructuring

    * Fix linting

    ---------

    Co-authored-by: github-actions <github-actions@github.com>
    Co-authored-by: Sam Seay <samueljseay@gmail.com>

diff --git a/plugins/woocommerce/changelog/62045-dev-pass-input-element-as-context-in-quantity-selector b/plugins/woocommerce/changelog/62045-dev-pass-input-element-as-context-in-quantity-selector
new file mode 100644
index 0000000000..3581433b5f
--- /dev/null
+++ b/plugins/woocommerce/changelog/62045-dev-pass-input-element-as-context-in-quantity-selector
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Pass input element as context in quantity selector
\ No newline at end of file
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts
index c8b5e4990f..69885603a6 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts
@@ -18,6 +18,7 @@ import type { ProductDataStore } from '@woocommerce/stores/woocommerce/product-d
 import { getMatchedVariation } from '../../base/utils/variations/get-matched-variation';
 import { doesCartItemMatchAttributes } from '../../base/utils/variations/does-cart-item-match-attributes';
 import type { GroupedProductAddToCartWithOptionsStore } from './grouped-product-selector/frontend';
+import type { Context as QuantitySelectorContext } from './quantity-selector/frontend';
 import type { VariableProductAddToCartWithOptionsStore } from './variation-selector/frontend';
 import type { NormalizedProductData, NormalizedVariationData } from './types';

@@ -143,11 +144,6 @@ export const getNewQuantity = (
 	return currentQuantity + quantity;
 };

-type SetQuantityOptions = Partial< {
-	changeTarget: HTMLInputElement;
-	forceUpdate: boolean;
-} >;
-
 export type AddToCartWithOptionsStore = {
 	state: {
 		noticeIds: string[];
@@ -160,11 +156,7 @@ export type AddToCartWithOptionsStore = {
 	};
 	actions: {
 		validateQuantity: ( productId: number, value?: number ) => void;
-		setQuantity: (
-			productId: number,
-			value: number,
-			options?: SetQuantityOptions
-		) => void;
+		setQuantity: ( productId: number, value: number ) => void;
 		addError: ( error: AddToCartError ) => string;
 		clearErrors: ( group?: string ) => void;
 		addToCart: () => void;
@@ -246,16 +238,18 @@ const { actions, state } = store<
 					} );
 				}
 			},
-			setQuantity(
-				productId: number,
-				value: number,
-				options: SetQuantityOptions = {}
-			) {
+			setQuantity( productId: number, value: number ) {
 				const context = getContext< Context >();
+				const quantitySelectorContext =
+					getContext< QuantitySelectorContext >(
+						'woocommerce/add-to-cart-with-options-quantity-selector'
+					);
+				const inputElement = quantitySelectorContext?.inputElement;
 				const { products } = getConfig(
 					'woocommerce'
 				) as WooCommerceConfig;
 				const variations = products?.[ productId ].variations;
+				const isValueNaN = Number.isNaN( inputElement?.valueAsNumber );

 				if ( variations ) {
 					const variationIds = Object.keys( variations );
@@ -264,7 +258,7 @@ const { actions, state } = store<
 					const idsToUpdate = [ productId, ...variationIds ];

 					idsToUpdate.forEach( ( id ) => {
-						if ( options?.forceUpdate ) {
+						if ( isValueNaN ) {
 							// Null the value first before setting the real value to ensure that
 							// a signal update happens.
 							context.quantity[ Number( id ) ] = null;
@@ -273,7 +267,7 @@ const { actions, state } = store<
 						context.quantity[ Number( id ) ] = value;
 					} );
 				} else {
-					if ( options?.forceUpdate ) {
+					if ( isValueNaN ) {
 						// Null the value first before setting the real value to ensure that
 						// a signal update happens.
 						context.quantity = {
@@ -294,8 +288,8 @@ const { actions, state } = store<
 					actions.validateQuantity( productId, value );
 				}

-				if ( options?.changeTarget ) {
-					dispatchChangeEvent( options.changeTarget );
+				if ( inputElement ) {
+					dispatchChangeEvent( inputElement );
 				}
 			},
 			addError: ( error: AddToCartError ): string => {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/frontend.ts
index eb18b719c1..10ba278fff 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/frontend.ts
@@ -3,7 +3,6 @@
  */
 import { store, getContext, getElement } from '@wordpress/interactivity';
 import '@woocommerce/stores/woocommerce/product-data';
-import type { HTMLElementEvent } from '@woocommerce/types';

 /**
  * Internal dependencies
@@ -14,6 +13,7 @@ import type { AddToCartWithOptionsStore } from '../frontend';
 export type Context = {
 	productId: number;
 	allowZero?: boolean;
+	inputElement?: HTMLInputElement | null;
 };

 // Stores are locked to prevent 3PD usage until the API is stable.
@@ -34,18 +34,13 @@ export type QuantitySelectorStore = {
 		inputQuantity: number;
 	};
 	actions: {
-		increaseQuantity: (
-			event: HTMLElementEvent< HTMLButtonElement >
-		) => void;
-		decreaseQuantity: (
-			event: HTMLElementEvent< HTMLButtonElement >
-		) => void;
-		handleQuantityBlur: (
-			event: HTMLElementEvent< HTMLInputElement >
-		) => void;
-		handleQuantityCheckboxChange: (
-			event: HTMLElementEvent< HTMLInputElement >
-		) => void;
+		increaseQuantity: () => void;
+		decreaseQuantity: () => void;
+		handleQuantityBlur: () => void;
+		handleQuantityCheckboxChange: () => void;
+	};
+	callbacks: {
+		storeInputElementRef: () => void;
 	};
 };

@@ -122,11 +117,8 @@ store< QuantitySelectorStore >(
 			},
 		},
 		actions: {
-			increaseQuantity: (
-				event: HTMLElementEvent< HTMLButtonElement >
-			) => {
-				const inputElement =
-					event.target.parentElement?.querySelector( '.qty' );
+			increaseQuantity: () => {
+				const { productId, inputElement } = getContext< Context >();

 				if ( ! ( inputElement instanceof HTMLInputElement ) ) {
 					return;
@@ -134,7 +126,6 @@ store< QuantitySelectorStore >(

 				const currentValue = Number( inputElement.value ) || 0;

-				const { productId } = getContext< Context >();
 				const { selectedAttributes } = addToCartWithOptionsStore.state;

 				const productObject = getProductData(
@@ -152,22 +143,18 @@ store< QuantitySelectorStore >(

 				addToCartWithOptionsStore.actions.setQuantity(
 					productId,
-					newValue,
-					{ changeTarget: inputElement }
+					newValue
 				);
 			},
-			decreaseQuantity: (
-				event: HTMLElementEvent< HTMLButtonElement >
-			) => {
-				const inputElement =
-					event.target.parentElement?.querySelector( '.qty' );
+			decreaseQuantity: () => {
+				const { allowZero, productId, inputElement } =
+					getContext< Context >();

 				if ( ! ( inputElement instanceof HTMLInputElement ) ) {
 					return;
 				}

 				const currentValue = Number( inputElement.value ) || 0;
-				const { allowZero, productId } = getContext< Context >();
 				const { selectedAttributes } = addToCartWithOptionsStore.state;

 				const productObject = getProductData(
@@ -190,18 +177,16 @@ store< QuantitySelectorStore >(
 				if ( newValue !== currentValue ) {
 					addToCartWithOptionsStore.actions.setQuantity(
 						productId,
-						newValue,
-						{ changeTarget: inputElement }
+						newValue
 					);
 				}
 			},
 			// We need to listen to blur events instead of change events because
 			// the change event isn't triggered in invalid numbers (ie: writing
 			// letters) if the current value is already invalid or an empty string.
-			handleQuantityBlur: (
-				event: HTMLElementEvent< HTMLInputElement >
-			) => {
-				const { allowZero, productId } = getContext< Context >();
+			handleQuantityBlur: () => {
+				const { allowZero, productId, inputElement } =
+					getContext< Context >();
 				const { selectedAttributes } = addToCartWithOptionsStore.state;

 				const productObject = getProductData(
@@ -213,35 +198,28 @@ store< QuantitySelectorStore >(
 					return;
 				}

-				const isValueNaN = Number.isNaN( event.target.valueAsNumber );
+				const isValueNaN = Number.isNaN( inputElement?.valueAsNumber );
 				const { min } = productObject;

 				if (
 					allowZero &&
-					( isValueNaN || event.target.valueAsNumber === 0 )
+					( isValueNaN || inputElement?.valueAsNumber === 0 )
 				) {
 					addToCartWithOptionsStore.actions.setQuantity(
 						productId,
-						0,
-						{
-							changeTarget: event.target,
-							forceUpdate: isValueNaN,
-						}
+						0
 					);
 					return;
 				}

 				// In other product types, we reset inputs to `min` if they are
 				// 0 or NaN.
-				const newValue =
-					! isValueNaN && event.target.valueAsNumber > 0
-						? event.target.valueAsNumber
-						: min;
+				const value = inputElement?.valueAsNumber ?? NaN;
+				const newValue = ! isNaN( value ) && value > 0 ? value : min;

 				addToCartWithOptionsStore.actions.setQuantity(
 					productId,
-					newValue,
-					{ changeTarget: event.target, forceUpdate: isValueNaN }
+					newValue
 				);
 			},
 			handleQuantityCheckboxChange: () => {
@@ -259,6 +237,17 @@ store< QuantitySelectorStore >(
 				);
 			},
 		},
+		callbacks: {
+			storeInputElementRef: () => {
+				const { ref } = getElement();
+				if ( ref ) {
+					const context = getContext< Context >();
+					const inputElement =
+						ref.querySelector< HTMLInputElement >( '.qty' );
+					context.inputElement = inputElement;
+				}
+			},
+		},
 	},
 	{ lock: universalLock }
 );
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 bd76a4d7ba..2cf4a2470c 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
@@ -358,8 +358,7 @@ const { actions, state } = store< VariableProductAddToCartWithOptionsStore >(
 					) {
 						actions.setQuantity(
 							productDataState.productId,
-							newValue,
-							{ changeTarget: ref }
+							newValue
 						);
 					}
 				}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/Utils.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/Utils.php
index ffade2459a..2a8f8d9034 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/Utils.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/Utils.php
@@ -43,10 +43,10 @@ class Utils {
 		$pattern = '/(<input[^>]*id="quantity_[^"]*"[^>]*\/>)/';
 		// Replacement string to add button AFTER the matched <input> element.
 		/* translators: %s refers to the item name in the cart. */
-		$minus_button = '$1<button aria-label="' . esc_attr( sprintf( __( 'Reduce quantity of %s', 'woocommerce' ), $product_name ) ) . '" type="button" data-wp-on--click="woocommerce/add-to-cart-with-options-quantity-selector::actions.decreaseQuantity" data-wp-bind--disabled="woocommerce/add-to-cart-with-options-quantity-selector::!state.allowsDecrease" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">−</button>';
+		$minus_button = '$1<button aria-label="' . esc_attr( sprintf( __( 'Reduce quantity of %s', 'woocommerce' ), $product_name ) ) . '" type="button" data-wp-on--click="actions.decreaseQuantity" data-wp-bind--disabled="!state.allowsDecrease" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">−</button>';
 		// Replacement string to add button AFTER the matched <input> element.
 		/* translators: %s refers to the item name in the cart. */
-		$plus_button = '$1<button aria-label="' . esc_attr( sprintf( __( 'Increase quantity of %s', 'woocommerce' ), $product_name ) ) . '" type="button" data-wp-on--click="woocommerce/add-to-cart-with-options-quantity-selector::actions.increaseQuantity" data-wp-bind--disabled="woocommerce/add-to-cart-with-options-quantity-selector::!state.allowsIncrease" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button>';
+		$plus_button = '$1<button aria-label="' . esc_attr( sprintf( __( 'Increase quantity of %s', 'woocommerce' ), $product_name ) ) . '" type="button" data-wp-on--click="actions.increaseQuantity" data-wp-bind--disabled="!state.allowsIncrease" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button>';
 		$new_html    = preg_replace( $pattern, $plus_button, $quantity_html );
 		$new_html    = preg_replace( $pattern, $minus_button, $new_html );
 		return $new_html;
@@ -107,8 +107,8 @@ class Utils {
 				)
 			);

-			$processor->set_attribute( 'data-wp-on--blur', 'woocommerce/add-to-cart-with-options-quantity-selector::actions.handleQuantityBlur' );
-			$processor->set_attribute( 'data-wp-bind--value', 'woocommerce/add-to-cart-with-options-quantity-selector::state.inputQuantity' );
+			$processor->set_attribute( 'data-wp-on--blur', 'actions.handleQuantityBlur' );
+			$processor->set_attribute( 'data-wp-bind--value', 'state.inputQuantity' );
 			foreach ( $input_attributes as $attribute => $value ) {
 				$processor->set_attribute( $attribute, $value );
 			}
@@ -119,6 +119,7 @@ class Utils {
 		$wrapper_attributes = array_merge(
 			array(
 				'data-wp-interactive' => 'woocommerce/add-to-cart-with-options-quantity-selector',
+				'data-wp-init'        => 'callbacks.storeInputElementRef',
 			),
 			$wrapper_attributes
 		);