@hero-design/rn 8.57.0 → 8.58.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 (28) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/es/index.js +172 -94
  4. package/lib/index.js +172 -94
  5. package/package.json +6 -6
  6. package/src/components/Calendar/__tests__/index.spec.tsx +5 -5
  7. package/src/components/Carousel/CardCarousel.tsx +7 -9
  8. package/src/components/Carousel/CarouselItem.tsx +26 -14
  9. package/src/components/Carousel/StyledCarousel.tsx +2 -3
  10. package/src/components/Carousel/__tests__/__snapshots__/index.spec.tsx.snap +494 -48
  11. package/src/components/Carousel/__tests__/index.spec.tsx +34 -1
  12. package/src/components/Carousel/types.ts +3 -3
  13. package/src/components/DatePicker/__tests__/DatePickerCalendar.spec.tsx +32 -29
  14. package/src/components/Tabs/ScrollableTabsHeader/ScrollableTabsHeader.tsx +10 -12
  15. package/src/components/Tabs/__tests__/__snapshots__/ScrollableTabs.spec.tsx.snap +18 -6
  16. package/src/components/Tabs/__tests__/__snapshots__/ScrollableTabsHeader.spec.tsx.snap +12 -4
  17. package/src/components/TextInput/__tests__/index.spec.tsx +148 -1
  18. package/src/components/TextInput/index.tsx +135 -57
  19. package/src/components/Toast/ToastContext.ts +20 -2
  20. package/src/components/Toast/ToastProvider.tsx +7 -4
  21. package/src/theme/__tests__/__snapshots__/index.spec.ts.snap +0 -1
  22. package/src/theme/components/carousel.ts +0 -1
  23. package/types/components/Carousel/CardCarousel.d.ts +1 -0
  24. package/types/components/Carousel/StyledCarousel.d.ts +2 -2
  25. package/types/components/Carousel/types.d.ts +3 -3
  26. package/types/components/TextInput/index.d.ts +29 -1
  27. package/types/components/Toast/ToastContext.d.ts +1 -0
  28. package/types/theme/components/carousel.d.ts +0 -1
@@ -7,8 +7,10 @@ import renderWithTheme from '../../../testHelpers/renderWithTheme';
7
7
  import Button from '../../Button/Button';
8
8
  import HeroDesignProvider from '../../HeroDesignProvider';
9
9
  import Image from '../../Image';
10
+ import { getCardCarouselValidIndex } from '../CardCarousel';
11
+ import { CarouselData } from '../types';
10
12
 
