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-12627096@ Improve accessibility of variation attribute swatches #1587

Merged
merged 22 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3adb8cb
Convert hard-coded text to translatable message.
wjhsf Nov 30, 2023
8243f1f
Remove unnecessary `onChange` handler.
wjhsf Nov 30, 2023
22f1586
Define attributes in product view, rather than swatch group.
wjhsf Nov 30, 2023
a6c82fe
Fix children prop type.
wjhsf Nov 30, 2023
d463d4f
Move variation attribute swatch groups into separate component.
wjhsf Nov 30, 2023
79ebe1a
Add arrow key support to swatch radios.
wjhsf Nov 30, 2023
37feb20
just click
wjhsf Nov 30, 2023
a755d22
Update changelog.
wjhsf Nov 30, 2023
58c4ad6
Merge branch 'develop' into wjh/a11y-radio/W-12627096
wjhsf Nov 30, 2023
cea3aae
fix test
wjhsf Dec 1, 2023
f64215d
Merge branch 'develop' into wjh/a11y-radio/W-12627096
wjhsf Dec 4, 2023
f52400f
Merge branch 'develop' into wjh/a11y-radio/W-12627096
wjhsf Dec 4, 2023
d40408f
Merge branch 'develop' into wjh/a11y-radio/W-12627096
wjhsf Dec 6, 2023
0199d26
Move `onKeyDown` logic to swatch and delete new component.
wjhsf Dec 7, 2023
f333a7f
lint fixes
wjhsf Dec 7, 2023
1c939dc
Fix buttons not receiving focus when nothing is selected.
wjhsf Dec 8, 2023
f24b6b0
Merge branch 'develop' into wjh/a11y-radio/W-12627096
wjhsf Dec 11, 2023
72700e1
Merge branch 'develop' into wjh/a11y-radio/W-12627096
wjhsf Dec 11, 2023
7706d10
Merge branch 'develop' into wjh/a11y-radio/W-12627096
wjhsf Dec 13, 2023
08960b5
Merge branch 'develop' into wjh/a11y-radio/W-12627096
wjhsf Dec 14, 2023
4f78b57
Merge branch 'develop' into wjh/a11y-radio/W-12627096
wjhsf Dec 14, 2023
d2cc00a
Merge branch 'develop' into wjh/a11y-radio/W-12627096
wjhsf Dec 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -3,6 +3,7 @@

### Accessibility improvements

- Add correct keyboard interaction behavior for variation attribute radio buttons [#1587](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1587)
- 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)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/*
* 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, {forwardRef, useEffect, useRef, useState} from 'react'
import PropTypes from 'prop-types'
import {useHistory, useLocation} from 'react-router-dom'
import {useLocation} from 'react-router-dom'
import {useIntl, FormattedMessage} from 'react-intl'

import {
Expand All @@ -25,8 +25,6 @@ import {useDerivedProduct} from '@salesforce/retail-react-app/app/hooks'
import {useAddToCartModalContext} from '@salesforce/retail-react-app/app/hooks/use-add-to-cart-modal'

// project components
import SwatchGroup from '@salesforce/retail-react-app/app/components/swatch-group'
import Swatch from '@salesforce/retail-react-app/app/components/swatch-group/swatch'
import ImageGallery from '@salesforce/retail-react-app/app/components/image-gallery'
import Breadcrumb from '@salesforce/retail-react-app/app/components/breadcrumb'
import Link from '@salesforce/retail-react-app/app/components/link'
Expand All @@ -37,6 +35,8 @@ import QuantityPicker from '@salesforce/retail-react-app/app/components/quantity
import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
import {API_ERROR_MESSAGE} from '@salesforce/retail-react-app/app/constants'
import DisplayPrice from '@salesforce/retail-react-app/app/components/display-price'
import Swatch from '@salesforce/retail-react-app/app/components/swatch-group/swatch'
import SwatchGroup from '@salesforce/retail-react-app/app/components/swatch-group'
import {getDisplayPrice} from '@salesforce/retail-react-app/app/utils/product-utils'

const ProductViewHeader = ({name, basePrice, discountPrice, currency, category, productType}) => {
Expand Down Expand Up @@ -104,7 +104,6 @@ const ProductView = forwardRef(
) => {
const showToast = useToast()
const intl = useIntl()
const history = useHistory()
const location = useLocation()
const {
isOpen: isAddToCartModalOpen,
Expand Down Expand Up @@ -357,69 +356,70 @@ const ProductView = forwardRef(
<Skeleton height={20} width={64} />
</>
) : (
<>
{/* Attribute Swatches */}
{variationAttributes.map((variationAttribute) => {
const {
id,
name,
selectedValue,
values = []
} = variationAttribute
return (
<SwatchGroup
key={id}
onChange={(_, href) => {
if (!href) return
history.replace(href)
}}
variant={id === 'color' ? 'circle' : 'square'}
value={selectedValue?.value}
displayName={selectedValue?.name || ''}
label={intl.formatMessage(
{
defaultMessage: '{variantType}',
id: 'product_view.label.variant_type'
},
{variantType: name}
)}
>
{values.map(
({href, name, image, value, orderable}) => (
<Swatch
key={value}
href={href}
disabled={!orderable}
value={value}
name={name}
>
{image ? (
<Box
height="100%"
width="100%"
minWidth="32px"
backgroundRepeat="no-repeat"
backgroundSize="cover"
backgroundColor={name.toLowerCase()}
backgroundImage={
image
? `url(${
image.disBaseLink ||
image.link
})`
: ''
}
/>
) : (
name
)}
</Swatch>
)
)}
</SwatchGroup>
)
})}
</>
variationAttributes.map(({id, name, selectedValue, values}) => {
const swatches = values.map(
({href, name, image, value, orderable}, index) => {
const content = image ? (
<Box
height="100%"
width="100%"
minWidth="32px"
backgroundRepeat="no-repeat"
backgroundSize="cover"
backgroundColor={name.toLowerCase()}
backgroundImage={`url(${
image.disBaseLink || image.link
})`}
/>
) : (
name
)
const hasSelection = Boolean(selectedValue?.value)
const isSelected = selectedValue?.value === value
const isFirst = index === 0
// To mimic the behavior of a native radio input, only
// one swatch should receive tab focus; the rest can be
// selected using arrow keys when the swatch group has
// focus. The focused element is the selected option or
// the first in the group, if no option is selected.
// This is a slight difference, for simplicity, from the
// native element, where the first element is focused on
// `Tab` and the _last_ element is focused on `Shift+Tab`
const isFocusable =
isSelected || (!hasSelection && isFirst)
return (
<Swatch
key={value}
href={href}
disabled={!orderable}
value={value}
name={name}
variant={id === 'color' ? 'circle' : 'square'}
selected={isSelected}
isFocusable={isFocusable}
>
{content}
</Swatch>
)
}
)
return (
<SwatchGroup
key={id}
value={selectedValue?.value}
displayName={selectedValue?.name || ''}
label={intl.formatMessage(
{
defaultMessage: '{variantType}',
id: 'product_view.label.variant_type'
},
{variantType: name}
)}
>
{swatches}
</SwatchGroup>
)
})
)}

