@telus-uds/components-base 1.81.0 → 1.83.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 (56) hide show
  1. package/CHANGELOG.md +30 -2
  2. package/lib/Autocomplete/Autocomplete.js +3 -1
  3. package/lib/Icon/Icon.js +24 -2
  4. package/lib/Listbox/Listbox.js +14 -6
  5. package/lib/Modal/Modal.js +40 -4
  6. package/lib/Modal/WebModal.js +73 -0
  7. package/lib/Notification/Notification.js +1 -1
  8. package/lib/PriceLockup/PriceLockup.js +4 -1
  9. package/lib/PriceLockup/utils/renderFootnoteContent.js +2 -2
  10. package/lib/PriceLockup/utils/renderFootnoteLinks.js +2 -2
  11. package/lib/PriceLockup/utils/renderPrice.js +2 -2
  12. package/lib/PriceLockup/utils/renderTypography.js +1 -1
  13. package/lib/ProductCard/ProductCard.js +238 -0
  14. package/lib/ProductCard/dictionary.js +45 -0
  15. package/lib/ProductCard/index.js +10 -0
  16. package/lib/ProductCardGroup/ProductCardGroup.js +79 -0
  17. package/lib/ProductCardGroup/index.js +10 -0
  18. package/lib/Radio/Radio.js +1 -1
  19. package/lib/index.js +16 -0
  20. package/lib-module/Autocomplete/Autocomplete.js +3 -1
  21. package/lib-module/Icon/Icon.js +24 -2
  22. package/lib-module/Listbox/Listbox.js +15 -7
  23. package/lib-module/Modal/Modal.js +42 -5
  24. package/lib-module/Modal/WebModal.js +65 -0
  25. package/lib-module/Notification/Notification.js +1 -1
  26. package/lib-module/PriceLockup/PriceLockup.js +4 -1
  27. package/lib-module/PriceLockup/utils/renderFootnoteContent.js +2 -2
  28. package/lib-module/PriceLockup/utils/renderFootnoteLinks.js +2 -2
  29. package/lib-module/PriceLockup/utils/renderPrice.js +2 -2
  30. package/lib-module/PriceLockup/utils/renderTypography.js +1 -1
  31. package/lib-module/ProductCard/ProductCard.js +231 -0
  32. package/lib-module/ProductCard/dictionary.js +38 -0
  33. package/lib-module/ProductCard/index.js +2 -0
  34. package/lib-module/ProductCardGroup/ProductCardGroup.js +69 -0
  35. package/lib-module/ProductCardGroup/index.js +2 -0
  36. package/lib-module/Radio/Radio.js +1 -1
  37. package/lib-module/index.js +2 -0
  38. package/package.json +2 -2
  39. package/src/Autocomplete/Autocomplete.jsx +4 -1
  40. package/src/Icon/Icon.jsx +30 -2
  41. package/src/Listbox/Listbox.jsx +112 -100
  42. package/src/Modal/Modal.jsx +42 -3
  43. package/src/Modal/WebModal.jsx +60 -0
  44. package/src/Notification/Notification.jsx +1 -1
  45. package/src/PriceLockup/PriceLockup.jsx +8 -2
  46. package/src/PriceLockup/utils/renderFootnoteContent.jsx +2 -2
  47. package/src/PriceLockup/utils/renderFootnoteLinks.jsx +2 -2
  48. package/src/PriceLockup/utils/renderPrice.jsx +2 -2
  49. package/src/PriceLockup/utils/renderTypography.jsx +1 -1
  50. package/src/ProductCard/ProductCard.jsx +193 -0
  51. package/src/ProductCard/dictionary.js +38 -0
  52. package/src/ProductCard/index.js +3 -0
  53. package/src/ProductCardGroup/ProductCardGroup.jsx +75 -0
  54. package/src/ProductCardGroup/index.js +3 -0
  55. package/src/Radio/Radio.jsx +1 -1
  56. package/src/index.js +2 -0
