@telus-uds/components-base 1.80.1 → 1.82.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +30 -2
  2. package/lib/Autocomplete/Autocomplete.js +3 -1
  3. package/lib/Badge/Badge.js +1 -10
  4. package/lib/Fieldset/Fieldset.js +3 -1
  5. package/lib/Fieldset/FieldsetContainer.js +9 -2
  6. package/lib/Fieldset/FieldsetContainer.native.js +7 -1
  7. package/lib/Icon/Icon.js +24 -2
  8. package/lib/Listbox/Listbox.js +7 -1
  9. package/lib/Modal/Modal.js +40 -4
  10. package/lib/Modal/WebModal.js +73 -0
  11. package/lib/PriceLockup/PriceLockup.js +4 -1
  12. package/lib/PriceLockup/utils/renderFootnoteContent.js +2 -2
  13. package/lib/PriceLockup/utils/renderFootnoteLinks.js +2 -2
  14. package/lib/PriceLockup/utils/renderPrice.js +2 -2
  15. package/lib/PriceLockup/utils/renderTypography.js +1 -1
  16. package/lib/ProductCard/ProductCard.js +238 -0
  17. package/lib/ProductCard/dictionary.js +45 -0
  18. package/lib/ProductCard/index.js +10 -0
  19. package/lib/ProductCardGroup/ProductCardGroup.js +79 -0
  20. package/lib/ProductCardGroup/index.js +10 -0
  21. package/lib/TextInput/TextInput.js +1 -0
  22. package/lib/TextInput/TextInputBase.js +4 -1
  23. package/lib/index.js +16 -0
  24. package/lib/utils/ssr-media-query/create-stylesheet/index.js +2 -1
  25. package/lib/utils/ssr-media-query/utils/common.js +21 -2
  26. package/lib-module/Autocomplete/Autocomplete.js +3 -1
  27. package/lib-module/Badge/Badge.js +1 -10
  28. package/lib-module/Fieldset/Fieldset.js +3 -1
  29. package/lib-module/Fieldset/FieldsetContainer.js +7 -2
  30. package/lib-module/Fieldset/FieldsetContainer.native.js +10 -3
  31. package/lib-module/Icon/Icon.js +24 -2
  32. package/lib-module/Listbox/Listbox.js +7 -1
  33. package/lib-module/Modal/Modal.js +42 -5
  34. package/lib-module/Modal/WebModal.js +65 -0
  35. package/lib-module/PriceLockup/PriceLockup.js +4 -1
  36. package/lib-module/PriceLockup/utils/renderFootnoteContent.js +2 -2
  37. package/lib-module/PriceLockup/utils/renderFootnoteLinks.js +2 -2
  38. package/lib-module/PriceLockup/utils/renderPrice.js +2 -2
  39. package/lib-module/PriceLockup/utils/renderTypography.js +1 -1
  40. package/lib-module/ProductCard/ProductCard.js +231 -0
  41. package/lib-module/ProductCard/dictionary.js +38 -0
  42. package/lib-module/ProductCard/index.js +2 -0
  43. package/lib-module/ProductCardGroup/ProductCardGroup.js +69 -0
  44. package/lib-module/ProductCardGroup/index.js +2 -0
  45. package/lib-module/TextInput/TextInput.js +1 -0
  46. package/lib-module/TextInput/TextInputBase.js +4 -1
  47. package/lib-module/index.js +2 -0
  48. package/lib-module/utils/ssr-media-query/create-stylesheet/index.js +3 -2
  49. package/lib-module/utils/ssr-media-query/utils/common.js +19 -1
  50. package/package.json +2 -2
  51. package/src/Autocomplete/Autocomplete.jsx +4 -1
  52. package/src/Badge/Badge.jsx +7 -10
  53. package/src/Fieldset/Fieldset.jsx +3 -1
  54. package/src/Fieldset/FieldsetContainer.jsx +8 -1
  55. package/src/Fieldset/FieldsetContainer.native.jsx +7 -2
  56. package/src/Icon/Icon.jsx +30 -2
  57. package/src/Listbox/Listbox.jsx +6 -1
  58. package/src/Modal/Modal.jsx +42 -3
  59. package/src/Modal/WebModal.jsx +60 -0
  60. package/src/PriceLockup/PriceLockup.jsx +8 -2
  61. package/src/PriceLockup/utils/renderFootnoteContent.jsx +2 -2
  62. package/src/PriceLockup/utils/renderFootnoteLinks.jsx +2 -2
  63. package/src/PriceLockup/utils/renderPrice.jsx +2 -2
  64. package/src/PriceLockup/utils/renderTypography.jsx +1 -1
  65. package/src/ProductCard/ProductCard.jsx +193 -0
  66. package/src/ProductCard/dictionary.js +38 -0
  67. package/src/ProductCard/index.js +3 -0
  68. package/src/ProductCardGroup/ProductCardGroup.jsx +75 -0
  69. package/src/ProductCardGroup/index.js +3 -0
  70. package/src/TextInput/TextInput.jsx +1 -0
  71. package/src/TextInput/TextInputBase.jsx +4 -1
  72. package/src/index.js +2 -0
  73. package/src/utils/ssr-media-query/create-stylesheet/index.js +3 -2
  74. package/src/utils/ssr-media-query/utils/common.js +19 -1
