Commit 563fcdb8a8 for woocommerce
commit 563fcdb8a877d797c23ca434dd4a507b9c3c4e13
Author: Kathy <507025+helgatheviking@users.noreply.github.com>
Date: Tue Nov 18 00:23:54 2025 -0500
Eliminate hard-coded 'grouped' product handling in frontend of quantity selector block (#61496)
---------
Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com>
diff --git a/plugins/woocommerce/changelog/issues-61495-quantity-input-extended b/plugins/woocommerce/changelog/issues-61495-quantity-input-extended
new file mode 100644
index 0000000000..a595a21be8
--- /dev/null
+++ b/plugins/woocommerce/changelog/issues-61495-quantity-input-extended
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Eliminate hard-coded 'grouped' product handling in frontend of quantity selector block
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 828473f620..53bd7c0911 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
@@ -13,6 +13,7 @@ import type { AddToCartWithOptionsStore } from '../frontend';
export type Context = {
productId: number;
+ allowZero?: boolean;
};
// Stores are locked to prevent 3PD usage until the API is stable.
@@ -83,24 +84,28 @@ store< QuantitySelectorStore >(
// Note: in grouped products, `productData` will be the parent product.
// We handle grouped products decrease differently because we
// allow setting the quantity to 0.
- const { productData, quantity } =
+ const { quantity, selectedAttributes } =
addToCartWithOptionsStore.state;
- if ( ! productData ) {
- return true;
- }
+ const { allowZero, productId } = getContext< Context >();
- if ( productData.type === 'grouped' ) {
- const { productId } = getContext< Context >();
+ const productObject = getProductData(
+ productId,
+ selectedAttributes
+ );
- return quantity[ productId ] > 0;
+ if ( ! productObject ) {
+ return true;
}
- const { id, min, step } = productData;
+ const { id, min, step } = productObject;
const currentQuantity = quantity[ id ] || 0;
- return currentQuantity - step >= min;
+ return (
+ ( allowZero && currentQuantity > 0 ) ||
+ currentQuantity - step >= min
+ );
},
get allowsIncrease() {
const { quantity, selectedAttributes } =
@@ -146,6 +151,7 @@ store< QuantitySelectorStore >(
);
let newValue = currentValue + 1;
+
if ( productObject ) {
const { max, min, step } = productObject;
newValue = currentValue + step;
@@ -170,40 +176,23 @@ store< QuantitySelectorStore >(
}
const currentValue = Number( inputElement.value ) || 0;
+ const { allowZero, productId } = getContext< Context >();
+ const { selectedAttributes } = addToCartWithOptionsStore.state;
- const { productId } = getContext< Context >();
- const { productData, selectedAttributes } =
- addToCartWithOptionsStore.state;
-
- const parentProductObject = productData;
-
- let productObject = parentProductObject;
-
- if ( parentProductObject?.type === 'grouped' ) {
- productObject = getProductData(
- productId,
- selectedAttributes
- );
- }
+ const productObject = getProductData(
+ productId,
+ selectedAttributes
+ );
let newValue = currentValue - 1;
if ( productObject ) {
- const { min, step } = productObject;
+ const { max, min, step } = productObject;
newValue = currentValue - step;
-
- if ( newValue < min ) {
- // In grouped product children, we allow decreasing the value
- // down to 0, even if the minimum value is greater than 0.
- if ( parentProductObject?.type === 'grouped' ) {
- if ( currentValue > min ) {
- newValue = min;
- } else {
- newValue = 0;
- }
- } else {
- newValue = min;
- }
+ if ( allowZero && newValue < min && currentValue === min ) {
+ newValue = 0;
+ } else {
+ newValue = Math.min( max, Math.max( min, newValue ) );
}
}
@@ -223,26 +212,31 @@ store< QuantitySelectorStore >(
handleQuantityBlur: (
event: HTMLElementEvent< HTMLInputElement >
) => {
- const { productData, selectedAttributes } =
- addToCartWithOptionsStore.state;
- let min = 1;
+ const { allowZero, productId } = getContext< Context >();
+ const { selectedAttributes } = addToCartWithOptionsStore.state;
- if ( ! productData ) {
+ const productObject = getProductData(
+ productId,
+ selectedAttributes
+ );
+
+ if ( ! productObject ) {
return;
}
- const { productId } = getContext< Context >();
+ const { min } = productObject;
// In grouped products, we reset invalid inputs to ''.
if (
+ allowZero &&
( Number.isNaN( event.target.valueAsNumber ) ||
- event.target.valueAsNumber === 0 ) &&
- productData.type === 'grouped'
+ event.target.valueAsNumber === 0 )
) {
addToCartWithOptionsStore.actions.setQuantity(
productId,
0
);
+
if ( Number.isNaN( event.target.valueAsNumber ) ) {
event.target.value = '';
}
@@ -250,24 +244,15 @@ store< QuantitySelectorStore >(
return;
}
- const childProductData =
- productData.type === 'grouped'
- ? getProductData( productId, selectedAttributes )
- : productData;
-
- if ( ! childProductData ) {
- return;
- }
-
- min = childProductData.min;
-
- // In other product types, we reset inputs to `min` if they are
- // 0 or NaN.
- const newValue =
+ let newValue;
+ if (
Number.isFinite( event.target.valueAsNumber ) &&
event.target.valueAsNumber > 0
- ? event.target.valueAsNumber
- : min;
+ ) {
+ newValue = event.target.valueAsNumber;
+ } else {
+ newValue = allowZero ? 0 : min;
+ }
addToCartWithOptionsStore.actions.setQuantity(
productId,
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/GroupedProductItemSelector.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/GroupedProductItemSelector.php
index ecd288be9b..a48e825763 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/GroupedProductItemSelector.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/GroupedProductItemSelector.php
@@ -82,8 +82,13 @@ class GroupedProductItemSelector extends AbstractBlock {
$quantity_html = AddToCartWithOptionsUtils::add_quantity_steppers( $quantity_html, $product_name );
$quantity_html = AddToCartWithOptionsUtils::add_quantity_stepper_classes( $quantity_html );
+ $context = array(
+ 'productId' => $product->get_id(),
+ 'allowZero' => true, // The item is optional in grouped products.
+ );
+
// Add interactive data attribute for the stepper functionality.
- $quantity_html = AddToCartWithOptionsUtils::make_quantity_input_interactive( $quantity_html, array(), array(), $product->get_id() );
+ $quantity_html = AddToCartWithOptionsUtils::make_quantity_input_interactive( $quantity_html, array(), array(), $context );
return $quantity_html;
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/Utils.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/Utils.php
index be403028d8..fac7dbb238 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/Utils.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/Utils.php
@@ -77,14 +77,18 @@ class Utils {
/**
* Make the quantity input interactive by wrapping it with the necessary data attribute and adding a blur event listener.
*
- * @param string $quantity_html The quantity HTML.
- * @param array $wrapper_attributes Optional wrapper attributes.
- * @param array $input_attributes Optional input attributes.
- * @param int|null $child_product_id Optional child product ID.
+ * @param string $quantity_html The quantity HTML.
+ * @param array $wrapper_attributes Optional wrapper attributes.
+ * @param array $input_attributes Optional input attributes.
+ * @param array $context {
+ * Optional context for quantity input.
+ * @type int $productId Product ID for context-specific behavior.
+ * @type bool $allowZero Whether to allow zero quantity.
+ * }
*
* @return string The quantity HTML with interactive wrapper.
*/
- public static function make_quantity_input_interactive( $quantity_html, $wrapper_attributes = array(), $input_attributes = array(), $child_product_id = null ) {
+ public static function make_quantity_input_interactive( $quantity_html, $wrapper_attributes = array(), $input_attributes = array(), $context = array() ) {
$processor = new \WP_HTML_Tag_Processor( $quantity_html );
if (
$processor->next_tag( 'input' ) &&
@@ -110,10 +114,11 @@ class Utils {
global $product;
$context_attribute = wp_interactivity_data_wp_context(
- array(
- 'productId' => $child_product_id || ! $product instanceof \WC_Product ?
- $child_product_id :
- $product->get_id(),
+ wp_parse_args(
+ $context,
+ array(
+ 'productId' => $product instanceof \WC_Product ? $product->get_id() : 0,
+ )
)
);