@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.
Files changed (64) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/lib/cjs/Button/Button.js +12 -3
  3. package/lib/cjs/Button/ButtonBase.js +63 -10
  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/Card/PressableCardBase.js +3 -1
  8. package/lib/cjs/Carousel/Carousel.js +63 -22
  9. package/lib/cjs/Carousel/CarouselItem/CarouselItem.js +23 -3
  10. package/lib/cjs/Icon/Icon.js +8 -11
  11. package/lib/cjs/Icon/IconText.js +0 -1
  12. package/lib/cjs/Listbox/GroupControl.js +33 -39
  13. package/lib/cjs/Listbox/Listbox.js +22 -13
  14. package/lib/cjs/Listbox/ListboxGroup.js +2 -1
  15. package/lib/cjs/Listbox/ListboxOverlay.js +5 -2
  16. package/lib/cjs/Listbox/PressableItem.js +8 -4
  17. package/lib/cjs/TextInput/TextInputBase.js +5 -1
  18. package/lib/cjs/ThemeProvider/index.js +9 -1
  19. package/lib/cjs/ThemeProvider/useResponsiveThemeTokensCallback.js +124 -0
  20. package/lib/cjs/Validator/Validator.js +171 -135
  21. package/lib/cjs/index.js +7 -0
  22. package/lib/esm/Button/Button.js +13 -4
  23. package/lib/esm/Button/ButtonBase.js +64 -11
  24. package/lib/esm/Button/ButtonDropdown.js +2 -0
  25. package/lib/esm/Button/ButtonGroup.js +44 -39
  26. package/lib/esm/Button/propTypes.js +6 -0
  27. package/lib/esm/Card/PressableCardBase.js +3 -1
  28. package/lib/esm/Carousel/Carousel.js +63 -22
  29. package/lib/esm/Carousel/CarouselItem/CarouselItem.js +23 -3
  30. package/lib/esm/Icon/Icon.js +8 -11
  31. package/lib/esm/Icon/IconText.js +0 -1
  32. package/lib/esm/Listbox/GroupControl.js +33 -39
  33. package/lib/esm/Listbox/Listbox.js +23 -14
  34. package/lib/esm/Listbox/ListboxGroup.js +2 -1
  35. package/lib/esm/Listbox/ListboxOverlay.js +5 -2
  36. package/lib/esm/Listbox/PressableItem.js +8 -4
  37. package/lib/esm/TextInput/TextInputBase.js +5 -1
  38. package/lib/esm/ThemeProvider/index.js +1 -0
  39. package/lib/esm/ThemeProvider/useResponsiveThemeTokensCallback.js +117 -0
  40. package/lib/esm/Validator/Validator.js +171 -135
  41. package/lib/esm/index.js +1 -1
  42. package/lib/package.json +2 -2
  43. package/package.json +2 -2
  44. package/src/Button/Button.jsx +26 -5
  45. package/src/Button/ButtonBase.jsx +79 -16
  46. package/src/Button/ButtonDropdown.jsx +2 -0
  47. package/src/Button/ButtonGroup.jsx +62 -45
  48. package/src/Button/propTypes.js +6 -0
  49. package/src/Card/PressableCardBase.jsx +3 -1
  50. package/src/Carousel/Carousel.jsx +71 -7
  51. package/src/Carousel/CarouselItem/CarouselItem.jsx +31 -3
  52. package/src/Icon/Icon.jsx +11 -14
  53. package/src/Icon/IconText.jsx +0 -1
  54. package/src/Listbox/GroupControl.jsx +41 -47
  55. package/src/Listbox/Listbox.jsx +26 -9
  56. package/src/Listbox/ListboxGroup.jsx +2 -1
  57. package/src/Listbox/ListboxOverlay.jsx +7 -2
  58. package/src/Listbox/PressableItem.jsx +8 -4
  59. package/src/PriceLockup/utils/renderPrice.jsx +15 -17
  60. package/src/TextInput/TextInputBase.jsx +5 -1
  61. package/src/ThemeProvider/index.js +1 -0
  62. package/src/ThemeProvider/useResponsiveThemeTokensCallback.js +129 -0
  63. package/src/Validator/Validator.jsx +180 -159
  64. package/src/index.js +2 -1
@@ -30,12 +30,14 @@ const DropdownOverlay = /*#__PURE__*/React.forwardRef((_ref, ref) => {
30
30
  maxWidth,
31
31
  minWidth,
32
32
  onLayout,
33
- tokens
33
+ tokens,
34
+ testID
34
35
  } = _ref;
35
36
  const systemTokens = useThemeTokens('Listbox', {}, {});
36
37
  return /*#__PURE__*/_jsx(View, {
37
38
  ref: ref,
38
39
  onLayout: onLayout,
40
+ testID: testID,
39
41
  style: [overlaidPosition, {
40
42
  maxWidth,
41
43
  minWidth
@@ -74,6 +76,7 @@ DropdownOverlay.propTypes = {
74
76
  maxWidth: PropTypes.number,
75
77
  minWidth: PropTypes.number,
76
78
  onLayout: PropTypes.func,
77
- tokens: PropTypes.object
79
+ tokens: PropTypes.object,
80
+ testID: PropTypes.string
78
81
  };
79
82
  export default Platform.OS === 'web' ? withPortal(DropdownOverlay) : DropdownOverlay;
@@ -41,10 +41,14 @@ const getItemStyles = _ref => {
41
41
  color: itemColor,
42
42
  outline: itemOutline,
43
43
  textDecoration: itemTextDecoration,
44
- borderLeft: `${itemBorderLeftWidth}px solid ${itemBorderLeftColor}`,
45
- borderRight: `${itemBorderRightWidth}px solid ${itemBorderRightColor}`,
46
- borderTop: `${itemBorderTopWidth}px solid ${itemBorderTopColor}`,
47
- borderBottom: `${itemBorderBottomWidth}px solid ${itemBorderBottomColor}`,
44
+ borderLeftWidth: itemBorderLeftWidth,
45
+ borderLeftColor: itemBorderLeftColor,
46
+ borderRightWidth: itemBorderRightWidth,
47
+ borderRightColor: itemBorderRightColor,
48
+ borderTopWidth: itemBorderTopWidth,
49
+ borderTopColor: itemBorderTopColor,
50
+ borderBottomWidth: itemBorderBottomWidth,
51
+ borderBottomColor: itemBorderBottomColor,
48
52
  borderRadius: itemBorderRadius,
49
53
  justifyContent: 'center'
50
54
  };
@@ -279,6 +279,10 @@ const TextInputBase = /*#__PURE__*/React.forwardRef((_ref8, ref) => {
279
279
  // Add a space every 4 digits starting from the 5th position
280
280
  filteredText = formattedValue.replace(regex, '$1 ').trim();
281
281
  }
282
+ // Apply maxLength if provided
283
+ if (rest.maxLength && filteredText && filteredText.length > rest.maxLength) {
284
+ filteredText = filteredText.substring(0, rest.maxLength);
285
+ }
282
286
  setValue(filteredText, event);
283
287
  if (typeof onChangeText === 'function') onChangeText(filteredText, event);
284
288
  };
@@ -351,7 +355,7 @@ const TextInputBase = /*#__PURE__*/React.forwardRef((_ref8, ref) => {
351
355
  onMouseOut: handleMouseOut,
352
356
  onChange: handleChangeText,
353
357
  defaultValue: initialValue,
354
- maxLength: type === 'card' ? 19 : undefined,
358
+ maxLength: type === 'card' ? 19 : rest.maxLength,
355
359
  value: isControlled ? currentValue : undefined,
356
360
  onKeyPress
357
361
  };
@@ -2,6 +2,7 @@ import ThemeProvider from './ThemeProvider';
2
2
  export { default as useTheme } from './useTheme';
3
3
  export { default as useSetTheme } from './useSetTheme';
4
4
  export { default as useResponsiveThemeTokens } from './useResponsiveThemeTokens';
5
+ export { default as useResponsiveThemeTokensCallback } from './useResponsiveThemeTokensCallback';
5
6
  export * from './useThemeTokens';
6
7
  export * from './utils';
7
8
  export default ThemeProvider;
@@ -0,0 +1,117 @@
1
+ import { useCallback } from 'react';
2
+ import { viewports } from '@telus-uds/system-constants';
3
+ import useTheme from './useTheme';
4
+ import { getComponentTheme, mergeAppearances, resolveThemeTokens } from './utils';
5
+ const getResponsiveThemeTokens = function (_ref, tokensProp) {
6
+ let {
7
+ rules = [],
8
+ tokens: defaultThemeTokens = {}
9
+ } = _ref;
10
+ let variants = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
11
+ let states = arguments.length > 3 ? arguments[3] : undefined;
12
+ const appearances = mergeAppearances(variants, states);
13
+ const tokensByViewport = Object.fromEntries(viewports.keys.map(viewport => [viewport, {
14
+ ...defaultThemeTokens
15
+ }]));
16
+
17
+ // Go through each rule and collect them for the corresponding viewport if they apply
18
+ rules.forEach(rule => {
19
+ if (doesRuleApply(rule, appearances)) {
20
+ // If the rule does not have a viewport specified, we collect it in all viewports
21
+ let targetViewports = rule.if.viewport || viewports.keys;
22
+ if (!Array.isArray(targetViewports)) {
23
+ targetViewports = [targetViewports];
24
+ }
25
+ targetViewports.forEach(viewport => {
26
+ tokensByViewport[viewport] = {
27
+ ...tokensByViewport[viewport],
28
+ ...rule.tokens
29
+ };
30
+ });
31
+ }
32
+ });
33
+ Object.keys(tokensByViewport).forEach(viewport => {
34
+ tokensByViewport[viewport] = resolveThemeTokens(tokensByViewport[viewport], appearances, tokensProp);
35
+ });
36
+ return tokensByViewport;
37
+ };
38
+ const doesRuleApply = (rule, appearances) => Object.entries(rule.if).every(condition => doesConditionApply(condition, appearances));
39
+ const doesConditionApply = (_ref2, appearances) => {
40
+ let [key, value] = _ref2;
41
+ if (key === 'viewport') {
42
+ return true;
43
+ }
44
+ // use null rather than undefined so we can serialise the value in themes
45
+ const appearanceValue = appearances[key] ?? null;
46
+ return Array.isArray(value) ? value.includes(appearanceValue) : value === appearanceValue;
47
+ };
48
+
49
+ /**
50
+ * @typedef {import('../utils/props/tokens.js').TokensSet} TokensSet
51
+ * @typedef {import('../utils/props/tokens.js').TokensProp} TokensProp
52
+ * @typedef {import('../utils/props/variantProp').AppearanceSet} AppearanceSet
53
+ */
54
+
55
+ /**
56
+ * Returns a memoised tokens getter function that gets responsive tokens for all viewports,
57
+ * similar to calling useResponsiveThemeTokens but with the callback pattern of useThemeTokensCallback.
58
+ *
59
+ * Scenarios where `useResponsiveThemeTokensCallback` should be used:
60
+ *
61
+ * - Where responsive tokens are to be obtained from state that is accessible only in scopes like callbacks
62
+ * and render functions, where calling useResponsiveThemeTokens directly would be disallowed by React's hook rules.
63
+ * - When using media query stylesheets and need to resolve tokens based on dynamic state (e.g., pressed, hovered)
64
+ * that changes at runtime.
65
+ * - Passing a responsive tokens getter down via a child component's `tokens` prop, applying rules using the
66
+ * child component's current state.
67
+ *
68
+ * The function returned may be called with an object of state appearances to get an object
69
+ * of tokens for each viewport, which can then be passed to createMediaQueryStyles.
70
+ *
71
+ * @example
72
+ * // Resolving responsive tokens inside Pressable's style function, based on Pressable state
73
+ * const PressMe = ({ tokens, variant, children }) => {
74
+ * const getResponsiveTokens = useResponsiveThemeTokensCallback('PressMe', tokens, variant)
75
+ * const getPressableStyle = ({ pressed }) => {
76
+ * const responsiveTokens = getResponsiveTokens({ pressed })
77
+ * const mediaQueryStyles = createMediaQueryStyles(responsiveTokens)
78
+ * return mediaQueryStyles
79
+ * }
80
+ * return <Pressable style={getPressableStyle}>{children}</Pressable>
81
+ * }
82
+ *
83
+ * @example
84
+ * // Setting the theme in a parent and resolving it in a child based on child's state
85
+ * const MenuButton = ({ tokens, variant, ...buttonProps }) => {
86
+ * // Define what theme, variant etc we want in this component...
87
+ * const getResponsiveTokens = useResponsiveThemeTokensCallback('Button', tokens, variant)
88
+ * // ...resolve them in another component based on its state (e.g. press, hover...)
89
+ * return <ButtonBase tokens={getResponsiveTokens} accessibilityRole="menuitem" {...buttonProps} />
90
+ * }
91
+ *
92
+ * @typedef {Object} ResponsiveObject
93
+ * @property {TokensSet} xs
94
+ * @property {TokensSet} sm
95
+ * @property {TokensSet} md
96
+ * @property {TokensSet} lg
97
+ * @property {TokensSet} xl
98
+ *
99
+ * @param {string} componentName - the name as defined in the theme schema of the component whose theme is to be used
100
+ * @param {TokensProp} [tokens] - every themed component should accept a `tokens` prop allowing theme tokens to be overridden
101
+ * @param {AppearanceSet} [variants] - variants passed in as props that don't change dynamically
102
+ * @returns {(states: AppearanceSet, tokenOverrides?: TokensProp) => ResponsiveObject}
103
+ * - callback function that returns an overridable responsive tokens object for current state. Only pass
104
+ * tokenOverrides in rare cases where tokens overrides are also generated outside hook scope.
105
+ */
106
+ const useResponsiveThemeTokensCallback = function (componentName) {
107
+ let tokens = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
108
+ let variants = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
109
+ const theme = useTheme();
110
+ const componentTheme = getComponentTheme(theme, componentName);
111
+ const getResponsiveThemeTokensCallback = useCallback((states, tokenOverrides) => {
112
+ const resolvedTokens = resolveThemeTokens(tokens, mergeAppearances(variants, states), tokenOverrides);
113
+ return getResponsiveThemeTokens(componentTheme, resolvedTokens, variants, states);
114
+ }, [componentTheme, tokens, variants]);
115
+ return getResponsiveThemeTokensCallback;
116
+ };
117
+ export default useResponsiveThemeTokensCallback;
@@ -37,11 +37,10 @@ const Validator = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
37
37
  supportsProps
38
38
  } = selectProps(rest);
39
39
  const strValidation = supportsProps.validation;
40
- const [, setIndividualCodes] = React.useState({});
41
- const [text, setText] = React.useState(value);
42
40
  const validatorsLength = 6;
43
41
  const prefix = 'code';
44
42
  const sufixValidation = 'Validation';
43
+ const [codes, setCodes] = React.useState(() => Array(validatorsLength).fill(''));
45
44
  const [isHover, setIsHover] = React.useState(false);
46
45
  const handleMouseOver = () => {
47
46
  setIsHover(true);
@@ -52,84 +51,87 @@ const Validator = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
52
51
  const themeTokens = useThemeTokens('TextInput', tokens, variant, {
53
52
  hover: isHover
54
53
  });
55
- const [codeReferences, singleCodes] = React.useMemo(() => {
56
- const codes = [];
57
- const valueCodes = {};
58
- Array.from({
54
+
55
+ // Create refs for input elements
56
+ const codeReferences = React.useMemo(() => {
57
+ return Array.from({
59
58
  length: validatorsLength
60
- }, (_, i) => {
61
- codes[prefix + i] = /*#__PURE__*/React.createRef();
62
- valueCodes[prefix + i] = '';
63
- valueCodes[prefix + i + sufixValidation] = '';
64
- return null;
59
+ }, () => /*#__PURE__*/React.createRef());
60
+ }, [validatorsLength]);
61
+
62
+ // Keep onChange and mask in refs to avoid re-creating event listeners
63
+ const onChangeRef = React.useRef(onChange);
64
+ const maskRef = React.useRef(mask);
65
+ React.useEffect(() => {
66
+ onChangeRef.current = onChange;
67
+ maskRef.current = mask;
68
+ }, [onChange, mask]);
69
+
70
+ // Update a single code digit
71
+ const updateCode = React.useCallback((index, digit) => {
72
+ setCodes(prevCodes => {
73
+ const newCodes = [...prevCodes];
74
+ newCodes[index] = digit;
75
+ if (onChangeRef.current) {
76
+ const codeString = newCodes.join('');
77
+ const singleCodesObj = {};
78
+ newCodes.forEach((code, i) => {
79
+ singleCodesObj[prefix + i] = code;
80
+ singleCodesObj[prefix + i + sufixValidation] = code ? 'success' : '';
81
+ });
82
+ onChangeRef.current(codeString, singleCodesObj);
83
+ }
84
+ return newCodes;
65
85
  });
66
- return [codes, valueCodes];
67
- }, [validatorsLength, prefix, sufixValidation]);
68
- const handleSingleCodes = React.useCallback((codeId, val, validation) => {
69
- singleCodes[codeId] = val;
70
- singleCodes[codeId + sufixValidation] = validation;
71
- setIndividualCodes(prev => ({
72
- ...prev,
73
- [codeId]: val
74
- }));
75
- }, [singleCodes, sufixValidation]);
76
- const changeDataMasking = React.useCallback(boxElement => {
77
- let charMasking = '';
78
- const element = boxElement;
79
- if (mask && mask.length === 1) {
80
- charMasking = mask;
81
- } else if (mask && mask.length > 1) {
82
- charMasking = mask.substring(0, 1);
83
- }
84
- if (charMasking && element) {
85
- element.value = charMasking;
86
- }
87
- }, [mask]);
88
- const handleChangeCode = React.useCallback(() => {
89
- const code = Array.from({
90
- length: validatorsLength
91
- }, (_, i) => singleCodes[prefix + i] || '').join('');
92
- if (typeof onChange === 'function') {
93
- onChange(code, singleCodes);
94
- }
95
- }, [validatorsLength, singleCodes, prefix, onChange]);
96
- const handleChangeCodeValues = React.useCallback((event, codeId, nextIndex) => {
97
- const codeElement = codeReferences[codeId]?.current;
86
+ }, [prefix, sufixValidation]);
87
+
88
+ // Handle input change
89
+ const handleInputChange = React.useCallback((index, event) => {
98
90
  const val = event.nativeEvent?.value || event.target?.value;
99
91
 
100
- // Only allow numeric characters and limit to single digit
101
- const numericOnly = val.replace(/\D/g, '').substring(0, 1);
102
- if (codeElement && codeElement.value) {
103
- codeElement.value = numericOnly;
104
- }
105
- handleSingleCodes(codeId, numericOnly, numericOnly ? 'success' : '');
106
- handleChangeCode();
107
- if (nextIndex === validatorsLength) {
108
- codeElement?.blur();
109
- changeDataMasking(codeElement);
92
+ // This prevents the infinite loop where setting element.value triggers another input event
93
+ if (maskRef.current && val === maskRef.current.substring(0, 1)) {
110
94
  return;
111
95
  }
112
- if (numericOnly.length > 0) {
113
- const nextElement = codeReferences[prefix + nextIndex]?.current;
96
+ const numericOnly = val.replace(/\D/g, '').substring(0, 1);
97
+
98
+ // Update state
99
+ updateCode(index, numericOnly);
100
+
101
+ // Update DOM element
102
+ const element = codeReferences[index]?.current;
103
+ if (element) {
104
+ if (maskRef.current && numericOnly) {
105
+ element.value = maskRef.current.substring(0, 1);
106
+ } else {
107
+ element.value = numericOnly;
108
+ }
109
+ }
110
+
111
+ // Move to next field if digit entered
112
+ if (numericOnly && index < validatorsLength - 1) {
113
+ const nextElement = codeReferences[index + 1]?.current;
114
114
  nextElement?.focus();
115
- changeDataMasking(codeElement);
115
+ } else if (index === validatorsLength - 1) {
116
+ element?.blur();
116
117
  }
117
- }, [codeReferences, handleSingleCodes, handleChangeCode, validatorsLength, changeDataMasking, prefix]);
118
- const handleKeyPress = (event, currentIndex, previousIndex) => {
118
+ }, [codeReferences, updateCode, validatorsLength]);
119
+
120
+ // Handle backspace
121
+ const handleKeyPress = React.useCallback((index, event) => {
119
122
  if (!(event.keyCode === 8 || event.code === 'Backspace')) {
120
123
  return;
121
124
  }
122
- if (currentIndex > 0) {
123
- const currentElement = codeReferences[prefix + currentIndex]?.current;
124
- const previousElement = codeReferences[prefix + previousIndex]?.current;
125
- if (currentElement && currentElement.value) {
126
- currentElement.value = '';
127
- }
125
+ const currentElement = codeReferences[index]?.current;
126
+ if (currentElement) {
127
+ currentElement.value = '';
128
+ }
129
+ updateCode(index, '');
130
+ if (index > 0) {
131
+ const previousElement = codeReferences[index - 1]?.current;
128
132
  previousElement?.focus();
129
133
  }
130
- handleSingleCodes(prefix + currentIndex, '', '');
131
- handleChangeCode();
132
- };
134
+ }, [codeReferences, updateCode]);
133
135
  const getCodeComponents = () => {
134
136
  return Array.from({
135
137
  length: validatorsLength
@@ -138,11 +140,14 @@ const Validator = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
138
140
  const codeInputProps = {
139
141
  nativeID: codeId,
140
142
  keyboardType: 'numeric',
141
- ref: codeReferences[codeId] ?? null,
142
- validation: strValidation || singleCodes[codeId + sufixValidation],
143
+ ref: codeReferences[i] ?? null,
144
+ validation: strValidation || (codes[i] ? 'success' : ''),
143
145
  tokens: selectCodeTextInputTokens(themeTokens),
146
+ // Only use secureTextEntry in React Native, web handles mask differently
147
+ secureTextEntry: !!(Platform.OS !== 'web' && mask),
148
+ selectTextOnFocus: Platform.OS !== 'web',
144
149
  onFocus: () => {
145
- const element = codeReferences[codeId]?.current;
150
+ const element = codeReferences[i]?.current;
146
151
  if (Platform.OS === 'web' && element?.select) {
147
152
  return element.select() ?? null;
148
153
  }
@@ -151,11 +156,35 @@ const Validator = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
151
156
  }
152
157
  return null;
153
158
  },
154
- onKeyPress: event => handleKeyPress(event, i, i - 1),
159
+ onKeyPress: event => handleKeyPress(i, event),
155
160
  onMouseOver: handleMouseOver,
156
161
  onMouseOut: handleMouseOut,
157
162
  inactive
158
163
  };
164
+
165
+ // For React Native, use onChangeText and maxLength
166
+ if (Platform.OS !== 'web') {
167
+ codeInputProps.maxLength = 1;
168
+ codeInputProps.value = codes[i];
169
+ codeInputProps.onChange = () => {};
170
+ codeInputProps.onChangeText = text => {
171
+ if (text) {
172
+ updateCode(i, text);
173
+ if (i < validatorsLength - 1) {
174
+ setTimeout(() => {
175
+ codeReferences[i + 1]?.current?.focus();
176
+ }, 50);
177
+ }
178
+ } else {
179
+ updateCode(i, '');
180
+ if (i > 0) {
181
+ setTimeout(() => {
182
+ codeReferences[i - 1]?.current?.focus();
183
+ }, 50);
184
+ }
185
+ }
186
+ };
187
+ }
159
188
  if (!codeInputProps.validation) {
160
189
  delete codeInputProps.validation;
161
190
  }
@@ -167,89 +196,96 @@ const Validator = /*#__PURE__*/React.forwardRef((_ref2, ref) => {
167
196
  }, codeId);
168
197
  });
169
198
  };
199
+
200
+ // Sync external value prop to internal state
170
201
  React.useEffect(() => {
171
- if (Number(value).toString() !== 'NaN') {
172
- setText(value);
202
+ if (value && Number(value).toString() !== 'NaN') {
203
+ const digits = value.split('').slice(0, validatorsLength);
204
+ const newCodes = Array(validatorsLength).fill('');
205
+ digits.forEach((digit, i) => {
206
+ if (/\d/.test(digit)) {
207
+ newCodes[i] = digit;
208
+ }
209
+ });
210
+ setCodes(newCodes);
173
211
  }
174
- }, [value]);
212
+ }, [value, validatorsLength]);
213
+
214
+ // Sync codes state to DOM elements
175
215
  React.useEffect(() => {
176
- Array.from({
177
- length: validatorsLength
178
- }, (_, i) => {
179
- const element = codeReferences[prefix + i]?.current;
216
+ codes.forEach((code, i) => {
217
+ const element = codeReferences[i]?.current;
180
218
  if (element && element.value !== undefined) {
181
- if (mask && text[i]) {
182
- element.value = mask;
219
+ if (mask && code) {
220
+ element.value = mask.substring(0, 1);
183
221
  } else {
184
- element.value = text[i] ?? '';
222
+ element.value = code;
185
223
  }
186
224
  }
187
- handleSingleCodes(prefix + i, text[i] ?? '', text[i] ? 'success' : '');
188
- return null;
189
225
  });
190
- }, [text, mask, validatorsLength, prefix, codeReferences, handleSingleCodes]);
226
+ }, [codes, codeReferences, mask]);
227
+
228
+ // Setup event listeners - only runs once on mount
191
229
  React.useEffect(() => {
192
- const handlePasteCode = event => {
230
+ if (Platform.OS !== 'web') {
231
+ return undefined;
232
+ }
233
+ const handlePaste = event => {
193
234
  event.preventDefault();
194
-
195
- // Clear current state first
196
- setText('');
197
-
198
- // Clear all individual input fields and their state
199
- Array.from({
200
- length: validatorsLength
201
- }, (_, i) => {
202
- const element = codeReferences[prefix + i]?.current;
203
- if (element && element.value !== undefined) {
204
- element.value = '';
205
- }
206
- handleSingleCodes(prefix + i, '', '');
207
- return null;
208
- });
209
235
  const clipBoardText = event.clipboardData.getData('text');
210
-
211
- // Validate that input contains only digits and truncate to 6 characters
212
236
  const numericOnly = clipBoardText.replace(/\D/g, '').substring(0, validatorsLength);
213
- if (numericOnly.length > 0) {
214
- setText(numericOnly);
237
+ const newCodes = Array(validatorsLength).fill('');
238
+ numericOnly.split('').forEach((digit, i) => {
239
+ newCodes[i] = digit;
240
+ });
241
+ setCodes(newCodes);
242
+ if (onChangeRef.current) {
243
+ const singleCodesObj = {};
244
+ newCodes.forEach((code, i) => {
245
+ singleCodesObj[prefix + i] = code;
246
+ singleCodesObj[prefix + i + sufixValidation] = code ? 'success' : '';
247
+ });
248
+ onChangeRef.current(numericOnly, singleCodesObj);
215
249
  }
216
250
  };
217
251
  const handleCopy = event => {
218
- const clipBoardText = Array.from({
219
- length: validatorsLength
220
- }, (_, i) => singleCodes[prefix + i] || '').join('');
221
- event.clipboardData.setData('text/plain', clipBoardText);
222
- event.preventDefault();
252
+ setCodes(currentCodes => {
253
+ const clipBoardText = currentCodes.join('');
254
+ event.clipboardData.setData('text/plain', clipBoardText);
255
+ event.preventDefault();
256
+ return currentCodes;
257
+ });
223
258
  };
224
- if (Platform.OS === 'web') {
225
- Array.from({
226
- length: validatorsLength
227
- }, (_, i) => {
228
- const element = codeReferences[prefix + i]?.current;
229
- if (element && typeof element.addEventListener === 'function') {
230
- element.addEventListener('paste', handlePasteCode);
231
- element.addEventListener('copy', handleCopy);
232
- element.addEventListener('input', event => handleChangeCodeValues(event, prefix + i, i + 1));
259
+
260
+ // Add event listeners to each input
261
+ codeReferences.forEach((inputRef, i) => {
262
+ const element = inputRef?.current;
263
+ if (element && typeof element.addEventListener === 'function') {
264
+ element.addEventListener('paste', handlePaste);
265
+ element.addEventListener('copy', handleCopy);
266
+ element.addEventListener('input', event => handleInputChange(i, event));
267
+ }
268
+ });
269
+
270
+ // Cleanup
271
+ return () => {
272
+ codeReferences.forEach(inputRef => {
273
+ const element = inputRef?.current;
274
+ if (element && typeof element.removeEventListener === 'function') {
275
+ element.removeEventListener('paste', handlePaste);
276
+ element.removeEventListener('copy', handleCopy);
277
+ element.removeEventListener('input', event => handleInputChange(event.target.dataset.index, event));
233
278
  }
234
- return null;
235
279
  });
236
- }
237
- return () => {
238
- if (Platform.OS === 'web') {
239
- Array.from({
240
- length: validatorsLength
241
- }, (_, i) => {
242
- const element = codeReferences[prefix + i]?.current;
243
- if (element && typeof element.removeEventListener === 'function') {
244
- element.removeEventListener('paste', handlePasteCode);
245
- element.removeEventListener('copy', handleCopy);
246
- element.removeEventListener('input', event => handleChangeCodeValues(event, prefix + i, i + 1));
247
- }
248
- return null;
249
- });
250
- }
251
280
  };
252
- }, [validatorsLength, prefix, codeReferences, handleChangeCodeValues, handleSingleCodes, singleCodes]);
281
+ /*
282
+ * codeReferences and handleInputChange are intentionally omitted from dependencies
283
+ * because we want event listeners to be registered ONLY ONCE when the component mounts.
284
+ * codeReferences is stable (created with useMemo) and handleInputChange is stable (useCallback).
285
+ * Including them would cause unnecessary re-registration of listeners on each render.
286
+ */
287
+ // eslint-disable-next-line react-hooks/exhaustive-deps
288
+ }, []);
253
289
  return /*#__PURE__*/_jsx(InputSupports, {
254
290
  ...supportsProps,
255
291
  feedbackProps: {
package/lib/esm/index.js CHANGED
@@ -68,6 +68,6 @@ export { default as BaseProvider } from './BaseProvider';
68
68
  export { useHydrationContext } from './BaseProvider/HydrationContext';
69
69
  export { default as Validator } from './Validator';
70
70
  export { default as ViewportProvider, useViewport, ViewportContext } from './ViewportProvider';
71
- export { default as ThemeProvider, useTheme, useSetTheme, useThemeTokens, useThemeTokensCallback, getThemeTokens, applyOuterBorder, applyTextStyles, applyShadowToken, useResponsiveThemeTokens } from './ThemeProvider';
71
+ export { default as ThemeProvider, useTheme, useSetTheme, useThemeTokens, useThemeTokensCallback, getThemeTokens, applyOuterBorder, applyTextStyles, applyShadowToken, useResponsiveThemeTokens, useResponsiveThemeTokensCallback } from './ThemeProvider';
72
72
  export * from './utils';
73
73
  export { default as Portal } from './Portal';
package/lib/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "@gorhom/portal": "^1.0.14",
13
13
  "@react-native-picker/picker": "^2.9.0",
14
14
  "@telus-uds/system-constants": "^3.0.0",
15
- "@telus-uds/system-theme-tokens": "^4.15.1",
15
+ "@telus-uds/system-theme-tokens": "^4.16.0",
16
16
  "airbnb-prop-types": "^2.16.0",
17
17
  "css-mediaquery": "^0.1.2",
18
18
  "expo-document-picker": "^13.0.1",
@@ -84,6 +84,6 @@
84
84
  "standard-engine": {
85
85
  "skip": true
86
86
  },
87
- "version": "3.21.0",
87
+ "version": "3.23.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "@gorhom/portal": "^1.0.14",
13
13
  "@react-native-picker/picker": "^2.9.0",
14
14
  "@telus-uds/system-constants": "^3.0.0",
15
- "@telus-uds/system-theme-tokens": "^4.15.1",
15
+ "@telus-uds/system-theme-tokens": "^4.16.0",
16
16
  "airbnb-prop-types": "^2.16.0",
17
17
  "css-mediaquery": "^0.1.2",
18
18
  "expo-document-picker": "^13.0.1",
@@ -84,6 +84,6 @@
84
84
  "standard-engine": {
85
85
  "skip": true
86
86
  },
87
- "version": "3.21.0",
87
+ "version": "3.23.0",
88
88
  "types": "types/index.d.ts"
89
89
  }
@@ -2,17 +2,38 @@ import React from 'react'
2
2
 
3
3
  import ButtonBase from './ButtonBase'
4
4
  import buttonPropTypes, { textAndA11yText } from './propTypes'
5
- import { useThemeTokensCallback } from '../ThemeProvider'
5
+ import {
6
+ useThemeTokensCallback,
7
+ useResponsiveThemeTokensCallback,
8
+ useTheme
9
+ } from '../ThemeProvider'
6
10
  import { a11yProps } from '../utils/props'
7
11
  import { useViewport } from '../ViewportProvider'
8
12
 
9
13
  const Button = React.forwardRef(
10
- ({ accessibilityRole = 'button', tokens, variant, ...props }, ref) => {
14
+ ({ accessibilityRole = 'button', tokens, variant, heightFull = true, ...props }, ref) => {
11
15
  const viewport = useViewport()
12
- const buttonVariant = { viewport, ...variant }
13
- const getTokens = useThemeTokensCallback('Button', tokens, buttonVariant)
16
+ const {
17
+ themeOptions: { enableMediaQueryStyleSheet }
18
+ } = useTheme()
19
+
20
+ const buttonVariant = enableMediaQueryStyleSheet ? variant : { viewport, ...variant }
21
+
22
+ const useTokens = enableMediaQueryStyleSheet
23
+ ? useResponsiveThemeTokensCallback
24
+ : useThemeTokensCallback
25
+
26
+ const getTokens = useTokens('Button', tokens, buttonVariant)
27
+
14
28
  return (
15
- <ButtonBase {...props} tokens={getTokens} accessibilityRole={accessibilityRole} ref={ref} />
29
+ <ButtonBase
30
+ {...props}
31
+ tokens={getTokens}
32
+ heightFull={heightFull}
33
+ accessibilityRole={accessibilityRole}
34
+ ref={ref}
35
+ viewport={viewport}
36
+ />
16
37
  )
17
38
  }
18
39
  )