diff --git a/packages/template-retail-react-app/CHANGELOG.md b/packages/template-retail-react-app/CHANGELOG.md index d97f868cac..ec94bf0ae5 100644 --- a/packages/template-retail-react-app/CHANGELOG.md +++ b/packages/template-retail-react-app/CHANGELOG.md @@ -2,6 +2,7 @@ ### Accessibility improvements - Change radio refinements (for example, filtering by Price) from radio inputs to styled buttons [#1605](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1605) +- Update search refinements ARIA labels to include "add/remove filter" [#1607](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1607) ## v2.2.0 (Nov 8, 2023) diff --git a/packages/template-retail-react-app/app/pages/product-list/index.test.js b/packages/template-retail-react-app/app/pages/product-list/index.test.js index 2764c27fdc..98b57b09c5 100644 --- a/packages/template-retail-react-app/app/pages/product-list/index.test.js +++ b/packages/template-retail-react-app/app/pages/product-list/index.test.js @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, salesforce.com, inc. + * Copyright (c) 2023, Salesforce, Inc. * All rights reserved. * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause @@ -150,23 +150,23 @@ test('clicking a filter on mobile or desktop applies changes to both', async () // Only desktop filters should be present // Test using two buttons since there was a bug where using only one filter would properly // apply changes to both desktop and mobile, but 2 or more would cause it to fail - let beigeBtns = screen.getAllByLabelText('Beige (6)') - let blueBtns = screen.getAllByLabelText('Blue (27)') + let beigeBtns = screen.getAllByLabelText('Add filter: Beige (6)') + let blueBtns = screen.getAllByLabelText('Add filter: Blue (27)') expect(beigeBtns).toHaveLength(1) expect(blueBtns).toHaveLength(1) // click beige filter and ensure that only beige is checked await user.click(beigeBtns[0]) expect(beigeBtns[0]).toHaveAttribute('aria-checked', 'true') - expect(screen.getByLabelText('Blue (27)')).toHaveAttribute('aria-checked', 'false') + expect(screen.getByLabelText('Add filter: Blue (27)')).toHaveAttribute('aria-checked', 'false') // click filter button for mobile that is hidden on desktop but present in DOM // this opens the filter modal on mobile - await user.click(screen.getByRole('button', {name: /filter/i})) + await user.click(screen.getByText('Filter')) // re-query for desktop and mobile filters - beigeBtns = screen.getAllByLabelText('Beige (6)') - blueBtns = screen.getAllByLabelText('Blue (27)') + beigeBtns = screen.getAllByLabelText('Remove filter: Beige (6)') + blueBtns = screen.getAllByLabelText('Add filter: Blue (27)') // both mobile and desktop filters are present in DOM expect(beigeBtns).toHaveLength(2) diff --git a/packages/template-retail-react-app/app/pages/product-list/partials/checkbox-refinements.jsx b/packages/template-retail-react-app/app/pages/product-list/partials/checkbox-refinements.jsx index 3a6271ea94..8b82fb72b5 100644 --- a/packages/template-retail-react-app/app/pages/product-list/partials/checkbox-refinements.jsx +++ b/packages/template-retail-react-app/app/pages/product-list/partials/checkbox-refinements.jsx @@ -1,15 +1,21 @@ /* - * Copyright (c) 2021, salesforce.com, inc. + * Copyright (c) 2023, Salesforce, Inc. * All rights reserved. * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import React from 'react' -import {Box, Checkbox, Stack} from '@salesforce/retail-react-app/app/components/shared/ui' import PropTypes from 'prop-types' +import {useIntl} from 'react-intl' +import {Box, Checkbox, Stack} from '@salesforce/retail-react-app/app/components/shared/ui' +import { + ADD_FILTER, + REMOVE_FILTER +} from '@salesforce/retail-react-app/app/pages/product-list/partials/refinements-utils' const CheckboxRefinements = ({filter, toggleFilter, selectedFilters}) => { + const {formatMessage} = useIntl() return ( {filter.values @@ -22,6 +28,10 @@ const CheckboxRefinements = ({filter, toggleFilter, selectedFilters}) => { toggleFilter(value, filter.attributeId, isChecked)} + aria-label={formatMessage( + isChecked ? REMOVE_FILTER : ADD_FILTER, + value + )} > {value.label} diff --git a/packages/template-retail-react-app/app/pages/product-list/partials/color-refinements.jsx b/packages/template-retail-react-app/app/pages/product-list/partials/color-refinements.jsx index cb2244e18b..cf40ccfce7 100644 --- a/packages/template-retail-react-app/app/pages/product-list/partials/color-refinements.jsx +++ b/packages/template-retail-react-app/app/pages/product-list/partials/color-refinements.jsx @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, salesforce.com, inc. + * Copyright (c) 2023, Salesforce, Inc. * All rights reserved. * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause @@ -18,6 +18,10 @@ import { import PropTypes from 'prop-types' import {cssColorGroups} from '@salesforce/retail-react-app/app/constants' import {useIntl} from 'react-intl' +import { + ADD_FILTER_HIT_COUNT, + REMOVE_FILTER_HIT_COUNT +} from '@salesforce/retail-react-app/app/pages/product-list/partials/refinements-utils' const ColorRefinements = ({filter, toggleFilter, selectedFilters}) => { const intl = useIntl() @@ -49,11 +53,8 @@ const ColorRefinements = ({filter, toggleFilter, selectedFilters}) => { marginRight={0} marginBottom="-1px" aria-label={intl.formatMessage( - { - id: 'colorRefinements.label.hitCount', - defaultMessage: '{colorLabel} ({colorHitCount})' - }, - {colorLabel: value.label, colorHitCount: value.hitCount} + isSelected ? REMOVE_FILTER_HIT_COUNT : ADD_FILTER_HIT_COUNT, + value )} >
{ const buttonRef = useRef() + const {formatMessage} = useIntl() + const selected = selectedFilters.includes(value.value) // Because choosing a refinement is equivalent to a form submission, the best semantic choice // for the refinement is a button or a link, rather than a radio input. The radio element here // is purely for visual purposes, and should probably be replaced with a simple icon. @@ -19,19 +26,20 @@ const RadioRefinement = ({filter, value, toggleFilter, selectedFilters}) => { buttonRef.current?.click()} - /> + > toggleFilter(value, filter.attributeId, false, false)} + aria-label={formatMessage(selected ? REMOVE_FILTER : ADD_FILTER, value)} > {value.label} diff --git a/packages/template-retail-react-app/app/pages/product-list/partials/refinements-utils.js b/packages/template-retail-react-app/app/pages/product-list/partials/refinements-utils.js new file mode 100644 index 0000000000..2ea2a8ff17 --- /dev/null +++ b/packages/template-retail-react-app/app/pages/product-list/partials/refinements-utils.js @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import {defineMessage} from 'react-intl' + +/** ARIA label for refinement pickers when the option has not been selected. */ +export const ADD_FILTER = defineMessage({ + id: 'product_list.refinements.button.assistive_msg.add_filter', + defaultMessage: 'Add filter: {label}' +}) + +/** ARIA label for refinement pickers when the option has been selected. */ +export const REMOVE_FILTER = defineMessage({ + id: 'product_list.refinements.button.assistive_msg.remove_filter', + defaultMessage: 'Remove filter: {label}' +}) + +/** + * ARIA label for refinement pickers when the option has not been selected. + * Includes the number of results. + */ +export const ADD_FILTER_HIT_COUNT = defineMessage({ + id: 'product_list.refinements.button.assistive_msg.add_filter_with_hit_count', + defaultMessage: 'Add filter: {label} ({hitCount})' +}) + +/** + * ARIA label for refinement pickers when the option has not been selected. + * Includes the number of results. + */ +export const REMOVE_FILTER_HIT_COUNT = defineMessage({ + id: 'product_list.refinements.button.assistive_msg.remove_filter_with_hit_count', + defaultMessage: 'Remove filter: {label} ({hitCount})' +}) diff --git a/packages/template-retail-react-app/app/pages/product-list/partials/selected-refinements.jsx b/packages/template-retail-react-app/app/pages/product-list/partials/selected-refinements.jsx index 5e032a70dc..8528a6e32c 100644 --- a/packages/template-retail-react-app/app/pages/product-list/partials/selected-refinements.jsx +++ b/packages/template-retail-react-app/app/pages/product-list/partials/selected-refinements.jsx @@ -1,18 +1,19 @@ /* - * Copyright (c) 2021, salesforce.com, inc. + * Copyright (c) 2023, Salesforce, Inc. * All rights reserved. * SPDX-License-Identifier: BSD-3-Clause * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import React from 'react' -import {Box, Button, Wrap, WrapItem} from '@salesforce/retail-react-app/app/components/shared/ui' +import {FormattedMessage, useIntl} from 'react-intl' import PropTypes from 'prop-types' +import {Box, Button, Wrap, WrapItem} from '@salesforce/retail-react-app/app/components/shared/ui' import {CloseIcon} from '@salesforce/retail-react-app/app/components/icons' - -import {FormattedMessage} from 'react-intl' +import {REMOVE_FILTER} from '@salesforce/retail-react-app/app/pages/product-list/partials/refinements-utils' const SelectedRefinements = ({toggleFilter, selectedFilterValues, filters, handleReset}) => { + const {formatMessage} = useIntl() const priceFilterValues = filters?.find((filter) => filter.attributeId === 'price') let selectedFilters = [] @@ -61,6 +62,7 @@ const SelectedRefinements = ({toggleFilter, selectedFilterValues, filters, handl onClick={() => toggleFilter({value: filter.apiLabel}, filter.value, true) } + aria-label={formatMessage(REMOVE_FILTER, {label: filter.uiLabel})} > {filter.uiLabel} @@ -77,6 +79,10 @@ const SelectedRefinements = ({toggleFilter, selectedFilterValues, filters, handl variant="link" size="sm" onClick={handleReset} + aria-label={formatMessage({ + id: 'selected_refinements.action.assistive_msg.clear_all', + defaultMessage: 'Clear all filters' + })} > { + const {formatMessage} = useIntl() const styles = useMultiStyleConfig('SwatchGroup', { variant: 'square', disabled: false @@ -42,6 +47,10 @@ const SizeRefinements = ({filter, toggleFilter, selectedFilters}) => { variant="outline" marginBottom={0} marginRight={0} + aria-label={formatMessage( + isSelected ? REMOVE_FILTER : ADD_FILTER, + value + )} >
{value.label}
diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json index 7b9d33f1a9..51110c2be2 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json @@ -2351,6 +2351,70 @@ "value": "Filter" } ], + "product_list.refinements.button.assistive_msg.add_filter": [ + { + "type": 0, + "value": "Add filter: " + }, + { + "type": 1, + "value": "label" + } + ], + "product_list.refinements.button.assistive_msg.add_filter_with_hit_count": [ + { + "type": 0, + "value": "Add filter: " + }, + { + "type": 1, + "value": "label" + }, + { + "type": 0, + "value": " (" + }, + { + "type": 1, + "value": "hitCount" + }, + { + "type": 0, + "value": ")" + } + ], + "product_list.refinements.button.assistive_msg.remove_filter": [ + { + "type": 0, + "value": "Remove filter: " + }, + { + "type": 1, + "value": "label" + } + ], + "product_list.refinements.button.assistive_msg.remove_filter_with_hit_count": [ + { + "type": 0, + "value": "Remove filter: " + }, + { + "type": 1, + "value": "label" + }, + { + "type": 0, + "value": " (" + }, + { + "type": 1, + "value": "hitCount" + }, + { + "type": 0, + "value": ")" + } + ], "product_list.select.sort_by": [ { "type": 0, @@ -2689,6 +2753,12 @@ "value": "Cancel" } ], + "selected_refinements.action.assistive_msg.clear_all": [ + { + "type": 0, + "value": "Clear all filters" + } + ], "selected_refinements.action.clear_all": [ { "type": 0, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json index 7b9d33f1a9..51110c2be2 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json @@ -2351,6 +2351,70 @@ "value": "Filter" } ], + "product_list.refinements.button.assistive_msg.add_filter": [ + { + "type": 0, + "value": "Add filter: " + }, + { + "type": 1, + "value": "label" + } + ], + "product_list.refinements.button.assistive_msg.add_filter_with_hit_count": [ + { + "type": 0, + "value": "Add filter: " + }, + { + "type": 1, + "value": "label" + }, + { + "type": 0, + "value": " (" + }, + { + "type": 1, + "value": "hitCount" + }, + { + "type": 0, + "value": ")" + } + ], + "product_list.refinements.button.assistive_msg.remove_filter": [ + { + "type": 0, + "value": "Remove filter: " + }, + { + "type": 1, + "value": "label" + } + ], + "product_list.refinements.button.assistive_msg.remove_filter_with_hit_count": [ + { + "type": 0, + "value": "Remove filter: " + }, + { + "type": 1, + "value": "label" + }, + { + "type": 0, + "value": " (" + }, + { + "type": 1, + "value": "hitCount" + }, + { + "type": 0, + "value": ")" + } + ], "product_list.select.sort_by": [ { "type": 0, @@ -2689,6 +2753,12 @@ "value": "Cancel" } ], + "selected_refinements.action.assistive_msg.clear_all": [ + { + "type": 0, + "value": "Clear all filters" + } + ], "selected_refinements.action.clear_all": [ { "type": 0, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json index 423a3e6563..42b9eecb69 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json @@ -5047,6 +5047,102 @@ "value": "]" } ], + "product_list.refinements.button.assistive_msg.add_filter": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "Ȧḓḓ ƒīŀŧḗḗř: " + }, + { + "type": 1, + "value": "label" + }, + { + "type": 0, + "value": "]" + } + ], + "product_list.refinements.button.assistive_msg.add_filter_with_hit_count": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "Ȧḓḓ ƒīŀŧḗḗř: " + }, + { + "type": 1, + "value": "label" + }, + { + "type": 0, + "value": " (" + }, + { + "type": 1, + "value": "hitCount" + }, + { + "type": 0, + "value": ")" + }, + { + "type": 0, + "value": "]" + } + ], + "product_list.refinements.button.assistive_msg.remove_filter": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "Řḗḗḿǿǿṽḗḗ ƒīŀŧḗḗř: " + }, + { + "type": 1, + "value": "label" + }, + { + "type": 0, + "value": "]" + } + ], + "product_list.refinements.button.assistive_msg.remove_filter_with_hit_count": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "Řḗḗḿǿǿṽḗḗ ƒīŀŧḗḗř: " + }, + { + "type": 1, + "value": "label" + }, + { + "type": 0, + "value": " (" + }, + { + "type": 1, + "value": "hitCount" + }, + { + "type": 0, + "value": ")" + }, + { + "type": 0, + "value": "]" + } + ], "product_list.select.sort_by": [ { "type": 0, @@ -5753,6 +5849,20 @@ "value": "]" } ], + "selected_refinements.action.assistive_msg.clear_all": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "Ƈŀḗḗȧȧř ȧȧŀŀ ƒīŀŧḗḗřş" + }, + { + "type": 0, + "value": "]" + } + ], "selected_refinements.action.clear_all": [ { "type": 0, diff --git a/packages/template-retail-react-app/translations/en-GB.json b/packages/template-retail-react-app/translations/en-GB.json index 8c215363e4..74d36e724f 100644 --- a/packages/template-retail-react-app/translations/en-GB.json +++ b/packages/template-retail-react-app/translations/en-GB.json @@ -1023,6 +1023,18 @@ "product_list.modal.title.filter": { "defaultMessage": "Filter" }, + "product_list.refinements.button.assistive_msg.add_filter": { + "defaultMessage": "Add filter: {label}" + }, + "product_list.refinements.button.assistive_msg.add_filter_with_hit_count": { + "defaultMessage": "Add filter: {label} ({hitCount})" + }, + "product_list.refinements.button.assistive_msg.remove_filter": { + "defaultMessage": "Remove filter: {label}" + }, + "product_list.refinements.button.assistive_msg.remove_filter_with_hit_count": { + "defaultMessage": "Remove filter: {label} ({hitCount})" + }, "product_list.select.sort_by": { "defaultMessage": "Sort By: {sortOption}" }, @@ -1162,6 +1174,9 @@ "search.action.cancel": { "defaultMessage": "Cancel" }, + "selected_refinements.action.assistive_msg.clear_all": { + "defaultMessage": "Clear all filters" + }, "selected_refinements.action.clear_all": { "defaultMessage": "Clear All" }, diff --git a/packages/template-retail-react-app/translations/en-US.json b/packages/template-retail-react-app/translations/en-US.json index 8c215363e4..74d36e724f 100644 --- a/packages/template-retail-react-app/translations/en-US.json +++ b/packages/template-retail-react-app/translations/en-US.json @@ -1023,6 +1023,18 @@ "product_list.modal.title.filter": { "defaultMessage": "Filter" }, + "product_list.refinements.button.assistive_msg.add_filter": { + "defaultMessage": "Add filter: {label}" + }, + "product_list.refinements.button.assistive_msg.add_filter_with_hit_count": { + "defaultMessage": "Add filter: {label} ({hitCount})" + }, + "product_list.refinements.button.assistive_msg.remove_filter": { + "defaultMessage": "Remove filter: {label}" + }, + "product_list.refinements.button.assistive_msg.remove_filter_with_hit_count": { + "defaultMessage": "Remove filter: {label} ({hitCount})" + }, "product_list.select.sort_by": { "defaultMessage": "Sort By: {sortOption}" }, @@ -1162,6 +1174,9 @@ "search.action.cancel": { "defaultMessage": "Cancel" }, + "selected_refinements.action.assistive_msg.clear_all": { + "defaultMessage": "Clear all filters" + }, "selected_refinements.action.clear_all": { "defaultMessage": "Clear All" },