11
- const carouselData = [
13
+ const carouselData: CarouselData[] = [
12
14
  {
13
15
  image: 1, // Test the case when we import .png files as React Native component, it's convert to ImageRequireSource type (which is a number).
14
16
  heading: 'Welcome to the new Employment Hero app',
@@ -42,6 +44,10 @@ const carouselData = [
42
44
  heading: 'Test slide 4',
43
45
  background: theme.colors.decorativePrimarySurface,
44
46
  },
47
+ {
48
+ heading: 'Test slide 5',
49
+ background: theme.colors.decorativePrimarySurface,
50
+ },
45
51
  ];
46
52
 
47
53
  describe('Carousel', () => {
@@ -179,4 +185,31 @@ describe('Carousel', () => {
179
185
 
180
186
  expect(queryByTestId('page-control-indicator1')).toBeFalsy();
181
187
  });
188
+ it('should render correctly when image is undefined', () => {
189
+ const onPress = jest.fn();
190
+ const { toJSON } = renderWithTheme(
191
+ <Carousel
192
+ testID="carousel"
193
+ items={[carouselData[4]]}
194
+ renderActions={(_) => {
195
+ return <Button text="Skip" onPress={onPress} />;
196
+ }}
197
+ />
198
+ );
199
+
200
+ expect(toJSON()).toMatchSnapshot();
201
+ });
202
+ });
203
+
204
+ // write test for getCardCarouselValidIndex
205
+ describe('getCardCarouselValidIndex', () => {
206
+ it('should return 0 when index is less than 0', () => {
207
+ expect(getCardCarouselValidIndex(-1, 3)).toBe(0);
208
+ });
209
+ it('should return 2 when index is equal to 2', () => {
210
+ expect(getCardCarouselValidIndex(2, 3)).toBe(2);
211
+ });
212
+ it('should return 2 when index is greater than 2', () => {
213
+ expect(getCardCarouselValidIndex(3, 3)).toBe(2);
214
+ });
182
215
  });
@@ -2,13 +2,13 @@ import { ReactNode } from 'react';
2
2
  import { ImageSourcePropType, ImageResizeMode } from 'react-native';
3
3
 
4
4
  export type CarouselImageProps = ImageSourcePropType & {
5
- height?: number;
6
- width?: number;
5
+ height?: number | `${number}%`;
6
+ width?: number | `${number}%`;
7
7
  resizeMode?: ImageResizeMode;
8
8
  };
9
9
 
10
10
  export type CarouselData = {
11
- image: CarouselImageProps | string;
11
+ image?: CarouselImageProps | string;
12
12
  content?: ReactNode;
13
13
  heading: string;
14
14
  body?: string;
@@ -63,38 +63,41 @@ describe('DatePickerCalendar', () => {
63
63
  platform
64
64
  ${'ios'}
65
65
  ${'android'}
66
- `('renders month year picker when pressing on title', ({ platform }) => {
67
- jest.mock('react-native/Libraries/Utilities/Platform', () => ({
68
- OS: platform,
69
- select: () => null,
70
- }));
66
+ `(
67
+ 'renders month year picker when pressing on title',
68
+ ({ platform: mockedPlatform }) => {
69
+ jest.mock('react-native/Libraries/Utilities/Platform', () => ({
70
+ OS: mockedPlatform,
71
+ select: () => null,
72
+ }));
71
73
 
72
- const { queryByText, getByText, getByTestId } = renderWithTheme(
73
- <DatePickerCalendar
74
- value={new Date('December 21, 1995')}
75
- label="Start date"
76
- confirmLabel="Confirm"
77
- helpText="This is help text"
78
- onChange={jest.fn()}
79
- />
80
- );
74
+ const { queryByText, getByText, getByTestId } = renderWithTheme(
75
+ <DatePickerCalendar
76
+ value={new Date('December 21, 1995')}
77
+ label="Start date"
78
+ confirmLabel="Confirm"
79
+ helpText="This is help text"
80
+ onChange={jest.fn()}
81
+ />
82
+ );
81
83
 
82
- fireEvent.press(getByText('Start date'));
83
- fireEvent.press(getByText('December 1995'));
84
+ fireEvent.press(getByText('Start date'));
85
+ fireEvent.press(getByText('December 1995'));
84
86
 
85
- if (platform === 'ios') {
86
- expect(queryByText('IOS picker')).toBeDefined();
87
- } else {
88
- expect(queryByText('Android picker')).toBeDefined();
89
- }
87
+ if (mockedPlatform === 'ios') {
88
+ expect(queryByText('IOS picker')).toBeDefined();
89
+ } else {
90
+ expect(queryByText('Android picker')).toBeDefined();
91
+ }
90
92
 
91
- // Selecting month
92
- fireEvent(
93
- getByTestId('calendar'),
94
- 'onMonthChange',
95
- new Date('January 17, 1993')
96
- );
93
+ // Selecting month
94
+ fireEvent(
95
+ getByTestId('calendar'),
96
+ 'onMonthChange',
97
+ new Date('January 17, 1993')
98
+ );
97
99
 
98
- expect(queryByText('January 1993')).toBeDefined();
99
- });
100
+ expect(queryByText('January 1993')).toBeDefined();
101
+ }
102
+ );
100
103
  });
@@ -156,21 +156,19 @@ const ScrollableTabHeader = ({
156
156
  100
157
157
  );
158
158
  }}
