@telus-uds/components-base 3.22.0 → 3.23.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 (51) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/lib/cjs/Button/Button.js +2 -0
  3. package/lib/cjs/Button/ButtonBase.js +10 -5
  4. package/lib/cjs/Button/ButtonDropdown.js +2 -0
  5. package/lib/cjs/Button/ButtonGroup.js +45 -38
  6. package/lib/cjs/Button/propTypes.js +6 -0
  7. package/lib/cjs/Carousel/Carousel.js +52 -19
  8. package/lib/cjs/Carousel/CarouselItem/CarouselItem.js +23 -3
  9. package/lib/cjs/Icon/Icon.js +8 -11
  10. package/lib/cjs/Icon/IconText.js +0 -1
  11. package/lib/cjs/Listbox/GroupControl.js +33 -39
  12. package/lib/cjs/Listbox/Listbox.js +22 -13
  13. package/lib/cjs/Listbox/ListboxGroup.js +2 -1
  14. package/lib/cjs/Listbox/ListboxOverlay.js +5 -2
  15. package/lib/cjs/Listbox/PressableItem.js +8 -4
  16. package/lib/cjs/TextInput/TextInputBase.js +5 -1
  17. package/lib/cjs/Validator/Validator.js +171 -135
  18. package/lib/esm/Button/Button.js +2 -0
  19. package/lib/esm/Button/ButtonBase.js +10 -5
  20. package/lib/esm/Button/ButtonDropdown.js +2 -0
  21. package/lib/esm/Button/ButtonGroup.js +44 -39
  22. package/lib/esm/Button/propTypes.js +6 -0
  23. package/lib/esm/Carousel/Carousel.js +52 -19
  24. package/lib/esm/Carousel/CarouselItem/CarouselItem.js +23 -3
  25. package/lib/esm/Icon/Icon.js +8 -11
  26. package/lib/esm/Icon/IconText.js +0 -1
  27. package/lib/esm/Listbox/GroupControl.js +33 -39
  28. package/lib/esm/Listbox/Listbox.js +23 -14
  29. package/lib/esm/Listbox/ListboxGroup.js +2 -1
  30. package/lib/esm/Listbox/ListboxOverlay.js +5 -2
  31. package/lib/esm/Listbox/PressableItem.js +8 -4
  32. package/lib/esm/TextInput/TextInputBase.js +5 -1
  33. package/lib/esm/Validator/Validator.js +171 -135
  34. package/lib/package.json +2 -2
  35. package/package.json +2 -2
  36. package/src/Button/Button.jsx +2 -1
  37. package/src/Button/ButtonBase.jsx +18 -12
  38. package/src/Button/ButtonDropdown.jsx +2 -0
  39. package/src/Button/ButtonGroup.jsx +62 -45
  40. package/src/Button/propTypes.js +6 -0
  41. package/src/Carousel/Carousel.jsx +58 -5
  42. package/src/Carousel/CarouselItem/CarouselItem.jsx +31 -3
  43. package/src/Icon/Icon.jsx +11 -14
  44. package/src/Icon/IconText.jsx +0 -1
  45. package/src/Listbox/GroupControl.jsx +41 -47
  46. package/src/Listbox/Listbox.jsx +26 -9
  47. package/src/Listbox/ListboxGroup.jsx +2 -1
  48. package/src/Listbox/ListboxOverlay.jsx +7 -2
  49. package/src/Listbox/PressableItem.jsx +8 -4
  50. package/src/TextInput/TextInputBase.jsx +5 -1
  51. package/src/Validator/Validator.jsx +180 -159
