Cherry pick 49929 into release/9.2 (#50094)
* Add validation to select fields and add placeholder option to additional fields API (#49929) * Add validation and empty value to select components * address feedback * Add changefile(s) from automation for the following project(s): woocommerce-blocks, woocommerce * remove console log * Update plugins/woocommerce-blocks/docs/third-party-developers/extensibility/checkout-block/additional-checkout-fields.md Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> * Update plugins/woocommerce-blocks/docs/third-party-developers/extensibility/checkout-block/additional-checkout-fields.md Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> * remove test * Fix tests * also move select to parent * fix tests * add empty enum to schema --------- Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> * Prep for cherry pick 49929 --------- Co-authored-by: Seghir Nadir <nadir.seghir@gmail.com> Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> Co-authored-by: WooCommerce Bot <no-reply@woocommerce.com>
This commit is contained in:
parent
289e6b9652
commit
05cf21536d
|
@ -34,7 +34,11 @@ import prepareFormFields from './prepare-form-fields';
|
|||
import validateCountry from './validate-country';
|
||||
import customValidationHandler from './custom-validation-handler';
|
||||
import AddressLineFields from './address-line-fields';
|
||||
import { createFieldProps, getFieldData } from './utils';
|
||||
import {
|
||||
createFieldProps,
|
||||
createCheckboxFieldProps,
|
||||
getFieldData,
|
||||
} from './utils';
|
||||
import { Select } from '../../select';
|
||||
import { validateState } from './validate-state';
|
||||
|
||||
|
@ -161,6 +165,8 @@ const Form = < T extends AddressFormValues | ContactFormValues >( {
|
|||
}
|
||||
|
||||
const fieldProps = createFieldProps( field, id, addressType );
|
||||
const checkboxFieldProps =
|
||||
createCheckboxFieldProps( fieldProps );
|
||||
|
||||
if ( field.key === 'email' ) {
|
||||
fieldProps.id = 'email';
|
||||
|
@ -178,7 +184,7 @@ const Form = < T extends AddressFormValues | ContactFormValues >( {
|
|||
[ field.key ]: checked,
|
||||
} );
|
||||
} }
|
||||
{ ...fieldProps }
|
||||
{ ...checkboxFieldProps }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -292,6 +298,10 @@ const Form = < T extends AddressFormValues | ContactFormValues >( {
|
|||
} );
|
||||
} }
|
||||
options={ field.options }
|
||||
required={ field.required }
|
||||
errorMessage={
|
||||
fieldProps.errorMessage || undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ export interface FieldProps {
|
|||
autoComplete: string | undefined;
|
||||
errorMessage: string | undefined;
|
||||
required: boolean | undefined;
|
||||
placeholder: string | undefined;
|
||||
className: string;
|
||||
}
|
||||
|
||||
|
@ -36,6 +37,7 @@ export const createFieldProps = (
|
|||
autoComplete: field?.autocomplete,
|
||||
errorMessage: field?.errorMessage,
|
||||
required: field?.required,
|
||||
placeholder: field?.placeholder,
|
||||
className: `wc-block-components-address-form__${ field?.key }`.replaceAll(
|
||||
'/',
|
||||
'-'
|
||||
|
@ -43,6 +45,17 @@ export const createFieldProps = (
|
|||
...field?.attributes,
|
||||
} );
|
||||
|
||||
export const createCheckboxFieldProps = ( fieldProps: FieldProps ) => {
|
||||
const {
|
||||
errorId,
|
||||
errorMessage,
|
||||
autoCapitalize,
|
||||
autoComplete,
|
||||
placeholder,
|
||||
...rest
|
||||
} = fieldProps;
|
||||
return rest;
|
||||
};
|
||||
export const getFieldData = < T extends AddressFormValues | ContactFormValues >(
|
||||
key: 'address_1' | 'address_2',
|
||||
fields: AddressFormFields[ 'fields' ],
|
||||
|
|
|
@ -4,10 +4,6 @@
|
|||
import { useMemo } from '@wordpress/element';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import clsx from 'clsx';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { ValidationInputError } from '@woocommerce/blocks-components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -25,44 +21,19 @@ export const CountryInput = ( {
|
|||
value = '',
|
||||
autoComplete = 'off',
|
||||
required = false,
|
||||
errorId,
|
||||
}: CountryInputWithCountriesProps ): JSX.Element => {
|
||||
const emptyCountryOption: SelectOption = {
|
||||
value: '',
|
||||
label: sprintf(
|
||||
// translators: %s will be label of the country input. For example "country/region".
|
||||
__( 'Select a %s', 'woocommerce' ),
|
||||
label?.toLowerCase()
|
||||
),
|
||||
disabled: true,
|
||||
};
|
||||
const options = useMemo< SelectOption[] >( () => {
|
||||
return [ emptyCountryOption ].concat(
|
||||
Object.entries( countries ).map(
|
||||
return Object.entries( countries ).map(
|
||||
( [ countryCode, countryName ] ) => ( {
|
||||
value: countryCode,
|
||||
label: decodeEntities( countryName ),
|
||||
} )
|
||||
)
|
||||
);
|
||||
}, [ countries ] );
|
||||
|
||||
const validationError = useSelect( ( select ) => {
|
||||
const store = select( VALIDATION_STORE_KEY );
|
||||
return (
|
||||
store.getValidationError( errorId || '' ) || {
|
||||
hidden: true,
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ clsx( className, 'wc-block-components-country-input', {
|
||||
'has-error': ! validationError.hidden,
|
||||
} ) }
|
||||
>
|
||||
<Select
|
||||
className={ clsx( className, 'wc-block-components-country-input' ) }
|
||||
id={ id }
|
||||
label={ label || '' }
|
||||
onChange={ onChange }
|
||||
|
@ -71,12 +42,6 @@ export const CountryInput = ( {
|
|||
required={ required }
|
||||
autoComplete={ autoComplete }
|
||||
/>
|
||||
{ validationError && validationError.hidden !== true && (
|
||||
<ValidationInputError
|
||||
errorMessage={ validationError.message }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,12 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { Icon, chevronDown } from '@wordpress/icons';
|
||||
import { useCallback, useId } from '@wordpress/element';
|
||||
import { useCallback, useId, useMemo, useEffect } from '@wordpress/element';
|
||||
import { sprintf, __ } from '@wordpress/i18n';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import clsx from 'clsx';
|
||||
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { ValidationInputError } from '@woocommerce/blocks-components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -22,12 +27,25 @@ type SelectProps = Omit<
|
|||
options: SelectOption[];
|
||||
label: string;
|
||||
onChange: ( newVal: string ) => void;
|
||||
errorId?: string;
|
||||
required?: boolean;
|
||||
errorMessage?: string;
|
||||
};
|
||||
|
||||
export const Select = ( props: SelectProps ) => {
|
||||
const { onChange, options, label, value, className, size, ...restOfProps } =
|
||||
props;
|
||||
|
||||
const {
|
||||
onChange,
|
||||
options,
|
||||
label,
|
||||
value = '',
|
||||
className,
|
||||
size,
|
||||
errorId: incomingErrorId,
|
||||
required,
|
||||
errorMessage = __( 'Please select a valid option', 'woocommerce' ),
|
||||
placeholder,
|
||||
...restOfProps
|
||||
} = props;
|
||||
const selectOnChange = useCallback(
|
||||
( event: React.ChangeEvent< HTMLSelectElement > ) => {
|
||||
onChange( event.target.value );
|
||||
|
@ -35,13 +53,83 @@ export const Select = ( props: SelectProps ) => {
|
|||
[ onChange ]
|
||||
);
|
||||
|
||||
const generatedId = useId();
|
||||
const emptyOption: SelectOption = useMemo(
|
||||
() => ( {
|
||||
value: '',
|
||||
label:
|
||||
placeholder ??
|
||||
sprintf(
|
||||
// translators: %s will be label of the field. For example "country/region".
|
||||
__( 'Select a %s', 'woocommerce' ),
|
||||
label?.toLowerCase()
|
||||
),
|
||||
disabled: !! required,
|
||||
} ),
|
||||
[ label, placeholder, required ]
|
||||
);
|
||||
|
||||
const generatedId = useId();
|
||||
const inputId =
|
||||
restOfProps.id || `wc-blocks-components-select-${ generatedId }`;
|
||||
const errorId = incomingErrorId || inputId;
|
||||
|
||||
const optionsWithEmpty = useMemo< SelectOption[] >( () => {
|
||||
if ( required && value ) {
|
||||
return options;
|
||||
}
|
||||
return [ emptyOption ].concat( options );
|
||||
}, [ required, value, emptyOption, options ] );
|
||||
|
||||
const { setValidationErrors, clearValidationError } =
|
||||
useDispatch( VALIDATION_STORE_KEY );
|
||||
|
||||
const { error, validationErrorId } = useSelect( ( select ) => {
|
||||
const store = select( VALIDATION_STORE_KEY );
|
||||
return {
|
||||
error: store.getValidationError( errorId ),
|
||||
validationErrorId: store.getValidationErrorId( errorId ),
|
||||
};
|
||||
} );
|
||||
|
||||
useEffect( () => {
|
||||
if ( ! required || value ) {
|
||||
clearValidationError( errorId );
|
||||
} else {
|
||||
setValidationErrors( {
|
||||
[ errorId ]: {
|
||||
message: errorMessage,
|
||||
hidden: true,
|
||||
},
|
||||
} );
|
||||
}
|
||||
return () => {
|
||||
clearValidationError( errorId );
|
||||
};
|
||||
}, [
|
||||
clearValidationError,
|
||||
value,
|
||||
errorId,
|
||||
errorMessage,
|
||||
required,
|
||||
setValidationErrors,
|
||||
] );
|
||||
|
||||
const validationError = useSelect( ( select ) => {
|
||||
const store = select( VALIDATION_STORE_KEY );
|
||||
return (
|
||||
store.getValidationError( errorId || '' ) || {
|
||||
hidden: true,
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
return (
|
||||
<div className={ `wc-blocks-components-select ${ className || '' }` }>
|
||||
<div
|
||||
className={ clsx( className, {
|
||||
'has-error': ! validationError.hidden,
|
||||
} ) }
|
||||
>
|
||||
<div className="wc-blocks-components-select">
|
||||
<div className="wc-blocks-components-select__container">
|
||||
<label
|
||||
htmlFor={ inputId }
|
||||
|
@ -55,9 +143,13 @@ export const Select = ( props: SelectProps ) => {
|
|||
size={ size !== undefined ? size : 1 }
|
||||
onChange={ selectOnChange }
|
||||
value={ value }
|
||||
aria-invalid={
|
||||
error?.message && ! error?.hidden ? true : false
|
||||
}
|
||||
aria-errormessage={ validationErrorId }
|
||||
{ ...restOfProps }
|
||||
>
|
||||
{ options.map( ( option ) => (
|
||||
{ optionsWithEmpty.map( ( option ) => (
|
||||
<option
|
||||
key={ option.value }
|
||||
value={ option.value }
|
||||
|
@ -78,5 +170,7 @@ export const Select = ( props: SelectProps ) => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ValidationInputError propertyName={ errorId } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,13 +3,7 @@
|
|||
*/
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import { useCallback, useMemo, useEffect, useRef } from '@wordpress/element';
|
||||
import {
|
||||
ValidatedTextInput,
|
||||
ValidationInputError,
|
||||
} from '@woocommerce/blocks-components';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { ValidatedTextInput } from '@woocommerce/blocks-components';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
/**
|
||||
|
@ -41,40 +35,17 @@ const StateInput = ( {
|
|||
autoComplete = 'off',
|
||||
value = '',
|
||||
required = false,
|
||||
errorId,
|
||||
}: StateInputWithStatesProps ): JSX.Element => {
|
||||
const countryStates = states[ country ];
|
||||
const options = useMemo< SelectOption[] >( () => {
|
||||
if ( countryStates && Object.keys( countryStates ).length > 0 ) {
|
||||
const emptyStateOption: SelectOption = {
|
||||
value: '',
|
||||
label: sprintf(
|
||||
/* translators: %s will be the type of province depending on country, e.g "state" or "state/county" or "department" */
|
||||
__( 'Select a %s', 'woocommerce' ),
|
||||
label?.toLowerCase()
|
||||
),
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
return [
|
||||
emptyStateOption,
|
||||
...Object.keys( countryStates ).map( ( key ) => ( {
|
||||
return Object.keys( countryStates ).map( ( key ) => ( {
|
||||
value: key,
|
||||
label: decodeEntities( countryStates[ key ] ),
|
||||
} ) ),
|
||||
];
|
||||
} ) );
|
||||
}
|
||||
return [];
|
||||
}, [ countryStates, label ] );
|
||||
|
||||
const validationError = useSelect( ( select ) => {
|
||||
const store = select( VALIDATION_STORE_KEY );
|
||||
return (
|
||||
store.getValidationError( errorId || '' ) || {
|
||||
hidden: true,
|
||||
}
|
||||
);
|
||||
} );
|
||||
}, [ countryStates ] );
|
||||
|
||||
/**
|
||||
* Handles state selection onChange events. Finds a matching state by key or value.
|
||||
|
@ -117,35 +88,19 @@ const StateInput = ( {
|
|||
|
||||
if ( options.length > 0 ) {
|
||||
return (
|
||||
<div
|
||||
<Select
|
||||
className={ clsx(
|
||||
className,
|
||||
'wc-block-components-state-input',
|
||||
{
|
||||
'has-error': ! validationError.hidden,
|
||||
}
|
||||
'wc-block-components-state-input'
|
||||
) }
|
||||
>
|
||||
<Select
|
||||
options={ options }
|
||||
label={ label || '' }
|
||||
className={ `${ className || '' }` }
|
||||
id={ id }
|
||||
onChange={ ( newValue ) => {
|
||||
if ( required ) {
|
||||
}
|
||||
onChangeState( newValue );
|
||||
} }
|
||||
onChange={ onChangeState }
|
||||
value={ value }
|
||||
autoComplete={ autoComplete }
|
||||
required={ required }
|
||||
/>
|
||||
{ validationError && validationError.hidden !== true && (
|
||||
<ValidationInputError
|
||||
errorMessage={ validationError.message }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -40,6 +40,8 @@ export interface FormField {
|
|||
type?: string;
|
||||
// The options if this is a select field
|
||||
options?: ComboboxControlOption[];
|
||||
// The placeholder for the field, only applicable for select fields.
|
||||
placeholder?: string;
|
||||
// Additional attributes added when registering a field. String in key is required for data attributes.
|
||||
attributes?: Record< keyof CustomFieldAttributes, string >;
|
||||
}
|
||||
|
|
|
@ -292,12 +292,15 @@ As well as the options above, text fields also support a `required` option. If t
|
|||
|
||||
As well as the options above, select fields must also be registered with an `options` option. This is used to specify what options the shopper can select.
|
||||
|
||||
Select fields can also be marked as required. If they are not (i.e. they are optional), then an empty entry will be added to allow the shopper to unset the field.
|
||||
Select fields will mount with no value selected by default, if the field is required, the user will be required to select a value.
|
||||
|
||||
You can set a placeholder to be shown on the select by passing a `placeholder` value when registering the field. This will be the first option in the select and will not be selectable if the field is required.
|
||||
|
||||
| Option name | Description | Required? | Example | Default value |
|
||||
|-----|-----|-----|----------------|--------------|
|
||||
| `options` | An array of options to show in the select input. Each options must be an array containing a `label` and `value` property. Each entry must have a unique `value`. Any duplicate options will be removed. The `value` is what gets submitted to the server during checkout and the `label` is simply a user-friendly representation of this value. It is not transmitted to the server in any way. | Yes | see below | No default - this must be provided. |
|
||||
| `required` | If this is `true` then the shopper _must_ provide a value for this field during the checkout process. | No | `true` | `false` |
|
||||
| `placeholder` | If this value is set, the shopper will see this option in the select. If the select is required, the shopper cannot select this option. | No | `Select a role | Select a $label |
|
||||
|
||||
##### Example of `options` value
|
||||
|
||||
|
@ -427,6 +430,7 @@ add_action(
|
|||
array(
|
||||
'id' => 'namespace/how-did-you-hear-about-us',
|
||||
'label' => 'How did you hear about us?',
|
||||
'placeholder' => 'Select a source',
|
||||
'location' => 'order',
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
|
@ -463,7 +467,7 @@ This results in the order information section being rendered like so:
|
|||
|
||||
![The select input when focused](https://github.com/woocommerce/woocommerce/assets/5656702/bd943906-621b-404f-aa84-b951323e25d3)
|
||||
|
||||
If it is undesirable to force the shopper to select a value, providing a value such as "None of the above" may help.
|
||||
If it is undesirable to force the shopper to select a value, mark the select as optional by setting the `required` option to `false`.
|
||||
|
||||
## Validation and sanitization
|
||||
|
||||
|
|
|
@ -32,6 +32,8 @@ export const CheckboxControl = ( {
|
|||
hasError = false,
|
||||
checked = false,
|
||||
disabled = false,
|
||||
errorId,
|
||||
errorMessage,
|
||||
...rest
|
||||
}: CheckboxControlProps & Record< string, unknown > ): JSX.Element => {
|
||||
const instanceId = useInstanceId( CheckboxControl );
|
||||
|
|
|
@ -96,39 +96,21 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
shipping: {
|
||||
'Government ID': '12345',
|
||||
'Confirm government ID': '12345',
|
||||
'How wide is your road? (optional)': 'wide',
|
||||
},
|
||||
billing: {
|
||||
'Government ID': '54321',
|
||||
'Confirm government ID': '54321',
|
||||
'How wide is your road? (optional)': 'narrow',
|
||||
},
|
||||
},
|
||||
order: {
|
||||
'How did you hear about us?': 'Other',
|
||||
'How did you hear about us? (optional)': 'other',
|
||||
'What is your favourite colour?': 'Blue',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Fill select fields "manually" (Not part of "fillInCheckoutWithTestData"). This is a workaround for select
|
||||
// fields until we recreate th Combobox component. This is because the aria-label includes the value so getting
|
||||
// by label alone is not reliable unless we know the value.
|
||||
await checkoutPageObject.page
|
||||
.getByRole( 'group', {
|
||||
name: 'Shipping address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.selectOption( 'wide' );
|
||||
await checkoutPageObject.page
|
||||
.getByRole( 'group', {
|
||||
name: 'Billing address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.selectOption( 'narrow' );
|
||||
|
||||
await checkoutPageObject.page.evaluate(
|
||||
'document.activeElement.blur()'
|
||||
);
|
||||
|
||||
await checkoutPageObject.page
|
||||
.getByLabel( 'Would you like a free gift with your order?' )
|
||||
.check();
|
||||
|
@ -246,7 +228,7 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
.getByRole( 'group', {
|
||||
name: 'Shipping address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.getByLabel( 'How wide is your road? (optional)' )
|
||||
).toHaveValue( 'wide' );
|
||||
await expect(
|
||||
checkoutPageObject.page
|
||||
|
@ -274,7 +256,7 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
.getByRole( 'group', {
|
||||
name: 'Billing address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.getByLabel( 'How wide is your road? (optional)' )
|
||||
).toHaveValue( 'narrow' );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -52,10 +52,12 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
shipping: {
|
||||
'Government ID': '12345',
|
||||
'Confirm government ID': '12345',
|
||||
'How wide is your road? (optional)': 'wide',
|
||||
},
|
||||
billing: {
|
||||
'Government ID': '54321',
|
||||
'Confirm government ID': '54321',
|
||||
'How wide is your road? (optional)': 'narrow',
|
||||
},
|
||||
},
|
||||
order: {
|
||||
|
@ -65,26 +67,6 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
}
|
||||
);
|
||||
|
||||
// Fill select fields "manually" (Not part of "fillInCheckoutWithTestData"). This is a workaround for select
|
||||
// fields until we recreate th Combobox component. This is because the aria-label includes the value so getting
|
||||
// by label alone is not reliable unless we know the value.
|
||||
await checkoutPageObject.page
|
||||
.getByRole( 'group', {
|
||||
name: 'Shipping address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.selectOption( 'wide' );
|
||||
await checkoutPageObject.page
|
||||
.getByRole( 'group', {
|
||||
name: 'Billing address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.selectOption( 'narrow' );
|
||||
|
||||
await checkoutPageObject.page.evaluate(
|
||||
'document.activeElement.blur()'
|
||||
);
|
||||
|
||||
await checkoutPageObject.page
|
||||
.getByLabel( 'Would you like a free gift with your order?' )
|
||||
.check();
|
||||
|
@ -189,7 +171,7 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
.getByRole( 'group', {
|
||||
name: 'Shipping address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.getByLabel( 'How wide is your road? (optional)' )
|
||||
).toHaveValue( 'wide' );
|
||||
await expect(
|
||||
checkoutPageObject.page
|
||||
|
@ -217,7 +199,7 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
.getByRole( 'group', {
|
||||
name: 'Billing address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.getByLabel( 'How wide is your road? (optional)' )
|
||||
).toHaveValue( 'narrow' );
|
||||
} );
|
||||
|
||||
|
@ -240,10 +222,12 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
shipping: {
|
||||
'Government ID': '12345',
|
||||
'Confirm government ID': '12345',
|
||||
'How wide is your road? (optional)': 'wide',
|
||||
},
|
||||
billing: {
|
||||
'Government ID': '54321',
|
||||
'Confirm government ID': '54321',
|
||||
'How wide is your road? (optional)': 'narrow',
|
||||
},
|
||||
},
|
||||
order: {
|
||||
|
@ -253,36 +237,23 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
}
|
||||
);
|
||||
|
||||
// Fill select fields "manually" (Not part of "fillInCheckoutWithTestData"). This is a workaround for select
|
||||
// fields until we recreate the Combobox component. This is because the aria-label includes the value so getting
|
||||
// by label alone is not reliable unless we know the value.
|
||||
await checkoutPageObject.page
|
||||
.getByRole( 'group', {
|
||||
name: 'Shipping address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.selectOption( 'wide' );
|
||||
await checkoutPageObject.page
|
||||
.getByRole( 'group', {
|
||||
name: 'Billing address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.selectOption( 'narrow' );
|
||||
await checkoutPageObject.waitForCustomerDataUpdate();
|
||||
|
||||
// Change the shipping and billing select fields again.
|
||||
await checkoutPageObject.page
|
||||
.getByRole( 'group', {
|
||||
name: 'Billing address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.selectOption( 'wide' );
|
||||
await checkoutPageObject.page
|
||||
.getByRole( 'group', {
|
||||
name: 'Shipping address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.selectOption( 'super-wide' );
|
||||
await checkoutPageObject.fillInCheckoutWithTestData(
|
||||
{},
|
||||
{
|
||||
address: {
|
||||
shipping: {
|
||||
'How wide is your road? (optional)': 'super-wide',
|
||||
},
|
||||
billing: {
|
||||
'How wide is your road? (optional)': 'wide',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
await checkoutPageObject.waitForCustomerDataUpdate();
|
||||
|
||||
await checkoutPageObject.page
|
||||
|
@ -341,22 +312,12 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
},
|
||||
order: {
|
||||
'What is your favourite colour?': 'Red',
|
||||
'How did you hear about us?':
|
||||
'Select a how did you hear about us? (optional)',
|
||||
},
|
||||
}
|
||||
);
|
||||
await checkoutPageObject.page
|
||||
.getByRole( 'group', {
|
||||
name: 'Additional order information',
|
||||
} )
|
||||
.getByLabel( 'How did you hear about us?' )
|
||||
.click();
|
||||
await checkoutPageObject.page
|
||||
.getByRole( 'group', {
|
||||
name: 'Additional order information',
|
||||
} )
|
||||
.locator( 'select' )
|
||||
.first()
|
||||
.selectOption( { index: 0 } );
|
||||
|
||||
await checkoutPageObject.waitForCustomerDataUpdate();
|
||||
|
||||
await checkoutPageObject.placeOrder();
|
||||
|
@ -422,10 +383,12 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
shipping: {
|
||||
'Government ID': ' 1. 2 3 4 5 ',
|
||||
'Confirm government ID': '1 2345',
|
||||
'How wide is your road? (optional)': 'wide',
|
||||
},
|
||||
billing: {
|
||||
'Government ID': ' 5. 4 3 2 1 ',
|
||||
'Confirm government ID': '543 21',
|
||||
'How wide is your road? (optional)': 'narrow',
|
||||
},
|
||||
},
|
||||
order: {
|
||||
|
@ -435,26 +398,6 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
}
|
||||
);
|
||||
|
||||
// Fill select fields "manually" (Not part of "fillInCheckoutWithTestData"). This is a workaround for select
|
||||
// fields until we recreate th Combobox component. This is because the aria-label includes the value so getting
|
||||
// by label alone is not reliable unless we know the value.
|
||||
await checkoutPageObject.page
|
||||
.getByRole( 'group', {
|
||||
name: 'Shipping address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.selectOption( 'wide' );
|
||||
await checkoutPageObject.page
|
||||
.getByRole( 'group', {
|
||||
name: 'Billing address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.selectOption( 'narrow' );
|
||||
|
||||
await checkoutPageObject.page.evaluate(
|
||||
'document.activeElement.blur()'
|
||||
);
|
||||
|
||||
await checkoutPageObject.page
|
||||
.getByLabel( 'Would you like a free gift with your order?' )
|
||||
.check();
|
||||
|
@ -559,7 +502,7 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
.getByRole( 'group', {
|
||||
name: 'Shipping address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.getByLabel( 'How wide is your road? (optional)' )
|
||||
).toHaveValue( 'wide' );
|
||||
await expect(
|
||||
checkoutPageObject.page
|
||||
|
@ -587,7 +530,7 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
.getByRole( 'group', {
|
||||
name: 'Billing address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.getByLabel( 'How wide is your road? (optional)' )
|
||||
).toHaveValue( 'narrow' );
|
||||
} );
|
||||
|
||||
|
@ -662,35 +605,18 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
shipping: {
|
||||
'Government ID': '12345',
|
||||
'Confirm government ID': '12345',
|
||||
'How wide is your road? (optional)': 'wide',
|
||||
},
|
||||
billing: {
|
||||
'Government ID': '54321',
|
||||
'Confirm government ID': '54321',
|
||||
'How wide is your road? (optional)': 'narrow',
|
||||
},
|
||||
},
|
||||
order: { 'How did you hear about us?': 'Other' },
|
||||
}
|
||||
);
|
||||
|
||||
// Fill select fields "manually" (Not part of "fillInCheckoutWithTestData"). This is a workaround for select
|
||||
// fields until we recreate th Combobox component. This is because the aria-label includes the value so getting
|
||||
// by label alone is not reliable unless we know the value.
|
||||
await checkoutPageObject.page
|
||||
.getByRole( 'group', {
|
||||
name: 'Shipping address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.selectOption( 'wide' );
|
||||
await checkoutPageObject.page
|
||||
.getByRole( 'group', {
|
||||
name: 'Billing address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.selectOption( 'narrow' );
|
||||
await checkoutPageObject.page.evaluate(
|
||||
'document.activeElement.blur()'
|
||||
);
|
||||
|
||||
await checkoutPageObject.placeOrder();
|
||||
|
||||
expect(
|
||||
|
@ -723,10 +649,12 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
shipping: {
|
||||
'Government ID': '12345',
|
||||
'Confirm government ID': '12345',
|
||||
'How wide is your road? (optional)': 'wide',
|
||||
},
|
||||
billing: {
|
||||
'Government ID': '54321',
|
||||
'Confirm government ID': '54321',
|
||||
'How wide is your road? (optional)': 'narrow',
|
||||
},
|
||||
},
|
||||
order: { 'How did you hear about us?': 'Other' },
|
||||
|
@ -756,26 +684,6 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
.getByLabel( 'Can a truck fit down your road?' )
|
||||
.uncheck();
|
||||
|
||||
// Fill select fields manually. (Not part of "fillInCheckoutWithTestData"). This is a workaround for select
|
||||
// fields until we recreate th Combobox component. This is because the aria-label includes the value so getting
|
||||
// by label alone is not reliable unless we know the value.
|
||||
await checkoutPageObject.page
|
||||
.getByRole( 'group', {
|
||||
name: 'Shipping address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.selectOption( 'wide' );
|
||||
await checkoutPageObject.page
|
||||
.getByRole( 'group', {
|
||||
name: 'Billing address',
|
||||
} )
|
||||
.getByLabel( 'How wide is your road?' )
|
||||
.selectOption( 'narrow' );
|
||||
|
||||
// Blur after editing the select fields since they need to be blurred to save.
|
||||
await checkoutPageObject.page.evaluate(
|
||||
'document.activeElement.blur()'
|
||||
);
|
||||
await checkoutPageObject.placeOrder();
|
||||
|
||||
expect(
|
||||
|
@ -853,7 +761,7 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
|
||||
// Check select in edit mode match the expected value.
|
||||
const roadSizeSelect = checkoutPageObject.page.getByLabel(
|
||||
'How wide is your road?'
|
||||
'How wide is your road? (optional)'
|
||||
);
|
||||
await expect( roadSizeSelect ).toHaveValue( 'narrow' );
|
||||
|
||||
|
@ -916,7 +824,7 @@ test.describe( 'Shopper → Additional Checkout Fields', () => {
|
|||
|
||||
// Check select in edit mode match the expected value.
|
||||
const shippingRoadSizeSelect = checkoutPageObject.page.getByLabel(
|
||||
'How wide is your road?'
|
||||
'How wide is your road? (optional)'
|
||||
);
|
||||
await expect( shippingRoadSizeSelect ).toHaveValue( 'wide' );
|
||||
|
||||
|
|
|
@ -386,17 +386,8 @@ class CheckoutFields {
|
|||
|
||||
$field_data['options'] = $cleaned_options;
|
||||
|
||||
// If the field is not required, inject an empty option at the start.
|
||||
if ( isset( $field_data['required'] ) && false === $field_data['required'] && ! in_array( '', $added_values, true ) ) {
|
||||
$field_data['options'] = array_merge(
|
||||
[
|
||||
[
|
||||
'value' => '',
|
||||
'label' => '',
|
||||
],
|
||||
],
|
||||
$field_data['options']
|
||||
);
|
||||
if ( isset( $field_data['placeholder'] ) ) {
|
||||
$field_data['placeholder'] = sanitize_text_field( $field_data['placeholder'] );
|
||||
}
|
||||
|
||||
return $field_data;
|
||||
|
|
|
@ -319,6 +319,9 @@ class CheckoutSchema extends AbstractSchema {
|
|||
},
|
||||
$field['options']
|
||||
);
|
||||
if ( true !== $field['required'] ) {
|
||||
$field_schema['enum'][] = '';
|
||||
}
|
||||
}
|
||||
|
||||
if ( 'checkbox' === $field['type'] ) {
|
||||
|
|
|
@ -1021,45 +1021,6 @@ class AdditionalFields extends MockeryTestCase {
|
|||
$this->assertFalse( $this->controller->is_field( $id ), \sprintf( '%s is still registered', $id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a select has an extra empty option if it's optional.
|
||||
*/
|
||||
public function test_optional_select_has_empty_value() {
|
||||
$id = 'plugin-namespace/optional-select';
|
||||
\woocommerce_register_additional_checkout_field(
|
||||
array(
|
||||
'id' => $id,
|
||||
'label' => 'Optional Select',
|
||||
'location' => 'order',
|
||||
'type' => 'select',
|
||||
'options' => array(
|
||||
array(
|
||||
'label' => 'Option 1',
|
||||
'value' => 'option-1',
|
||||
),
|
||||
array(
|
||||
'label' => 'Option 2',
|
||||
'value' => 'option-2',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
$request = new \WP_REST_Request( 'OPTIONS', '/wc/store/v1/checkout' );
|
||||
$response = rest_get_server()->dispatch( $request );
|
||||
|
||||
$data = $response->get_data();
|
||||
$this->assertEquals(
|
||||
array( '', 'option-1', 'option-2' ),
|
||||
$data['schema']['properties']['additional_fields']['properties'][ $id ]['enum'],
|
||||
print_r( $data['schema']['properties']['additional_fields']['properties'][ $id ], true )
|
||||
);
|
||||
|
||||
\__internal_woocommerce_blocks_deregister_checkout_field( $id );
|
||||
|
||||
// Ensures the field isn't registered.
|
||||
$this->assertFalse( $this->controller->is_field( $id ), \sprintf( '%s is still registered', $id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure an error is triggered when a checkbox is registered as required.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue