Add validation context provider and implement validation for shipping country and coupons. (https://github.com/woocommerce/woocommerce-blocks/pull/1972)
* add errormessage handling to countryinput (along with storybook) * add types for react * Add validation context and implement * implement validation context for country field validation * tweak ValidationInputError so that it can receive property name for getting error from * improve storybook webpack config to pull from tsconfig.json * update storybook story to cover changes with context * Wrap Checkout Provider with Validation Context Provider * add screen-reader-text style to storybook * add styles for input error validation to text input * improve styling for ValidationInputError component * add validation error handling to TotalsCouponCode component And story * make sure errors are cleared on successful receive/remove item * dispatch loading cancellation on catching errors This is needed because loading would be cancelled before the error is thrown so any error handling after the thrown error will not be able to rely on loading. * implement validation setting for coupon errors * add error color to labels on inputs too * fix borders back and force border color * remove extra structure and improve validation error with alignment for coupon code * add aria-describedby for text inputs * add back in validation context provider to fix rebase issue * rework validation so it works for both checkout and cart * Some styling tweaks * more style fixes * remove unnecessary method * make sure new function is included in context defaults * package.lock update? seems harmless so rolling with it.
This commit is contained in:
parent
bd6c7f657c
commit
ad4c981793
|
@ -11,6 +11,9 @@ import {
|
|||
BillingStateInput,
|
||||
ShippingStateInput,
|
||||
} from '@woocommerce/base-components/state-input';
|
||||
import { useValidationContext } from '@woocommerce/base-context';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -18,6 +21,29 @@ import {
|
|||
import defaultAddressFields from './default-address-fields';
|
||||
import countryAddressFields from './country-address-fields';
|
||||
|
||||
const validateCountry = (
|
||||
values,
|
||||
setValidationErrors,
|
||||
clearValidationError,
|
||||
hasValidationError
|
||||
) => {
|
||||
if (
|
||||
! hasValidationError &&
|
||||
! values.country &&
|
||||
Object.values( values ).some( ( value ) => value !== '' )
|
||||
) {
|
||||
setValidationErrors( {
|
||||
country: __(
|
||||
'Please select a country to calculate rates.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
} );
|
||||
}
|
||||
if ( hasValidationError && values.country ) {
|
||||
clearValidationError( 'country' );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checkout address form.
|
||||
*/
|
||||
|
@ -28,6 +54,11 @@ const AddressForm = ( {
|
|||
type = 'shipping',
|
||||
values,
|
||||
} ) => {
|
||||
const {
|
||||
getValidationError,
|
||||
setValidationErrors,
|
||||
clearValidationError,
|
||||
} = useValidationContext();
|
||||
const countryLocale = countryAddressFields[ values.country ] || {};
|
||||
const addressFields = fields.map( ( field ) => ( {
|
||||
key: field,
|
||||
|
@ -38,7 +69,20 @@ const AddressForm = ( {
|
|||
const sortedAddressFields = addressFields.sort(
|
||||
( a, b ) => a.index - b.index
|
||||
);
|
||||
|
||||
const countryValidationError = getValidationError( 'country' );
|
||||
useEffect( () => {
|
||||
validateCountry(
|
||||
values,
|
||||
setValidationErrors,
|
||||
clearValidationError,
|
||||
!! countryValidationError
|
||||
);
|
||||
}, [
|
||||
values,
|
||||
countryValidationError,
|
||||
setValidationErrors,
|
||||
clearValidationError,
|
||||
] );
|
||||
return (
|
||||
<div className="wc-block-address-form">
|
||||
{ sortedAddressFields.map( ( field ) => {
|
||||
|
|
|
@ -3,11 +3,14 @@
|
|||
*/
|
||||
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';
|
||||
|
||||
const CountryInput = ( {
|
||||
className,
|
||||
|
@ -22,16 +25,18 @@ const CountryInput = ( {
|
|||
key,
|
||||
name: decodeEntities( countries[ key ] ),
|
||||
} ) );
|
||||
const { getValidationError } = useValidationContext();
|
||||
const errorMessage = getValidationError( 'country' );
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={ classnames( className, 'wc-block-country-input' ) }>
|
||||
<Select
|
||||
className={ className }
|
||||
label={ label }
|
||||
onChange={ onChange }
|
||||
options={ options }
|
||||
value={ options.find( ( option ) => option.key === value ) }
|
||||
required={ required }
|
||||
hasError={ !! errorMessage }
|
||||
/>
|
||||
{ autoComplete !== 'off' && (
|
||||
<input
|
||||
|
@ -56,7 +61,8 @@ const CountryInput = ( {
|
|||
tabIndex={ -1 }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
<ValidationInputError errorMessage={ errorMessage } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,251 @@
|
|||
export const countries = {
|
||||
AX: 'Åland Islands',
|
||||
AF: 'Afghanistan',
|
||||
AL: 'Albania',
|
||||
DZ: 'Algeria',
|
||||
AS: 'American Samoa',
|
||||
AD: 'Andorra',
|
||||
AO: 'Angola',
|
||||
AI: 'Anguilla',
|
||||
AQ: 'Antarctica',
|
||||
AG: 'Antigua and Barbuda',
|
||||
AR: 'Argentina',
|
||||
AM: 'Armenia',
|
||||
AW: 'Aruba',
|
||||
AU: 'Australia',
|
||||
AT: 'Austria',
|
||||
AZ: 'Azerbaijan',
|
||||
BS: 'Bahamas',
|
||||
BH: 'Bahrain',
|
||||
BD: 'Bangladesh',
|
||||
BB: 'Barbados',
|
||||
BY: 'Belarus',
|
||||
PW: 'Belau',
|
||||
BE: 'Belgium',
|
||||
BZ: 'Belize',
|
||||
BJ: 'Benin',
|
||||
BM: 'Bermuda',
|
||||
BT: 'Bhutan',
|
||||
BO: 'Bolivia',
|
||||
BQ: 'Bonaire, Saint Eustatius and Saba',
|
||||
BA: 'Bosnia and Herzegovina',
|
||||
BW: 'Botswana',
|
||||
BV: 'Bouvet Island',
|
||||
BR: 'Brazil',
|
||||
IO: 'British Indian Ocean Territory',
|
||||
BN: 'Brunei',
|
||||
BG: 'Bulgaria',
|
||||
BF: 'Burkina Faso',
|
||||
BI: 'Burundi',
|
||||
KH: 'Cambodia',
|
||||
CM: 'Cameroon',
|
||||
CA: 'Canada',
|
||||
CV: 'Cape Verde',
|
||||
KY: 'Cayman Islands',
|
||||
CF: 'Central African Republic',
|
||||
TD: 'Chad',
|
||||
CL: 'Chile',
|
||||
CN: 'China',
|
||||
CX: 'Christmas Island',
|
||||
CC: 'Cocos (Keeling) Islands',
|
||||
CO: 'Colombia',
|
||||
KM: 'Comoros',
|
||||
CG: 'Congo (Brazzaville)',
|
||||
CD: 'Congo (Kinshasa)',
|
||||
CK: 'Cook Islands',
|
||||
CR: 'Costa Rica',
|
||||
HR: 'Croatia',
|
||||
CU: 'Cuba',
|
||||
CW: 'Curaçao',
|
||||
CY: 'Cyprus',
|
||||
CZ: 'Czech Republic',
|
||||
DK: 'Denmark',
|
||||
DJ: 'Djibouti',
|
||||
DM: 'Dominica',
|
||||
DO: 'Dominican Republic',
|
||||
EC: 'Ecuador',
|
||||
EG: 'Egypt',
|
||||
SV: 'El Salvador',
|
||||
GQ: 'Equatorial Guinea',
|
||||
ER: 'Eritrea',
|
||||
EE: 'Estonia',
|
||||
ET: 'Ethiopia',
|
||||
FK: 'Falkland Islands',
|
||||
FO: 'Faroe Islands',
|
||||
FJ: 'Fiji',
|
||||
FI: 'Finland',
|
||||
FR: 'France',
|
||||
GF: 'French Guiana',
|
||||
PF: 'French Polynesia',
|
||||
TF: 'French Southern Territories',
|
||||
GA: 'Gabon',
|
||||
GM: 'Gambia',
|
||||
GE: 'Georgia',
|
||||
DE: 'Germany',
|
||||
GH: 'Ghana',
|
||||
GI: 'Gibraltar',
|
||||
GR: 'Greece',
|
||||
GL: 'Greenland',
|
||||
GD: 'Grenada',
|
||||
GP: 'Guadeloupe',
|
||||
GU: 'Guam',
|
||||
GT: 'Guatemala',
|
||||
GG: 'Guernsey',
|
||||
GN: 'Guinea',
|
||||
GW: 'Guinea-Bissau',
|
||||
GY: 'Guyana',
|
||||
HT: 'Haiti',
|
||||
HM: 'Heard Island and McDonald Islands',
|
||||
HN: 'Honduras',
|
||||
HK: 'Hong Kong',
|
||||
HU: 'Hungary',
|
||||
IS: 'Iceland',
|
||||
IN: 'India',
|
||||
ID: 'Indonesia',
|
||||
IR: 'Iran',
|
||||
IQ: 'Iraq',
|
||||
IE: 'Ireland',
|
||||
IM: 'Isle of Man',
|
||||
IL: 'Israel',
|
||||
IT: 'Italy',
|
||||
CI: 'Ivory Coast',
|
||||
JM: 'Jamaica',
|
||||
JP: 'Japan',
|
||||
JE: 'Jersey',
|
||||
JO: 'Jordan',
|
||||
KZ: 'Kazakhstan',
|
||||
KE: 'Kenya',
|
||||
KI: 'Kiribati',
|
||||
KW: 'Kuwait',
|
||||
KG: 'Kyrgyzstan',
|
||||
LA: 'Laos',
|
||||
LV: 'Latvia',
|
||||
LB: 'Lebanon',
|
||||
LS: 'Lesotho',
|
||||
LR: 'Liberia',
|
||||
LY: 'Libya',
|
||||
LI: 'Liechtenstein',
|
||||
LT: 'Lithuania',
|
||||
LU: 'Luxembourg',
|
||||
MO: 'Macao',
|
||||
MG: 'Madagascar',
|
||||
MW: 'Malawi',
|
||||
MY: 'Malaysia',
|
||||
MV: 'Maldives',
|
||||
ML: 'Mali',
|
||||
MT: 'Malta',
|
||||
MH: 'Marshall Islands',
|
||||
MQ: 'Martinique',
|
||||
MR: 'Mauritania',
|
||||
MU: 'Mauritius',
|
||||
YT: 'Mayotte',
|
||||
MX: 'Mexico',
|
||||
FM: 'Micronesia',
|
||||
MD: 'Moldova',
|
||||
MC: 'Monaco',
|
||||
MN: 'Mongolia',
|
||||
ME: 'Montenegro',
|
||||
MS: 'Montserrat',
|
||||
MA: 'Morocco',
|
||||
MZ: 'Mozambique',
|
||||
MM: 'Myanmar',
|
||||
NA: 'Namibia',
|
||||
NR: 'Nauru',
|
||||
NP: 'Nepal',
|
||||
NL: 'Netherlands',
|
||||
NC: 'New Caledonia',
|
||||
NZ: 'New Zealand',
|
||||
NI: 'Nicaragua',
|
||||
NE: 'Niger',
|
||||
NG: 'Nigeria',
|
||||
NU: 'Niue',
|
||||
NF: 'Norfolk Island',
|
||||
KP: 'North Korea',
|
||||
MK: 'North Macedonia',
|
||||
MP: 'Northern Mariana Islands',
|
||||
NO: 'Norway',
|
||||
OM: 'Oman',
|
||||
PK: 'Pakistan',
|
||||
PS: 'Palestinian Territory',
|
||||
PA: 'Panama',
|
||||
PG: 'Papua New Guinea',
|
||||
PY: 'Paraguay',
|
||||
PE: 'Peru',
|
||||
PH: 'Philippines',
|
||||
PN: 'Pitcairn',
|
||||
PL: 'Poland',
|
||||
PT: 'Portugal',
|
||||
PR: 'Puerto Rico',
|
||||
QA: 'Qatar',
|
||||
RE: 'Reunion',
|
||||
RO: 'Romania',
|
||||
RU: 'Russia',
|
||||
RW: 'Rwanda',
|
||||
ST: 'São Tomé and Príncipe',
|
||||
BL: 'Saint Barthélemy',
|
||||
SH: 'Saint Helena',
|
||||
KN: 'Saint Kitts and Nevis',
|
||||
LC: 'Saint Lucia',
|
||||
SX: 'Saint Martin (Dutch part)',
|
||||
MF: 'Saint Martin (French part)',
|
||||
PM: 'Saint Pierre and Miquelon',
|
||||
VC: 'Saint Vincent and the Grenadines',
|
||||
WS: 'Samoa',
|
||||
SM: 'San Marino',
|
||||
SA: 'Saudi Arabia',
|
||||
SN: 'Senegal',
|
||||
RS: 'Serbia',
|
||||
SC: 'Seychelles',
|
||||
SL: 'Sierra Leone',
|
||||
SG: 'Singapore',
|
||||
SK: 'Slovakia',
|
||||
SI: 'Slovenia',
|
||||
SB: 'Solomon Islands',
|
||||
SO: 'Somalia',
|
||||
ZA: 'South Africa',
|
||||
GS: 'South Georgia/Sandwich Islands',
|
||||
KR: 'South Korea',
|
||||
SS: 'South Sudan',
|
||||
ES: 'Spain',
|
||||
LK: 'Sri Lanka',
|
||||
SD: 'Sudan',
|
||||
SR: 'Suriname',
|
||||
SJ: 'Svalbard and Jan Mayen',
|
||||
SZ: 'Swaziland',
|
||||
SE: 'Sweden',
|
||||
CH: 'Switzerland',
|
||||
SY: 'Syria',
|
||||
TW: 'Taiwan',
|
||||
TJ: 'Tajikistan',
|
||||
TZ: 'Tanzania',
|
||||
TH: 'Thailand',
|
||||
TL: 'Timor-Leste',
|
||||
TG: 'Togo',
|
||||
TK: 'Tokelau',
|
||||
TO: 'Tonga',
|
||||
TT: 'Trinidad and Tobago',
|
||||
TN: 'Tunisia',
|
||||
TR: 'Turkey',
|
||||
TM: 'Turkmenistan',
|
||||
TC: 'Turks and Caicos Islands',
|
||||
TV: 'Tuvalu',
|
||||
UG: 'Uganda',
|
||||
UA: 'Ukraine',
|
||||
AE: 'United Arab Emirates',
|
||||
GB: 'United Kingdom (UK)',
|
||||
US: 'United States (US)',
|
||||
UM: 'United States (US) Minor Outlying Islands',
|
||||
UY: 'Uruguay',
|
||||
UZ: 'Uzbekistan',
|
||||
VU: 'Vanuatu',
|
||||
VA: 'Vatican',
|
||||
VE: 'Venezuela',
|
||||
VN: 'Vietnam',
|
||||
VG: 'Virgin Islands (British)',
|
||||
VI: 'Virgin Islands (US)',
|
||||
WF: 'Wallis and Futuna',
|
||||
EH: 'Western Sahara',
|
||||
YE: 'Yemen',
|
||||
ZM: 'Zambia',
|
||||
ZW: 'Zimbabwe',
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { text } from '@storybook/addon-knobs';
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import {
|
||||
ValidationContextProvider,
|
||||
useValidationContext,
|
||||
} from '@woocommerce/base-context';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CountryInput } from '../';
|
||||
import { countries as exampleCountries } from './countries-filler';
|
||||
|
||||
export default {
|
||||
title: 'WooCommerce Blocks/@base-components/CountryInput',
|
||||
component: CountryInput,
|
||||
};
|
||||
|
||||
const StoryComponent = ( { label, errorMessage } ) => {
|
||||
const [ selectedCountry, selectCountry ] = useState();
|
||||
const {
|
||||
setValidationErrors,
|
||||
clearValidationError,
|
||||
} = useValidationContext();
|
||||
useEffect( () => {
|
||||
setValidationErrors( { country: errorMessage } );
|
||||
}, [ errorMessage ] );
|
||||
const updateCountry = ( country ) => {
|
||||
clearValidationError( 'country' );
|
||||
selectCountry( country );
|
||||
};
|
||||
return (
|
||||
<CountryInput
|
||||
countries={ exampleCountries }
|
||||
label={ label }
|
||||
value={ selectedCountry }
|
||||
onChange={ updateCountry }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const label = text( 'Input Label', 'Countries:' );
|
||||
const errorMessage = text( 'Error Message', '' );
|
||||
return (
|
||||
<ValidationContextProvider>
|
||||
<StoryComponent label={ label } errorMessage={ errorMessage } />
|
||||
</ValidationContextProvider>
|
||||
);
|
||||
};
|
|
@ -10,11 +10,12 @@ import { CustomSelectControl } from 'wordpress-components';
|
|||
*/
|
||||
import './style.scss';
|
||||
|
||||
const Select = ( { className, label, onChange, options, value } ) => {
|
||||
const Select = ( { className, label, onChange, options, value, hasError } ) => {
|
||||
return (
|
||||
<CustomSelectControl
|
||||
className={ classnames( 'wc-block-select', className, {
|
||||
'is-active': value,
|
||||
'has-error': hasError,
|
||||
} ) }
|
||||
label={ label }
|
||||
onChange={ ( { selectedItem } ) => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.wc-block-select {
|
||||
height: 48px;
|
||||
position: relative;
|
||||
margin-top: $gap;
|
||||
margin-bottom: $gap-large;
|
||||
|
||||
label {
|
||||
position: absolute;
|
||||
|
@ -26,6 +26,16 @@
|
|||
transform: translateY(#{$gap-smallest}) scale(0.75);
|
||||
}
|
||||
|
||||
&.has-error {
|
||||
.components-custom-select-control__button {
|
||||
border-color: $error-red;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-error label {
|
||||
color: $error-red;
|
||||
}
|
||||
|
||||
.components-custom-select-control__button {
|
||||
background-color: #fff;
|
||||
box-shadow: none;
|
||||
|
|
|
@ -6,6 +6,7 @@ import { __ } from '@wordpress/i18n';
|
|||
import { Button } from '@woocommerce/base-components/cart-checkout';
|
||||
import { useState } from '@wordpress/element';
|
||||
import isShallowEqual from '@wordpress/is-shallow-equal';
|
||||
import { useValidationContext } from '@woocommerce/base-context';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -19,6 +20,7 @@ const ShippingCalculatorAddress = ( {
|
|||
addressFields,
|
||||
} ) => {
|
||||
const [ address, setAddress ] = useState( initialAddress );
|
||||
const { getValidationError } = useValidationContext();
|
||||
|
||||
return (
|
||||
<form className="wc-block-shipping-calculator-address">
|
||||
|
@ -29,7 +31,10 @@ const ShippingCalculatorAddress = ( {
|
|||
/>
|
||||
<Button
|
||||
className="wc-block-shipping-calculator-address__button"
|
||||
disabled={ isShallowEqual( address, initialAddress ) }
|
||||
disabled={
|
||||
isShallowEqual( address, initialAddress ) ||
|
||||
getValidationError( 'country' )
|
||||
}
|
||||
onClick={ ( e ) => {
|
||||
e.preventDefault();
|
||||
return onUpdate( address );
|
||||
|
|
|
@ -6,3 +6,7 @@
|
|||
margin-top: $gap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wc-block-cart__shipping-calculator {
|
||||
padding-top: $gap-large;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ const TextInput = ( {
|
|||
id,
|
||||
type = 'text',
|
||||
ariaLabel,
|
||||
ariaDescribedBy,
|
||||
label,
|
||||
screenReaderLabel,
|
||||
disabled,
|
||||
|
@ -48,7 +49,9 @@ const TextInput = ( {
|
|||
aria-label={ ariaLabel || label }
|
||||
disabled={ disabled }
|
||||
aria-describedby={
|
||||
!! help ? textInputId + '__help' : undefined
|
||||
!! help && ! ariaDescribedBy
|
||||
? textInputId + '__help'
|
||||
: ariaDescribedBy
|
||||
}
|
||||
required={ required }
|
||||
/>
|
||||
|
@ -78,6 +81,7 @@ TextInput.propTypes = {
|
|||
id: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
ariaLabel: PropTypes.string,
|
||||
ariaDescribedBy: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
screenReaderLabel: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
|
|
|
@ -1,7 +1,23 @@
|
|||
.wc-block-text-input {
|
||||
position: relative;
|
||||
margin-top: $gap;
|
||||
margin-bottom: $gap-large;
|
||||
white-space: nowrap;
|
||||
|
||||
&.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;
|
||||
&:focus {
|
||||
outline-color: $error-red;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-error label {
|
||||
color: $error-red;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-text-input label {
|
||||
|
|
|
@ -7,8 +7,11 @@ import { PanelBody, PanelRow } from 'wordpress-components';
|
|||
import { Button } from '@woocommerce/base-components/cart-checkout';
|
||||
import TextInput 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';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -24,15 +27,23 @@ const TotalsCouponCodeInput = ( {
|
|||
} ) => {
|
||||
const [ couponValue, setCouponValue ] = useState( '' );
|
||||
const currentIsLoading = useRef( false );
|
||||
const {
|
||||
getValidationError,
|
||||
clearValidationError,
|
||||
getValidationErrorId,
|
||||
} = useValidationContext();
|
||||
const validationMessage = getValidationError( 'coupon' );
|
||||
|
||||
useEffect( () => {
|
||||
if ( currentIsLoading.current !== isLoading ) {
|
||||
if ( ! isLoading && couponValue ) {
|
||||
if ( ! isLoading && couponValue && ! validationMessage ) {
|
||||
setCouponValue( '' );
|
||||
}
|
||||
currentIsLoading.current = isLoading;
|
||||
}
|
||||
}, [ isLoading, couponValue ] );
|
||||
}, [ isLoading, couponValue, validationMessage ] );
|
||||
|
||||
const textInputId = `wc-block-coupon-code__input-${ instanceId }`;
|
||||
|
||||
return (
|
||||
<PanelBody
|
||||
|
@ -47,7 +58,7 @@ const TotalsCouponCodeInput = ( {
|
|||
'Introduce Coupon Code',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
htmlFor={ `wc-block-coupon-code__input-${ instanceId }` }
|
||||
htmlFor={ textInputId }
|
||||
/>
|
||||
}
|
||||
initialOpen={ initialOpen }
|
||||
|
@ -63,16 +74,25 @@ const TotalsCouponCodeInput = ( {
|
|||
<PanelRow className="wc-block-coupon-code__row">
|
||||
<form className="wc-block-coupon-code__form">
|
||||
<TextInput
|
||||
id={ `wc-block-coupon-code__input-${ instanceId }` }
|
||||
className="wc-block-coupon-code__input"
|
||||
id={ textInputId }
|
||||
className={ classnames(
|
||||
'wc-block-coupon-code__input',
|
||||
{
|
||||
'has-error': !! validationMessage,
|
||||
}
|
||||
) }
|
||||
label={ __(
|
||||
'Enter code',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
value={ couponValue }
|
||||
onChange={ ( newCouponValue ) =>
|
||||
setCouponValue( newCouponValue )
|
||||
}
|
||||
ariaDescribedBy={ getValidationErrorId(
|
||||
textInputId
|
||||
) }
|
||||
onChange={ ( newCouponValue ) => {
|
||||
setCouponValue( newCouponValue );
|
||||
clearValidationError( 'coupon' );
|
||||
} }
|
||||
/>
|
||||
<Button
|
||||
className="wc-block-coupon-code__button"
|
||||
|
@ -87,6 +107,10 @@ const TotalsCouponCodeInput = ( {
|
|||
</Button>
|
||||
</form>
|
||||
</PanelRow>
|
||||
<ValidationInputError
|
||||
errorMessage={ validationMessage }
|
||||
elementId={ textInputId }
|
||||
/>
|
||||
</LoadingMask>
|
||||
</PanelBody>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { text, boolean } from '@storybook/addon-knobs';
|
||||
import {
|
||||
useValidationContext,
|
||||
ValidationContextProvider,
|
||||
} from '@woocommerce/base-context';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import CouponInput from '../';
|
||||
|
||||
export default {
|
||||
title: 'WooCommerce Blocks/@base-components/CouponInput',
|
||||
component: CouponInput,
|
||||
};
|
||||
|
||||
const StoryComponent = ( { validCoupon, isLoading, invalidCouponText } ) => {
|
||||
const { setValidationErrors } = useValidationContext();
|
||||
const onSubmit = ( coupon ) => {
|
||||
if ( coupon !== validCoupon ) {
|
||||
setValidationErrors( { coupon: invalidCouponText } );
|
||||
}
|
||||
};
|
||||
return <CouponInput isLoading={ isLoading } onSubmit={ onSubmit } />;
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const validCoupon = text( 'A valid coupon code', 'validcoupon' );
|
||||
const invalidCouponText = text(
|
||||
'Error message for invalid code',
|
||||
'Invalid coupon code.'
|
||||
);
|
||||
const isLoading = boolean( 'Toggle isLoading state', false );
|
||||
return (
|
||||
<ValidationContextProvider>
|
||||
<StoryComponent
|
||||
validCoupon={ validCoupon }
|
||||
isLoading={ isLoading }
|
||||
invalidCouponText={ invalidCouponText }
|
||||
/>
|
||||
</ValidationContextProvider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './validation-input-error';
|
|
@ -0,0 +1,16 @@
|
|||
.wc-block-form-input-validation-error {
|
||||
color: $error-red;
|
||||
@include font-size(12);
|
||||
margin-top: -$gap-large;
|
||||
padding-top: $gap-smallest;
|
||||
display: block;
|
||||
|
||||
> p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-select + .wc-block-form-input-validation-error {
|
||||
margin-bottom: $gap-large;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useValidationContext } from '@woocommerce/base-context';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
export const ValidationInputError = ( {
|
||||
errorMessage = '',
|
||||
propertyName = '',
|
||||
elementId = '',
|
||||
} ) => {
|
||||
const { getValidationError, getValidationErrorId } = useValidationContext();
|
||||
if ( ! errorMessage && ! propertyName ) {
|
||||
return null;
|
||||
}
|
||||
errorMessage = errorMessage || getValidationError( propertyName );
|
||||
return (
|
||||
<div className="wc-block-form-input-validation-error" role="alert">
|
||||
<p id={ getValidationErrorId( elementId ) }>{ errorMessage }</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ValidationInputError.propTypes = {
|
||||
errorMessage: PropTypes.string,
|
||||
propertyName: PropTypes.string,
|
||||
elementId: PropTypes.string,
|
||||
};
|
|
@ -1,3 +1,4 @@
|
|||
export * from './payment-methods';
|
||||
export * from './shipping';
|
||||
export * from './checkout';
|
||||
export * from './validation';
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createContext, useContext, useState } from '@wordpress/element';
|
||||
import { omit, pickBy } from 'lodash';
|
||||
|
||||
/**
|
||||
* @typedef { import('@woocommerce/type-defs/contexts').ValidationContext } ValidationContext
|
||||
*/
|
||||
|
||||
const ValidationContext = createContext( {
|
||||
getValidationError: () => '',
|
||||
setValidationErrors: ( errors ) => void errors,
|
||||
clearValidationError: ( property ) => void property,
|
||||
clearAllValidationErrors: () => void null,
|
||||
getValidationErrorId: ( inputId ) => void inputId,
|
||||
} );
|
||||
|
||||
/**
|
||||
* @return {ValidationContext} The context values for the validation context.
|
||||
*/
|
||||
export const useValidationContext = () => {
|
||||
return useContext( ValidationContext );
|
||||
};
|
||||
|
||||
/**
|
||||
* Validation context provider
|
||||
*
|
||||
* Any children of this context will be exposed to validation state and helpers
|
||||
* for tracking validation.
|
||||
*/
|
||||
export const ValidationContextProvider = ( { children } ) => {
|
||||
const [ validationErrors, updateValidationErrors ] = useState( {} );
|
||||
|
||||
/**
|
||||
* This retrieves any validation error message that exists in state for the
|
||||
* given property name.
|
||||
*
|
||||
* @param {string} property The property the error message is for.
|
||||
*
|
||||
* @return {string} Either the error message for the given property or an
|
||||
* empty string.
|
||||
*/
|
||||
const getValidationError = ( property ) =>
|
||||
validationErrors[ property ] || '';
|
||||
|
||||
/**
|
||||
* Clears any validation error that exists in state for the given property
|
||||
* name.
|
||||
*
|
||||
* @param {string} property The name of the property to clear if exists in
|
||||
* validation error state.
|
||||
*/
|
||||
const clearValidationError = ( property ) => {
|
||||
if ( validationErrors[ property ] ) {
|
||||
updateValidationErrors( omit( validationErrors, [ property ] ) );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the entire validation error state.
|
||||
*/
|
||||
const clearAllValidationErrors = () => void updateValidationErrors( {} );
|
||||
|
||||
/**
|
||||
* Used to record new validation errors in the state.
|
||||
*
|
||||
* @param {Object} newErrors An object where keys are the property names the
|
||||
* validation error is for and values are the
|
||||
* validation error message displayed to the user.
|
||||
*/
|
||||
const setValidationErrors = ( newErrors ) => {
|
||||
// all values must be a string.
|
||||
newErrors = pickBy(
|
||||
newErrors,
|
||||
( message ) => typeof message === 'string'
|
||||
);
|
||||
if ( Object.values( newErrors ).length > 0 ) {
|
||||
updateValidationErrors( { ...validationErrors, ...newErrors } );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides an id for the validation error that can be used to fill out
|
||||
* aria-describedby attribute values.
|
||||
*
|
||||
* @param {string} inputId The input css id the validation error is related
|
||||
* to.
|
||||
* @return {string} The id to use for the validation error container.
|
||||
*/
|
||||
const getValidationErrorId = ( inputId ) => {
|
||||
return inputId ? `validate-error-${ inputId }` : '';
|
||||
};
|
||||
|
||||
const context = {
|
||||
getValidationError,
|
||||
setValidationErrors,
|
||||
clearValidationError,
|
||||
clearAllValidationErrors,
|
||||
getValidationErrorId,
|
||||
};
|
||||
return (
|
||||
<ValidationContext.Provider value={ context }>
|
||||
{ children }
|
||||
</ValidationContext.Provider>
|
||||
);
|
||||
};
|
|
@ -6,12 +6,13 @@
|
|||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import { useStoreNotices } from '@woocommerce/base-hooks';
|
||||
import { useValidationContext } from '@woocommerce/base-context';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useStoreCart } from './use-store-cart';
|
||||
import { useStoreNotices } from '../use-store-notices';
|
||||
|
||||
/**
|
||||
* This is a custom hook for loading the Store API /cart/coupons endpoint and an
|
||||
|
@ -24,13 +25,18 @@ import { useStoreCart } from './use-store-cart';
|
|||
export const useStoreCartCoupons = () => {
|
||||
const { cartCoupons, cartErrors, cartIsLoading } = useStoreCart();
|
||||
const { addErrorNotice, addSnackbarNotice } = useStoreNotices();
|
||||
const { setValidationErrors } = useValidationContext();
|
||||
|
||||
const results = useSelect(
|
||||
( select, { dispatch } ) => {
|
||||
const store = select( storeKey );
|
||||
const isApplyingCoupon = store.isApplyingCoupon();
|
||||
const isRemovingCoupon = store.isRemovingCoupon();
|
||||
const { applyCoupon, removeCoupon } = dispatch( storeKey );
|
||||
const {
|
||||
applyCoupon,
|
||||
removeCoupon,
|
||||
receiveApplyingCoupon,
|
||||
} = dispatch( storeKey );
|
||||
|
||||
const applyCouponWithNotices = ( couponCode ) => {
|
||||
applyCoupon( couponCode )
|
||||
|
@ -52,9 +58,9 @@ export const useStoreCartCoupons = () => {
|
|||
}
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
addErrorNotice( error.message, {
|
||||
id: 'coupon-form',
|
||||
} );
|
||||
setValidationErrors( { coupon: error.message } );
|
||||
// Finished handling the coupon.
|
||||
receiveApplyingCoupon( '' );
|
||||
} );
|
||||
};
|
||||
|
||||
|
@ -81,6 +87,8 @@ export const useStoreCartCoupons = () => {
|
|||
addErrorNotice( error.message, {
|
||||
id: 'coupon-form',
|
||||
} );
|
||||
// Finished handling the coupon.
|
||||
receiveApplyingCoupon( '' );
|
||||
} );
|
||||
};
|
||||
|
||||
|
|
|
@ -9,7 +9,10 @@ import { __ } from '@wordpress/i18n';
|
|||
import { useStoreCart } from '@woocommerce/base-hooks';
|
||||
import { RawHTML } from '@wordpress/element';
|
||||
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||
import { StoreNoticesProvider } from '@woocommerce/base-context';
|
||||
import {
|
||||
StoreNoticesProvider,
|
||||
ValidationContextProvider,
|
||||
} from '@woocommerce/base-context';
|
||||
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/block-settings';
|
||||
import { __experimentalCreateInterpolateElement } from 'wordpress-element';
|
||||
|
||||
|
@ -32,7 +35,9 @@ const Block = ( { emptyCart, attributes } ) => {
|
|||
<RawHTML>{ emptyCart }</RawHTML>
|
||||
) : (
|
||||
<LoadingMask showSpinner={ true } isLoading={ cartIsLoading }>
|
||||
<FullCart attributes={ attributes } />
|
||||
<ValidationContextProvider>
|
||||
<FullCart attributes={ attributes } />
|
||||
</ValidationContextProvider>
|
||||
</LoadingMask>
|
||||
) }
|
||||
</>
|
||||
|
|
|
@ -20,7 +20,10 @@ import ShippingRatesControl from '@woocommerce/base-components/shipping-rates-co
|
|||
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 } from '@woocommerce/base-context';
|
||||
import {
|
||||
CheckoutProvider,
|
||||
ValidationContextProvider,
|
||||
} from '@woocommerce/base-context';
|
||||
import {
|
||||
ExpressCheckoutFormControl,
|
||||
PaymentMethods,
|
||||
|
@ -97,261 +100,265 @@ const Block = ( {
|
|||
} = useShippingRates();
|
||||
|
||||
return (
|
||||
<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>
|
||||
) }
|
||||
>
|
||||
<TextInput
|
||||
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 && (
|
||||
<ValidationContextProvider>
|
||||
<CheckoutProvider isEditor={ isEditor }>
|
||||
<SidebarLayout className="wc-block-checkout">
|
||||
<Main>
|
||||
<ExpressCheckoutFormControl />
|
||||
<CheckoutForm>
|
||||
<FormStep
|
||||
id="shipping-fields"
|
||||
className="wc-block-checkout__shipping-fields"
|
||||
id="contact-fields"
|
||||
className="wc-block-checkout__contact-fields"
|
||||
title={ __(
|
||||
'Shipping address',
|
||||
'Contact information',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
description={ __(
|
||||
'Enter the physical address where you want us to deliver your order.',
|
||||
"We'll use this email to send you details and updates about 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
|
||||
}
|
||||
/>
|
||||
stepHeadingContent={ () => (
|
||||
<Fragment>
|
||||
{ __(
|
||||
'Already have an account? ',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
<a href="/wp-login.php">
|
||||
{ __(
|
||||
'Log in.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
</a>
|
||||
</Fragment>
|
||||
) }
|
||||
<CheckboxControl
|
||||
className="wc-block-checkout__use-address-for-billing"
|
||||
>
|
||||
<TextInput
|
||||
type="email"
|
||||
label={ __(
|
||||
'Use same address for billing',
|
||||
'Email address',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
checked={ useShippingAddressAsBilling }
|
||||
onChange={ ( isChecked ) =>
|
||||
setUseShippingAsBilling( isChecked )
|
||||
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>
|
||||
) }
|
||||
{ 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 &&
|
||||
( shippingRates.length === 0 && isEditor ? (
|
||||
<NoShipping />
|
||||
) : (
|
||||
{ SHIPPING_ENABLED && (
|
||||
<FormStep
|
||||
id="shipping-option"
|
||||
className="wc-block-checkout__shipping-option"
|
||||
id="shipping-fields"
|
||||
className="wc-block-checkout__shipping-fields"
|
||||
title={ __(
|
||||
'Shipping options',
|
||||
'Shipping address',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
description={ __(
|
||||
'Select your shipping method below.',
|
||||
'Enter the physical address where you want us to deliver your order.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
<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.',
|
||||
<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'
|
||||
) }
|
||||
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,
|
||||
} )
|
||||
checked={ useShippingAddressAsBilling }
|
||||
onChange={ ( isChecked ) =>
|
||||
setUseShippingAsBilling( isChecked )
|
||||
}
|
||||
/>
|
||||
</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'
|
||||
{ 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>
|
||||
) }
|
||||
>
|
||||
<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.',
|
||||
{ SHIPPING_ENABLED &&
|
||||
( shippingRates.length === 0 && isEditor ? (
|
||||
<NoShipping />
|
||||
) : (
|
||||
<FormStep
|
||||
id="shipping-option"
|
||||
className="wc-block-checkout__shipping-option"
|
||||
title={ __(
|
||||
'Shipping options',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
description={ __(
|
||||
'Select your shipping method below.',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
<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,
|
||||
} )
|
||||
}
|
||||
/>
|
||||
</FormStep>
|
||||
) ) }
|
||||
<FormStep
|
||||
id="payment-method"
|
||||
className="wc-block-checkout__payment-method"
|
||||
title={ __(
|
||||
'Payment method',
|
||||
'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
|
||||
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
|
||||
)
|
||||
}
|
||||
/>
|
||||
) }
|
||||
<PlaceOrderButton />
|
||||
</div>
|
||||
{ attributes.showPolicyLinks && <Policies /> }
|
||||
</CheckoutForm>
|
||||
</Main>
|
||||
<Sidebar className="wc-block-checkout__sidebar">
|
||||
<CheckoutSidebar
|
||||
cartCoupons={ cartCoupons }
|
||||
cartItems={ cartItems }
|
||||
cartTotals={ cartTotals }
|
||||
/>
|
||||
</Sidebar>
|
||||
</SidebarLayout>
|
||||
</CheckoutProvider>
|
||||
</FormStep>
|
||||
<div className="wc-block-checkout__actions">
|
||||
{ attributes.showReturnToCart && (
|
||||
<ReturnToCartButton
|
||||
link={ getSetting(
|
||||
'page-' + attributes?.cartPageId,
|
||||
false
|
||||
) }
|
||||
/>
|
||||
) }
|
||||
<PlaceOrderButton />
|
||||
</div>
|
||||
{ attributes.showPolicyLinks && <Policies /> }
|
||||
</CheckoutForm>
|
||||
</Main>
|
||||
<Sidebar className="wc-block-checkout__sidebar">
|
||||
<CheckoutSidebar
|
||||
cartCoupons={ cartCoupons }
|
||||
cartItems={ cartItems }
|
||||
cartTotals={ cartTotals }
|
||||
/>
|
||||
</Sidebar>
|
||||
</SidebarLayout>
|
||||
</CheckoutProvider>
|
||||
</ValidationContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -113,12 +113,20 @@
|
|||
}
|
||||
|
||||
.wc-block-text-input,
|
||||
.wc-block-country-input,
|
||||
.wc-block-select {
|
||||
float: left;
|
||||
margin-left: #{$gap-small / 2};
|
||||
margin-right: #{$gap-small / 2};
|
||||
position: relative;
|
||||
width: calc(50% - #{$gap-small});
|
||||
|
||||
.wc-block-select {
|
||||
float: none;
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-address-form__company,
|
||||
|
|
|
@ -98,8 +98,6 @@ export function* applyCoupon( couponCode ) {
|
|||
} catch ( error ) {
|
||||
// Store the error message in state.
|
||||
yield receiveError( error );
|
||||
// Finished handling the coupon.
|
||||
yield receiveApplyingCoupon( '' );
|
||||
// Re-throw the error.
|
||||
throw error;
|
||||
}
|
||||
|
|
|
@ -149,6 +149,7 @@ const reducer = (
|
|||
case types.RECEIVE_REMOVED_ITEM:
|
||||
state = {
|
||||
...state,
|
||||
errors: [],
|
||||
cartData: {
|
||||
...state.cartData,
|
||||
items: cartItemsReducer( state.cartData.items, action ),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { getSetting } from '../shared';
|
||||
|
||||
export const CURRENT_USER_IS_ADMIN = getSetting( 'currentUserIsAdmin', false );
|
||||
export const REVIEW_RATINGS_ENABLED = getSetting(
|
||||
|
|
|
@ -215,4 +215,22 @@
|
|||
* @property {number} currentPostId The post ID being edited.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ValidationContext
|
||||
*
|
||||
* @property {function(string):string} getValidationError Return validation error or empty
|
||||
* string if it doesn't exist for the
|
||||
* given property.
|
||||
* @property {function(Object<string>)} 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
|
||||
* given property name.
|
||||
* @property {function()} clearAllValidationErrors Clears all validation errors
|
||||
* currently in state.
|
||||
* @property {function(string)} getValidationErrorId Returns the css id for the
|
||||
* validation error using the given
|
||||
* inputId string.
|
||||
*/
|
||||
|
||||
export {};
|
||||
|
|
|
@ -28580,7 +28580,7 @@
|
|||
}
|
||||
},
|
||||
"woocommerce": {
|
||||
"version": "git+https://github.com/woocommerce/woocommerce.git#6a8d8b27d98fe3036f2240d14be2b7a5e4ecf10e",
|
||||
"version": "git+https://github.com/woocommerce/woocommerce.git#6f2b232fc7fa53417691a742a419147bb0134a5c",
|
||||
"from": "git+https://github.com/woocommerce/woocommerce.git",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
"@storybook/addons": "5.3.12",
|
||||
"@storybook/react": "5.3.12",
|
||||
"@types/jest": "25.1.4",
|
||||
"@types/react": "^16.9.23",
|
||||
"@wordpress/babel-preset-default": "4.10.0",
|
||||
"@wordpress/base-styles": "1.4.0",
|
||||
"@wordpress/blocks": "6.12.0",
|
||||
|
|
|
@ -7,3 +7,26 @@
|
|||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
// screen-reader-text applied in WP environment
|
||||
|
||||
/* Hide visually but not from screen readers */
|
||||
.screen-reader-text,
|
||||
.screen-reader-text span,
|
||||
.ui-helper-hidden-accessible {
|
||||
border: 0;
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
-webkit-clip-path: inset(50%);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
word-wrap: normal !important; /* many screen reader and browser combinations announce broken words as they would appear visually */
|
||||
}
|
||||
|
||||
.button .screen-reader-text {
|
||||
height: auto; /* Fixes a Safari+VoiceOver bug, see ticket #42006 */
|
||||
}
|
||||
|
|
|
@ -2,18 +2,31 @@
|
|||
* External dependencies
|
||||
*/
|
||||
const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' );
|
||||
const path = require( 'path' );
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
const { getAlias, getMainConfig } = require( '../bin/webpack-helpers.js' );
|
||||
const tsConfig = require( '../tsconfig.json' );
|
||||
|
||||
const aliases = Object.keys( tsConfig.compilerOptions.paths ).reduce(
|
||||
( acc, key ) => {
|
||||
const currentPath = tsConfig.compilerOptions.paths[ key ][ 0 ];
|
||||
acc[ key.replace( '/*', '' ) ] = path.resolve(
|
||||
__dirname,
|
||||
'../' + currentPath.replace( '/*', '/' )
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
module.exports = ( { config: storybookConfig } ) => {
|
||||
const wooBlocksConfig = getMainConfig( { alias: getAlias() } );
|
||||
|
||||
storybookConfig.resolve.alias = {
|
||||
...storybookConfig.resolve.alias,
|
||||
...wooBlocksConfig.resolve.alias,
|
||||
...aliases,
|
||||
};
|
||||
storybookConfig.module.rules.push(
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue