All Products & filters accessibility improvements (https://github.com/woocommerce/woocommerce-blocks/pull/1656)

* Add aria-label to All Products ratings

* Add specific screen reader text to some buttons

* Increase All Products regular price color constrast

* Remove invalid CSS declaration

* Make styleint-disable comment more specific

* Attributes Filter: make input non-focusable if we display the 'change filter' button

* Improve translator documentation

* Hide price slider from screen readers if price inputs are enabled

* Linting fixes

* Price slider: make it non-focusable if input fields are displayed

* All Products: announce how many products were found

* All Products: announce when a filter is removed

* Revert "All Products: announce when a filter is removed"

This reverts commit 2c861bf1b988155313ad44bafbcaf3f4f1549296.

* Pagination component: improve screen reader texts

* Filter submit button: improve screen reader texts

* Remove unnecessary text

* Improve comment

* Use %d for numeric values

* Add label and screenReaderLabel props to FilterSubmitButton component
This commit is contained in:
Albert Juhé Lluveras 2020-01-30 11:04:39 +01:00 committed by GitHub
parent aa4bc302a5
commit c8f297a700
12 changed files with 126 additions and 30 deletions

View File

@ -18,6 +18,11 @@ const ProductRating = ( { className, product } ) => {
width: ( rating / 5 ) * 100 + '%', width: ( rating / 5 ) * 100 + '%',
}; };
const ratingText = sprintf(
__( 'Rated %d out of 5', 'woo-gutenberg-products-block' ),
rating
);
return ( return (
<div <div
className={ classnames( className={ classnames(
@ -28,16 +33,9 @@ const ProductRating = ( { className, product } ) => {
<div <div
className={ `${ layoutStyleClassPrefix }__product-rating__stars` } className={ `${ layoutStyleClassPrefix }__product-rating__stars` }
role="img" role="img"
aria-label={ ratingText }
> >
<span style={ starStyle }> <span style={ starStyle }>{ ratingText }</span>
{ sprintf(
__(
'Rated %d out of 5',
'woo-gutenberg-products-block'
),
rating
) }
</span>
</div> </div>
</div> </div>
); );

View File

@ -4,6 +4,7 @@
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import classnames from 'classnames'; import classnames from 'classnames';
import { useProductLayoutContext } from '@woocommerce/base-context/product-layout-context'; import { useProductLayoutContext } from '@woocommerce/base-context/product-layout-context';
import Label from '@woocommerce/base-components/label';
const ProductSaleBadge = ( { className, product, align } ) => { const ProductSaleBadge = ( { className, product, align } ) => {
const { layoutStyleClassPrefix } = useProductLayoutContext(); const { layoutStyleClassPrefix } = useProductLayoutContext();
@ -21,7 +22,13 @@ const ProductSaleBadge = ( { className, product, align } ) => {
`${ layoutStyleClassPrefix }__product-onsale` `${ layoutStyleClassPrefix }__product-onsale`
) } ) }
> >
{ __( 'Sale', 'woo-gutenberg-products-block' ) } <Label
label={ __( 'Sale', 'woo-gutenberg-products-block' ) }
screenReaderLabel={ __(
'Product on sale',
'woo-gutenberg-products-block'
) }
/>
</div> </div>
); );
} }

View File

@ -145,6 +145,13 @@ const DropdownSelector = ( {
attributeLabel attributeLabel
) )
} }
tabIndex={
// When it's a single selector and there is one element selected,
// we make the input non-focusable with the keyboard because it's
// visually hidden. The input is still rendered, though, because it
// must be possible to focus it when pressing the select value chip.
! multiple && checked.length > 0 ? '-1' : '0'
}
value={ inputValue } value={ inputValue }
/> />
</DropdownSelectorInputWrapper> </DropdownSelectorInputWrapper>

View File

@ -6,6 +6,7 @@ const DropdownSelectorInput = ( {
onFocus, onFocus,
onRemoveItem, onRemoveItem,
placeholder, placeholder,
tabIndex,
value, value,
} ) => { } ) => {
return ( return (
@ -25,6 +26,7 @@ const DropdownSelectorInput = ( {
} }
}, },
placeholder, placeholder,
tabIndex,
} ) } } ) }
/> />
); );

View File