@@ -0,0 +1,193 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { StyleSheet, View, Image } from 'react-native'
4
+ import { useViewport } from '../ViewportProvider'
5
+ import { useThemeTokens } from '../ThemeProvider'
6
+ import defaultDictionary from './dictionary'
7
+ import {
8
+ selectSystemProps,
9
+ getTokensPropType,
10
+ htmlAttrs,
11
+ viewProps,
12
+ useInputValue,
13
+ useCopy,
14
+ a11yProps
15
+ } from '../utils'
16
+
17
+ import Badge from '../Badge'
18
+ import PriceLockup from '../PriceLockup'
19
+ import Typography from '../Typography'
20
+ import { Button } from '../Button'
21
+ import StackView from '../StackView'
22
+ import Box from '../Box'
23
+ import Icon from '../Icon'
24
+
25
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs, viewProps, a11yProps])
26
+
27
+ const selectProductCardTokens = ({
28
+ borderStyle,
29
+ borderColor,
30
+ borderWidth,
31
+ borderRadius,
32
+ paddingHorizontal,
33
+ paddingVertical
34
+ }) => ({
35
+ borderStyle,
36
+ borderColor,
37
+ borderWidth,
38
+ borderRadius,
39
+ paddingHorizontal,
40
+ paddingVertical
41
+ })
42
+
43
+ const ProductCard = ({
44
+ copy = 'en',
45
+ dictionary = defaultDictionary,
46
+ image = {
47
+ src: '',
48
+ alt: ''
49
+ },
50
+ cardId,
51
+ isSelected,
52
+ onSelect,
53
+ tokens,
54
+ ...rest
55
+ }) => {
56
+ const viewport = useViewport()
57
+ const themeTokens = useThemeTokens('ProductCard', tokens, { viewport })
58
+
59
+ const getCopy = useCopy({ copy, dictionary })
60
+
61
+ const { currentValue, setValue } = useInputValue()
62
+ const hasClicked = isSelected || currentValue
63
+
64
+ const handlePress = (event) => {
65
+ if (cardId) {
66
+ onSelect(cardId)
67
+ } else {
68
+ setValue(!currentValue, event)
69
+ }
70
+ }
71
+
72
+ const renderButton = hasClicked ? (
73
+ <Box vertical={1}>
74
+ <StackView space={2} direction="row">
75
+ <Icon icon={themeTokens.selectedButtonIcon} variant={{ color: 'success' }} />
76
+ <Typography variant={{ size: 'h4' }} tokens={{ fontWeight: 400 }}>
77
+ {getCopy('selectedButtonLabel')}
78
+ </Typography>
79
+ </StackView>
80
+ </Box>
81
+ ) : (
82
+ <Button onPress={handlePress} variant={{ purpose: 'primary', size: 'small', width: 'full' }}>
83
+ {getCopy('buttonLabel')}
84
+ </Button>
85
+ )
86
+
87
+ return (
88
+ <View
89
+ {...selectProps(rest)}
90
+ style={[selectProductCardTokens(themeTokens), staticStyles.container]}
91
+ >
92
+ {image?.src ? (
93
+ <View style={staticStyles.imageContainer}>
94
+ <Image
95
+ source={image.src}
96
+ style={staticStyles.image}
97
+ alt={image.alt}
98
+ accessibilityLabel={image.alt}
99
+ resizeMethod="resize"
100
+ accessibilityIgnoresInvertColors
101
+ />
102
+ </View>
103
+ ) : null}
104
+
105
+ <View style={staticStyles.textContainer}>
106
+ <Box left={3}>
107
+ <StackView space={1}>
108
+ {getCopy('badgeText') ? (
109
+ <Badge variant={{ outline: true, purpose: 'editorial' }}>
110
+ {getCopy('badgeText')}
111
+ </Badge>
112
+ ) : null}
113
+ <Typography variant={{ size: 'h6' }}>{getCopy('brandName')}</Typography>
114
+ <Typography variant={{ size: 'h4', colour: 'brand' }} tokens={{ fontWeight: 400 }}>
115
+ {getCopy('productName')}
116
+ </Typography>
117
+ <StackView space={3} divider direction="row">
118
+ <PriceLockup {...getCopy('primaryPrice')} size="small" ratePosition="bottom" />
119
+ {getCopy('secondaryPrice')?.price ? (
120
+ <PriceLockup {...getCopy('secondaryPrice')} size="small" ratePosition="bottom" />
121
+ ) : null}
122
+ </StackView>
123
+ <Box top={2}>
124
+ <StackView space={2}>
125
+ <Typography variant={{ size: 'h6' }} tokens={{ fontWeight: 500 }}>
126
+ {getCopy('term')}
127
+ </Typography>
128
+ {getCopy('buttonLabel') ? <Box top={1}>{renderButton}</Box> : null}
129
+ </StackView>
130
+ </Box>
131
+ </StackView>
132
+ </Box>
133
+ </View>
134
+ </View>
135
+ )
136
+ }
137
+
138
+ ProductCard.displayName = 'ProductCard'
139
+
140
+ // If a language dictionary entry is provided, it must contain every key
141
+ const dictionaryContentShape = PropTypes.shape({
142
+ badgeText: PropTypes.string,
143
+ brandName: PropTypes.string.isRequired,
144
+ productName: PropTypes.string.isRequired,
145
+ primaryPrice: PropTypes.object.isRequired,
146
+ secondaryPrice: PropTypes.object,
147
+ term: PropTypes.string.isRequired,
148
+ buttonLabel: PropTypes.string.isRequired,
149
+ selectedButtonLabel: PropTypes.string.isRequired
150
+ })
151
+
152
+ ProductCard.propTypes = {
153
+ ...selectedSystemPropTypes,
154
+ image: PropTypes.shape({
155
+ src: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
156
+ alt: PropTypes.string
157
+ }),
158
+ /**
159
+ * Select English or French copy for the place holder labels.
160
+ * You may also pass in a custom dictionary object.
161
+ */
162
+ copy: PropTypes.oneOfType([PropTypes.oneOf(['en', 'fr'])]),
163
+ /**
164
+ * Override the default dictionary, by passing the complete dictionary object for `en` and `fr`
165
+ */
166
+ dictionary: PropTypes.shape({
167
+ en: dictionaryContentShape,
168
+ fr: dictionaryContentShape
169
+ }),
170
+ tokens: getTokensPropType('ProductCard')
171
+ }
172
+ export default ProductCard
173
+
174
+ const staticStyles = StyleSheet.create({
175
+ container: {
176
+ flexDirection: 'row',
177
+ flex: 1
178
+ },
179
+ imageContainer: {
180
+ width: '30%',
181
+ minWidth: 96,
182
+ maxWidth: 96
183
+ },
184
+ image: {
185
+ resizeMode: 'contain',
186
+ width: '100%',
187
+ height: undefined, // This is to maintain the aspect ratio
188
+ aspectRatio: 0.8
189
+ },
190
+ textContainer: {
191
+ width: '70%'
192
+ }
193
+ })
@@ -0,0 +1,38 @@
1
+ export default {
2
+ en: {
3
+ badgeText: '',
4
+ brandName: 'Brand name',
5
+ productName: 'Product name',
6
+ primaryPrice: {
7
+ price: '00',
8
+ signDirection: 'left',
9
+ rateText: '/month'
10
+ },
11
+ secondaryPrice: {
12
+ price: '',
13
+ signDirection: 'left',
14
+ rateText: 'Upfront'
15
+ },
16
+ term: '24 months | 0% APR Bring-it-Back applied',
17
+ buttonLabel: 'Select this phone',
18
+ selectedButtonLabel: 'Selected phone'
19
+ },
20
+ fr: {
21
+ badgeText: '',
22
+ brandName: 'Brand name',
23
+ productName: 'Product name',
24
+ primaryPrice: {
25
+ price: '00',
26
+ signDirection: 'right',
27
+ rateText: '/mois'
28
+ },
29
+ secondaryPrice: {
30
+ price: '',
31
+ signDirection: 'right',
32
+ rateText: "d'acompte"
33
+ },
34
+ term: '24 mois | TAP de 0% | avec Option retour',
35
+ buttonLabel: 'Sélectionner ce téléphone',
36
+ selectedButtonLabel: 'Téléphone sélectionné'
37
+ }
38
+ }
@@ -0,0 +1,3 @@
1
+ import ProductCard from './ProductCard'
2
+
3
+ export default ProductCard
@@ -0,0 +1,75 @@
1
+ import React, { useState } from 'react'
2
+ import PropTypes from 'prop-types'
3
+ import { View } from 'react-native'
4
+ import { selectSystemProps, htmlAttrs, viewProps, a11yProps } from '../utils'
5
+
6
+ import ProductCard from '../ProductCard'
7
+ import { StackWrap } from '../StackView'
8
+
9
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs, viewProps, a11yProps])
10
+
11
+ const ProductCardGroup = ({ productCards, maxSelection = 1, ...rest }) => {
12
+ const [selectedCardIds, setSelectedCardIds] = useState([])
13
+
14
+ const handleSelect = (id) => {
15
+ const isAlreadySelected = selectedCardIds.includes(id)
16
+ let updatedSelectedCardIds
17
+
18
+ if (isAlreadySelected) {
19
+ updatedSelectedCardIds = selectedCardIds.filter((cardId) => cardId !== id)
20
+ } else if (maxSelection && selectedCardIds.length >= maxSelection) {
21
+ updatedSelectedCardIds = selectedCardIds.filter((_, index) => index !== 0).concat(id)
22
+ } else {
23
+ updatedSelectedCardIds = [...selectedCardIds, id]
24
+ }
25
+
26
+ setSelectedCardIds(updatedSelectedCardIds)
27
+ }
28
+
29
+ return (
30
+ <View {...selectProps(rest)}>
31
+ <StackWrap direction={{ xs: 'column', lg: 'row' }} space={4}>
32
+ {productCards.map((cardProperties, index) => {
33
+ const cardId = cardProperties.id || index
34
+
35
+ return (
36
+ <View key={cardId}>
37
+ <ProductCard
38
+ key={cardId}
39
+ cardId={cardId}
40
+ isSelected={selectedCardIds.includes(cardId)}
41
+ onSelect={handleSelect}
42
+ {...cardProperties}
43
+ />
44
+ </View>
45
+ )
46
+ })}
47
+ </StackWrap>
48
+ </View>
49
+ )
50
+ }
51
+
52
+ ProductCardGroup.displayName = 'ProductCardGroup'
53
+
54
+ ProductCardGroup.propTypes = {
55
+ ...selectedSystemPropTypes,
56
+ /**
57
+ * The maximum number of ProductCards a user may select at once. Defaults to 1.
58
+ * To have no limit and allow any number of selections, pass `null`.
59
+ */
60
+ maxSelection: PropTypes.number,
61
+ /**
62
+ * Props to be passed to the `ProductCard` component.
63
+ * id is required for each card.
64
+ * You may also pass in a custom dictionary object.
65
+ */
66
+ productCards: PropTypes.arrayOf(
67
+ PropTypes.shape({
68
+ id: PropTypes.string.isRequired,
69
+ image: PropTypes.object,
70
+ dictionary: PropTypes.object
71
+ })
72
+ ).isRequired
73
+ }
74
+
75
+ export default ProductCardGroup
@@ -0,0 +1,3 @@
1
+ import ProductCardGroup from './ProductCardGroup'
2
+
3
+ export default ProductCardGroup
@@ -71,6 +71,7 @@ TextInput.propTypes = {
71
71
  * A callback which if provided will get a clear button rendered and will be called whenever that button gets pressed.
72
72
  */
73
73
  onClear: PropTypes.func,
74
+ onKeyPress: PropTypes.func,
74
75
  tokens: getTokensPropType('TextInput'),
75
76
  variant: variantProp.propType
76
77
  }