159
- style={
160
- // Border styles specified in contentContainerStyle don't work
161
- // so they need to be placed here instead on Android.
162
- Platform.OS === 'android'
163
- ? {
164
- borderBottomColor: theme.__hd__.tabs.colors.headerBottom,
165
- borderBottomWidth: theme.__hd__.tabs.sizes.indicator,
166
- }
167
- : undefined
168
- }
169
- contentContainerStyle={{
170
- paddingHorizontal: theme.__hd__.tabs.space.flatListHorizontalPadding,
159
+ style={{
171
160
  borderBottomColor: theme.__hd__.tabs.colors.headerBottom,
172
161
  borderBottomWidth: theme.__hd__.tabs.sizes.indicator,
173
162
  }}
163
+ contentContainerStyle={{
164
+ paddingHorizontal: theme.__hd__.tabs.space.flatListHorizontalPadding,
165
+
166
+ // Specify it here again or the indicator won't show
167
+ ...(Platform.OS === 'android' && {
168
+ borderBottomColor: theme.__hd__.tabs.colors.headerBottom,
169
+ borderBottomWidth: theme.__hd__.tabs.sizes.indicator,
170
+ }),
171
+ }}
174
172
  renderItem={({ item: tab, index }) => {
175
173
  const {
176
174
  key,
@@ -145,8 +145,6 @@ exports[`Tabs.Scroll lazy not render lazy screen: xxx 1`] = `
145
145
  <RCTScrollView
146
146
  contentContainerStyle={
147
147
  {
148
- "borderBottomColor": "#e8e9ea",
149
- "borderBottomWidth": 2,
150
148
  "paddingHorizontal": 8,
151
149
  }
152
150
  }
@@ -209,6 +207,12 @@ exports[`Tabs.Scroll lazy not render lazy screen: xxx 1`] = `
209
207
  scrollEventThrottle={50}
210
208
  showsHorizontalScrollIndicator={false}
211
209
  stickyHeaderIndices={[]}
210
+ style={
211
+ {
212
+ "borderBottomColor": "#e8e9ea",
213
+ "borderBottomWidth": 2,
214
+ }
215
+ }
212
216
  viewabilityConfigCallbackPairs={[]}
213
217
  >
214
218
  <View>
@@ -1061,8 +1065,6 @@ exports[`Tabs.Scroll renders correctly 1`] = `
1061
1065
  <RCTScrollView
1062
1066
  contentContainerStyle={
1063
1067
  {
1064
- "borderBottomColor": "#e8e9ea",
1065
- "borderBottomWidth": 2,
1066
1068
  "paddingHorizontal": 8,
1067
1069
  }
1068
1070
  }
@@ -1125,6 +1127,12 @@ exports[`Tabs.Scroll renders correctly 1`] = `
1125
1127
  scrollEventThrottle={50}
1126
1128
  showsHorizontalScrollIndicator={false}
1127
1129
  stickyHeaderIndices={[]}
1130
+ style={
1131
+ {
1132
+ "borderBottomColor": "#e8e9ea",
1133
+ "borderBottomWidth": 2,
1134
+ }
1135
+ }
1128
1136
  viewabilityConfigCallbackPairs={[]}
1129
1137
  >
1130
1138
  <View>
@@ -1977,8 +1985,6 @@ exports[`useIsFocused renders correctly 1`] = `
1977
1985
  <RCTScrollView
1978
1986
  contentContainerStyle={
1979
1987
  {
1980
- "borderBottomColor": "#e8e9ea",
1981
- "borderBottomWidth": 2,
1982
1988
  "paddingHorizontal": 8,
1983
1989
  }
1984
1990
  }
@@ -2041,6 +2047,12 @@ exports[`useIsFocused renders correctly 1`] = `
2041
2047
  scrollEventThrottle={50}
2042
2048
  showsHorizontalScrollIndicator={false}
2043
2049
  stickyHeaderIndices={[]}
2050
+ style={
2051
+ {
2052
+ "borderBottomColor": "#e8e9ea",
2053
+ "borderBottomWidth": 2,
2054
+ }
2055
+ }
2044
2056
  viewabilityConfigCallbackPairs={[]}
2045
2057
  >
2046
2058
  <View>
@@ -40,8 +40,6 @@ exports[`ScrollableTabsHeader highlighted variant renders correctly 1`] = `
40
40
  <RCTScrollView
41
41
  contentContainerStyle={
42
42
  {
43
- "borderBottomColor": "#e8e9ea",
44
- "borderBottomWidth": 2,
45
43
  "paddingHorizontal": 8,
46
44
  }
47
45
  }
@@ -104,6 +102,12 @@ exports[`ScrollableTabsHeader highlighted variant renders correctly 1`] = `
104
102
  scrollEventThrottle={50}
105
103
  showsHorizontalScrollIndicator={false}
106
104
  stickyHeaderIndices={[]}
105
+ style={
106
+ {
107
+ "borderBottomColor": "#e8e9ea",
108
+ "borderBottomWidth": 2,
109
+ }
110
+ }
107
111
  viewabilityConfigCallbackPairs={[]}
108
112
  >
109
113
  <View>
@@ -700,8 +704,6 @@ exports[`ScrollableTabsHeader underlined variant renders correctly 1`] = `
700
704
  <RCTScrollView
701
705
  contentContainerStyle={
702
706
  {
703
- "borderBottomColor": "#e8e9ea",
704
- "borderBottomWidth": 2,
705
707
  "paddingHorizontal": 8,
706
708
  }
707
709
  }
@@ -764,6 +766,12 @@ exports[`ScrollableTabsHeader underlined variant renders correctly 1`] = `
764
766
  scrollEventThrottle={50}
765
767
  showsHorizontalScrollIndicator={false}
766
768
  stickyHeaderIndices={[]}
769
+ style={
770
+ {
771
+ "borderBottomColor": "#e8e9ea",
772
+ "borderBottomWidth": 2,
773
+ }
774
+ }
767
775
  viewabilityConfigCallbackPairs={[]}
768
776
  >
769
777
  <View>
@@ -4,7 +4,15 @@ import { TextInput as RNTextInput } from 'react-native';
4
4
  import { theme } from '../../..';
5
5
  import renderWithTheme from '../../../testHelpers/renderWithTheme';
6
6
  import Icon from '../../Icon';
7
- import TextInput, { getState, TextInputHandles } from '../index';
7
+ import TextInput, {
8
+ getState,
9
+ renderErrorOrHelpText,
10
+ renderInput,
11
+ renderMaxLengthMessage,
12
+ renderPrefix,
13
+ renderSuffix,
14
+ TextInputHandles,
15
+ } from '../index';
8
16
 
9
17
  describe('getState', () => {
10
18
  it.each`
@@ -496,3 +504,142 @@ describe('TextInput', () => {
496
504
  });
497
505
  });
498
506
  });
507
+
508
+ describe('renderErrorOrHelpText', () => {
509
+ it('renders correctly with error', () => {
510
+ const { queryAllByText } = renderWithTheme(
511
+ <>
512
+ {renderErrorOrHelpText({
513
+ error: 'This is error',
514
+ helpText: 'This is help text',
515
+ })}
516
+ </>
517
+ );
518
+
519
+ expect(queryAllByText('This is error')).toHaveLength(1);
520
+ expect(queryAllByText('This is help text')).toHaveLength(0);
521
+ });
522
+
523
+ it('renders correctly with help text', () => {
524
+ const { queryAllByText } = renderWithTheme(
525
+ <>
526
+ {renderErrorOrHelpText({
527
+ error: '',
528
+ helpText: 'This is help text',
529
+ })}
530
+ </>
531
+ );
532
+
533
+ expect(queryAllByText('This is help text')).toHaveLength(1);
534
+ });
535
+ });
536
+
537
+ describe('renderInput', () => {
538
+ it('renders correctly with renderInputValue', () => {
539
+ const wrapper = renderWithTheme(
540
+ <>
541
+ {renderInput({
542
+ variant: 'textarea',
543
+ nativeInputProps: {},
544
+ renderInputValue: (props) => {
545
+ return (
546
+ <RNTextInput
547
+ {...props}
548
+ value="customised text"
549
+ testID="custom-text-input"
550
+ />
551
+ );
552
+ },
553
+ })}
554
+ </>
555
+ );
556
+
557
+ expect(wrapper.queryAllByTestId('custom-text-input')).toHaveLength(1);
558
+ expect(wrapper.queryAllByDisplayValue('customised text')).toHaveLength(1);
559
+ });
560
+
561
+ it('renders correctly without renderInputValue', () => {
562
+ const wrapper = renderWithTheme(
563
+ <>
564
+ {renderInput({
565
+ variant: 'textarea',
566
+ nativeInputProps: {
567
+ testID: 'text-input',
568
+ value: 'text input value',
569
+ },
570
+ })}
571
+ </>
572
+ );
573
+
574
+ expect(wrapper.queryAllByTestId('text-input')).toHaveLength(1);
575
+ expect(wrapper.queryAllByDisplayValue('text input value')).toHaveLength(1);
576
+ });
577
+ });
578
+ describe('renderSuffix', () => {
579
+ it('renders loading icon with loading', () => {
580
+ const wrapper = renderWithTheme(
581
+ <>
582
+ {renderSuffix({
583
+ loading: true,
584
+ suffix: 'dollar-sign',
585
+ state: 'default',
586
+ })}
587
+ </>
588
+ );
589
+
590
+ expect(wrapper.getByTestId('input-suffix')).toHaveProp('name', 'loading');
591
+ });
592
+
593
+ it('renders suffix icon', () => {
594
+ const wrapper = renderWithTheme(
595
+ <>
596
+ {renderSuffix({
597
+ loading: false,
598
+ suffix: 'dollar-sign',
599
+ state: 'default',
600
+ })}
601
+ </>
602
+ );
603
+
604
+ // verify element has prop name = icon
605
+ expect(wrapper.getByTestId('input-suffix')).toHaveProp(
606
+ 'name',
607
+ 'dollar-sign'
608
+ );
609
+ });
610
+ });
611
+
612
+ describe('renderPrefix', () => {
613
+ it('renders prefix icon', () => {
614
+ const wrapper = renderWithTheme(
615
+ <>
616
+ {renderPrefix({
617
+ prefix: 'dollar-sign',
618
+ state: 'default',
619
+ })}
620
+ </>
621
+ );
622
+
623
+ expect(wrapper.getByTestId('input-prefix')).toHaveProp(
624
+ 'name',
625
+ 'dollar-sign'
626
+ );
627
+ });
628
+ });
629
+
630
+ describe('renderMaxLengthMessage', () => {
631
+ it('renders correctly with maxLength', () => {
632
+ const { queryAllByText } = renderWithTheme(
633
+ <>
634
+ {renderMaxLengthMessage({
635
+ maxLength: 10,
636
+ state: 'default',
637
+ currentLength: 5,
638
+ hideCharacterCount: false,
639
+ })}
640
+ </>
641
+ );
642
+
643
+ expect(queryAllByText('5/10')).toHaveLength(1);
644
+ });
645
+ });
@@ -46,6 +46,8 @@ export type TextInputHandles = Pick<
46
46
  'focus' | 'clear' | 'blur' | 'isFocused' | 'setNativeProps'
47
47
  >;
48
48
 
49
+ export type TextInputVariant = 'text' | 'textarea';
50
+
49
51
  export interface TextInputProps extends NativeTextInputProps {
50
52
  /**
51
53
  * Field label.
@@ -123,7 +125,7 @@ export interface TextInputProps extends NativeTextInputProps {
123
125
  /**
124
126
  * Component variant.
125
127
  */
126
- variant?: 'text' | 'textarea';
128
+ variant?: TextInputVariant;
127
129
  }
128
130
 
129
131
  export const getState = ({
@@ -161,6 +163,120 @@ const EMPTY_PLACEHOLDER_VALUE = ' ';
161
163
 
162
164
  export const LABEL_ANIMATION_DURATION = 150;
163
165
 
166
+ export const renderErrorOrHelpText = ({
167
+ error,
168
+ helpText,
169
+ }: {
170
+ error?: string;
171
+ helpText?: string;
172
+ }) => {
173
+ return error ? (
174
+ <StyledErrorContainer>
175
+ <Icon
176
+ testID="input-error-icon"
177
+ icon="circle-info"
178
+ size="xsmall"
179
+ intent="danger"
180
+ />
181
+
182
+ <StyledError testID="input-error-message">{error}</StyledError>
183
+ </StyledErrorContainer>
184
+ ) : (
185
+ !!helpText && <StyledHelperText>{helpText}</StyledHelperText>
186
+ );
187
+ };
188
+
189
+ export const renderInput = ({
190
+ variant,
191
+ nativeInputProps,
192
+ renderInputValue,
193
+ ref,
194
+ }: {
195
+ variant: TextInputVariant;
196
+ nativeInputProps: NativeTextInputProps;
197
+ multiline?: boolean;
198
+ renderInputValue?: (inputProps: NativeTextInputProps) => React.ReactNode;
199
+ ref?: React.Ref<RNTextInput>;
200
+ }) => {
201
+ return renderInputValue ? (
202
+ renderInputValue(nativeInputProps)
203
+ ) : (
204
+ <StyledTextInput
205
+ {...nativeInputProps}
206
+ themeVariant={variant}
207
+ multiline={variant === 'textarea' || nativeInputProps.multiline}
208
+ ref={ref}
209
+ />
210
+ );
211
+ };
212
+
213
+ export const renderSuffix = ({
214
+ state,
215
+ loading,
216
+ suffix,
217
+ }: {
218
+ state: State;
219
+ loading: boolean;
220
+ suffix?: IconName | React.ReactElement;
221
+ }) => {
222
+ const actualSuffix = loading ? 'loading' : suffix;
223
+ return typeof actualSuffix === 'string' ? (
224
+ <Icon
225
+ intent={state === 'disabled' ? 'disabled-text' : 'text'}
226
+ testID="input-suffix"
227
+ icon={actualSuffix}
228
+ spin={actualSuffix === 'loading'}
229
+ size="medium"
230
+ />
231
+ ) : (
232
+ suffix
233
+ );
234
+ };
235
+
236
+ export const renderPrefix = ({
237
+ state,
238
+ prefix,
239
+ }: {
240
+ state: State;
241
+ prefix?: IconName | React.ReactElement;
242
+ }) => {
243
+ return typeof prefix === 'string' ? (
244
+ <Icon
245
+ intent={state === 'disabled' ? 'disabled-text' : 'text'}
246
+ testID="input-prefix"
247
+ icon={prefix}
248
+ size="xsmall"
249
+ />
250
+ ) : (
251
+ prefix
252
+ );
253
+ };
254
+
255
+ export const renderMaxLengthMessage = ({
256
+ maxLength,
257
+ state,
258
+ currentLength,
259
+ hideCharacterCount,
260
+ }: {
261
+ state: State;
262
+ currentLength: number;
263
+ maxLength?: number;
264
+ hideCharacterCount: boolean;
265
+ }) => {
266
+ const shouldShowMaxLength = maxLength !== undefined && !hideCharacterCount;
267
+ return (
268
+ shouldShowMaxLength && (
269
+ <StyledMaxLengthMessage themeState={state}>
270
+ {currentLength}/{maxLength}
271
+ </StyledMaxLengthMessage>
272
+ )
273
+ );
274
+ };
275
+
276
+ export const getDisplayText = (value?: string, defaultValue?: string) => {
277
+ return (value !== undefined ? value : defaultValue) ?? '';
278
+ };
279
+
164
280
  const TextInput = forwardRef<TextInputHandles, TextInputProps>(
165
281
  (
166
282
  {
@@ -188,9 +304,8 @@ const TextInput = forwardRef<TextInputHandles, TextInputProps>(
188
304
  }: TextInputProps,
189
305
  ref?: React.Ref<TextInputHandles>
190
306
  ) => {
191
- const displayText = (value !== undefined ? value : defaultValue) ?? '';
307
+ const displayText = getDisplayText(value, defaultValue);
192
308
  const isEmptyValue = displayText.length === 0;
193
- const actualSuffix = loading ? 'loading' : suffix;
194
309
 
195
310
  const [inputSize, setInputSize] = React.useState<{
196
311
  height: number;
@@ -210,8 +325,6 @@ const TextInput = forwardRef<TextInputHandles, TextInputProps>(
210
325
  isEmptyValue,
211
326
  });
212
327
 
213
- const shouldShowMaxLength = maxLength !== undefined && !hideCharacterCount;
214
-
215
328
  const theme = useTheme();
216
329
 
217
330
  const focusAnimation = useRef(new Animated.Value(0)).current;
@@ -346,16 +459,7 @@ const TextInput = forwardRef<TextInputHandles, TextInputProps>(
346
459
  />
347
460
 
348
461
  <View onLayout={onPrefixLayout}>
349
- {typeof prefix === 'string' ? (
350
- <Icon
351
- intent={state === 'disabled' ? 'disabled-text' : 'text'}
352
- testID="input-prefix"
353
- icon={prefix}
354
- size="xsmall"
355
- />
356
- ) : (
357
- prefix
358
- )}
462
+ {renderPrefix({ state, prefix })}
359
463
  </View>
360
464
  <StyledLabelContainerInsideTextInput
361
465
  themeVariant={variant}
@@ -424,52 +528,26 @@ const TextInput = forwardRef<TextInputHandles, TextInputProps>(
424
528
  </StyledLabelContainerInsideTextInput>
425
529
 
426
530
  <StyledTextInputAndLabelContainer>
427
- {renderInputValue ? (
428
- renderInputValue(nativeInputProps)
429
- ) : (
430
- <StyledTextInput
431
- {...nativeInputProps}
432
- themeVariant={variant}
433
- multiline={variant === 'textarea' || nativeProps.multiline}
434
- ref={(reference) => {
435
- innerTextInput.current = reference;
436
- }}
437
- />
438
- )}
531
+ {renderInput({
532
+ variant,
533
+ nativeInputProps,
534
+ renderInputValue,
535
+ ref: (rnTextInputRef) => {
536
+ innerTextInput.current = rnTextInputRef;
537
+ },
538
+ })}
439
539
  </StyledTextInputAndLabelContainer>
440
- {typeof actualSuffix === 'string' ? (
441
- <Icon
442
- intent={state === 'disabled' ? 'disabled-text' : 'text'}
443
- testID="input-suffix"
444
- icon={actualSuffix}
445
- spin={actualSuffix === 'loading'}
446
- size="medium"
447
- />
448
- ) : (
449
- suffix
450
- )}
540
+ {renderSuffix({ state, loading, suffix })}
451
541
  </StyledTextInputContainer>
452
542
  <StyledErrorAndHelpTextContainer>
453
543
  <StyledErrorAndMaxLengthContainer>
454
- {error ? (
455
- <StyledErrorContainer>
456
- <Icon
457
- testID="input-error-icon"
458
- icon="circle-info"
459
- size="xsmall"
460
- intent="danger"
461
- />
462
-
463
- <StyledError testID="input-error-message">{error}</StyledError>
464
- </StyledErrorContainer>
465
- ) : (
466
- !!helpText && <StyledHelperText>{helpText}</StyledHelperText>
467
- )}
468
- {shouldShowMaxLength && (
469
- <StyledMaxLengthMessage themeState={state}>
470
- {displayText.length}/{maxLength}
471
- </StyledMaxLengthMessage>
472
- )}
544
+ {renderErrorOrHelpText({ error, helpText })}
545
+ {renderMaxLengthMessage({
546
+ state,
547
+ currentLength: displayText.length,
548
+ maxLength,
549
+ hideCharacterCount,
550
+ })}
473
551
  </StyledErrorAndMaxLengthContainer>
474
552
  </StyledErrorAndHelpTextContainer>
475
553
  </StyledContainer>