From 05cf21536df6e74a322ce974c66ee5a7cbc1e83c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 18:43:06 +0200 Subject: [PATCH] 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 Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> * Prep for cherry pick 49929 --------- Co-authored-by: Seghir Nadir Co-authored-by: github-actions Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> Co-authored-by: WooCommerce Bot --- .../components/cart-checkout/form/form.tsx | 14 +- .../components/cart-checkout/form/utils.ts | 13 ++ .../country-input/country-input.tsx | 65 ++----- .../js/base/components/select/index.tsx | 174 ++++++++++++++---- .../components/state-input/state-input.tsx | 77 ++------ .../js/settings/shared/default-fields.ts | 2 + .../additional-checkout-fields.md | 18 +- .../components/checkbox-control/index.tsx | 2 + ...l-fields.guest-shopper.block_theme.spec.ts | 28 +-- ...itional-fields.shopper.block_theme.spec.ts | 158 ++++------------ .../Blocks/Domain/Services/CheckoutFields.php | 13 +- .../StoreApi/Schemas/V1/CheckoutSchema.php | 3 + .../StoreApi/Routes/AdditionalFields.php | 39 ---- 13 files changed, 248 insertions(+), 358 deletions(-) diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/form.tsx b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/form.tsx index 810724b5dfe..22718c1c2f9 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/form.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/form.tsx @@ -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 + } /> ); } diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/utils.ts b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/utils.ts index dd2a7227fad..4eaca30a38a 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/utils.ts +++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/form/utils.ts @@ -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' ], diff --git a/plugins/woocommerce-blocks/assets/js/base/components/country-input/country-input.tsx b/plugins/woocommerce-blocks/assets/js/base/components/country-input/country-input.tsx index 680d2bca43b..b61a5157835 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/country-input/country-input.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/country-input/country-input.tsx @@ -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,58 +21,27 @@ 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( - ( [ countryCode, countryName ] ) => ( { - value: countryCode, - label: decodeEntities( countryName ), - } ) - ) + 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 ( -
- ); }; diff --git a/plugins/woocommerce-blocks/assets/js/base/components/select/index.tsx b/plugins/woocommerce-blocks/assets/js/base/components/select/index.tsx index d05a83261cb..561ed12592b 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/select/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/select/index.tsx @@ -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,48 +53,124 @@ 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 ( -
-
- - - +
+
+
+ + + +
+
); }; diff --git a/plugins/woocommerce-blocks/assets/js/base/components/state-input/state-input.tsx b/plugins/woocommerce-blocks/assets/js/base/components/state-input/state-input.tsx index c135ba856d0..9099ebe6e1b 100644 --- a/plugins/woocommerce-blocks/assets/js/base/components/state-input/state-input.tsx +++ b/plugins/woocommerce-blocks/assets/js/base/components/state-input/state-input.tsx @@ -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 ) => ( { - value: key, - label: decodeEntities( countryStates[ 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 ( -
-