@@ -407,6 +407,7 @@ const Carousel = React.forwardRef(
407
407
  ref
408
408
  ) => {
409
409
  let childrenArray = unpackFragment(children)
410
+ const isTransitioningRef = React.useRef(false)
410
411
  const viewport = useViewport()
411
412
  const totalItems = getTotalItems(enableDisplayMultipleItemsPerSlide, childrenArray, viewport)
412
413
  const autoPlayFeatureEnabled =
@@ -482,10 +483,14 @@ const Carousel = React.forwardRef(
482
483
  )
483
484
  const handleAnimationEnd = React.useCallback(
484
485
  (...args) => {
485
- if (typeof onAnimationEnd === 'function') onAnimationEnd(...args)
486
- setIsAnimating(false)
486
+ const result = args[args.length - 1]
487
+ if (result?.finished) {
488
+ if (typeof onAnimationEnd === 'function') onAnimationEnd(...args)
489
+ setIsAnimating(false)
490
+ isTransitioningRef.current = false
491
+ }
487
492
  },
488
- [onAnimationEnd]
493
+ [onAnimationEnd, isTransitioningRef]
489
494
  )
490
495
 
491
496
  const updateOffset = React.useCallback(() => {
@@ -602,7 +607,9 @@ const Carousel = React.forwardRef(
602
607
 
603
608
  const index = activeIndexRef.current + calcDelta
604
609
  if (skipChanges) {
610
+ isTransitioningRef.current = true
605
611
  animate(pan, toValue, index)
612
+
606
613
  if (enableHero) {
607
614
  animate(heroPan, toValue, index)
608
615
  }
@@ -622,6 +629,7 @@ const Carousel = React.forwardRef(
622
629
  toValue.x = finalWidth * -1 * calcDelta
623
630
  const heroToValue = { x: 0, y: 0 }
624
631
  heroToValue.x = heroContainerLayoutRef.current.width * -1 * calcDelta
632
+ isTransitioningRef.current = true
625
633
  animate(pan, toValue, index)
626
634
  if (enableHero) {
627
635
  animate(heroPan, heroToValue, index)
@@ -1012,6 +1020,15 @@ const Carousel = React.forwardRef(
1012
1020
  setisCarouselPlaying((prevState) => !prevState)
1013
1021
  }, [isCarouselPlaying, stopAutoplay, startAutoplay])
1014
1022
 
1023
+ const handleKeyDown = React.useCallback(
1024
+ (event) => {
1025
+ if (isTransitioningRef.current && event.key === 'Tab') {
1026
+ event.preventDefault()
1027
+ }
1028
+ },
1029
+ [isTransitioningRef]
1030
+ )
1031
+
1015
1032
  return (
1016
1033
  <View style={selectRootContainerStyles(enableHero, viewport)}>
1017
1034
  <View style={selectMainContainerStyles(enableHero, viewport)}>
@@ -1041,6 +1058,7 @@ const Carousel = React.forwardRef(
1041
1058
  ref={ref}
1042
1059
  {...systemProps}
1043
1060
  {...containerProps}
1061
+ {...(Platform.OS === 'web' ? { onKeyDown: handleKeyDown } : {})}
1044
1062
  >
1045
1063
  {isAutoPlayEnabled ? (
1046
1064
  <View
@@ -1130,10 +1148,45 @@ const Carousel = React.forwardRef(
1130
1148
  accessibilityLiveRegion={accessibilityLiveRegion}
1131
1149
  >
1132
1150
  {childrenArray.map((element, index) => {
1133
- const hidden = !isAnimating && index !== activeIndex
1151
+ let hidden = !isAnimating && index !== activeIndex
1152
+
1153
+ if (enablePeeking && !isAnimating) {
1154
+ if (enableDisplayMultipleItemsPerSlide) {
1155
+ const maxItemsForSlide = getMaximumItemsForSlide(
1156
+ enableDisplayMultipleItemsPerSlide,
1157
+ viewport
1158
+ )
1159
+ if (
1160
+ index >= activeIndex * maxItemsForSlide - 1 &&
1161
+ index < activeIndex * maxItemsForSlide + maxItemsForSlide + 1
1162
+ ) {
1163
+ hidden = false
1164
+ } else {
1165
+ hidden = true
1166
+ }
1167
+ } else if (index >= activeIndex - 1 && index <= activeIndex + 1) {
1168
+ hidden = false
1169
+ }
1170
+ } else if (
1171
+ !enablePeeking &&
1172
+ enableDisplayMultipleItemsPerSlide &&
1173
+ !isAnimating
1174
+ ) {
1175
+ const maxItemsForSlide = getMaximumItemsForSlide(
1176
+ enableDisplayMultipleItemsPerSlide,
1177
+ viewport
1178
+ )
1179
+ if (
1180
+ index >= activeIndex * maxItemsForSlide &&
1181
+ index < activeIndex * maxItemsForSlide + maxItemsForSlide
1182
+ ) {
1183
+ hidden = false
1184
+ }
1185
+ }
1186
+
1134
1187
  const clonedElement = React.cloneElement(element, {
1135
1188
  elementIndex: index,
1136
- hidden: enablePeeking || enableDisplayMultipleItemsPerSlide ? false : hidden,
1189
+ hidden,
1137
1190
  enablePeeking,
1138
1191
  peekingProps: getPeekingProps(viewport),
1139
1192
  enableDisplayMultipleItemsPerSlide,
@@ -114,15 +114,43 @@ const CarouselItem = React.forwardRef(
114
114
 
115
115
  const handleFocus = React.useCallback(
116
116
  (event) => {
117
- if (Platform.OS === 'web' && elementIndex >= maximumItemsForSlide * (activeIndex + 1)) {
118
- goTo(activeIndex + 1)
117
+ if (Platform.OS === 'web') {
118
+ if (enablePeeking) {
119
+ if (enableDisplayMultipleItemsPerSlide) {
120
+ const startIndex = maximumItemsForSlide * activeIndex
121
+ const endIndex = startIndex + maximumItemsForSlide - 1
122
+ if (elementIndex < startIndex) {
123
+ if (activeIndex - 1 < 0) {
124
+ goTo(0)
125
+ } else {
126
+ goTo(activeIndex - 1)
127
+ }
128
+ } else if (elementIndex > endIndex) {
129
+ goTo(activeIndex + 1)
130
+ }
131
+ } else if (elementIndex !== activeIndex) {
132
+ if (elementIndex > activeIndex) {
133
+ goTo(activeIndex + 1)
134
+ } else if (elementIndex < activeIndex) {
135
+ goTo(activeIndex - 1)
136
+ }
137
+ }
138
+ }
119
139
  }
120
140
 
121
141
  if (rest.onFocus) {
122
142
  rest.onFocus(event)
123
143
  }
124
144
  },
125
- [elementIndex, activeIndex, goTo, maximumItemsForSlide, rest]
145
+ [
146
+ elementIndex,
147
+ activeIndex,
148
+ goTo,
149
+ maximumItemsForSlide,
150
+ rest,
151
+ enablePeeking,
152
+ enableDisplayMultipleItemsPerSlide
153
+ ]
126
154
  )
127
155
 
128
156
  return (
package/src/Icon/Icon.jsx CHANGED
@@ -43,20 +43,17 @@ const Icon = React.forwardRef(
43
43
  }
44
44
 
45
45
  const getIconContentForMobile = () => {
46
- if (Object.keys(paddingStyles).length) {
47
- return (
48
- <View
49
- style={{
50
- backgroundColor: themeTokens.backgroundColor,
51
- borderRadius: themeTokens.borderRadius,
52
- ...paddingStyles
53
- }}
54
- >
55
- {iconContent}
56
- </View>
57
- )
58
- }
59
- return iconContent
46
+ return (
47
+ <View
48
+ style={{
49
+ backgroundColor: themeTokens.backgroundColor,
50
+ borderRadius: themeTokens.borderRadius,
51
+ ...paddingStyles
52
+ }}
53
+ >
54
+ {iconContent}
55
+ </View>
56
+ )
60
57
  }
61
58
 
62
59
  return Platform.OS === 'web' ? (
@@ -90,7 +90,6 @@ IconText.propTypes = {
90
90
  * `<Typography>` component, or a component that renders `<Text>`.
91
91
  */
92
92
  children: PropTypes.node
93
- /* eslint-enable react/no-unused-prop-types */
94
93
  }
95
94
 
96
95
  const staticStyles = StyleSheet.create({
@@ -1,13 +1,13 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { View, StyleSheet, Text } from 'react-native'
3
+ import { View, Text, StyleSheet } from 'react-native'
4
4
  import { useThemeTokens } from '../ThemeProvider'
5
5
  import Icon from '../Icon'
6
6
  import Spacer from '../Spacer'
7
7
  import { useListboxContext } from './ListboxContext'
8
8
 
9
9
  const styles = StyleSheet.create({
10
- controlWrapper: {
10
+ container: {
11
11
  width: '100%',
12
12
  flex: 1,
13
13
  alignItems: 'center',
@@ -17,6 +17,36 @@ const styles = StyleSheet.create({
17
17
  }
18
18
  })
19
19
 
20
+ const selectTextStyles = (tokens) => ({
21
+ color: tokens.groupColor,
22
+ fontFamily: `${tokens.groupFontName}${tokens.groupFontWeight}normal`,
23
+ fontSize: tokens.groupFontSize,
24
+ fontWeight: tokens.groupFontWeight
25
+ })
26
+
27
+ const selectContainerStyles = (tokens) => ({
28
+ fontFamily: `${tokens.groupFontName}${tokens.groupFontWeight}normal`,
29
+ fontSize: tokens.groupFontSize,
30
+ color: tokens.groupColor,
31
+ textDecoration: tokens.itemTextDecoration,
32
+ backgroundColor: tokens.groupBackgroundColor,
33
+ outline: tokens.itemOutline,
34
+ minHeight: tokens.groupHeight,
35
+ borderRadius: tokens.groupBorderRadius,
36
+ paddingLeft: tokens.groupPaddingLeft - tokens.groupBorderLeftWidth,
37
+ paddingRight: tokens.groupPaddingRight - tokens.groupBorderRightWidth,
38
+ paddingTop: tokens.groupPaddingTop - tokens.groupBorderTopWidth,
39
+ paddingBottom: tokens.groupPaddingBottom - tokens.groupBorderBottomWidth,
40
+ borderLeftWidth: tokens.groupBorderLeftWidth,
41
+ borderLeftColor: tokens.groupBorderLeftColor,
42
+ borderRightWidth: tokens.groupBorderRightWidth,
43
+ borderRightColor: tokens.groupBorderRightColor,
44
+ borderTopWidth: tokens.groupBorderTopWidth,
45
+ borderTopColor: tokens.groupBorderTopColor,
46
+ borderBottomWidth: tokens.groupBorderBottomWidth,
47
+ borderBottomColor: tokens.groupBorderBottomColor
48
+ })
49
+
20
50
  const GroupControl = React.forwardRef(({ expanded, pressed, hover, focus, label, id }, ref) => {
21
51
  const { selectedId, setSelectedId } = useListboxContext()
22
52
  const tokens = useThemeTokens(
@@ -31,58 +61,22 @@ const GroupControl = React.forwardRef(({ expanded, pressed, hover, focus, label,
31
61
  focus
32
62
  }
33
63
  )
34
- const {
35
- groupFontName,
36
- groupFontWeight,
37
- groupFontSize,
38
- groupColor,
39
- groupBackgroundColor,
40
- groupBorderColor,
41
- groupBorderWidth,
42
- groupBorderRadius,
43
- groupPaddingLeft,
44
- groupPaddingRight,
45
- groupPaddingTop,
46
- groupPaddingBottom,
47
- itemTextDecoration,
48
- itemOutline,
49
- groupHeight
50
- } = tokens
51
-
52
- const getTextStyles = () => ({
53
- color: groupColor
54
- })
55
64
 
56
65
  return (
57
66
  <View
58
67
  onPress={() => setSelectedId(id)}
59
- style={[
60
- styles.controlWrapper,
61
- {
62
- fontFamily: `${groupFontName}${groupFontWeight}normal`,
63
- fontSize: groupFontSize,
64
- color: groupColor,
65
- textDecoration: itemTextDecoration,
66
- backgroundColor: groupBackgroundColor,
67
- outline: itemOutline,
68
- height: groupHeight,
69
- border: `${groupBorderWidth}px solid ${groupBorderColor}`,
70
- borderRadius: groupBorderRadius,
71
- paddingLeft: groupPaddingLeft - groupBorderWidth,
72
- paddingRight: groupPaddingRight - groupBorderWidth,
73
- paddingTop: groupPaddingTop - groupBorderWidth,
74
- paddingBottom: groupPaddingBottom - groupBorderWidth
75
- }
76
- ]}
68
+ style={[styles.container, selectContainerStyles(tokens)]}
77
69
  ref={ref}
78
70
  >
79
- <Text style={getTextStyles()}>{label}</Text>
71
+ <Text style={selectTextStyles(tokens)}>{label}</Text>
80
72
  <Spacer space={1} direction="row" />
81
- <Icon
82
- icon={tokens.groupIcon}
83
- tokens={{ color: tokens.groupColor }}
84
- variant={{ size: 'micro' }}
85
- />
73
+ {tokens.groupIcon && (
74
+ <Icon
75
+ icon={tokens.groupIcon}
76
+ tokens={{ color: tokens.groupColor }}
77
+ variant={{ size: 'micro' }}
78
+ />
79
+ )}
86
80
  </View>
87
81
  )
88
82
  })
@@ -1,8 +1,8 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { View, StyleSheet, Platform } from 'react-native'
3
+ import { View, Platform, StyleSheet } from 'react-native'
4
4
  import { useThemeTokens } from '../ThemeProvider'
5
- import { withLinkRouter, getTokensPropType } from '../utils'
5
+ import { withLinkRouter, getTokensPropType, variantProp } from '../utils'
6
6
  import ExpandCollapse from '../ExpandCollapse'
7
7
  import ListboxGroup from './ListboxGroup'
8
8
  import ListboxItem from './ListboxItem'
@@ -10,12 +10,18 @@ import { ListboxContext } from './ListboxContext'
10
10
  import DropdownOverlay from './ListboxOverlay'
11
11
 
12
12
  const styles = StyleSheet.create({
13
- list: {
13
+ container: {
14
14
  padding: 0,
15
15
  margin: 0
16
16
  }
17
17
  })
18
18
 
19
+ const selectContainerStyles = (tokens) => ({
20
+ minHeight: tokens.minHeight,
21
+ minWidth: tokens.minWidth,
22
+ backgroundColor: tokens.containerBackgroundColor
23
+ })
24
+
19
25
  const getInitialOpen = (items, selectedId) =>
20
26
  items
21
27
  .filter(
@@ -36,15 +42,14 @@ const Listbox = React.forwardRef(
36
42
  itemRouterProps,
37
43
  onClose,
38
44
  variant,
39
- tokens
45
+ tokens,
46
+ testID
40
47
  },
41
48
  ref
42
49
  ) => {
43
50
  const initialOpen = getInitialOpen(items, defaultSelectedId)
44
-
45
51
  const [selectedId, setSelectedId] = React.useState(defaultSelectedId)
46
-
47
- const { minHeight, minWidth } = useThemeTokens('Listbox', variant, tokens)
52
+ const listboxTokens = useThemeTokens('Listbox', tokens, variant)
48
53
 
49
54
  // We need to keep track of each item's ref in order to be able to
50
55
  // focus on a specific item via keyboard navigation
@@ -101,7 +106,11 @@ const Listbox = React.forwardRef(
101
106
  <ListboxContext.Provider value={{ selectedId, setSelectedId }}>
102
107
  <ExpandCollapse initialOpen={initialOpen} maxOpen={1} ref={ref}>
103
108
  {(expandProps) => (
104
- <View style={[styles.list, { minHeight, minWidth }]} role="listbox">
109
+ <View
110
+ style={[styles.container, selectContainerStyles(listboxTokens)]}
111
+ accessibilityRole="combobox"
112
+ testID={testID}
113
+ >
105
114
  {items.map((item, index) => {
106
115
  const { id, label, items: nestedItems } = item
107
116
  const itemId = id ?? label
@@ -169,7 +178,15 @@ Listbox.propTypes = {
169
178
  /**
170
179
  * onClose event
171
180
  */
172
- onClose: PropTypes.func
181
+ onClose: PropTypes.func,
182
+ /**
183
+ * Test ID for testing
184
+ */
185
+ testID: PropTypes.string,
186
+ /**
187
+ * Listbox variant
188
+ */
189
+ variant: variantProp.propType
173
190
  }
174
191
 
175
192
  Listbox.Overlay = DropdownOverlay
@@ -74,7 +74,8 @@ const ListboxGroup = React.forwardRef(
74
74
  borderColor: 'transparent',
75
75
  borderRadius: 0,
76
76
  borderWidth: 0,
77
- marginBottom: 0
77
+ marginBottom: 0,
78
+ contentPanelBackgroundColor: 'transparent'
78
79
  }}
79
80
  controlRef={ref}
80
81
  >
@@ -22,13 +22,17 @@ const paddingVertical = 0
22
22
  const paddingHorizontal = 0
23
23
 
24
24
  const DropdownOverlay = React.forwardRef(
25
- ({ children, isReady = false, overlaidPosition, maxWidth, minWidth, onLayout, tokens }, ref) => {
25
+ (
26
+ { children, isReady = false, overlaidPosition, maxWidth, minWidth, onLayout, tokens, testID },
27
+ ref
28
+ ) => {
26
29
  const systemTokens = useThemeTokens('Listbox', {}, {})
27
30
 
28
31
  return (
29
32
  <View
30
33
  ref={ref}
31
34
  onLayout={onLayout}
35
+ testID={testID}
32
36
  style={[
33
37
  overlaidPosition,
34
38
  { maxWidth, minWidth },
@@ -75,7 +79,8 @@ DropdownOverlay.propTypes = {
75
79
  maxWidth: PropTypes.number,
76
80
  minWidth: PropTypes.number,
77
81
  onLayout: PropTypes.func,
78
- tokens: PropTypes.object
82
+ tokens: PropTypes.object,
83
+ testID: PropTypes.string
79
84
  }
80
85
 
81
86
  export default Platform.OS === 'web' ? withPortal(DropdownOverlay) : DropdownOverlay
@@ -39,10 +39,14 @@ const getItemStyles = ({
39
39
  color: itemColor,
40
40
  outline: itemOutline,
41
41
  textDecoration: itemTextDecoration,
42
- borderLeft: `${itemBorderLeftWidth}px solid ${itemBorderLeftColor}`,
43
- borderRight: `${itemBorderRightWidth}px solid ${itemBorderRightColor}`,
44
- borderTop: `${itemBorderTopWidth}px solid ${itemBorderTopColor}`,
45
- borderBottom: `${itemBorderBottomWidth}px solid ${itemBorderBottomColor}`,
42
+ borderLeftWidth: itemBorderLeftWidth,
43
+ borderLeftColor: itemBorderLeftColor,
44
+ borderRightWidth: itemBorderRightWidth,
45
+ borderRightColor: itemBorderRightColor,
46
+ borderTopWidth: itemBorderTopWidth,
47
+ borderTopColor: itemBorderTopColor,
48
+ borderBottomWidth: itemBorderBottomWidth,
49
+ borderBottomColor: itemBorderBottomColor,
46
50
  borderRadius: itemBorderRadius,
47
51
  justifyContent: 'center'
48
52
  })
@@ -263,6 +263,10 @@ const TextInputBase = React.forwardRef(
263
263
  // Add a space every 4 digits starting from the 5th position
264
264
  filteredText = formattedValue.replace(regex, '$1 ').trim()
265
265
  }
266
+ // Apply maxLength if provided
267
+ if (rest.maxLength && filteredText && filteredText.length > rest.maxLength) {
268
+ filteredText = filteredText.substring(0, rest.maxLength)
269
+ }
266
270
  setValue(filteredText, event)
267
271
  if (typeof onChangeText === 'function') onChangeText(filteredText, event)
268
272
  }
@@ -340,7 +344,7 @@ const TextInputBase = React.forwardRef(
340
344
  onMouseOut: handleMouseOut,
341
345
  onChange: handleChangeText,
342
346
  defaultValue: initialValue,
343
- maxLength: type === 'card' ? 19 : undefined,
347
+ maxLength: type === 'card' ? 19 : rest.maxLength,
344
348
  value: isControlled ? currentValue : undefined,
345
349
  onKeyPress
346
350
  }