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 + '%',
|
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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
} ) }
|
} ) }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
|
||||||
) }
|
) }
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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 }
|
||||||
>
|
>
|
||||||
1
|
<Label
|
||||||
|
label={ 1 }
|
||||||
|
screenReaderLabel={ sprintf(
|
||||||
|
/* translators: %d is the page number (1, 2, 3...). */
|
||||||
|
__( 'Page %d', 'woo-gutenberg-products-block' ),
|
||||||
|
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 && (
|
||||||
|
|
|
@ -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' ) }
|
||||||
|
:
|
||||||
<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>
|
||||||
|
|
|
@ -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 ] );
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue