diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/simple.js b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/simple.js
index 0aff18b9ea8..a08fe69ee38 100644
--- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/simple.js
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/simple.js
@@ -18,8 +18,8 @@ const Simple = () => {
quantity,
minQuantity,
maxQuantity,
- setQuantity,
- formDisabled,
+ dispatchActions,
+ isDisabled,
} = useAddToCartFormContext();
if ( product.id && ! product.is_purchasable ) {
@@ -43,8 +43,8 @@ const Simple = () => {
value={ quantity }
min={ minQuantity }
max={ maxQuantity }
- disabled={ formDisabled }
- onChange={ setQuantity }
+ disabled={ isDisabled }
+ onChange={ dispatchActions.setQuantity }
/>
>
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/index.js b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/index.js
index f0a209dbd97..fb7d707e306 100644
--- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/index.js
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/index.js
@@ -23,8 +23,8 @@ const Variable = () => {
quantity,
minQuantity,
maxQuantity,
- setQuantity,
- formDisabled,
+ dispatchActions,
+ isDisabled,
} = useAddToCartFormContext();
if ( product.id && ! product.is_purchasable ) {
@@ -44,13 +44,16 @@ const Variable = () => {
return (
<>
-
+
>
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/attribute-picker.js b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/attribute-picker.js
index 9b865838bd5..7a98817707a 100644
--- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/attribute-picker.js
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/attribute-picker.js
@@ -2,14 +2,15 @@
* External dependencies
*/
import { useState, useEffect, useMemo } from '@wordpress/element';
+import { useShallowEqual } from '@woocommerce/base-hooks';
/**
* Internal dependencies
*/
import AttributeSelectControl from './attribute-select-control';
import {
- getVariationsMatchingSelectedAttributes,
- getSelectControlOptions,
+ getVariationMatchingSelectedAttributes,
+ getActiveSelectControlOptions,
} from './utils';
/**
@@ -17,88 +18,74 @@ import {
*
* @param {*} props Component props.
*/
-const AttributePicker = ( { attributes, variationAttributes } ) => {
+const AttributePicker = ( {
+ attributes,
+ variationAttributes,
+ setRequestParams,
+} ) => {
+ const currentAttributes = useShallowEqual( attributes );
+ const currentVariationAttributes = useShallowEqual( variationAttributes );
const [ variationId, setVariationId ] = useState( 0 );
+ const [ selectedAttributes, setSelectedAttributes ] = useState( {} );
- // @todo Support default selected attributes in Variation Picker.
- const [ selectedAttributes, setSelectedAttributes ] = useState( [] );
-
- const attributeNames = Object.keys( attributes );
- const hasSelectedAttributes =
- Object.values( selectedAttributes ).filter( Boolean ).length > 0;
- const hasSelectedAllAttributes =
- Object.values( selectedAttributes ).filter(
- ( selected ) => selected !== ''
- ).length === attributeNames.length;
-
- // Gets valid attribute options for the picker taking current selections into account.
+ // Get options for each attribute picker.
const filteredAttributeOptions = useMemo( () => {
- const options = [];
+ return getActiveSelectControlOptions(
+ currentAttributes,
+ currentVariationAttributes,
+ selectedAttributes
+ );
+ }, [ selectedAttributes, currentAttributes, currentVariationAttributes ] );
- attributeNames.forEach( ( attributeName ) => {
- const currentAttribute = attributes[ attributeName ];
- const attributeNamesExcludingCurrentAttribute = attributeNames.filter(
- ( name ) => name !== attributeName
- );
- const matchingVariationIds = hasSelectedAttributes
- ? getVariationsMatchingSelectedAttributes( {
- selectedAttributes,
- variationAttributes,
- attributeNames: attributeNamesExcludingCurrentAttribute,
- } )
- : null;
- const validAttributeTerms =
- matchingVariationIds !== null
- ? matchingVariationIds.map(
- ( varId ) =>
- variationAttributes[ varId ][ attributeName ]
- )
- : null;
- options[ attributeName ] = getSelectControlOptions(
- currentAttribute.terms,
- validAttributeTerms
- );
- } );
-
- return options;
- }, [
- attributes,
- variationAttributes,
- attributeNames,
- selectedAttributes,
- hasSelectedAttributes,
- ] );
-
- // Select variation when selections change.
+ // Select variations when selections are change.
useEffect( () => {
- if ( ! hasSelectedAllAttributes ) {
+ const hasSelectedAllAttributes =
+ Object.values( selectedAttributes ).filter(
+ ( selected ) => selected !== ''
+ ).length === Object.keys( currentAttributes ).length;
+
+ if ( hasSelectedAllAttributes ) {
+ setVariationId(
+ getVariationMatchingSelectedAttributes(
+ currentAttributes,
+ currentVariationAttributes,
+ selectedAttributes
+ )
+ );
+ } else if ( variationId > 0 ) {
+ // Unset variation when form is incomplete.
setVariationId( 0 );
- return;
}
-
- const matchingVariationIds = getVariationsMatchingSelectedAttributes( {
- selectedAttributes,
- variationAttributes,
- attributeNames,
- } );
-
- setVariationId( matchingVariationIds[ 0 ] || 0 );
}, [
selectedAttributes,
- variationAttributes,
- attributeNames,
- hasSelectedAllAttributes,
+ variationId,
+ currentAttributes,
+ currentVariationAttributes,
] );
- // @todo Hook up Variation Picker with Cart Form.
+ // Set requests params as variation ID and data changes.
+ useEffect( () => {
+ setRequestParams( {
+ id: variationId,
+ variation: Object.keys( selectedAttributes ).map(
+ ( attributeName ) => {
+ return {
+ attribute: attributeName,
+ value: selectedAttributes[ attributeName ],
+ };
+ }
+ ),
+ } );
+ }, [ setRequestParams, variationId, selectedAttributes ] );
+
return (
- { attributeNames.map( ( attributeName ) => (
+ { Object.keys( currentAttributes ).map( ( attributeName ) => (
{
setSelectedAttributes( {
...selectedAttributes,
@@ -107,7 +94,6 @@ const AttributePicker = ( { attributes, variationAttributes } ) => {
} }
/>
) ) }
- Matched variation ID: { variationId }
);
};
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/attribute-select-control.js b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/attribute-select-control.js
index 7c12d3b489d..c2092dab7df 100644
--- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/attribute-select-control.js
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/attribute-select-control.js
@@ -4,6 +4,10 @@
import { __ } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';
import { SelectControl } from '@wordpress/components';
+import { useValidationContext } from '@woocommerce/base-context';
+import { useEffect } from 'react';
+import classnames from 'classnames';
+import { ValidationInputError } from '@woocommerce/base-components/validation';
// Default option for select boxes.
const selectAnOption = {
@@ -19,17 +23,66 @@ const selectAnOption = {
const AttributeSelectControl = ( {
attributeName,
options = [],
- selected = '',
+ value = '',
onChange = () => {},
+ errorMessage = __(
+ 'Please select a value.',
+ 'woo-gutenberg-products-block'
+ ),
} ) => {
+ const {
+ getValidationError,
+ setValidationErrors,
+ clearValidationError,
+ } = useValidationContext();
+ const errorId = attributeName;
+ const error = getValidationError( errorId ) || {};
+
+ useEffect( () => {
+ if ( value ) {
+ clearValidationError( errorId );
+ } else {
+ setValidationErrors( {
+ [ errorId ]: {
+ message: errorMessage,
+ hidden: true,
+ },
+ } );
+ }
+ }, [
+ value,
+ errorId,
+ errorMessage,
+ clearValidationError,
+ setValidationErrors,
+ ] );
+
+ // Remove validation errors when unmounted.
+ useEffect( () => () => void clearValidationError( errorId ), [
+ errorId,
+ clearValidationError,
+ ] );
+
return (
-
+
+
+
+
);
};
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/index.js b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/index.js
index cc8dee8091f..8509e7e518e 100644
--- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/index.js
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/index.js
@@ -7,17 +7,10 @@ import { getAttributes, getVariationAttributes } from './utils';
/**
* VariationAttributes component.
- *
- * @param {*} props Component props.
*/
-const VariationAttributes = ( { product } ) => {
- const {
- attributes: productAttributes = {},
- variations: productVariations = [],
- } = product;
-
- const attributes = getAttributes( productAttributes );
- const variationAttributes = getVariationAttributes( productVariations );
+const VariationAttributes = ( { product, dispatchers } ) => {
+ const attributes = getAttributes( product.attributes );
+ const variationAttributes = getVariationAttributes( product.variations );
if (
Object.keys( attributes ).length === 0 ||
@@ -30,6 +23,7 @@ const VariationAttributes = ( { product } ) => {
);
};
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/style.scss b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/style.scss
index 30c2a32efef..a9dbfd50c8a 100644
--- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/style.scss
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/style.scss
@@ -7,6 +7,10 @@
@include font-size(regular);
}
+ .wc-block-components-product-add-to-cart-attribute-picker__container {
+ position: relative;
+ }
+
.wc-block-components-product-add-to-cart-attribute-picker__select {
margin: 0 0 em($gap-small) 0;
@@ -14,5 +18,16 @@
min-width: 60%;
min-height: 1.75em;
}
+
+ &.has-error {
+ margin-bottom: $gap-large;
+
+ select {
+ border-color: $error-red;
+ &:focus {
+ outline-color: $error-red;
+ }
+ }
+ }
}
}
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/test/index.js b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/test/index.js
new file mode 100644
index 00000000000..d17cc1e32ef
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/test/index.js
@@ -0,0 +1,395 @@
+/**
+ * Internal dependencies
+ */
+import {
+ getAttributes,
+ getVariationAttributes,
+ getVariationsMatchingSelectedAttributes,
+ getVariationMatchingSelectedAttributes,
+ getActiveSelectControlOptions,
+} from '../utils';
+
+const rawAttributeData = [
+ {
+ id: 1,
+ name: 'Color',
+ taxonomy: 'pa_color',
+ has_variations: true,
+ terms: [
+ {
+ id: 22,
+ name: 'Blue',
+ slug: 'blue',
+ },
+ {
+ id: 23,
+ name: 'Green',
+ slug: 'green',
+ },
+ {
+ id: 24,
+ name: 'Red',
+ slug: 'red',
+ },
+ ],
+ },
+ {
+ id: 0,
+ name: 'Logo',
+ taxonomy: null,
+ has_variations: true,
+ terms: [
+ {
+ id: 0,
+ name: 'Yes',
+ slug: 'Yes',
+ },
+ {
+ id: 0,
+ name: 'No',
+ slug: 'No',
+ },
+ ],
+ },
+ {
+ id: 0,
+ name: 'Non-variable attribute',
+ taxonomy: null,
+ has_variations: false,
+ terms: [
+ {
+ id: 0,
+ name: 'Test',
+ slug: 'Test',
+ },
+ {
+ id: 0,
+ name: 'Test 2',
+ slug: 'Test 2',
+ },
+ ],
+ },
+];
+
+const rawVariations = [
+ {
+ id: 35,
+ attributes: [
+ {
+ name: 'Color',
+ value: 'blue',
+ },
+ {
+ name: 'Logo',
+ value: 'Yes',
+ },
+ ],
+ },
+ {
+ id: 28,
+ attributes: [
+ {
+ name: 'Color',
+ value: 'red',
+ },
+ {
+ name: 'Logo',
+ value: 'No',
+ },
+ ],
+ },
+ {
+ id: 29,
+ attributes: [
+ {
+ name: 'Color',
+ value: 'green',
+ },
+ {
+ name: 'Logo',
+ value: 'No',
+ },
+ ],
+ },
+ {
+ id: 30,
+ attributes: [
+ {
+ name: 'Color',
+ value: 'blue',
+ },
+ {
+ name: 'Logo',
+ value: 'No',
+ },
+ ],
+ },
+];
+
+describe( 'Testing utils', () => {
+ describe( 'Testing getAttributes()', () => {
+ it( 'returns empty object if there are no attributes', () => {
+ const attributes = getAttributes( null );
+ expect( attributes ).toStrictEqual( {} );
+ } );
+ it( 'returns list of attributes when given valid data', () => {
+ const attributes = getAttributes( rawAttributeData );
+ expect( attributes ).toStrictEqual( {
+ Color: {
+ id: 1,
+ name: 'Color',
+ taxonomy: 'pa_color',
+ has_variations: true,
+ terms: [
+ {
+ id: 22,
+ name: 'Blue',
+ slug: 'blue',
+ },
+ {
+ id: 23,
+ name: 'Green',
+ slug: 'green',
+ },
+ {
+ id: 24,
+ name: 'Red',
+ slug: 'red',
+ },
+ ],
+ },
+ Logo: {
+ id: 0,
+ name: 'Logo',
+ taxonomy: null,
+ has_variations: true,
+ terms: [
+ {
+ id: 0,
+ name: 'Yes',
+ slug: 'Yes',
+ },
+ {
+ id: 0,
+ name: 'No',
+ slug: 'No',
+ },
+ ],
+ },
+ } );
+ } );
+ } );
+ describe( 'Testing getVariationAttributes()', () => {
+ it( 'returns empty object if there are no variations', () => {
+ const variationAttributes = getVariationAttributes( null );
+ expect( variationAttributes ).toStrictEqual( {} );
+ } );
+ it( 'returns list of attribute names and value pairs when given valid data', () => {
+ const variationAttributes = getVariationAttributes( rawVariations );
+ expect( variationAttributes ).toStrictEqual( {
+ 'id:35': {
+ id: 35,
+ attributes: {
+ Color: 'blue',
+ Logo: 'Yes',
+ },
+ },
+ 'id:28': {
+ id: 28,
+ attributes: {
+ Color: 'red',
+ Logo: 'No',
+ },
+ },
+ 'id:29': {
+ id: 29,
+ attributes: {
+ Color: 'green',
+ Logo: 'No',
+ },
+ },
+ 'id:30': {
+ id: 30,
+ attributes: {
+ Color: 'blue',
+ Logo: 'No',
+ },
+ },
+ } );
+ } );
+ } );
+ describe( 'Testing getVariationsMatchingSelectedAttributes()', () => {
+ const attributes = getAttributes( rawAttributeData );
+ const variationAttributes = getVariationAttributes( rawVariations );
+
+ it( 'returns all variations, in the correct order, if no selections have been made yet', () => {
+ const selectedAttributes = {};
+ const matches = getVariationsMatchingSelectedAttributes(
+ attributes,
+ variationAttributes,
+ selectedAttributes
+ );
+ expect( matches ).toStrictEqual( [ 35, 28, 29, 30 ] );
+ } );
+
+ it( 'returns correct subset of variations after a selection', () => {
+ const selectedAttributes = {
+ Color: 'blue',
+ };
+ const matches = getVariationsMatchingSelectedAttributes(
+ attributes,
+ variationAttributes,
+ selectedAttributes
+ );
+ expect( matches ).toStrictEqual( [ 35, 30 ] );
+ } );
+
+ it( 'returns correct subset of variations after all selections', () => {
+ const selectedAttributes = {
+ Color: 'blue',
+ Logo: 'No',
+ };
+ const matches = getVariationsMatchingSelectedAttributes(
+ attributes,
+ variationAttributes,
+ selectedAttributes
+ );
+ expect( matches ).toStrictEqual( [ 30 ] );
+ } );
+
+ it( 'returns no results if selection does not match or is invalid', () => {
+ const selectedAttributes = {
+ Color: 'brown',
+ };
+ const matches = getVariationsMatchingSelectedAttributes(
+ attributes,
+ variationAttributes,
+ selectedAttributes
+ );
+ expect( matches ).toStrictEqual( [] );
+ } );
+ } );
+ describe( 'Testing getVariationMatchingSelectedAttributes()', () => {
+ const attributes = getAttributes( rawAttributeData );
+ const variationAttributes = getVariationAttributes( rawVariations );
+
+ it( 'returns first match if no selections have been made yet', () => {
+ const selectedAttributes = {};
+ const matches = getVariationMatchingSelectedAttributes(
+ attributes,
+ variationAttributes,
+ selectedAttributes
+ );
+ expect( matches ).toStrictEqual( 35 );
+ } );
+
+ it( 'returns first match after single selection', () => {
+ const selectedAttributes = {
+ Color: 'blue',
+ };
+ const matches = getVariationMatchingSelectedAttributes(
+ attributes,
+ variationAttributes,
+ selectedAttributes
+ );
+ expect( matches ).toStrictEqual( 35 );
+ } );
+
+ it( 'returns correct match after all selections', () => {
+ const selectedAttributes = {
+ Color: 'blue',
+ Logo: 'No',
+ };
+ const matches = getVariationMatchingSelectedAttributes(
+ attributes,
+ variationAttributes,
+ selectedAttributes
+ );
+ expect( matches ).toStrictEqual( 30 );
+ } );
+
+ it( 'returns no match if invalid', () => {
+ const selectedAttributes = {
+ Color: 'brown',
+ };
+ const matches = getVariationMatchingSelectedAttributes(
+ attributes,
+ variationAttributes,
+ selectedAttributes
+ );
+ expect( matches ).toStrictEqual( 0 );
+ } );
+ } );
+ describe( 'Testing getActiveSelectControlOptions()', () => {
+ const attributes = getAttributes( rawAttributeData );
+ const variationAttributes = getVariationAttributes( rawVariations );
+
+ it( 'returns all possible options if no selections have been made yet', () => {
+ const selectedAttributes = {};
+ const controlOptions = getActiveSelectControlOptions(
+ attributes,
+ variationAttributes,
+ selectedAttributes
+ );
+ expect( controlOptions ).toStrictEqual( {
+ Color: [
+ {
+ value: 'blue',
+ label: 'Blue',
+ },
+ {
+ value: 'green',
+ label: 'Green',
+ },
+ {
+ value: 'red',
+ label: 'Red',
+ },
+ ],
+ Logo: [
+ {
+ value: 'Yes',
+ label: 'Yes',
+ },
+ {
+ value: 'No',
+ label: 'No',
+ },
+ ],
+ } );
+ } );
+
+ it( 'returns only valid options if color is selected', () => {
+ const selectedAttributes = {
+ Color: 'green',
+ };
+ const controlOptions = getActiveSelectControlOptions(
+ attributes,
+ variationAttributes,
+ selectedAttributes
+ );
+ expect( controlOptions ).toStrictEqual( {
+ Color: [
+ {
+ value: 'blue',
+ label: 'Blue',
+ },
+ {
+ value: 'green',
+ label: 'Green',
+ },
+ {
+ value: 'red',
+ label: 'Red',
+ },
+ ],
+ Logo: [
+ {
+ value: 'No',
+ label: 'No',
+ },
+ ],
+ } );
+ } );
+ } );
+} );
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/utils.js b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/utils.js
index 132454b920a..e53eb5a9930 100644
--- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/utils.js
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/utils.js
@@ -2,6 +2,7 @@
* External dependencies
*/
import { keyBy } from 'lodash';
+import { decodeEntities } from '@wordpress/html-entities';
/**
* Key an array of attributes by name,
@@ -16,13 +17,16 @@ export const getAttributes = ( attributes ) => {
),
'name'
)
- : [];
+ : {};
};
/**
* Format variations from the API into a map of just the attribute names and values.
*
- * @param {Array} variations Variations array.
+ * Note, each item is keyed by the variation ID with an id: prefix. This is to prevent the object
+ * being reordered when iterated.
+ *
+ * @param {Object} variations List of Variation objects and attributes keyed by variation ID.
*/
export const getVariationAttributes = ( variations ) => {
if ( ! variations ) {
@@ -32,65 +36,56 @@ export const getVariationAttributes = ( variations ) => {
const attributesMap = {};
variations.forEach( ( { id, attributes } ) => {
- attributesMap[ id ] = attributes.reduce( ( acc, { name, value } ) => {
- acc[ name ] = value;
- return acc;
- }, [] );
+ attributesMap[ `id:${ id }` ] = {
+ id,
+ attributes: attributes.reduce( ( acc, { name, value } ) => {
+ acc[ name ] = value;
+ return acc;
+ }, {} ),
+ };
} );
return attributesMap;
};
-/**
- * Given a list of terms, filter them and return options for the select boxes.
- *
- * @param {Object} attributeTerms List of attribute term objects.
- * @param {?Array} validAttributeTerms Valid values if selections have been made already.
- * @return {Array} Value/Label pairs of select box options.
- */
-export const getSelectControlOptions = (
- attributeTerms,
- validAttributeTerms = null
-) => {
- return Object.values( attributeTerms )
- .map( ( { name, slug } ) => {
- if (
- validAttributeTerms === null ||
- validAttributeTerms.includes( null ) ||
- validAttributeTerms.includes( slug )
- ) {
- return {
- value: slug,
- label: name,
- };
- }
- return null;
- } )
- .filter( Boolean );
-};
-
/**
* Given a list of variations and a list of attribute values, return variations which match.
*
* Allows an attribute to be excluded by name. This is used to filter displayed options for
* individual attribute selects.
*
- * @param {Object} props
- * @param {Object} props.selectedAttributes List of selected attributes.
- * @param {Object} props.variationAttributes List of variations and their attributes.
- * @param {Object} props.attributeNames List of all possible attribute names.
+ * @param {Object} attributes List of attribute names and terms.
+ * @param {Object} variationAttributes Attributes for each variation keyed by variation ID.
+ * @param {Object} selectedAttributes Attribute Name Value pairs of current selections by the user.
* @return {Array} List of matching variation IDs.
*/
-export const getVariationsMatchingSelectedAttributes = ( {
- selectedAttributes,
+export const getVariationsMatchingSelectedAttributes = (
+ attributes,
variationAttributes,
- attributeNames,
-} ) => {
- return Object.keys( variationAttributes ).filter( ( variationId ) =>
+ selectedAttributes
+) => {
+ const variationIds = Object.values( variationAttributes ).map(
+ ( { id: variationId } ) => {
+ return variationId;
+ }
+ );
+
+ // If nothing is selected yet, just return all variations.
+ if (
+ Object.values( selectedAttributes ).every( ( value ) => value === '' )
+ ) {
+ return variationIds;
+ }
+
+ const attributeNames = Object.keys( attributes );
+
+ return variationIds.filter( ( variationId ) =>
attributeNames.every( ( attributeName ) => {
const selectedAttribute = selectedAttributes[ attributeName ] || '';
const variationAttribute =
- variationAttributes[ variationId ][ attributeName ];
+ variationAttributes[ 'id:' + variationId ].attributes[
+ attributeName
+ ];
// If there is no selected attribute, consider this a match.
if ( selectedAttribute === '' ) {
@@ -105,3 +100,109 @@ export const getVariationsMatchingSelectedAttributes = ( {
} )
);
};
+
+/**
+ * Given a list of variations and a list of attribute values, returns the first matched variation ID.
+ *
+ * @param {Object} attributes List of attribute names and terms.
+ * @param {Object} variationAttributes Attributes for each variation keyed by variation ID.
+ * @param {Object} selectedAttributes Attribute Name Value pairs of current selections by the user.
+ * @return {number} Variation ID.
+ */
+export const getVariationMatchingSelectedAttributes = (
+ attributes,
+ variationAttributes,
+ selectedAttributes
+) => {
+ const matchingVariationIds = getVariationsMatchingSelectedAttributes(
+ attributes,
+ variationAttributes,
+ selectedAttributes
+ );
+ return matchingVariationIds[ 0 ] || 0;
+};
+
+/**
+ * Given a list of terms, filter them and return valid options for the select boxes.
+ *
+ * @see getActiveSelectControlOptions
+ * @param {Object} attributeTerms List of attribute term objects.
+ * @param {?Array} validAttributeTerms Valid values if selections have been made already.
+ * @return {Array} Value/Label pairs of select box options.
+ */
+const getValidSelectControlOptions = (
+ attributeTerms,
+ validAttributeTerms = null
+) => {
+ return Object.values( attributeTerms )
+ .map( ( { name, slug } ) => {
+ if (
+ validAttributeTerms === null ||
+ validAttributeTerms.includes( null ) ||
+ validAttributeTerms.includes( slug )
+ ) {
+ return {
+ value: slug,
+ label: decodeEntities( name ),
+ };
+ }
+ return null;
+ } )
+ .filter( Boolean );
+};
+
+/**
+ * Given a list of terms, filter them and return active options for the select boxes. This factors in
+ * which options should be hidden due to current selections.
+ *
+ * @param {Object} attributes List of attribute names and terms.
+ * @param {Object} variationAttributes Attributes for each variation keyed by variation ID.
+ * @param {Object} selectedAttributes Attribute Name Value pairs of current selections by the user.
+ * @return {Object} Select box options.
+ */
+export const getActiveSelectControlOptions = (
+ attributes,
+ variationAttributes,
+ selectedAttributes
+) => {
+ const options = {};
+ const attributeNames = Object.keys( attributes );
+ const hasSelectedAttributes =
+ Object.values( selectedAttributes ).filter( Boolean ).length > 0;
+
+ attributeNames.forEach( ( attributeName ) => {
+ const currentAttribute = attributes[ attributeName ];
+ const selectedAttributesExcludingCurrentAttribute = {
+ ...selectedAttributes,
+ [ attributeName ]: null,
+ };
+ // This finds matching variations for selected attributes apart from this one. This will be
+ // used to get valid attribute terms of the current attribute narrowed down by those matching
+ // variation IDs. For example, if I had Large Blue Shirts and Medium Red Shirts, I want to only
+ // show Red shirts if Medium is selected.
+ const matchingVariationIds = hasSelectedAttributes
+ ? getVariationsMatchingSelectedAttributes(
+ attributes,
+ variationAttributes,
+ selectedAttributesExcludingCurrentAttribute
+ )
+ : null;
+ // Uses the above matching variation IDs to get the attributes from just those variations.
+ const validAttributeTerms =
+ matchingVariationIds !== null
+ ? matchingVariationIds.map(
+ ( varId ) =>
+ variationAttributes[ 'id:' + varId ].attributes[
+ attributeName
+ ]
+ )
+ : null;
+ // Intersects attributes with valid attributes.
+ options[ attributeName ] = getValidSelectControlOptions(
+ currentAttribute.terms,
+ validAttributeTerms
+ );
+ } );
+
+ return options;
+};
diff --git a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/shared/add-to-cart-button.js b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/shared/add-to-cart-button.js
index 66c5763f007..58e992b1ec7 100644
--- a/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/shared/add-to-cart-button.js
+++ b/plugins/woocommerce-blocks/assets/js/atomic/blocks/product-elements/add-to-cart/shared/add-to-cart-button.js
@@ -4,8 +4,9 @@
import { __, _n, sprintf } from '@wordpress/i18n';
import Button from '@woocommerce/base-components/button';
import { Icon, done as doneIcon } from '@woocommerce/icons';
-import { useState } from '@wordpress/element';
+import { useState, useEffect } from '@wordpress/element';
import { useAddToCartFormContext } from '@woocommerce/base-context';
+import { useStoreAddToCart } from '@woocommerce/base-hooks';
/**
* Add to Cart Form Button Component.
@@ -14,20 +15,37 @@ const AddToCartButton = () => {
const {
showFormElements,
product,
- quantityInCart,
- formDisabled,
- formSubmitting,
- onSubmit,
+ isDisabled,
+ isProcessing,
+ eventRegistration,
+ hasError,
+ dispatchActions,
} = useAddToCartFormContext();
+ const { cartQuantity } = useStoreAddToCart( product.id || 0 );
+ const [ addedToCart, setAddedToCart ] = useState( false );
+ const isPurchasable = product.is_purchasable;
+ const hasOptions = product.has_options;
+ const addToCartButtonData = product.add_to_cart || {
+ url: '',
+ text: '',
+ };
- const {
- is_purchasable: isPurchasable = true,
- has_options: hasOptions,
- add_to_cart: addToCartButtonData = {
- url: '',
- text: '',
- },
- } = product;
+ // Subscribe to emitter for after processing.
+ useEffect( () => {
+ const onSuccess = () => {
+ if ( ! hasError ) {
+ setAddedToCart( true );
+ }
+ return true;
+ };
+ const unsubscribeProcessing = eventRegistration.onAddToCartAfterProcessingWithSuccess(
+ onSuccess,
+ 0
+ );
+ return () => {
+ unsubscribeProcessing();
+ };
+ }, [ eventRegistration, hasError ] );
// If we are showing form elements, OR if the product has no additional form options, we can show
// a functional direct add to cart button, provided that the product is purchasable.
@@ -36,10 +54,11 @@ const AddToCartButton = () => {
return (
dispatchActions.submitForm() }
/>
);
}
@@ -73,23 +92,19 @@ const LinkComponent = ( { className, href, text } ) => {
const ButtonComponent = ( {
className,
quantityInCart,
- loading,
- disabled,
+ isProcessing,
+ isDisabled,
+ isDone,
onClick,
} ) => {
- const [ wasClicked, setWasClicked ] = useState( false );
-
return (