@@ -182,6 +182,7 @@ const TextInputBase = forwardRef(
182
182
  value,
183
183
  variant = {},
184
184
  type,
185
+ onKeyPress,
185
186
  ...rest
186
187
  },
187
188
  ref
@@ -311,7 +312,8 @@ const TextInputBase = forwardRef(
311
312
  onChange: handleChangeText,
312
313
  defaultValue: initialValue,
313
314
  maxLength: type === 'card' ? 19 : undefined,
314
- value: isControlled ? currentValue : undefined
315
+ value: isControlled ? currentValue : undefined,
316
+ onKeyPress
315
317
  }
316
318
 
317
319
  const { themeOptions } = useTheme()
@@ -388,6 +390,7 @@ TextInputBase.propTypes = {
388
390
  onFocus: PropTypes.func,
389
391
  onMouseOut: PropTypes.func,
390
392
  onMouseOver: PropTypes.func,
393
+ onKeyPress: PropTypes.func,
391
394
  readOnly: PropTypes.bool,
392
395
  tokens: getTokensPropType('TextInput', 'TextArea'),
393
396
  value: PropTypes.string,
package/src/index.js CHANGED
@@ -33,6 +33,8 @@ export { default as Notification } from './Notification'
33
33
  export { default as OrderedList } from './OrderedList'
34
34
  export { default as Pagination } from './Pagination'
35
35
  export { default as PriceLockup } from './PriceLockup'
36
+ export { default as ProductCard } from './ProductCard'
37
+ export { default as ProductCardGroup } from './ProductCardGroup'
36
38
  export { default as Progress } from './Progress'
37
39
  export { default as QuickLinks } from './QuickLinks'
38
40
  export { default as QuickLinksFeature } from './QuickLinksFeature'
@@ -1,7 +1,7 @@
1
1
  import { addCss } from '../utils/inject'
2
2
  import createDeclarationBlock from '../utils/create-declaration-block'
3
3
  import hash from '../hash'
4
- import { isMediaOrPseudo, deepClone, createCssRule } from '../utils/common'
4
+ import { isMediaOrPseudo, deepClone, createCssRule, sanitizeStyle } from '../utils/common'
5
5
 
6
6
  const createStyleSheet = (stylesWithQuery) => {
7
7
  if (!stylesWithQuery) return { ids: {}, styles: {}, fullStyles: {} }
@@ -14,7 +14,8 @@ const createStyleSheet = (stylesWithQuery) => {
14
14
 
15
15
  const mediaQueriesAndPseudoClasses = Object.keys(stylesWithQuery[key]).filter(isMediaOrPseudo)
16
16
  mediaQueriesAndPseudoClasses.forEach((query) => {
17
- const css = createDeclarationBlock(stylesWithQuery[key][query])
17
+ const sanitizedStyle = sanitizeStyle(stylesWithQuery[key][query])
18
+ const css = createDeclarationBlock(sanitizedStyle)
18
19
  const stringHash = `rnmq-${hash(`${key}${query}${css}`)}`
19
20
  const rule = createCssRule(query, stringHash, css)
20
21
 
@@ -17,4 +17,22 @@ const createCssRule = (query, stringHash, css) => {
17
17
  return rule
18
18
  }
19
19
 
20
- export { isMedia, isPseudo, isMediaOrPseudo, deepClone, createCssRule }
20
+ /**
21
+ * Sanitizes the style object by converting any functions to their string representation.
22
+ *
23
+ * @param {Object} style - The style object to sanitize.
24
+ * @returns {Object} - The sanitized style object.
25
+ */
26
+ const sanitizeStyle = (style) => {
27
+ const sanitizedStyle = { ...style }
28
+
29
+ Object.keys(sanitizedStyle).forEach((property) => {
30
+ if (typeof sanitizedStyle[property] === 'function') {
31
+ sanitizedStyle[property] = sanitizedStyle[property].toString()
32
+ }
33
+ })
34
+
35
+ return sanitizedStyle
36
+ }
37
+
38
+ export { isMedia, isPseudo, isMediaOrPseudo, deepClone, createCssRule, sanitizeStyle }