Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@W-12627152@ Improve refinement button ARIA labels #1607

Merged
merged 12 commits into from
Dec 13, 2023
1 change: 1 addition & 0 deletions packages/template-retail-react-app/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Stack spacing={1}>
{filter.values
Expand All @@ -22,6 +28,10 @@ const CheckboxRefinements = ({filter, toggleFilter, selectedFilters}) => {
<Checkbox
isChecked={isChecked}
onChange={() => toggleFilter(value, filter.attributeId, isChecked)}
aria-label={formatMessage(
isChecked ? REMOVE_FILTER : ADD_FILTER,
value
)}
>
{value.label}
</Checkbox>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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
)}
>
<Center
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@
*/

import React, {useRef} from 'react'
import {Box, Text, Radio, Stack} from '@salesforce/retail-react-app/app/components/shared/ui'
import PropTypes from 'prop-types'
import {useIntl} from 'react-intl'
import {Box, Text, Radio, 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 RadioRefinement = ({filter, value, toggleFilter, selectedFilters}) => {
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.
Expand All @@ -19,19 +26,20 @@ const RadioRefinement = ({filter, value, toggleFilter, selectedFilters}) => {
<Radio
display="inline-flex"
height={{base: '44px', lg: '24px'}}
isChecked={selectedFilters.includes(value.value)}
isChecked={selected}
// Ideally, this "icon" would be part of the button, but doing so with a radio input
// triggers `onClick` twice. The radio must be separate, and therefore we must add
// these workarounds to prevent it from receiving focus.
inputProps={{'aria-hidden': true, tabIndex: -1}}
onClick={() => buttonRef.current?.click()}
/>
></Radio>
<Text
ref={buttonRef}
ml={2}
as="button"
fontSize="sm"
onClick={() => toggleFilter(value, filter.attributeId, false, false)}
aria-label={formatMessage(selected ? REMOVE_FILTER : ADD_FILTER, value)}
>
{value.label}
</Text>
Expand Down
Original file line number Diff line number Diff line change
@@ -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})'
})
Original file line number Diff line number Diff line change
@@ -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 = []
Expand Down Expand Up @@ -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}
</Button>
Expand All @@ -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'
})}
>
<FormattedMessage
defaultMessage="Clear All"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
/*
* 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 PropTypes from 'prop-types'
import {useIntl} from 'react-intl'
import {
SimpleGrid,
Button,
Center,
useMultiStyleConfig
} from '@salesforce/retail-react-app/app/components/shared/ui'
import PropTypes from 'prop-types'
import {
ADD_FILTER,
REMOVE_FILTER
} from '@salesforce/retail-react-app/app/pages/product-list/partials/refinements-utils'

const SizeRefinements = ({filter, toggleFilter, selectedFilters}) => {
const {formatMessage} = useIntl()
const styles = useMultiStyleConfig('SwatchGroup', {
variant: 'square',
disabled: false
Expand Down Expand Up @@ -42,6 +47,10 @@ const SizeRefinements = ({filter, toggleFilter, selectedFilters}) => {
variant="outline"
marginBottom={0}
marginRight={0}
aria-label={formatMessage(
isSelected ? REMOVE_FILTER : ADD_FILTER,
value
)}
>
<Center {...styles.swatchButton}>{value.label}</Center>
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading