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-14338017@ Fix: accessibility focus for my account pages and promo code #1625

Merged
merged 18 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -6,6 +6,7 @@
- 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)
- Improve focus behavior on my account pages, address forms, and promo codes [#1625](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1625)

### Other features

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import LoadingSpinner from '@salesforce/retail-react-app/app/components/loading-
* The provided `onRemove` callback triggers a loading spinner internally
* if given a promise.
*/
const ActionCard = ({children, onEdit, onRemove, ...props}) => {
const ActionCard = ({children, onEdit, onRemove, editBtnRef, ...props}) => {
const [showLoading, setShowLoading] = useState(false)

const handleRemove = async () => {
Expand All @@ -43,7 +43,7 @@ const ActionCard = ({children, onEdit, onRemove, ...props}) => {
<Box>{children}</Box>
<Stack direction="row" spacing={4}>
{onEdit && (
<Button onClick={onEdit} variant="link" size="sm">
<Button onClick={onEdit} variant="link" size="sm" ref={editBtnRef}>
<FormattedMessage defaultMessage="Edit" id="action_card.action.edit" />
</Button>
)}
Expand Down Expand Up @@ -75,7 +75,10 @@ ActionCard.propTypes = {
onRemove: PropTypes.func,

/** Content rendered in card */
children: PropTypes.node
children: PropTypes.node,

/** Ref for the edit button so that it can be focused on for accessibility */
editBtnRef: PropTypes.object
}

export default ActionCard
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
* 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 React, {useEffect, useRef} from 'react'
import {useIntl} from 'react-intl'
import PropTypes from 'prop-types'
import {
Grid,
Expand All @@ -19,9 +20,24 @@ import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-cur
const AddressFields = ({form, prefix = ''}) => {
const {data: customer} = useCurrentCustomer()
const fields = useAddressFields({form, prefix})
const intl = useIntl()

const addressFormRef = useRef()
useEffect(() => {
// Focus on the form when the component mounts for accessibility
addressFormRef?.current?.focus()
}, [])

return (
<Stack spacing={5}>
<Stack
spacing={5}
aria-label={intl.formatMessage({
id: 'use_address_fields.label.address_form',
defaultMessage: 'Address Form'
})}
tabIndex="0"
ref={addressFormRef}
>
<SimpleGrid columns={[1, 1, 2]} gap={5}>
<Field {...fields.firstName} />
<Field {...fields.lastName} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const PromoCodeFields = ({form, prefix = '', ...props}) => {
const code = form.watch('code')

return (
<Box {...props}>
<Box aria-labelledby="code-feedback" {...props}>
<Field inputProps={{flex: 1, mr: 2}} {...fields.code}>
<Button
type="submit"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const ToggleCard = ({
fontSize="lg"
lineHeight="30px"
color={disabled && !editing && 'gray.600'}
tabIndex="0"
>
{title}
</Heading>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import React, {useState} from 'react'
import React, {useEffect, useRef, useState} from 'react'
import {defineMessage, FormattedMessage, useIntl} from 'react-intl'
import PropTypes from 'prop-types'

Expand Down Expand Up @@ -147,6 +147,22 @@ const AccountAddresses = () => {
const showToast = useToast()
const form = useForm()

const headingRef = useRef()
useEffect(() => {
// Focus the 'Addresses' header when the component mounts for accessibility
headingRef?.current?.focus()
}, [])

// keep track of the edit buttons so we can focus on them later for accessibility
const [editBtnRefs, setEditBtnRefs] = useState({})
useEffect(() => {
const currentRefs = {}
addresses?.forEach(({addressId}) => {
currentRefs[addressId] = React.createRef()
})
setEditBtnRefs(currentRefs)
}, [addresses])

const hasAddresses = addresses?.length > 0
const showError = () => {
showToast({
Expand Down Expand Up @@ -216,6 +232,8 @@ const AccountAddresses = () => {
status: 'success',
isClosable: true
})
// Move focus to header after we successfully remove address
headingRef?.current?.focus()
}
}
)
Expand All @@ -232,14 +250,18 @@ const AccountAddresses = () => {
setSelectedAddressId(address.addressId)
setIsEditing(true)
} else {
// Focus on the edit button that opened the form when the form closes
// otherwise focus on the heading if we can't find the button
const focusAfterClose = editBtnRefs[selectedAddressId]?.current ?? headingRef?.current
focusAfterClose?.focus()
setSelectedAddressId(undefined)
setIsEditing(!isEditing)
}
}

return (
<Stack spacing={4} data-testid="account-addresses-page">
<Heading as="h1" fontSize="2xl">
<Heading as="h1" fontSize="2xl" tabIndex="0" ref={headingRef}>
<FormattedMessage
defaultMessage="Addresses"
id="account_addresses.title.addresses"
Expand Down Expand Up @@ -304,6 +326,7 @@ const AccountAddresses = () => {
<ActionCard
borderColor="gray.200"
key={address.addressId}
editBtnRef={editBtnRefs[address.addressId]}
onRemove={() => removeAddress(address.addressId)}
onEdit={() => toggleEdit(address)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@ const MockedComponent = () => {
)
}

const helperAddNewAddress = async (user) => {
await user.click(screen.getByText(/add address/i))
await user.type(screen.getByLabelText('First Name'), 'Test')
await user.type(screen.getByLabelText('Last Name'), 'McTester')
await user.type(screen.getByLabelText('Phone'), '7275551234')
await user.type(screen.getByLabelText('Address'), '123 Main St')
await user.type(screen.getByLabelText('City'), 'Tampa')
await user.selectOptions(screen.getByLabelText(/state/i), ['FL'])
await user.type(screen.getByLabelText('Zip Code'), '33712')

global.server.use(
rest.get('*/customers/:customerId', (req, res, ctx) =>
res(ctx.delay(0), ctx.status(200), ctx.json(mockedRegisteredCustomer))
)
)
await user.click(screen.getByText(/^Save$/i))
}

// Set up and clean up
beforeEach(() => {
jest.resetModules()
Expand Down Expand Up @@ -82,21 +100,7 @@ test('Allows customer to add addresses', async () => {
expect(screen.getByText(/no saved addresses/i)).toBeInTheDocument()
})

await user.click(screen.getByText(/add address/i))
await user.type(screen.getByLabelText('First Name'), 'Test')
await user.type(screen.getByLabelText('Last Name'), 'McTester')
await user.type(screen.getByLabelText('Phone'), '7275551234')
await user.type(screen.getByLabelText('Address'), '123 Main St')
await user.type(screen.getByLabelText('City'), 'Tampa')
await user.selectOptions(screen.getByLabelText(/state/i), ['FL'])
await user.type(screen.getByLabelText('Zip Code'), '33712')

global.server.use(
rest.get('*/customers/:customerId', (req, res, ctx) =>
res(ctx.delay(0), ctx.status(200), ctx.json(mockedRegisteredCustomer))
)
)
await user.click(screen.getByText(/^Save$/i))
await helperAddNewAddress(user)
expect(await screen.findByText(/123 Main St/i)).toBeInTheDocument()
})

Expand All @@ -118,3 +122,35 @@ test('Allows customer to remove addresses', async () => {
await user.click(screen.getByText(/remove/i))
expect(await screen.findByText(/no saved addresses/i)).toBeInTheDocument()
})

test('Handles focus for cancel/save buttons in address form correctly', async () => {
global.server.use(
rest.get('*/customers/:customerId', (req, res, ctx) =>
res(ctx.delay(0), ctx.status(200), ctx.json(mockedRegisteredCustomerWithNoAddress))
)
)
const {user} = renderWithProviders(<MockedComponent />, {
wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app}
})

await waitFor(() => {
expect(screen.getByText(/no saved addresses/i)).toBeInTheDocument()
})

// Focus is on heading when component initially renders
expect(document.activeElement).toBe(screen.getByRole('heading', {name: /addresses/i}))

await helperAddNewAddress(user)

const editBtn = screen.getByRole('button', {name: /edit/i})

// hitting cancel button on edit form brings focus back to edit button
await user.click(editBtn)
await user.click(screen.getByRole('button', {name: /cancel/i}))
expect(document.activeElement).toBe(editBtn)

// hitting save button on edit form brings focus back to edit button
await user.click(editBtn)
await user.click(screen.getByRole('button', {name: /save/i}))
expect(document.activeElement).toBe(editBtn)
})
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 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 React, {useEffect, useRef} from 'react'
import {FormattedMessage, useIntl} from 'react-intl'
import {useHistory, useRouteMatch} from 'react-router'
import {
Expand Down Expand Up @@ -123,6 +123,12 @@ const AccountOrderDetail = () => {
const CardIcon = getCreditCardIcon(paymentCard?.cardType)
const itemCount = order?.productItems.reduce((count, item) => item.quantity + count, 0) || 0

const headingRef = useRef()
useEffect(() => {
// Focus the 'Order Details' header when the component mounts for accessibility
headingRef?.current?.focus()
}, [])

return (
<Stack spacing={6} data-testid="account-order-details-page">
<Stack>
Expand All @@ -148,7 +154,7 @@ const AccountOrderDetail = () => {
</Box>

<Stack spacing={[1, 2]}>
<Heading as="h1" fontSize={['lg', '2xl']}>
<Heading as="h1" fontSize={['lg', '2xl']} tabIndex="0" ref={headingRef}>
<FormattedMessage
defaultMessage="Order Details"
id="account_order_detail.title.order_details"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import React, {useEffect} from 'react'
import React, {useEffect, useRef} from 'react'
import {FormattedMessage, FormattedNumber, useIntl} from 'react-intl'
import {useLocation} from 'react-router'
import {
Expand Down Expand Up @@ -99,14 +99,20 @@ const AccountOrderHistory = () => {

const pageUrls = usePageUrls({total: paging.total, limit})

const headingRef = useRef()
useEffect(() => {
// Focus the 'Order History' header when the component mounts for accessibility
headingRef?.current?.focus()
}, [])

useEffect(() => {
window.scrollTo(0, 0)
}, [customer, searchParams.offset])

return (
<Stack spacing={4} data-testid="account-order-history-page">
<Stack>
<Heading as="h1" fontSize="2xl">
<Heading as="h1" fontSize="2xl" tabIndex="0" ref={headingRef}>
joeluong-sfcc marked this conversation as resolved.
Show resolved Hide resolved
<FormattedMessage
defaultMessage="Order History"
id="account_order_history.title.order_history"
Expand Down
Loading