@@ -0,0 +1,231 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import StyleSheet from "react-native-web/dist/exports/StyleSheet";
4
+ import View from "react-native-web/dist/exports/View";
5
+ import Image from "react-native-web/dist/exports/Image";
6
+ import { useViewport } from '../ViewportProvider';
7
+ import { useThemeTokens } from '../ThemeProvider';
8
+ import defaultDictionary from './dictionary';
9
+ import { selectSystemProps, getTokensPropType, htmlAttrs, viewProps, useInputValue, useCopy, a11yProps } from '../utils';
10
+ import Badge from '../Badge';
11
+ import PriceLockup from '../PriceLockup';
12
+ import Typography from '../Typography';
13
+ import { Button } from '../Button';
14
+ import StackView from '../StackView';
15
+ import Box from '../Box';
16
+ import Icon from '../Icon';
17
+ import { jsx as _jsx } from "react/jsx-runtime";
18
+ import { jsxs as _jsxs } from "react/jsx-runtime";
19
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs, viewProps, a11yProps]);
20
+ const selectProductCardTokens = _ref => {
21
+ let {
22
+ borderStyle,
23
+ borderColor,
24
+ borderWidth,
25
+ borderRadius,
26
+ paddingHorizontal,
27
+ paddingVertical
28
+ } = _ref;
29
+ return {
30
+ borderStyle,
31
+ borderColor,
32
+ borderWidth,
33
+ borderRadius,
34
+ paddingHorizontal,
35
+ paddingVertical
36
+ };
37
+ };
38
+ const ProductCard = _ref2 => {
39
+ var _getCopy;
40
+ let {
41
+ copy = 'en',
42
+ dictionary = defaultDictionary,
43
+ image = {
44
+ src: '',
45
+ alt: ''
46
+ },
47
+ cardId,
48
+ isSelected,
49
+ onSelect,
50
+ tokens,
51
+ ...rest
52
+ } = _ref2;
53
+ const viewport = useViewport();
54
+ const themeTokens = useThemeTokens('ProductCard', tokens, {
55
+ viewport
56
+ });
57
+ const getCopy = useCopy({
58
+ copy,
59
+ dictionary
60
+ });
61
+ const {
62
+ currentValue,
63
+ setValue
64
+ } = useInputValue();
65
+ const hasClicked = isSelected || currentValue;
66
+ const handlePress = event => {
67
+ if (cardId) {
68
+ onSelect(cardId);
69
+ } else {
70
+ setValue(!currentValue, event);
71
+ }
72
+ };
73
+ const renderButton = hasClicked ? /*#__PURE__*/_jsx(Box, {
74
+ vertical: 1,
75
+ children: /*#__PURE__*/_jsxs(StackView, {
76
+ space: 2,
77
+ direction: "row",
78
+ children: [/*#__PURE__*/_jsx(Icon, {
79
+ icon: themeTokens.selectedButtonIcon,
80
+ variant: {
81
+ color: 'success'
82
+ }
83
+ }), /*#__PURE__*/_jsx(Typography, {
84
+ variant: {
85
+ size: 'h4'
86
+ },
87
+ tokens: {
88
+ fontWeight: 400
89
+ },
90
+ children: getCopy('selectedButtonLabel')
91
+ })]
92
+ })
93
+ }) : /*#__PURE__*/_jsx(Button, {
94
+ onPress: handlePress,
95
+ variant: {
96
+ purpose: 'primary',
97
+ size: 'small',
98
+ width: 'full'
99
+ },
100
+ children: getCopy('buttonLabel')
101
+ });
102
+ return /*#__PURE__*/_jsxs(View, {
103
+ ...selectProps(rest),
104
+ style: [selectProductCardTokens(themeTokens), staticStyles.container],
105
+ children: [image !== null && image !== void 0 && image.src ? /*#__PURE__*/_jsx(View, {
106
+ style: staticStyles.imageContainer,
107
+ children: /*#__PURE__*/_jsx(Image, {
108
+ source: image.src,
109
+ style: staticStyles.image,
110
+ alt: image.alt,
111
+ accessibilityLabel: image.alt,
112
+ resizeMethod: "resize",
113
+ accessibilityIgnoresInvertColors: true
114
+ })
115
+ }) : null, /*#__PURE__*/_jsx(View, {
116
+ style: staticStyles.textContainer,
117
+ children: /*#__PURE__*/_jsx(Box, {
118
+ left: 3,
119
+ children: /*#__PURE__*/_jsxs(StackView, {
120
+ space: 1,
121
+ children: [getCopy('badgeText') ? /*#__PURE__*/_jsx(Badge, {
122
+ variant: {
123
+ outline: true,
124
+ purpose: 'editorial'
125
+ },
126
+ children: getCopy('badgeText')
127
+ }) : null, /*#__PURE__*/_jsx(Typography, {
128
+ variant: {
129
+ size: 'h6'
130
+ },
131
+ children: getCopy('brandName')
132
+ }), /*#__PURE__*/_jsx(Typography, {
133
+ variant: {
134
+ size: 'h4',
135
+ colour: 'brand'
136
+ },
137
+ tokens: {
138
+ fontWeight: 400
139
+ },
140
+ children: getCopy('productName')
141
+ }), /*#__PURE__*/_jsxs(StackView, {
142
+ space: 3,
143
+ divider: true,
144
+ direction: "row",
145
+ children: [/*#__PURE__*/_jsx(PriceLockup, {
146
+ ...getCopy('primaryPrice'),
147
+ size: "small",
148
+ ratePosition: "bottom"
149
+ }), (_getCopy = getCopy('secondaryPrice')) !== null && _getCopy !== void 0 && _getCopy.price ? /*#__PURE__*/_jsx(PriceLockup, {
150
+ ...getCopy('secondaryPrice'),
151
+ size: "small",
152
+ ratePosition: "bottom"
153
+ }) : null]
154
+ }), /*#__PURE__*/_jsx(Box, {
155
+ top: 2,
156
+ children: /*#__PURE__*/_jsxs(StackView, {
157
+ space: 2,
158
+ children: [/*#__PURE__*/_jsx(Typography, {
159
+ variant: {
160
+ size: 'h6'
161
+ },
162
+ tokens: {
163
+ fontWeight: 500
164
+ },
165
+ children: getCopy('term')
166
+ }), getCopy('buttonLabel') ? /*#__PURE__*/_jsx(Box, {
167
+ top: 1,
168
+ children: renderButton
169
+ }) : null]
170
+ })
171
+ })]
172
+ })
173
+ })
174
+ })]
175
+ });
176
+ };
177
+ ProductCard.displayName = 'ProductCard';
178
+
179
+ // If a language dictionary entry is provided, it must contain every key
180
+ const dictionaryContentShape = PropTypes.shape({
181
+ badgeText: PropTypes.string,
182
+ brandName: PropTypes.string.isRequired,
183
+ productName: PropTypes.string.isRequired,
184
+ primaryPrice: PropTypes.object.isRequired,
185
+ secondaryPrice: PropTypes.object,
186
+ term: PropTypes.string.isRequired,
187
+ buttonLabel: PropTypes.string.isRequired,
188
+ selectedButtonLabel: PropTypes.string.isRequired
189
+ });
190
+ ProductCard.propTypes = {
191
+ ...selectedSystemPropTypes,
192
+ image: PropTypes.shape({
193
+ src: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
194
+ alt: PropTypes.string
195
+ }),
196
+ /**
197
+ * Select English or French copy for the place holder labels.
198
+ * You may also pass in a custom dictionary object.
199
+ */
200
+ copy: PropTypes.oneOfType([PropTypes.oneOf(['en', 'fr'])]),
201
+ /**
202
+ * Override the default dictionary, by passing the complete dictionary object for `en` and `fr`
203
+ */
204
+ dictionary: PropTypes.shape({
205
+ en: dictionaryContentShape,
206
+ fr: dictionaryContentShape
207
+ }),
208
+ tokens: getTokensPropType('ProductCard')
209
+ };
210
+ export default ProductCard;
211
+ const staticStyles = StyleSheet.create({
212
+ container: {
213
+ flexDirection: 'row',
214
+ flex: 1
215
+ },
216
+ imageContainer: {
217
+ width: '30%',
218
+ minWidth: 96,
219
+ maxWidth: 96
220
+ },
221
+ image: {
222
+ resizeMode: 'contain',
223
+ width: '100%',
224
+ height: undefined,
225
+ // This is to maintain the aspect ratio
226
+ aspectRatio: 0.8
227
+ },
228
+ textContainer: {
229
+ width: '70%'
230
+ }
231
+ });
@@ -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,2 @@
1
+ import ProductCard from './ProductCard';
2
+ export default ProductCard;
@@ -0,0 +1,69 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import View from "react-native-web/dist/exports/View";
4
+ import { selectSystemProps, htmlAttrs, viewProps, a11yProps } from '../utils';
5
+ import ProductCard from '../ProductCard';
6
+ import { StackWrap } from '../StackView';
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ const [selectProps, selectedSystemPropTypes] = selectSystemProps([htmlAttrs, viewProps, a11yProps]);
9
+ const ProductCardGroup = _ref => {
10
+ let {
11
+ productCards,
12
+ maxSelection = 1,
13
+ ...rest
14
+ } = _ref;
15
+ const [selectedCardIds, setSelectedCardIds] = useState([]);
16
+ const handleSelect = id => {
17
+ const isAlreadySelected = selectedCardIds.includes(id);
18
+ let updatedSelectedCardIds;
19
+ if (isAlreadySelected) {
20
+ updatedSelectedCardIds = selectedCardIds.filter(cardId => cardId !== id);
21
+ } else if (maxSelection && selectedCardIds.length >= maxSelection) {
22
+ updatedSelectedCardIds = selectedCardIds.filter((_, index) => index !== 0).concat(id);
23
+ } else {
24
+ updatedSelectedCardIds = [...selectedCardIds, id];
25
+ }
26
+ setSelectedCardIds(updatedSelectedCardIds);
27
+ };
28
+ return /*#__PURE__*/_jsx(View, {
29
+ ...selectProps(rest),
30
+ children: /*#__PURE__*/_jsx(StackWrap, {
31
+ direction: {
32
+ xs: 'column',
33
+ lg: 'row'
34
+ },
35
+ space: 4,
36
+ children: productCards.map((cardProperties, index) => {
37
+ const cardId = cardProperties.id || index;
38
+ return /*#__PURE__*/_jsx(View, {
39
+ children: /*#__PURE__*/_jsx(ProductCard, {
40
+ cardId: cardId,
41
+ isSelected: selectedCardIds.includes(cardId),
42
+ onSelect: handleSelect,
43
+ ...cardProperties
44
+ }, cardId)
45
+ }, cardId);
46
+ })
47
+ })
48
+ });
49
+ };
50
+ ProductCardGroup.displayName = 'ProductCardGroup';
51
+ ProductCardGroup.propTypes = {
52
+ ...selectedSystemPropTypes,
53
+ /**
54
+ * The maximum number of ProductCards a user may select at once. Defaults to 1.
55
+ * To have no limit and allow any number of selections, pass `null`.
56
+ */
57
+ maxSelection: PropTypes.number,
58
+ /**
59
+ * Props to be passed to the `ProductCard` component.
60
+ * id is required for each card.
61
+ * You may also pass in a custom dictionary object.
62
+ */
63
+ productCards: PropTypes.arrayOf(PropTypes.shape({
64
+ id: PropTypes.string.isRequired,
65
+ image: PropTypes.object,
66
+ dictionary: PropTypes.object
67
+ })).isRequired
68
+ };
69
+ export default ProductCardGroup;
@@ -0,0 +1,2 @@
1
+ import ProductCardGroup from './ProductCardGroup';
2
+ export default ProductCardGroup;
@@ -240,7 +240,7 @@ Radio.propTypes = {
240
240
  /**
241
241
  * The label.
242
242
  */
243
- label: PropTypes.string,
243
+ label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
244
244
  /**
245
245
  * Associate this radio button with a group (set as the name attribute).
246
246
  */
@@ -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';
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "@floating-ui/react-native": "^0.8.1",
12
12
  "@gorhom/portal": "^1.0.14",
13
13
  "@telus-uds/system-constants": "^1.3.0",
14
- "@telus-uds/system-theme-tokens": "^2.53.0",
14
+ "@telus-uds/system-theme-tokens": "^2.54.0",
15
15
  "airbnb-prop-types": "^2.16.0",
16
16
  "css-mediaquery": "^0.1.2",
17
17
  "expo-linear-gradient": "^12.5.0",
@@ -85,6 +85,6 @@
85
85
  "standard-engine": {
86
86
  "skip": true
87
87
  },
88
- "version": "1.81.0",
88
+ "version": "1.83.0",
89
89
  "types": "types/index.d.ts"
90
90
  }
