@salesforce/retail-react-app 2.2.0 → 2.3.0-nightly-20231212080144

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## v2.3.0-dev (Nov 8, 2023)
2
+ ### Accessibility improvements
3
+
4
+ - Change radio refinements (for example, filtering by Price) from radio inputs to styled buttons [#1605](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1605)
5
+
1
6
  ## v2.2.0 (Nov 8, 2023)
2
7
 
3
8
  ### Accessibility Improvements
@@ -14,12 +19,14 @@
14
19
  - Improve keyboard accessibility of product scroller [#1559](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1559)
15
20
  - Fix focus indicator for hero features links on homepage [#1561](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1561)
16
21
  - Ensure color is not the sole means of communicating information [#1570](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1570)
22
+ - Improve keyboard accessibility of account menu and nav bar [#1572](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1572)
17
23
 
18
24
  ### Other Features
19
25
 
20
26
  - Add [Active Data](https://help.salesforce.com/s/articleView?id=cc.b2c_active_data_attributes.htm&type=5) files, update pages (app index.jsx, product list and product details pages) to trigger events on product category and product detail views [#1555](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1555)
21
27
  - Replace max-age with s-maxage to only cache shared caches [#1564](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1564)
22
28
  - Implement gift option for basket [#1546](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1546)
29
+ - Added option to specify `isLoginPage` function to the `withRegistration` component. The default behavior is "all pages ending in `/login`". [#1572](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1572)
23
30
  - Update `extract-default-messages` script to support multiple locales [#1574](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1574)
24
31
  - Update engine compatibility to include npm 10 [#1597](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1597)
25
32
  - Add support for localization in icon component [#1609](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1609)
@@ -18,7 +18,6 @@ import {
18
18
  useAccessToken,
19
19
  useCategory,
20
20
  useCommerceApi,
21
- useCustomerType,
22
21
  useCustomerBaskets,
23
22
  useShopperBasketsMutation
24
23
  } from '@salesforce/commerce-sdk-react'
@@ -123,7 +122,6 @@ const App = (props) => {
123
122
  const history = useHistory()
124
123
  const location = useLocation()
125
124
  const authModal = useAuthModal()
126
- const {isRegistered} = useCustomerType()
127
125
  const {site, locale, buildUrl} = useMultiSite()
128
126
 
129
127
  const [isOnline, setIsOnline] = useState(true)
@@ -257,18 +255,13 @@ const App = (props) => {
257
255
  }
258
256
 
259
257
  const onAccountClick = () => {
260
- // Link to account page for registered customer, open auth modal otherwise
261
- if (isRegistered) {
262
- const path = buildUrl('/account')
263
- history.push(path)
264
- } else {
265
- // if they already are at the login page, do not show login modal
266
- if (new RegExp(`^/login$`).test(location.pathname)) return
267
- authModal.onOpen()
268
- }
258
+ // Link to account page if registered; Header component will show auth modal for guest users
259
+ const path = buildUrl('/account')
260
+ history.push(path)
269
261
  }
270
262
 
271
263
  const onWishlistClick = () => {
264
+ // Link to wishlist page if registered; Header component will show auth modal for guest users
272
265
  const path = buildUrl('/account/wishlist')
273
266
  history.push(path)
274
267
  }
@@ -19,6 +19,7 @@ import {
19
19
  Checkbox
20
20
  } from '@salesforce/retail-react-app/app/components/shared/ui'
21
21
  import {VisibilityIcon, VisibilityOffIcon} from '@salesforce/retail-react-app/app/components/icons'
22
+ import {useIntl} from 'react-intl'
22
23
 
23
24
  const Field = ({
24
25
  name,
@@ -35,8 +36,18 @@ const Field = ({
35
36
  helpText,
36
37
  children
37
38
  }) => {
39
+ const intl = useIntl()
38
40
  const [hidePassword, setHidePassword] = useState(true)
39
41
  const PasswordIcon = hidePassword ? VisibilityIcon : VisibilityOffIcon
42
+ const passwordIconLabel = hidePassword
43
+ ? intl.formatMessage({
44
+ id: 'field.password.assistive_msg.show_password',
45
+ defaultMessage: 'Show password'
46
+ })
47
+ : intl.formatMessage({
48
+ id: 'field.password.assistive_msg.hide_password',
49
+ defaultMessage: 'Hide password'
50
+ })
40
51
  const inputType =
41
52
  type === 'password' && hidePassword ? 'password' : type === 'password' ? 'text' : type
42
53
  return (
@@ -51,8 +62,7 @@ const Field = ({
51
62
 
52
63
  return (
53
64
  <FormControl id={name} isInvalid={error}>
54
- {!['checkbox', 'radio'].includes(type) &&
55
- type !== 'hidden' &&
65
+ {!['checkbox', 'radio', 'hidden'].includes(type) &&
56
66
  (formLabel || <FormLabel>{label}</FormLabel>)}
57
67
 
58
68
  <InputGroup>
@@ -83,7 +93,7 @@ const Field = ({
83
93
  <InputRightElement>
84
94
  <IconButton
85
95
  variant="ghosted"
86
- aria-label="Show password"
96
+ aria-label={passwordIconLabel}
87
97
  icon={<PasswordIcon color="gray.500" boxSize={6} />}
88
98
  onClick={() => setHidePassword(!hidePassword)}
89
99
  />
@@ -120,7 +130,7 @@ const Field = ({
120
130
  {children}
121
131
  </InputGroup>
122
132
 
123
- {error && !type !== 'hidden' && (
133
+ {error && type !== 'hidden' && (
124
134
  <FormErrorMessage color="red.600">{error.message}</FormErrorMessage>
125
135
  )}
126
136
 
@@ -49,8 +49,6 @@ import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation
49
49
  import LoadingSpinner from '@salesforce/retail-react-app/app/components/loading-spinner'
50
50
  import {isHydrated, noop} from '@salesforce/retail-react-app/app/utils/utils'
51
51
 
52
- const ENTER_KEY = 'Enter'
53
-
54
52
  const IconButtonWithRegistration = withRegistration(IconButton)
55
53
  /**
56
54
  * The header is the main source for accessing
@@ -86,7 +84,13 @@ const Header = ({
86
84
  const {isRegistered} = useCustomerType()
87
85
  const logout = useAuthHelper(AuthHelpers.Logout)
88
86
  const navigate = useNavigation()
89
- const {isOpen, onClose, onOpen} = useDisclosure()
87
+ const {
88
+ getButtonProps: getAccountMenuButtonProps,
89
+ getDisclosureProps: getAccountMenuDisclosureProps,
90
+ isOpen: isAccountMenuOpen,
91
+ onClose: onAccountMenuClose,
92
+ onOpen: onAccountMenuOpen
93
+ } = useDisclosure()
90
94
  const [isDesktop] = useMediaQuery('(min-width: 992px)')
91
95
 
92
96
  const [showLoading, setShowLoading] = useState(false)
@@ -103,15 +107,10 @@ const Header = ({
103
107
  setShowLoading(false)
104
108
  }
105
109
 
106
- const keyMap = {
107
- Escape: () => onClose(),
108
- Enter: () => onOpen()
109
- }
110
-
111
110
  const handleIconsMouseLeave = () => {
112
111
  // don't close the menu if users enter the popover content
113
112
  setTimeout(() => {
114
- if (!hasEnterPopoverContent.current) onClose()
113
+ if (!hasEnterPopoverContent.current) onAccountMenuClose()
115
114
  }, 100)
116
115
  }
117
116
 
@@ -154,39 +153,37 @@ const Header = ({
154
153
  {...styles.search}
155
154
  />
156
155
  </Box>
157
- <AccountIcon
158
- {...styles.accountIcon}
159
- tabIndex={0}
160
- onMouseOver={isDesktop ? onOpen : noop}
161
- onKeyDown={(e) => {
162
- e.key === ENTER_KEY ? onMyAccountClick() : noop
163
- }}
164
- onClick={onMyAccountClick}
156
+ <IconButtonWithRegistration
157
+ icon={<AccountIcon {...styles.accountIcon} />}
165
158
  aria-label={intl.formatMessage({
166
159
  id: 'header.button.assistive_msg.my_account',
167
160
  defaultMessage: 'My account'
168
161
  })}
162
+ variant="unstyled"
163
+ onClick={onMyAccountClick}
164
+ onMouseOver={isDesktop ? onAccountMenuOpen : noop}
169
165
  />
170
166
 
171
167
  {isRegistered && isHydrated() && (
172
168
  <Popover
173
169
  isLazy
174
170
  arrowSize={15}
175
- isOpen={isOpen}
171
+ isOpen={isAccountMenuOpen}
176
172
  placement="bottom-end"
177
- onClose={onClose}
178
- onOpen={onOpen}
173
+ onClose={onAccountMenuClose}
174
+ onOpen={onAccountMenuOpen}
179
175
  >
180
176
  <PopoverTrigger>
181
- <ChevronDownIcon
182
- aria-label="My account trigger"
177
+ <IconButton
178
+ aria-label={intl.formatMessage({
179
+ id: 'header.button.assistive_msg.my_account_menu',
180
+ defaultMessage: 'Open account menu'
181
+ })}
182
+ icon={<ChevronDownIcon {...styles.arrowDown} />}
183
+ variant="unstyled"
184
+ {...getAccountMenuButtonProps()}
185
+ onMouseOver={onAccountMenuOpen}
183
186
  onMouseLeave={handleIconsMouseLeave}
184
- onKeyDown={(e) => {
185
- keyMap[e.key]?.(e)
186
- }}
187
- {...styles.arrowDown}
188
- onMouseOver={onOpen}
189
- tabIndex={0}
190
187
  />
191
188
  </PopoverTrigger>
192
189
 
@@ -194,11 +191,12 @@ const Header = ({
194
191
  {...styles.popoverContent}
195
192
  onMouseLeave={() => {
196
193
  hasEnterPopoverContent.current = false
197
- onClose()
194
+ onAccountMenuClose()
198
195
  }}
199
196
  onMouseOver={() => {
200
197
  hasEnterPopoverContent.current = true
201
198
  }}
199
+ {...getAccountMenuDisclosureProps()}
202
200
  >
203
201
  <PopoverArrow />
204
202
  <PopoverHeader>
@@ -62,11 +62,11 @@ test('renders Header', async () => {
62
62
  renderWithProviders(<Header />)
63
63
 
64
64
  await waitFor(() => {
65
- const menu = document.querySelector('button[aria-label="Menu"]')
66
- const logo = document.querySelector('button[aria-label="Logo"]')
67
- const account = document.querySelector('svg[aria-label="My account"]')
68
- const cart = document.querySelector('button[aria-label="My cart, number of items: 0"]')
69
- const wishlist = document.querySelector('button[aria-label="Wishlist"]')
65
+ const menu = screen.getByLabelText('Menu')
66
+ const logo = screen.getByLabelText('Logo')
67
+ const account = screen.getByLabelText('My account')
68
+ const cart = screen.getByLabelText('My cart, number of items: 0')
69
+ const wishlist = screen.getByLabelText('Wishlist')
70
70
  const searchInput = document.querySelector('input[type="search"]')
71
71
  expect(menu).toBeInTheDocument()
72
72
  expect(logo).toBeInTheDocument()
@@ -91,10 +91,10 @@ test('renders Header with event handlers', async () => {
91
91
  />
92
92
  )
93
93
  await waitFor(() => {
94
- const menu = document.querySelector('button[aria-label="Menu"]')
95
- const logo = document.querySelector('button[aria-label="Logo"]')
96
- const account = document.querySelector('svg[aria-label="My account"]')
97
- const cart = document.querySelector('button[aria-label="My cart, number of items: 0"]')
94
+ const menu = screen.getByLabelText('Menu')
95
+ const logo = screen.getByLabelText('Logo')
96
+ const account = screen.getByLabelText('My account')
97
+ const cart = screen.getByLabelText('My cart, number of items: 0')
98
98
  expect(menu).toBeInTheDocument()
99
99
  fireEvent.click(menu)
100
100
  expect(onMenuClick).toHaveBeenCalledTimes(1)
@@ -124,7 +124,7 @@ test.each(testBaskets)(
124
124
  renderWithProviders(<Header />)
125
125
 
126
126
  await waitFor(() => {
127
- const cart = document.querySelector('button[aria-label="My cart, number of items: 0"]')
127
+ const cart = screen.getByLabelText('My cart, number of items: 0')
128
128
  const badge = document.querySelector(
129
129
  'button[aria-label="My cart, number of items: 0"] .chakra-badge'
130
130
  )
@@ -141,9 +141,7 @@ test('renders cart badge when basket is loaded', async () => {
141
141
 
142
142
  await waitFor(() => {
143
143
  // Look for badge.
144
- const badge = document.querySelector(
145
- 'button[aria-label="My cart, number of items: 2"] .chakra-badge'
146
- )
144
+ const badge = screen.getByLabelText('My cart, number of items: 2')
147
145
  expect(badge).toBeInTheDocument()
148
146
  })
149
147
  })
@@ -156,10 +154,10 @@ test('route to account page when an authenticated users click on account icon',
156
154
 
157
155
  await waitFor(() => {
158
156
  // Look for account icon
159
- const accountTrigger = document.querySelector('svg[aria-label="My account trigger"]')
157
+ const accountTrigger = screen.getByLabelText('Open account menu')
160
158
  expect(accountTrigger).toBeInTheDocument()
161
159
  })
162
- const accountIcon = document.querySelector('svg[aria-label="My account"]')
160
+ const accountIcon = screen.getByLabelText('My account')
163
161
  fireEvent.click(accountIcon)
164
162
  await waitFor(() => {
165
163
  expect(history.push).toHaveBeenCalledWith(createPathWithDefaults('/account'))
@@ -181,7 +179,7 @@ test('route to wishlist page when an authenticated users click on wishlist icon'
181
179
 
182
180
  await waitFor(() => {
183
181
  // Look for account icon
184
- const accountTrigger = document.querySelector('svg[aria-label="My account trigger"]')
182
+ const accountTrigger = screen.getByLabelText('Open account menu')
185
183
  expect(accountTrigger).toBeInTheDocument()
186
184
  })
187
185
  const wishlistIcon = screen.getByRole('button', {name: /wishlist/i})
@@ -207,10 +205,10 @@ test('shows dropdown menu when an authenticated users hover on the account icon'
207
205
 
208
206
  await waitFor(() => {
209
207
  // Look for account icon
210
- const accountTrigger = document.querySelector('svg[aria-label="My account trigger"]')
208
+ const accountTrigger = screen.getByLabelText('Open account menu')
211
209
  expect(accountTrigger).toBeInTheDocument()
212
210
  })
213
- const accountIcon = document.querySelector('svg[aria-label="My account"]')
211
+ const accountIcon = screen.getByLabelText('My account')
214
212
  fireEvent.click(accountIcon)
215
213
  await waitFor(() => {
216
214
  expect(history.push).toHaveBeenCalledWith(createPathWithDefaults('/account'))
@@ -225,6 +225,7 @@ const ListMenu = ({root, maxColumns = MAXIMUM_NUMBER_COLUMNS}) => {
225
225
  const theme = useTheme()
226
226
  const {baseStyle} = theme.components.ListMenu
227
227
  const [ariaBusy, setAriaBusy] = useState(true)
228
+ const intl = useIntl()
228
229
 
229
230
  useEffect(() => {
230
231
  setAriaBusy(false)
@@ -233,7 +234,10 @@ const ListMenu = ({root, maxColumns = MAXIMUM_NUMBER_COLUMNS}) => {
233
234
  return (
234
235
  <nav
235
236
  id="list-menu"
236
- aria-label="main"
237
+ aria-label={intl.formatMessage({
238
+ id: 'list_menu.nav.assistive_msg',
239
+ defaultMessage: 'Main navigation'
240
+ })}
237
241
  aria-live="polite"
238
242
  aria-busy={ariaBusy}
239
243
  aria-atomic="true"
@@ -19,7 +19,7 @@ describe('ListMenu', () => {
19
19
  const categoryTrigger = screen.getByText(/Mens/i)
20
20
  await user.hover(categoryTrigger)
21
21
  expect(categoryTrigger).toBeInTheDocument()
22
- expect(screen.getByRole('navigation', {name: 'main'})).toBeInTheDocument()
22
+ expect(screen.getByRole('navigation', {name: 'Main navigation'})).toBeInTheDocument()
23
23
  const suit = screen.getByText(/suits/i)
24
24
  expect(suit).toBeInTheDocument()
25
25
  })
@@ -53,7 +53,10 @@ const Pagination = (props) => {
53
53
  // as intended, the workaround is to use the current url when its disabled.
54
54
  href={prev || currentURL}
55
55
  to={prev || currentURL}
56
- aria-label="Previous Page"
56
+ aria-label={intl.formatMessage({
57
+ id: 'pagination.link.prev.assistive_msg',
58
+ defaultMessage: 'Previous Page'
59
+ })}
57
60
  aria-disabled={!prev}
58
61
  variant="link"
59
62
  >
@@ -102,7 +105,10 @@ const Pagination = (props) => {
102
105
  // as intended, the workaround is to use the current url when its disabled.
103
106
  href={next || currentURL}
104
107
  to={next || currentURL}
105
- aria-label="Next Page"
108
+ aria-label={intl.formatMessage({
109
+ id: 'pagination.link.next.assistive_msg',
110
+ defaultMessage: 'Next Page'
111
+ })}
106
112
  aria-disabled={!next}
107
113
  variant="link"
108
114
  >
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2021, salesforce.com, inc.
2
+ * Copyright (c) 2023, Salesforce, Inc.
3
3
  * All rights reserved.
4
4
  * SPDX-License-Identifier: BSD-3-Clause
5
5
  * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
@@ -13,20 +13,31 @@ import {useLocation} from 'react-router-dom'
13
13
  import {useToast} from '@salesforce/retail-react-app/app/hooks/use-toast'
14
14
  import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer'
15
15
 
16
- const withRegistration = (Component) => {
16
+ /**
17
+ * Higher-order component that modifies the given component's `onClick` to show the login form if
18
+ * the user is not logged in.
19
+ * @param {React.Component} component Component to wrap.
20
+ * @param {(location: Location, locale: string) => boolean} [options.isLoginPage] Function that
21
+ * determines whether the current page is the "login" page, to avoid showing a duplicate modal.
22
+ * Defaults to all paths ending with "/login".
23
+ * @returns {React.Component} wrapped component
24
+ */
25
+ const withRegistration = (
26
+ Component,
27
+ {isLoginPage = (loc) => loc.pathname.endsWith('/login')} = {}
28
+ ) => {
17
29
  const WrappedComponent = ({onClick = noop, ...passThroughProps}) => {
18
30
  const {data: customer} = useCurrentCustomer()
19
31
  const authModal = useAuthModal()
32
+ const showToast = useToast()
20
33
  const location = useLocation()
21
34
  const {formatMessage, locale} = useIntl()
22
- const isLoginPage = new RegExp(`^/${locale}/login$`).test(location.pathname)
23
- const showToast = useToast()
24
35
 
25
36
  const handleClick = (e) => {
26
37
  e.preventDefault()
27
38
  if (!customer.isRegistered) {
28
39
  // Do not show auth modal if users is already on the login page
29
- if (isLoginPage) {
40
+ if (isLoginPage(location, locale)) {
30
41
  showToast({
31
42
  title: formatMessage({
32
43
  defaultMessage: 'Please sign in to continue!',
@@ -278,7 +278,12 @@ export const AuthModal = ({
278
278
  >
279
279
  <ModalOverlay />
280
280
  <ModalContent>
281
- <ModalCloseButton />
281
+ <ModalCloseButton
282
+ aria-label={formatMessage({
283
+ id: 'auth_modal.button.close.assistive_msg',
284
+ defaultMessage: 'Close login form'
285
+ })}
286
+ />
282
287
  <ModalBody pb={8} bg="white" paddingBottom={14} marginTop={14}>
283
288
  {!form.formState.isSubmitSuccessful && currentView === LOGIN_VIEW && (
284
289
  <LoginForm
@@ -18,6 +18,7 @@ import {
18
18
  import {Component, regionPropType} from '@salesforce/commerce-sdk-react/components'
19
19
  import {ChevronLeftIcon, ChevronRightIcon} from '@salesforce/retail-react-app/app/components/icons'
20
20
  import {useEffect} from 'react'
21
+ import {useIntl} from 'react-intl'
21
22
 
22
23
  /**
23
24
  * Display child components in a carousel slider manner. Configurations include the number of
@@ -37,6 +38,7 @@ import {useEffect} from 'react'
37
38
  * @returns {React.ReactElement} - Carousel component.
38
39
  */
39
40
  export const Carousel = (props = {}) => {
41
+ const intl = useIntl()
40
42
  const scrollRef = useRef()
41
43
  const breakpoint = useBreakpoint()
42
44
  const [hasOverflow, setHasOverflow] = useState(false)
@@ -172,11 +174,14 @@ export const Carousel = (props = {}) => {
172
174
  {/* boxShadow requires !important --> https://github.com/chakra-ui/chakra-ui/issues/3553 */}
173
175
  <IconButton
174
176
  data-testid="carousel-nav-left"
175
- aria-label="Scroll carousel left"
177
+ aria-label={intl.formatMessage({
178
+ id: 'carousel.button.scroll_left.assistive_msg',
179
+ defaultMessage: 'Scroll carousel left'
180
+ })}
176
181
  icon={<ChevronLeftIcon color="black" />}
177
182
  borderRadius="full"
178
183
  colorScheme="whiteAlpha"
179
- boxShadow={'0 3px 10px rgb(0 0 0 / 20%) !important'}
184
+ boxShadow="0 3px 10px rgb(0 0 0 / 20%) !important"
180
185
  onClick={() => scroll(-1)}
181
186
  />
182
187
  </Box>
@@ -191,11 +196,14 @@ export const Carousel = (props = {}) => {
191
196
  {/* boxShadow requires !important --> https://github.com/chakra-ui/chakra-ui/issues/3553 */}
192
197
  <IconButton
193
198
  data-testid="carousel-nav-right"
194
- aria-label="Scroll carousel right"
199
+ aria-label={intl.formatMessage({
200
+ id: 'carousel.button.scroll_right.assistive_msg',
201
+ defaultMessage: 'Scroll carousel right'
202
+ })}
195
203
  icon={<ChevronRightIcon color="black" />}
196
204
  borderRadius="full"
197
205
  colorScheme="whiteAlpha"
198
- boxShadow={'0 3px 10px rgb(0 0 0 / 20%) !important'}
206
+ boxShadow="0 3px 10px rgb(0 0 0 / 20%) !important"
199
207
  onClick={() => scroll(1)}
200
208
  />
201
209
  </Box>
@@ -1,66 +1,74 @@
1
1
  /*
2
- * Copyright (c) 2021, salesforce.com, inc.
2
+ * Copyright (c) 2023, Salesforce, Inc.
3
3
  * All rights reserved.
4
4
  * SPDX-License-Identifier: BSD-3-Clause
5
5
  * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
6
  */
7
7
 
8
- import React from 'react'
9
- import {
10
- Box,
11
- Text,
12
- Radio,
13
- RadioGroup,
14
- Stack
15
- } from '@salesforce/retail-react-app/app/components/shared/ui'
8
+ import React, {useRef} from 'react'
9
+ import {Box, Text, Radio, Stack} from '@salesforce/retail-react-app/app/components/shared/ui'
16
10
  import PropTypes from 'prop-types'
17
11
 
18
- const RadioRefinements = ({filter, toggleFilter, selectedFilters}) => {
12
+ const RadioRefinement = ({filter, value, toggleFilter, selectedFilters}) => {
13
+ const buttonRef = useRef()
14
+ // Because choosing a refinement is equivalent to a form submission, the best semantic choice
15
+ // for the refinement is a button or a link, rather than a radio input. The radio element here
16
+ // is purely for visual purposes, and should probably be replaced with a simple icon.
19
17
  return (
20
18
  <Box>
21
- <RadioGroup
22
- // The following `false` fallback is required to avoid the radio group
23
- // from switching to "uncontrolled mode" when `selectedFilters` is empty.
24
- value={selectedFilters[0] ?? false}
19
+ <Radio
20
+ display="inline-flex"
21
+ height={{base: '44px', lg: '24px'}}
22
+ isChecked={selectedFilters.includes(value.value)}
23
+ // Ideally, this "icon" would be part of the button, but doing so with a radio input
24
+ // triggers `onClick` twice. The radio must be separate, and therefore we must add
25
+ // these workarounds to prevent it from receiving focus.
26
+ inputProps={{'aria-hidden': true, tabIndex: -1}}
27
+ onClick={() => buttonRef.current?.click()}
28
+ />
29
+ <Text
30
+ ref={buttonRef}
31
+ ml={2}
32
+ as="button"
33
+ fontSize="sm"
34
+ onClick={() => toggleFilter(value, filter.attributeId, false, false)}
25
35
  >
26
- <Stack spacing={1}>
27
- {filter.values
28
- .filter((refinementValue) => refinementValue.hitCount > 0)
29
- .map((value) => {
30
- return (
31
- <Box key={value.value}>
32
- <Radio
33
- display="flex"
34
- alignItems="center"
35
- height={{base: '44px', lg: '24px'}}
36
- value={value.value}
37
- onChange={() =>
38
- toggleFilter(
39
- value,
40
- filter.attributeId,
41
- selectedFilters.includes(value.value),
42
- false
43
- )
44
- }
45
- fontSize="sm"
46
- >
47
- <Text marginLeft={-1} fontSize="sm">
48
- {value.label}
49
- </Text>
50
- </Radio>
51
- </Box>
52
- )
53
- })}
54
- </Stack>
55
- </RadioGroup>
36
+ {value.label}
37
+ </Text>
56
38
  </Box>
57
39
  )
58
40
  }
59
41
 
42
+ RadioRefinement.propTypes = {
43
+ filter: PropTypes.object,
44
+ value: PropTypes.object,
45
+ toggleFilter: PropTypes.func,
46
+ selectedFilters: PropTypes.arrayOf(PropTypes.object)
47
+ }
48
+
49
+ const RadioRefinements = ({filter, toggleFilter, selectedFilters}) => {
50
+ return (
51
+ <Stack spacing={1}>
52
+ {filter.values.map(
53
+ (value) =>
54
+ value.hitCount > 0 && (
55
+ <RadioRefinement
56
+ key={value.value}
57
+ value={value}
58
+ filter={filter}
59
+ toggleFilter={toggleFilter}
60
+ selectedFilters={selectedFilters}
61
+ />
62
+ )
63
+ )}
64
+ </Stack>
65
+ )
66
+ }
67
+
60
68
  RadioRefinements.propTypes = {
61
69
  filter: PropTypes.object,
62
70
  toggleFilter: PropTypes.func,
63
- selectedFilters: PropTypes.array
71
+ selectedFilters: PropTypes.arrayOf(PropTypes.object)
64
72
  }
65
73
 
66
74
  export default RadioRefinements
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2021, salesforce.com, inc.
2
+ * Copyright (c) 2023, Salesforce, Inc.
3
3
  * All rights reserved.
4
4
  * SPDX-License-Identifier: BSD-3-Clause
5
5
  * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
@@ -25,7 +25,8 @@ import LinkRefinements from '@salesforce/retail-react-app/app/pages/product-list
25
25
  import {isServer} from '@salesforce/retail-react-app/app/utils/utils'
26
26
  import {FILTER_ACCORDION_SATE} from '@salesforce/retail-react-app/app/constants'
27
27
 
28
- const componentMap = {
28
+ /** Map of refinement attribute IDs to the components used to display values as filter options. */
29
+ export const componentMap = {
29
30
  cgid: LinkRefinements,
30
31
  c_refinementColor: ColorRefinements,
31
32
  c_size: SizeRefinements,
@@ -355,6 +355,12 @@
355
355
  "value": "You Might Also Like"
356
356
  }
357
357
  ],
358
+ "auth_modal.button.close.assistive_msg": [
359
+ {
360
+ "type": 0,
361
+ "value": "Close login form"
362
+ }
363
+ ],
358
364
  "auth_modal.description.now_signed_in": [
359
365
  {
360
366
  "type": 0,
@@ -413,6 +419,18 @@
413
419
  "value": "Password Reset"
414
420
  }
415
421
  ],
422
+ "carousel.button.scroll_left.assistive_msg": [
423
+ {
424
+ "type": 0,
425
+ "value": "Scroll carousel left"
426
+ }
427
+ ],
428
+ "carousel.button.scroll_right.assistive_msg": [
429
+ {
430
+ "type": 0,
431
+ "value": "Scroll carousel right"
432
+ }
433
+ ],
416
434
  "cart.info.removed_from_cart": [
417
435
  {
418
436
  "type": 0,
@@ -1163,6 +1181,18 @@
1163
1181
  "value": "Top Sellers"
1164
1182
  }
1165
1183
  ],
1184
+ "field.password.assistive_msg.hide_password": [
1185
+ {
1186
+ "type": 0,
1187
+ "value": "Hide password"
1188
+ }
1189
+ ],
1190
+ "field.password.assistive_msg.show_password": [
1191
+ {
1192
+ "type": 0,
1193
+ "value": "Show password"
1194
+ }
1195
+ ],
1166
1196
  "footer.column.account": [
1167
1197
  {
1168
1198
  "type": 0,
@@ -1375,6 +1405,12 @@
1375
1405
  "value": "My account"
1376
1406
  }
1377
1407
  ],
1408
+ "header.button.assistive_msg.my_account_menu": [
1409
+ {
1410
+ "type": 0,
1411
+ "value": "Open account menu"
1412
+ }
1413
+ ],
1378
1414
  "header.button.assistive_msg.my_cart_with_num_items": [
1379
1415
  {
1380
1416
  "type": 0,
@@ -1613,6 +1649,12 @@
1613
1649
  "value": "Please select all your options above"
1614
1650
  }
1615
1651
  ],
1652
+ "list_menu.nav.assistive_msg": [
1653
+ {
1654
+ "type": 0,
1655
+ "value": "Main navigation"
1656
+ }
1657
+ ],
1616
1658
  "locale_text.message.ar-SA": [
1617
1659
  {
1618
1660
  "type": 0,
@@ -2123,12 +2165,24 @@
2123
2165
  "value": "Next"
2124
2166
  }
2125
2167
  ],
2168
+ "pagination.link.next.assistive_msg": [
2169
+ {
2170
+ "type": 0,
2171
+ "value": "Next Page"
2172
+ }
2173
+ ],
2126
2174
  "pagination.link.prev": [
2127
2175
  {
2128
2176
  "type": 0,
2129
2177
  "value": "Prev"
2130
2178
  }
2131
2179
  ],
2180
+ "pagination.link.prev.assistive_msg": [
2181
+ {
2182
+ "type": 0,
2183
+ "value": "Previous Page"
2184
+ }
2185
+ ],
2132
2186
  "password_card.info.password_updated": [
2133
2187
  {
2134
2188
  "type": 0,
@@ -355,6 +355,12 @@
355
355
  "value": "You Might Also Like"
356
356
  }
357
357
  ],
358
+ "auth_modal.button.close.assistive_msg": [
359
+ {
360
+ "type": 0,
361
+ "value": "Close login form"
362
+ }
363
+ ],
358
364
  "auth_modal.description.now_signed_in": [
359
365
  {
360
366
  "type": 0,
@@ -413,6 +419,18 @@
413
419
  "value": "Password Reset"
414
420
  }
415
421
  ],
422
+ "carousel.button.scroll_left.assistive_msg": [
423
+ {
424
+ "type": 0,
425
+ "value": "Scroll carousel left"
426
+ }
427
+ ],
428
+ "carousel.button.scroll_right.assistive_msg": [
429
+ {
430
+ "type": 0,
431
+ "value": "Scroll carousel right"
432
+ }
433
+ ],
416
434
  "cart.info.removed_from_cart": [
417
435
  {
418
436
  "type": 0,
@@ -1163,6 +1181,18 @@
1163
1181
  "value": "Top Sellers"
1164
1182
  }
1165
1183
  ],
1184
+ "field.password.assistive_msg.hide_password": [
1185
+ {
1186
+ "type": 0,
1187
+ "value": "Hide password"
1188
+ }
1189
+ ],
1190
+ "field.password.assistive_msg.show_password": [
1191
+ {
1192
+ "type": 0,
1193
+ "value": "Show password"
1194
+ }
1195
+ ],
1166
1196
  "footer.column.account": [
1167
1197
  {
1168
1198
  "type": 0,
@@ -1375,6 +1405,12 @@
1375
1405
  "value": "My account"
1376
1406
  }
1377
1407
  ],
1408
+ "header.button.assistive_msg.my_account_menu": [
1409
+ {
1410
+ "type": 0,
1411
+ "value": "Open account menu"
1412
+ }
1413
+ ],
1378
1414
  "header.button.assistive_msg.my_cart_with_num_items": [
1379
1415
  {
1380
1416
  "type": 0,
@@ -1613,6 +1649,12 @@
1613
1649
  "value": "Please select all your options above"
1614
1650
  }
1615
1651
  ],
1652
+ "list_menu.nav.assistive_msg": [
1653
+ {
1654
+ "type": 0,
1655
+ "value": "Main navigation"
1656
+ }
1657
+ ],
1616
1658
  "locale_text.message.ar-SA": [
1617
1659
  {
1618
1660
  "type": 0,
@@ -2123,12 +2165,24 @@
2123
2165
  "value": "Next"
2124
2166
  }
2125
2167
  ],
2168
+ "pagination.link.next.assistive_msg": [
2169
+ {
2170
+ "type": 0,
2171
+ "value": "Next Page"
2172
+ }
2173
+ ],
2126
2174
  "pagination.link.prev": [
2127
2175
  {
2128
2176
  "type": 0,
2129
2177
  "value": "Prev"
2130
2178
  }
2131
2179
  ],
2180
+ "pagination.link.prev.assistive_msg": [
2181
+ {
2182
+ "type": 0,
2183
+ "value": "Previous Page"
2184
+ }
2185
+ ],
2132
2186
  "password_card.info.password_updated": [
2133
2187
  {
2134
2188
  "type": 0,
@@ -739,6 +739,20 @@
739
739
  "value": "]"
740
740
  }
741
741
  ],
742
+ "auth_modal.button.close.assistive_msg": [
743
+ {
744
+ "type": 0,
745
+ "value": "["
746
+ },
747
+ {
748
+ "type": 0,
749
+ "value": "Ƈŀǿǿşḗḗ ŀǿǿɠīƞ ƒǿǿřḿ"
750
+ },
751
+ {
752
+ "type": 0,
753
+ "value": "]"
754
+ }
755
+ ],
742
756
  "auth_modal.description.now_signed_in": [
743
757
  {
744
758
  "type": 0,
@@ -845,6 +859,34 @@
845
859
  "value": "]"
846
860
  }
847
861
  ],
862
+ "carousel.button.scroll_left.assistive_msg": [
863
+ {
864
+ "type": 0,
865
+ "value": "["
866
+ },
867
+ {
868
+ "type": 0,
869
+ "value": "Şƈřǿǿŀŀ ƈȧȧřǿǿŭŭşḗḗŀ ŀḗḗƒŧ"
870
+ },
871
+ {
872
+ "type": 0,
873
+ "value": "]"
874
+ }
875
+ ],
876
+ "carousel.button.scroll_right.assistive_msg": [
877
+ {
878
+ "type": 0,
879
+ "value": "["
880
+ },
881
+ {
882
+ "type": 0,
883
+ "value": "Şƈřǿǿŀŀ ƈȧȧřǿǿŭŭşḗḗŀ řīɠħŧ"
884
+ },
885
+ {
886
+ "type": 0,
887
+ "value": "]"
888
+ }
889
+ ],
848
890
  "cart.info.removed_from_cart": [
849
891
  {
850
892
  "type": 0,
@@ -2411,6 +2453,34 @@
2411
2453
  "value": "]"
2412
2454
  }
2413
2455
  ],
2456
+ "field.password.assistive_msg.hide_password": [
2457
+ {
2458
+ "type": 0,
2459
+ "value": "["
2460
+ },
2461
+ {
2462
+ "type": 0,
2463
+ "value": "Ħīḓḗḗ ƥȧȧşşẇǿǿřḓ"
2464
+ },
2465
+ {
2466
+ "type": 0,
2467
+ "value": "]"
2468
+ }
2469
+ ],
2470
+ "field.password.assistive_msg.show_password": [
2471
+ {
2472
+ "type": 0,
2473
+ "value": "["
2474
+ },
2475
+ {
2476
+ "type": 0,
2477
+ "value": "Şħǿǿẇ ƥȧȧşşẇǿǿřḓ"
2478
+ },
2479
+ {
2480
+ "type": 0,
2481
+ "value": "]"
2482
+ }
2483
+ ],
2414
2484
  "footer.column.account": [
2415
2485
  {
2416
2486
  "type": 0,
@@ -2863,6 +2933,20 @@
2863
2933
  "value": "]"
2864
2934
  }
2865
2935
  ],
2936
+ "header.button.assistive_msg.my_account_menu": [
2937
+ {
2938
+ "type": 0,
2939
+ "value": "["
2940
+ },
2941
+ {
2942
+ "type": 0,
2943
+ "value": "Ǿƥḗḗƞ ȧȧƈƈǿǿŭŭƞŧ ḿḗḗƞŭŭ"
2944
+ },
2945
+ {
2946
+ "type": 0,
2947
+ "value": "]"
2948
+ }
2949
+ ],
2866
2950
  "header.button.assistive_msg.my_cart_with_num_items": [
2867
2951
  {
2868
2952
  "type": 0,
@@ -3397,6 +3481,20 @@
3397
3481
  "value": "]"
3398
3482
  }
3399
3483
  ],
3484
+ "list_menu.nav.assistive_msg": [
3485
+ {
3486
+ "type": 0,
3487
+ "value": "["
3488
+ },
3489
+ {
3490
+ "type": 0,
3491
+ "value": "Ḿȧȧīƞ ƞȧȧṽīɠȧȧŧīǿǿƞ"
3492
+ },
3493
+ {
3494
+ "type": 0,
3495
+ "value": "]"
3496
+ }
3497
+ ],
3400
3498
  "locale_text.message.ar-SA": [
3401
3499
  {
3402
3500
  "type": 0,
@@ -4531,6 +4629,20 @@
4531
4629
  "value": "]"
4532
4630
  }
4533
4631
  ],
4632
+ "pagination.link.next.assistive_msg": [
4633
+ {
4634
+ "type": 0,
4635
+ "value": "["
4636
+ },
4637
+ {
4638
+ "type": 0,
4639
+ "value": "Ƞḗḗẋŧ Ƥȧȧɠḗḗ"
4640
+ },
4641
+ {
4642
+ "type": 0,
4643
+ "value": "]"
4644
+ }
4645
+ ],
4534
4646
  "pagination.link.prev": [
4535
4647
  {
4536
4648
  "type": 0,
@@ -4545,6 +4657,20 @@
4545
4657
  "value": "]"
4546
4658
  }
4547
4659
  ],
4660
+ "pagination.link.prev.assistive_msg": [
4661
+ {
4662
+ "type": 0,
4663
+ "value": "["
4664
+ },
4665
+ {
4666
+ "type": 0,
4667
+ "value": "Ƥřḗḗṽīǿǿŭŭş Ƥȧȧɠḗḗ"
4668
+ },
4669
+ {
4670
+ "type": 0,
4671
+ "value": "]"
4672
+ }
4673
+ ],
4548
4674
  "password_card.info.password_updated": [
4549
4675
  {
4550
4676
  "type": 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/retail-react-app",
3
- "version": "2.2.0",
3
+ "version": "2.3.0-nightly-20231212080144",
4
4
  "license": "See license in LICENSE",
5
5
  "author": "cc-pwa-kit@salesforce.com",
6
6
  "ccExtensibility": {
@@ -45,10 +45,10 @@
45
45
  "@lhci/cli": "^0.11.0",
46
46
  "@loadable/component": "^5.15.3",
47
47
  "@peculiar/webcrypto": "^1.4.2",
48
- "@salesforce/commerce-sdk-react": "1.2.0",
49
- "@salesforce/pwa-kit-dev": "3.3.0",
50
- "@salesforce/pwa-kit-react-sdk": "3.3.0",
51
- "@salesforce/pwa-kit-runtime": "3.3.0",
48
+ "@salesforce/commerce-sdk-react": "1.3.0-nightly-20231212080144",
49
+ "@salesforce/pwa-kit-dev": "3.4.0-nightly-20231212080144",
50
+ "@salesforce/pwa-kit-react-sdk": "3.4.0-nightly-20231212080144",
51
+ "@salesforce/pwa-kit-runtime": "3.4.0-nightly-20231212080144",
52
52
  "@tanstack/react-query": "^4.28.0",
53
53
  "@tanstack/react-query-devtools": "^4.29.1",
54
54
  "@testing-library/dom": "^9.0.1",
@@ -100,5 +100,5 @@
100
100
  "maxSize": "320 kB"
101
101
  }
102
102
  ],
103
- "gitHead": "93894bb7d823f3510e10affdb1eb8e6750b25822"
103
+ "gitHead": "dbf0d4f2e45234cfc395f412a1fa94c364a877c2"
104
104
  }
@@ -144,6 +144,9 @@
144
144
  "add_to_cart_modal.recommended_products.title.might_also_like": {
145
145
  "defaultMessage": "You Might Also Like"
146
146
  },
147
+ "auth_modal.button.close.assistive_msg": {
148
+ "defaultMessage": "Close login form"
149
+ },
147
150
  "auth_modal.description.now_signed_in": {
148
151
  "defaultMessage": "You're now signed in."
149
152
  },
@@ -162,6 +165,12 @@
162
165
  "auth_modal.password_reset_success.title.password_reset": {
163
166
  "defaultMessage": "Password Reset"
164
167
  },
168
+ "carousel.button.scroll_left.assistive_msg": {
169
+ "defaultMessage": "Scroll carousel left"
170
+ },
171
+ "carousel.button.scroll_right.assistive_msg": {
172
+ "defaultMessage": "Scroll carousel right"
173
+ },
165
174
  "cart.info.removed_from_cart": {
166
175
  "defaultMessage": "Item removed from cart"
167
176
  },
@@ -471,6 +480,12 @@
471
480
  "empty_search_results.recommended_products.title.top_sellers": {
472
481
  "defaultMessage": "Top Sellers"
473
482
  },
483
+ "field.password.assistive_msg.hide_password": {
484
+ "defaultMessage": "Hide password"
485
+ },
486
+ "field.password.assistive_msg.show_password": {
487
+ "defaultMessage": "Show password"
488
+ },
474
489
  "footer.column.account": {
475
490
  "defaultMessage": "Account"
476
491
  },
@@ -561,6 +576,9 @@
561
576
  "header.button.assistive_msg.my_account": {
562
577
  "defaultMessage": "My account"
563
578
  },
579
+ "header.button.assistive_msg.my_account_menu": {
580
+ "defaultMessage": "Open account menu"
581
+ },
564
582
  "header.button.assistive_msg.my_cart_with_num_items": {
565
583
  "defaultMessage": "My cart, number of items: {numItems}"
566
584
  },
@@ -674,6 +692,9 @@
674
692
  "lCPCxk": {
675
693
  "defaultMessage": "Please select all your options above"
676
694
  },
695
+ "list_menu.nav.assistive_msg": {
696
+ "defaultMessage": "Main navigation"
697
+ },
677
698
  "locale_text.message.ar-SA": {
678
699
  "defaultMessage": "Arabic (Saudi Arabia)"
679
700
  },
@@ -909,9 +930,15 @@
909
930
  "pagination.link.next": {
910
931
  "defaultMessage": "Next"
911
932
  },
933
+ "pagination.link.next.assistive_msg": {
934
+ "defaultMessage": "Next Page"
935
+ },
912
936
  "pagination.link.prev": {
913
937
  "defaultMessage": "Prev"
914
938
  },
939
+ "pagination.link.prev.assistive_msg": {
940
+ "defaultMessage": "Previous Page"
941
+ },
915
942
  "password_card.info.password_updated": {
916
943
  "defaultMessage": "Password updated"
917
944
  },
@@ -144,6 +144,9 @@
144
144
  "add_to_cart_modal.recommended_products.title.might_also_like": {
145
145
  "defaultMessage": "You Might Also Like"
146
146
  },
147
+ "auth_modal.button.close.assistive_msg": {
148
+ "defaultMessage": "Close login form"
149
+ },
147
150
  "auth_modal.description.now_signed_in": {
148
151
  "defaultMessage": "You're now signed in."
149
152
  },
@@ -162,6 +165,12 @@
162
165
  "auth_modal.password_reset_success.title.password_reset": {
163
166
  "defaultMessage": "Password Reset"
164
167
  },
168
+ "carousel.button.scroll_left.assistive_msg": {
169
+ "defaultMessage": "Scroll carousel left"
170
+ },
171
+ "carousel.button.scroll_right.assistive_msg": {
172
+ "defaultMessage": "Scroll carousel right"
173
+ },
165
174
  "cart.info.removed_from_cart": {
166
175
  "defaultMessage": "Item removed from cart"
167
176
  },
@@ -471,6 +480,12 @@
471
480
  "empty_search_results.recommended_products.title.top_sellers": {
472
481
  "defaultMessage": "Top Sellers"
473
482
  },
483
+ "field.password.assistive_msg.hide_password": {
484
+ "defaultMessage": "Hide password"
485
+ },
486
+ "field.password.assistive_msg.show_password": {
487
+ "defaultMessage": "Show password"
488
+ },
474
489
  "footer.column.account": {
475
490
  "defaultMessage": "Account"
476
491
  },
@@ -561,6 +576,9 @@
561
576
  "header.button.assistive_msg.my_account": {
562
577
  "defaultMessage": "My account"
563
578
  },
579
+ "header.button.assistive_msg.my_account_menu": {
580
+ "defaultMessage": "Open account menu"
581
+ },
564
582
  "header.button.assistive_msg.my_cart_with_num_items": {
565
583
  "defaultMessage": "My cart, number of items: {numItems}"
566
584
  },
@@ -674,6 +692,9 @@
674
692
  "lCPCxk": {
675
693
  "defaultMessage": "Please select all your options above"
676
694
  },
695
+ "list_menu.nav.assistive_msg": {
696
+ "defaultMessage": "Main navigation"
697
+ },
677
698
  "locale_text.message.ar-SA": {
678
699
  "defaultMessage": "Arabic (Saudi Arabia)"
679
700
  },
@@ -909,9 +930,15 @@
909
930
  "pagination.link.next": {
910
931
  "defaultMessage": "Next"
911
932
  },
933
+ "pagination.link.next.assistive_msg": {
934
+ "defaultMessage": "Next Page"
935
+ },
912
936
  "pagination.link.prev": {
913
937
  "defaultMessage": "Prev"
914
938
  },
939
+ "pagination.link.prev.assistive_msg": {
940
+ "defaultMessage": "Previous Page"
941
+ },
915
942
  "password_card.info.password_updated": {
916
943
  "defaultMessage": "Password updated"
917
944
  },