@telus-uds/components-base 1.65.0 → 1.66.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 CHANGED
@@ -1,12 +1,22 @@
1
1
  # Change Log - @telus-uds/components-base
2
2
 
3
- This log was last generated on Thu, 26 Oct 2023 19:24:58 GMT and should not be manually modified.
3
+ This log was last generated on Wed, 01 Nov 2023 00:54:31 GMT and should not be manually modified.
4
4
 
5
5
  <!-- Start content -->
6
6
 
7
+ ## 1.66.0
8
+
9
+ Wed, 01 Nov 2023 00:54:31 GMT
10
+
11
+ ### Minor changes
12
+
13
+ - card type for textInput component (srikanthkhari@gmail.com)
14
+ - Add responsive `width` variant to `ButtonGroup` (shahzaibkhalidmalik@outlook.com)
15
+ - Bump @telus-uds/system-theme-tokens to v2.43.0
16
+
7
17
  ## 1.65.0
8
18
 
9
- Thu, 26 Oct 2023 19:24:58 GMT
19
+ Thu, 26 Oct 2023 19:35:09 GMT
10
20
 
11
21
  ### Minor changes
12
22
 
@@ -23,6 +23,8 @@ var _StackView = _interopRequireDefault(require("../StackView"));
23
23
 
24
24
  var _IconButton = _interopRequireDefault(require("../IconButton"));
25
25
 
26
+ var _Icon = _interopRequireDefault(require("../Icon"));
27
+
26
28
  var _utils = require("../utils");
27
29
 
28
30
  var _dictionary = _interopRequireDefault(require("./dictionary"));
@@ -37,7 +39,7 @@ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj &&
37
39
 
38
40
  const [selectProps, selectedSystemPropTypes] = (0, _utils.selectSystemProps)([_utils.a11yProps, _utils.textInputHandlerProps, _utils.textInputProps, _utils.viewProps]);
39
41
 
40
- const selectInputStyles = (_ref, themeOptions, inactive) => {
42
+ const selectInputStyles = (_ref, themeOptions, inactive, type) => {
41
43
  let {
42
44
  backgroundColor,
43
45
  color,
@@ -98,7 +100,7 @@ const selectInputStyles = (_ref, themeOptions, inactive) => {
98
100
  borderWidth,
99
101
  borderColor,
100
102
  borderRadius,
101
- paddingLeft: offsetBorder(paddingLeft),
103
+ paddingLeft: type === 'card' ? offsetBorder(paddingLeft + 34) : offsetBorder(paddingLeft),
102
104
  paddingRight: icon ? offsetBorder(paddingWithIcon) : offsetBorder(paddingRight),
103
105
  paddingTop: offsetBorder(paddingTop),
104
106
  paddingBottom: offsetBorder(paddingBottom),
@@ -152,16 +154,64 @@ const selectIconContainerStyles = (_ref4, buttonCount) => {
152
154
  };
153
155
  };
154
156
 
155
- const selectButtonsContainerStyle = _ref5 => {
157
+ const selectLeftIconContainerStyles = _ref5 => {
156
158
  let {
157
- buttonsPaddingRight
159
+ leftIconPaddingBottom
158
160
  } = _ref5;
161
+ return {
162
+ // not tokenizing paddingLeft as it remains same across brands for now
163
+ paddingLeft: 10,
164
+ paddingBottom: leftIconPaddingBottom
165
+ };
166
+ };
167
+
168
+ const selectButtonsContainerStyle = _ref6 => {
169
+ let {
170
+ buttonsPaddingRight
171
+ } = _ref6;
159
172
  return {
160
173
  paddingRight: buttonsPaddingRight
161
174
  };
162
175
  };
163
176
 
164
- const TextInputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref6, ref) => {
177
+ const getIcon = function () {
178
+ let cardNumber = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
179
+ let {
180
+ defaultCreditIcon,
181
+ amexIcon,
182
+ visaIcon,
183
+ masterCardIcon
184
+ } = arguments.length > 1 ? arguments[1] : undefined;
185
+ const cardType = {
186
+ 1: {
187
+ icon: visaIcon,
188
+ testID: 'visa'
189
+ },
190
+ 2: {
191
+ icon: amexIcon,
192
+ testID: 'amex'
193
+ },
194
+ 4: {
195
+ icon: masterCardIcon,
196
+ testID: 'mastercard'
197
+ }
198
+ };
199
+ const firstDigit = cardNumber ? cardNumber[0] : '';
200
+ const defaultIcon = {
201
+ icon: defaultCreditIcon,
202
+ testID: 'default'
203
+ };
204
+ const selectedIcon = cardNumber.length > 4 ? cardType[firstDigit] || defaultIcon : defaultIcon;
205
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_Icon.default, {
206
+ icon: selectedIcon.icon,
207
+ variant: {
208
+ size: 'large'
209
+ },
210
+ testID: selectedIcon.testID
211
+ });
212
+ };
213
+
214
+ const TextInputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref7, ref) => {
165
215
  let {
166
216
  buttons = [],
167
217
  copy = 'en',
@@ -180,8 +230,9 @@ const TextInputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref6, ref) => {
180
230
  tokens,
181
231
  value,
182
232
  variant = {},
233
+ type,
183
234
  ...rest
184
- } = _ref6;
235
+ } = _ref7;
185
236
  const [isFocused, setIsFocused] = (0, _react.useState)(false);
186
237
  const [showPassword, setShowPassword] = (0, _react.useState)(false);
187
238
 
@@ -222,6 +273,12 @@ const TextInputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref6, ref) => {
222
273
  onChange,
223
274
  readOnly
224
275
  });
276
+ const {
277
+ password,
278
+ numeric
279
+ } = variant;
280
+ const isNumeric = numeric || type === 'card' || type === 'number';
281
+ const isPassword = password || type === 'password';
225
282
  const element = inputRef === null || inputRef === void 0 ? void 0 : inputRef.current;
226
283
  (0, _react.useEffect)(() => {
227
284
  if (_Platform.default.OS === 'web' && pattern && element) {
@@ -234,11 +291,16 @@ const TextInputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref6, ref) => {
234
291
  const handleChangeText = event => {
235
292
  var _event$nativeEvent, _event$target;
236
293
 
237
- const {
238
- numeric
239
- } = variant;
240
294
  const text = ((_event$nativeEvent = event.nativeEvent) === null || _event$nativeEvent === void 0 ? void 0 : _event$nativeEvent.text) || ((_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.value);
241
- const filteredText = numeric ? text.replace(/[^\d]/g, '') : text;
295
+ let filteredText = isNumeric ? text === null || text === void 0 ? void 0 : text.replace(/[^\d]/g, '') : text;
296
+
297
+ if (type === 'card' && filteredText) {
298
+ const formattedValue = filteredText.replace(/[^a-zA-Z0-9]/g, '');
299
+ const regex = new RegExp(`([a-zA-Z0-9]{4})(?=[a-zA-Z0-9])`, 'g'); // Add a space every 4 digits starting from the 5th position
300
+
301
+ filteredText = formattedValue.replace(regex, '$1 ').trim();
302
+ }
303
+
242
304
  setValue(filteredText, event);
243
305
  if (typeof onChangeText === 'function') onChangeText(filteredText, event);
244
306
  };
@@ -267,7 +329,11 @@ const TextInputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref6, ref) => {
267
329
  clearButtonIcon: ClearButtonIcon,
268
330
  icon: IconComponent,
269
331
  passwordShowButtonIcon,
270
- passwordHideButtonIcon
332
+ passwordHideButtonIcon,
333
+ defaultCreditIcon,
334
+ amexIcon,
335
+ visaIcon,
336
+ masterCardIcon
271
337
  } = themeTokens;
272
338
  const buttonsGapSize = (0, _utils.useSpacingScale)(buttonsGap);
273
339
  const getCopy = (0, _utils.useCopy)({
@@ -287,7 +353,7 @@ const TextInputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref6, ref) => {
287
353
  }, "clear"));
288
354
  }
289
355
 
290
- if (variant.password) {
356
+ if (isPassword) {
291
357
  textInputButtons === null || textInputButtons === void 0 ? void 0 : textInputButtons.unshift( /*#__PURE__*/(0, _jsxRuntime.jsx)(_IconButton.default, {
292
358
  accessibilityLabel: !showPassword ? getCopy('hidePasswordAccessibilityLabel') : getCopy('showPasswordAccessibilityLabel'),
293
359
  icon: !showPassword ? passwordShowButtonIcon : passwordHideButtonIcon,
@@ -313,6 +379,7 @@ const TextInputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref6, ref) => {
313
379
  onMouseOut: handleMouseOut,
314
380
  onChange: handleChangeText,
315
381
  defaultValue: initialValue,
382
+ maxLength: type === 'card' ? 19 : undefined,
316
383
  value: isControlled ? currentValue : undefined
317
384
  };
318
385
  const {
@@ -320,20 +387,29 @@ const TextInputBase = /*#__PURE__*/(0, _react.forwardRef)((_ref6, ref) => {
320
387
  } = (0, _ThemeProvider.useTheme)();
321
388
  const nativeInputStyle = selectInputStyles({ ...themeTokens,
322
389
  height
323
- }, themeOptions, inactive);
390
+ }, themeOptions, inactive, type);
324
391
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_View.default, {
325
392
  style: selectOuterBorderStyles(themeTokens),
326
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_TextInput.default, {
393
+ children: [type === 'card' && /*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
394
+ pointerEvents: "none",
395
+ style: [staticStyles.leftIconContainer, selectLeftIconContainerStyles(themeTokens)],
396
+ children: getIcon(currentValue, {
397
+ defaultCreditIcon,
398
+ amexIcon,
399
+ visaIcon,
400
+ masterCardIcon
401
+ })
402
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_TextInput.default, {
327
403
  ref: inputRef,
328
- keyboardType: variant.numeric && 'numeric',
329
- inputMode: variant.numeric && 'numeric',
404
+ keyboardType: isNumeric && 'numeric',
405
+ inputMode: isNumeric && 'numeric',
330
406
  style: nativeInputStyle,
331
- secureTextEntry: variant.password && !showPassword,
407
+ secureTextEntry: isPassword && !showPassword,
332
408
  ...inputProps
333
409
  }), IconComponent && /*#__PURE__*/(0, _jsxRuntime.jsx)(_View.default, {
334
410
  pointerEvents: "none" // avoid hijacking input press events
335
411
  ,
336
- style: [staticStyles.iconContainer, selectIconContainerStyles({ ...themeTokens,
412
+ style: [staticStyles.rightIconContainer, selectIconContainerStyles({ ...themeTokens,
337
413
  buttonsGapSize
338
414
  }, buttons === null || buttons === void 0 ? void 0 : buttons.length)],
339
415
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(IconComponent, { ...selectIconTokens(themeTokens)
@@ -359,6 +435,7 @@ TextInputBase.propTypes = { ...selectedSystemPropTypes,
359
435
  copy: _propTypes.default.oneOfType([_propTypes.default.oneOf(['en', 'fr']), _propTypes.default.shape({
360
436
  clearButtonAccessibilityLabel: _propTypes.default.string
361
437
  })]),
438
+ type: _propTypes.default.oneOfType([_propTypes.default.oneOf(['password', 'card', 'number'])]),
362
439
  height: _propTypes.default.number,
363
440
  inactive: _propTypes.default.bool,
364
441
  initialValue: _propTypes.default.string,
@@ -387,9 +464,15 @@ const staticStyles = _StyleSheet.default.create({
387
464
  bottom: 0,
388
465
  justifyContent: 'center'
389
466
  },
390
- iconContainer: {
467
+ rightIconContainer: {
391
468
  position: 'absolute',
392
469
  right: 0,
393
470
  bottom: 0
471
+ },
472
+ leftIconContainer: {
473
+ position: 'absolute',
474
+ left: 0,
475
+ bottom: 0,
476
+ zIndex: 1
394
477
  }
395
478
  });
@@ -18,6 +18,11 @@ const textInputPropTypes = {
18
18
  */
19
19
  value: _propTypes.default.string,
20
20
 
21
+ /**
22
+ * Use this to set the type of the input. Defaults to `text`.
23
+ */
24
+ type: _propTypes.default.string,
25
+
21
26
  /**
22
27
  * Use this to set the initial value of an uncontrolled input.
23
28
  * Updating `initialValue` will **not** update the actual value.
@@ -31,6 +31,7 @@ const textProps = {
31
31
 
32
32
  const inputValueProps = {
33
33
  value: _propTypes.default.string,
34
+ type: _propTypes.default.string,
34
35
  initialValue: _propTypes.default.string,
35
36
  readOnly: _propTypes.default.bool,
36
37
  inactive: _propTypes.default.bool
@@ -7,13 +7,14 @@ import View from "react-native-web/dist/exports/View";
7
7
  import { applyTextStyles, useTheme, useThemeTokens, applyOuterBorder } from '../ThemeProvider';
8
8
  import StackView from '../StackView';
9
9
  import IconButton from '../IconButton';
10
+ import Icon from '../Icon';
10
11
  import { a11yProps, getTokensPropType, selectSystemProps, textInputHandlerProps, textInputProps, useCopy, useInputValue, useSpacingScale, variantProp, viewProps } from '../utils';
11
12
  import dictionary from './dictionary';
12
13
  import { jsx as _jsx } from "react/jsx-runtime";
13
14
  import { jsxs as _jsxs } from "react/jsx-runtime";
14
15
  const [selectProps, selectedSystemPropTypes] = selectSystemProps([a11yProps, textInputHandlerProps, textInputProps, viewProps]);
15
16
 
16
- const selectInputStyles = (_ref, themeOptions, inactive) => {
17
+ const selectInputStyles = (_ref, themeOptions, inactive, type) => {
17
18
  let {
18
19
  backgroundColor,
19
20
  color,
@@ -72,7 +73,7 @@ const selectInputStyles = (_ref, themeOptions, inactive) => {
72
73
  borderWidth,
73
74
  borderColor,
74
75
  borderRadius,
75
- paddingLeft: offsetBorder(paddingLeft),
76
+ paddingLeft: type === 'card' ? offsetBorder(paddingLeft + 34) : offsetBorder(paddingLeft),
76
77
  paddingRight: icon ? offsetBorder(paddingWithIcon) : offsetBorder(paddingRight),
77
78
  paddingTop: offsetBorder(paddingTop),
78
79
  paddingBottom: offsetBorder(paddingBottom),
@@ -126,16 +127,64 @@ const selectIconContainerStyles = (_ref4, buttonCount) => {
126
127
  };
127
128
  };
128
129
 
129
- const selectButtonsContainerStyle = _ref5 => {
130
+ const selectLeftIconContainerStyles = _ref5 => {
130
131
  let {
131
- buttonsPaddingRight
132
+ leftIconPaddingBottom
132
133
  } = _ref5;
134
+ return {
135
+ // not tokenizing paddingLeft as it remains same across brands for now
136
+ paddingLeft: 10,
137
+ paddingBottom: leftIconPaddingBottom
138
+ };
139
+ };
140
+
141
+ const selectButtonsContainerStyle = _ref6 => {
142
+ let {
143
+ buttonsPaddingRight
144
+ } = _ref6;
133
145
  return {
134
146
  paddingRight: buttonsPaddingRight
135
147
  };
136
148
  };
137
149
 
138
- const TextInputBase = /*#__PURE__*/forwardRef((_ref6, ref) => {
150
+ const getIcon = function () {
151
+ let cardNumber = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
152
+ let {
153
+ defaultCreditIcon,
154
+ amexIcon,
155
+ visaIcon,
156
+ masterCardIcon
157
+ } = arguments.length > 1 ? arguments[1] : undefined;
158
+ const cardType = {
159
+ 1: {
160
+ icon: visaIcon,
161
+ testID: 'visa'
162
+ },
163
+ 2: {
164
+ icon: amexIcon,
165
+ testID: 'amex'
166
+ },
167
+ 4: {
168
+ icon: masterCardIcon,
169
+ testID: 'mastercard'
170
+ }
171
+ };
172
+ const firstDigit = cardNumber ? cardNumber[0] : '';
173
+ const defaultIcon = {
174
+ icon: defaultCreditIcon,
175
+ testID: 'default'
176
+ };
177
+ const selectedIcon = cardNumber.length > 4 ? cardType[firstDigit] || defaultIcon : defaultIcon;
178
+ return /*#__PURE__*/_jsx(Icon, {
179
+ icon: selectedIcon.icon,
180
+ variant: {
181
+ size: 'large'
182
+ },
183
+ testID: selectedIcon.testID
184
+ });
185
+ };
186
+
187
+ const TextInputBase = /*#__PURE__*/forwardRef((_ref7, ref) => {
139
188
  let {
140
189
  buttons = [],
141
190
  copy = 'en',
@@ -154,8 +203,9 @@ const TextInputBase = /*#__PURE__*/forwardRef((_ref6, ref) => {
154
203
  tokens,
155
204
  value,
156
205
  variant = {},
206
+ type,
157
207
  ...rest
158
- } = _ref6;
208
+ } = _ref7;
159
209
  const [isFocused, setIsFocused] = useState(false);
160
210
  const [showPassword, setShowPassword] = useState(false);
161
211
 
@@ -196,6 +246,12 @@ const TextInputBase = /*#__PURE__*/forwardRef((_ref6, ref) => {
196
246
  onChange,
197
247
  readOnly
198
248
  });
249
+ const {
250
+ password,
251
+ numeric
252
+ } = variant;
253
+ const isNumeric = numeric || type === 'card' || type === 'number';
254
+ const isPassword = password || type === 'password';
199
255
  const element = inputRef === null || inputRef === void 0 ? void 0 : inputRef.current;
200
256
  useEffect(() => {
201
257
  if (Platform.OS === 'web' && pattern && element) {
@@ -208,11 +264,16 @@ const TextInputBase = /*#__PURE__*/forwardRef((_ref6, ref) => {
208
264
  const handleChangeText = event => {
209
265
  var _event$nativeEvent, _event$target;
210
266
 
211
- const {
212
- numeric
213
- } = variant;
214
267
  const text = ((_event$nativeEvent = event.nativeEvent) === null || _event$nativeEvent === void 0 ? void 0 : _event$nativeEvent.text) || ((_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.value);
215
- const filteredText = numeric ? text.replace(/[^\d]/g, '') : text;
268
+ let filteredText = isNumeric ? text === null || text === void 0 ? void 0 : text.replace(/[^\d]/g, '') : text;
269
+
270
+ if (type === 'card' && filteredText) {
271
+ const formattedValue = filteredText.replace(/[^a-zA-Z0-9]/g, '');
272
+ const regex = new RegExp(`([a-zA-Z0-9]{4})(?=[a-zA-Z0-9])`, 'g'); // Add a space every 4 digits starting from the 5th position
273
+
274
+ filteredText = formattedValue.replace(regex, '$1 ').trim();
275
+ }
276
+
216
277
  setValue(filteredText, event);
217
278
  if (typeof onChangeText === 'function') onChangeText(filteredText, event);
218
279
  };
@@ -241,7 +302,11 @@ const TextInputBase = /*#__PURE__*/forwardRef((_ref6, ref) => {
241
302
  clearButtonIcon: ClearButtonIcon,
242
303
  icon: IconComponent,
243
304
  passwordShowButtonIcon,
244
- passwordHideButtonIcon
305
+ passwordHideButtonIcon,
306
+ defaultCreditIcon,
307
+ amexIcon,
308
+ visaIcon,
309
+ masterCardIcon
245
310
  } = themeTokens;
246
311
  const buttonsGapSize = useSpacingScale(buttonsGap);
247
312
  const getCopy = useCopy({
@@ -261,7 +326,7 @@ const TextInputBase = /*#__PURE__*/forwardRef((_ref6, ref) => {
261
326
  }, "clear"));
262
327
  }
263
328
 
264
- if (variant.password) {
329
+ if (isPassword) {
265
330
  textInputButtons === null || textInputButtons === void 0 ? void 0 : textInputButtons.unshift( /*#__PURE__*/_jsx(IconButton, {
266
331
  accessibilityLabel: !showPassword ? getCopy('hidePasswordAccessibilityLabel') : getCopy('showPasswordAccessibilityLabel'),
267
332
  icon: !showPassword ? passwordShowButtonIcon : passwordHideButtonIcon,
@@ -287,6 +352,7 @@ const TextInputBase = /*#__PURE__*/forwardRef((_ref6, ref) => {
287
352
  onMouseOut: handleMouseOut,
288
353
  onChange: handleChangeText,
289
354
  defaultValue: initialValue,
355
+ maxLength: type === 'card' ? 19 : undefined,
290
356
  value: isControlled ? currentValue : undefined
291
357
  };
292
358
  const {
@@ -294,20 +360,29 @@ const TextInputBase = /*#__PURE__*/forwardRef((_ref6, ref) => {
294
360
  } = useTheme();
295
361
  const nativeInputStyle = selectInputStyles({ ...themeTokens,
296
362
  height
297
- }, themeOptions, inactive);
363
+ }, themeOptions, inactive, type);
298
364
  return /*#__PURE__*/_jsxs(View, {
299
365
  style: selectOuterBorderStyles(themeTokens),
300
- children: [/*#__PURE__*/_jsx(NativeTextInput, {
366
+ children: [type === 'card' && /*#__PURE__*/_jsx(View, {
367
+ pointerEvents: "none",
368
+ style: [staticStyles.leftIconContainer, selectLeftIconContainerStyles(themeTokens)],
369
+ children: getIcon(currentValue, {
370
+ defaultCreditIcon,
371
+ amexIcon,
372
+ visaIcon,
373
+ masterCardIcon
374
+ })
375
+ }), /*#__PURE__*/_jsx(NativeTextInput, {
301
376
  ref: inputRef,
302
- keyboardType: variant.numeric && 'numeric',
303
- inputMode: variant.numeric && 'numeric',
377
+ keyboardType: isNumeric && 'numeric',
378
+ inputMode: isNumeric && 'numeric',
304
379
  style: nativeInputStyle,
305
- secureTextEntry: variant.password && !showPassword,
380
+ secureTextEntry: isPassword && !showPassword,
306
381
  ...inputProps
307
382
  }), IconComponent && /*#__PURE__*/_jsx(View, {
308
383
  pointerEvents: "none" // avoid hijacking input press events
309
384
  ,
310
- style: [staticStyles.iconContainer, selectIconContainerStyles({ ...themeTokens,
385
+ style: [staticStyles.rightIconContainer, selectIconContainerStyles({ ...themeTokens,
311
386
  buttonsGapSize
312
387
  }, buttons === null || buttons === void 0 ? void 0 : buttons.length)],
313
388
  children: /*#__PURE__*/_jsx(IconComponent, { ...selectIconTokens(themeTokens)
@@ -333,6 +408,7 @@ TextInputBase.propTypes = { ...selectedSystemPropTypes,
333
408
  copy: PropTypes.oneOfType([PropTypes.oneOf(['en', 'fr']), PropTypes.shape({
334
409
  clearButtonAccessibilityLabel: PropTypes.string
335
410
  })]),
411
+ type: PropTypes.oneOfType([PropTypes.oneOf(['password', 'card', 'number'])]),
336
412
  height: PropTypes.number,
337
413
  inactive: PropTypes.bool,
338
414
  initialValue: PropTypes.string,
@@ -359,9 +435,15 @@ const staticStyles = StyleSheet.create({
359
435
  bottom: 0,
360
436
  justifyContent: 'center'
361
437
  },
362
- iconContainer: {
438
+ rightIconContainer: {
363
439
  position: 'absolute',
364
440
  right: 0,
365
441
  bottom: 0
442
+ },
443
+ leftIconContainer: {
444
+ position: 'absolute',
445
+ left: 0,
446
+ bottom: 0,
447
+ zIndex: 1
366
448
  }
367
449
  });
@@ -8,6 +8,11 @@ const textInputPropTypes = {
8
8
  */
9
9
  value: PropTypes.string,
10
10
 
11
+ /**
12
+ * Use this to set the type of the input. Defaults to `text`.
13
+ */
14
+ type: PropTypes.string,
15
+
11
16
  /**
12
17
  * Use this to set the initial value of an uncontrolled input.
13
18
  * Updating `initialValue` will **not** update the actual value.
@@ -19,6 +19,7 @@ const textProps = {
19
19
 
20
20
  const inputValueProps = {
21
21
  value: PropTypes.string,
22
+ type: PropTypes.string,
22
23
  initialValue: PropTypes.string,
23
24
  readOnly: PropTypes.bool,
24
25
  inactive: PropTypes.bool
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.42.0",
14
+ "@telus-uds/system-theme-tokens": "^2.43.0",
15
15
  "airbnb-prop-types": "^2.16.0",
16
16
  "lodash.debounce": "^4.0.8",
17
17
  "lodash.merge": "^4.6.2",
@@ -73,5 +73,5 @@
73
73
  "standard-engine": {
74
74
  "skip": true
75
75
  },
76
- "version": "1.65.0"
76
+ "version": "1.66.0"
77
77
  }
@@ -4,6 +4,7 @@ import { Platform, StyleSheet, TextInput as NativeTextInput, View } from 'react-
4
4
  import { applyTextStyles, useTheme, useThemeTokens, applyOuterBorder } from '../ThemeProvider'
5
5
  import StackView from '../StackView'
6
6
  import IconButton from '../IconButton'
7
+ import Icon from '../Icon'
7
8
  import {
8
9
  a11yProps,
9
10
  getTokensPropType,
@@ -48,7 +49,8 @@ const selectInputStyles = (
48
49
  height
49
50
  },
50
51
  themeOptions,
51
- inactive
52
+ inactive,
53
+ type
52
54
  ) => {
53
55
  // Subtract border width from padding so overall input width/height doesn't
54
56
  // jump around if the border width changes (avoiding NaN and negative padding)
@@ -88,7 +90,7 @@ const selectInputStyles = (
88
90
  borderWidth,
89
91
  borderColor,
90
92
  borderRadius,
91
- paddingLeft: offsetBorder(paddingLeft),
93
+ paddingLeft: type === 'card' ? offsetBorder(paddingLeft + 34) : offsetBorder(paddingLeft),
92
94
  paddingRight: icon ? offsetBorder(paddingWithIcon) : offsetBorder(paddingRight),
93
95
  paddingTop: offsetBorder(paddingTop),
94
96
  paddingBottom: offsetBorder(paddingBottom),
@@ -128,10 +130,29 @@ const selectIconContainerStyles = (
128
130
  paddingBottom
129
131
  })
130
132
 
133
+ const selectLeftIconContainerStyles = ({ leftIconPaddingBottom }) => ({
134
+ // not tokenizing paddingLeft as it remains same across brands for now
135
+ paddingLeft: 10,
136
+ paddingBottom: leftIconPaddingBottom
137
+ })
138
+
131
139
  const selectButtonsContainerStyle = ({ buttonsPaddingRight }) => ({
132
140
  paddingRight: buttonsPaddingRight
133
141
  })
134
142
 
143
+ const getIcon = (cardNumber = '', { defaultCreditIcon, amexIcon, visaIcon, masterCardIcon }) => {
144
+ const cardType = {
145
+ 1: { icon: visaIcon, testID: 'visa' },
146
+ 2: { icon: amexIcon, testID: 'amex' },
147
+ 4: { icon: masterCardIcon, testID: 'mastercard' }
148
+ }
149
+
150
+ const firstDigit = cardNumber ? cardNumber[0] : ''
151
+ const defaultIcon = { icon: defaultCreditIcon, testID: 'default' }
152
+ const selectedIcon = cardNumber.length > 4 ? cardType[firstDigit] || defaultIcon : defaultIcon
153
+ return <Icon icon={selectedIcon.icon} variant={{ size: 'large' }} testID={selectedIcon.testID} />
154
+ }
155
+
135
156
  const TextInputBase = forwardRef(
136
157
  (
137
158
  {
@@ -152,6 +173,7 @@ const TextInputBase = forwardRef(
152
173
  tokens,
153
174
  value,
154
175
  variant = {},
176
+ type,
155
177
  ...rest
156
178
  },
157
179
  ref
@@ -187,6 +209,9 @@ const TextInputBase = forwardRef(
187
209
  onChange,
188
210
  readOnly
189
211
  })
212
+ const { password, numeric } = variant
213
+ const isNumeric = numeric || type === 'card' || type === 'number'
214
+ const isPassword = password || type === 'password'
190
215
 
191
216
  const element = inputRef?.current
192
217
  useEffect(() => {
@@ -198,9 +223,14 @@ const TextInputBase = forwardRef(
198
223
  }, [element, pattern])
199
224
 
200
225
  const handleChangeText = (event) => {
201
- const { numeric } = variant
202
226
  const text = event.nativeEvent?.text || event.target?.value
203
- const filteredText = numeric ? text.replace(/[^\d]/g, '') : text
227
+ let filteredText = isNumeric ? text?.replace(/[^\d]/g, '') : text
228
+ if (type === 'card' && filteredText) {
229
+ const formattedValue = filteredText.replace(/[^a-zA-Z0-9]/g, '')
230
+ const regex = new RegExp(`([a-zA-Z0-9]{4})(?=[a-zA-Z0-9])`, 'g')
231
+ // Add a space every 4 digits starting from the 5th position
232
+ filteredText = formattedValue.replace(regex, '$1 ').trim()
233
+ }
204
234
  setValue(filteredText, event)
205
235
  if (typeof onChangeText === 'function') onChangeText(filteredText, event)
206
236
  }
@@ -224,7 +254,11 @@ const TextInputBase = forwardRef(
224
254
  clearButtonIcon: ClearButtonIcon,
225
255
  icon: IconComponent,
226
256
  passwordShowButtonIcon,
227
- passwordHideButtonIcon
257
+ passwordHideButtonIcon,
258
+ defaultCreditIcon,
259
+ amexIcon,
260
+ visaIcon,
261
+ masterCardIcon
228
262
  } = themeTokens
229
263
  const buttonsGapSize = useSpacingScale(buttonsGap)
230
264
  const getCopy = useCopy({ dictionary, copy })
@@ -242,7 +276,7 @@ const TextInputBase = forwardRef(
242
276
  )
243
277
  }
244
278
 
245
- if (variant.password) {
279
+ if (isPassword) {
246
280
  textInputButtons?.unshift(
247
281
  <IconButton
248
282
  accessibilityLabel={
@@ -268,27 +302,41 @@ const TextInputBase = forwardRef(
268
302
  onMouseOut: handleMouseOut,
269
303
  onChange: handleChangeText,
270
304
  defaultValue: initialValue,
305
+ maxLength: type === 'card' ? 19 : undefined,
271
306
  value: isControlled ? currentValue : undefined
272
307
  }
273
308
 
274
309
  const { themeOptions } = useTheme()
275
- const nativeInputStyle = selectInputStyles({ ...themeTokens, height }, themeOptions, inactive)
310
+ const nativeInputStyle = selectInputStyles(
311
+ { ...themeTokens, height },
312
+ themeOptions,
313
+ inactive,
314
+ type
315
+ )
276
316
 
277
317
  return (
278
318
  <View style={selectOuterBorderStyles(themeTokens)}>
319
+ {type === 'card' && (
320
+ <View
321
+ pointerEvents="none"
322
+ style={[staticStyles.leftIconContainer, selectLeftIconContainerStyles(themeTokens)]}
323
+ >
324
+ {getIcon(currentValue, { defaultCreditIcon, amexIcon, visaIcon, masterCardIcon })}
325
+ </View>
326
+ )}
279
327
  <NativeTextInput
280
328
  ref={inputRef}
281
- keyboardType={variant.numeric && 'numeric'}
282
- inputMode={variant.numeric && 'numeric'}
329
+ keyboardType={isNumeric && 'numeric'}
330
+ inputMode={isNumeric && 'numeric'}
283
331
  style={nativeInputStyle}
284
- secureTextEntry={variant.password && !showPassword}
332
+ secureTextEntry={isPassword && !showPassword}
285
333
  {...inputProps}
286
334
  />
287
335
  {IconComponent && (
288
336
  <View
289
337
  pointerEvents="none" // avoid hijacking input press events
290
338
  style={[
291
- staticStyles.iconContainer,
339
+ staticStyles.rightIconContainer,
292
340
  selectIconContainerStyles({ ...themeTokens, buttonsGapSize }, buttons?.length)
293
341
  ]}
294
342
  >
@@ -321,6 +369,7 @@ TextInputBase.propTypes = {
321
369
  clearButtonAccessibilityLabel: PropTypes.string
322
370
  })
323
371
  ]),
372
+ type: PropTypes.oneOfType([PropTypes.oneOf(['password', 'card', 'number'])]),
324
373
  height: PropTypes.number,
325
374
  inactive: PropTypes.bool,
326
375
  initialValue: PropTypes.string,
@@ -349,9 +398,15 @@ const staticStyles = StyleSheet.create({
349
398
  bottom: 0,
350
399
  justifyContent: 'center'
351
400
  },
352
- iconContainer: {
401
+ rightIconContainer: {
353
402
  position: 'absolute',
354
403
  right: 0,
355
404
  bottom: 0
405
+ },
406
+ leftIconContainer: {
407
+ position: 'absolute',
408
+ left: 0,
409
+ bottom: 0,
410
+ zIndex: 1
356
411
  }
357
412
  })
@@ -8,6 +8,10 @@ const textInputPropTypes = {
8
8
  * together with the `onChange` to pass down and update the lifted state.
9
9
  */
10
10
  value: PropTypes.string,
11
+ /**
12
+ * Use this to set the type of the input. Defaults to `text`.
13
+ */
14
+ type: PropTypes.string,
11
15
  /**
12
16
  * Use this to set the initial value of an uncontrolled input.
13
17
  * Updating `initialValue` will **not** update the actual value.
@@ -20,6 +20,7 @@ const textProps = {
20
20
  */
21
21
  const inputValueProps = {
22
22
  value: PropTypes.string,
23
+ type: PropTypes.string,
23
24
  initialValue: PropTypes.string,
24
25
  readOnly: PropTypes.bool,
25
26
  inactive: PropTypes.bool