{/* Quantity Selector */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,28 @@ import {
HStack,
useStyleConfig
} from '@salesforce/retail-react-app/app/components/shared/ui'
import {noop} from '@salesforce/retail-react-app/app/utils/utils'
import {FormattedMessage} from 'react-intl'

/**
* SwatchGroup allows you to create a list of swatches
* Each Swatch is a link with will direct to a href passed to them
*/
const SwatchGroup = (props) => {
const {
displayName,
children,
value: selectedValue,
label = '',
variant = 'square',
onChange = noop
} = props
const {displayName, children, label = ''} = props
const styles = useStyleConfig('SwatchGroup')
return (
<Flex {...styles.swatchGroup} role="radiogroup" aria-label={label}>
<HStack {...styles.swatchLabel}>
<Box fontWeight="semibold">{`${label}:`}</Box>
<Box fontWeight="semibold">
<FormattedMessage
id="swatch_group.selected.label"
defaultMessage="{label}:"
values={{label}}
/>
</Box>
<Box>{displayName}</Box>
</HStack>
<Flex {...styles.swatchesWrapper}>
{React.Children.map(children, (child) => {
const childValue = child.props.value

return React.cloneElement(child, {
selected: childValue === selectedValue,
variant,
onChange
})
})}
</Flex>
<Flex {...styles.swatchesWrapper}>{children}</Flex>
</Flex>
)
}
Expand All @@ -57,26 +46,14 @@ SwatchGroup.propTypes = {
* The attribute name of the swatch group. E.g color, size
*/
label: PropTypes.string,
/**
* The selected Swatch value.
*/
value: PropTypes.string,
/**
* The display value of the selected option
*/
displayName: PropTypes.string,
/**
* The Swatch options to choose between
*/
children: PropTypes.array,
/**
* The shape of the swatches
*/
variant: PropTypes.oneOf(['square', 'circle']),
/**
* This function is called when a new option is selected
*/
onChange: PropTypes.func
children: PropTypes.node
}

export default SwatchGroup
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
* 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 {screen, render, fireEvent, waitFor} from '@testing-library/react'
import {createMemoryHistory} from 'history'
import {Router, useHistory, useLocation} from 'react-router-dom'
import SwatchGroup from '@salesforce/retail-react-app/app/components/swatch-group/index'
import {screen, fireEvent, waitFor} from '@testing-library/react'

import {Box} from '@salesforce/retail-react-app/app/components/shared/ui'
import SwatchGroup from '@salesforce/retail-react-app/app/components/swatch-group/index'
import Swatch from '@salesforce/retail-react-app/app/components/swatch-group/swatch'
import {createMemoryHistory} from 'history'
import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'

const data = {
id: 'color',
Expand Down Expand Up @@ -108,7 +110,7 @@ describe('Swatch Component', () => {
const history = createMemoryHistory()
history.push('/en-GB/swatch-example?color=JJ2XNXX')

render(
renderWithProviders(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to use renderWithProviders? I think it was intentional to not use the it since there is not need to render other providers to test this component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test fails without the providers. With these changes it needs the IntlProvider.

<Router history={history}>
<Page />
</Router>
Expand All @@ -120,7 +122,7 @@ describe('Swatch Component', () => {
const history = createMemoryHistory()
history.push('/en-GB/swatch-example')

render(
renderWithProviders(
<Router history={history}>
<Page />
</Router>
Expand Down
Loading