Commit b8de41cdb2 for woocommerce
commit b8de41cdb20afd1d117ebf12848de1437b43f513
Author: CAT the Tech <88114675+catthetech@users.noreply.github.com>
Date: Wed Jan 14 09:18:28 2026 -0500
Add variations autoselect (new) (#61389)
* Added the functionality to display attribute values in conflict and specify what to do with them
Signed-off-by: CAT the Tech <trottier.charlesantoine@gmail.com>
* Added the "Auto-select on page load" option
Signed-off-by: CAT the Tech <trottier.charlesantoine@gmail.com>
* Made it so initial options are filtered according to possible variations
Signed-off-by: CAT the Tech <trottier.charlesantoine@gmail.com>
* Replace "option_gt_filter" by [value=""]
- "option_gt_filter" targets the "Choose an option"/empty option
- The "Choose an option"/empty option could be replaced by a
plugin and not be the first option anymore
- [value=""] is better because it will ALWAYS target the "Choose an
option"/empty option because it is always empty
- [value="" removes the need to handle the special case of show_option_none === 'no'
Signed-off-by: CAT the Tech <trottier.charlesantoine@gmail.com>
* Added changelog
Signed-off-by: CAT the Tech <trottier.charlesantoine@gmail.com>
* Modify the on page load logic to trigger a change and a click instead of a manual check-variations
* Add settings for autoselect and autoselectOnPageLoad on the new Add to Cart + Options block
* Enable autoselect for the new Add to Cart + Options block (both dropdown- and pill-style)
* Enable autoselectOnPageLoad on the new Add to Cart + Options block
* Handle disabled attributes when autoselecting
* Correct labels and help texts; Be more consistent
* Rename autoselect_lock to in_autoselect_scope; Explain a bit more what it does
* More comments; Minor fixes
* Change unattached_action to unattached_attributes_action
* Fix an incompatibility with the dropdown and pill styles
* Add in_autoselect_scope check to the old/legacy Add to Cart With Options block
* Legacy Add to Cart block: Changed where the disabled options' color was set and allow enabled options to be dealt with along unattached options
* Legacy Add to Cart block: check variations after resetting form when no variations are possible
* Add disabledAttributesAction setting to the new Add to Cart + Options block
* Add more comments, modify texts and other small tweaks
* Write e2e-pw tests for autoselect and related options
* Stay up-to-date with the development of the new Add to Cart + Options block
* Moved changelog to reflect new branch name
* Tests: clean up and remove the forced timeout in favor of manually checking if block settings change
* Tests: Also test without autoselect (both on page load and on user selection)
* Add to Cart + Options block: Allow auto-selecting disabled choices if they are valid but not yet enabled
* Tests: add the selectAttribute function and shorten some expect statements
* Tests: Add a test about combining Auto-select on user selection and the Values in conflict setting
* Tests: Rename selectAttributes to selectBlockAttributes and add function expectSelectedAttributes
* Tests: add else statements to throw errors
* Tests: loop over attributeValues instead of putting them in a RegExp in expectSelectedAttributes
* Revert all changes on the old/legacy Add to Cart block
* Merge Autoselect and Autoselect on page load
* Remove "gray" option from disabledAttributesAction
* Tests: Add common setup and revert to the old/legacy Add to Cart block after testing is complete
* Listen to CodeRabbitAI's suggestions
* Add to Cart + options block Tests: Ensure the product type panel is open
* Add to Cart + options block Tests: Only upgrade to the new block if it is not already
* Move autoselect tests to the block's test file in @woocommerce/block-library
* Don't use jQuery anymore in the new Add to Cart + Options block
* Tests: Get the product's attributes directly instead of maintaining a copy in the test file
* Always use 'data-wp-bind--disabled', and remove the switch statement in favor of a ternary operator
* Added id and autoselect to the context to support multiple cart blocks at the same time
* Rename attributesAutoselect and attributesAutoselectOthers to make it clear they're doing something, and move them under actions
* Remove a switch statement in favor of a ternary operator
* Improve typing
* Manually check/select the value when auto-selecting instead of simulating a click
WIP
* Fix post_names in sample_products.xml
* Modify the changelog to clarify the modifications apply to the Add to Cart + Options block
* Use the pre-existing $attributes variable instead of fetching it manually
* Refactor Add to Cart + Options autoselect so it doesn't query the DOM directly
* Remove an unused mention of id; autoselectAttributes now has both includedAttributes and excludedAttributes as arguments; Don't auto-select when the user deselects an attribute
* Listen to CodeRabbitAI's suggestions
* Ensure productObject.variations is an array and productObject.variations.attributes is an object inside getProductAttributesAndOptions
* Don't define context as it is not needed anymore
* Fix typing for frontend.js
* Nest test.describe under the root one and improve typing
* Tests: productId is not needed anymore; Removed it
* Simply check if productObject?.variations exists instead of checking if it is an array or object
* Tests: Add missing parentheses
* Tests: Removed function goToProductTemplateEditor and replaced by calls to pageObject.updateSingleProductTemplate
* Tests: Make optionStyle values lowercase
* Tests: Refactor the selectBlockAttributes and expectedSelectedAttributes fucntions and move them where other tests can use them
* Tests: We don't need to import Page anymore; import Editor from woocommerce, not wordpress
* Tests: Call pageObject.updateSingleProductTemplate manually before each test to make it clear instead of inside test.beforeEach
* Tests: Don't forcefully set block attributes to their default values at the beginning of each test
* Tests: Merge the two auto-select tests (on page load and on user selection) into one
* Tests: Rename the setCartBlockAttributes function to setAddToCartWithOptionsAttributes
* Tests: Remove the setDisabledAttributesAction function as it is redundant
* Tests: Remove the preselect functions as they are used only once or twice
* Tests: Set the optionStyle in the template editor at the beginning of each tests
* Tests: Capitalize the optionStyle values again, because we are using them to locate the optionStyle radio button
* Remove a useless return statement and rename blockCart to addToCartWithOptionsBlock
* Tests: Rename some tests to not duplicate "Add to Cart + Options" at the start
* Tests: Improve typing
* Tests: Use wpCLI to add products before each tests instead of adding them in sample_products.xml where they affected all tests
* Improve Linting and typing
* Tests: Remove duplicate updateSingleProductTemplate and setAddToCartWithOptionsBlockAttributes function calls
* Tests: Rename selectBlockAttribute to selectVariationSelectorBlockAttribute
* Tests: Reword comments, use accurate names, and improve typing
* Tests: Fix the disabledAttributesAction block attribute's main test
* Tests: More linting
* Make disabledAttributesAction an enum instead of a string in block.json
* Tests: attributes and productVariations passed to wpCLI were not properly formatted
* Tests: Linting
* Specify the values of enum disabledAttributesAction are of type string
---------
Signed-off-by: CAT the Tech <trottier.charlesantoine@gmail.com>
Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com>
Co-authored-by: Sam Seay <samueljseay@gmail.com>
diff --git a/plugins/woocommerce/changelog/CATtheTech-autoselect-woo10 b/plugins/woocommerce/changelog/CATtheTech-autoselect-woo10
new file mode 100644
index 0000000000..279e68d430
--- /dev/null
+++ b/plugins/woocommerce/changelog/CATtheTech-autoselect-woo10
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Modified the Add to Cart + Options block to auto-select attribute fields, and display attributes in conflict
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/block.json
index 50bc8448c6..cc5caee3a8 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/block.json
@@ -11,6 +11,15 @@
"optionStyle": {
"type": "string",
"enum": [ "pills", "dropdown" ]
+ },
+ "autoselect": {
+ "type": "boolean",
+ "default": false
+ },
+ "disabledAttributesAction": {
+ "type": "string",
+ "enum": [ "disable", "hide" ],
+ "default": "disable"
}
},
"textdomain": "woocommerce",
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/edit.tsx
index 45420b7a8b..966af28c78 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/edit.tsx
@@ -8,6 +8,7 @@ import {
Disabled,
PanelBody,
SelectControl,
+ ToggleControl,
__experimentalToggleGroupControl as ToggleGroupControl,
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
} from '@wordpress/components';
@@ -23,6 +24,8 @@ import { useThemeColors } from '../../../../shared/hooks/use-theme-colors';
interface Attributes {
className?: string;
optionStyle?: 'pills' | 'dropdown';
+ autoselect: boolean;
+ disabledAttributesAction: 'disable' | 'hide';
}
function Pills( {
@@ -61,7 +64,8 @@ export default function AttributeOptionsEdit(
props: BlockEditProps< Attributes >
) {
const { attributes, setAttributes } = props;
- const { className, optionStyle } = attributes;
+ const { className, optionStyle, autoselect, disabledAttributesAction } =
+ attributes;
const blockProps = useBlockProps( {
className,
@@ -120,6 +124,55 @@ export default function AttributeOptionsEdit(
/>
</ToggleGroupControl>
</PanelBody>
+ <PanelBody title={ __( 'Auto-select', 'woocommerce' ) }>
+ <ToggleControl
+ label={ __(
+ 'Auto-select when only one attribute is compatible',
+ 'woocommerce'
+ ) }
+ help={ __(
+ 'This controls whether attributes will be auto-selected once upon loading the page and when an attribute is changed by the user. Only attributes with a single compatible value will be auto-selected.',
+ 'woocommerce'
+ ) }
+ checked={ autoselect }
+ onChange={ () =>
+ setAttributes( { autoselect: ! autoselect } )
+ }
+ __nextHasNoMarginBottom
+ />
+ <SelectControl
+ label={ __(
+ 'Values in conflict with current selection',
+ 'woocommerce'
+ ) }
+ help={ __(
+ 'This controls what to do with attribute values that conflict with the current selection.',
+ 'woocommerce'
+ ) }
+ value={ disabledAttributesAction }
+ options={ [
+ {
+ label: __( 'Hidden', 'woocommerce' ),
+ value: 'hide',
+ },
+ {
+ label: __(
+ 'Grayed-out/crossed-out and disabled',
+ 'woocommerce'
+ ),
+ value: 'disable',
+ },
+ ] }
+ onChange={ ( value ) =>
+ setAttributes( {
+ disabledAttributesAction: value as
+ | 'disable'
+ | 'hide',
+ } )
+ }
+ __nextHasNoMarginBottom
+ />
+ </PanelBody>
</InspectorControls>
<Disabled>
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/style.scss
index 2067adecf2..0250b5a71d 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/attribute-options/style.scss
@@ -41,6 +41,11 @@
);
}
+ /* When the pill button is hidden, we also need to hide the label (&) */
+ &:has(.wc-block-add-to-cart-with-options-variation-selector-attribute-options__pill-input[hidden]) {
+ display: none;
+ }
+
&:where(:focus-within) {
outline: 1px solid var(--pill-color);
outline-offset: 1px;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/frontend.ts
index 2cf4a2470c..7b155e8de9 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/variation-selector/frontend.ts
@@ -7,7 +7,10 @@ import {
getConfig,
getElement,
} from '@wordpress/interactivity';
-import { SelectedAttributes } from '@woocommerce/stores/woocommerce/cart';
+import {
+ SelectedAttributes,
+ VariationData,
+} from '@woocommerce/stores/woocommerce/cart';
import type { ChangeEvent } from 'react';
import type { ProductDataStore } from '@woocommerce/stores/woocommerce/product-data';
@@ -19,6 +22,7 @@ import type {
AddToCartWithOptionsStore,
Context as AddToCartWithOptionsStoreContext,
} from '../frontend';
+import type { NormalizedProductData } from '../types';
import { getMatchedVariation } from '../../../base/utils/variations/get-matched-variation';
import setStyles from './set-styles';
@@ -33,6 +37,7 @@ type Context = AddToCartWithOptionsStoreContext & {
selectedValue: string | null;
option: Option;
options: Option[];
+ autoselect: boolean;
};
// Set selected pill styles for proper contrast.
@@ -134,6 +139,40 @@ const isAttributeValueValid = ( {
} );
};
+/**
+ * Return the product attributes and options.
+ */
+const getProductAttributesAndOptions = (
+ productObject: NormalizedProductData | null
+): Record< string, string[] > => {
+ if ( ! productObject?.variations ) {
+ return {};
+ }
+
+ const variations: VariationData[] = Object.values(
+ productObject.variations
+ );
+ const productAttributesAndOptions = {} as Record< string, string[] >;
+ variations.forEach( ( variation: VariationData ) => {
+ if ( ! variation?.attributes ) {
+ return;
+ }
+ Object.entries( variation.attributes ).forEach( ( [ key, value ] ) => {
+ if ( typeof key !== 'string' || typeof value !== 'string' ) {
+ return;
+ }
+ if ( ! Array.isArray( productAttributesAndOptions[ key ] ) ) {
+ productAttributesAndOptions[ key ] = [];
+ }
+ if ( ! productAttributesAndOptions[ key ].includes( value ) ) {
+ productAttributesAndOptions[ key ].push( value );
+ }
+ } );
+ } );
+
+ return productAttributesAndOptions;
+};
+
export type VariableProductAddToCartWithOptionsStore =
AddToCartWithOptionsStore & {
state: {
@@ -148,6 +187,10 @@ export type VariableProductAddToCartWithOptionsStore =
handleDropdownChange: (
event: ChangeEvent< HTMLSelectElement >
) => void;
+ autoselectAttributes: ( args: {
+ includedAttributes?: string[];
+ excludedAttributes?: string[];
+ } ) => void;
};
callbacks: {
setDefaultSelectedAttribute: () => void;
@@ -175,8 +218,15 @@ const { actions, state } = store< VariableProductAddToCartWithOptionsStore >(
return context.selectedAttributes;
},
get isOptionSelected() {
- const { selectedValue, option } = getContext< Context >();
- return selectedValue === option.value;
+ const { selectedAttributes, option, name } =
+ getContext< Context >();
+
+ return selectedAttributes.some( ( attrObject ) => {
+ return (
+ attrObject.attribute === name &&
+ attrObject.value === option.value
+ );
+ } );
},
get isOptionDisabled() {
const { name, option, selectedAttributes } =
@@ -231,21 +281,78 @@ const { actions, state } = store< VariableProductAddToCartWithOptionsStore >(
}
},
handlePillClick() {
- if ( state.isOptionDisabled ) {
- return;
- }
const context = getContext< Context >();
- if ( context.selectedValue === context.option.value ) {
+
+ if ( state.isOptionSelected ) {
context.selectedValue = '';
} else {
context.selectedValue = context.option.value;
}
actions.setAttribute( context.name, context.selectedValue );
+ if ( context.selectedValue !== '' ) {
+ actions.autoselectAttributes( {
+ excludedAttributes: [ context.name ],
+ } );
+ }
},
handleDropdownChange( event: ChangeEvent< HTMLSelectElement > ) {
const context = getContext< Context >();
context.selectedValue = event.currentTarget.value;
actions.setAttribute( context.name, context.selectedValue );
+ if ( context.selectedValue !== '' ) {
+ actions.autoselectAttributes( {
+ excludedAttributes: [ context.name ],
+ } );
+ }
+ },
+ autoselectAttributes( {
+ includedAttributes = [],
+ excludedAttributes = [],
+ }: {
+ includedAttributes?: Array< string >;
+ excludedAttributes?: Array< string >;
+ } = {} ) {
+ const { autoselect, selectedAttributes } =
+ getContext< Context >();
+
+ if ( ! autoselect ) {
+ return;
+ }
+
+ const productObject: NormalizedProductData | null =
+ getProductData( productDataState.productId, [] );
+ if ( ! productObject ) {
+ return;
+ }
+ const productAttributesAndOptions: Record< string, string[] > =
+ getProductAttributesAndOptions( productObject );
+ Object.entries( productAttributesAndOptions ).forEach(
+ ( [ attribute, options ] ) => {
+ if (
+ includedAttributes.length !== 0 &&
+ ! includedAttributes.includes( attribute )
+ ) {
+ return;
+ }
+ if (
+ excludedAttributes.length !== 0 &&
+ excludedAttributes.includes( attribute )
+ ) {
+ return;
+ }
+ const validOptions = options.filter( ( option ) =>
+ isAttributeValueValid( {
+ attributeName: attribute,
+ attributeValue: option,
+ selectedAttributes,
+ } )
+ );
+ if ( validOptions.length === 1 ) {
+ const validOption = validOptions[ 0 ];
+ actions.setAttribute( attribute, validOption );
+ }
+ }
+ );
},
},
callbacks: {
@@ -255,6 +362,9 @@ const { actions, state } = store< VariableProductAddToCartWithOptionsStore >(
if ( context.selectedValue ) {
actions.setAttribute( context.name, context.selectedValue );
}
+ actions.autoselectAttributes( {
+ includedAttributes: [ context.name ],
+ } );
},
setSelectedVariationId: () => {
const { products } = getConfig( 'woocommerce' );
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts b/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts
index ca07f623af..2e53309bb4 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { test as base, expect, wpCLI } from '@woocommerce/e2e-utils';
+import { test as base, expect, wpCLI, Editor } from '@woocommerce/e2e-utils';
/**
* Internal dependencies
@@ -1026,4 +1026,421 @@ test.describe( 'Add to Cart + Options Block', () => {
editor.canvas.getByLabel( 'Block: Product Gallery' )
).toBeVisible();
} );
+
+ test.describe( 'autoselect behavior', () => {
+ const productSlug = 'autoselect-t-shirt';
+ const productName = 'Autoselect T-shirt';
+ const productPermalink = '/product/' + productSlug;
+ const productPrice = '13.99';
+ const productAttributes: {
+ name: string;
+ options: string[];
+ variation: boolean;
+ visible: boolean;
+ }[] = [
+ {
+ name: 'Type',
+ options: [ 'T-shirt' ],
+ variation: true,
+ visible: true,
+ },
+ {
+ name: 'Color',
+ options: [ 'Red', 'Blue', 'Green' ],
+ variation: true,
+ visible: true,
+ },
+ {
+ name: 'Size',
+ options: [ 'S', 'L', 'XL' ],
+ variation: true,
+ visible: true,
+ },
+ ];
+ const productVariations: {
+ attributes: {
+ name: string;
+ option: string;
+ }[];
+ }[] = [
+ {
+ attributes: [
+ {
+ name: 'Type',
+ option: 'T-shirt',
+ },
+ {
+ name: 'Color',
+ option: 'Green',
+ },
+ {
+ name: 'Size',
+ option: 'S',
+ },
+ ],
+ },
+ {
+ attributes: [
+ {
+ name: 'Type',
+ option: 'T-shirt',
+ },
+ {
+ name: 'Color',
+ option: 'Red',
+ },
+ {
+ name: 'Size',
+ option: 'L',
+ },
+ ],
+ },
+ {
+ attributes: [
+ {
+ name: 'Type',
+ option: 'T-shirt',
+ },
+ {
+ name: 'Color',
+ option: 'Red',
+ },
+ {
+ name: 'Size',
+ option: 'XL',
+ },
+ ],
+ },
+ {
+ attributes: [
+ {
+ name: 'Type',
+ option: 'T-shirt',
+ },
+ {
+ name: 'Color',
+ option: 'Blue',
+ },
+ {
+ name: 'Size',
+ option: 'XL',
+ },
+ ],
+ },
+ ];
+
+ async function setAddToCartWithOptionsBlockAttributes(
+ pageObject: AddToCartWithOptionsPage,
+ editor: Editor,
+ {
+ optionStyle = 'Pills',
+ autoselect = false,
+ disabledAttributesAction = 'disable',
+ }: {
+ optionStyle?: 'Pills' | 'Dropdown';
+ autoselect?: boolean;
+ disabledAttributesAction?: 'disable' | 'hide';
+ } = {}
+ ) {
+ const page = editor.page;
+ let isOnlyCurrentEntityDirty = true;
+
+ await pageObject.switchProductType( 'Variable product' );
+ await page.getByRole( 'tab', { name: 'Block' } ).click();
+ const addToCartWithOptionsBlock = editor.canvas.getByLabel(
+ 'Block: Add to Cart + Options'
+ );
+ await addToCartWithOptionsBlock.click();
+ await addToCartWithOptionsBlock
+ .getByLabel( 'Block: Variation Selector: Attribute Options' )
+ .first()
+ .click();
+
+ const optionStyleInput = page.getByRole( 'radio', {
+ name: optionStyle,
+ exact: true,
+ } );
+ if ( ! ( await optionStyleInput.isChecked() ) ) {
+ isOnlyCurrentEntityDirty = false;
+ await optionStyleInput.click();
+ }
+
+ const autoselectInput = page.getByRole( 'checkbox', {
+ name: 'Auto-select when only one attribute is compatible',
+ } );
+ const disabledAttributesActionInput =
+ page.getByLabel( 'Values in conflict' );
+ if (
+ ( await autoselectInput.isChecked() ) !== autoselect ||
+ ( await disabledAttributesActionInput.inputValue() ) !==
+ disabledAttributesAction
+ ) {
+ isOnlyCurrentEntityDirty = false;
+ }
+ await autoselectInput.setChecked( autoselect );
+ await disabledAttributesActionInput.selectOption( {
+ value: disabledAttributesAction,
+ } );
+ if (
+ await page
+ .getByRole( 'region', {
+ name: 'Editor top bar',
+ } )
+ .getByRole( 'button', {
+ name: 'Save',
+ exact: true,
+ } )
+ .isEnabled()
+ ) {
+ await editor.saveSiteEditorEntities( {
+ isOnlyCurrentEntityDirty,
+ } );
+ }
+ }
+
+ test.beforeEach( async () => {
+ const cliOutput = await wpCLI(
+ `wc product create --user=1 --slug="${ productSlug }" --name="${ productName }" --type="variable" --attributes='${ JSON.stringify(
+ productAttributes
+ ) }'`
+ );
+ const match: RegExpMatchArray | null = cliOutput.stdout.match(
+ /Success:\s+Created\s+product\s+(\d+)\.\n?$/
+ );
+ const productId: string | null = match ? match[ 1 ] : null;
+ if ( ! productId ) {
+ throw new Error(
+ `No productId found, cliOutput: ${ JSON.stringify(
+ cliOutput,
+ null,
+ 2
+ ) }`
+ );
+ }
+
+ for ( const productVariation of productVariations ) {
+ await wpCLI(
+ `wc product_variation create --user=1 "${ productId }" --regular_price="${ productPrice }" --attributes='${ JSON.stringify(
+ productVariation.attributes
+ ) }'`
+ );
+ }
+ } );
+
+ for ( const optionStyle of [ 'Pills', 'Dropdown' ] as (
+ | 'Pills'
+ | 'Dropdown'
+ )[] ) {
+ // eslint-disable-next-line playwright/expect-expect
+ test( `${ optionStyle }: Test the autoselect block attribute`, async ( {
+ page,
+ pageObject,
+ editor,
+ } ) => {
+ await pageObject.updateSingleProductTemplate();
+ await setAddToCartWithOptionsBlockAttributes(
+ pageObject,
+ editor,
+ { optionStyle }
+ );
+
+ await test.step( `${ optionStyle }: Expect NOTHING to be auto-selected (on page load)`, async () => {
+ await page.goto( productPermalink );
+
+ await pageObject.expectSelectedAttributes(
+ productAttributes,
+ { Type: '', Color: '', Size: '' },
+ optionStyle
+ );
+ } );
+
+ await test.step( `${ optionStyle }: Expect attributes to NOT auto-select when user selects something`, async () => {
+ await page.goto( productPermalink );
+
+ await pageObject.selectVariationSelectorOptionsBlockAttribute(
+ 'Color',
+ 'Blue',
+ optionStyle
+ );
+
+ // Expect nothing to be auto-selected
+ await pageObject.expectSelectedAttributes(
+ productAttributes,
+ { Type: '', Color: 'Blue', Size: '' },
+ optionStyle
+ );
+ } );
+
+ await test.step( `${ optionStyle }: Set the autoselect setting to true`, async () => {
+ await pageObject.updateSingleProductTemplate();
+ await setAddToCartWithOptionsBlockAttributes(
+ pageObject,
+ editor,
+ { optionStyle, autoselect: true }
+ );
+ } );
+
+ await test.step( `${ optionStyle }: Expect only the Type attribute to be auto-selected (on page load)`, async () => {
+ await page.goto( productPermalink );
+
+ // Expect the Type attribute to be auto-selected (on page load) to "T-shirt", the rest of the attributes should not be selected.
+ await pageObject.expectSelectedAttributes(
+ productAttributes,
+ { Type: 'T-shirt', Color: '', Size: '' },
+ optionStyle
+ );
+ } );
+
+ await test.step( `${ optionStyle }: Expect attributes to auto-select when user selects something`, async () => {
+ await page.goto( productPermalink );
+
+ // By setting the Color to "Blue", we expect the Type attribute to be auto-selected to "T-shirt", and the Size to "XL".
+ await pageObject.selectVariationSelectorOptionsBlockAttribute(
+ 'Color',
+ 'Blue',
+ optionStyle
+ );
+
+ await pageObject.expectSelectedAttributes(
+ productAttributes,
+ { Type: 'T-shirt', Color: 'Blue', Size: 'XL' },
+ optionStyle
+ );
+ } );
+ } );
+ test( `${ optionStyle }: Test the disabledAttributesAction block attribute`, async ( {
+ page,
+ pageObject,
+ editor,
+ } ) => {
+ await test.step( `${ optionStyle }: Set the disabledAttributesAction block attribute to "disable"`, async () => {
+ await pageObject.updateSingleProductTemplate();
+ await setAddToCartWithOptionsBlockAttributes(
+ pageObject,
+ editor,
+ {
+ optionStyle,
+ disabledAttributesAction: 'disable',
+ }
+ );
+ } );
+ await test.step( `${ optionStyle }: Expect invalid options to be disabled (by prop) and visible`, async () => {
+ await page.goto( productPermalink );
+
+ // By setting the Color to "Blue", the only possible Size remaining is "XL".
+ await pageObject.selectVariationSelectorOptionsBlockAttribute(
+ 'Color',
+ 'Blue',
+ optionStyle
+ );
+
+ await expect(
+ page
+ .getByLabel( 'Size' )
+ .getByText( 'L', { exact: true } )
+ ).toBeDisabled();
+ await expect(
+ page
+ .getByLabel( 'Size' )
+ .getByText( 'L', { exact: true } )
+ ).not.toHaveAttribute( 'hidden' );
+ } );
+
+ await test.step( `${ optionStyle }: Set the disabledAttributesAction block attribute to "hide"`, async () => {
+ await pageObject.updateSingleProductTemplate();
+ await setAddToCartWithOptionsBlockAttributes(
+ pageObject,
+ editor,
+ {
+ optionStyle,
+ disabledAttributesAction: 'hide',
+ }
+ );
+ } );
+ await test.step( `${ optionStyle }: Expect invalid options to be isabled (by prop) and hidden`, async () => {
+ await page.goto( productPermalink );
+
+ // By setting the Color to "Blue", the only possible Size remaining is "XL".
+ await pageObject.selectVariationSelectorOptionsBlockAttribute(
+ 'Color',
+ 'Blue',
+ optionStyle
+ );
+
+ await expect(
+ page
+ .getByLabel( 'Size' )
+ .getByText( 'L', { exact: true } )
+ ).toBeDisabled();
+ await expect(
+ page
+ .getByLabel( 'Size' )
+ .getByText( 'L', { exact: true } )
+ ).toBeHidden();
+ } );
+ } );
+ // eslint-disable-next-line playwright/expect-expect
+ test( `${ optionStyle }: Combining autoselect and disabledAttributesAction block attributes should work`, async ( {
+ page,
+ pageObject,
+ editor,
+ } ) => {
+ for ( const disabledAttributesAction of [
+ 'disable',
+ 'hide',
+ ] as ( 'disable' | 'hide' )[] ) {
+ await pageObject.updateSingleProductTemplate();
+
+ await test.step( `${ optionStyle }: Set the disabledAttributesAction block attribute to "${ disabledAttributesAction }"`, async () => {
+ await setAddToCartWithOptionsBlockAttributes(
+ pageObject,
+ editor,
+ {
+ autoselect: true,
+ optionStyle,
+ disabledAttributesAction,
+ }
+ );
+ } );
+ await test.step( `disabledAttributesAction === ${ disabledAttributesAction }: Expect options to be properly auto-selected`, async () => {
+ await page.goto( productPermalink );
+
+ // By selecting the Color to "Blue", the only possible Size remaining is "XL".
+ await pageObject.selectVariationSelectorOptionsBlockAttribute(
+ 'Color',
+ 'Blue',
+ optionStyle
+ );
+ // Now, we deselect the Color.
+ await pageObject.selectVariationSelectorOptionsBlockAttribute(
+ 'Color',
+ '',
+ optionStyle
+ );
+ // Now, the attributes should look like this:
+ // Type: T-shirt
+ // Color: ''
+ // Size: XL
+ // Because the Size is XL, the only Colors possible are Red and Blue.
+ // Now if we select Size: S, the Color should auto-select to Green.
+ await pageObject.selectVariationSelectorOptionsBlockAttribute(
+ 'Size',
+ 'S',
+ optionStyle
+ );
+ // Now, the options should look like this:
+ // Type: T-shirt
+ // Color: Green
+ // Size: S
+
+ await pageObject.expectSelectedAttributes(
+ productAttributes,
+ { Type: 'T-shirt', Color: 'Green', Size: 'S' },
+ optionStyle
+ );
+ } );
+ }
+ } );
+ }
+ } );
} );
diff --git a/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.page.ts b/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.page.ts
index e22f0ac2ae..a193e036e9 100644
--- a/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.page.ts
+++ b/plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.page.ts
@@ -2,7 +2,12 @@
* External dependencies
*/
import { Page } from '@playwright/test';
-import { Editor, Admin, BLOCK_THEME_SLUG } from '@woocommerce/e2e-utils';
+import {
+ expect,
+ Editor,
+ Admin,
+ BLOCK_THEME_SLUG,
+} from '@woocommerce/e2e-utils';
class AddToCartWithOptionsPage {
private page: Page;
@@ -27,9 +32,16 @@ class AddToCartWithOptionsPage {
async switchProductType( productType: string ) {
await this.page.getByRole( 'tab', { name: 'Template' } ).click();
- await this.page
- .getByRole( 'button', { name: 'Product Type', exact: true } )
- .click();
+ const productTypePanel = this.page.getByRole( 'button', {
+ name: 'Product Type',
+ exact: true,
+ } );
+ if (
+ ( await productTypePanel.getAttribute( 'aria-expanded' ) ) !==
+ 'true'
+ ) {
+ await productTypePanel.click();
+ }
await this.page
.getByLabel( 'Type switcher' )
.selectOption( { label: productType } );
@@ -76,13 +88,15 @@ class AddToCartWithOptionsPage {
const addToCartFormBlock = await this.editor.getBlockByName(
'woocommerce/add-to-cart-form'
);
- await this.editor.selectBlocks( addToCartFormBlock );
+ if ( await addToCartFormBlock.isVisible() ) {
+ await this.editor.selectBlocks( addToCartFormBlock );
- await this.page
- .getByRole( 'button', {
- name: 'Use the Add to Cart + Options block',
- } )
- .click();
+ await this.page
+ .getByRole( 'button', {
+ name: 'Use the Add to Cart + Options block',
+ } )
+ .click();
+ }
}
async updateSingleProductTemplate() {
@@ -120,6 +134,88 @@ class AddToCartWithOptionsPage {
await this.editor.publishAndVisitPost();
}
+
+ async selectVariationSelectorOptionsBlockAttribute(
+ attributeName: string,
+ attributeValue: string,
+ optionStyle: 'Pills' | 'Dropdown'
+ ) {
+ if ( optionStyle === 'Dropdown' ) {
+ await this.page
+ .getByLabel( attributeName )
+ .selectOption( attributeValue );
+ return;
+ }
+ if ( attributeValue !== '' ) {
+ await this.page
+ .getByLabel( attributeName )
+ .getByText( attributeValue )
+ .click();
+ return;
+ }
+ await this.page
+ .getByLabel( attributeName )
+ .locator( 'label:has(:checked)' )
+ .click();
+ }
+
+ async expectSelectedAttributes(
+ productAttributes: {
+ name: string;
+ options: string[];
+ variation: boolean;
+ visible: boolean;
+ }[],
+ expectedValues: Record< string, string | RegExp > = {},
+ optionStyle: 'Pills' | 'Dropdown'
+ ) {
+ for ( let {
+ name: attributeName,
+ options: attributeValues,
+ } of productAttributes ) {
+ const attributeNameLocator = this.page.getByLabel( attributeName, {
+ exact: true,
+ } );
+ if ( optionStyle === 'Dropdown' ) {
+ let expectedValue: string | RegExp;
+ if (
+ attributeName in expectedValues &&
+ expectedValues[ attributeName ] !== ''
+ ) {
+ expectedValue = expectedValues[ attributeName ];
+ } else {
+ expectedValue = '';
+ }
+ await expect( attributeNameLocator ).toHaveValue(
+ expectedValue
+ );
+ return;
+ }
+ if (
+ attributeName in expectedValues &&
+ expectedValues[ attributeName ] !== ''
+ ) {
+ attributeValues = attributeValues.filter(
+ ( item ) => item !== expectedValues[ attributeName ]
+ ); // Omit attributeName
+ await expect(
+ attributeNameLocator.getByLabel(
+ expectedValues[ attributeName ],
+ { exact: true }
+ )
+ ).toBeChecked();
+ }
+ if ( attributeValues.length ) {
+ for ( const attributeValue of attributeValues ) {
+ await expect(
+ attributeNameLocator.getByLabel( attributeValue, {
+ exact: true,
+ } )
+ ).not.toBeChecked();
+ }
+ }
+ }
+ }
}
export default AddToCartWithOptionsPage;
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeOptions.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeOptions.php
index 96d70c60aa..fd637c3ba9 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeOptions.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/VariationSelectorAttributeOptions.php
@@ -139,9 +139,11 @@ class VariationSelectorAttributeOptions extends AbstractBlock {
* @return string The pills.
*/
protected function render_pills( $attributes, $content, $block ) {
- $attribute_id = $block->context['woocommerce/attributeId'];
- $attribute_slug = wc_variation_attribute_name( $block->context['woocommerce/attributeName'] );
- $attribute_terms = $block->context['woocommerce/attributeTerms'];
+ $attribute_id = $block->context['woocommerce/attributeId'];
+ $attribute_slug = wc_variation_attribute_name( $block->context['woocommerce/attributeName'] );
+ $attribute_terms = $block->context['woocommerce/attributeTerms'];
+ $autoselect = $attributes['autoselect'] ?? false;
+ $disabled_attributes_action = $attributes['disabledAttributesAction'] ?? 'disable';
wp_interactivity_state(
'woocommerce/add-to-cart-with-options',
@@ -166,6 +168,7 @@ class VariationSelectorAttributeOptions extends AbstractBlock {
'value' => $attribute_term['value'],
'data-wp-bind--checked' => 'state.isOptionSelected',
'data-wp-bind--disabled' => 'state.isOptionDisabled',
+ 'data-wp-bind--hidden' => 'hide' === $disabled_attributes_action ? 'state.isOptionDisabled' : null,
'data-wp-watch' => 'callbacks.watchSelected',
'data-wp-on--click' => 'actions.handlePillClick',
'data-wp-on--keydown' => 'actions.handleKeyDown',
@@ -192,6 +195,7 @@ class VariationSelectorAttributeOptions extends AbstractBlock {
'options' => $attribute_terms,
'selectedValue' => $this->get_default_selected_attribute( $attribute_slug, $attribute_terms ),
'focused' => '',
+ 'autoselect' => $autoselect,
),
'data-wp-init' => 'callbacks.setDefaultSelectedAttribute',
),
@@ -223,13 +227,17 @@ class VariationSelectorAttributeOptions extends AbstractBlock {
$attribute_terms
);
- $selected_attribute = $this->get_default_selected_attribute( $attribute_slug, $attribute_terms );
+ $selected_attribute = $this->get_default_selected_attribute( $attribute_slug, $attribute_terms );
+ $autoselect = $attributes['autoselect'] ?? false;
+ $disabled_attributes_action = $attributes['disabledAttributesAction'] ?? 'disable';
$options = '';
foreach ( $attribute_terms as $attribute_term ) {
$option_attributes = array(
'value' => $attribute_term['value'],
+ 'data-wp-bind--selected' => 'state.isOptionSelected',
'data-wp-bind--disabled' => 'state.isOptionDisabled',
+ 'data-wp-bind--hidden' => 'hide' === $disabled_attributes_action ? 'state.isOptionDisabled' : null,
'data-wp-context' => array(
'option' => $attribute_term,
'name' => $attribute_slug,
@@ -260,6 +268,7 @@ class VariationSelectorAttributeOptions extends AbstractBlock {
'name' => $attribute_slug,
'options' => $attribute_terms,
'selectedValue' => $selected_attribute,
+ 'autoselect' => $autoselect,
),
'data-wp-init' => 'callbacks.setDefaultSelectedAttribute',
'data-wp-on--change' => 'actions.handleDropdownChange',