Commit c00eaa3c21 for woocommerce
commit c00eaa3c21fb94fc309be30772212601efa409bd
Author: Amit Raj <77401999+amitraj2203@users.noreply.github.com>
Date: Fri Apr 25 15:10:30 2025 +0530
[Experimental] Blockified Update Quantity Selector and Grouped Product Selector to use Stepper mode only (#57442)
* refactor: remove input mode from quantity selector, allowing only stepper mode
* feat: Add stepper mode and remove input mode
* feat: Add changelog message
* refactor: Quantity input handling in Add to Cart blocks to utilize utility methods for improved maintainability and readability
* refactor: Consolidate quantity handling logic and remove redundant frontend file
* refactor: Remove redundant quantity selector styles and consolidate into main style file
* refactor: Introduce QuantitySelector component to consolidate the component between different blocks
* refactor: Remove unused style import from quantity selector component
* refactor: Update description in quantity selector block to clarify input functionality
* refactor: Simplify type assertion for input element in getInputElementFromEvent function
* refactor: Rename quantity action methods for consistency and clarity in AddtoCartWithOptions block
* refactor: Consolidate quantity selector styles and remove unused files
* refactor: Wrap all the styles in this file with the two blocks parent classes
* refactor: Simplify quantity selector style assignment in downgrade notice
* refactor: Remove unused script enqueue method from grouped product selector CTA
* fix: Add check for $previous_product is an instance of WC_Product.
* refactor: Remove isSiteEditor prop from QuantityStepper component
* refactor: Simplify product retrieval in context methods and update validation logic
* fix: Correct namespace casing for AddtoCartWithOptions in relevant files
* fix: Linitng error
---------
Co-authored-by: Vladimir Reznichenko <kalessil@gmail.com>
diff --git a/plugins/woocommerce/changelog/57442-quantity-selector-stepper-mode b/plugins/woocommerce/changelog/57442-quantity-selector-stepper-mode
new file mode 100644
index 0000000000..45fe20d7bb
--- /dev/null
+++ b/plugins/woocommerce/changelog/57442-quantity-selector-stepper-mode
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Update Quantity Selector and Grouped Product Selector Item CTA blocks to have only Stepper mode.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/components/downgrade-notice/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/components/downgrade-notice/index.tsx
index 936819db5c..b419adde14 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/components/downgrade-notice/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/components/downgrade-notice/index.tsx
@@ -25,17 +25,8 @@ const downgradeToClassicAddToCartWithOptions = ( blockClientId: string ) => {
return false;
}
- const foundQuantitySelectorBlock = findBlock( {
- blocks,
- findCondition: ( block ) =>
- block.name ===
- 'woocommerce/add-to-cart-with-options-quantity-selector',
- } );
-
const newBlock = createBlock( 'woocommerce/add-to-cart-form', {
- quantitySelectorStyle:
- foundQuantitySelectorBlock?.attributes?.quantitySelectorStyle ||
- 'input',
+ quantitySelectorStyle: 'input',
} );
dispatch( 'core/block-editor' ).replaceBlock(
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/components/quantity-stepper/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/components/quantity-stepper/index.tsx
new file mode 100644
index 0000000000..a7a96a7998
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/components/quantity-stepper/index.tsx
@@ -0,0 +1,20 @@
+const QuantityStepper = () => {
+ return (
+ <div className="quantity wc-block-components-quantity-selector">
+ <button className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">
+ -
+ </button>
+ <input
+ type="number"
+ value="1"
+ className="input-text qty text"
+ readOnly
+ />
+ <button className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">
+ +
+ </button>
+ </div>
+ );
+};
+
+export default QuantityStepper;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/editor.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/editor.scss
index 8d9d2f4bf7..9c2705234e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/editor.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/editor.scss
@@ -28,3 +28,15 @@
color: inherit;
}
}
+
+.wc-block-add-to-cart-with-options-grouped-product-selector-item-cta,
+.wc-block-add-to-cart-with-options__quantity-selector {
+ .quantity .qty {
+ // WooCommerce core styles: https://github.com/woocommerce/woocommerce/blob/c9f62609155825cd817976c7611b9b0415e90f2f/plugins/woocommerce/client/legacy/css/woocommerce.scss/#L111-L112
+ width: 3.631em;
+ }
+
+ .wc-block-components-quantity-selector {
+ margin: 0;
+ }
+}
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 ab43061bc1..efd899cc2a 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
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import type { FormEvent } from 'react';
+import type { FormEvent, HTMLElementEvent } from 'react';
import { store, getContext } from '@wordpress/interactivity';
import type { Store as WooCommerce } from '@woocommerce/stores/woocommerce/cart';
import type { CartVariationItem } from '@woocommerce/types';
@@ -23,6 +23,49 @@ const { state: wooState } = store< WooCommerce >(
{ lock: universalLock }
);
+const getInputElementFromEvent = (
+ event: HTMLElementEvent< HTMLButtonElement >
+) => {
+ const target = event.target as HTMLButtonElement;
+
+ const inputElement = target.parentElement?.querySelector(
+ '.input-text.qty.text'
+ ) as HTMLInputElement | null;
+
+ return inputElement;
+};
+
+const getInputData = ( event: HTMLElementEvent< HTMLButtonElement > ) => {
+ const inputElement = getInputElementFromEvent( event );
+
+ if ( ! inputElement ) {
+ return;
+ }
+
+ const parsedValue = parseInt( inputElement.value, 10 );
+ const parsedMinValue = parseInt( inputElement.min, 10 );
+ const parsedMaxValue = parseInt( inputElement.max, 10 );
+ const parsedStep = parseInt( inputElement.step, 10 );
+
+ const currentValue = isNaN( parsedValue ) ? 0 : parsedValue;
+ const minValue = isNaN( parsedMinValue ) ? 1 : parsedMinValue;
+ const maxValue = isNaN( parsedMaxValue ) ? undefined : parsedMaxValue;
+ const step = isNaN( parsedStep ) ? 1 : parsedStep;
+
+ return {
+ currentValue,
+ minValue,
+ maxValue,
+ step,
+ inputElement,
+ };
+};
+
+const dispatchChangeEvent = ( inputElement: HTMLInputElement ) => {
+ const event = new Event( 'change' );
+ inputElement.dispatchEvent( event );
+};
+
const addToCartWithOptionsStore = store(
'woocommerce/add-to-cart-with-options',
{
@@ -57,6 +100,40 @@ const addToCartWithOptionsStore = store(
context.variation.splice( index, 1 );
}
},
+ increaseQuantity: (
+ event: HTMLElementEvent< HTMLButtonElement >
+ ) => {
+ const inputData = getInputData( event );
+ if ( ! inputData ) {
+ return;
+ }
+ const { currentValue, maxValue, step, inputElement } =
+ inputData;
+ const newValue = currentValue + step;
+
+ if ( maxValue === undefined || newValue <= maxValue ) {
+ addToCartWithOptionsStore.actions.setQuantity( newValue );
+ inputElement.value = newValue.toString();
+ dispatchChangeEvent( inputElement );
+ }
+ },
+ decreaseQuantity: (
+ event: HTMLElementEvent< HTMLButtonElement >
+ ) => {
+ const inputData = getInputData( event );
+ if ( ! inputData ) {
+ return;
+ }
+ const { currentValue, minValue, step, inputElement } =
+ inputData;
+ const newValue = currentValue - step;
+
+ if ( newValue >= minValue ) {
+ addToCartWithOptionsStore.actions.setQuantity( newValue );
+ inputElement.value = newValue.toString();
+ dispatchChangeEvent( inputElement );
+ }
+ },
*handleSubmit( event: FormEvent< HTMLFormElement > ) {
event.preventDefault();
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/grouped-product-selector/product-item-cta/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/grouped-product-selector/product-item-cta/edit.tsx
index d970922bd8..d5139c590b 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/grouped-product-selector/product-item-cta/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/grouped-product-selector/product-item-cta/edit.tsx
@@ -3,17 +3,16 @@
*/
import { useProductDataContext } from '@woocommerce/shared-context';
import { Disabled, Spinner } from '@wordpress/components';
-import { useSelect } from '@wordpress/data';
-import { isSiteEditorPage } from '@woocommerce/utils';
import { useBlockProps } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
+/**
+ * Internal dependencies
+ */
+import QuantityStepper from '../../components/quantity-stepper';
+
const CTA = () => {
const { isLoading, product } = useProductDataContext();
- const isSiteEditor = useSelect(
- ( select ) => isSiteEditorPage( select( 'core/edit-site' ) ),
- []
- );
if ( isLoading ) {
return <Spinner />;
@@ -38,28 +37,7 @@ const CTA = () => {
/>
);
}
- return (
- <div className="quantity">
- <input
- style={
- // In the post editor, the editor isn't in an iframe, so WordPress styles are applied. We need to remove them.
- ! isSiteEditor
- ? {
- backgroundColor: '#ffffff',
- lineHeight: 'normal',
- minHeight: 'unset',
- boxSizing: 'unset',
- borderRadius: 'unset',
- }
- : {}
- }
- type="number"
- value="1"
- className="input-text qty text"
- readOnly
- />
- </div>
- );
+ return <QuantityStepper />;
}
return (
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/grouped-product-selector/product-item-cta/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/grouped-product-selector/product-item-cta/style.scss
index d2efa0ca32..3b58b72fdc 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/grouped-product-selector/product-item-cta/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/grouped-product-selector/product-item-cta/style.scss
@@ -4,14 +4,4 @@
.button {
display: block;
}
-
- /**
- * This is a base style for the input text element in WooCommerce that prevents inputs from appearing too small.
- *
- * @link https://github.com/woocommerce/woocommerce/blob/95ca53675f2817753d484583c96ca9ab9f725172/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss#L203-L206
- */
- .quantity .input-text {
- font-size: var(--wp--preset--font-size--small);
- padding: 0.9rem 1.1rem;
- }
}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/index.tsx
index 53b2a3ad30..da19261136 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/index.tsx
@@ -13,6 +13,8 @@ import ProductTypeSelectorPlugin from './plugins';
import metadata from './block.json';
import AddToCartOptionsEdit from './edit';
import { shouldBlockifiedAddToCartWithOptionsBeRegistered } from './utils';
+import '../../base/components/quantity-selector/style.scss';
+import './editor.scss';
import './style.scss';
import type { Attributes } from './types';
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/block.json
index 0352471572..57511ab4d5 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/block.json
@@ -1,15 +1,8 @@
{
"name": "woocommerce/add-to-cart-with-options-quantity-selector",
"title": "Quantity Selector (Experimental)",
- "description": "Display an input field to select the number of products to add to cart.",
+ "description": "Display an input field with stepper to select the number of products to add to cart.",
"category": "woocommerce-product-elements",
- "attributes": {
- "quantitySelectorStyle": {
- "type": "string",
- "enum": [ "input", "stepper" ],
- "default": "input"
- }
- },
"keywords": [ "WooCommerce" ],
"usesContext": [ "postId" ],
"ancestor": [ "woocommerce/add-to-cart-with-options" ],
@@ -18,8 +11,5 @@
"supports": {
"interactivity": true
},
- "$schema": "https://schemas.wp.org/trunk/block.json",
- "viewScriptModule": "woocommerce/add-to-cart-with-options-quantity-selector",
- "style": "file:../woocommerce/add-to-cart-with-options-quantity-selector-style.css",
- "editorStyle": "file:../woocommerce/add-to-cart-with-options-quantity-selector-editor.css"
+ "$schema": "https://schemas.wp.org/trunk/block.json"
}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/edit.tsx
index 7a8a628dcb..20acce4165 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/edit.tsx
@@ -1,153 +1,25 @@
/**
* External dependencies
*/
-import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
-import { __ } from '@wordpress/i18n';
-import { BlockEditProps } from '@wordpress/blocks';
-import { useSelect } from '@wordpress/data';
-import { isSiteEditorPage } from '@woocommerce/utils';
-import {
- Disabled,
- PanelBody,
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore - Ignoring because `__experimentalToggleGroupControl` is not yet in the type definitions.
- // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
- __experimentalToggleGroupControl as ToggleGroupControl,
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore - Ignoring because `__experimentalToggleGroupControl` is not yet in the type definitions.
- // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
- __experimentalToggleGroupControlOption as ToggleGroupControlOption,
-} from '@wordpress/components';
+import { useBlockProps } from '@wordpress/block-editor';
+import { Disabled } from '@wordpress/components';
-enum QuantitySelectorStyle {
- Input = 'input',
- Stepper = 'stepper',
-}
-
-interface Attributes {
- className?: string;
- quantitySelectorStyle: QuantitySelectorStyle;
-}
-
-const getHelpText = ( quantitySelectorStyle: QuantitySelectorStyle ) => {
- if ( quantitySelectorStyle === QuantitySelectorStyle.Input ) {
- return __(
- 'Shoppers can enter a number of items to add to cart.',
- 'woocommerce'
- );
- }
- if ( quantitySelectorStyle === QuantitySelectorStyle.Stepper ) {
- return __(
- 'Shoppers can use buttons to change the number of items to add to cart.',
- 'woocommerce'
- );
- }
-};
-
-const AddToCartWithOptionsQuantitySelectorEdit = (
- props: BlockEditProps< Attributes >
-) => {
- const { setAttributes } = props;
- const { quantitySelectorStyle } = props.attributes;
-
- const quantitySelectorStyleClass =
- quantitySelectorStyle === QuantitySelectorStyle.Input
- ? 'wc-block-add-to-cart-with-options__quantity-selector--input'
- : 'wc-block-add-to-cart-with-options__quantity-selector--stepper';
+/**
+ * Internal dependencies
+ */
+import QuantityStepper from '../components/quantity-stepper';
+const AddToCartWithOptionsQuantitySelectorEdit = () => {
const blockProps = useBlockProps( {
- className: `wc-block-add-to-cart-with-options__quantity-selector ${ quantitySelectorStyleClass }`,
+ className: 'wc-block-add-to-cart-with-options__quantity-selector',
} );
- const isSiteEditor = useSelect(
- ( select ) => isSiteEditorPage( select( 'core/edit-site' ) ),
- []
- );
-
return (
- <>
- <InspectorControls>
- <PanelBody title={ __( 'Settings', 'woocommerce' ) }>
- <ToggleGroupControl
- className="wc-block-editor-add-to-cart-with-options__quantity-selector"
- __nextHasNoMarginBottom
- value={ quantitySelectorStyle }
- isBlock
- onChange={ ( value: QuantitySelectorStyle ) => {
- setAttributes( {
- quantitySelectorStyle:
- value as QuantitySelectorStyle,
- } );
- } }
- help={ getHelpText( quantitySelectorStyle ) }
- >
- <ToggleGroupControlOption
- label={ __( 'Input', 'woocommerce' ) }
- value={ QuantitySelectorStyle.Input }
- />
- <ToggleGroupControlOption
- label={ __( 'Stepper', 'woocommerce' ) }
- value={ QuantitySelectorStyle.Stepper }
- />
- </ToggleGroupControl>
- </PanelBody>
- </InspectorControls>
- <div { ...blockProps }>
- <Disabled>
- { quantitySelectorStyle === QuantitySelectorStyle.Input && (
- <div className="quantity">
- <input
- style={
- // In the post editor, the editor isn't in an iframe, so WordPress styles are applied. We need to remove them.
- ! isSiteEditor
- ? {
- backgroundColor: '#ffffff',
- lineHeight: 'normal',
- minHeight: 'unset',
- boxSizing: 'unset',
- borderRadius: 'unset',
- }
- : {}
- }
- type="number"
- value="1"
- className="input-text qty text"
- readOnly
- />
- </div>
- ) }
- { quantitySelectorStyle ===
- QuantitySelectorStyle.Stepper && (
- <div className="quantity wc-block-components-quantity-selector">
- <button className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">
- -
- </button>
- <input
- style={
- // In the post editor, the editor isn't in an iframe, so WordPress styles are applied. We need to remove them.
- ! isSiteEditor
- ? {
- backgroundColor: '#ffffff',
- lineHeight: 'normal',
- minHeight: 'unset',
- boxSizing: 'unset',
- borderRadius: 'unset',
- }
- : {}
- }
- type="number"
- value="1"
- className="input-text qty text"
- readOnly
- />
- <button className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">
- +
- </button>
- </div>
- ) }
- </Disabled>
- </div>
- </>
+ <div { ...blockProps }>
+ <Disabled>
+ <QuantityStepper />
+ </Disabled>
+ </div>
);
};
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/editor.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/editor.scss
deleted file mode 100644
index 72c70777d0..0000000000
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/editor.scss
+++ /dev/null
@@ -1,23 +0,0 @@
-.wc-block-add-to-cart-with-options__quantity-selector {
- .quantity .qty {
- // WooCommerce core styles: https://github.com/woocommerce/woocommerce/blob/c9f62609155825cd817976c7611b9b0415e90f2f/plugins/woocommerce/client/legacy/css/woocommerce.scss/#L111-L112
- width: 3.631em;
- text-align: center;
- }
-
- &.wc-block-add-to-cart-with-options__quantity-selector--input {
- .quantity .qty {
- padding-top: 0;
- padding-bottom: 0;
- height: 2.5rem;
- width: 3rem;
- }
- }
-
- &.wc-block-add-to-cart-with-options__quantity-selector--stepper {
- .wc-block-components-quantity-selector {
- margin: 0 4px 0 0;
- width: unset !important;
- }
- }
-}
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
deleted file mode 100644
index e3d1b35eef..0000000000
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/frontend.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-/**
- * External dependencies
- */
-import { store } from '@wordpress/interactivity';
-import type { HTMLElementEvent } from '@woocommerce/types';
-
-/**
- * Internal dependencies
- */
-import type { AddToCartWithOptionsStore } from '../frontend';
-
-// Stores are locked to prevent 3PD usage until the API is stable.
-const universalLock =
- 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.';
-
-const getInputElementFromEvent = (
- event: HTMLElementEvent< HTMLButtonElement >
-) => {
- const target = event.target as HTMLButtonElement;
-
- const inputElement = target.parentElement?.querySelector(
- '.input-text.qty.text'
- ) as HTMLInputElement | null | undefined;
-
- return inputElement;
-};
-
-const getInputData = ( event: HTMLElementEvent< HTMLButtonElement > ) => {
- const inputElement = getInputElementFromEvent( event );
-
- if ( ! inputElement ) {
- return;
- }
-
- const parsedValue = parseInt( inputElement.value, 10 );
- const parsedMinValue = parseInt( inputElement.min, 10 );
- const parsedMaxValue = parseInt( inputElement.max, 10 );
- const parsedStep = parseInt( inputElement.step, 10 );
-
- const currentValue = isNaN( parsedValue ) ? 0 : parsedValue;
- const minValue = isNaN( parsedMinValue ) ? 1 : parsedMinValue;
- const maxValue = isNaN( parsedMaxValue ) ? undefined : parsedMaxValue;
- const step = isNaN( parsedStep ) ? 1 : parsedStep;
-
- return {
- currentValue,
- minValue,
- maxValue,
- step,
- inputElement,
- };
-};
-
-const dispatchChangeEvent = ( inputElement: HTMLInputElement ) => {
- const event = new Event( 'change' );
-
- inputElement.dispatchEvent( event );
-};
-
-const { actions: wooAddToCartWithOptionsActions } =
- store< AddToCartWithOptionsStore >(
- 'woocommerce/add-to-cart-with-options',
- {},
- { lock: universalLock }
- );
-
-store( 'woocommerce/add-to-cart-with-options', {
- actions: {
- addQuantity: ( event: HTMLElementEvent< HTMLButtonElement > ) => {
- const inputData = getInputData( event );
- if ( ! inputData ) {
- return;
- }
- const { currentValue, maxValue, step, inputElement } = inputData;
- const newValue = currentValue + step;
-
- if ( maxValue === undefined || newValue <= maxValue ) {
- wooAddToCartWithOptionsActions?.setQuantity( newValue );
- inputElement.value = newValue.toString();
- dispatchChangeEvent( inputElement );
- }
- },
- removeQuantity: ( event: HTMLElementEvent< HTMLButtonElement > ) => {
- const inputData = getInputData( event );
- if ( ! inputData ) {
- return;
- }
- const { currentValue, minValue, step, inputElement } = inputData;
- const newValue = currentValue - step;
-
- if ( newValue >= minValue ) {
- wooAddToCartWithOptionsActions?.setQuantity( newValue );
- inputElement.value = newValue.toString();
- dispatchChangeEvent( inputElement );
- }
- },
- },
-} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/index.tsx
index 5ab61aefbf..8fe8626cbe 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/index.tsx
@@ -10,9 +10,6 @@ import { Icon, button } from '@wordpress/icons';
import metadata from './block.json';
import AddToCartWithOptionsQuantitySelectorEdit from './edit';
import { shouldBlockifiedAddToCartWithOptionsBeRegistered } from '../utils';
-import '../../../base/components/quantity-selector/style.scss';
-import './style.scss';
-import './editor.scss';
if ( shouldBlockifiedAddToCartWithOptionsBeRegistered ) {
registerBlockType( metadata, {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/style.scss
deleted file mode 100644
index 2f48c35415..0000000000
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/style.scss
+++ /dev/null
@@ -1,59 +0,0 @@
-.wc-block-add-to-cart-with-options__quantity-selector {
- /**
- * This is a base style for the input text element in WooCommerce that prevents inputs from appearing too small.
- *
- * @link https://github.com/woocommerce/woocommerce/blob/95ca53675f2817753d484583c96ca9ab9f725172/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss#L203-L206
- */
- .input-text {
- font-size: var(--wp--preset--font-size--small);
- padding: 0.9rem 1.1rem;
- }
-}
-
-// Stepper CSS
-.wc-block-add-to-cart-with-options__quantity-selector--stepper {
- .wc-block-components-quantity-selector {
- &::after {
- border: 1px solid currentColor;
- opacity: 0.3;
- }
-
- display: inline-flex;
- width: unset;
- margin-right: 0.5em;
- margin-bottom: 0;
-
- .input-text {
- font-size: var(--wp--preset--font-size--small);
- }
-
- input[type="number"].input-text.qty.text {
- -moz-appearance: textfield;
- border: unset;
- text-align: center;
- order: 1;
- font-weight: 600;
- background-color: transparent;
- color: currentColor;
- margin: 0;
- /**
- * This is a base style for the input text element in WooCommerce that prevents inputs from appearing too small.
- *
- * @link https://github.com/woocommerce/woocommerce/blob/95ca53675f2817753d484583c96ca9ab9f725172/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss#L203-L206
- */
- padding: 0.9rem 0;
- margin-right: unset;
- font-size: var(--wp--preset--font-size--small);
-
- &:focus {
- border-radius: unset;
- }
- }
-
- input[type="number"]::-webkit-inner-spin-button,
- input[type="number"]::-webkit-outer-spin-button {
- -webkit-appearance: none;
- margin: 0;
- }
- }
-}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/style.scss
index dcab9495f0..6ed147502e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/style.scss
@@ -1 +1,58 @@
-/* Styles here */
+.wc-block-add-to-cart-with-options__quantity-selector,
+.wc-block-add-to-cart-with-options-grouped-product-selector-item-cta {
+ /**
+ * This is a base style for the input text element in WooCommerce that prevents inputs from appearing too small.
+ *
+ * @link https://github.com/woocommerce/woocommerce/blob/95ca53675f2817753d484583c96ca9ab9f725172/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss#L203-L206
+ */
+ .quantity .input-text {
+ font-size: var(--wp--preset--font-size--small);
+ padding: 0.9rem 1.1rem;
+ }
+
+ // Stepper CSS
+ .wc-block-components-quantity-selector {
+ &::after {
+ border: 1px solid currentColor;
+ opacity: 0.3;
+ }
+
+ display: inline-flex;
+ width: unset;
+ margin-right: 0.5em;
+ margin-bottom: 0;
+
+ .input-text {
+ font-size: var(--wp--preset--font-size--small);
+ }
+
+ input[type="number"].input-text.qty.text {
+ -moz-appearance: textfield;
+ border: unset;
+ text-align: center;
+ order: 1;
+ font-weight: 600;
+ background-color: transparent;
+ color: currentColor;
+ margin: 0;
+ /**
+ * This is a base style for the input text element in WooCommerce that prevents inputs from appearing too small.
+ *
+ * @link https://github.com/woocommerce/woocommerce/blob/95ca53675f2817753d484583c96ca9ab9f725172/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss#L203-L206
+ */
+ padding: 0.9rem 0;
+ margin-right: unset;
+ font-size: var(--wp--preset--font-size--small);
+
+ &:focus {
+ border-radius: unset;
+ }
+ }
+
+ input[type="number"]::-webkit-inner-spin-button,
+ input[type="number"]::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+ }
+}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptionsGroupedProductSelectorItemCTA.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptionsGroupedProductSelectorItemCTA.php
index f7791f03e3..9b4b3cdbaa 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptionsGroupedProductSelectorItemCTA.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptionsGroupedProductSelectorItemCTA.php
@@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes;
+use Automattic\WooCommerce\Blocks\BlockTypes\AddtoCartWithOptions\Utils;
use WP_Block;
/**
@@ -30,31 +31,20 @@ class AddToCartWithOptionsGroupedProductSelectorItemCTA extends AbstractBlock {
private function get_quantity_selector_markup( $product ) {
ob_start();
- woocommerce_quantity_input(
- array(
- /**
- * Filter the minimum quantity value allowed for the product.
- *
- * @since 2.0.0
- * @param int $min_value Minimum quantity value.
- * @param WC_Product $product Product object.
- */
- 'min_value' => apply_filters( 'woocommerce_quantity_input_min', $product->get_min_purchase_quantity(), $product ),
- /**
- * Filter the maximum quantity value allowed for the product.
- *
- * @since 2.0.0
- * @param int $max_value Maximum quantity value.
- * @param WC_Product $product Product object.
- */
- 'max_value' => apply_filters( 'woocommerce_quantity_input_max', $product->get_max_purchase_quantity(), $product ),
- 'input_value' => isset( $_POST['quantity'] ) ? wc_stock_amount( wp_unslash( $_POST['quantity'] ) ) : $product->get_min_purchase_quantity(), // phpcs:ignore WordPress.Security.NonceVerification.Missing
- )
- );
-
- $quantity_selector_html = ob_get_clean();
-
- return $quantity_selector_html;
+ woocommerce_quantity_input( Utils::get_quantity_input_args( $product ) );
+
+ $quantity_html = ob_get_clean();
+
+ // Modify the quantity input to add stepper buttons.
+ $product_name = $product->get_name();
+
+ $quantity_html = Utils::add_quantity_steppers( $quantity_html, $product_name );
+ $quantity_html = Utils::add_quantity_stepper_classes( $quantity_html );
+
+ // Add interactive data attribute for the stepper functionality.
+ $quantity_html = Utils::make_quantity_input_interactive( $quantity_html );
+
+ return $quantity_html;
}
/**
@@ -106,19 +96,13 @@ class AddToCartWithOptionsGroupedProductSelectorItemCTA extends AbstractBlock {
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ): string {
- $post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
-
global $product;
-
$previous_product = $product;
- if ( ! empty( $post_id ) ) {
- $product = wc_get_product( $post_id );
- }
-
- $markup = '';
+ $product = Utils::get_product_from_context( $block, $previous_product );
+ $markup = '';
- if ( $product instanceof \WC_Product ) {
+ if ( $product ) {
if ( ! $product->is_purchasable() || $product->has_options() || ! $product->is_in_stock() ) {
$markup = $this->get_button_markup( $product );
} elseif ( $product->is_sold_individually() ) {
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptionsQuantitySelector.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptionsQuantitySelector.php
index 7e3ebb6731..ae7c8889d3 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptionsQuantitySelector.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptionsQuantitySelector.php
@@ -3,6 +3,7 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Blocks\BlockTypes;
+use Automattic\WooCommerce\Blocks\BlockTypes\AddtoCartWithOptions\Utils;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
use Automattic\WooCommerce\Enums\ProductType;
@@ -20,83 +21,6 @@ class AddToCartWithOptionsQuantitySelector extends AbstractBlock {
*/
protected $block_name = 'add-to-cart-with-options-quantity-selector';
- /**
- * Get the block's attributes.
- *
- * @param array $attributes Block attributes. Default empty array.
- * @return array Block attributes merged with defaults.
- */
- private function parse_attributes( $attributes ) {
- // These should match what's set in JS `registerBlockType`.
- $defaults = array(
- 'quantitySelectorStyle' => 'input',
- );
-
- return wp_parse_args( $attributes, $defaults );
- }
-
-
- /**
- * Enqueue assets specific to this block.
- * We enqueue frontend scripts only if the quantitySelectorStyle is set to 'stepper'.
- *
- * @param array $attributes Block attributes.
- * @param string $content Block content.
- * @param WP_Block $block Block instance.
- */
- protected function enqueue_assets( $attributes, $content, $block ) {
- if ( ! isset( $attributes['quantitySelectorStyle'] ) || 'stepper' !== $attributes['quantitySelectorStyle'] ) {
- return;
- }
-
- parent::enqueue_assets( $attributes, $content, $block );
- }
-
- /**
- * Add increment and decrement buttons to the quantity input field.
- *
- * @param string $product_html Quantity input HTML.
- * @param string $product_name Product name.
- * @return stringa Quantity input HTML with increment and decrement buttons.
- */
- private function add_steppers( $product_html, $product_name ) {
- // Regex pattern to match the <input> element with id starting with 'quantity_'.
- $pattern = '/(<input[^>]*id="quantity_[^"]*"[^>]*\/>)/';
- // Replacement string to add button BEFORE the matched <input> element.
- /* translators: %s refers to the item name in the cart. */
- $minus_button = '<button aria-label="' . esc_attr( sprintf( __( 'Reduce quantity of %s', 'woocommerce' ), $product_name ) ) . '"type="button" data-wp-on--click="actions.removeQuantity" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">-</button>$1';
- // 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="actions.addQuantity" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button>';
- $new_html = preg_replace( $pattern, $minus_button, $product_html );
- $new_html = preg_replace( $pattern, $plus_button, $new_html );
- return $new_html;
- }
-
- /**
- * Add classes to the Quantity Selector needed for the stepper style.
- *
- * @param string $product_html The Quantity Selector HTML.
- *
- * @return string The Quantity Selector HTML with classes added.
- */
- private function add_stepper_classes( $product_html ) {
- $html = new \WP_HTML_Tag_Processor( $product_html );
-
- // Add classes to the form.
- while ( $html->next_tag( array( 'class_name' => 'quantity' ) ) ) {
- $html->add_class( 'wc-block-components-quantity-selector' );
- }
-
- $html = new \WP_HTML_Tag_Processor( $html->get_updated_html() );
- while ( $html->next_tag( array( 'class_name' => 'input-text' ) ) ) {
- $html->add_class( 'wc-block-components-quantity-selector__input' );
- }
-
- return $html->get_updated_html();
- }
-
-
/**
* Render the block.
*
@@ -116,13 +40,9 @@ class AddToCartWithOptionsQuantitySelector extends AbstractBlock {
global $product;
$previous_product = $product;
- // Try to load the product from the block context, if not available,
- // use the global $product.
- $post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
- $post = $post_id ? wc_get_product( $post_id ) : null;
- if ( $post instanceof \WC_Product ) {
- $product = $post;
- } elseif ( ! $product instanceof \WC_Product ) {
+ $product = Utils::get_product_from_context( $block, $previous_product );
+
+ if ( ! $product ) {
return '';
}
@@ -145,39 +65,17 @@ class AddToCartWithOptionsQuantitySelector extends AbstractBlock {
wp_enqueue_script_module( $this->get_full_block_name() );
- $is_stepper_style = isset( $attributes['quantitySelectorStyle'] ) && 'stepper' === $attributes['quantitySelectorStyle'];
-
ob_start();
- woocommerce_quantity_input(
- array(
- /**
- * Filter the minimum quantity value allowed for the product.
- *
- * @since 2.0.0
- * @param int $min_value Minimum quantity value.
- * @param WC_Product $product Product object.
- */
- 'min_value' => apply_filters( 'woocommerce_quantity_input_min', $product->get_min_purchase_quantity(), $product ),
- /**
- * Filter the maximum quantity value allowed for the product.
- *
- * @since 2.0.0
- * @param int $max_value Maximum quantity value.
- * @param WC_Product $product Product object.
- */
- 'max_value' => apply_filters( 'woocommerce_quantity_input_max', $product->get_max_purchase_quantity(), $product ),
- 'input_value' => isset( $_POST['quantity'] ) ? wc_stock_amount( wp_unslash( $_POST['quantity'] ) ) : $product->get_min_purchase_quantity(), // phpcs:ignore WordPress.Security.NonceVerification.Missing
- )
- );
+ woocommerce_quantity_input( Utils::get_quantity_input_args( $product ) );
$product_html = ob_get_clean();
$product_name = $product->get_name();
- $product_html = $is_stepper_style ? $this->add_steppers( $product_html, $product_name ) : $product_html;
- $parsed_attributes = $this->parse_attributes( $attributes );
- $product_html = $is_stepper_style ? $this->add_stepper_classes( $product_html ) : $product_html;
+ $product_html = Utils::add_quantity_steppers( $product_html, $product_name );
+ $product_html = Utils::add_quantity_stepper_classes( $product_html );
+
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'extra_classes' ) );
$classes = implode(
@@ -186,7 +84,6 @@ class AddToCartWithOptionsQuantitySelector extends AbstractBlock {
array(
'wp-block-add-to-cart-with-options-quantity-selector wc-block-add-to-cart-with-options__quantity-selector',
esc_attr( $classes_and_styles['classes'] ),
- $is_stepper_style ? 'wc-block-add-to-cart-with-options__quantity-selector--stepper' : 'wc-block-add-to-cart-with-options__quantity-selector--input',
)
)
);
@@ -198,12 +95,7 @@ class AddToCartWithOptionsQuantitySelector extends AbstractBlock {
)
);
- $form = sprintf(
- '<div %1$s %2$s>%3$s</div>',
- $wrapper_attributes,
- $is_stepper_style ? 'data-wp-interactive="woocommerce/add-to-cart-with-options"' : '',
- $product_html
- );
+ $form = Utils::make_quantity_input_interactive( $product_html, $wrapper_attributes );
$product = $previous_product;
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddtoCartWithOptions/Utils.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddtoCartWithOptions/Utils.php
new file mode 100644
index 0000000000..a28c8bd288
--- /dev/null
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddtoCartWithOptions/Utils.php
@@ -0,0 +1,123 @@
+<?php
+declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Blocks\BlockTypes\AddtoCartWithOptions;
+
+/**
+ * Utility methods used for the Add to Cart with Options block.
+ * {@internal This class and its methods are not intended for public use.}
+ */
+class Utils {
+ /**
+ * Add increment and decrement buttons to the quantity input field.
+ *
+ * @param string $quantity_html Quantity input HTML.
+ * @param string $product_name Product name.
+ * @return string Quantity input HTML with increment and decrement buttons.
+ */
+ public static function add_quantity_steppers( $quantity_html, $product_name ) {
+ // Regex pattern to match the <input> element with id starting with 'quantity_'.
+ $pattern = '/(<input[^>]*id="quantity_[^"]*"[^>]*\/>)/';
+ // Replacement string to add button BEFORE the matched <input> element.
+ /* translators: %s refers to the item name in the cart. */
+ $minus_button = '<button aria-label="' . esc_attr( sprintf( __( 'Reduce quantity of %s', 'woocommerce' ), $product_name ) ) . '"type="button" data-wp-on--click="actions.decreaseQuantity" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">-</button>$1';
+ // 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="actions.increaseQuantity" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button>';
+ $new_html = preg_replace( $pattern, $minus_button, $quantity_html );
+ $new_html = preg_replace( $pattern, $plus_button, $new_html );
+ return $new_html;
+ }
+
+ /**
+ * Add classes to the Quantity Selector needed for the stepper style.
+ *
+ * @param string $quantity_html The Quantity Selector HTML.
+ *
+ * @return string The Quantity Selector HTML with classes added.
+ */
+ public static function add_quantity_stepper_classes( $quantity_html ) {
+ $html = new \WP_HTML_Tag_Processor( $quantity_html );
+
+ // Add classes to the form.
+ while ( $html->next_tag( array( 'class_name' => 'quantity' ) ) ) {
+ $html->add_class( 'wc-block-components-quantity-selector' );
+ }
+
+ $html = new \WP_HTML_Tag_Processor( $html->get_updated_html() );
+ while ( $html->next_tag( array( 'class_name' => 'input-text' ) ) ) {
+ $html->add_class( 'wc-block-components-quantity-selector__input' );
+ }
+
+ return $html->get_updated_html();
+ }
+
+ /**
+ * Get standardized quantity input arguments for WooCommerce quantity input.
+ *
+ * @param \WC_Product $product The product object.
+ * @return array Arguments for woocommerce_quantity_input().
+ */
+ public static function get_quantity_input_args( $product ) {
+ return array(
+ /**
+ * Filter the minimum quantity value allowed for the product.
+ *
+ * @since 2.0.0
+ * @param int $min_value Minimum quantity value.
+ * @param WC_Product $product Product object.
+ */
+ 'min_value' => apply_filters( 'woocommerce_quantity_input_min', $product->get_min_purchase_quantity(), $product ),
+ /**
+ * Filter the maximum quantity value allowed for the product.
+ *
+ * @since 2.0.0
+ * @param int $max_value Maximum quantity value.
+ * @param WC_Product $product Product object.
+ */
+ 'max_value' => apply_filters( 'woocommerce_quantity_input_max', $product->get_max_purchase_quantity(), $product ),
+ 'input_value' => isset( $_POST['quantity'] ) ? wc_stock_amount( wp_unslash( $_POST['quantity'] ) ) : $product->get_min_purchase_quantity(), // phpcs:ignore WordPress.Security.NonceVerification.Missing
+ );
+ }
+
+ /**
+ * Make the quantity input interactive by wrapping it with the necessary data attribute.
+ *
+ * @param string $quantity_html The quantity HTML.
+ * @param string $wrapper_attributes Optional wrapper attributes.
+ * @return string The quantity HTML with interactive wrapper.
+ */
+ public static function make_quantity_input_interactive( $quantity_html, $wrapper_attributes = '' ) {
+ if ( ! empty( $wrapper_attributes ) ) {
+ return sprintf(
+ '<div %1$s data-wp-interactive="woocommerce/add-to-cart-with-options">%2$s</div>',
+ $wrapper_attributes,
+ $quantity_html
+ );
+ }
+
+ return '<div data-wp-interactive="woocommerce/add-to-cart-with-options">' . $quantity_html . '</div>';
+ }
+
+ /**
+ * Get product from block context.
+ *
+ * @param \WP_Block $block The block instance.
+ * @param \WC_Product|null $previous_product The previous product (usually from global scope).
+ * @return \WC_Product|null The product instance or null if not found.
+ */
+ public static function get_product_from_context( $block, $previous_product ) {
+ $post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
+ $product = null;
+
+ if ( ! empty( $post_id ) ) {
+ $product = wc_get_product( $post_id );
+ }
+
+ if ( ! $product instanceof \WC_Product && $previous_product instanceof \WC_Product ) {
+ $product = $previous_product;
+ }
+
+ return $product instanceof \WC_Product ? $product : null;
+ }
+}