@@ -217,6 +217,8 @@ const Autocomplete = forwardRef(
217
217
  if (!isControlled && inputRef?.current) inputRef.current.value = newValue
218
218
 
219
219
  if (nested) setNestedSelectedValue(newValue)
220
+
221
+ inputRef.current.focus()
220
222
  }
221
223
 
222
224
  /**
@@ -253,11 +255,12 @@ const Autocomplete = forwardRef(
253
255
  setNestedSelectedValue(null)
254
256
  } else if (
255
257
  event.type === 'keydown' &&
256
- event.key === 'ArrowDown' &&
258
+ (event.key === 'ArrowDown' || event.key === 'Tab') &&
257
259
  isExpanded &&
258
260
  !isLoading &&
259
261
  targetRef?.current
260
262
  ) {
263
+ event.preventDefault()
261
264
  targetRef.current.focus()
262
265
  }
263
266
  }
package/src/Icon/Icon.jsx CHANGED
@@ -26,6 +26,31 @@ const Icon = forwardRef(
26
26
  <IconComponent title={accessibilityLabel} size={size} color={themeTokens.color} />
27
27
  )
28
28
 
29
+ const paddingStyles = variant?.padding
30
+ ? {
31
+ padding: themeTokens.width,
32
+ width: themeTokens.size + themeTokens.width * 2, // sets the diameter of the circle which is the size of the icon plus twice the general padding established to obtain a perfect circle
33
+ height: themeTokens.size + themeTokens.width * 2
34
+ }
35
+ : {}
36
+
37
+ const getIconContentForMobile = () => {
38
+ if (Object.keys(paddingStyles).length) {
39
+ return (
40
+ <View
41
+ style={{
42
+ backgroundColor: themeTokens.backgroundColor,
43
+ borderRadius: themeTokens.borderRadius,
44
+ ...paddingStyles
45
+ }}
46
+ >
47
+ {iconContent}
48
+ </View>
49
+ )
50
+ }
51
+ return iconContent
52
+ }
53
+
29
54
  return Platform.OS === 'web' ? (
30
55
  <View
31
56
  ref={ref}
@@ -41,14 +66,17 @@ const Icon = forwardRef(
41
66
  ]
42
67
  .filter((exists) => exists)
43
68
  .join(' '),
44
- ...style
69
+ ...style,
70
+ backgroundColor: themeTokens.backgroundColor,
71
+ borderRadius: themeTokens.borderRadius,
72
+ ...paddingStyles
45
73
  }}
46
74
  dataSet={dataSet}
47
75
  >
48
76
  {iconContent}
49
77
  </View>
50
78
  ) : (
51
- iconContent
79
+ getIconContentForMobile()
52
80
  )
53
81
  }
54
82
  )