@telus-uds/components-base 3.21.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.
- package/CHANGELOG.md +31 -1
- package/lib/cjs/Button/Button.js +12 -3
- package/lib/cjs/Button/ButtonBase.js +63 -10
- package/lib/cjs/Button/ButtonDropdown.js +2 -0
- package/lib/cjs/Button/ButtonGroup.js +45 -38
- package/lib/cjs/Button/propTypes.js +6 -0
- package/lib/cjs/Card/PressableCardBase.js +3 -1
- package/lib/cjs/Carousel/Carousel.js +63 -22
- package/lib/cjs/Carousel/CarouselItem/CarouselItem.js +23 -3
- package/lib/cjs/Icon/Icon.js +8 -11
- package/lib/cjs/Icon/IconText.js +0 -1
- package/lib/cjs/Listbox/GroupControl.js +33 -39
- package/lib/cjs/Listbox/Listbox.js +22 -13
- package/lib/cjs/Listbox/ListboxGroup.js +2 -1
- package/lib/cjs/Listbox/ListboxOverlay.js +5 -2
- package/lib/cjs/Listbox/PressableItem.js +8 -4
- package/lib/cjs/TextInput/TextInputBase.js +5 -1
- package/lib/cjs/ThemeProvider/index.js +9 -1
- package/lib/cjs/ThemeProvider/useResponsiveThemeTokensCallback.js +124 -0
- package/lib/cjs/Validator/Validator.js +171 -135
- package/lib/cjs/index.js +7 -0
- package/lib/esm/Button/Button.js +13 -4
- package/lib/esm/Button/ButtonBase.js +64 -11
- package/lib/esm/Button/ButtonDropdown.js +2 -0
- package/lib/esm/Button/ButtonGroup.js +44 -39
- package/lib/esm/Button/propTypes.js +6 -0
- package/lib/esm/Card/PressableCardBase.js +3 -1
- package/lib/esm/Carousel/Carousel.js +63 -22
- package/lib/esm/Carousel/CarouselItem/CarouselItem.js +23 -3
- package/lib/esm/Icon/Icon.js +8 -11
- package/lib/esm/Icon/IconText.js +0 -1
- package/lib/esm/Listbox/GroupControl.js +33 -39
- package/lib/esm/Listbox/Listbox.js +23 -14
- package/lib/esm/Listbox/ListboxGroup.js +2 -1
- package/lib/esm/Listbox/ListboxOverlay.js +5 -2
- package/lib/esm/Listbox/PressableItem.js +8 -4
- package/lib/esm/TextInput/TextInputBase.js +5 -1
- package/lib/esm/ThemeProvider/index.js +1 -0
- package/lib/esm/ThemeProvider/useResponsiveThemeTokensCallback.js +117 -0
- package/lib/esm/Validator/Validator.js +171 -135
- package/lib/esm/index.js +1 -1
- package/lib/package.json +2 -2
- package/package.json +2 -2
- package/src/Button/Button.jsx +26 -5
- package/src/Button/ButtonBase.jsx +79 -16
- package/src/Button/ButtonDropdown.jsx +2 -0
- package/src/Button/ButtonGroup.jsx +62 -45
- package/src/Button/propTypes.js +6 -0
- package/src/Card/PressableCardBase.jsx +3 -1
- package/src/Carousel/Carousel.jsx +71 -7
- package/src/Carousel/CarouselItem/CarouselItem.jsx +31 -3
- package/src/Icon/Icon.jsx +11 -14
- package/src/Icon/IconText.jsx +0 -1
- package/src/Listbox/GroupControl.jsx +41 -47
- package/src/Listbox/Listbox.jsx +26 -9
- package/src/Listbox/ListboxGroup.jsx +2 -1
- package/src/Listbox/ListboxOverlay.jsx +7 -2
- package/src/Listbox/PressableItem.jsx +8 -4
- package/src/PriceLockup/utils/renderPrice.jsx +15 -17
- package/src/TextInput/TextInputBase.jsx +5 -1
- package/src/ThemeProvider/index.js +1 -0
- package/src/ThemeProvider/useResponsiveThemeTokensCallback.js +129 -0
- package/src/Validator/Validator.jsx +180 -159
- package/src/index.js +2 -1
|
@@ -24,12 +24,12 @@ const Validator = React.forwardRef(
|
|
|
24
24
|
|
|
25
25
|
const { supportsProps } = selectProps(rest)
|
|
26
26
|
const strValidation = supportsProps.validation
|
|
27
|
-
const [, setIndividualCodes] = React.useState({})
|
|
28
|
-
const [text, setText] = React.useState(value)
|
|
29
27
|
const validatorsLength = 6
|
|
30
28
|
const prefix = 'code'
|
|
31
29
|
const sufixValidation = 'Validation'
|
|
32
30
|
|
|
31
|
+
const [codes, setCodes] = React.useState(() => Array(validatorsLength).fill(''))
|
|
32
|
+
|
|
33
33
|
const [isHover, setIsHover] = React.useState(false)
|
|
34
34
|
const handleMouseOver = () => {
|
|
35
35
|
setIsHover(true)
|
|
@@ -41,110 +41,99 @@ const Validator = React.forwardRef(
|
|
|
41
41
|
|
|
42
42
|
const themeTokens = useThemeTokens('TextInput', tokens, variant, { hover: isHover })
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
codes[prefix + i] = React.createRef()
|
|
49
|
-
valueCodes[prefix + i] = ''
|
|
50
|
-
valueCodes[prefix + i + sufixValidation] = ''
|
|
51
|
-
return null
|
|
52
|
-
})
|
|
53
|
-
return [codes, valueCodes]
|
|
54
|
-
}, [validatorsLength, prefix, sufixValidation])
|
|
55
|
-
|
|
56
|
-
const handleSingleCodes = React.useCallback(
|
|
57
|
-
(codeId, val, validation) => {
|
|
58
|
-
singleCodes[codeId] = val
|
|
59
|
-
singleCodes[codeId + sufixValidation] = validation
|
|
60
|
-
setIndividualCodes((prev) => ({
|
|
61
|
-
...prev,
|
|
62
|
-
[codeId]: val
|
|
63
|
-
}))
|
|
64
|
-
},
|
|
65
|
-
[singleCodes, sufixValidation]
|
|
66
|
-
)
|
|
44
|
+
// Create refs for input elements
|
|
45
|
+
const codeReferences = React.useMemo(() => {
|
|
46
|
+
return Array.from({ length: validatorsLength }, () => React.createRef())
|
|
47
|
+
}, [validatorsLength])
|
|
67
48
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
49
|
+
// Keep onChange and mask in refs to avoid re-creating event listeners
|
|
50
|
+
const onChangeRef = React.useRef(onChange)
|
|
51
|
+
const maskRef = React.useRef(mask)
|
|
52
|
+
React.useEffect(() => {
|
|
53
|
+
onChangeRef.current = onChange
|
|
54
|
+
maskRef.current = mask
|
|
55
|
+
}, [onChange, mask])
|
|
56
|
+
|
|
57
|
+
// Update a single code digit
|
|
58
|
+
const updateCode = React.useCallback(
|
|
59
|
+
(index, digit) => {
|
|
60
|
+
setCodes((prevCodes) => {
|
|
61
|
+
const newCodes = [...prevCodes]
|
|
62
|
+
newCodes[index] = digit
|
|
63
|
+
|
|
64
|
+
if (onChangeRef.current) {
|
|
65
|
+
const codeString = newCodes.join('')
|
|
66
|
+
const singleCodesObj = {}
|
|
67
|
+
newCodes.forEach((code, i) => {
|
|
68
|
+
singleCodesObj[prefix + i] = code
|
|
69
|
+
singleCodesObj[prefix + i + sufixValidation] = code ? 'success' : ''
|
|
70
|
+
})
|
|
71
|
+
onChangeRef.current(codeString, singleCodesObj)
|
|
72
|
+
}
|
|
77
73
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
74
|
+
return newCodes
|
|
75
|
+
})
|
|
81
76
|
},
|
|
82
|
-
[
|
|
77
|
+
[prefix, sufixValidation]
|
|
83
78
|
)
|
|
84
79
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
(_, i) => singleCodes[prefix + i] || ''
|
|
89
|
-
).join('')
|
|
90
|
-
if (typeof onChange === 'function') {
|
|
91
|
-
onChange(code, singleCodes)
|
|
92
|
-
}
|
|
93
|
-
}, [validatorsLength, singleCodes, prefix, onChange])
|
|
94
|
-
|
|
95
|
-
const handleChangeCodeValues = React.useCallback(
|
|
96
|
-
(event, codeId, nextIndex) => {
|
|
97
|
-
const codeElement = codeReferences[codeId]?.current
|
|
80
|
+
// Handle input change
|
|
81
|
+
const handleInputChange = React.useCallback(
|
|
82
|
+
(index, event) => {
|
|
98
83
|
const val = event.nativeEvent?.value || event.target?.value
|
|
99
84
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (codeElement && codeElement.value) {
|
|
104
|
-
codeElement.value = numericOnly
|
|
85
|
+
// This prevents the infinite loop where setting element.value triggers another input event
|
|
86
|
+
if (maskRef.current && val === maskRef.current.substring(0, 1)) {
|
|
87
|
+
return
|
|
105
88
|
}
|
|
106
89
|
|
|
107
|
-
|
|
108
|
-
|
|
90
|
+
const numericOnly = val.replace(/\D/g, '').substring(0, 1)
|
|
91
|
+
|
|
92
|
+
// Update state
|
|
93
|
+
updateCode(index, numericOnly)
|
|
109
94
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
95
|
+
// Update DOM element
|
|
96
|
+
const element = codeReferences[index]?.current
|
|
97
|
+
if (element) {
|
|
98
|
+
if (maskRef.current && numericOnly) {
|
|
99
|
+
element.value = maskRef.current.substring(0, 1)
|
|
100
|
+
} else {
|
|
101
|
+
element.value = numericOnly
|
|
102
|
+
}
|
|
114
103
|
}
|
|
115
104
|
|
|
116
|
-
|
|
117
|
-
|
|
105
|
+
// Move to next field if digit entered
|
|
106
|
+
if (numericOnly && index < validatorsLength - 1) {
|
|
107
|
+
const nextElement = codeReferences[index + 1]?.current
|
|
118
108
|
nextElement?.focus()
|
|
119
|
-
|
|
109
|
+
} else if (index === validatorsLength - 1) {
|
|
110
|
+
element?.blur()
|
|
120
111
|
}
|
|
121
112
|
},
|
|
122
|
-
[
|
|
123
|
-
codeReferences,
|
|
124
|
-
handleSingleCodes,
|
|
125
|
-
handleChangeCode,
|
|
126
|
-
validatorsLength,
|
|
127
|
-
changeDataMasking,
|
|
128
|
-
prefix
|
|
129
|
-
]
|
|
113
|
+
[codeReferences, updateCode, validatorsLength]
|
|
130
114
|
)
|
|
131
115
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
116
|
+
// Handle backspace
|
|
117
|
+
const handleKeyPress = React.useCallback(
|
|
118
|
+
(index, event) => {
|
|
119
|
+
if (!(event.keyCode === 8 || event.code === 'Backspace')) {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const currentElement = codeReferences[index]?.current
|
|
139
124
|
|
|
140
|
-
if (currentElement
|
|
125
|
+
if (currentElement) {
|
|
141
126
|
currentElement.value = ''
|
|
142
127
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
128
|
+
updateCode(index, '')
|
|
129
|
+
|
|
130
|
+
if (index > 0) {
|
|
131
|
+
const previousElement = codeReferences[index - 1]?.current
|
|
132
|
+
previousElement?.focus()
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
[codeReferences, updateCode]
|
|
136
|
+
)
|
|
148
137
|
|
|
149
138
|
const getCodeComponents = () => {
|
|
150
139
|
return Array.from({ length: validatorsLength }, (_, i) => {
|
|
@@ -152,11 +141,14 @@ const Validator = React.forwardRef(
|
|
|
152
141
|
const codeInputProps = {
|
|
153
142
|
nativeID: codeId,
|
|
154
143
|
keyboardType: 'numeric',
|
|
155
|
-
ref: codeReferences[
|
|
156
|
-
validation: strValidation ||
|
|
144
|
+
ref: codeReferences[i] ?? null,
|
|
145
|
+
validation: strValidation || (codes[i] ? 'success' : ''),
|
|
157
146
|
tokens: selectCodeTextInputTokens(themeTokens),
|
|
147
|
+
// Only use secureTextEntry in React Native, web handles mask differently
|
|
148
|
+
secureTextEntry: !!(Platform.OS !== 'web' && mask),
|
|
149
|
+
selectTextOnFocus: Platform.OS !== 'web',
|
|
158
150
|
onFocus: () => {
|
|
159
|
-
const element = codeReferences[
|
|
151
|
+
const element = codeReferences[i]?.current
|
|
160
152
|
if (Platform.OS === 'web' && element?.select) {
|
|
161
153
|
return element.select() ?? null
|
|
162
154
|
}
|
|
@@ -165,11 +157,39 @@ const Validator = React.forwardRef(
|
|
|
165
157
|
}
|
|
166
158
|
return null
|
|
167
159
|
},
|
|
168
|
-
onKeyPress: (event) => handleKeyPress(
|
|
160
|
+
onKeyPress: (event) => handleKeyPress(i, event),
|
|
169
161
|
onMouseOver: handleMouseOver,
|
|
170
162
|
onMouseOut: handleMouseOut,
|
|
171
163
|
inactive
|
|
172
164
|
}
|
|
165
|
+
|
|
166
|
+
// For React Native, use onChangeText and maxLength
|
|
167
|
+
if (Platform.OS !== 'web') {
|
|
168
|
+
codeInputProps.maxLength = 1
|
|
169
|
+
codeInputProps.value = codes[i]
|
|
170
|
+
codeInputProps.onChange = () => {}
|
|
171
|
+
|
|
172
|
+
codeInputProps.onChangeText = (text) => {
|
|
173
|
+
if (text) {
|
|
174
|
+
updateCode(i, text)
|
|
175
|
+
|
|
176
|
+
if (i < validatorsLength - 1) {
|
|
177
|
+
setTimeout(() => {
|
|
178
|
+
codeReferences[i + 1]?.current?.focus()
|
|
179
|
+
}, 50)
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
updateCode(i, '')
|
|
183
|
+
|
|
184
|
+
if (i > 0) {
|
|
185
|
+
setTimeout(() => {
|
|
186
|
+
codeReferences[i - 1]?.current?.focus()
|
|
187
|
+
}, 50)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
173
193
|
if (!codeInputProps.validation) {
|
|
174
194
|
delete codeInputProps.validation
|
|
175
195
|
}
|
|
@@ -182,100 +202,101 @@ const Validator = React.forwardRef(
|
|
|
182
202
|
})
|
|
183
203
|
}
|
|
184
204
|
|
|
205
|
+
// Sync external value prop to internal state
|
|
185
206
|
React.useEffect(() => {
|
|
186
|
-
if (Number(value).toString() !== 'NaN') {
|
|
187
|
-
|
|
207
|
+
if (value && Number(value).toString() !== 'NaN') {
|
|
208
|
+
const digits = value.split('').slice(0, validatorsLength)
|
|
209
|
+
const newCodes = Array(validatorsLength).fill('')
|
|
210
|
+
digits.forEach((digit, i) => {
|
|
211
|
+
if (/\d/.test(digit)) {
|
|
212
|
+
newCodes[i] = digit
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
setCodes(newCodes)
|
|
188
216
|
}
|
|
189
|
-
}, [value])
|
|
217
|
+
}, [value, validatorsLength])
|
|
190
218
|
|
|
219
|
+
// Sync codes state to DOM elements
|
|
191
220
|
React.useEffect(() => {
|
|
192
|
-
|
|
193
|
-
const element = codeReferences[
|
|
221
|
+
codes.forEach((code, i) => {
|
|
222
|
+
const element = codeReferences[i]?.current
|
|
194
223
|
if (element && element.value !== undefined) {
|
|
195
|
-
if (mask &&
|
|
196
|
-
element.value = mask
|
|
224
|
+
if (mask && code) {
|
|
225
|
+
element.value = mask.substring(0, 1)
|
|
197
226
|
} else {
|
|
198
|
-
element.value =
|
|
227
|
+
element.value = code
|
|
199
228
|
}
|
|
200
229
|
}
|
|
201
|
-
handleSingleCodes(prefix + i, text[i] ?? '', text[i] ? 'success' : '')
|
|
202
|
-
return null
|
|
203
230
|
})
|
|
204
|
-
}, [
|
|
231
|
+
}, [codes, codeReferences, mask])
|
|
205
232
|
|
|
233
|
+
// Setup event listeners - only runs once on mount
|
|
206
234
|
React.useEffect(() => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// Clear current state first
|
|
211
|
-
setText('')
|
|
212
|
-
|
|
213
|
-
// Clear all individual input fields and their state
|
|
214
|
-
Array.from({ length: validatorsLength }, (_, i) => {
|
|
215
|
-
const element = codeReferences[prefix + i]?.current
|
|
216
|
-
if (element && element.value !== undefined) {
|
|
217
|
-
element.value = ''
|
|
218
|
-
}
|
|
219
|
-
handleSingleCodes(prefix + i, '', '')
|
|
220
|
-
return null
|
|
221
|
-
})
|
|
235
|
+
if (Platform.OS !== 'web') {
|
|
236
|
+
return undefined
|
|
237
|
+
}
|
|
222
238
|
|
|
239
|
+
const handlePaste = (event) => {
|
|
240
|
+
event.preventDefault()
|
|
223
241
|
const clipBoardText = event.clipboardData.getData('text')
|
|
224
|
-
|
|
225
|
-
// Validate that input contains only digits and truncate to 6 characters
|
|
226
242
|
const numericOnly = clipBoardText.replace(/\D/g, '').substring(0, validatorsLength)
|
|
227
243
|
|
|
228
|
-
|
|
229
|
-
|
|
244
|
+
const newCodes = Array(validatorsLength).fill('')
|
|
245
|
+
numericOnly.split('').forEach((digit, i) => {
|
|
246
|
+
newCodes[i] = digit
|
|
247
|
+
})
|
|
248
|
+
setCodes(newCodes)
|
|
249
|
+
|
|
250
|
+
if (onChangeRef.current) {
|
|
251
|
+
const singleCodesObj = {}
|
|
252
|
+
newCodes.forEach((code, i) => {
|
|
253
|
+
singleCodesObj[prefix + i] = code
|
|
254
|
+
singleCodesObj[prefix + i + sufixValidation] = code ? 'success' : ''
|
|
255
|
+
})
|
|
256
|
+
onChangeRef.current(numericOnly, singleCodesObj)
|
|
230
257
|
}
|
|
231
258
|
}
|
|
232
259
|
|
|
233
260
|
const handleCopy = (event) => {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
261
|
+
setCodes((currentCodes) => {
|
|
262
|
+
const clipBoardText = currentCodes.join('')
|
|
263
|
+
event.clipboardData.setData('text/plain', clipBoardText)
|
|
264
|
+
event.preventDefault()
|
|
265
|
+
return currentCodes
|
|
266
|
+
})
|
|
240
267
|
}
|
|
241
268
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
269
|
+
// Add event listeners to each input
|
|
270
|
+
codeReferences.forEach((inputRef, i) => {
|
|
271
|
+
const element = inputRef?.current
|
|
272
|
+
if (element && typeof element.addEventListener === 'function') {
|
|
273
|
+
element.addEventListener('paste', handlePaste)
|
|
274
|
+
element.addEventListener('copy', handleCopy)
|
|
275
|
+
element.addEventListener('input', (event) => handleInputChange(i, event))
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
// Cleanup
|
|
280
|
+
return () => {
|
|
281
|
+
codeReferences.forEach((inputRef) => {
|
|
282
|
+
const element = inputRef?.current
|
|
283
|
+
if (element && typeof element.removeEventListener === 'function') {
|
|
284
|
+
element.removeEventListener('paste', handlePaste)
|
|
285
|
+
element.removeEventListener('copy', handleCopy)
|
|
286
|
+
element.removeEventListener('input', (event) =>
|
|
287
|
+
handleInputChange(event.target.dataset.index, event)
|
|
250
288
|
)
|
|
251
289
|
}
|
|
252
|
-
return null
|
|
253
290
|
})
|
|
254
291
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
element.removeEventListener('input', (event) =>
|
|
264
|
-
handleChangeCodeValues(event, prefix + i, i + 1)
|
|
265
|
-
)
|
|
266
|
-
}
|
|
267
|
-
return null
|
|
268
|
-
})
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}, [
|
|
272
|
-
validatorsLength,
|
|
273
|
-
prefix,
|
|
274
|
-
codeReferences,
|
|
275
|
-
handleChangeCodeValues,
|
|
276
|
-
handleSingleCodes,
|
|
277
|
-
singleCodes
|
|
278
|
-
])
|
|
292
|
+
/*
|
|
293
|
+
* codeReferences and handleInputChange are intentionally omitted from dependencies
|
|
294
|
+
* because we want event listeners to be registered ONLY ONCE when the component mounts.
|
|
295
|
+
* codeReferences is stable (created with useMemo) and handleInputChange is stable (useCallback).
|
|
296
|
+
* Including them would cause unnecessary re-registration of listeners on each render.
|
|
297
|
+
*/
|
|
298
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
299
|
+
}, [])
|
|
279
300
|
|
|
280
301
|
return (
|
|
281
302
|
<InputSupports
|