@salesforce/retail-react-app 2.2.0-preview.0 → 2.2.0

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,8 +1,9 @@
1
- ## v2.2.0-dev (Nov 3, 2023)
1
+ ## v2.2.0 (Nov 8, 2023)
2
2
 
3
3
  ### Accessibility Improvements
4
4
 
5
5
  <!-- Order by Pull Request ID! -->
6
+ - Ensure the ListMenuTrigger component applies ARIA attributes to the correct element for the trigger icon [#1600](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1600)
6
7
  - Ensure form fields and icons have accessible labels [#1526](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1526)
7
8
  - Ensure active user interface components have sufficient contrast [#1534](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1534)
8
9
  - Fix outline on keyboard focus [#1536](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1536/files)
@@ -11,6 +12,7 @@
11
12
  - Make security code tooltip receive keyboard focus [#1551](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1551)
12
13
  - Improve accessibility of quantity picker [#1552](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1552)
13
14
  - Improve keyboard accessibility of product scroller [#1559](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1559)
15
+ - Fix focus indicator for hero features links on homepage [#1561](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1561)
14
16
  - Ensure color is not the sole means of communicating information [#1570](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1570)
15
17
 
16
18
  ### Other Features
@@ -18,11 +20,15 @@
18
20
  - 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)
19
21
  - Replace max-age with s-maxage to only cache shared caches [#1564](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1564)
20
22
  - Implement gift option for basket [#1546](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1546)
23
+ - Update `extract-default-messages` script to support multiple locales [#1574](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1574)
24
+ - Update engine compatibility to include npm 10 [#1597](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1597)
25
+ - Add support for localization in icon component [#1609](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1609)
21
26
 
22
27
  ### Bug Fixes
23
28
 
24
29
  - Remove internal linter rule that is missing in generated projects [#1554](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1554)
25
30
  - Fix bug where you can add duplicates of the same item to the wishlist. Also fixes bug where skeleton appears when removing last item from the wishlist. [#1560](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1560)
31
+ - Replace max-age with s-maxage to only cache shared caches [#1564](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1564)
26
32
  - Fix PLP filters for mobile [#1565](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/1565)
27
33
 
28
34
  ## v2.1.1 (Nov 7, 2023)
@@ -5,21 +5,24 @@
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
  import React from 'react'
8
- import {screen} from '@testing-library/react'
9
- import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
10
8
  import Error from '@salesforce/retail-react-app/app/components/_error/index'
9
+ // !!! ----- WARNING ----- WARNING ----- WARNING ----- !!!
10
+ // Tests use render instead of renderWithProviders because
11
+ // error component is rendered outside provider tree
12
+ // !!! ----------------------------------------------- !!!
13
+ import {screen, render} from '@testing-library/react'
11
14
 
12
15
  test('Error renders without errors', () => {
13
- expect(renderWithProviders(<Error />)).toBeDefined()
16
+ expect(render(<Error />)).toBeDefined()
14
17
  })
15
18
 
16
19
  test('Error status 500', () => {
17
- renderWithProviders(<Error status={500} />)
20
+ render(<Error status={500} />)
18
21
  expect(screen.getByRole('heading', {level: 2})).toHaveTextContent("This page isn't working")
19
22
  })
20
23
 
21
24
  test('Error status 500 with stack trace', () => {
22
- renderWithProviders(<Error status={500} stack={'Stack trace error message'} />)
25
+ render(<Error status={500} stack={'Stack trace error message'} />)
23
26
  expect(screen.getByRole('heading', {level: 2})).toHaveTextContent("This page isn't working")
24
27
  expect(screen.getByText(/stack trace error message/i)).toBeInTheDocument()
25
28
  })
@@ -4,8 +4,9 @@
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
- import React, {forwardRef} from 'react'
8
- import {useIntl, defineMessage} from 'react-intl'
7
+ import React, {forwardRef, useContext} from 'react'
8
+ import {defineMessage, IntlContext} from 'react-intl'
9
+ import PropTypes from 'prop-types'
9
10
  import {Icon, useTheme} from '@salesforce/retail-react-app/app/components/shared/ui'
10
11
 
11
12
  // Our own SVG imports. These will be extracted to a single sprite sheet by the
@@ -85,7 +86,9 @@ VisaSymbol.viewBox = VisaSymbol.viewBox || '0 0 38 22'
85
86
  * @param {string} name - the filename of the imported svg (does not include extension)
86
87
  * @param {Object} passProps - props that will be passed onto the underlying Icon component
87
88
  * @param {Object} localizationAttributes - attributes with localized values that will be passed
88
- * onto the underlying Icon component, use `defineMessage` to create localized string
89
+ * onto the underlying Icon component, use `defineMessage` to create localized string.
90
+ * Additionally, if the icon is rendered outside the provider tree, you'll also need to
91
+ * pass an intl object from react-intl as a prop to translate the messages.
89
92
  */
90
93
  /* istanbul ignore next */
91
94
  export const icon = (name, passProps, localizationAttributes) => {
@@ -95,8 +98,21 @@ export const icon = (name, passProps, localizationAttributes) => {
95
98
  .replace(/-/g, '')
96
99
  const component = forwardRef((props, ref) => {
97
100
  const theme = useTheme()
98
- const intl = useIntl()
101
+ // NOTE: We want to avoid `useIntl` here because that throws when <IntlProvider> is not in
102
+ // the component ancestry, but we only enforce `intl` if we have `localizationAttributes`.
103
+ let intl = useContext(IntlContext)
99
104
  if (localizationAttributes) {
105
+ if (props?.intl) {
106
+ const {intl: intlProp, ...otherProps} = props
107
+ // Allow `props.intl` to take precedence over the intl we found
108
+ intl = intlProp
109
+ props = otherProps
110
+ }
111
+ if (!intl) {
112
+ throw new Error(
113
+ 'To localize messages, you must either have <IntlProvider> in the component ancestry or provide `intl` as a prop'
114
+ )
115
+ }
100
116
  Object.keys(localizationAttributes).forEach((key) => {
101
117
  passProps[key] = intl.formatMessage(localizationAttributes[key])
102
118
  })
@@ -108,6 +124,11 @@ export const icon = (name, passProps, localizationAttributes) => {
108
124
  </Icon>
109
125
  )
110
126
  })
127
+
128
+ component.propTypes = {
129
+ intl: PropTypes.object
130
+ }
131
+
111
132
  component.displayName = `${displayName}Icon`
112
133
  return component
113
134
  }
@@ -5,10 +5,21 @@
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
  import React from 'react'
8
+ import {useIntl} from 'react-intl'
8
9
  import {within} from '@testing-library/dom'
10
+ import {render} from '@testing-library/react'
9
11
  import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
10
12
  import * as Icons from '@salesforce/retail-react-app/app/components/icons/index'
11
13
 
14
+ jest.mock('react-intl', () => ({
15
+ ...jest.requireActual('react-intl'),
16
+ useIntl: jest.fn()
17
+ }))
18
+
19
+ beforeEach(() => {
20
+ jest.clearAllMocks()
21
+ })
22
+
12
23
  test('renders svg icons with Chakra Icon component', () => {
13
24
  renderWithProviders(<Icons.CheckIcon />)
14
25
  const svg = document.querySelector('.chakra-icon')
@@ -18,3 +29,30 @@ test('renders svg icons with Chakra Icon component', () => {
18
29
  expect(svg).toHaveAttribute('viewBox', '0 0 24 24')
19
30
  expect(use).toHaveAttribute('xlink:href', '#check')
20
31
  })
32
+
33
+ test('uses intl from context when rendered with providers', () => {
34
+ renderWithProviders(<Icons.LockIcon />)
35
+ expect(useIntl).toHaveBeenCalled()
36
+ })
37
+
38
+ // the icon component can exist outside the provider tree via the error component
39
+ // therefore we cannot use the useIntl hook because the <IntlProvider> component
40
+ // will not exist in the component tree, so we pass the intl object as a prop
41
+ test('uses intl from props when rendered outside provider tree', () => {
42
+ const mockIntl = {
43
+ formatMessage: jest.fn()
44
+ }
45
+
46
+ // render without providers
47
+ render(<Icons.LockIcon intl={mockIntl} />)
48
+
49
+ expect(mockIntl.formatMessage).toHaveBeenCalled()
50
+ expect(useIntl).not.toHaveBeenCalled()
51
+ })
52
+
53
+ test('throws error when rendered outside provider tree and no intl prop is passed', async () => {
54
+ const errorMsg =
55
+ 'To localize messages, you must either have <IntlProvider> in the component ancestry or provide `intl` as a prop'
56
+ // render without providers
57
+ expect(() => render(<Icons.LockIcon />)).toThrow(errorMsg)
58
+ })
@@ -69,21 +69,21 @@ const ListMenuTrigger = ({item, name, isOpen, onOpen, onClose, hasItems}) => {
69
69
  {name}
70
70
  </Link>
71
71
 
72
- <Link
73
- as={RouteLink}
74
- to={'#'}
75
- onMouseOver={onOpen}
76
- onKeyDown={(e) => {
77
- keyMap[e.key]?.(e)
78
- }}
79
- {...baseStyle.listMenuTriggerLinkIcon}
80
- >
81
- <PopoverTrigger>
72
+ <PopoverTrigger>
73
+ <Link
74
+ as={RouteLink}
75
+ to={'#'}
76
+ onMouseOver={onOpen}
77
+ onKeyDown={(e) => {
78
+ keyMap[e.key]?.(e)
79
+ }}
80
+ {...baseStyle.listMenuTriggerLinkIcon}
81
+ >
82
82
  <Fade in={hasItems}>
83
83
  <ChevronIconTrigger {...baseStyle.selectedButtonIcon} />
84
84
  </Fade>
85
- </PopoverTrigger>
86
- </Link>
85
+ </Link>
86
+ </PopoverTrigger>
87
87
  </Box>
88
88
  )
89
89
  }
@@ -16,7 +16,6 @@ import {
16
16
  PopoverContent,
17
17
  PopoverHeader,
18
18
  PopoverTrigger,
19
- Portal,
20
19
  Text
21
20
  } from '@salesforce/retail-react-app/app/components/shared/ui'
22
21
  import {InfoIcon} from '@salesforce/retail-react-app/app/components/icons'
@@ -7,7 +7,6 @@
7
7
  import React from 'react'
8
8
  import PropTypes from 'prop-types'
9
9
  import {
10
- Box,
11
10
  Button,
12
11
  ButtonGroup,
13
12
  Checkbox,
@@ -130,13 +130,12 @@ const Home = () => {
130
130
  {heroFeatures.map((feature, index) => {
131
131
  const featureMessage = feature.message
132
132
  return (
133
- <Box
134
- key={index}
135
- background={'white'}
136
- boxShadow={'0px 2px 2px rgba(0, 0, 0, 0.1)'}
137
- borderRadius={'4px'}
138
- >
139
- <Link target="_blank" href={feature.href}>
133
+ <Link key={index} target="_blank" href={feature.href}>
134
+ <Box
135
+ background={'white'}
136
+ boxShadow="0px 2px 2px rgba(0, 0, 0, 0.1)"
137
+ borderRadius={'4px'}
138
+ >
140
139
  <HStack>
141
140
  <Flex
142
141
  paddingLeft={6}
@@ -150,8 +149,8 @@ const Home = () => {
150
149
  {intl.formatMessage(featureMessage.title)}
151
150
  </Text>
152
151
  </HStack>
153
- </Link>
154
- </Box>
152
+ </Box>
153
+ </Link>
155
154
  )
156
155
  })}
157
156
  </SimpleGrid>
@@ -1339,6 +1339,12 @@
1339
1339
  "value": " added to wishlist"
1340
1340
  }
1341
1341
  ],
1342
+ "global.info.already_in_wishlist": [
1343
+ {
1344
+ "type": 0,
1345
+ "value": "Item is already in wishlist"
1346
+ }
1347
+ ],
1342
1348
  "global.info.removed_from_wishlist": [
1343
1349
  {
1344
1350
  "type": 0,
@@ -2327,7 +2333,7 @@
2327
2333
  "value": " to wishlist"
2328
2334
  }
2329
2335
  ],
2330
- "product_tile.assistive_msg.remove_from wishlist": [
2336
+ "product_tile.assistive_msg.remove_from_wishlist": [
2331
2337
  {
2332
2338
  "type": 0,
2333
2339
  "value": "Remove "
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/retail-react-app",
3
- "version": "2.2.0-preview.0",
3
+ "version": "2.2.0",
4
4
  "license": "See license in LICENSE",
5
5
  "author": "cc-pwa-kit@salesforce.com",
6
6
  "ccExtensibility": {
@@ -14,7 +14,7 @@
14
14
  "build-translations": "npm run extract-default-translations && npm run compile-translations && npm run compile-translations:pseudo",
15
15
  "compile-translations": "node ./scripts/translations/compile-folder.js translations",
16
16
  "compile-translations:pseudo": "node ./scripts/translations/compile-pseudo.js translations/en-US.json",
17
- "extract-default-translations": "node ./scripts/translations/extract-default-messages.js en-US",
17
+ "extract-default-translations": "node ./scripts/translations/extract-default-messages.js en-US en-GB",
18
18
  "format": "pwa-kit-dev format \"**/*.{js,jsx}\"",
19
19
  "lint": "pwa-kit-dev lint \"**/*.{js,jsx}\"",
20
20
  "lint:fix": "npm run lint -- --fix",
@@ -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-preview.0",
49
- "@salesforce/pwa-kit-dev": "3.3.0-preview.0",
50
- "@salesforce/pwa-kit-react-sdk": "3.3.0-preview.0",
51
- "@salesforce/pwa-kit-runtime": "3.3.0-preview.0",
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",
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",
@@ -88,7 +88,7 @@
88
88
  },
89
89
  "engines": {
90
90
  "node": "^16.11.0 || ^18.0.0",
91
- "npm": "^8.0.0 || ^9.0.0"
91
+ "npm": "^8.0.0 || ^9.0.0 || ^10.0.0"
92
92
  },
93
93
  "bundlesize": [
94
94
  {
@@ -100,5 +100,5 @@
100
100
  "maxSize": "320 kB"
101
101
  }
102
102
  ],
103
- "gitHead": "e96474c4e78bf0ab74c44fc6647f5614db71d769"
103
+ "gitHead": "93894bb7d823f3510e10affdb1eb8e6750b25822"
104
104
  }
@@ -16,77 +16,68 @@ const fs = require('fs')
16
16
  const path = require('path')
17
17
  const packagePath = path.join(process.cwd(), 'package.json')
18
18
  const pkgJSON = JSON.parse(fs.readFileSync(packagePath))
19
- const locale = process.argv[2]
20
19
 
21
20
  const getAllFilesByExtensions = (dirPath, arrayOfFiles = [], extensions = []) => {
22
- const files = fs.readdirSync(dirPath)
21
+ const files = fs.readdirSync(dirPath, {withFileTypes: true})
23
22
 
24
23
  files.forEach(function (file) {
25
- if (fs.statSync(path.join(dirPath, file)).isDirectory()) {
26
- arrayOfFiles = getAllFilesByExtensions(
27
- path.join(dirPath, file),
28
- arrayOfFiles,
29
- extensions
30
- )
31
- } else {
32
- arrayOfFiles.push(path.join(dirPath, file))
24
+ const filePath = path.join(dirPath, file.name)
25
+ if (file.isDirectory()) {
26
+ arrayOfFiles = getAllFilesByExtensions(filePath, arrayOfFiles, extensions)
27
+ } else if (extensions.length === 0 || extensions.includes(path.extname(filePath))) {
28
+ arrayOfFiles.push(filePath)
33
29
  }
34
30
  })
35
- if (extensions.length) {
36
- return arrayOfFiles.filter((filePath) => {
37
- const getExtension = path.extname(filePath).replace('.', '')
38
- return extensions.includes(getExtension)
39
- })
40
- }
41
31
 
42
32
  return arrayOfFiles
43
33
  }
44
34
 
45
- try {
46
- const overridesDir = pkgJSON.ccExtensibility?.overridesDir
35
+ function extract(locale) {
36
+ // `extends` is a reserved word (`class A extends B {}`)
37
+ const {extends: extendsPkg, overridesDir} = pkgJSON.ccExtensibility || {}
47
38
  if (!overridesDir) {
48
- const command = `formatjs extract "app/**/*.{js,jsx,ts,tsx}" --out-file translations/${locale}.json --id-interpolation-pattern [sha512:contenthash:base64:6]`
39
+ const command = [
40
+ 'formatjs extract "app/**/*.{js,jsx,ts,tsx}"',
41
+ `--out-file translations/${locale}.json`,
42
+ '--id-interpolation-pattern [sha512:contenthash:base64:6]'
43
+ ].join(' ')
49
44
  exec(command, (err) => {
50
45
  if (err) {
51
46
  console.error(err)
52
47
  }
53
48
  })
54
49
  } else {
55
- const overridesPath = path.join(process.cwd(), pkgJSON.ccExtensibility?.overridesDir)
50
+ const overridesPath = path.join(process.cwd(), overridesDir)
56
51
  // get all the files in extended app
57
52
  const files = getAllFilesByExtensions(
58
53
  path.join(overridesPath, 'app'),
59
54
  [],
60
- ['js', 'jsx', 'ts', 'tsx']
55
+ ['.js', '.jsx', '.ts', '.tsx']
61
56
  )
62
57
  // get the file names that are overridden in base template
63
58
  const overriddenFiles = files
64
- .map((path) => {
65
- const replacedPath = path.replace(
66
- overridesDir,
67
- `node_modules/${pkgJSON.ccExtensibility?.extends}`
68
- )
69
- // check if this file does exist in base template
70
- const isFileExist = fs.existsSync(replacedPath)
71
- return isFileExist ? replacedPath : ''
72
- })
73
- .filter(Boolean)
74
- // rename the files needs to ignore to have .ignore extensions so it won't be recognized by formatjs
75
- overriddenFiles.forEach((filePath) => {
76
- fs.rename(filePath, `${filePath}.ignore`, (err) => err && console.error(err))
77
- })
78
-
79
- const extractCommand = `formatjs extract "./node_modules/${pkgJSON.ccExtensibility?.extends}/app/**/*.{js,jsx,ts,tsx}" "${pkgJSON.ccExtensibility?.overridesDir}/app/**/*.{js,jsx,ts,tsx}" --out-file translations/${locale}.json --id-interpolation-pattern [sha512:contenthash:base64:6]`
59
+ .map((path) => path.replace(overridesDir, `node_modules/${extendsPkg}`))
60
+ .filter((file) => fs.existsSync(file))
61
+ const extractCommand = [
62
+ 'formatjs extract',
63
+ '"./node_modules/${extendsPkg}/app/**/*.{js,jsx,ts,tsx}"',
64
+ '"${overridesDir}/app/**/*.{js,jsx,ts,tsx}"',
65
+ `--out-file translations/${locale}.json`,
66
+ '--id-interpolation-pattern [sha512:contenthash:base64:6]',
67
+ '--ignore',
68
+ ...overriddenFiles.map((file) => `'${file}'`)
69
+ ].join(' ')
80
70
  exec(extractCommand, (err) => {
81
71
  if (err) {
82
72
  console.error(err)
83
73
  }
84
- // restore file names
85
- overriddenFiles.forEach((filePath) => {
86
- fs.rename(`${filePath}.ignore`, filePath, (err) => err && console.error(err))
87
- })
88
74
  })
89
75
  }
76
+ }
77
+
78
+ try {
79
+ // example usage: node ./scripts/translations/extract-default-messages en-US en-GB
80
+ process.argv.slice(2).forEach(extract)
90
81
  } catch (error) {
91
82
  console.error(error)
92
83
  }
@@ -543,6 +543,9 @@
543
543
  "global.info.added_to_wishlist": {
544
544
  "defaultMessage": "{quantity} {quantity, plural, one {item} other {items}} added to wishlist"
545
545
  },
546
+ "global.info.already_in_wishlist": {
547
+ "defaultMessage": "Item is already in wishlist"
548
+ },
546
549
  "global.info.removed_from_wishlist": {
547
550
  "defaultMessage": "Item removed from wishlist"
548
551
  },
@@ -1005,7 +1008,7 @@
1005
1008
  "product_tile.assistive_msg.add_to_wishlist": {
1006
1009
  "defaultMessage": "Add {product} to wishlist"
1007
1010
  },
1008
- "product_tile.assistive_msg.remove_from wishlist": {
1011
+ "product_tile.assistive_msg.remove_from_wishlist": {
1009
1012
  "defaultMessage": "Remove {product} from wishlist"
1010
1013
  },
1011
1014
  "product_tile.label.starting_at_price": {