@telus-uds/components-base 2.2.0 → 2.4.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 (46) hide show
  1. package/CHANGELOG.md +36 -2
  2. package/lib/Autocomplete/Autocomplete.js +8 -3
  3. package/lib/Card/CardBase.js +7 -1
  4. package/lib/Card/PressableCardBase.js +1 -1
  5. package/lib/FileUpload/FileUpload.js +57 -22
  6. package/lib/FileUpload/dictionary.js +6 -2
  7. package/lib/InputLabel/InputLabel.js +36 -2
  8. package/lib/InputSupports/InputSupports.js +31 -8
  9. package/lib/InputSupports/dictionary.js +12 -0
  10. package/lib/InputSupports/useInputSupports.js +12 -3
  11. package/lib/Link/LinkBase.js +17 -15
  12. package/lib/Listbox/ListboxOverlay.js +6 -3
  13. package/lib/Modal/Modal.js +2 -2
  14. package/lib/MultiSelectFilter/MultiSelectFilter.js +54 -41
  15. package/lib/Notification/Notification.js +7 -3
  16. package/lib/Search/Search.js +39 -8
  17. package/lib/Select/Picker.native.js +8 -4
  18. package/lib/Select/constants.js +4 -2
  19. package/lib/StepTracker/StepTracker.js +10 -3
  20. package/lib/TextInput/TextArea.js +7 -6
  21. package/lib/TextInput/TextInput.js +7 -6
  22. package/lib/TextInput/TextInputBase.js +48 -14
  23. package/lib/utils/props/inputSupportsProps.js +15 -3
  24. package/package.json +3 -3
  25. package/src/Autocomplete/Autocomplete.jsx +9 -2
  26. package/src/Card/CardBase.jsx +5 -2
  27. package/src/Card/PressableCardBase.jsx +12 -1
  28. package/src/FileUpload/FileUpload.jsx +71 -28
  29. package/src/FileUpload/dictionary.js +6 -2
  30. package/src/InputLabel/InputLabel.jsx +39 -2
  31. package/src/InputSupports/InputSupports.jsx +33 -7
  32. package/src/InputSupports/dictionary.js +12 -0
  33. package/src/InputSupports/useInputSupports.js +24 -3
  34. package/src/Link/LinkBase.jsx +17 -15
  35. package/src/Listbox/ListboxOverlay.jsx +5 -3
  36. package/src/Modal/Modal.jsx +1 -1
  37. package/src/MultiSelectFilter/MultiSelectFilter.jsx +55 -27
  38. package/src/Notification/Notification.jsx +9 -3
  39. package/src/Search/Search.jsx +52 -24
  40. package/src/Select/Picker.native.jsx +10 -4
  41. package/src/Select/constants.js +4 -1
  42. package/src/StepTracker/StepTracker.jsx +16 -5
  43. package/src/TextInput/TextArea.jsx +12 -5
  44. package/src/TextInput/TextInput.jsx +13 -6
  45. package/src/TextInput/TextInputBase.jsx +57 -10
  46. package/src/utils/props/inputSupportsProps.js +15 -3
@@ -30,14 +30,6 @@ import ModalOverlay from './ModalOverlay'
30
30
 
31
31
  const { Col, Row } = FlexGrid
32
32
 
33
- const selectSubTitleTokens = ({ subtitleColor }) => ({
34
- color: subtitleColor
35
- })
36
-
37
- const selectDividerTokens = ({ dividerColor }) => ({
38
- color: dividerColor
39
- })
40
-
41
33
  const selectHeaderTokens = ({
42
34
  contentMarginLeft,
43
35
  contentMarginRight,
@@ -113,23 +105,6 @@ const MultiSelectFilter = React.forwardRef(
113
105
  const [checkedIds, setCheckedIds] = React.useState(currentValues ?? [])
114
106
  const [maxWidth, setMaxWidth] = React.useState(false)
115
107
 
116
- const themeTokens = useThemeTokens('ButtonDropdown', tokens, variant)
117
- const getItemTokens = useThemeTokensCallback('ButtonDropdown', tokens, variant)
118
- const getButtonTokens = (buttonState) => selectTokens('Button', getItemTokens(buttonState))
119
- const getCopy = useCopy({ dictionary, copy })
120
- const colSizeNotMobile = items.length > rowLimit ? 2 : 1
121
- const colSize = viewport !== 'xs' ? colSizeNotMobile : 1
122
- const itemsLengthNotMobile = items.length > 24 ? items.length / 2 : rowLimit
123
- const isSelected = currentValues.length > 0
124
- const rowLength = viewport !== 'xs' ? itemsLengthNotMobile : items.length
125
-
126
- React.useEffect(() => {
127
- if (colSize === 1) return setMaxWidth(false)
128
- return colSize === 2 && setMaxWidth(true)
129
- }, [colSize])
130
-
131
- React.useEffect(() => setCheckedIds(currentValues ?? []), [currentValues])
132
-
133
108
  const {
134
109
  headerFontColor,
135
110
  headerFontSize,
@@ -143,6 +118,20 @@ const MultiSelectFilter = React.forwardRef(
143
118
  subHeaderLineHeight,
144
119
  minHeight,
145
120
  minWidth,
121
+ labelFontName,
122
+ labelFontSize,
123
+ labelFontWeight,
124
+ labelColor,
125
+ labelLineHeight,
126
+ labelPaddingTop,
127
+ labelPaddingBottom,
128
+ labelPaddingLeft,
129
+ labelPaddingRight,
130
+ buttonBorderColor,
131
+ buttonIconSize,
132
+ buttonIconPadding,
133
+ subtitleColor,
134
+ dividerColor,
146
135
  ...restTokens
147
136
  } = useThemeTokens(
148
137
  'MultiSelectFilter',
@@ -150,6 +139,40 @@ const MultiSelectFilter = React.forwardRef(
150
139
  { ...variant, maxHeight, maxWidth },
151
140
  { viewport }
152
141
  )
142
+ const dropdownTokens = {
143
+ borderColor: buttonBorderColor,
144
+ iconSize: buttonIconSize,
145
+ iconPadding: buttonIconPadding,
146
+ lineHeight: labelLineHeight,
147
+ fontName: labelFontName,
148
+ fontSize: labelFontSize,
149
+ fontWeight: labelFontWeight,
150
+ color: labelColor,
151
+ paddingTop: labelPaddingTop,
152
+ paddingBottom: labelPaddingBottom,
153
+ paddingLeft: labelPaddingLeft,
154
+ paddingRight: labelPaddingRight
155
+ }
156
+ const getButtonDropdownTokens = useThemeTokensCallback(
157
+ 'ButtonDropdown',
158
+ dropdownTokens,
159
+ variant
160
+ )
161
+ const getButtonTokens = (buttonState) =>
162
+ selectTokens('ButtonDropdown', getButtonDropdownTokens(buttonState))
163
+ const getCopy = useCopy({ dictionary, copy })
164
+ const colSizeNotMobile = items.length > rowLimit ? 2 : 1
165
+ const colSize = viewport !== 'xs' ? colSizeNotMobile : 1
166
+ const itemsLengthNotMobile = items.length > 24 ? items.length / 2 : rowLimit
167
+ const isSelected = currentValues.length > 0
168
+ const rowLength = viewport !== 'xs' ? itemsLengthNotMobile : items.length
169
+
170
+ React.useEffect(() => {
171
+ if (colSize === 1) return setMaxWidth(false)
172
+ return colSize === 2 && setMaxWidth(true)
173
+ }, [colSize])
174
+
175
+ React.useEffect(() => setCheckedIds(currentValues ?? []), [currentValues])
153
176
 
154
177
  const uniqueFields = ['id', 'label']
155
178
  if (!containUniqueFields(items, uniqueFields)) {
@@ -199,7 +222,7 @@ const MultiSelectFilter = React.forwardRef(
199
222
  const subeHeaderStyles = applyTextStyles({
200
223
  fontSize: subHeaderFontSize,
201
224
  fontWeight: subHeaderFontWeight,
202
- fontColor: selectSubTitleTokens(themeTokens)
225
+ fontColor: subtitleColor
203
226
  })
204
227
 
205
228
  const { overlaidPosition, onTargetLayout, isReady, sourceRef } = useOverlaidPosition({
@@ -275,7 +298,12 @@ const MultiSelectFilter = React.forwardRef(
275
298
  const controlsContent = (
276
299
  <>
277
300
  {isScrolling ? (
278
- <Divider tokens={selectDividerTokens(themeTokens)} space={4} />
301
+ <Divider
302
+ tokens={{
303
+ color: dividerColor
304
+ }}
305
+ space={4}
306
+ />
279
307
  ) : (
280
308
  <Spacer space={4} />
281
309
  )}
@@ -247,10 +247,16 @@ const Notification = React.forwardRef(
247
247
  const viewport = useViewport()
248
248
  const getCopy = useCopy({ dictionary, copy })
249
249
 
250
+ // TODO: Remove this once the system style variant is deprecated
251
+ const isSystemEnabled = system || variant?.style?.includes('system')
252
+
250
253
  const { themeOptions } = useTheme()
251
254
  const { enableMediaQueryStyleSheet } = themeOptions
252
255
  const useTokens = enableMediaQueryStyleSheet ? useResponsiveThemeTokens : useThemeTokens
253
- const themeTokens = useTokens('Notification', tokens, variant, { system, viewport })
256
+ const themeTokens = useTokens('Notification', tokens, variant, {
257
+ system: isSystemEnabled,
258
+ viewport
259
+ })
254
260
  const maxWidth = useResponsiveProp(themeOptions?.contentMaxWidth)
255
261
 
256
262
  const notificationComponentRef = React.useRef({
@@ -281,7 +287,7 @@ const Notification = React.forwardRef(
281
287
  mediaIdsRef,
282
288
  dismissible,
283
289
  viewport,
284
- system
290
+ isSystemEnabled
285
291
  )
286
292
  } else {
287
293
  notificationComponentRef.current = getDefaultStyles(
@@ -290,7 +296,7 @@ const Notification = React.forwardRef(
290
296
  maxWidth,
291
297
  dismissible,
292
298
  viewport,
293
- system
299
+ isSystemEnabled
294
300
  )
295
301
  }
296
302
 
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { View } from 'react-native'
2
+ import { View, Platform } from 'react-native'
3
3
 
4
4
  import PropTypes from 'prop-types'
5
5
  import { useThemeTokens, useThemeTokensCallback } from '../ThemeProvider'
@@ -20,6 +20,7 @@ import TextInputBase from '../TextInput/TextInputBase'
20
20
  import ButtonBase from '../Button/ButtonBase'
21
21
  import useCopy from '../utils/useCopy'
22
22
  import dictionary from './dictionary'
23
+ import Icon from '../Icon'
23
24
 
24
25
  const [selectContainerProps, selectedContainerPropTypes] = selectSystemProps([
25
26
  a11yProps,
@@ -44,10 +45,25 @@ const selectInputTokens = ({ searchTokens, buttonTokens, buttonsGapSize }) => {
44
45
 
45
46
  return { ...selectTokens('TextInput', searchTokens), paddingRight: paddingWithButtons }
46
47
  }
47
- const selectButtonTokens = (tokens) => selectTokens('Button', tokens)
48
+ const selectButtonTokens = (tokens) => selectTokens('SearchButton', tokens)
48
49
 
49
50
  const selectIconTokens = ({ iconSize, iconColor }) => ({ color: iconColor, size: iconSize })
50
51
 
52
+ const selectBorderTokens = ({ borderWidth, mobileBorderWidth, ...tokens }) => ({
53
+ borderWidth: Platform.OS === 'web' ? borderWidth : mobileBorderWidth,
54
+ ...tokens
55
+ })
56
+
57
+ const modifyButtonTokens = (tokens) => {
58
+ const modifiedTokens = { ...tokens }
59
+
60
+ if (Platform.OS !== 'web') {
61
+ modifiedTokens.backgroundColor = tokens.mobileBackgroundColor
62
+ }
63
+
64
+ return modifiedTokens
65
+ }
66
+
51
67
  /**
52
68
  * The `Search` component is a combination of a `TextInput` and 2 different kinds of custom buttons.
53
69
  * Use `Search` to feature autocomplete interactions.
@@ -77,6 +93,7 @@ const Search = React.forwardRef(
77
93
  tokens,
78
94
  variant,
79
95
  nativeSubmitBtnID,
96
+ searchIconPosition = 'right',
80
97
  dataSet,
81
98
  ...rest
82
99
  },
@@ -137,9 +154,9 @@ const Search = React.forwardRef(
137
154
  testID={testID}
138
155
  {...selectInputProps(rest)}
139
156
  ref={ref}
140
- tokens={(appearances) =>
157
+ tokens={(pressableStates) =>
141
158
  selectInputTokens({
142
- searchTokens: getThemeTokens(appearances),
159
+ searchTokens: selectBorderTokens(getThemeTokens(pressableStates)),
143
160
  buttonTokens,
144
161
  buttonsGapSize,
145
162
  isEmpty
@@ -155,6 +172,7 @@ const Search = React.forwardRef(
155
172
  onSubmitEditing={handleSubmit}
156
173
  onFocus={handleFocus}
157
174
  accessibilityLabel={a11yLabelText}
175
+ direction={searchIconPosition}
158
176
  buttons={[
159
177
  ClearButtonIcon && !isEmpty && (
160
178
  <ButtonBase
@@ -163,32 +181,42 @@ const Search = React.forwardRef(
163
181
  inactive={inactive}
164
182
  key="clear"
165
183
  onPress={handleClear}
166
- tokens={(appearances) => selectButtonTokens(getButtonTokens(appearances))}
184
+ tokens={(pressableStates) =>
185
+ modifyButtonTokens(selectButtonTokens(getButtonTokens(pressableStates)))
186
+ }
167
187
  >
168
188
  {(buttonState) => (
169
189
  <ClearButtonIcon {...selectIconTokens(getButtonTokens(buttonState))} />
170
190
  )}
171
191
  </ButtonBase>
172
192
  ),
173
- SubmitButtonIcon && (
174
- <ButtonBase
175
- accessibilityLabel={getCopy('submitButtonAccessibilityLabel')}
176
- accessibilityRole="button"
177
- inactive={inactive}
178
- key="submit"
179
- onPress={handleSubmit}
180
- tokens={(buttonState) =>
181
- selectButtonTokens(getButtonTokens({ ...buttonState, priority: 'high' }))
182
- }
183
- nativeID={nativeSubmitBtnID}
184
- >
185
- {(buttonState) => (
186
- <SubmitButtonIcon
187
- {...selectIconTokens(getButtonTokens({ ...buttonState, priority: 'high' }))}
188
- />
189
- )}
190
- </ButtonBase>
191
- )
193
+ SubmitButtonIcon &&
194
+ (searchIconPosition === 'right' ? (
195
+ <ButtonBase
196
+ accessibilityLabel={getCopy('submitButtonAccessibilityLabel')}
197
+ accessibilityRole="button"
198
+ inactive={inactive}
199
+ key="submit-right"
200
+ onPress={handleSubmit}
201
+ tokens={(buttonState) =>
202
+ selectButtonTokens(getButtonTokens({ ...buttonState, priority: 'high' }))
203
+ }
204
+ nativeID={nativeSubmitBtnID}
205
+ >
206
+ {(buttonState) => (
207
+ <SubmitButtonIcon
208
+ {...selectIconTokens(getButtonTokens({ ...buttonState, priority: 'high' }))}
209
+ />
210
+ )}
211
+ </ButtonBase>
212
+ ) : (
213
+ <Icon
214
+ icon={SubmitButtonIcon}
215
+ key="submitIcon"
216
+ testID="iconLeft"
217
+ tokens={{ color: themeTokens.iconLeftColor, size: themeTokens.iconLeftSize }}
218
+ />
219
+ ))
192
220
  ]}
193
221
  />
194
222
  </View>
@@ -7,9 +7,11 @@ import { a11yProps, componentPropType } from '../utils'
7
7
  import Group from './Group'
8
8
 
9
9
  import {
10
- ANDROID_HEIGHT_OFFSET,
11
10
  ANDROID_HORIZONTAL_PADDING_OFFSET,
12
- ANDROID_DEFAULT_PADDING
11
+ ANDROID_DEFAULT_PADDING,
12
+ ALLOW_FONT_SCALING,
13
+ ADJUSTS_FONT_SIZE_TO_FIT,
14
+ MAX_FONT_SIZE_MULTIPLIER
13
15
  } from './constants'
14
16
 
15
17
  // Styling of the native input is very limited, most of the styles have to be applied to an additional View
@@ -36,8 +38,7 @@ const selectAndroidContainerStyles = ({
36
38
  ? paddingRight - ANDROID_HORIZONTAL_PADDING_OFFSET
37
39
  : ANDROID_DEFAULT_PADDING,
38
40
  paddingBottom: ANDROID_DEFAULT_PADDING,
39
- paddingTop: ANDROID_DEFAULT_PADDING,
40
- height: rest.height + ANDROID_HEIGHT_OFFSET
41
+ paddingTop: ANDROID_DEFAULT_PADDING
41
42
  })
42
43
 
43
44
  const Picker = React.forwardRef(
@@ -85,6 +86,11 @@ const Picker = React.forwardRef(
85
86
  inputAndroid: selectAndroidInputStyles(style)
86
87
  }}
87
88
  placeholder={placeholder !== undefined ? placeholder : {}}
89
+ textInputProps={{
90
+ allowFontScaling: ALLOW_FONT_SCALING,
91
+ adjustFontSizeToFit: ADJUSTS_FONT_SIZE_TO_FIT,
92
+ maxFontSizeMultiplier: MAX_FONT_SIZE_MULTIPLIER
93
+ }}
88
94
  />
89
95
  )
90
96
 
@@ -1,5 +1,8 @@
1
1
  // Because Android
2
2
  export const ANDROID_VALIDATION_ICON_CONTAINER_OFFSET = 5
3
- export const ANDROID_HEIGHT_OFFSET = 12
4
3
  export const ANDROID_HORIZONTAL_PADDING_OFFSET = 8
5
4
  export const ANDROID_DEFAULT_PADDING = 0
5
+
6
+ export const ALLOW_FONT_SCALING = true
7
+ export const ADJUSTS_FONT_SIZE_TO_FIT = true
8
+ export const MAX_FONT_SIZE_MULTIPLIER = 3
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
  import PropTypes from 'prop-types'
3
- import { StyleSheet, Text, View } from 'react-native'
3
+ import { Platform, StyleSheet, Text, View } from 'react-native'
4
4
  import StackView from '../StackView'
5
5
  import { applyTextStyles, useTheme, useThemeTokens } from '../ThemeProvider'
6
6
  import { a11yProps, getTokensPropType, selectSystemProps, variantProp, viewProps } from '../utils'
@@ -105,7 +105,7 @@ const StepTracker = React.forwardRef(
105
105
  '%{stepLabel}',
106
106
  current < steps.length ? steps[current] : steps[steps.length - 1]
107
107
  )
108
- : ''
108
+ : getCopy('stepTrackerLabel')
109
109
 
110
110
  const getStepLabel = (index) => {
111
111
  if (themeTokens.showStepLabel) {
@@ -118,7 +118,6 @@ const StepTracker = React.forwardRef(
118
118
  if (!steps.length) return null
119
119
  const selectedProps = selectProps({
120
120
  accessibilityLabel: stepTrackerLabel,
121
- accessibilityLevel: 1,
122
121
  accessibilityRole: 'progressbar',
123
122
  accessibilityValue: {
124
123
  min: 0,
@@ -129,10 +128,16 @@ const StepTracker = React.forwardRef(
129
128
  ...rest
130
129
  })
131
130
 
131
+ const stepsContainerAccessibilityRole = Platform.OS === 'web' ? 'list' : 'tablist'
132
+ const stepAccessibilityRole = Platform.OS === 'web' ? 'listitem' : 'tab'
133
+
132
134
  return (
133
135
  <View ref={ref} style={selectContainerStyles(themeTokens)} {...selectedProps}>
134
136
  <StackView space={0}>
135
- <View style={staticStyles.stepsContainer}>
137
+ <View
138
+ style={staticStyles.stepsContainer}
139
+ accessibilityRole={stepsContainerAccessibilityRole}
140
+ >
136
141
  {steps.map((label, index) => {
137
142
  return (
138
143
  <Step
@@ -143,6 +148,8 @@ const StepTracker = React.forwardRef(
143
148
  stepIndex={index}
144
149
  stepCount={steps.length}
145
150
  tokens={themeTokens}
151
+ accessibilityRole={stepAccessibilityRole}
152
+ accessibilityCurrent={current === index && Platform.OS === 'web' && 'step'}
146
153
  />
147
154
  )
148
155
  })}
@@ -154,7 +161,11 @@ const StepTracker = React.forwardRef(
154
161
  selectStepTrackerLabelContainerStyles(themeTokens)
155
162
  ]}
156
163
  >
157
- <Text style={selectStepTrackerLabelStyles(themeTokens, themeOptions)}>
164
+ <Text
165
+ style={selectStepTrackerLabelStyles(themeTokens, themeOptions)}
166
+ accessibilityRole="header"
167
+ accessibilityLevel={2}
168
+ >
158
169
  {stepTrackerLabel}
159
170
  </Text>
160
171
  </View>
@@ -70,7 +70,6 @@ const TextArea = React.forwardRef(({ tokens, variant = {}, ...rest }, ref) => {
70
70
 
71
71
  const inputProps = {
72
72
  ...selectedProps,
73
- variant: { ...variant, validation: supportsProps.validation },
74
73
  multiline: true,
75
74
  onContentSizeChange: onHeightChange,
76
75
  height: inputHeight,
@@ -78,10 +77,18 @@ const TextArea = React.forwardRef(({ tokens, variant = {}, ...rest }, ref) => {
78
77
  }
79
78
 
80
79
  return (
81
- <InputSupports {...supportsProps}>
82
- {({ inputId, ...props }) => (
83
- <TextInputBase ref={ref} {...inputProps} nativeID={inputId} {...props} />
84
- )}
80
+ <InputSupports {...supportsProps} inputValue={selectedProps?.value}>
81
+ {({ inputId, ...props }) => {
82
+ return (
83
+ <TextInputBase
84
+ ref={ref}
85
+ nativeID={inputId}
86
+ {...props}
87
+ {...inputProps}
88
+ variant={{ ...variant, validation: props.validation }}
89
+ />
90
+ )
91
+ }}
85
92
  </InputSupports>
86
93
  )
87
94
  })
@@ -47,16 +47,23 @@ const TextInput = React.forwardRef(({ tokens, nativeID, variant = {}, ...rest },
47
47
 
48
48
  const inputProps = {
49
49
  ...selectedProps,
50
- tokens,
51
- variant: { ...variant, validation: supportsProps.validation }
50
+ tokens
52
51
  }
53
52
 
54
53
  return (
55
54
  <>
56
- <InputSupports nativeID={nativeID} {...supportsProps}>
57
- {({ inputId, ...propsFromInputSupports }) => (
58
- <TextInputBase ref={ref} nativeID={inputId} {...propsFromInputSupports} {...inputProps} />
59
- )}
55
+ <InputSupports nativeID={nativeID} {...supportsProps} inputValue={selectedProps?.value}>
56
+ {({ inputId, ...propsFromInputSupports }) => {
57
+ return (
58
+ <TextInputBase
59
+ ref={ref}
60
+ nativeID={inputId}
61
+ {...propsFromInputSupports}
62
+ {...inputProps}
63
+ variant={{ ...variant, validation: propsFromInputSupports.validation }}
64
+ />
65
+ )
66
+ }}
60
67
  </InputSupports>
61
68
  {rest.children}
62
69
  </>
@@ -53,7 +53,10 @@ const selectInputStyles = (
53
53
  buttonCount = 0,
54
54
  buttonSize = 0,
55
55
  buttonsGapSize = 0,
56
- isPassword
56
+ isPassword,
57
+ iconLeftWidth,
58
+ iconLeftGap,
59
+ direction
57
60
  ) => {
58
61
  // Subtract border width from padding so overall input width/height doesn't
59
62
  // jump around if the border width changes (avoiding NaN and negative padding)
@@ -85,6 +88,16 @@ const selectInputStyles = (
85
88
  }
86
89
  })
87
90
 
91
+ const getPaddingLeft = () => {
92
+ if (type === 'card') {
93
+ return offsetBorder(paddingLeft + 34)
94
+ }
95
+ if (direction === 'left') {
96
+ return offsetBorder(iconLeftWidth + iconLeftGap + staticStyles.iconLeftContainer.paddingLeft)
97
+ }
98
+ return offsetBorder(paddingLeft)
99
+ }
100
+
88
101
  const buttonSpacing = isPassword ? buttonsGapSize : -buttonsGapSize
89
102
  const adjustedPaddingRight = paddingRight + (buttonCount ? 1 : 0) * (buttonSize + buttonSpacing)
90
103
  const adjustedPaddingWithButtons = buttonCount > 1 ? paddingRight : adjustedPaddingRight
@@ -96,7 +109,7 @@ const selectInputStyles = (
96
109
  borderWidth,
97
110
  borderColor,
98
111
  borderRadius,
99
- paddingLeft: type === 'card' ? offsetBorder(paddingLeft + 34) : offsetBorder(paddingLeft),
112
+ paddingLeft: getPaddingLeft(),
100
113
  paddingRight: icon ? offsetBorder(paddingWithIcon) : offsetBorder(adjustedPaddingWithButtons),
101
114
  paddingTop: offsetBorder(paddingTop),
102
115
  paddingBottom: offsetBorder(paddingBottom),
@@ -136,12 +149,14 @@ const selectIconContainerStyles = (
136
149
  paddingBottom
137
150
  })
138
151
 
139
- const selectLeftIconContainerStyles = ({ leftIconPaddingBottom }) => ({
140
- // not tokenizing paddingLeft as it remains same across brands for now
141
- paddingLeft: 10,
152
+ const selectIconCardLeftContainerStyles = ({ leftIconPaddingBottom }) => ({
142
153
  paddingBottom: leftIconPaddingBottom
143
154
  })
144
155
 
156
+ const selectIconLeftContainerStyles = ({ iconLeftPaddingBottom }) => ({
157
+ paddingBottom: iconLeftPaddingBottom
158
+ })
159
+
145
160
  const selectButtonsContainerStyle = ({ buttonsPaddingRight }) => ({
146
161
  paddingRight: buttonsPaddingRight
147
162
  })
@@ -189,12 +204,14 @@ const TextInputBase = React.forwardRef(
189
204
  variant = {},
190
205
  type,
191
206
  onKeyPress,
207
+ direction,
192
208
  ...rest
193
209
  },
194
210
  ref
195
211
  ) => {
196
212
  const [isFocused, setIsFocused] = React.useState(false)
197
213
  const [showPassword, setShowPassword] = React.useState(false)
214
+
198
215
  const handleFocus = (event) => {
199
216
  setIsFocused(true)
200
217
  if (typeof onFocus === 'function') onFocus(event)
@@ -273,11 +290,17 @@ const TextInputBase = React.forwardRef(
273
290
  defaultCreditIcon,
274
291
  amexIcon,
275
292
  visaIcon,
276
- masterCardIcon
293
+ masterCardIcon,
294
+ iconLeftGap
277
295
  } = themeTokens
278
296
  const buttonsGapSize = useSpacingScale(buttonsGap)
279
297
  const getCopy = useCopy({ dictionary, copy })
280
- const textInputButtons = buttons
298
+ const textInputButtons =
299
+ direction === 'left'
300
+ ? buttons.filter((button) => button && button.key !== 'submitIcon')
301
+ : buttons
302
+ const submitIcon =
303
+ direction === 'left' ? buttons.find((button) => button && button.key === 'submitIcon') : null
281
304
 
282
305
  if (onClear && isDirty) {
283
306
  textInputButtons?.unshift(
@@ -323,6 +346,7 @@ const TextInputBase = React.forwardRef(
323
346
  }
324
347
 
325
348
  const { themeOptions } = useTheme()
349
+ const iconLeftWidth = submitIcon ? submitIcon.props.tokens.size ?? 0 : 0
326
350
  const nativeInputStyle = selectInputStyles(
327
351
  { ...themeTokens, height },
328
352
  themeOptions,
@@ -331,15 +355,30 @@ const TextInputBase = React.forwardRef(
331
355
  buttons?.length,
332
356
  themeTokens.buttonSize,
333
357
  buttonsGapSize,
334
- isPassword
358
+ isPassword,
359
+ iconLeftWidth,
360
+ iconLeftGap,
361
+ direction
335
362
  )
336
363
 
364
+ const shouldShowSubmitIcon = submitIcon && direction === 'left' && !inactive
365
+
337
366
  return (
338
367
  <View style={selectOuterBorderStyles(themeTokens)}>
368
+ {shouldShowSubmitIcon && (
369
+ <View
370
+ style={[staticStyles.iconLeftContainer, selectIconLeftContainerStyles(themeTokens)]}
371
+ >
372
+ {submitIcon}
373
+ </View>
374
+ )}
339
375
  {type === 'card' && (
340
376
  <View
341
377
  pointerEvents="none"
342
- style={[staticStyles.leftIconContainer, selectLeftIconContainerStyles(themeTokens)]}
378
+ style={[
379
+ staticStyles.iconCardLeftContainer,
380
+ selectIconCardLeftContainerStyles(themeTokens)
381
+ ]}
343
382
  >
344
383
  {getIcon(currentValue, { defaultCreditIcon, amexIcon, visaIcon, masterCardIcon })}
345
384
  </View>
@@ -424,7 +463,15 @@ const staticStyles = StyleSheet.create({
424
463
  right: 0,
425
464
  bottom: 0
426
465
  },
427
- leftIconContainer: {
466
+ iconCardLeftContainer: {
467
+ paddingLeft: 10,
468
+ position: 'absolute',
469
+ left: 0,
470
+ bottom: 0,
471
+ zIndex: 1
472
+ },
473
+ iconLeftContainer: {
474
+ paddingLeft: 10,
428
475
  position: 'absolute',
429
476
  left: 0,
430
477
  bottom: 0,
@@ -38,7 +38,15 @@ export default {
38
38
  /**
39
39
  * Use to visually mark an input as valid or invalid.
40
40
  */
41
- validation: PropTypes.oneOf(['error', 'success'])
41
+ validation: PropTypes.oneOf(['error', 'success']),
42
+ /**
43
+ * The text value from an input field.
44
+ */
45
+ inputValue: PropTypes.string,
46
+ /**
47
+ * Max number of characters allowed id an input text.
48
+ */
49
+ maxCharacterAllowed: PropTypes.number
42
50
  },
43
51
  select: ({
44
52
  copy,
@@ -49,7 +57,9 @@ export default {
49
57
  feedbackTokens,
50
58
  feedbackProps,
51
59
  tooltip,
52
- validation
60
+ validation,
61
+ inputValue,
62
+ maxCharacterAllowed
53
63
  }) => ({
54
64
  supportsProps: {
55
65
  copy,
@@ -60,7 +70,9 @@ export default {
60
70
  feedbackTokens,
61
71
  feedbackProps,
62
72
  tooltip,
63
- validation
73
+ validation,
74
+ inputValue,
75
+ maxCharacterAllowed
64
76
  }
65
77
  })
66
78
  }