@ -21,6 +21,7 @@ const DropdownSelectorSelectedValue = ( { onClick, onRemoveItem, option } ) => {
onClick( option.value ); onClick( option.value );
} } } }
aria-label={ sprintf( aria-label={ sprintf(
/* translators: %s attribute value used in the filter. For example: yellow, green, small, large. */
__( __(
'Replace current %s filter', 'Replace current %s filter',
'woo-gutenberg-products-block' 'woo-gutenberg-products-block'
@ -41,6 +42,7 @@ const DropdownSelectorSelectedValue = ( { onClick, onRemoveItem, option } ) => {
} }
} } } }
aria-label={ sprintf( aria-label={ sprintf(
/* translators: %s attribute value used in the filter. For example: yellow, green, small, large. */
__( 'Remove %s filter', 'woo-gutenberg-products-block' ), __( 'Remove %s filter', 'woo-gutenberg-products-block' ),
option.name option.name
) } ) }

View File

@ -4,13 +4,21 @@
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import Label from '@woocommerce/base-components/label';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import './style.scss'; import './style.scss';
const FilterSubmitButton = ( { className, disabled, onClick } ) => { const FilterSubmitButton = ( {
className,
disabled,
// translators: Submit button text for filters.
label = __( 'Go', 'woo-gutenberg-products-block' ),
onClick,
screenReaderLabel = __( 'Apply filter', 'woo-gutenberg-products-block' ),
} ) => {
return ( return (
<button <button
type="submit" type="submit"
@ -21,8 +29,7 @@ const FilterSubmitButton = ( { className, disabled, onClick } ) => {
disabled={ disabled } disabled={ disabled }
onClick={ onClick } onClick={ onClick }
> >
{ // translators: Submit button text for filters. <Label label={ label } screenReaderLabel={ screenReaderLabel } />
__( 'Go', 'woo-gutenberg-products-block' ) }
</button> </button>
); );
}; };
@ -37,6 +44,8 @@ FilterSubmitButton.propTypes = {
* On click callback. * On click callback.
*/ */
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
label: PropTypes.string,
screenReaderLabel: PropTypes.string,
}; };
FilterSubmitButton.defaultProps = { FilterSubmitButton.defaultProps = {

View File

@ -1,7 +1,7 @@
/** /**
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import Label from '@woocommerce/base-components/label'; import Label from '@woocommerce/base-components/label';
@ -83,7 +83,14 @@ const Pagination = ( {
onClick={ () => onPageChange( 1 ) } onClick={ () => onPageChange( 1 ) }
disabled={ currentPage === 1 } disabled={ currentPage === 1 }
> >
<Label
label={ 1 }
screenReaderLabel={ sprintf(
/* translators: %d is the page number (1, 2, 3...). */
__( 'Page %d', 'woo-gutenberg-products-block' ),
1 1
) }
/>
</button> </button>
) } ) }
{ showFirstPageEllipsis && ( { showFirstPageEllipsis && (
@ -109,7 +116,14 @@ const Pagination = ( {
} }
disabled={ currentPage === page } disabled={ currentPage === page }
> >
{ page } <Label
label={ page }
screenReaderLabel={ sprintf(
/* translators: %d is the page number (1, 2, 3...). */
__( 'Page %d', 'woo-gutenberg-products-block' ),
page
) }
/>
</button> </button>
); );
} ) } } ) }
@ -130,7 +144,14 @@ const Pagination = ( {
onClick={ () => onPageChange( totalPages ) } onClick={ () => onPageChange( totalPages ) }
disabled={ currentPage === totalPages } disabled={ currentPage === totalPages }
> >
{ totalPages } <Label
label={ totalPages }
screenReaderLabel={ sprintf(
/* translators: %d is the page number (1, 2, 3...). */
__( 'Page %d', 'woo-gutenberg-products-block' ),
totalPages
) }
/>
</button> </button>
) } ) }
{ displayNextAndPreviousArrows && ( { displayNextAndPreviousArrows && (

View File

@ -241,7 +241,7 @@ const PriceSlider = ( {
onFocus={ findClosestRange } onFocus={ findClosestRange }
> >
{ hasValidConstraints && ( { hasValidConstraints && (
<Fragment> <div aria-hidden={ showInputFields }>
<div <div
className="wc-block-price-filter__range-input-progress" className="wc-block-price-filter__range-input-progress"
style={ progressStyles } style={ progressStyles }
@ -264,6 +264,7 @@ const PriceSlider = ( {
max={ maxConstraint } max={ maxConstraint }
ref={ minRange } ref={ minRange }
disabled={ isLoading } disabled={ isLoading }
tabIndex={ showInputFields ? '-1' : '0' }
/> />
<input <input
type="range" type="range"
@ -283,8 +284,9 @@ const PriceSlider = ( {
max={ maxConstraint } max={ maxConstraint }
ref={ maxRange } ref={ maxRange }
disabled={ isLoading } disabled={ isLoading }
tabIndex={ showInputFields ? '-1' : '0' }
/> />
</Fragment> </div>
) } ) }
</div> </div>
<div className="wc-block-price-filter__controls"> <div className="wc-block-price-filter__controls">
@ -333,8 +335,8 @@ const PriceSlider = ( {
Number.isFinite( minPrice ) && Number.isFinite( minPrice ) &&
Number.isFinite( maxPrice ) && ( Number.isFinite( maxPrice ) && (
<div className="wc-block-price-filter__range-text"> <div className="wc-block-price-filter__range-text">
{ __( 'Price', 'woo-gutenberg-products-block' ) }: { __( 'Price', 'woo-gutenberg-products-block' ) }
&nbsp; : &nbsp;
<FormattedMonetaryAmount <FormattedMonetaryAmount
currency={ currency } currency={ currency }
displayType="text" displayType="text"
@ -353,6 +355,10 @@ const PriceSlider = ( {
className="wc-block-price-filter__button" className="wc-block-price-filter__button"
disabled={ isLoading || ! hasValidConstraints } disabled={ isLoading || ! hasValidConstraints }
onClick={ onSubmit } onClick={ onSubmit }
screenReaderLabel={ __(
'Apply price filter',
'woo-gutenberg-products-block'
) }
/> />
) } ) }
</div> </div>

View File

@ -1,6 +1,7 @@
/** /**
* External dependencies * External dependencies
*/ */
import { __, _n, sprintf } from '@wordpress/i18n';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
@ -16,6 +17,7 @@ import {
} from '@woocommerce/base-hooks'; } from '@woocommerce/base-hooks';
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top'; import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
import { useProductLayoutContext } from '@woocommerce/base-context/product-layout-context'; import { useProductLayoutContext } from '@woocommerce/base-context/product-layout-context';
import { speak } from '@wordpress/a11y';
/** /**
* Internal dependencies * Internal dependencies
@ -70,6 +72,29 @@ const extractPaginationAndSortAttributes = ( query ) => {
return totalQuery; return totalQuery;
}; };
const announceLoadingCompletion = ( totalProducts ) => {
if ( ! Number.isFinite( totalProducts ) ) {
return;
}
if ( totalProducts === 0 ) {
speak( __( 'No products found', 'woo-gutenberg-products-block' ) );
} else {
speak(
sprintf(
// translators: %s is an integer higher than 0 (1, 2, 3...)
_n(
'%d product found',
'%d products found',
totalProducts,
'woo-gutenberg-products-block'
),
totalProducts
)
);
}
};
const ProductList = ( { const ProductList = ( {
attributes, attributes,
currentPage, currentPage,
@ -119,6 +144,11 @@ const ProductList = ( {
// reset pagination to the first page. // reset pagination to the first page.
if ( ! isPreviousTotalQueryEqual ) { if ( ! isPreviousTotalQueryEqual ) {
onPageChange( 1 ); onPageChange( 1 );
// Make sure there was a previous query, so we don't announce it on page load.
if ( previousQueryTotals ) {
announceLoadingCompletion( totalProducts );
}
} }
}, [ queryState ] ); }, [ queryState ] );

View File

@ -108,7 +108,7 @@
.wc-block-grid__product-price__regular { .wc-block-grid__product-price__regular {
font-size: 0.8em; font-size: 0.8em;
line-height: 1; line-height: 1;
color: #aaa; color: #555;
margin-top: -0.25em; margin-top: -0.25em;
display: block; display: block;
} }
@ -181,9 +181,9 @@
height: 1.618em; height: 1.618em;
line-height: 1.618; line-height: 1.618;
font-size: 1em; font-size: 1em;
font-family: star; /* stylelint-disable-line */ /* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
font-family: star;
font-weight: 400; font-weight: 400;
display: -block;
margin: 0 auto; margin: 0 auto;
text-align: left; text-align: left;

View File

@ -6,6 +6,7 @@ import { useQueryStateByKey } from '@woocommerce/base-hooks';
import { useMemo, Fragment } from '@wordpress/element'; import { useMemo, Fragment } from '@wordpress/element';
import classnames from 'classnames'; import classnames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Label from '@woocommerce/base-components/label';
/** /**
* Internal dependencies * Internal dependencies
@ -37,8 +38,8 @@ const ActiveFiltersBlock = ( {
__( 'Price', 'woo-gutenberg-products-block' ), __( 'Price', 'woo-gutenberg-products-block' ),
formatPriceRange( minPrice, maxPrice ), formatPriceRange( minPrice, maxPrice ),
() => { () => {
setMinPrice( null ); setMinPrice( undefined );
setMaxPrice( null ); setMaxPrice( undefined );
} }
); );
}, [ minPrice, maxPrice, formatPriceRange ] ); }, [ minPrice, maxPrice, formatPriceRange ] );
@ -104,12 +105,21 @@ const ActiveFiltersBlock = ( {
<button <button
className="wc-block-active-filters__clear-all" className="wc-block-active-filters__clear-all"
onClick={ () => { onClick={ () => {
setMinPrice( null ); setMinPrice( undefined );
setMaxPrice( null ); setMaxPrice( undefined );
setProductAttributes( [] ); setProductAttributes( [] );
} } } }
> >
{ __( 'Clear All', 'woo-gutenberg-products-block' ) } <Label
label={ __(
'Clear All',
'woo-gutenberg-products-block'
) }
screenReaderLabel={ __(
'Clear All Filters',
'woo-gutenberg-products-block'
) }
/>
</button> </button>
</div> </div>
</Fragment> </Fragment>

View File

@ -59,7 +59,11 @@ export const renderRemovableListItem = (
{ name } { name }
</strong> </strong>
<button onClick={ removeCallback }> <button onClick={ removeCallback }>
{ __( 'Remove', 'woo-gutenberg-products-block' ) } { sprintf(
/* translators: %s attribute value used in the filter. For example: yellow, green, small, large. */
__( 'Remove %s filter', 'woo-gutenberg-products-block' ),
name
) }
</button> </button>
</li> </li>
); );