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:
parent
aa4bc302a5
commit
c8f297a700
|
@ -18,6 +18,11 @@ const ProductRating = ( { className, product } ) => {
|
|||
width: ( rating / 5 ) * 100 + '%',
|
||||
};
|
||||
|
||||
const ratingText = sprintf(
|
||||
__( 'Rated %d out of 5', 'woo-gutenberg-products-block' ),
|
||||
rating
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
|
@ -28,16 +33,9 @@ const ProductRating = ( { className, product } ) => {
|
|||
<div
|
||||
className={ `${ layoutStyleClassPrefix }__product-rating__stars` }
|
||||
role="img"
|
||||
aria-label={ ratingText }
|
||||
>
|
||||
<span style={ starStyle }>
|
||||
{ sprintf(
|
||||
__(
|
||||
'Rated %d out of 5',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
rating
|
||||
) }
|
||||
</span>
|
||||
<span style={ starStyle }>{ ratingText }</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import { useProductLayoutContext } from '@woocommerce/base-context/product-layout-context';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
|
||||
const ProductSaleBadge = ( { className, product, align } ) => {
|
||||
const { layoutStyleClassPrefix } = useProductLayoutContext();
|
||||
|
@ -21,7 +22,13 @@ const ProductSaleBadge = ( { className, product, align } ) => {
|
|||
`${ layoutStyleClassPrefix }__product-onsale`
|
||||
) }
|
||||
>
|
||||
{ __( 'Sale', 'woo-gutenberg-products-block' ) }
|
||||
<Label
|
||||
label={ __( 'Sale', 'woo-gutenberg-products-block' ) }
|
||||
screenReaderLabel={ __(
|
||||
'Product on sale',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -145,6 +145,13 @@ const DropdownSelector = ( {
|
|||
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 }
|
||||
/>
|
||||
</DropdownSelectorInputWrapper>
|
||||
|
|
|
@ -6,6 +6,7 @@ const DropdownSelectorInput = ( {
|
|||
onFocus,
|
||||
onRemoveItem,
|
||||
placeholder,
|
||||
tabIndex,
|
||||
value,
|
||||
} ) => {
|
||||
return (
|
||||
|
@ -25,6 +26,7 @@ const DropdownSelectorInput = ( {
|
|||
}
|
||||
},
|
||||
placeholder,
|
||||
tabIndex,
|
||||
} ) }
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -21,6 +21,7 @@ const DropdownSelectorSelectedValue = ( { onClick, onRemoveItem, option } ) => {
|
|||
onClick( option.value );
|
||||
} }
|
||||
aria-label={ sprintf(
|
||||
/* translators: %s attribute value used in the filter. For example: yellow, green, small, large. */
|
||||
__(
|
||||
'Replace current %s filter',
|
||||
'woo-gutenberg-products-block'
|
||||
|
@ -41,6 +42,7 @@ const DropdownSelectorSelectedValue = ( { onClick, onRemoveItem, option } ) => {
|
|||
}
|
||||
} }
|
||||
aria-label={ sprintf(
|
||||
/* translators: %s attribute value used in the filter. For example: yellow, green, small, large. */
|
||||
__( 'Remove %s filter', 'woo-gutenberg-products-block' ),
|
||||
option.name
|
||||
) }
|
||||
|
|
|
@ -4,13 +4,21 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
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 (
|
||||
<button
|
||||
type="submit"
|
||||
|
@ -21,8 +29,7 @@ const FilterSubmitButton = ( { className, disabled, onClick } ) => {
|
|||
disabled={ disabled }
|
||||
onClick={ onClick }
|
||||
>
|
||||
{ // translators: Submit button text for filters.
|
||||
__( 'Go', 'woo-gutenberg-products-block' ) }
|
||||
<Label label={ label } screenReaderLabel={ screenReaderLabel } />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
@ -37,6 +44,8 @@ FilterSubmitButton.propTypes = {
|
|||
* On click callback.
|
||||
*/
|
||||
onClick: PropTypes.func.isRequired,
|
||||
label: PropTypes.string,
|
||||
screenReaderLabel: PropTypes.string,
|
||||
};
|
||||
|
||||
FilterSubmitButton.defaultProps = {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
|
@ -83,7 +83,14 @@ const Pagination = ( {
|
|||
onClick={ () => onPageChange( 1 ) }
|
||||
disabled={ currentPage === 1 }
|
||||
>
|
||||
1
|
||||
<Label
|
||||
label={ 1 }
|
||||
screenReaderLabel={ sprintf(
|
||||
/* translators: %d is the page number (1, 2, 3...). */
|
||||
__( 'Page %d', 'woo-gutenberg-products-block' ),
|
||||
1
|
||||
) }
|
||||
/>
|
||||
</button>
|
||||
) }
|
||||
{ showFirstPageEllipsis && (
|
||||
|
@ -109,7 +116,14 @@ const Pagination = ( {
|
|||
}
|
||||
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>
|
||||
);
|
||||
} ) }
|
||||
|
@ -130,7 +144,14 @@ const Pagination = ( {
|
|||
onClick={ () => onPageChange( 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>
|
||||
) }
|
||||
{ displayNextAndPreviousArrows && (
|
||||
|
|
|
@ -241,7 +241,7 @@ const PriceSlider = ( {
|
|||
onFocus={ findClosestRange }
|
||||
>
|
||||
{ hasValidConstraints && (
|
||||
<Fragment>
|
||||
<div aria-hidden={ showInputFields }>
|
||||
<div
|
||||
className="wc-block-price-filter__range-input-progress"
|
||||
style={ progressStyles }
|
||||
|
@ -264,6 +264,7 @@ const PriceSlider = ( {
|
|||
max={ maxConstraint }
|
||||
ref={ minRange }
|
||||
disabled={ isLoading }
|
||||
tabIndex={ showInputFields ? '-1' : '0' }
|
||||
/>
|
||||
<input
|
||||
type="range"
|
||||
|
@ -283,8 +284,9 @@ const PriceSlider = ( {
|
|||
max={ maxConstraint }
|
||||
ref={ maxRange }
|
||||
disabled={ isLoading }
|
||||
tabIndex={ showInputFields ? '-1' : '0' }
|
||||
/>
|
||||
</Fragment>
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
<div className="wc-block-price-filter__controls">
|
||||
|
@ -333,8 +335,8 @@ const PriceSlider = ( {
|
|||
Number.isFinite( minPrice ) &&
|
||||
Number.isFinite( maxPrice ) && (
|
||||
<div className="wc-block-price-filter__range-text">
|
||||
{ __( 'Price', 'woo-gutenberg-products-block' ) }:
|
||||
|
||||
{ __( 'Price', 'woo-gutenberg-products-block' ) }
|
||||
:
|
||||
<FormattedMonetaryAmount
|
||||
currency={ currency }
|
||||
displayType="text"
|
||||
|
@ -353,6 +355,10 @@ const PriceSlider = ( {
|
|||
className="wc-block-price-filter__button"
|
||||
disabled={ isLoading || ! hasValidConstraints }
|
||||
onClick={ onSubmit }
|
||||
screenReaderLabel={ __(
|
||||
'Apply price filter',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import { isEqual } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
@ -16,6 +17,7 @@ import {
|
|||
} from '@woocommerce/base-hooks';
|
||||
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
|
||||
import { useProductLayoutContext } from '@woocommerce/base-context/product-layout-context';
|
||||
import { speak } from '@wordpress/a11y';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -70,6 +72,29 @@ const extractPaginationAndSortAttributes = ( query ) => {
|
|||
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 = ( {
|
||||
attributes,
|
||||
currentPage,
|
||||
|
@ -119,6 +144,11 @@ const ProductList = ( {
|
|||
// reset pagination to the first page.
|
||||
if ( ! isPreviousTotalQueryEqual ) {
|
||||
onPageChange( 1 );
|
||||
|
||||
// Make sure there was a previous query, so we don't announce it on page load.
|
||||
if ( previousQueryTotals ) {
|
||||
announceLoadingCompletion( totalProducts );
|
||||
}
|
||||
}
|
||||
}, [ queryState ] );
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@
|
|||
.wc-block-grid__product-price__regular {
|
||||
font-size: 0.8em;
|
||||
line-height: 1;
|
||||
color: #aaa;
|
||||
color: #555;
|
||||
margin-top: -0.25em;
|
||||
display: block;
|
||||
}
|
||||
|
@ -181,9 +181,9 @@
|
|||
height: 1.618em;
|
||||
line-height: 1.618;
|
||||
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;
|
||||
display: -block;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import { useQueryStateByKey } from '@woocommerce/base-hooks';
|
|||
import { useMemo, Fragment } from '@wordpress/element';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -37,8 +38,8 @@ const ActiveFiltersBlock = ( {
|
|||
__( 'Price', 'woo-gutenberg-products-block' ),
|
||||
formatPriceRange( minPrice, maxPrice ),
|
||||
() => {
|
||||
setMinPrice( null );
|
||||
setMaxPrice( null );
|
||||
setMinPrice( undefined );
|
||||
setMaxPrice( undefined );
|
||||
}
|
||||
);
|
||||
}, [ minPrice, maxPrice, formatPriceRange ] );
|
||||
|
@ -104,12 +105,21 @@ const ActiveFiltersBlock = ( {
|
|||
<button
|
||||
className="wc-block-active-filters__clear-all"
|
||||
onClick={ () => {
|
||||
setMinPrice( null );
|
||||
setMaxPrice( null );
|
||||
setMinPrice( undefined );
|
||||
setMaxPrice( undefined );
|
||||
setProductAttributes( [] );
|
||||
} }
|
||||
>
|
||||
{ __( 'Clear All', 'woo-gutenberg-products-block' ) }
|
||||
<Label
|
||||
label={ __(
|
||||
'Clear All',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
screenReaderLabel={ __(
|
||||
'Clear All Filters',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</Fragment>
|
||||
|
|
|
@ -59,7 +59,11 @@ export const renderRemovableListItem = (
|
|||
{ name }
|
||||
</strong>
|
||||
<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>
|
||||
</li>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue