@telus-uds/components-base 1.64.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/src/Tabs/Tabs.jsx CHANGED
@@ -1,4 +1,5 @@
1
1
  import React, { forwardRef, useCallback } from 'react'
2
+ import { Platform } from 'react-native'
2
3
  import PropTypes from 'prop-types'
3
4
  import ABBPropTypes from 'airbnb-prop-types'
4
5
  import { useThemeTokens } from '../ThemeProvider'
@@ -42,6 +43,17 @@ const getDefaultTabItemAccessibilityRole = (parentRole) => {
42
43
  }
43
44
  }
44
45
 
46
+ const getStackViewTokens = (variant) => {
47
+ const equalWidth = variant?.equalWidth
48
+ return Platform.select({
49
+ web: {
50
+ justifyContent: equalWidth ? 'space-evenly' : 'flex-start',
51
+ width: equalWidth ? '100%' : 'auto'
52
+ },
53
+ default: {}
54
+ })
55
+ }
56
+
45
57
  /**
46
58
  * Tabs renders a horizontally-scrolling menu of selectable buttons which may link
47
59
  * to a page or control what content is displayed on this page.
@@ -88,6 +100,7 @@ const Tabs = forwardRef(
88
100
  const parentAccessibilityRole = restProps.accessibilityRole ?? 'tablist'
89
101
  const defaultTabItemAccessibiltyRole =
90
102
  getDefaultTabItemAccessibilityRole(parentAccessibilityRole)
103
+ const stackViewTokens = getStackViewTokens(variant)
91
104
 
92
105
  return (
93
106
  <HorizontalScroll
@@ -99,7 +112,7 @@ const Tabs = forwardRef(
99
112
  accessibilityRole={parentAccessibilityRole}
100
113
  {...restProps}
101
114
  >
102
- <StackView space={space} direction="row">
115
+ <StackView space={space} direction="row" tokens={stackViewTokens}>
103
116
  {items.map(
104
117
  (
105
118
  {
@@ -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.
package/src/index.js CHANGED
@@ -8,6 +8,7 @@ export * from './Carousel'
8
8
  export { default as Listbox } from './Listbox'
9
9
  export { default as Checkbox } from './Checkbox'
10
10
  export * from './Checkbox'
11
+ export { default as CheckboxCard } from './CheckboxCard'
11
12
  export { default as Divider } from './Divider'
12
13
  export { default as ExpandCollapse, Accordion } from './ExpandCollapse'
13
14
  export { default as Feedback } from './Feedback'
@@ -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