* Make Product Price component accessible

* Render checkout form step number before the content instead of after

* Announce changes in the Shipping Rates Control results

* Render product name before product price in the Checkout sidebar

* Verify error element id exists before using it in describedBy

* Avoid reading 'Choose a shipping method' if no shipping methods are available

* Hide product image from screen readers if there is no alt text and make link not focusable

* Move Checkout block buttons below the sidebar

* Simplify selector

* Remove ternary to make code easier to understand

* Fix rebase issue

* Fix step number not visible for payment methods
This commit is contained in:
Albert Juhé Lluveras 2020-03-30 14:43:42 +02:00 committed by GitHub
parent fb3cae67e6
commit e6f6dc9915
12 changed files with 349 additions and 244 deletions

View File

@ -1,6 +1,7 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
import classNames from 'classnames';
import PropTypes from 'prop-types';
@ -11,20 +12,38 @@ import PropTypes from 'prop-types';
import './style.scss';
const ProductPrice = ( { className, currency, regularValue, value } ) => {
const isDiscounted =
Number.isFinite( regularValue ) && regularValue !== value;
return (
<>
{ Number.isFinite( regularValue ) && regularValue !== value && (
<FormattedMonetaryAmount
className={ classNames(
'wc-block-product-price--regular',
className
) }
currency={ currency }
value={ regularValue }
/>
{ isDiscounted && (
<>
<span className="screen-reader-text">
{ __(
'Previous price:',
'woo-gutenberg-products-block'
) }
</span>
<FormattedMonetaryAmount
className={ classNames(
'wc-block-product-price--regular',
className
) }
currency={ currency }
value={ regularValue }
/>
<span className="screen-reader-text">
{ __(
'Discounted price:',
'woo-gutenberg-products-block'
) }
</span>
</>
) }
<FormattedMonetaryAmount
className={ classNames( 'wc-block-product-price', className ) }
className={ classNames( 'wc-block-product-price', className, {
'is-discounted': isDiscounted,
} ) }
currency={ currency }
value={ value }
/>

View File

@ -1,12 +1,12 @@
.wc-block-product-price {
color: $black;
&.is-discounted {
margin-left: 0.5em;
}
}
.wc-block-product-price--regular {
color: $core-grey-dark-400;
text-decoration: line-through;
+ .wc-block-product-price {
margin-left: 0.5em;
}
}

View File

@ -11,10 +11,6 @@ $line-offset-from-circle-size: 8px;
padding: 0 $gap-larger $gap-larger $gap-larger;
background: none;
margin: 0;
&:last-child::before {
content: none;
}
}
.wc-block-checkout-step__heading {
@ -54,20 +50,7 @@ $line-offset-from-circle-size: 8px;
margin-bottom: $gap-large;
}
// because themes can register different background colors, we can't always
// relay on using white border to offest the step left line,
.wc-block-checkout-step::before {
content: "";
// 1 Circle size + offset of the first circle and next circle.
height: calc(100% - #{$circle-size + $line-offset-from-circle-size * 2});
width: 1px;
background-color: $gray-10;
position: absolute;
left: $circle-size/2;
top: $circle-size + $line-offset-from-circle-size;
}
.wc-block-checkout-step::after {
counter-increment: checkout-step;
content: counter(checkout-step);
position: absolute;
@ -83,3 +66,20 @@ $line-offset-from-circle-size: 8px;
border-radius: $circle-size / 2;
box-sizing: content-box;
}
// because themes can register different background colors, we can't always
// relay on using white border to offest the step left line,
.wc-block-checkout-step::after {
content: "";
// 1 Circle size + offset of the first circle and next circle.
height: calc(100% - #{$circle-size + $line-offset-from-circle-size * 2});
width: 1px;
background-color: $gray-10;
position: absolute;
left: $circle-size/2;
top: $circle-size + $line-offset-from-circle-size;
}
.wc-block-checkout-step:last-child::after {
content: none;
}

View File

@ -1,8 +1,10 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { __, _n, sprintf } from '@wordpress/i18n';
import { useEffect } from 'react';
import PropTypes from 'prop-types';
import { speak } from '@wordpress/a11y';
/**
* Internal dependencies
@ -19,6 +21,62 @@ const ShippingRatesControl = ( {
noResultsMessage,
renderOption,
} ) => {
useEffect( () => {
if ( shippingRatesLoading ) {
return;
}
const packages = shippingRates.length;
const shippingOptions = shippingRates.reduce(
( acc, shippingRate ) => acc + shippingRate.shipping_rates.length,
0
);
if ( shippingOptions === 0 ) {
speak(
__(
'No shipping options were found.',
'woo-gutenberg-products-block'
)
);
} else if ( packages === 1 ) {
speak(
sprintf(
// translators: %d number of shipping options found.
_n(
'%d shipping option was found.',
'%d shipping options were found.',
shippingOptions,
'woo-gutenberg-products-block'
),
shippingOptions
)
);
} else {
speak(
sprintf(
// translators: %d number of shipping packages packages.
_n(
'Shipping option searched for %d package.',
'Shipping options searched for %d packages.',
packages,
'woo-gutenberg-products-block'
),
packages
) +
' ' +
sprintf(
// translators: %d number of shipping options available.
_n(
'%d shipping option was found',
'%d shipping options were found',
shippingOptions,
'woo-gutenberg-products-block'
),
shippingOptions
)
);
}
}, [ shippingRatesLoading, shippingRates ] );
return (
<LoadingMask
isLoading={ shippingRatesLoading }

View File

@ -8,6 +8,7 @@
margin: 0;
padding: 0 $gap;
min-width: 500px;
max-width: 65%;
}
}
@ -65,6 +66,7 @@
.wc-block-main {
padding: 0;
flex: none;
max-width: 100%;
min-width: 200px;
}
.wc-block-sidebar {

View File

@ -61,8 +61,8 @@ const ValidatedTextInput = ( {
const errorMessage = getValidationError( errorId ) || {};
const hasError = errorMessage.message && ! errorMessage.hidden;
const describedBy =
showError && hasError
? getValidationErrorId( textInputId )
showError && hasError && getValidationErrorId( errorId )
? getValidationErrorId( errorId )
: ariaDescribedBy;
return (

View File

@ -131,6 +131,7 @@ const TotalsShippingItem = ( {
/>
{ ! isCheckout && showingRates && (
<ShippingRateSelector
hasRates={ hasRates }
shippingRates={ shippingRates }
shippingRatesLoading={ shippingRatesLoading }
/>

View File

@ -24,14 +24,20 @@ const renderShippingRatesControlOption = ( option ) => ( {
),
} );
const ShippingRateSelector = ( { shippingRates, shippingRatesLoading } ) => {
const ShippingRateSelector = ( {
hasRates,
shippingRates,
shippingRatesLoading,
} ) => {
return (
<fieldset className="wc-block-totals__shipping-options-fieldset">
<legend className="screen-reader-text">
{ __(
'Choose a shipping method',
'woo-gutenberg-products-block'
) }
{ hasRates
? __( 'Shipping methods', 'woo-gutenberg-products-block' )
: __(
'Choose a shipping method',
'woo-gutenberg-products-block'
) }
</legend>
<ShippingRatesControl
className="wc-block-totals__shipping-options"

View File

@ -151,12 +151,16 @@ export const ValidationContextProvider = ( { children } ) => {
* 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
* @param {string} errorId 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 getValidationErrorId = ( errorId ) => {
const error = getValidationError( errorId );
if ( ! error || error.hidden ) {
return '';
}
return `validate-error-${ errorId }`;
};
const context = {

View File

@ -99,12 +99,18 @@ const CartLineItemRow = ( { lineItem } ) => {
precision: parseInt( prices.raw_prices.precision, 10 ),
} ).multiply( quantity );
const saleAmount = regularAmount.subtract( purchaseAmount );
const firstImage = images.length ? images[ 0 ] : {};
return (
<tr className="wc-block-cart-items__row">
<td className="wc-block-cart-item__image">
<a href={ permalink }>
<ProductImage image={ images.length ? images[ 0 ] : {} } />
{ /* If the image has no alt text, this link is unnecessary and can be hidden. */ }
<td
className="wc-block-cart-item__image"
aria-hidden={ ! firstImage.alt }
>
{ /* We don't need to make it focusable, because product name has the same link. */ }
<a href={ permalink } tabIndex={ -1 }>
<ProductImage image={ firstImage } />
</a>
</td>
<td className="wc-block-cart-item__product">

View File

@ -129,211 +129,227 @@ const Checkout = ( {
}
}, [ shippingAsBilling, setBillingData ] );
return (
<SidebarLayout className="wc-block-checkout">
<Main>
<ExpressCheckoutFormControl />
<CheckoutForm>
<FormStep
id="contact-fields"
className="wc-block-checkout__contact-fields"
title={ __(
'Contact information',
'woo-gutenberg-products-block'
) }
description={ __(
"We'll use this email to send you details and updates about your order.",
'woo-gutenberg-products-block'
) }
stepHeadingContent={ () => (
<Fragment>
{ __(
'Already have an account? ',
'woo-gutenberg-products-block'
) }
<a href="/wp-login.php">
{ __(
'Log in.',
'woo-gutenberg-products-block'
) }
</a>
</Fragment>
) }
>
<ValidatedTextInput
type="email"
label={ __(
'Email address',
'woo-gutenberg-products-block'
) }
value={ billingData.email }
autoComplete="email"
onChange={ ( newValue ) =>
setBillingData( { 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 && (
<>
<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'
) }
stepHeadingContent={ () => (
<Fragment>
{ __(
'Already have an account? ',
'woo-gutenberg-products-block'
) }
<a href="/wp-login.php">
{ __(
'Log in.',
'woo-gutenberg-products-block'
) }
</a>
</Fragment>
) }
>
<AddressForm
onChange={ setShippingFields }
values={ shippingAddress }
fields={ Object.keys( addressFields ) }
fieldConfig={ addressFields }
<ValidatedTextInput
type="email"
label={ __(
'Email address',
'woo-gutenberg-products-block'
) }
value={ billingData.email }
autoComplete="email"
onChange={ ( newValue ) =>
setBillingData( { email: newValue } )
}
required={ true }
/>
{ attributes.showPhoneField && (
<ValidatedTextInput
type="tel"
label={
attributes.requirePhoneField
? __(
'Phone',
'woo-gutenberg-products-block'
)
: __(
'Phone (optional)',
'woo-gutenberg-products-block'
)
}
value={ billingData.phone }
autoComplete="tel"
onChange={ ( newValue ) =>
setBillingData( { phone: newValue } )
<CheckboxControl
className="wc-block-checkout__keep-updated"
label={ __(
'Keep me up to date on news and exclusive offers',
'woo-gutenberg-products-block'
) }
checked={ contactFields.keepUpdated }
onChange={ () =>
setContactFields( {
...contactFields,
keepUpdated: ! contactFields.keepUpdated,
} )
}
/>
</FormStep>
{ SHIPPING_ENABLED && (
<FormStep
id="shipping-fields"
className="wc-block-checkout__shipping-fields"
title={ __(
'Shipping address',
'woo-gutenberg-products-block'
) }
description={ __(
'Enter the physical address where you want us to deliver your order.',
'woo-gutenberg-products-block'
) }
>
<AddressForm
onChange={ setShippingFields }
values={ shippingAddress }
fields={ Object.keys( addressFields ) }
fieldConfig={ addressFields }
/>
{ attributes.showPhoneField && (
<ValidatedTextInput
type="tel"
label={
attributes.requirePhoneField
? __(
'Phone',
'woo-gutenberg-products-block'
)
: __(
'Phone (optional)',
'woo-gutenberg-products-block'
)
}
value={ billingData.phone }
autoComplete="tel"
onChange={ ( newValue ) =>
setBillingData( {
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={ shippingAsBilling }
onChange={ ( isChecked ) =>
setShippingAsBilling( isChecked )
}
required={ attributes.requirePhoneField }
/>
) }
<CheckboxControl
className="wc-block-checkout__use-address-for-billing"
label={ __(
'Use same address for billing',
</FormStep>
) }
{ showBillingFields && (
<FormStep
id="billing-fields"
className="wc-block-checkout__billing-fields"
title={ __(
'Billing address',
'woo-gutenberg-products-block'
) }
checked={ shippingAsBilling }
onChange={ ( isChecked ) =>
setShippingAsBilling( isChecked )
}
required={ attributes.requirePhoneField }
/>
</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={ setBillingData }
type="billing"
values={ billingData }
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={
shippingAddress.country
? {
address_1:
shippingAddress.address_1,
address_2:
shippingAddress.address_2,
city: shippingAddress.city,
state:
shippingAddress.state,
postcode:
shippingAddress.postcode,
country:
shippingAddress.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
}
description={ __(
'Enter the address that matches your card or payment method.',
'woo-gutenberg-products-block'
) }
>
<AddressForm
onChange={ setBillingData }
type="billing"
values={ billingData }
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={
shippingAddress.country
? {
address_1:
shippingAddress.address_1,
address_2:
shippingAddress.address_2,
city:
shippingAddress.city,
state:
shippingAddress.state,
postcode:
shippingAddress.postcode,
country:
shippingAddress.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
}
/>
) }
{ /*@todo This is not implemented*/ }
<CheckboxControl
className="wc-block-checkout__add-note"
label="Add order notes?"
checked={ false }
onChange={ () => null }
/>
</FormStep>
) }
<FormStep
id="payment-method"
className="wc-block-checkout__payment-method"
title={ __(
'Payment method',
'woo-gutenberg-products-block'
) }
{ /*@todo This is not implemented*/ }
<CheckboxControl
className="wc-block-checkout__add-note"
label="Add order notes?"
checked={ false }
onChange={ () => null }
/>
description={ __(
'Select a payment method below.',
'woo-gutenberg-products-block'
) }
>
<PaymentMethods />
</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 />
</FormStep>
</CheckoutForm>
</Main>
<Sidebar className="wc-block-checkout__sidebar">
<CheckoutSidebar
cartCoupons={ cartCoupons }
cartItems={ cartItems }
cartTotals={ cartTotals }
/>
</Sidebar>
<Main>
<div className="wc-block-checkout__actions">
{ attributes.showReturnToCart && (
<ReturnToCartButton
@ -346,16 +362,9 @@ const Checkout = ( {
<PlaceOrderButton validateSubmit={ validateSubmit } />
</div>
{ attributes.showPolicyLinks && <Policies /> }
</CheckoutForm>
</Main>
<Sidebar className="wc-block-checkout__sidebar">
<CheckoutSidebar
cartCoupons={ cartCoupons }
cartItems={ cartItems }
cartTotals={ cartTotals }
/>
</Sidebar>
</SidebarLayout>
</Main>
</SidebarLayout>
</>
);
};

View File

@ -54,12 +54,12 @@ const CheckoutOrderSummaryItem = ( { cartItem } ) => {
</div>
<div className="wc-block-order-summary-item__description">
<div className="wc-block-order-summary-item__header">
<ProductName permalink={ permalink } name={ name } />
<ProductPrice
className="wc-block-order-summary-item__total-price"
currency={ currency }
value={ linePrice }
/>
<ProductName permalink={ permalink } name={ name } />
</div>
<div className="wc-block-order-summary-item__prices">
<ProductPrice