From b63e502f34bcdac5971d571ff4587c8d886ce384 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Wed, 27 Oct 2021 12:26:30 +0100 Subject: [PATCH 01/17] add min and step to Store API --- bin/hook-docs/data/filters.json | 54 ++++++++++++++++++++++++ docs/extensibility/filters.md | 56 +++++++++++++++++++++++++ src/StoreApi/Schemas/CartItemSchema.php | 46 ++++++++++++++++++++ 3 files changed, 156 insertions(+) diff --git a/bin/hook-docs/data/filters.json b/bin/hook-docs/data/filters.json index 7c319bbd862..9b3f3a9e24a 100644 --- a/bin/hook-docs/data/filters.json +++ b/bin/hook-docs/data/filters.json @@ -814,6 +814,60 @@ }, "args": 1 }, + { + "name": "woocommerce_store_api_item_quantity_increment", + "file": "StoreApi/Schemas/CartItemSchema.php", + "type": "filter", + "doc": { + "description": "Filters the quantity increment for a cart item in Store API.", + "long_description": "", + "tags": [ + { + "name": "param", + "content": "Cart item array.", + "types": [ + "array" + ], + "variable": "$cart_item" + }, + { + "name": "return", + "content": "", + "types": [ + "\\Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\number" + ] + } + ], + "long_description_html": "" + } + }, + { + "name": "woocommerce_store_api_item_quantity_minimum", + "file": "StoreApi/Schemas/CartItemSchema.php", + "type": "filter", + "doc": { + "description": "Filters the quantity minimum for a cart item in Store API.", + "long_description": "", + "tags": [ + { + "name": "param", + "content": "Cart item array.", + "types": [ + "array" + ], + "variable": "$cart_item" + }, + { + "name": "return", + "content": "", + "types": [ + "\\Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\number" + ] + } + ], + "long_description_html": "" + } + }, { "name": "woocommerce_store_api_product_quantity_limit", "file": "StoreApi/Schemas/ProductSchema.php", diff --git a/docs/extensibility/filters.md b/docs/extensibility/filters.md index 4a47c98f11c..5b13fb42a49 100644 --- a/docs/extensibility/filters.md +++ b/docs/extensibility/filters.md @@ -29,6 +29,8 @@ - [woocommerce_shipping_package_name](#woocommerce_shipping_package_name) - [woocommerce_show_page_title](#woocommerce_show_page_title) - [woocommerce_store_api_disable_nonce_check](#woocommerce_store_api_disable_nonce_check) + - [woocommerce_store_api_item_quantity_increment](#woocommerce_store_api_item_quantity_increment) + - [woocommerce_store_api_item_quantity_minimum](#woocommerce_store_api_item_quantity_minimum) - [woocommerce_store_api_product_quantity_limit](#woocommerce_store_api_product_quantity_limit) - [woocommerce_variation_option_name](#woocommerce_variation_option_name) @@ -692,6 +694,60 @@ apply_filters( 'woocommerce_store_api_disable_nonce_check', boolean $disable_non --- +## woocommerce_store_api_item_quantity_increment + + +Filters the quantity increment for a cart item in Store API. + +```php +apply_filters( 'woocommerce_store_api_item_quantity_increment', array $cart_item ) +``` + +### Parameters + +| Argument | Type | Description | +| -------- | ---- | ----------- | +| $cart_item | array | Cart item array. | + +### Returns + + +`\Automattic\WooCommerce\Blocks\StoreApi\Schemas\number` + +### Source + + +File: [StoreApi/Schemas/CartItemSchema.php](../src/StoreApi/Schemas/CartItemSchema.php) + +--- + +## woocommerce_store_api_item_quantity_minimum + + +Filters the quantity minimum for a cart item in Store API. + +```php +apply_filters( 'woocommerce_store_api_item_quantity_minimum', array $cart_item ) +``` + +### Parameters + +| Argument | Type | Description | +| -------- | ---- | ----------- | +| $cart_item | array | Cart item array. | + +### Returns + + +`\Automattic\WooCommerce\Blocks\StoreApi\Schemas\number` + +### Source + + +File: [StoreApi/Schemas/CartItemSchema.php](../src/StoreApi/Schemas/CartItemSchema.php) + +--- + ## woocommerce_store_api_product_quantity_limit diff --git a/src/StoreApi/Schemas/CartItemSchema.php b/src/StoreApi/Schemas/CartItemSchema.php index f384d970dff..c431735e50c 100644 --- a/src/StoreApi/Schemas/CartItemSchema.php +++ b/src/StoreApi/Schemas/CartItemSchema.php @@ -57,6 +57,18 @@ public function get_properties() { 'context' => [ 'view', 'edit' ], 'readonly' => true, ], + 'quantity_min' => [ + 'description' => __( 'The minimum quantity than can be added to the cart at once.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], + 'quantity_increment' => [ + 'description' => __( 'The amount quantity can change with.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], 'name' => [ 'description' => __( 'Product name.', 'woo-gutenberg-products-block' ), 'type' => 'string', @@ -314,6 +326,8 @@ public function get_item_response( $cart_item ) { 'id' => $product->get_id(), 'quantity' => wc_stock_amount( $cart_item['quantity'] ), 'quantity_limit' => $this->get_product_quantity_limit( $product ), + 'quantity_min' => $this->get_item_quantity_min( $cart_item ), + 'quantity_increment' => $this->get_item_quantity_increment( $cart_item ), 'name' => $this->prepare_html_response( $product->get_title() ), 'short_description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_short_description() ) ) ), 'description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_description() ) ) ), @@ -451,6 +465,38 @@ protected function get_item_data( $cart_item ) { return array_map( [ $this, 'format_item_data_element' ], $item_data ); } + /** + * Return the minimum quantity that's allowed for this item. + * + * @param array $cart_item Cart item array. + * @return number + */ + protected function get_item_quantity_min( $cart_item ) { + /** + * Filters the quantity minimum for a cart item in Store API. + * + * @param array $cart_item Cart item array. + * @return number + */ + return apply_filters( 'woocommerce_store_api_item_quantity_minimum', 1, $cart_item ); + } + + /** + * Return the increment quantity that an item is allowed to change with. + * + * @param array $cart_item Cart item array. + * @return number + */ + protected function get_item_quantity_increment( $cart_item ) { + /** + * Filters the quantity increment for a cart item in Store API. + * + * @param array $cart_item Cart item array. + * @return number + */ + return apply_filters( 'woocommerce_store_api_item_quantity_increment', 1, $cart_item ); + } + /** * Remove HTML tags from cart item data and set the `hidden` property to * `__experimental_woocommerce_blocks_hidden`. From 6ecec1f6301f594cb92c41a5728177a6dbd6a226 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Thu, 28 Oct 2021 17:50:03 +0100 Subject: [PATCH 02/17] add min and step support --- .../components/quantity-selector/index.tsx | 80 ++++++++++++++----- .../cart-line-item-row.tsx | 4 + assets/js/types/type-defs/cart.ts | 2 + bin/hook-docs/data/filters.json | 8 +- docs/extensibility/filters.md | 14 ++-- src/StoreApi/Schemas/CartItemSchema.php | 6 +- 6 files changed, 82 insertions(+), 32 deletions(-) diff --git a/assets/js/base/components/quantity-selector/index.tsx b/assets/js/base/components/quantity-selector/index.tsx index 516740236c4..b2a0c250de0 100644 --- a/assets/js/base/components/quantity-selector/index.tsx +++ b/assets/js/base/components/quantity-selector/index.tsx @@ -6,6 +6,7 @@ import { speak } from '@wordpress/a11y'; import classNames from 'classnames'; import { useCallback } from '@wordpress/element'; import { DOWN, UP } from '@wordpress/keycodes'; +import useDebouncedCallback from 'use-debounce/lib/useDebouncedCallback'; /** * Internal dependencies @@ -31,6 +32,10 @@ export interface QuantitySelectorProps { * Maximum quantity */ maximum: number; + /** + * Input step attribute. + */ + step?: number; /** * Event handler triggered when the quantity is changed */ @@ -53,6 +58,7 @@ const QuantitySelector = ( { minimum = 1, maximum, onChange = () => void 0, + step = 1, itemName = '', disabled, }: QuantitySelectorProps ): JSX.Element => { @@ -62,8 +68,46 @@ const QuantitySelector = ( { ); const hasMaximum = typeof maximum !== 'undefined'; - const canDecrease = quantity > minimum; - const canIncrease = ! hasMaximum || quantity < maximum; + const canDecrease = quantity - step > minimum; + const canIncrease = ! hasMaximum || quantity + step < maximum; + + /** + * The goal of this function is to normalize what was inserted, + * but after the customer has stopped typing. + * + * It's important to wait before normalizing or we end up with + * a frustrating expiernce, for example, if the minimun is 2 and + * the customer is trying to type "10", premature normalizing would + * always kick in at "1" and turn that into 2. + */ + const [ normalizeQuantity ] = useDebouncedCallback( + ( initialValue: number ) => { + // We copy the starting value. + let value = initialValue; + + // We check if we have a maximum value, and select the lowest between what was inserted, and the maximum. + if ( hasMaximum ) { + value = Math.min( + value, + // the maximum possible value in step increments. + Math.floor( maximum / step ) * step + ); + } + + // Select the biggest between what's inserted, the the minimum value in steps. + value = Math.max( value, Math.ceil( minimum / step ) * step ); + + // We round off the value to our steps. + value = Math.floor( value / step ) * step; + + // Only commit if the value has changed + if ( value !== initialValue ) { + onChange( value ); + } + }, + // This value is delibrtly smaller than what's in useStoreCartItemQuantity so we don't end up with two requests. + 300 + ); /** * Handles keyboard up and down keys to change quantity value. @@ -83,15 +127,15 @@ const QuantitySelector = ( { if ( isArrowDown && canDecrease ) { event.preventDefault(); - onChange( quantity - 1 ); + onChange( quantity - step ); } if ( isArrowUp && canIncrease ) { event.preventDefault(); - onChange( quantity + 1 ); + onChange( quantity + step ); } }, - [ quantity, onChange, canIncrease, canDecrease ] + [ quantity, onChange, canIncrease, canDecrease, step ] ); return ( @@ -100,22 +144,22 @@ const QuantitySelector = ( { className="wc-block-components-quantity-selector__input" disabled={ disabled } type="number" - step="1" - min="0" + step={ step } + min={ minimum } value={ quantity } onKeyDown={ quantityInputOnKeyDown } onChange={ ( event ) => { - let value = - Number.isNaN( event.target.value ) || - ! event.target.value - ? 0 - : parseInt( event.target.value, 10 ); - if ( hasMaximum ) { - value = Math.min( value, maximum ); - } - value = Math.max( value, minimum ); + // Inputs values are strings, we parse them here. + let value = parseInt( event.target.value, 10 ); + // parseInt would throw NaN for anything not a number, + // so we revert value to the quantity value. + value = isNaN( value ) ? quantity : value; + if ( value !== quantity ) { + // we commit this value immideatly. onChange( value ); + // but once the customer has stopped typing, we make sure his value is respecting the bounds (maximum value, minimum value, step value), and commit the normlized value. + normalizeQuantity( value ); } } } aria-label={ sprintf( @@ -135,7 +179,7 @@ const QuantitySelector = ( { className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus" disabled={ disabled || ! canDecrease } onClick={ () => { - const newQuantity = quantity - 1; + const newQuantity = quantity - step; onChange( newQuantity ); speak( sprintf( @@ -159,7 +203,7 @@ const QuantitySelector = ( { disabled={ disabled || ! canIncrease } className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus" onClick={ () => { - const newQuantity = quantity + 1; + const newQuantity = quantity + step; onChange( newQuantity ); speak( sprintf( diff --git a/assets/js/blocks/cart-checkout/cart/cart-line-items-table/cart-line-item-row.tsx b/assets/js/blocks/cart-checkout/cart/cart-line-items-table/cart-line-item-row.tsx index 94bc2b0e05d..b6b4c3ab27f 100644 --- a/assets/js/blocks/cart-checkout/cart/cart-line-items-table/cart-line-item-row.tsx +++ b/assets/js/blocks/cart-checkout/cart/cart-line-items-table/cart-line-item-row.tsx @@ -71,6 +71,8 @@ const CartLineItemRow = forwardRef< HTMLTableRowElement, CartLineItemRowProps >( low_stock_remaining: lowStockRemaining = null, show_backorder_badge: showBackorderBadge = false, quantity_limit: quantityLimit = 99, + quantity_min: quantityMin = 1, + quantity_step: quantityStep = 1, permalink = '', images = [], variation = [], @@ -282,6 +284,8 @@ const CartLineItemRow = forwardRef< HTMLTableRowElement, CartLineItemRowProps >( disabled={ isPendingDelete } quantity={ quantity } maximum={ quantityLimit } + minimum={ quantityMin } + step={ quantityStep } onChange={ ( newQuantity ) => { setItemQuantity( newQuantity ); dispatchStoreEvent( 'cart-set-item-quantity', { diff --git a/assets/js/types/type-defs/cart.ts b/assets/js/types/type-defs/cart.ts index 31d17c53fea..59b4c80c562 100644 --- a/assets/js/types/type-defs/cart.ts +++ b/assets/js/types/type-defs/cart.ts @@ -118,6 +118,8 @@ export interface CartItem { quantity: number; catalog_visibility: CatalogVisibility; quantity_limit: number; + quantity_min: number; + quantity_step: number; name: string; summary: string; short_description: string; diff --git a/bin/hook-docs/data/filters.json b/bin/hook-docs/data/filters.json index 9b3f3a9e24a..379eb2cacdf 100644 --- a/bin/hook-docs/data/filters.json +++ b/bin/hook-docs/data/filters.json @@ -815,11 +815,11 @@ "args": 1 }, { - "name": "woocommerce_store_api_item_quantity_increment", + "name": "woocommerce_store_api_item_quantity_minimum", "file": "StoreApi/Schemas/CartItemSchema.php", "type": "filter", "doc": { - "description": "Filters the quantity increment for a cart item in Store API.", + "description": "Filters the quantity minimum for a cart item in Store API.", "long_description": "", "tags": [ { @@ -842,11 +842,11 @@ } }, { - "name": "woocommerce_store_api_item_quantity_minimum", + "name": "woocommerce_store_api_item_quantity_step", "file": "StoreApi/Schemas/CartItemSchema.php", "type": "filter", "doc": { - "description": "Filters the quantity minimum for a cart item in Store API.", + "description": "Filters the quantity increment for a cart item in Store API.", "long_description": "", "tags": [ { diff --git a/docs/extensibility/filters.md b/docs/extensibility/filters.md index 5b13fb42a49..7a87746fbcf 100644 --- a/docs/extensibility/filters.md +++ b/docs/extensibility/filters.md @@ -29,8 +29,8 @@ - [woocommerce_shipping_package_name](#woocommerce_shipping_package_name) - [woocommerce_show_page_title](#woocommerce_show_page_title) - [woocommerce_store_api_disable_nonce_check](#woocommerce_store_api_disable_nonce_check) - - [woocommerce_store_api_item_quantity_increment](#woocommerce_store_api_item_quantity_increment) - [woocommerce_store_api_item_quantity_minimum](#woocommerce_store_api_item_quantity_minimum) + - [woocommerce_store_api_item_quantity_step](#woocommerce_store_api_item_quantity_step) - [woocommerce_store_api_product_quantity_limit](#woocommerce_store_api_product_quantity_limit) - [woocommerce_variation_option_name](#woocommerce_variation_option_name) @@ -694,13 +694,13 @@ apply_filters( 'woocommerce_store_api_disable_nonce_check', boolean $disable_non --- -## woocommerce_store_api_item_quantity_increment +## woocommerce_store_api_item_quantity_minimum -Filters the quantity increment for a cart item in Store API. +Filters the quantity minimum for a cart item in Store API. ```php -apply_filters( 'woocommerce_store_api_item_quantity_increment', array $cart_item ) +apply_filters( 'woocommerce_store_api_item_quantity_minimum', array $cart_item ) ``` ### Parameters @@ -721,13 +721,13 @@ File: [StoreApi/Schemas/CartItemSchema.php](../src/StoreApi/Schemas/CartItemSche --- -## woocommerce_store_api_item_quantity_minimum +## woocommerce_store_api_item_quantity_step -Filters the quantity minimum for a cart item in Store API. +Filters the quantity increment for a cart item in Store API. ```php -apply_filters( 'woocommerce_store_api_item_quantity_minimum', array $cart_item ) +apply_filters( 'woocommerce_store_api_item_quantity_step', array $cart_item ) ``` ### Parameters diff --git a/src/StoreApi/Schemas/CartItemSchema.php b/src/StoreApi/Schemas/CartItemSchema.php index c431735e50c..413fc871ef4 100644 --- a/src/StoreApi/Schemas/CartItemSchema.php +++ b/src/StoreApi/Schemas/CartItemSchema.php @@ -327,7 +327,7 @@ public function get_item_response( $cart_item ) { 'quantity' => wc_stock_amount( $cart_item['quantity'] ), 'quantity_limit' => $this->get_product_quantity_limit( $product ), 'quantity_min' => $this->get_item_quantity_min( $cart_item ), - 'quantity_increment' => $this->get_item_quantity_increment( $cart_item ), + 'quantity_step' => $this->get_item_quantity_step( $cart_item ), 'name' => $this->prepare_html_response( $product->get_title() ), 'short_description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_short_description() ) ) ), 'description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_description() ) ) ), @@ -487,14 +487,14 @@ protected function get_item_quantity_min( $cart_item ) { * @param array $cart_item Cart item array. * @return number */ - protected function get_item_quantity_increment( $cart_item ) { + protected function get_item_quantity_step( $cart_item ) { /** * Filters the quantity increment for a cart item in Store API. * * @param array $cart_item Cart item array. * @return number */ - return apply_filters( 'woocommerce_store_api_item_quantity_increment', 1, $cart_item ); + return apply_filters( 'woocommerce_store_api_item_quantity_step', 1, $cart_item ); } /** From 4058acd9979577766da05bed35ee0155292168c2 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Thu, 28 Oct 2021 19:09:56 +0100 Subject: [PATCH 03/17] typo --- src/StoreApi/Schemas/CartItemSchema.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StoreApi/Schemas/CartItemSchema.php b/src/StoreApi/Schemas/CartItemSchema.php index 413fc871ef4..7d54cb96398 100644 --- a/src/StoreApi/Schemas/CartItemSchema.php +++ b/src/StoreApi/Schemas/CartItemSchema.php @@ -63,7 +63,7 @@ public function get_properties() { 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'quantity_increment' => [ + 'quantity_step' => [ 'description' => __( 'The amount quantity can change with.', 'woo-gutenberg-products-block' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], From 5529eb0fe7aa3e1856c5a22391b4651292d8a15f Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Wed, 15 Dec 2021 11:08:32 +0000 Subject: [PATCH 04/17] Update assets/js/base/components/quantity-selector/index.tsx --- assets/js/base/components/quantity-selector/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/base/components/quantity-selector/index.tsx b/assets/js/base/components/quantity-selector/index.tsx index b2a0c250de0..08117014c29 100644 --- a/assets/js/base/components/quantity-selector/index.tsx +++ b/assets/js/base/components/quantity-selector/index.tsx @@ -76,7 +76,7 @@ const QuantitySelector = ( { * but after the customer has stopped typing. * * It's important to wait before normalizing or we end up with - * a frustrating expiernce, for example, if the minimun is 2 and + * a frustrating experience, for example, if the minimum is 2 and * the customer is trying to type "10", premature normalizing would * always kick in at "1" and turn that into 2. */ From 512e6e8554c47be58ba67e265ae64604183fb7fd Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Wed, 15 Dec 2021 11:08:39 +0000 Subject: [PATCH 05/17] Update assets/js/base/components/quantity-selector/index.tsx --- assets/js/base/components/quantity-selector/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/base/components/quantity-selector/index.tsx b/assets/js/base/components/quantity-selector/index.tsx index 08117014c29..57c849a0011 100644 --- a/assets/js/base/components/quantity-selector/index.tsx +++ b/assets/js/base/components/quantity-selector/index.tsx @@ -156,7 +156,7 @@ const QuantitySelector = ( { value = isNaN( value ) ? quantity : value; if ( value !== quantity ) { - // we commit this value immideatly. + // we commit this value immediately. onChange( value ); // but once the customer has stopped typing, we make sure his value is respecting the bounds (maximum value, minimum value, step value), and commit the normlized value. normalizeQuantity( value ); From f9b2119dbb47502b803e58f991c05739c753faaa Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Wed, 15 Dec 2021 11:58:35 +0000 Subject: [PATCH 06/17] Fix debounce callback --- assets/js/base/components/quantity-selector/index.tsx | 6 +++--- src/StoreApi/Schemas/CartItemSchema.php | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/assets/js/base/components/quantity-selector/index.tsx b/assets/js/base/components/quantity-selector/index.tsx index 57c849a0011..aa4b5e541be 100644 --- a/assets/js/base/components/quantity-selector/index.tsx +++ b/assets/js/base/components/quantity-selector/index.tsx @@ -6,7 +6,7 @@ import { speak } from '@wordpress/a11y'; import classNames from 'classnames'; import { useCallback } from '@wordpress/element'; import { DOWN, UP } from '@wordpress/keycodes'; -import useDebouncedCallback from 'use-debounce/lib/useDebouncedCallback'; +import { useDebouncedCallback } from 'use-debounce'; /** * Internal dependencies @@ -80,7 +80,7 @@ const QuantitySelector = ( { * the customer is trying to type "10", premature normalizing would * always kick in at "1" and turn that into 2. */ - const [ normalizeQuantity ] = useDebouncedCallback( + const normalizeQuantity = useDebouncedCallback( ( initialValue: number ) => { // We copy the starting value. let value = initialValue; @@ -105,7 +105,7 @@ const QuantitySelector = ( { onChange( value ); } }, - // This value is delibrtly smaller than what's in useStoreCartItemQuantity so we don't end up with two requests. + // This value is deliberately smaller than what's in useStoreCartItemQuantity so we don't end up with two requests. 300 ); diff --git a/src/StoreApi/Schemas/CartItemSchema.php b/src/StoreApi/Schemas/CartItemSchema.php index 7d54cb96398..83580625cfe 100644 --- a/src/StoreApi/Schemas/CartItemSchema.php +++ b/src/StoreApi/Schemas/CartItemSchema.php @@ -498,8 +498,7 @@ protected function get_item_quantity_step( $cart_item ) { } /** - * Remove HTML tags from cart item data and set the `hidden` property to - * `__experimental_woocommerce_blocks_hidden`. + * Remove HTML tags from cart item data and set the `hidden` property to `__experimental_woocommerce_blocks_hidden`. * * @param array $item_data_element Individual element of a cart item data. * @return array From 835058af0ebd3d20391e858f5979d2125a2b2232 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Wed, 15 Dec 2021 15:16:29 +0000 Subject: [PATCH 07/17] Style qty input to show steps --- assets/js/base/components/quantity-selector/index.tsx | 4 ++-- assets/js/base/components/quantity-selector/style.scss | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/assets/js/base/components/quantity-selector/index.tsx b/assets/js/base/components/quantity-selector/index.tsx index aa4b5e541be..fc37680cd9d 100644 --- a/assets/js/base/components/quantity-selector/index.tsx +++ b/assets/js/base/components/quantity-selector/index.tsx @@ -193,7 +193,7 @@ const QuantitySelector = ( { ); } } > - - + { step === 1 || quantity < step ? `-` : quantity - step } ); diff --git a/assets/js/base/components/quantity-selector/style.scss b/assets/js/base/components/quantity-selector/style.scss index 3a7095e9eea..a2ab45f775f 100644 --- a/assets/js/base/components/quantity-selector/style.scss +++ b/assets/js/base/components/quantity-selector/style.scss @@ -41,6 +41,7 @@ line-height: 1; vertical-align: middle; -moz-appearance: textfield; + font-weight: 600; &:focus { background: $gray-100; @@ -70,11 +71,12 @@ .wc-block-components-quantity-selector__button { @include reset-button; - @include font-size(regular); + @include font-size(regular, 0.9em); min-width: 30px; cursor: pointer; - color: $gray-900; + color: $gray-600; font-style: normal; + font-weight: normal; text-align: center; text-decoration: none; From 434931734acdb1424f9f6f024b1fa796b468ba70 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Wed, 15 Dec 2021 17:53:31 +0000 Subject: [PATCH 08/17] Implement quantity_limits in API --- .../components/quantity-selector/index.tsx | 3 +- .../cart-line-item-row.tsx | 43 +++--- src/StoreApi/Routes/CartAddItem.php | 10 +- src/StoreApi/Schemas/CartItemSchema.php | 131 +++++++++++------- 4 files changed, 111 insertions(+), 76 deletions(-) diff --git a/assets/js/base/components/quantity-selector/index.tsx b/assets/js/base/components/quantity-selector/index.tsx index fc37680cd9d..94d3ca7a61b 100644 --- a/assets/js/base/components/quantity-selector/index.tsx +++ b/assets/js/base/components/quantity-selector/index.tsx @@ -69,7 +69,7 @@ const QuantitySelector = ( { const hasMaximum = typeof maximum !== 'undefined'; const canDecrease = quantity - step > minimum; - const canIncrease = ! hasMaximum || quantity + step < maximum; + const canIncrease = ! hasMaximum || quantity + step <= maximum; /** * The goal of this function is to normalize what was inserted, @@ -146,6 +146,7 @@ const QuantitySelector = ( { type="number" step={ step } min={ minimum } + max={ maximum } value={ quantity } onKeyDown={ quantityInputOnKeyDown } onChange={ ( event ) => { diff --git a/assets/js/blocks/cart-checkout/cart/cart-line-items-table/cart-line-item-row.tsx b/assets/js/blocks/cart-checkout/cart/cart-line-items-table/cart-line-item-row.tsx index b6b4c3ab27f..8c1fa5445bc 100644 --- a/assets/js/blocks/cart-checkout/cart/cart-line-items-table/cart-line-item-row.tsx +++ b/assets/js/blocks/cart-checkout/cart/cart-line-items-table/cart-line-item-row.tsx @@ -70,9 +70,11 @@ const CartLineItemRow = forwardRef< HTMLTableRowElement, CartLineItemRowProps >( description: fullDescription = '', low_stock_remaining: lowStockRemaining = null, show_backorder_badge: showBackorderBadge = false, - quantity_limit: quantityLimit = 99, - quantity_min: quantityMin = 1, - quantity_step: quantityStep = 1, + quantity_limits: quantityLimits = { + minimum: 1, + maximum: 99, + multipleOf: 1, + }, permalink = '', images = [], variation = [], @@ -280,21 +282,26 @@ const CartLineItemRow = forwardRef< HTMLTableRowElement, CartLineItemRowProps >( />
- { - setItemQuantity( newQuantity ); - dispatchStoreEvent( 'cart-set-item-quantity', { - product: lineItem, - quantity: newQuantity, - } ); - } } - itemName={ name } - /> + { !! quantityLimits && ( + { + setItemQuantity( newQuantity ); + dispatchStoreEvent( + 'cart-set-item-quantity', + { + product: lineItem, + quantity: newQuantity, + } + ); + } } + itemName={ name } + /> + ) }
); From 75c4e200becd6fb9f941c66c188db699cb8f4a1b Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 21 Dec 2021 14:46:56 +0000 Subject: [PATCH 16/17] Normalize on mount --- .../components/quantity-selector/index.tsx | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/assets/js/base/components/quantity-selector/index.tsx b/assets/js/base/components/quantity-selector/index.tsx index f84f795bb75..1076c3c6dd4 100644 --- a/assets/js/base/components/quantity-selector/index.tsx +++ b/assets/js/base/components/quantity-selector/index.tsx @@ -4,7 +4,7 @@ import { __, sprintf } from '@wordpress/i18n'; import { speak } from '@wordpress/a11y'; import classNames from 'classnames'; -import { useCallback } from '@wordpress/element'; +import { useCallback, useLayoutEffect } from '@wordpress/element'; import { DOWN, UP } from '@wordpress/keycodes'; import { useDebouncedCallback } from 'use-debounce'; @@ -74,13 +74,8 @@ const QuantitySelector = ( { /** * The goal of this function is to normalize what was inserted, * but after the customer has stopped typing. - * - * It's important to wait before normalizing or we end up with - * a frustrating experience, for example, if the minimum is 2 and - * the customer is trying to type "10", premature normalizing would - * always kick in at "1" and turn that into 2. */ - const normalizeQuantity = useDebouncedCallback( + const normalizeQuantity = useCallback( ( initialValue: number ) => { // We copy the starting value. let value = initialValue; @@ -105,10 +100,28 @@ const QuantitySelector = ( { onChange( value ); } }, + [ hasMaximum, maximum, minimum, onChange, step ] + ); + + /* + * It's important to wait before normalizing or we end up with + * a frustrating experience, for example, if the minimum is 2 and + * the customer is trying to type "10", premature normalizing would + * always kick in at "1" and turn that into 2. + */ + const debouncedNormalizeQuantity = useDebouncedCallback( + normalizeQuantity, // This value is deliberately smaller than what's in useStoreCartItemQuantity so we don't end up with two requests. 300 ); + /** + * Normalize qty on mount before render. + */ + useLayoutEffect( () => { + normalizeQuantity( quantity ); + }, [ quantity, normalizeQuantity ] ); + /** * Handles keyboard up and down keys to change quantity value. * @@ -160,7 +173,7 @@ const QuantitySelector = ( { // we commit this value immediately. onChange( value ); // but once the customer has stopped typing, we make sure his value is respecting the bounds (maximum value, minimum value, step value), and commit the normalized value. - normalizeQuantity( value ); + debouncedNormalizeQuantity( value ); } } } aria-label={ sprintf( From 27315f95c746f7dc76db339387637e3a6b4d70ae Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Tue, 11 Jan 2022 10:54:31 +0000 Subject: [PATCH 17/17] Update docs --- bin/hook-docs/data/filters.json | 77 +++++++++++++++------------------ docs/extensibility/filters.md | 60 ++++++++----------------- 2 files changed, 53 insertions(+), 84 deletions(-) diff --git a/bin/hook-docs/data/filters.json b/bin/hook-docs/data/filters.json index 379eb2cacdf..178329e58a3 100644 --- a/bin/hook-docs/data/filters.json +++ b/bin/hook-docs/data/filters.json @@ -815,94 +815,85 @@ "args": 1 }, { - "name": "woocommerce_store_api_item_quantity_minimum", - "file": "StoreApi/Schemas/CartItemSchema.php", + "name": "woocommerce_store_api_product_quantity_limit", + "file": "StoreApi/Utilities/QuantityLimits.php", "type": "filter", "doc": { - "description": "Filters the quantity minimum for a cart item in Store API.", - "long_description": "", + "description": "Filters the quantity limit for a product being added to the cart via the Store API.", + "long_description": "Filters the variation option name for custom option slugs.", "tags": [ { "name": "param", - "content": "Cart item array.", + "content": "Quantity limit which defaults to 99 unless sold individually.", "types": [ - "array" + "integer" ], - "variable": "$cart_item" + "variable": "$quantity_limit" }, - { - "name": "return", - "content": "", - "types": [ - "\\Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\number" - ] - } - ], - "long_description_html": "" - } - }, - { - "name": "woocommerce_store_api_item_quantity_step", - "file": "StoreApi/Schemas/CartItemSchema.php", - "type": "filter", - "doc": { - "description": "Filters the quantity increment for a cart item in Store API.", - "long_description": "", - "tags": [ { "name": "param", - "content": "Cart item array.", + "content": "Product instance.", "types": [ - "array" + "\\WC_Product" ], - "variable": "$cart_item" + "variable": "$product" }, { "name": "return", "content": "", "types": [ - "\\Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\number" + "integer" ] } ], - "long_description_html": "" - } + "long_description_html": "

Filters the variation option name for custom option slugs.

" + }, + "args": 2 }, { - "name": "woocommerce_store_api_product_quantity_limit", - "file": "StoreApi/Schemas/ProductSchema.php", + "name": "woocommerce_store_api_product_quantity_{$value_type}", + "file": "StoreApi/Utilities/QuantityLimits.php", "type": "filter", "doc": { - "description": "Filters the quantity limit for a product being added to the cart via the Store API.", - "long_description": "Filters the variation option name for custom option slugs.", + "description": "Filters the quantity minimum for a cart item in Store API. This allows extensions to control the minimum qty of items already within the cart.", + "long_description": "The suffix of the hook will vary depending on the value being filtered. For example, minimum, maximum, multiple_of, editable.", "tags": [ { "name": "param", - "content": "Quantity limit which defaults to 99 unless sold individually.", + "content": "The value being filtered.", "types": [ - "integer" + "mixed" ], - "variable": "$quantity_limit" + "variable": "$value" }, { "name": "param", - "content": "Product instance.", + "content": "The product object.", "types": [ "\\WC_Product" ], "variable": "$product" }, + { + "name": "param", + "content": "The cart item if the product exists in the cart, or null.", + "types": [ + "array", + "null" + ], + "variable": "$cart_item" + }, { "name": "return", "content": "", "types": [ - "integer" + "mixed" ] } ], - "long_description_html": "

Filters the variation option name for custom option slugs.

" + "long_description_html": "

The suffix of the hook will vary depending on the value being filtered. For example, minimum, maximum, multiple_of, editable.

" }, - "args": 2 + "args": 3 }, { "name": "woocommerce_variation_option_name", diff --git a/docs/extensibility/filters.md b/docs/extensibility/filters.md index 7a87746fbcf..db690f3e034 100644 --- a/docs/extensibility/filters.md +++ b/docs/extensibility/filters.md @@ -29,9 +29,8 @@ - [woocommerce_shipping_package_name](#woocommerce_shipping_package_name) - [woocommerce_show_page_title](#woocommerce_show_page_title) - [woocommerce_store_api_disable_nonce_check](#woocommerce_store_api_disable_nonce_check) - - [woocommerce_store_api_item_quantity_minimum](#woocommerce_store_api_item_quantity_minimum) - - [woocommerce_store_api_item_quantity_step](#woocommerce_store_api_item_quantity_step) - [woocommerce_store_api_product_quantity_limit](#woocommerce_store_api_product_quantity_limit) + - [woocommerce_store_api_product_quantity_{$value_type}](#woocommerce_store_api_product_quantity_-value_type) - [woocommerce_variation_option_name](#woocommerce_variation_option_name) --- @@ -694,89 +693,68 @@ apply_filters( 'woocommerce_store_api_disable_nonce_check', boolean $disable_non --- -## woocommerce_store_api_item_quantity_minimum +## woocommerce_store_api_product_quantity_limit -Filters the quantity minimum for a cart item in Store API. +Filters the quantity limit for a product being added to the cart via the Store API. ```php -apply_filters( 'woocommerce_store_api_item_quantity_minimum', array $cart_item ) +apply_filters( 'woocommerce_store_api_product_quantity_limit', integer $quantity_limit, \WC_Product $product ) ``` -### Parameters - -| Argument | Type | Description | -| -------- | ---- | ----------- | -| $cart_item | array | Cart item array. | - -### Returns - - -`\Automattic\WooCommerce\Blocks\StoreApi\Schemas\number` - -### Source - - -File: [StoreApi/Schemas/CartItemSchema.php](../src/StoreApi/Schemas/CartItemSchema.php) - ---- - -## woocommerce_store_api_item_quantity_step - - -Filters the quantity increment for a cart item in Store API. +### Description -```php -apply_filters( 'woocommerce_store_api_item_quantity_step', array $cart_item ) -``` +

Filters the variation option name for custom option slugs.

### Parameters | Argument | Type | Description | | -------- | ---- | ----------- | -| $cart_item | array | Cart item array. | +| $quantity_limit | integer | Quantity limit which defaults to 99 unless sold individually. | +| $product | \WC_Product | Product instance. | ### Returns -`\Automattic\WooCommerce\Blocks\StoreApi\Schemas\number` +`integer` ### Source -File: [StoreApi/Schemas/CartItemSchema.php](../src/StoreApi/Schemas/CartItemSchema.php) + - [StoreApi/Utilities/QuantityLimits.php](../src/StoreApi/Utilities/QuantityLimits.php) --- -## woocommerce_store_api_product_quantity_limit +## woocommerce_store_api_product_quantity_{$value_type} -Filters the quantity limit for a product being added to the cart via the Store API. +Filters the quantity minimum for a cart item in Store API. This allows extensions to control the minimum qty of items already within the cart. ```php -apply_filters( 'woocommerce_store_api_product_quantity_limit', integer $quantity_limit, \WC_Product $product ) +apply_filters( 'woocommerce_store_api_product_quantity_{$value_type}', mixed $value, \WC_Product $product, array|null $cart_item ) ``` ### Description -

Filters the variation option name for custom option slugs.

+

The suffix of the hook will vary depending on the value being filtered. For example, minimum, maximum, multiple_of, editable.

### Parameters | Argument | Type | Description | | -------- | ---- | ----------- | -| $quantity_limit | integer | Quantity limit which defaults to 99 unless sold individually. | -| $product | \WC_Product | Product instance. | +| $value | mixed | The value being filtered. | +| $product | \WC_Product | The product object. | +| $cart_item | array, null | The cart item if the product exists in the cart, or null. | ### Returns -`integer` +`mixed` ### Source - - [StoreApi/Schemas/ProductSchema.php](../src/StoreApi/Schemas/ProductSchema.php) + - [StoreApi/Utilities/QuantityLimits.php](../src/StoreApi/Utilities/QuantityLimits.php) ---