Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Add minimum quantity and step so Cart Block and Store API. #5037

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 59 additions & 18 deletions assets/js/base/components/quantity-selector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,6 +18,7 @@ interface QuantitySelectorProps {
quantity?: number;
minimum?: number;
maximum: number;
step?: number;
onChange: ( newQuantity: number ) => void;
itemName?: string;
disabled: boolean;
Expand All @@ -27,6 +29,7 @@ const QuantitySelector = ( {
quantity = 1,
minimum = 1,
maximum,
step = 1,
onChange = () => {
/* Do nothing. */
},
Expand All @@ -39,8 +42,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 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(
( 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.
Expand All @@ -60,15 +101,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 (
Expand All @@ -77,22 +118,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 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 );
}
} }
aria-label={ sprintf(
Expand All @@ -112,7 +153,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(
Expand All @@ -136,7 +177,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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [],
Expand Down Expand Up @@ -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', {
Expand Down
2 changes: 2 additions & 0 deletions assets/js/types/type-defs/cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
54 changes: 54 additions & 0 deletions bin/hook-docs/data/filters.json
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,60 @@
"long_description_html": "<p>This can be used to disable the nonce check when testing API endpoints via a REST API client.</p>"
}
},
{
"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_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.",
"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",
Expand Down
56 changes: 56 additions & 0 deletions docs/extensibility/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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_variation_option_name](#woocommerce_variation_option_name)

Expand Down Expand Up @@ -781,6 +783,60 @@ File: [StoreApi/Routes/AbstractCartRoute.php](../src/StoreApi/Routes/AbstractCar

---

## 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_item_quantity_step


Filters the quantity increment for a cart item in Store API.

```php
apply_filters( 'woocommerce_store_api_item_quantity_step', 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


Expand Down
46 changes: 46 additions & 0 deletions src/StoreApi/Schemas/CartItemSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -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_step' => [
mikejolley marked this conversation as resolved.
Show resolved Hide resolved
'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',
Expand Down Expand Up @@ -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_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() ) ) ),
Expand Down Expand Up @@ -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 );
mikejolley marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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_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_step', 1, $cart_item );
}

/**
* Remove HTML tags from cart item data and set the `hidden` property to
* `__experimental_woocommerce_blocks_hidden`.
Expand Down