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
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TextInput from '@woocommerce/base-components/text-input';
|
import { ValidatedTextInput } from '@woocommerce/base-components/text-input';
|
||||||
import {
|
import {
|
||||||
BillingCountryInput,
|
BillingCountryInput,
|
||||||
ShippingCountryInput,
|
ShippingCountryInput,
|
||||||
|
@ -21,7 +21,9 @@ import { __ } from '@wordpress/i18n';
|
||||||
import defaultAddressFields from './default-address-fields';
|
import defaultAddressFields from './default-address-fields';
|
||||||
import countryAddressFields from './country-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,
|
values,
|
||||||
setValidationErrors,
|
setValidationErrors,
|
||||||
clearValidationError,
|
clearValidationError,
|
||||||
|
@ -30,17 +32,20 @@ const validateCountry = (
|
||||||
if (
|
if (
|
||||||
! hasValidationError &&
|
! hasValidationError &&
|
||||||
! values.country &&
|
! values.country &&
|
||||||
Object.values( values ).some( ( value ) => value !== '' )
|
( values.city || values.state || values.postcode )
|
||||||
) {
|
) {
|
||||||
setValidationErrors( {
|
setValidationErrors( {
|
||||||
country: __(
|
'shipping-missing-country': {
|
||||||
'Please select a country to calculate rates.',
|
message: __(
|
||||||
'woo-gutenberg-products-block'
|
'Please select a country to calculate rates.',
|
||||||
),
|
'woo-gutenberg-products-block'
|
||||||
|
),
|
||||||
|
hidden: false,
|
||||||
|
},
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
if ( hasValidationError && values.country ) {
|
if ( hasValidationError && values.country ) {
|
||||||
clearValidationError( 'country' );
|
clearValidationError( 'shipping-missing-country' );
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -63,26 +68,31 @@ const AddressForm = ( {
|
||||||
const addressFields = fields.map( ( field ) => ( {
|
const addressFields = fields.map( ( field ) => ( {
|
||||||
key: field,
|
key: field,
|
||||||
...defaultAddressFields[ field ],
|
...defaultAddressFields[ field ],
|
||||||
...fieldConfig[ field ],
|
|
||||||
...countryLocale[ field ],
|
...countryLocale[ field ],
|
||||||
|
...fieldConfig[ field ],
|
||||||
} ) );
|
} ) );
|
||||||
const sortedAddressFields = addressFields.sort(
|
const sortedAddressFields = addressFields.sort(
|
||||||
( a, b ) => a.index - b.index
|
( a, b ) => a.index - b.index
|
||||||
);
|
);
|
||||||
const countryValidationError = getValidationError( 'country' );
|
const countryValidationError =
|
||||||
|
getValidationError( 'shipping-missing-country' ) || {};
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
validateCountry(
|
if ( type === 'shipping' ) {
|
||||||
values,
|
validateShippingCountry(
|
||||||
setValidationErrors,
|
values,
|
||||||
clearValidationError,
|
setValidationErrors,
|
||||||
!! countryValidationError
|
clearValidationError,
|
||||||
);
|
countryValidationError.message &&
|
||||||
|
! countryValidationError.hidden
|
||||||
|
);
|
||||||
|
}
|
||||||
}, [
|
}, [
|
||||||
values,
|
values,
|
||||||
countryValidationError,
|
countryValidationError,
|
||||||
setValidationErrors,
|
setValidationErrors,
|
||||||
clearValidationError,
|
clearValidationError,
|
||||||
] );
|
] );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="wc-block-address-form">
|
<div className="wc-block-address-form">
|
||||||
{ sortedAddressFields.map( ( field ) => {
|
{ sortedAddressFields.map( ( field ) => {
|
||||||
|
@ -114,6 +124,12 @@ const AddressForm = ( {
|
||||||
postcode: '',
|
postcode: '',
|
||||||
} )
|
} )
|
||||||
}
|
}
|
||||||
|
errorId={
|
||||||
|
type === 'shipping'
|
||||||
|
? 'shipping-missing-country'
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
errorMessage={ field.errorMessage }
|
||||||
required={ field.required }
|
required={ field.required }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -141,13 +157,14 @@ const AddressForm = ( {
|
||||||
state: newValue,
|
state: newValue,
|
||||||
} )
|
} )
|
||||||
}
|
}
|
||||||
|
errorMessage={ field.errorMessage }
|
||||||
required={ field.required }
|
required={ field.required }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<ValidatedTextInput
|
||||||
key={ field.key }
|
key={ field.key }
|
||||||
className={ `wc-block-address-form__${ field.key }` }
|
className={ `wc-block-address-form__${ field.key }` }
|
||||||
label={
|
label={
|
||||||
|
@ -161,6 +178,7 @@ const AddressForm = ( {
|
||||||
[ field.key ]: newValue,
|
[ field.key ]: newValue,
|
||||||
} )
|
} )
|
||||||
}
|
}
|
||||||
|
errorMessage={ field.errorMessage }
|
||||||
required={ field.required }
|
required={ field.required }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,23 +2,33 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useCheckoutContext } from '@woocommerce/base-context';
|
import { useCheckoutContext } from '@woocommerce/base-context';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import Button from '../button';
|
import Button from '../button';
|
||||||
|
|
||||||
const PlaceOrderButton = () => {
|
const PlaceOrderButton = ( { validateSubmit } ) => {
|
||||||
const { submitLabel, onSubmit } = useCheckoutContext();
|
const { submitLabel, onSubmit } = useCheckoutContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className="wc-block-components-checkout-place-order-button"
|
className="wc-block-components-checkout-place-order-button"
|
||||||
onClick={ onSubmit }
|
onClick={ () => {
|
||||||
|
const isValid = validateSubmit();
|
||||||
|
if ( isValid ) {
|
||||||
|
onSubmit();
|
||||||
|
}
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
{ submitLabel }
|
{ submitLabel }
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
PlaceOrderButton.propTypes = {
|
||||||
|
validateSubmit: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default PlaceOrderButton;
|
export default PlaceOrderButton;
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { decodeEntities } from '@wordpress/html-entities';
|
import { decodeEntities } from '@wordpress/html-entities';
|
||||||
import { useValidationContext } from '@woocommerce/base-context';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import Select from '../select';
|
import { ValidatedSelect } from '../select';
|
||||||
import { ValidationInputError } from '../validation';
|
|
||||||
|
|
||||||
const CountryInput = ( {
|
const CountryInput = ( {
|
||||||
className,
|
className,
|
||||||
|
@ -20,23 +19,27 @@ const CountryInput = ( {
|
||||||
value = '',
|
value = '',
|
||||||
autoComplete = 'off',
|
autoComplete = 'off',
|
||||||
required = false,
|
required = false,
|
||||||
|
errorId,
|
||||||
|
errorMessage = __(
|
||||||
|
'Please select a country.',
|
||||||
|
'woo-gutenberg-products-block'
|
||||||
|
),
|
||||||
} ) => {
|
} ) => {
|
||||||
const options = Object.keys( countries ).map( ( key ) => ( {
|
const options = Object.keys( countries ).map( ( key ) => ( {
|
||||||
key,
|
key,
|
||||||
name: decodeEntities( countries[ key ] ),
|
name: decodeEntities( countries[ key ] ),
|
||||||
} ) );
|
} ) );
|
||||||
const { getValidationError } = useValidationContext();
|
|
||||||
const errorMessage = getValidationError( 'country' );
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ classnames( className, 'wc-block-country-input' ) }>
|
<div className={ classnames( className, 'wc-block-country-input' ) }>
|
||||||
<Select
|
<ValidatedSelect
|
||||||
label={ label }
|
label={ label }
|
||||||
onChange={ onChange }
|
onChange={ onChange }
|
||||||
options={ options }
|
options={ options }
|
||||||
value={ options.find( ( option ) => option.key === value ) }
|
value={ options.find( ( option ) => option.key === value ) }
|
||||||
|
errorId={ errorId }
|
||||||
|
errorMessage={ errorMessage }
|
||||||
required={ required }
|
required={ required }
|
||||||
hasError={ !! errorMessage }
|
|
||||||
/>
|
/>
|
||||||
{ autoComplete !== 'off' && (
|
{ autoComplete !== 'off' && (
|
||||||
<input
|
<input
|
||||||
|
@ -61,7 +64,6 @@ const CountryInput = ( {
|
||||||
tabIndex={ -1 }
|
tabIndex={ -1 }
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
<ValidationInputError errorMessage={ errorMessage } />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -73,6 +75,8 @@ CountryInput.propTypes = {
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
autoComplete: PropTypes.string,
|
autoComplete: PropTypes.string,
|
||||||
|
errorId: PropTypes.string,
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CountryInput;
|
export default CountryInput;
|
||||||
|
|
|
@ -10,20 +10,32 @@ import { CustomSelectControl } from 'wordpress-components';
|
||||||
*/
|
*/
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
|
||||||
const Select = ( { className, label, onChange, options, value, hasError } ) => {
|
const Select = ( {
|
||||||
|
className,
|
||||||
|
feedback,
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
onChange,
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
} ) => {
|
||||||
return (
|
return (
|
||||||
<CustomSelectControl
|
<div
|
||||||
|
id={ id }
|
||||||
className={ classnames( 'wc-block-select', className, {
|
className={ classnames( 'wc-block-select', className, {
|
||||||
'is-active': value,
|
'is-active': value,
|
||||||
'has-error': hasError,
|
|
||||||
} ) }
|
} ) }
|
||||||
label={ label }
|
>
|
||||||
onChange={ ( { selectedItem } ) => {
|
<CustomSelectControl
|
||||||
onChange( selectedItem.key );
|
label={ label }
|
||||||
} }
|
onChange={ ( { selectedItem } ) => {
|
||||||
options={ options }
|
onChange( selectedItem.key );
|
||||||
value={ value }
|
} }
|
||||||
/>
|
options={ options }
|
||||||
|
value={ value }
|
||||||
|
/>
|
||||||
|
{ feedback }
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,6 +48,8 @@ Select.propTypes = {
|
||||||
} ).isRequired
|
} ).isRequired
|
||||||
).isRequired,
|
).isRequired,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
feedback: PropTypes.node,
|
||||||
|
id: PropTypes.string,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
value: PropTypes.shape( {
|
value: PropTypes.shape( {
|
||||||
key: PropTypes.string.isRequired,
|
key: PropTypes.string.isRequired,
|
||||||
|
@ -44,3 +58,4 @@ Select.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Select;
|
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,
|
addressFields,
|
||||||
} ) => {
|
} ) => {
|
||||||
const [ address, setAddress ] = useState( initialAddress );
|
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 (
|
return (
|
||||||
<form className="wc-block-shipping-calculator-address">
|
<form className="wc-block-shipping-calculator-address">
|
||||||
<AddressForm
|
<AddressForm
|
||||||
fields={ addressFields }
|
fields={ addressFields }
|
||||||
|
fieldConfig={ fieldConfig }
|
||||||
onChange={ setAddress }
|
onChange={ setAddress }
|
||||||
values={ address }
|
values={ address }
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="wc-block-shipping-calculator-address__button"
|
className="wc-block-shipping-calculator-address__button"
|
||||||
disabled={
|
disabled={ isShallowEqual( address, initialAddress ) }
|
||||||
isShallowEqual( address, initialAddress ) ||
|
|
||||||
getValidationError( 'country' )
|
|
||||||
}
|
|
||||||
onClick={ ( e ) => {
|
onClick={ ( e ) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return onUpdate( address );
|
const isAddressValid = validateSubmit();
|
||||||
|
if ( isAddressValid ) {
|
||||||
|
return onUpdate( address );
|
||||||
|
}
|
||||||
} }
|
} }
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { decodeEntities } from '@wordpress/html-entities';
|
import { decodeEntities } from '@wordpress/html-entities';
|
||||||
import { useCallback, useEffect } from '@wordpress/element';
|
import { useCallback, useEffect } from '@wordpress/element';
|
||||||
|
@ -8,8 +9,8 @@ import { useCallback, useEffect } from '@wordpress/element';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import TextInput from '../text-input';
|
import { ValidatedTextInput } from '../text-input';
|
||||||
import Select from '../select';
|
import { ValidatedSelect } from '../select';
|
||||||
|
|
||||||
const StateInput = ( {
|
const StateInput = ( {
|
||||||
className,
|
className,
|
||||||
|
@ -61,12 +62,16 @@ const StateInput = ( {
|
||||||
if ( options.length > 0 ) {
|
if ( options.length > 0 ) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Select
|
<ValidatedSelect
|
||||||
className={ className }
|
className={ className }
|
||||||
label={ label }
|
label={ label }
|
||||||
onChange={ onChangeState }
|
onChange={ onChangeState }
|
||||||
options={ options }
|
options={ options }
|
||||||
value={ options.find( ( option ) => option.key === value ) }
|
value={ options.find( ( option ) => option.key === value ) }
|
||||||
|
errorMessage={ __(
|
||||||
|
'Please select a state.',
|
||||||
|
'woo-gutenberg-products-block'
|
||||||
|
) }
|
||||||
required={ required }
|
required={ required }
|
||||||
/>
|
/>
|
||||||
{ autoComplete !== 'off' && (
|
{ autoComplete !== 'off' && (
|
||||||
|
@ -92,7 +97,7 @@ const StateInput = ( {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<ValidatedTextInput
|
||||||
className={ className }
|
className={ className }
|
||||||
label={ label }
|
label={ label }
|
||||||
onChange={ onChangeState }
|
onChange={ onChangeState }
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
|
import { forwardRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { useState } from '@wordpress/element';
|
import { useState } from '@wordpress/element';
|
||||||
import { withInstanceId } from 'wordpress-compose';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -12,73 +12,84 @@ import { withInstanceId } from 'wordpress-compose';
|
||||||
import Label from '../label';
|
import Label from '../label';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
|
||||||
const TextInput = ( {
|
const TextInput = forwardRef(
|
||||||
className,
|
(
|
||||||
instanceId,
|
{
|
||||||
id,
|
className,
|
||||||
type = 'text',
|
id,
|
||||||
ariaLabel,
|
type = 'text',
|
||||||
ariaDescribedBy,
|
ariaLabel,
|
||||||
label,
|
ariaDescribedBy,
|
||||||
screenReaderLabel,
|
label,
|
||||||
disabled,
|
screenReaderLabel,
|
||||||
help,
|
disabled,
|
||||||
autoComplete = 'off',
|
help,
|
||||||
value = '',
|
autoComplete = 'off',
|
||||||
onChange,
|
value = '',
|
||||||
required = false,
|
onChange,
|
||||||
} ) => {
|
required = false,
|
||||||
const [ isActive, setIsActive ] = useState( false );
|
onBlur = () => {},
|
||||||
const onChangeValue = ( event ) => onChange( event.target.value );
|
feedback,
|
||||||
const textInputId = id || 'textinput-' + instanceId;
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const [ isActive, setIsActive ] = useState( false );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={ classnames( 'wc-block-text-input', className, {
|
className={ classnames( 'wc-block-text-input', className, {
|
||||||
'is-active': isActive || value,
|
'is-active': isActive || value,
|
||||||
} ) }
|
} ) }
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type={ type }
|
type={ type }
|
||||||
id={ textInputId }
|
id={ id }
|
||||||
value={ value }
|
value={ value }
|
||||||
autoComplete={ autoComplete }
|
ref={ ref }
|
||||||
onChange={ onChangeValue }
|
autoComplete={ autoComplete }
|
||||||
onFocus={ () => setIsActive( true ) }
|
onChange={ ( event ) => {
|
||||||
onBlur={ () => setIsActive( false ) }
|
onChange( event.target.value );
|
||||||
aria-label={ ariaLabel || label }
|
} }
|
||||||
disabled={ disabled }
|
onFocus={ () => setIsActive( true ) }
|
||||||
aria-describedby={
|
onBlur={ () => {
|
||||||
!! help && ! ariaDescribedBy
|
onBlur();
|
||||||
? textInputId + '__help'
|
setIsActive( false );
|
||||||
: ariaDescribedBy
|
} }
|
||||||
}
|
aria-label={ ariaLabel || label }
|
||||||
required={ required }
|
disabled={ disabled }
|
||||||
/>
|
aria-describedby={
|
||||||
<Label
|
!! help && ! ariaDescribedBy
|
||||||
label={ label }
|
? id + '__help'
|
||||||
screenReaderLabel={ screenReaderLabel || label }
|
: ariaDescribedBy
|
||||||
wrapperElement="label"
|
}
|
||||||
wrapperProps={ {
|
required={ required }
|
||||||
htmlFor: textInputId,
|
/>
|
||||||
} }
|
<Label
|
||||||
htmlFor={ textInputId }
|
label={ label }
|
||||||
/>
|
screenReaderLabel={ screenReaderLabel || label }
|
||||||
{ !! help && (
|
wrapperElement="label"
|
||||||
<p
|
wrapperProps={ {
|
||||||
id={ textInputId + '__help' }
|
htmlFor: id,
|
||||||
className="wc-block-text-input__help"
|
} }
|
||||||
>
|
htmlFor={ id }
|
||||||
{ help }
|
/>
|
||||||
</p>
|
{ !! help && (
|
||||||
) }
|
<p
|
||||||
</div>
|
id={ id + '__help' }
|
||||||
);
|
className="wc-block-text-input__help"
|
||||||
};
|
>
|
||||||
|
{ help }
|
||||||
|
</p>
|
||||||
|
) }
|
||||||
|
{ feedback }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
TextInput.propTypes = {
|
TextInput.propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
id: PropTypes.string,
|
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
ariaLabel: PropTypes.string,
|
ariaLabel: PropTypes.string,
|
||||||
ariaDescribedBy: PropTypes.string,
|
ariaDescribedBy: PropTypes.string,
|
||||||
|
@ -90,4 +101,5 @@ TextInput.propTypes = {
|
||||||
required: PropTypes.bool,
|
required: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withInstanceId( TextInput );
|
export default TextInput;
|
||||||
|
export { default as ValidatedTextInput } from './validated';
|
||||||
|
|
|
@ -3,13 +3,64 @@
|
||||||
margin-bottom: $gap-large;
|
margin-bottom: $gap-large;
|
||||||
white-space: nowrap;
|
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 {
|
&.has-error input {
|
||||||
/**
|
border-color: $error-red;
|
||||||
* 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;
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline-color: $error-red;
|
outline-color: $error-red;
|
||||||
}
|
}
|
||||||
|
@ -19,55 +70,3 @@
|
||||||
color: $error-red;
|
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 { useState, useEffect, useRef } from '@wordpress/element';
|
||||||
import { PanelBody, PanelRow } from 'wordpress-components';
|
import { PanelBody, PanelRow } from 'wordpress-components';
|
||||||
import { Button } from '@woocommerce/base-components/cart-checkout';
|
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 Label from '@woocommerce/base-components/label';
|
||||||
import { ValidationInputError } from '@woocommerce/base-components/validation';
|
import { ValidationInputError } from '@woocommerce/base-components/validation';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withInstanceId } from 'wordpress-compose';
|
import { withInstanceId } from 'wordpress-compose';
|
||||||
import classnames from 'classnames';
|
|
||||||
import { useValidationContext } from '@woocommerce/base-context';
|
import { useValidationContext } from '@woocommerce/base-context';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,21 +26,18 @@ const TotalsCouponCodeInput = ( {
|
||||||
} ) => {
|
} ) => {
|
||||||
const [ couponValue, setCouponValue ] = useState( '' );
|
const [ couponValue, setCouponValue ] = useState( '' );
|
||||||
const currentIsLoading = useRef( false );
|
const currentIsLoading = useRef( false );
|
||||||
const {
|
const { getValidationError, getValidationErrorId } = useValidationContext();
|
||||||
getValidationError,
|
|
||||||
clearValidationError,
|
const validationError = getValidationError( 'coupon' );
|
||||||
getValidationErrorId,
|
|
||||||
} = useValidationContext();
|
|
||||||
const validationMessage = getValidationError( 'coupon' );
|
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
if ( currentIsLoading.current !== isLoading ) {
|
if ( currentIsLoading.current !== isLoading ) {
|
||||||
if ( ! isLoading && couponValue && ! validationMessage ) {
|
if ( ! isLoading && couponValue && ! validationError ) {
|
||||||
setCouponValue( '' );
|
setCouponValue( '' );
|
||||||
}
|
}
|
||||||
currentIsLoading.current = isLoading;
|
currentIsLoading.current = isLoading;
|
||||||
}
|
}
|
||||||
}, [ isLoading, couponValue, validationMessage ] );
|
}, [ isLoading, couponValue, validationError ] );
|
||||||
|
|
||||||
const textInputId = `wc-block-coupon-code__input-${ instanceId }`;
|
const textInputId = `wc-block-coupon-code__input-${ instanceId }`;
|
||||||
|
|
||||||
|
@ -73,14 +69,10 @@ const TotalsCouponCodeInput = ( {
|
||||||
>
|
>
|
||||||
<PanelRow className="wc-block-coupon-code__row">
|
<PanelRow className="wc-block-coupon-code__row">
|
||||||
<form className="wc-block-coupon-code__form">
|
<form className="wc-block-coupon-code__form">
|
||||||
<TextInput
|
<ValidatedTextInput
|
||||||
id={ textInputId }
|
id={ textInputId }
|
||||||
className={ classnames(
|
errorId="coupon"
|
||||||
'wc-block-coupon-code__input',
|
className="wc-block-coupon-code__input"
|
||||||
{
|
|
||||||
'has-error': !! validationMessage,
|
|
||||||
}
|
|
||||||
) }
|
|
||||||
label={ __(
|
label={ __(
|
||||||
'Enter code',
|
'Enter code',
|
||||||
'woo-gutenberg-products-block'
|
'woo-gutenberg-products-block'
|
||||||
|
@ -91,8 +83,9 @@ const TotalsCouponCodeInput = ( {
|
||||||
) }
|
) }
|
||||||
onChange={ ( newCouponValue ) => {
|
onChange={ ( newCouponValue ) => {
|
||||||
setCouponValue( newCouponValue );
|
setCouponValue( newCouponValue );
|
||||||
clearValidationError( 'coupon' );
|
|
||||||
} }
|
} }
|
||||||
|
validateOnMount={ false }
|
||||||
|
showError={ false }
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="wc-block-coupon-code__button"
|
className="wc-block-coupon-code__button"
|
||||||
|
@ -106,11 +99,11 @@ const TotalsCouponCodeInput = ( {
|
||||||
{ __( 'Apply', 'woo-gutenberg-products-block' ) }
|
{ __( 'Apply', 'woo-gutenberg-products-block' ) }
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
<ValidationInputError
|
||||||
|
propertyName="coupon"
|
||||||
|
elementId={ textInputId }
|
||||||
|
/>
|
||||||
</PanelRow>
|
</PanelRow>
|
||||||
<ValidationInputError
|
|
||||||
errorMessage={ validationMessage }
|
|
||||||
elementId={ textInputId }
|
|
||||||
/>
|
|
||||||
</LoadingMask>
|
</LoadingMask>
|
||||||
</PanelBody>
|
</PanelBody>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
.wc-block-coupon-code__input {
|
.wc-block-coupon-code__input {
|
||||||
|
margin-bottom: 0;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
@ -16,3 +17,13 @@
|
||||||
white-space: nowrap;
|
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 {
|
.wc-block-form-input-validation-error {
|
||||||
color: $error-red;
|
color: $error-red;
|
||||||
@include font-size(12);
|
@include font-size(12);
|
||||||
margin-top: -$gap-large;
|
max-width: 100%;
|
||||||
padding-top: $gap-smallest;
|
position: absolute;
|
||||||
display: block;
|
top: calc(100% - 1px);
|
||||||
|
white-space: normal;
|
||||||
|
|
||||||
> p {
|
> p {
|
||||||
padding: 0;
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
line-height: 12px; // This allows two lines in a 24px bottom margin.
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,15 @@ export const ValidationInputError = ( {
|
||||||
elementId = '',
|
elementId = '',
|
||||||
} ) => {
|
} ) => {
|
||||||
const { getValidationError, getValidationErrorId } = useValidationContext();
|
const { getValidationError, getValidationErrorId } = useValidationContext();
|
||||||
if ( ! errorMessage && ! propertyName ) {
|
if ( ! errorMessage ) {
|
||||||
return null;
|
const error = getValidationError( propertyName ) || {};
|
||||||
|
if ( error.message && ! error.hidden ) {
|
||||||
|
errorMessage = error.message;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
errorMessage = errorMessage || getValidationError( propertyName );
|
|
||||||
return (
|
return (
|
||||||
<div className="wc-block-form-input-validation-error" role="alert">
|
<div className="wc-block-form-input-validation-error" role="alert">
|
||||||
<p id={ getValidationErrorId( elementId ) }>{ errorMessage }</p>
|
<p id={ getValidationErrorId( elementId ) }>{ errorMessage }</p>
|
||||||
|
|
|
@ -38,11 +38,9 @@ export const ValidationContextProvider = ( { children } ) => {
|
||||||
*
|
*
|
||||||
* @param {string} property The property the error message is for.
|
* @param {string} property The property the error message is for.
|
||||||
*
|
*
|
||||||
* @return {string} Either the error message for the given property or an
|
* @return {Object} The error object for the given property.
|
||||||
* empty string.
|
|
||||||
*/
|
*/
|
||||||
const getValidationError = ( property ) =>
|
const getValidationError = ( property ) => validationErrors[ property ];
|
||||||
validationErrors[ property ] || '';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears any validation error that exists in state for the given 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.
|
// all values must be a string.
|
||||||
newErrors = pickBy(
|
newErrors = pickBy(
|
||||||
newErrors,
|
newErrors,
|
||||||
( message ) => typeof message === 'string'
|
( { message } ) => typeof message === 'string'
|
||||||
);
|
);
|
||||||
if ( Object.values( newErrors ).length > 0 ) {
|
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
|
* Provides an id for the validation error that can be used to fill out
|
||||||
* aria-describedby attribute values.
|
* aria-describedby attribute values.
|
||||||
|
@ -98,6 +165,10 @@ export const ValidationContextProvider = ( { children } ) => {
|
||||||
clearValidationError,
|
clearValidationError,
|
||||||
clearAllValidationErrors,
|
clearAllValidationErrors,
|
||||||
getValidationErrorId,
|
getValidationErrorId,
|
||||||
|
hideValidationError,
|
||||||
|
showValidationError,
|
||||||
|
showAllValidationErrors,
|
||||||
|
hasValidationErrors,
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<ValidationContext.Provider value={ context }>
|
<ValidationContext.Provider value={ context }>
|
||||||
|
|
|
@ -58,7 +58,9 @@ export const useStoreCartCoupons = () => {
|
||||||
}
|
}
|
||||||
} )
|
} )
|
||||||
.catch( ( error ) => {
|
.catch( ( error ) => {
|
||||||
setValidationErrors( { coupon: error.message } );
|
setValidationErrors( {
|
||||||
|
coupon: { message: error.message, hidden: false },
|
||||||
|
} );
|
||||||
// Finished handling the coupon.
|
// Finished handling the coupon.
|
||||||
receiveApplyingCoupon( '' );
|
receiveApplyingCoupon( '' );
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -15,14 +15,14 @@ import {
|
||||||
PlaceOrderButton,
|
PlaceOrderButton,
|
||||||
ReturnToCartButton,
|
ReturnToCartButton,
|
||||||
} from '@woocommerce/base-components/cart-checkout';
|
} 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 ShippingRatesControl from '@woocommerce/base-components/shipping-rates-control';
|
||||||
import CheckboxControl from '@woocommerce/base-components/checkbox-control';
|
import CheckboxControl from '@woocommerce/base-components/checkbox-control';
|
||||||
import { getCurrencyFromPriceResponse } from '@woocommerce/base-utils';
|
import { getCurrencyFromPriceResponse } from '@woocommerce/base-utils';
|
||||||
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
|
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
|
||||||
import {
|
import {
|
||||||
CheckoutProvider,
|
CheckoutProvider,
|
||||||
ValidationContextProvider,
|
useValidationContext,
|
||||||
} from '@woocommerce/base-context';
|
} from '@woocommerce/base-context';
|
||||||
import {
|
import {
|
||||||
ExpressCheckoutFormControl,
|
ExpressCheckoutFormControl,
|
||||||
|
@ -37,6 +37,7 @@ import {
|
||||||
Main,
|
Main,
|
||||||
} from '@woocommerce/base-components/sidebar-layout';
|
} from '@woocommerce/base-components/sidebar-layout';
|
||||||
import { getSetting } from '@woocommerce/settings';
|
import { getSetting } from '@woocommerce/settings';
|
||||||
|
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -52,6 +53,7 @@ const Block = ( {
|
||||||
cartTotals = {},
|
cartTotals = {},
|
||||||
isEditor = false,
|
isEditor = false,
|
||||||
shippingRates = [],
|
shippingRates = [],
|
||||||
|
scrollToTop,
|
||||||
} ) => {
|
} ) => {
|
||||||
const [ selectedShippingRate, setSelectedShippingRate ] = useState( {} );
|
const [ selectedShippingRate, setSelectedShippingRate ] = useState( {} );
|
||||||
const [ contactFields, setContactFields ] = useState( {} );
|
const [ contactFields, setContactFields ] = useState( {} );
|
||||||
|
@ -60,6 +62,19 @@ const Block = ( {
|
||||||
const [ useShippingAsBilling, setUseShippingAsBilling ] = useState(
|
const [ useShippingAsBilling, setUseShippingAsBilling ] = useState(
|
||||||
attributes.useShippingAsBilling
|
attributes.useShippingAsBilling
|
||||||
);
|
);
|
||||||
|
const {
|
||||||
|
hasValidationErrors,
|
||||||
|
showAllValidationErrors,
|
||||||
|
} = useValidationContext();
|
||||||
|
|
||||||
|
const validateSubmit = () => {
|
||||||
|
if ( hasValidationErrors() ) {
|
||||||
|
showAllValidationErrors();
|
||||||
|
scrollToTop( { focusableSelector: 'input:invalid' } );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const renderShippingRatesControlOption = ( option ) => ( {
|
const renderShippingRatesControlOption = ( option ) => ( {
|
||||||
label: decodeEntities( option.name ),
|
label: decodeEntities( option.name ),
|
||||||
|
@ -100,266 +115,262 @@ const Block = ( {
|
||||||
} = useShippingRates();
|
} = useShippingRates();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ValidationContextProvider>
|
<CheckoutProvider isEditor={ isEditor }>
|
||||||
<CheckoutProvider isEditor={ isEditor }>
|
<SidebarLayout className="wc-block-checkout">
|
||||||
<SidebarLayout className="wc-block-checkout">
|
<Main>
|
||||||
<Main>
|
<ExpressCheckoutFormControl />
|
||||||
<ExpressCheckoutFormControl />
|
<CheckoutForm>
|
||||||
<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
|
<FormStep
|
||||||
id="contact-fields"
|
id="shipping-fields"
|
||||||
className="wc-block-checkout__contact-fields"
|
className="wc-block-checkout__shipping-fields"
|
||||||
title={ __(
|
title={ __(
|
||||||
'Contact information',
|
'Shipping address',
|
||||||
'woo-gutenberg-products-block'
|
'woo-gutenberg-products-block'
|
||||||
) }
|
) }
|
||||||
description={ __(
|
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'
|
'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
|
<AddressForm
|
||||||
type="email"
|
onChange={ setShippingFields }
|
||||||
label={ __(
|
values={ shippingFields }
|
||||||
'Email address',
|
fields={ Object.keys( addressFields ) }
|
||||||
'woo-gutenberg-products-block'
|
fieldConfig={ addressFields }
|
||||||
) }
|
|
||||||
value={ contactFields.email }
|
|
||||||
autoComplete="email"
|
|
||||||
onChange={ ( newValue ) =>
|
|
||||||
setContactFields( {
|
|
||||||
...contactFields,
|
|
||||||
email: newValue,
|
|
||||||
} )
|
|
||||||
}
|
|
||||||
required={ true }
|
|
||||||
/>
|
/>
|
||||||
<CheckboxControl
|
{ attributes.showPhoneField && (
|
||||||
className="wc-block-checkout__keep-updated"
|
<ValidatedTextInput
|
||||||
label={ __(
|
type="tel"
|
||||||
'Keep me up to date on news and exclusive offers',
|
label={
|
||||||
'woo-gutenberg-products-block'
|
attributes.requirePhoneField
|
||||||
) }
|
? __(
|
||||||
checked={ contactFields.keepUpdated }
|
'Phone',
|
||||||
onChange={ () =>
|
'woo-gutenberg-products-block'
|
||||||
setContactFields( {
|
)
|
||||||
...contactFields,
|
: __(
|
||||||
keepUpdated: ! contactFields.keepUpdated,
|
'Phone (optional)',
|
||||||
} )
|
'woo-gutenberg-products-block'
|
||||||
}
|
)
|
||||||
/>
|
|
||||||
</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 )
|
|
||||||
}
|
}
|
||||||
/>
|
value={ shippingFields.phone }
|
||||||
</FormStep>
|
autoComplete="tel"
|
||||||
) }
|
onChange={ ( newValue ) =>
|
||||||
{ showBillingFields && (
|
setShippingFields( {
|
||||||
<FormStep
|
...shippingFields,
|
||||||
id="billing-fields"
|
phone: newValue,
|
||||||
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,
|
|
||||||
} )
|
} )
|
||||||
}
|
}
|
||||||
|
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
|
<CheckboxControl
|
||||||
className="wc-block-checkout__save-card-info"
|
className="wc-block-checkout__use-address-for-billing"
|
||||||
label={ __(
|
label={ __(
|
||||||
'Save payment information to my account for future purchases.',
|
'Use same address for billing',
|
||||||
'woo-gutenberg-products-block'
|
'woo-gutenberg-products-block'
|
||||||
) }
|
) }
|
||||||
checked={ shouldSavePayment }
|
checked={ useShippingAddressAsBilling }
|
||||||
onChange={ () =>
|
onChange={ ( isChecked ) =>
|
||||||
setShouldSavePayment(
|
setUseShippingAsBilling( isChecked )
|
||||||
! shouldSavePayment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FormStep>
|
</FormStep>
|
||||||
<div className="wc-block-checkout__actions">
|
) }
|
||||||
{ attributes.showReturnToCart && (
|
{ showBillingFields && (
|
||||||
<ReturnToCartButton
|
<FormStep
|
||||||
link={ getSetting(
|
id="billing-fields"
|
||||||
'page-' + attributes?.cartPageId,
|
className="wc-block-checkout__billing-fields"
|
||||||
false
|
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 />
|
<CheckboxControl
|
||||||
</div>
|
className="wc-block-checkout__add-note"
|
||||||
{ attributes.showPolicyLinks && <Policies /> }
|
label="Add order notes?"
|
||||||
</CheckoutForm>
|
checked={ selectedShippingRate.orderNote }
|
||||||
</Main>
|
onChange={ () =>
|
||||||
<Sidebar className="wc-block-checkout__sidebar">
|
setSelectedShippingRate( {
|
||||||
<CheckoutSidebar
|
...selectedShippingRate,
|
||||||
cartCoupons={ cartCoupons }
|
orderNote: ! selectedShippingRate.orderNote,
|
||||||
cartItems={ cartItems }
|
} )
|
||||||
cartTotals={ cartTotals }
|
}
|
||||||
/>
|
/>
|
||||||
</Sidebar>
|
</FormStep>
|
||||||
</SidebarLayout>
|
) }
|
||||||
</CheckoutProvider>
|
<FormStep
|
||||||
</ValidationContextProvider>
|
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,
|
withStoreCartApiHydration,
|
||||||
} from '@woocommerce/block-hocs';
|
} from '@woocommerce/block-hocs';
|
||||||
import { useStoreCart } from '@woocommerce/base-hooks';
|
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 BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
||||||
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/block-settings';
|
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/block-settings';
|
||||||
import { __experimentalCreateInterpolateElement } from 'wordpress-element';
|
import { __experimentalCreateInterpolateElement } from 'wordpress-element';
|
||||||
|
@ -59,13 +62,15 @@ const CheckoutFrontend = ( props ) => {
|
||||||
showErrorMessage={ CURRENT_USER_IS_ADMIN }
|
showErrorMessage={ CURRENT_USER_IS_ADMIN }
|
||||||
>
|
>
|
||||||
<StoreNoticesProvider context="wc/checkout">
|
<StoreNoticesProvider context="wc/checkout">
|
||||||
<Block
|
<ValidationContextProvider>
|
||||||
{ ...props }
|
<Block
|
||||||
cartCoupons={ cartCoupons }
|
{ ...props }
|
||||||
cartItems={ cartItems }
|
cartCoupons={ cartCoupons }
|
||||||
cartTotals={ cartTotals }
|
cartItems={ cartItems }
|
||||||
shippingRates={ shippingRates }
|
cartTotals={ cartTotals }
|
||||||
/>
|
shippingRates={ shippingRates }
|
||||||
|
/>
|
||||||
|
</ValidationContextProvider>
|
||||||
</StoreNoticesProvider>
|
</StoreNoticesProvider>
|
||||||
</BlockErrorBoundary>
|
</BlockErrorBoundary>
|
||||||
) }
|
) }
|
||||||
|
|
|
@ -218,10 +218,9 @@
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} ValidationContext
|
* @typedef {Object} ValidationContext
|
||||||
*
|
*
|
||||||
* @property {function(string):string} getValidationError Return validation error or empty
|
* @property {function(string):Object} getValidationError Return validation error for the
|
||||||
* string if it doesn't exist for the
|
|
||||||
* given property.
|
* 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
|
* error messages as strings and adds
|
||||||
* to the validation error state.
|
* to the validation error state.
|
||||||
* @property {function(string)} clearValidationError Clears a validation error for the
|
* @property {function(string)} clearValidationError Clears a validation error for the
|
||||||
|
@ -231,6 +230,14 @@
|
||||||
* @property {function(string)} getValidationErrorId Returns the css id for the
|
* @property {function(string)} getValidationErrorId Returns the css id for the
|
||||||
* validation error using the given
|
* validation error using the given
|
||||||
* inputId string.
|
* 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 {};
|
export {};
|
||||||
|
|
Loading…
Reference in New Issue