diff --git a/plugins/woocommerce-blocks/assets/js/base/components/address-form/index.js b/plugins/woocommerce-blocks/assets/js/base/components/address-form/index.js
index 504df967a0b..b3dbd57c836 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/address-form/index.js
+++ b/plugins/woocommerce-blocks/assets/js/base/components/address-form/index.js
@@ -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 (
{ 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 (
-
);
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/place-order-button/index.js b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/place-order-button/index.js
index 1031d846b36..2159592501a 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/place-order-button/index.js
+++ b/plugins/woocommerce-blocks/assets/js/base/components/cart-checkout/place-order-button/index.js
@@ -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 (
);
};
+PlaceOrderButton.propTypes = {
+ validateSubmit: PropTypes.func.isRequired,
+};
+
export default PlaceOrderButton;
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/country-input/country-input.js b/plugins/woocommerce-blocks/assets/js/base/components/country-input/country-input.js
index 3b1fddde95d..c7a95fc4d36 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/country-input/country-input.js
+++ b/plugins/woocommerce-blocks/assets/js/base/components/country-input/country-input.js
@@ -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 (
-
);
};
@@ -73,6 +75,8 @@ CountryInput.propTypes = {
label: PropTypes.string,
value: PropTypes.string,
autoComplete: PropTypes.string,
+ errorId: PropTypes.string,
+ errorMessage: PropTypes.string,
};
export default CountryInput;
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/select/index.js b/plugins/woocommerce-blocks/assets/js/base/components/select/index.js
index ca3c0257725..26935d3ba92 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/select/index.js
+++ b/plugins/woocommerce-blocks/assets/js/base/components/select/index.js
@@ -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 (
-
{
- onChange( selectedItem.key );
- } }
- options={ options }
- value={ value }
- />
+ >
+ {
+ onChange( selectedItem.key );
+ } }
+ options={ options }
+ value={ value }
+ />
+ { feedback }
+
);
};
@@ -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';
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/select/validated.js b/plugins/woocommerce-blocks/assets/js/base/components/select/validated.js
new file mode 100644
index 00000000000..4b9cf7f08e9
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/base/components/select/validated.js
@@ -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 (
+ }
+ 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 );
diff --git a/plugins/woocommerce-blocks/assets/js/base/components/shipping-calculator/address.js b/plugins/woocommerce-blocks/assets/js/base/components/shipping-calculator/address.js
index 64f87eecaad..79a079f3f61 100644
--- a/plugins/woocommerce-blocks/assets/js/base/components/shipping-calculator/address.js
+++ b/plugins/woocommerce-blocks/assets/js/base/components/shipping-calculator/address.js
@@ -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 (