Add Checkout form validation (https://github.com/woocommerce/woocommerce-blocks/pull/1993)
* Add Checkout form validation * Add back validation when filling the address without having set a country * Split TextInput and Select so they can be used with or without validation * Cleanup * Only display the missing country error if city, state or postcode are entered * Fix CSS specificity conflict * Remove unnecessary semicolon * Rename areThereValidationErrors to hasValidationErrors
This commit is contained in:
parent
d07b7c6b52
commit
2593fcf7da
|
@ -2,7 +2,7 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
import TextInput from '@woocommerce/base-components/text-input';
|
||||
import { ValidatedTextInput } from '@woocommerce/base-components/text-input';
|
||||
import {
|
||||
BillingCountryInput,
|
||||
ShippingCountryInput,
|
||||
|
@ -21,7 +21,9 @@ import { __ } from '@wordpress/i18n';
|
|||
import defaultAddressFields from './default-address-fields';
|
||||
import countryAddressFields from './country-address-fields';
|
||||
|
||||
const validateCountry = (
|
||||
// If it's the shipping address form and the user starts entering address
|
||||
// values without having set the country first, show an error.
|
||||
const validateShippingCountry = (
|
||||
values,
|
||||
setValidationErrors,
|
||||
clearValidationError,
|
||||
|
@ -30,17 +32,20 @@ const validateCountry = (
|
|||
if (
|
||||
! hasValidationError &&
|
||||
! values.country &&
|
||||
Object.values( values ).some( ( value ) => value !== '' )
|
||||
( values.city || values.state || values.postcode )
|
||||
) {
|
||||
setValidationErrors( {
|
||||
country: __(
|
||||
'Please select a country to calculate rates.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
'shipping-missing-country': {
|
||||
message: __(
|
||||
'Please select a country to calculate rates.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
hidden: false,
|
||||
},
|
||||
} );
|
||||
}
|
||||
if ( hasValidationError && values.country ) {
|
||||
clearValidationError( 'country' );
|
||||
clearValidationError( 'shipping-missing-country' );
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -63,26 +68,31 @@ const AddressForm = ( {
|
|||
const addressFields = fields.map( ( field ) => ( {
|
||||
key: field,
|
||||
...defaultAddressFields[ field ],
|
||||
...fieldConfig[ field ],
|
||||
...countryLocale[ field ],
|
||||
...fieldConfig[ field ],
|
||||
} ) );
|
||||
const sortedAddressFields = addressFields.sort(
|
||||
( a, b ) => a.index - b.index
|
||||
);
|
||||
const countryValidationError = getValidationError( 'country' );
|
||||
const countryValidationError =
|
||||
getValidationError( 'shipping-missing-country' ) || {};
|
||||
useEffect( () => {
|
||||
validateCountry(
|
||||
values,
|
||||
setValidationErrors,
|
||||
clearValidationError,
|
||||
!! countryValidationError
|
||||
);
|
||||
if ( type === 'shipping' ) {
|
||||
validateShippingCountry(
|
||||
values,
|
||||
setValidationErrors,
|
||||
clearValidationError,
|
||||
countryValidationError.message &&
|
||||
! countryValidationError.hidden
|
||||
);
|
||||
}
|
||||
}, [
|
||||
values,
|
||||
countryValidationError,
|
||||
setValidationErrors,
|
||||
clearValidationError,
|
||||
] );
|
||||
|
||||
return (
|
||||
<div className="wc-block-address-form">
|
||||
{ sortedAddressFields.map( ( field ) => {
|
||||
|
@ -114,6 +124,12 @@ const AddressForm = ( {
|
|||
postcode: '',
|
||||
} )
|
||||
}
|
||||
errorId={
|
||||
type === 'shipping'
|
||||
? 'shipping-missing-country'
|
||||
: null
|
||||
}
|
||||
errorMessage={ field.errorMessage }
|
||||
required={ field.required }
|
||||
/>
|
||||
);
|
||||
|
@ -141,13 +157,14 @@ const AddressForm = ( {
|
|||
state: newValue,
|
||||
} )
|
||||
}
|
||||
errorMessage={ field.errorMessage }
|
||||
required={ field.required }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
<ValidatedTextInput
|
||||
key={ field.key }
|
||||
className={ `wc-block-address-form__${ field.key }` }
|
||||
label={
|
||||
|
@ -161,6 +178,7 @@ const AddressForm = ( {
|
|||
[ field.key ]: newValue,
|
||||
} )
|
||||
}
|
||||
errorMessage={ field.errorMessage }
|
||||
required={ field.required }
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -2,23 +2,33 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { useCheckoutContext } from '@woocommerce/base-context';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Button from '../button';
|
||||
|
||||
const PlaceOrderButton = () => {
|
||||
const PlaceOrderButton = ( { validateSubmit } ) => {
|
||||
const { submitLabel, onSubmit } = useCheckoutContext();
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="wc-block-components-checkout-place-order-button"
|
||||
onClick={ onSubmit }
|
||||
onClick={ () => {
|
||||
const isValid = validateSubmit();
|
||||
if ( isValid ) {
|
||||
onSubmit();
|
||||
}
|
||||
} }
|
||||
>
|
||||
{ submitLabel }
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
PlaceOrderButton.propTypes = {
|
||||
validateSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default PlaceOrderButton;
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import { useValidationContext } from '@woocommerce/base-context';
|
||||
import classnames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Select from '../select';
|
||||
import { ValidationInputError } from '../validation';
|
||||
import { ValidatedSelect } from '../select';
|
||||
|
||||
const CountryInput = ( {
|
||||
className,
|
||||
|
@ -20,23 +19,27 @@ const CountryInput = ( {
|
|||
value = '',
|
||||
autoComplete = 'off',
|
||||
required = false,
|
||||
errorId,
|
||||
errorMessage = __(
|
||||
'Please select a country.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
} ) => {
|
||||
const options = Object.keys( countries ).map( ( key ) => ( {
|
||||
key,
|
||||
name: decodeEntities( countries[ key ] ),
|
||||
} ) );
|
||||
const { getValidationError } = useValidationContext();
|
||||
const errorMessage = getValidationError( 'country' );
|
||||
|
||||
return (
|
||||
<div className={ classnames( className, 'wc-block-country-input' ) }>
|
||||
<Select
|
||||
<ValidatedSelect
|
||||
label={ label }
|
||||
onChange={ onChange }
|
||||
options={ options }
|
||||
value={ options.find( ( option ) => option.key === value ) }
|
||||
errorId={ errorId }
|
||||
errorMessage={ errorMessage }
|
||||
required={ required }
|
||||
hasError={ !! errorMessage }
|
||||
/>
|
||||
{ autoComplete !== 'off' && (
|
||||
<input
|
||||
|
@ -61,7 +64,6 @@ const CountryInput = ( {
|
|||
tabIndex={ -1 }
|
||||
/>
|
||||
) }
|
||||
<ValidationInputError errorMessage={ errorMessage } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -73,6 +75,8 @@ CountryInput.propTypes = {
|
|||
label: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
autoComplete: PropTypes.string,
|
||||
errorId: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CountryInput;
|
||||
|
|
|
@ -10,20 +10,32 @@ import { CustomSelectControl } from 'wordpress-components';
|
|||
*/
|
||||
import './style.scss';
|
||||
|
||||
const Select = ( { className, label, onChange, options, value, hasError } ) => {
|
||||
const Select = ( {
|
||||
className,
|
||||
feedback,
|
||||
id,
|
||||
label,
|
||||
onChange,
|
||||
options,
|
||||
value,
|
||||
} ) => {
|
||||
return (
|
||||
<CustomSelectControl
|
||||
<div
|
||||
id={ id }
|
||||
className={ classnames( 'wc-block-select', className, {
|
||||
'is-active': value,
|
||||
'has-error': hasError,
|
||||
} ) }
|
||||
label={ label }
|
||||
onChange={ ( { selectedItem } ) => {
|
||||
onChange( selectedItem.key );
|
||||
} }
|
||||
options={ options }
|
||||
value={ value }
|
||||
/>
|
||||
>
|
||||
<CustomSelectControl
|
||||
label={ label }
|
||||
onChange={ ( { selectedItem } ) => {
|
||||
onChange( selectedItem.key );
|
||||
} }
|
||||
options={ options }
|
||||
value={ value }
|
||||
/>
|
||||
{ feedback }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -36,6 +48,8 @@ Select.propTypes = {
|
|||
} ).isRequired
|
||||
).isRequired,
|
||||
className: PropTypes.string,
|
||||
feedback: PropTypes.node,
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.shape( {
|
||||
key: PropTypes.string.isRequired,
|
||||
|
@ -44,3 +58,4 @@ Select.propTypes = {
|
|||
};
|
||||
|
||||
export default Select;
|
||||
export { default as ValidatedSelect } from './validated';
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useEffect } from 'react';
|
||||
import { useValidationContext } from '@woocommerce/base-context';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { withInstanceId } from 'wordpress-compose';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ValidationInputError } from '../validation';
|
||||
import Select from './index';
|
||||
import './style.scss';
|
||||
|
||||
const ValidatedSelect = ( {
|
||||
className,
|
||||
id,
|
||||
value,
|
||||
instanceId,
|
||||
required,
|
||||
errorId,
|
||||
errorMessage = __(
|
||||
'Please select a value.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
...rest
|
||||
} ) => {
|
||||
const selectId = id || 'select-' + instanceId;
|
||||
errorId = errorId || selectId;
|
||||
const {
|
||||
getValidationError,
|
||||
setValidationErrors,
|
||||
clearValidationError,
|
||||
} = useValidationContext();
|
||||
const validateSelect = () => {
|
||||
if ( ! required || value ) {
|
||||
clearValidationError( errorId );
|
||||
} else {
|
||||
setValidationErrors( {
|
||||
[ errorId ]: {
|
||||
message: errorMessage,
|
||||
hidden: true,
|
||||
},
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
useEffect( () => {
|
||||
validateSelect();
|
||||
}, [ value ] );
|
||||
|
||||
const error = getValidationError( errorId ) || {};
|
||||
|
||||
return (
|
||||
<Select
|
||||
id={ selectId }
|
||||
className={ classnames( className, {
|
||||
'has-error': error.message && ! error.hidden,
|
||||
} ) }
|
||||
feedback={ <ValidationInputError propertyName={ errorId } /> }
|
||||
value={ value }
|
||||
{ ...rest }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ValidatedSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
errorId: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
value: PropTypes.shape( {
|
||||
key: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
} ),
|
||||
};
|
||||
|
||||
export default withInstanceId( ValidatedSelect );
|
|
@ -20,24 +20,56 @@ const ShippingCalculatorAddress = ( {
|
|||
addressFields,
|
||||
} ) => {
|
||||
const [ address, setAddress ] = useState( initialAddress );
|
||||
const { getValidationError } = useValidationContext();
|
||||
const {
|
||||
hasValidationErrors,
|
||||
showAllValidationErrors,
|
||||
} = useValidationContext();
|
||||
|
||||
const validateSubmit = () => {
|
||||
showAllValidationErrors();
|
||||
if ( hasValidationErrors() ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Make all fields optional except 'country'.
|
||||
const fieldConfig = {};
|
||||
addressFields.forEach( ( field ) => {
|
||||
if ( field === 'country' ) {
|
||||
fieldConfig[ field ] = {
|
||||
...fieldConfig[ field ],
|
||||
errorMessage: __(
|
||||
'Please select a country to calculate rates.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
required: true,
|
||||
};
|
||||
} else {
|
||||
fieldConfig[ field ] = {
|
||||
...fieldConfig[ field ],
|
||||
required: false,
|
||||
};
|
||||
}
|
||||
} );
|
||||
|
||||
return (
|
||||
<form className="wc-block-shipping-calculator-address">
|
||||
<AddressForm
|
||||
fields={ addressFields }
|
||||
fieldConfig={ fieldConfig }
|
||||
onChange={ setAddress }
|
||||
values={ address }
|
||||
/>
|
||||
<Button
|
||||
className="wc-block-shipping-calculator-address__button"
|
||||
disabled={
|
||||
isShallowEqual( address, initialAddress ) ||
|
||||
getValidationError( 'country' )
|
||||
}
|
||||
disabled={ isShallowEqual( address, initialAddress ) }
|
||||
onClick={ ( e ) => {
|
||||
e.preventDefault();
|
||||
return onUpdate( address );
|
||||
const isAddressValid = validateSubmit();
|
||||
if ( isAddressValid ) {
|
||||
return onUpdate( address );
|
||||
}
|
||||
} }
|
||||
type="submit"
|
||||
>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import { useCallback, useEffect } from '@wordpress/element';
|
||||
|
@ -8,8 +9,8 @@ import { useCallback, useEffect } from '@wordpress/element';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import TextInput from '../text-input';
|
||||
import Select from '../select';
|
||||
import { ValidatedTextInput } from '../text-input';
|
||||
import { ValidatedSelect } from '../select';
|
||||
|
||||
const StateInput = ( {
|
||||
className,
|
||||
|
@ -61,12 +62,16 @@ const StateInput = ( {
|
|||
if ( options.length > 0 ) {
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
<ValidatedSelect
|
||||
className={ className }
|
||||
label={ label }
|
||||
onChange={ onChangeState }
|
||||
options={ options }
|
||||
value={ options.find( ( option ) => option.key === value ) }
|
||||
errorMessage={ __(
|
||||
'Please select a state.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
required={ required }
|
||||
/>
|
||||
{ autoComplete !== 'off' && (
|
||||
|
@ -92,7 +97,7 @@ const StateInput = ( {
|
|||
);
|
||||
}
|
||||
return (
|
||||
<TextInput
|
||||
<ValidatedTextInput
|
||||
className={ className }
|
||||
label={ label }
|
||||
onChange={ onChangeState }
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { forwardRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { withInstanceId } from 'wordpress-compose';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -12,73 +12,84 @@ import { withInstanceId } from 'wordpress-compose';
|
|||
import Label from '../label';
|
||||
import './style.scss';
|
||||
|
||||
const TextInput = ( {
|
||||
className,
|
||||
instanceId,
|
||||
id,
|
||||
type = 'text',
|
||||
ariaLabel,
|
||||
ariaDescribedBy,
|
||||
label,
|
||||
screenReaderLabel,
|
||||
disabled,
|
||||
help,
|
||||
autoComplete = 'off',
|
||||
value = '',
|
||||
onChange,
|
||||
required = false,
|
||||
} ) => {
|
||||
const [ isActive, setIsActive ] = useState( false );
|
||||
const onChangeValue = ( event ) => onChange( event.target.value );
|
||||
const textInputId = id || 'textinput-' + instanceId;
|
||||
const TextInput = forwardRef(
|
||||
(
|
||||
{
|
||||
className,
|
||||
id,
|
||||
type = 'text',
|
||||
ariaLabel,
|
||||
ariaDescribedBy,
|
||||
label,
|
||||
screenReaderLabel,
|
||||
disabled,
|
||||
help,
|
||||
autoComplete = 'off',
|
||||
value = '',
|
||||
onChange,
|
||||
required = false,
|
||||
onBlur = () => {},
|
||||
feedback,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [ isActive, setIsActive ] = useState( false );
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ classnames( 'wc-block-text-input', className, {
|
||||
'is-active': isActive || value,
|
||||
} ) }
|
||||
>
|
||||
<input
|
||||
type={ type }
|
||||
id={ textInputId }
|
||||
value={ value }
|
||||
autoComplete={ autoComplete }
|
||||
onChange={ onChangeValue }
|
||||
onFocus={ () => setIsActive( true ) }
|
||||
onBlur={ () => setIsActive( false ) }
|
||||
aria-label={ ariaLabel || label }
|
||||
disabled={ disabled }
|
||||
aria-describedby={
|
||||
!! help && ! ariaDescribedBy
|
||||
? textInputId + '__help'
|
||||
: ariaDescribedBy
|
||||
}
|
||||
required={ required }
|
||||
/>
|
||||
<Label
|
||||
label={ label }
|
||||
screenReaderLabel={ screenReaderLabel || label }
|
||||
wrapperElement="label"
|
||||
wrapperProps={ {
|
||||
htmlFor: textInputId,
|
||||
} }
|
||||
htmlFor={ textInputId }
|
||||
/>
|
||||
{ !! help && (
|
||||
<p
|
||||
id={ textInputId + '__help' }
|
||||
className="wc-block-text-input__help"
|
||||
>
|
||||
{ help }
|
||||
</p>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={ classnames( 'wc-block-text-input', className, {
|
||||
'is-active': isActive || value,
|
||||
} ) }
|
||||
>
|
||||
<input
|
||||
type={ type }
|
||||
id={ id }
|
||||
value={ value }
|
||||
ref={ ref }
|
||||
autoComplete={ autoComplete }
|
||||
onChange={ ( event ) => {
|
||||
onChange( event.target.value );
|
||||
} }
|
||||
onFocus={ () => setIsActive( true ) }
|
||||
onBlur={ () => {
|
||||
onBlur();
|
||||
setIsActive( false );
|
||||
} }
|
||||
aria-label={ ariaLabel || label }
|
||||
disabled={ disabled }
|
||||
aria-describedby={
|
||||
!! help && ! ariaDescribedBy
|
||||
? id + '__help'
|
||||
: ariaDescribedBy
|
||||
}
|
||||
required={ required }
|
||||
/>
|
||||
<Label
|
||||
label={ label }
|
||||
screenReaderLabel={ screenReaderLabel || label }
|
||||
wrapperElement="label"
|
||||
wrapperProps={ {
|
||||
htmlFor: id,
|
||||
} }
|
||||
htmlFor={ id }
|
||||
/>
|
||||
{ !! help && (
|
||||
<p
|
||||
id={ id + '__help' }
|
||||
className="wc-block-text-input__help"
|
||||
>
|
||||
{ help }
|
||||
</p>
|
||||
) }
|
||||
{ feedback }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TextInput.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
id: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
ariaLabel: PropTypes.string,
|
||||
ariaDescribedBy: PropTypes.string,
|
||||
|
@ -90,4 +101,5 @@ TextInput.propTypes = {
|
|||
required: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default withInstanceId( TextInput );
|
||||
export default TextInput;
|
||||
export { default as ValidatedTextInput } from './validated';
|
||||
|
|
|
@ -3,13 +3,64 @@
|
|||
margin-bottom: $gap-large;
|
||||
white-space: nowrap;
|
||||
|
||||
label {
|
||||
position: absolute;
|
||||
transform: translateY(#{$gap-small});
|
||||
left: 0;
|
||||
transform-origin: top left;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
color: $gray-50;
|
||||
transition: transform 200ms ease;
|
||||
margin: 0 $gap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: calc(100% - #{ 2 * $gap });
|
||||
|
||||
@media screen and (prefers-reduced-motion: reduce) {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
input:-webkit-autofill + label {
|
||||
transform: translateY(#{$gap-smallest}) scale(0.75);
|
||||
}
|
||||
|
||||
&.is-active label {
|
||||
transform: translateY(#{$gap-smallest}) scale(0.75);
|
||||
}
|
||||
|
||||
input[type="tel"],
|
||||
input[type="url"],
|
||||
input[type="text"],
|
||||
input[type="email"] {
|
||||
background-color: #fff;
|
||||
padding: $gap-small $gap;
|
||||
border-radius: 4px;
|
||||
border: 1px solid $input-border-gray;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
font-family: inherit;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
height: 48px;
|
||||
color: $input-text-active;
|
||||
|
||||
&:focus {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active input[type="tel"],
|
||||
&.is-active input[type="url"],
|
||||
&.is-active input[type="text"],
|
||||
&.is-active input[type="email"] {
|
||||
padding: $gap-large $gap $gap-smallest;
|
||||
}
|
||||
|
||||
&.has-error input {
|
||||
/**
|
||||
* This is needed because of the input[type="*"] rules later in this
|
||||
* file (and in themes like storefront) have higher specificity for
|
||||
* border
|
||||
*/
|
||||
border-color: $error-red !important;
|
||||
border-color: $error-red;
|
||||
&:focus {
|
||||
outline-color: $error-red;
|
||||
}
|
||||
|
@ -19,55 +70,3 @@
|
|||
color: $error-red;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-text-input label {
|
||||
position: absolute;
|
||||
transform: translateY(#{$gap-small});
|
||||
left: 0;
|
||||
transform-origin: top left;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
color: $gray-50;
|
||||
transition: all 200ms ease;
|
||||
margin: 0 $gap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: calc(100% - #{ 2 * $gap });
|
||||
|
||||
@media screen and (prefers-reduced-motion: reduce) {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
.wc-block-text-input input:-webkit-autofill + label {
|
||||
transform: translateY(#{$gap-smallest}) scale(0.75);
|
||||
}
|
||||
.wc-block-text-input.is-active label {
|
||||
transform: translateY(#{$gap-smallest}) scale(0.75);
|
||||
}
|
||||
.wc-block-text-input input[type="tel"],
|
||||
.wc-block-text-input input[type="url"],
|
||||
.wc-block-text-input input[type="text"],
|
||||
.wc-block-text-input input[type="email"] {
|
||||
background-color: #fff;
|
||||
padding: $gap-small $gap;
|
||||
border-radius: 4px;
|
||||
border: 1px solid $input-border-gray;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
font-family: inherit;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
height: 48px;
|
||||
color: $input-text-active;
|
||||
|
||||
&:focus {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
.wc-block-text-input.is-active input[type="tel"],
|
||||
.wc-block-text-input.is-active input[type="url"],
|
||||
.wc-block-text-input.is-active input[type="text"],
|
||||
.wc-block-text-input.is-active input[type="email"] {
|
||||
padding: $gap-large $gap $gap-smallest;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { useValidationContext } from '@woocommerce/base-context';
|
||||
import { ValidationInputError } from '@woocommerce/base-components/validation';
|
||||
import { withInstanceId } from 'wordpress-compose';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import TextInput from './index';
|
||||
import './style.scss';
|
||||
|
||||
const ValidatedTextInput = ( {
|
||||
className,
|
||||
instanceId,
|
||||
id,
|
||||
ariaDescribedBy,
|
||||
errorId,
|
||||
validateOnMount = true,
|
||||
onChange,
|
||||
showError = true,
|
||||
...rest
|
||||
} ) => {
|
||||
const inputRef = useRef();
|
||||
const {
|
||||
getValidationError,
|
||||
hideValidationError,
|
||||
setValidationErrors,
|
||||
clearValidationError,
|
||||
getValidationErrorId,
|
||||
} = useValidationContext();
|
||||
|
||||
const textInputId = id || 'textinput-' + instanceId;
|
||||
errorId = errorId || textInputId;
|
||||
const validateInput = ( errorsHidden = true ) => {
|
||||
if ( inputRef.current.checkValidity() ) {
|
||||
clearValidationError( errorId );
|
||||
} else {
|
||||
setValidationErrors( {
|
||||
[ errorId ]: {
|
||||
message:
|
||||
inputRef.current.validationMessage ||
|
||||
__( 'Invalid value.', 'woo-gutenberg-products-block' ),
|
||||
hidden: errorsHidden,
|
||||
},
|
||||
} );
|
||||
}
|
||||
};
|
||||
|
||||
useEffect( () => {
|
||||
if ( validateOnMount ) {
|
||||
validateInput();
|
||||
}
|
||||
}, [ validateOnMount ] );
|
||||
|
||||
const errorMessage = getValidationError( errorId ) || {};
|
||||
const hasError = errorMessage.message && ! errorMessage.hidden;
|
||||
const describedBy =
|
||||
showError && hasError
|
||||
? getValidationErrorId( textInputId )
|
||||
: ariaDescribedBy;
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
className={ classnames( className, {
|
||||
'has-error': hasError,
|
||||
} ) }
|
||||
id={ textInputId }
|
||||
onBlur={ () => {
|
||||
validateInput( false );
|
||||
} }
|
||||
feedback={
|
||||
showError && <ValidationInputError propertyName={ errorId } />
|
||||
}
|
||||
ref={ inputRef }
|
||||
onChange={ ( val ) => {
|
||||
hideValidationError( errorId );
|
||||
onChange( val );
|
||||
} }
|
||||
ariaDescribedBy={ describedBy }
|
||||
{ ...rest }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ValidatedTextInput.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
id: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
ariaDescribedBy: PropTypes.string,
|
||||
errorId: PropTypes.string,
|
||||
validateOnMount: PropTypes.bool,
|
||||
showError: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default withInstanceId( ValidatedTextInput );
|
|
@ -5,12 +5,11 @@ import { __ } from '@wordpress/i18n';
|
|||
import { useState, useEffect, useRef } from '@wordpress/element';
|
||||
import { PanelBody, PanelRow } from 'wordpress-components';
|
||||
import { Button } from '@woocommerce/base-components/cart-checkout';
|
||||
import TextInput from '@woocommerce/base-components/text-input';
|
||||
import { ValidatedTextInput } from '@woocommerce/base-components/text-input';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
import { ValidationInputError } from '@woocommerce/base-components/validation';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withInstanceId } from 'wordpress-compose';
|
||||
import classnames from 'classnames';
|
||||
import { useValidationContext } from '@woocommerce/base-context';
|
||||
|
||||
/**
|
||||
|
@ -27,21 +26,18 @@ const TotalsCouponCodeInput = ( {
|
|||
} ) => {
|
||||
const [ couponValue, setCouponValue ] = useState( '' );
|
||||
const currentIsLoading = useRef( false );
|
||||
const {
|
||||
getValidationError,
|
||||
clearValidationError,
|
||||
getValidationErrorId,
|
||||
} = useValidationContext();
|
||||
const validationMessage = getValidationError( 'coupon' );
|
||||
const { getValidationError, getValidationErrorId } = useValidationContext();
|
||||
|
||||
const validationError = getValidationError( 'coupon' );
|
||||
|
||||
useEffect( () => {
|
||||
if ( currentIsLoading.current !== isLoading ) {
|
||||
if ( ! isLoading && couponValue && ! validationMessage ) {
|
||||
if ( ! isLoading && couponValue && ! validationError ) {
|
||||
setCouponValue( '' );
|
||||
}
|
||||
currentIsLoading.current = isLoading;
|
||||
}
|
||||
}, [ isLoading, couponValue, validationMessage ] );
|
||||
}, [ isLoading, couponValue, validationError ] );
|
||||
|
||||
const textInputId = `wc-block-coupon-code__input-${ instanceId }`;
|
||||
|
||||
|
@ -73,14 +69,10 @@ const TotalsCouponCodeInput = ( {
|
|||
>
|
||||
<PanelRow className="wc-block-coupon-code__row">
|
||||
<form className="wc-block-coupon-code__form">
|
||||
<TextInput
|
||||
<ValidatedTextInput
|
||||
id={ textInputId }
|
||||
className={ classnames(
|
||||
'wc-block-coupon-code__input',
|
||||
{
|
||||
'has-error': !! validationMessage,
|
||||
}
|
||||
) }
|
||||
errorId="coupon"
|
||||
className="wc-block-coupon-code__input"
|
||||
label={ __(
|
||||
'Enter code',
|
||||
'woo-gutenberg-products-block'
|
||||
|
@ -91,8 +83,9 @@ const TotalsCouponCodeInput = ( {
|
|||
) }
|
||||
onChange={ ( newCouponValue ) => {
|
||||
setCouponValue( newCouponValue );
|
||||
clearValidationError( 'coupon' );
|
||||
} }
|
||||
validateOnMount={ false }
|
||||
showError={ false }
|
||||
/>
|
||||
<Button
|
||||
className="wc-block-coupon-code__button"
|
||||
|
@ -106,11 +99,11 @@ const TotalsCouponCodeInput = ( {
|
|||
{ __( 'Apply', 'woo-gutenberg-products-block' ) }
|
||||
</Button>
|
||||
</form>
|
||||
<ValidationInputError
|
||||
propertyName="coupon"
|
||||
elementId={ textInputId }
|
||||
/>
|
||||
</PanelRow>
|
||||
<ValidationInputError
|
||||
errorMessage={ validationMessage }
|
||||
elementId={ textInputId }
|
||||
/>
|
||||
</LoadingMask>
|
||||
</PanelBody>
|
||||
);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
margin-bottom: 0;
|
||||
|
||||
.wc-block-coupon-code__input {
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
@ -16,3 +17,13 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-coupon-code__row {
|
||||
flex-direction: column;
|
||||
|
||||
.wc-block-form-input-validation-error {
|
||||
margin-top: $gap-smaller;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
.wc-block-form-input-validation-error {
|
||||
color: $error-red;
|
||||
@include font-size(12);
|
||||
margin-top: -$gap-large;
|
||||
padding-top: $gap-smallest;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
position: absolute;
|
||||
top: calc(100% - 1px);
|
||||
white-space: normal;
|
||||
|
||||
> p {
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
line-height: 12px; // This allows two lines in a 24px bottom margin.
|
||||
margin: 0;
|
||||
min-height: 24px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,10 +15,15 @@ export const ValidationInputError = ( {
|
|||
elementId = '',
|
||||
} ) => {
|
||||
const { getValidationError, getValidationErrorId } = useValidationContext();
|
||||
if ( ! errorMessage && ! propertyName ) {
|
||||
return null;
|
||||
if ( ! errorMessage ) {
|
||||
const error = getValidationError( propertyName ) || {};
|
||||
if ( error.message && ! error.hidden ) {
|
||||
errorMessage = error.message;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
errorMessage = errorMessage || getValidationError( propertyName );
|
||||
|
||||
return (
|
||||
<div className="wc-block-form-input-validation-error" role="alert">
|
||||
<p id={ getValidationErrorId( elementId ) }>{ errorMessage }</p>
|
||||
|
|
|
@ -38,11 +38,9 @@ export const ValidationContextProvider = ( { children } ) => {
|
|||
*
|
||||
* @param {string} property The property the error message is for.
|
||||
*
|
||||
* @return {string} Either the error message for the given property or an
|
||||
* empty string.
|
||||
* @return {Object} The error object for the given property.
|
||||
*/
|
||||
const getValidationError = ( property ) =>
|
||||
validationErrors[ property ] || '';
|
||||
const getValidationError = ( property ) => validationErrors[ property ];
|
||||
|
||||
/**
|
||||
* Clears any validation error that exists in state for the given property
|
||||
|
@ -73,13 +71,82 @@ export const ValidationContextProvider = ( { children } ) => {
|
|||
// all values must be a string.
|
||||
newErrors = pickBy(
|
||||
newErrors,
|
||||
( message ) => typeof message === 'string'
|
||||
( { message } ) => typeof message === 'string'
|
||||
);
|
||||
if ( Object.values( newErrors ).length > 0 ) {
|
||||
updateValidationErrors( { ...validationErrors, ...newErrors } );
|
||||
updateValidationErrors( ( prevErrors ) => ( {
|
||||
...prevErrors,
|
||||
...newErrors,
|
||||
} ) );
|
||||
}
|
||||
};
|
||||
|
||||
const updateValidationError = ( property, newError ) => {
|
||||
updateValidationErrors( ( prevErrors ) => {
|
||||
if ( ! prevErrors.hasOwnProperty( property ) ) {
|
||||
return prevErrors;
|
||||
}
|
||||
return {
|
||||
...prevErrors,
|
||||
[ property ]: {
|
||||
...prevErrors[ property ],
|
||||
...newError,
|
||||
},
|
||||
};
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a property name and if an associated error exists, it sets its
|
||||
* `hidden` value to true.
|
||||
*
|
||||
* @param {string} property The name of the property to set the `hidden`
|
||||
* value to true.
|
||||
*/
|
||||
const hideValidationError = ( property ) => {
|
||||
updateValidationError( property, {
|
||||
hidden: true,
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a property name and if an associated error exists, it sets its
|
||||
* `hidden` value to false.
|
||||
*
|
||||
* @param {string} property The name of the property to set the `hidden`
|
||||
* value to false.
|
||||
*/
|
||||
const showValidationError = ( property ) => {
|
||||
updateValidationError( property, {
|
||||
hidden: false,
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the `hidden` value of all errors to `false`.
|
||||
*/
|
||||
const showAllValidationErrors = () => {
|
||||
updateValidationErrors( ( prevErrors ) => {
|
||||
const newErrors = {};
|
||||
Object.keys( prevErrors ).forEach( ( property ) => {
|
||||
newErrors[ property ] = {
|
||||
...prevErrors[ property ],
|
||||
hidden: false,
|
||||
};
|
||||
} );
|
||||
return newErrors;
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Allows checking if the current context has at least one validation error.
|
||||
*
|
||||
* @return {boolean} Whether there is at least one error.
|
||||
*/
|
||||
const hasValidationErrors = () => {
|
||||
return Object.keys( validationErrors ).length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides an id for the validation error that can be used to fill out
|
||||
* aria-describedby attribute values.
|
||||
|
@ -98,6 +165,10 @@ export const ValidationContextProvider = ( { children } ) => {
|
|||
clearValidationError,
|
||||
clearAllValidationErrors,
|
||||
getValidationErrorId,
|
||||
hideValidationError,
|
||||
showValidationError,
|
||||
showAllValidationErrors,
|
||||
hasValidationErrors,
|
||||
};
|
||||
return (
|
||||
<ValidationContext.Provider value={ context }>
|
||||
|
|
|
@ -58,7 +58,9 @@ export const useStoreCartCoupons = () => {
|
|||
}
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
setValidationErrors( { coupon: error.message } );
|
||||
setValidationErrors( {
|
||||
coupon: { message: error.message, hidden: false },
|
||||
} );
|
||||
// Finished handling the coupon.
|
||||
receiveApplyingCoupon( '' );
|
||||
} );
|
||||
|
|
|
@ -15,14 +15,14 @@ import {
|
|||
PlaceOrderButton,
|
||||
ReturnToCartButton,
|
||||
} from '@woocommerce/base-components/cart-checkout';
|
||||
import TextInput from '@woocommerce/base-components/text-input';
|
||||
import { ValidatedTextInput } from '@woocommerce/base-components/text-input';
|
||||
import ShippingRatesControl from '@woocommerce/base-components/shipping-rates-control';
|
||||
import CheckboxControl from '@woocommerce/base-components/checkbox-control';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/base-utils';
|
||||
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
|
||||
import {
|
||||
CheckoutProvider,
|
||||
ValidationContextProvider,
|
||||
useValidationContext,
|
||||
} from '@woocommerce/base-context';
|
||||
import {
|
||||
ExpressCheckoutFormControl,
|
||||
|
@ -37,6 +37,7 @@ import {
|
|||
Main,
|
||||
} from '@woocommerce/base-components/sidebar-layout';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -52,6 +53,7 @@ const Block = ( {
|
|||
cartTotals = {},
|
||||
isEditor = false,
|
||||
shippingRates = [],
|
||||
scrollToTop,
|
||||
} ) => {
|
||||
const [ selectedShippingRate, setSelectedShippingRate ] = useState( {} );
|
||||
const [ contactFields, setContactFields ] = useState( {} );
|
||||
|
@ -60,6 +62,19 @@ const Block = ( {
|
|||
const [ useShippingAsBilling, setUseShippingAsBilling ] = useState(
|
||||
attributes.useShippingAsBilling
|
||||
);
|
||||
const {
|
||||
hasValidationErrors,
|
||||
showAllValidationErrors,
|
||||
} = useValidationContext();
|
||||
|
||||
const validateSubmit = () => {
|
||||
if ( hasValidationErrors() ) {
|
||||
showAllValidationErrors();
|
||||
scrollToTop( { focusableSelector: 'input:invalid' } );
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const renderShippingRatesControlOption = ( option ) => ( {
|
||||
label: decodeEntities( option.name ),
|
||||
|
@ -100,266 +115,262 @@ const Block = ( {
|
|||
} = useShippingRates();
|
||||
|
||||
return (
|
||||
<ValidationContextProvider>
|
||||
<CheckoutProvider isEditor={ isEditor }>
|
||||
<SidebarLayout className="wc-block-checkout">
|
||||
<Main>
|
||||
<ExpressCheckoutFormControl />
|
||||
<CheckoutForm>
|
||||
<CheckoutProvider isEditor={ isEditor }>
|
||||
<SidebarLayout className="wc-block-checkout">
|
||||
<Main>
|
||||
<ExpressCheckoutFormControl />
|
||||
<CheckoutForm>
|
||||
<FormStep
|
||||
id="contact-fields"
|
||||
className="wc-block-checkout__contact-fields"
|
||||
title={ __(
|
||||
'Contact information',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
description={ __(
|
||||
"We'll use this email to send you details and updates about your order.",
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
stepHeadingContent={ () => (
|
||||
<Fragment>
|
||||
{ __(
|
||||
'Already have an account? ',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
<a href="/wp-login.php">
|
||||
{ __(
|
||||
'Log in.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</a>
|
||||
</Fragment>
|
||||
) }
|
||||
>
|
||||
<ValidatedTextInput
|
||||
type="email"
|
||||
label={ __(
|
||||
'Email address',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
value={ contactFields.email }
|
||||
autoComplete="email"
|
||||
onChange={ ( newValue ) =>
|
||||
setContactFields( {
|
||||
...contactFields,
|
||||
email: newValue,
|
||||
} )
|
||||
}
|
||||
required={ true }
|
||||
/>
|
||||
<CheckboxControl
|
||||
className="wc-block-checkout__keep-updated"
|
||||
label={ __(
|
||||
'Keep me up to date on news and exclusive offers',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ contactFields.keepUpdated }
|
||||
onChange={ () =>
|
||||
setContactFields( {
|
||||
...contactFields,
|
||||
keepUpdated: ! contactFields.keepUpdated,
|
||||
} )
|
||||
}
|
||||
/>
|
||||
</FormStep>
|
||||
{ SHIPPING_ENABLED && (
|
||||
<FormStep
|
||||
id="contact-fields"
|
||||
className="wc-block-checkout__contact-fields"
|
||||
id="shipping-fields"
|
||||
className="wc-block-checkout__shipping-fields"
|
||||
title={ __(
|
||||
'Contact information',
|
||||
'Shipping address',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
description={ __(
|
||||
"We'll use this email to send you details and updates about your order.",
|
||||
'Enter the physical address where you want us to deliver your order.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
stepHeadingContent={ () => (
|
||||
<Fragment>
|
||||
{ __(
|
||||
'Already have an account? ',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
<a href="/wp-login.php">
|
||||
{ __(
|
||||
'Log in.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</a>
|
||||
</Fragment>
|
||||
) }
|
||||
>
|
||||
<TextInput
|
||||
type="email"
|
||||
label={ __(
|
||||
'Email address',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
value={ contactFields.email }
|
||||
autoComplete="email"
|
||||
onChange={ ( newValue ) =>
|
||||
setContactFields( {
|
||||
...contactFields,
|
||||
email: newValue,
|
||||
} )
|
||||
}
|
||||
required={ true }
|
||||
<AddressForm
|
||||
onChange={ setShippingFields }
|
||||
values={ shippingFields }
|
||||
fields={ Object.keys( addressFields ) }
|
||||
fieldConfig={ addressFields }
|
||||
/>
|
||||
<CheckboxControl
|
||||
className="wc-block-checkout__keep-updated"
|
||||
label={ __(
|
||||
'Keep me up to date on news and exclusive offers',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ contactFields.keepUpdated }
|
||||
onChange={ () =>
|
||||
setContactFields( {
|
||||
...contactFields,
|
||||
keepUpdated: ! contactFields.keepUpdated,
|
||||
} )
|
||||
}
|
||||
/>
|
||||
</FormStep>
|
||||
{ SHIPPING_ENABLED && (
|
||||
<FormStep
|
||||
id="shipping-fields"
|
||||
className="wc-block-checkout__shipping-fields"
|
||||
title={ __(
|
||||
'Shipping address',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
description={ __(
|
||||
'Enter the physical address where you want us to deliver your order.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
<AddressForm
|
||||
onChange={ setShippingFields }
|
||||
values={ shippingFields }
|
||||
fields={ Object.keys( addressFields ) }
|
||||
fieldConfig={ addressFields }
|
||||
/>
|
||||
{ attributes.showPhoneField && (
|
||||
<TextInput
|
||||
type="tel"
|
||||
label={
|
||||
attributes.requirePhoneField
|
||||
? __(
|
||||
'Phone',
|
||||
'woo-gutenberg-products-block'
|
||||
)
|
||||
: __(
|
||||
'Phone (optional)',
|
||||
'woo-gutenberg-products-block'
|
||||
)
|
||||
}
|
||||
value={ shippingFields.phone }
|
||||
autoComplete="tel"
|
||||
onChange={ ( newValue ) =>
|
||||
setShippingFields( {
|
||||
...shippingFields,
|
||||
phone: newValue,
|
||||
} )
|
||||
}
|
||||
required={
|
||||
attributes.requirePhoneField
|
||||
}
|
||||
/>
|
||||
) }
|
||||
<CheckboxControl
|
||||
className="wc-block-checkout__use-address-for-billing"
|
||||
label={ __(
|
||||
'Use same address for billing',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ useShippingAddressAsBilling }
|
||||
onChange={ ( isChecked ) =>
|
||||
setUseShippingAsBilling( isChecked )
|
||||
{ attributes.showPhoneField && (
|
||||
<ValidatedTextInput
|
||||
type="tel"
|
||||
label={
|
||||
attributes.requirePhoneField
|
||||
? __(
|
||||
'Phone',
|
||||
'woo-gutenberg-products-block'
|
||||
)
|
||||
: __(
|
||||
'Phone (optional)',
|
||||
'woo-gutenberg-products-block'
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormStep>
|
||||
) }
|
||||
{ showBillingFields && (
|
||||
<FormStep
|
||||
id="billing-fields"
|
||||
className="wc-block-checkout__billing-fields"
|
||||
title={ __(
|
||||
'Billing address',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
description={ __(
|
||||
'Enter the address that matches your card or payment method.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
<AddressForm
|
||||
onChange={ setBillingFields }
|
||||
type="billing"
|
||||
values={ billingFields }
|
||||
fields={ Object.keys( addressFields ) }
|
||||
fieldConfig={ addressFields }
|
||||
/>
|
||||
</FormStep>
|
||||
) }
|
||||
{ SHIPPING_ENABLED && (
|
||||
<FormStep
|
||||
id="shipping-option"
|
||||
className="wc-block-checkout__shipping-option"
|
||||
title={ __(
|
||||
'Shipping options',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
description={ __(
|
||||
'Select a shipping method below.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
{ shippingRates.length === 0 && isEditor ? (
|
||||
<NoShipping />
|
||||
) : (
|
||||
<ShippingRatesControl
|
||||
address={
|
||||
shippingFields.country
|
||||
? {
|
||||
address_1:
|
||||
shippingFields.address_1,
|
||||
address_2:
|
||||
shippingFields.apartment,
|
||||
city:
|
||||
shippingFields.city,
|
||||
state:
|
||||
shippingFields.state,
|
||||
postcode:
|
||||
shippingFields.postcode,
|
||||
country:
|
||||
shippingFields.country,
|
||||
}
|
||||
: null
|
||||
}
|
||||
noResultsMessage={ __(
|
||||
'There are no shipping options available. Please ensure that your address has been entered correctly, or contact us if you need any help.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
renderOption={
|
||||
renderShippingRatesControlOption
|
||||
}
|
||||
shippingRates={ shippingRates }
|
||||
shippingRatesLoading={
|
||||
shippingRatesLoading
|
||||
}
|
||||
/>
|
||||
) }
|
||||
<CheckboxControl
|
||||
className="wc-block-checkout__add-note"
|
||||
label="Add order notes?"
|
||||
checked={
|
||||
selectedShippingRate.orderNote
|
||||
}
|
||||
onChange={ () =>
|
||||
setSelectedShippingRate( {
|
||||
...selectedShippingRate,
|
||||
orderNote: ! selectedShippingRate.orderNote,
|
||||
value={ shippingFields.phone }
|
||||
autoComplete="tel"
|
||||
onChange={ ( newValue ) =>
|
||||
setShippingFields( {
|
||||
...shippingFields,
|
||||
phone: newValue,
|
||||
} )
|
||||
}
|
||||
required={
|
||||
attributes.requirePhoneField
|
||||
}
|
||||
/>
|
||||
</FormStep>
|
||||
) }
|
||||
<FormStep
|
||||
id="payment-method"
|
||||
className="wc-block-checkout__payment-method"
|
||||
title={ __(
|
||||
'Payment method',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
description={ __(
|
||||
'Select a payment method below.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
<PaymentMethods />
|
||||
{ /*@todo this should be something the payment method controls*/ }
|
||||
<CheckboxControl
|
||||
className="wc-block-checkout__save-card-info"
|
||||
className="wc-block-checkout__use-address-for-billing"
|
||||
label={ __(
|
||||
'Save payment information to my account for future purchases.',
|
||||
'Use same address for billing',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ shouldSavePayment }
|
||||
onChange={ () =>
|
||||
setShouldSavePayment(
|
||||
! shouldSavePayment
|
||||
)
|
||||
checked={ useShippingAddressAsBilling }
|
||||
onChange={ ( isChecked ) =>
|
||||
setUseShippingAsBilling( isChecked )
|
||||
}
|
||||
/>
|
||||
</FormStep>
|
||||
<div className="wc-block-checkout__actions">
|
||||
{ attributes.showReturnToCart && (
|
||||
<ReturnToCartButton
|
||||
link={ getSetting(
|
||||
'page-' + attributes?.cartPageId,
|
||||
false
|
||||
) }
|
||||
{ showBillingFields && (
|
||||
<FormStep
|
||||
id="billing-fields"
|
||||
className="wc-block-checkout__billing-fields"
|
||||
title={ __(
|
||||
'Billing address',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
description={ __(
|
||||
'Enter the address that matches your card or payment method.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
<AddressForm
|
||||
onChange={ setBillingFields }
|
||||
type="billing"
|
||||
values={ billingFields }
|
||||
fields={ Object.keys( addressFields ) }
|
||||
fieldConfig={ addressFields }
|
||||
/>
|
||||
</FormStep>
|
||||
) }
|
||||
{ SHIPPING_ENABLED && (
|
||||
<FormStep
|
||||
id="shipping-option"
|
||||
className="wc-block-checkout__shipping-option"
|
||||
title={ __(
|
||||
'Shipping options',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
description={ __(
|
||||
'Select a shipping method below.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
{ shippingRates.length === 0 && isEditor ? (
|
||||
<NoShipping />
|
||||
) : (
|
||||
<ShippingRatesControl
|
||||
address={
|
||||
shippingFields.country
|
||||
? {
|
||||
address_1:
|
||||
shippingFields.address_1,
|
||||
address_2:
|
||||
shippingFields.apartment,
|
||||
city:
|
||||
shippingFields.city,
|
||||
state:
|
||||
shippingFields.state,
|
||||
postcode:
|
||||
shippingFields.postcode,
|
||||
country:
|
||||
shippingFields.country,
|
||||
}
|
||||
: null
|
||||
}
|
||||
noResultsMessage={ __(
|
||||
'There are no shipping options available. Please ensure that your address has been entered correctly, or contact us if you need any help.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
renderOption={
|
||||
renderShippingRatesControlOption
|
||||
}
|
||||
shippingRates={ shippingRates }
|
||||
shippingRatesLoading={
|
||||
shippingRatesLoading
|
||||
}
|
||||
/>
|
||||
) }
|
||||
<PlaceOrderButton />
|
||||
</div>
|
||||
{ attributes.showPolicyLinks && <Policies /> }
|
||||
</CheckoutForm>
|
||||
</Main>
|
||||
<Sidebar className="wc-block-checkout__sidebar">
|
||||
<CheckoutSidebar
|
||||
cartCoupons={ cartCoupons }
|
||||
cartItems={ cartItems }
|
||||
cartTotals={ cartTotals }
|
||||
/>
|
||||
</Sidebar>
|
||||
</SidebarLayout>
|
||||
</CheckoutProvider>
|
||||
</ValidationContextProvider>
|
||||
<CheckboxControl
|
||||
className="wc-block-checkout__add-note"
|
||||
label="Add order notes?"
|
||||
checked={ selectedShippingRate.orderNote }
|
||||
onChange={ () =>
|
||||
setSelectedShippingRate( {
|
||||
...selectedShippingRate,
|
||||
orderNote: ! selectedShippingRate.orderNote,
|
||||
} )
|
||||
}
|
||||
/>
|
||||
</FormStep>
|
||||
) }
|
||||
<FormStep
|
||||
id="payment-method"
|
||||
className="wc-block-checkout__payment-method"
|
||||
title={ __(
|
||||
'Payment method',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
description={ __(
|
||||
'Select a payment method below.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
<PaymentMethods />
|
||||
{ /*@todo this should be something the payment method controls*/ }
|
||||
<CheckboxControl
|
||||
className="wc-block-checkout__save-card-info"
|
||||
label={ __(
|
||||
'Save payment information to my account for future purchases.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ shouldSavePayment }
|
||||
onChange={ () =>
|
||||
setShouldSavePayment( ! shouldSavePayment )
|
||||
}
|
||||
/>
|
||||
</FormStep>
|
||||
<div className="wc-block-checkout__actions">
|
||||
{ attributes.showReturnToCart && (
|
||||
<ReturnToCartButton
|
||||
link={ getSetting(
|
||||
'page-' + attributes?.cartPageId,
|
||||
false
|
||||
) }
|
||||
/>
|
||||
) }
|
||||
<PlaceOrderButton
|
||||
validateSubmit={ validateSubmit }
|
||||
/>
|
||||
</div>
|
||||
{ attributes.showPolicyLinks && <Policies /> }
|
||||
</CheckoutForm>
|
||||
</Main>
|
||||
<Sidebar className="wc-block-checkout__sidebar">
|
||||
<CheckoutSidebar
|
||||
cartCoupons={ cartCoupons }
|
||||
cartItems={ cartItems }
|
||||
cartTotals={ cartTotals }
|
||||
/>
|
||||
</Sidebar>
|
||||
</SidebarLayout>
|
||||
</CheckoutProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Block;
|
||||
export default withScrollToTop( Block );
|
||||
|
|
|
@ -7,7 +7,10 @@ import {
|
|||
withStoreCartApiHydration,
|
||||
} from '@woocommerce/block-hocs';
|
||||
import { useStoreCart } from '@woocommerce/base-hooks';
|
||||
import { StoreNoticesProvider } from '@woocommerce/base-context';
|
||||
import {
|
||||
StoreNoticesProvider,
|
||||
ValidationContextProvider,
|
||||
} from '@woocommerce/base-context';
|
||||
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/block-settings';
|
||||
import { __experimentalCreateInterpolateElement } from 'wordpress-element';
|
||||
|
@ -59,13 +62,15 @@ const CheckoutFrontend = ( props ) => {
|
|||
showErrorMessage={ CURRENT_USER_IS_ADMIN }
|
||||
>
|
||||
<StoreNoticesProvider context="wc/checkout">
|
||||
<Block
|
||||
{ ...props }
|
||||
cartCoupons={ cartCoupons }
|
||||
cartItems={ cartItems }
|
||||
cartTotals={ cartTotals }
|
||||
shippingRates={ shippingRates }
|
||||
/>
|
||||
<ValidationContextProvider>
|
||||
<Block
|
||||
{ ...props }
|
||||
cartCoupons={ cartCoupons }
|
||||
cartItems={ cartItems }
|
||||
cartTotals={ cartTotals }
|
||||
shippingRates={ shippingRates }
|
||||
/>
|
||||
</ValidationContextProvider>
|
||||
</StoreNoticesProvider>
|
||||
</BlockErrorBoundary>
|
||||
) }
|
||||
|
|
|
@ -218,10 +218,9 @@
|
|||
/**
|
||||
* @typedef {Object} ValidationContext
|
||||
*
|
||||
* @property {function(string):string} getValidationError Return validation error or empty
|
||||
* string if it doesn't exist for the
|
||||
* @property {function(string):Object} getValidationError Return validation error for the
|
||||
* given property.
|
||||
* @property {function(Object<string>)} setValidationErrors Receive an object of properties and
|
||||
* @property {function(Object<Object>)} setValidationErrors Receive an object of properties and
|
||||
* error messages as strings and adds
|
||||
* to the validation error state.
|
||||
* @property {function(string)} clearValidationError Clears a validation error for the
|
||||
|
@ -231,6 +230,14 @@
|
|||
* @property {function(string)} getValidationErrorId Returns the css id for the
|
||||
* validation error using the given
|
||||
* inputId string.
|
||||
* @property {function(string)} hideValidationError Sets the hidden prop of a specific
|
||||
* error to true.
|
||||
* @property {function(string)} showValidationError Sets the hidden prop of a specific
|
||||
* error to false.
|
||||
* @property {function()} showAllValidationErrors Sets the hidden prop of all
|
||||
* errors to false.
|
||||
* @property {function():boolean} hasValidationErrors Returns true if there is at least
|
||||
* one error.
|
||||
*/
|
||||
|
||||
export {};
|
||||
|
|
Loading…
Reference in New Issue