@hero-design/rn 8.57.0 → 8.57.1

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.
@@ -114,9 +114,9 @@ describe('Calendar', () => {
114
114
  platform
115
115
  ${'ios'}
116
116
  ${'android'}
117
- `('renders correct picker on $platform', ({ platform }) => {
117
+ `('renders correct picker on $platform', ({ platform: mockedPlatform }) => {
118
118
  jest.mock('react-native/Libraries/Utilities/Platform', () => ({
119
- OS: platform,
119
+ OS: mockedPlatform,
120
120
  select: () => null,
121
121
  }));
122
122
 
@@ -133,7 +133,7 @@ describe('Calendar', () => {
133
133
  fireEvent.press(getByTestId('calendar-month-picker'));
134
134
 
135
135
  // Pickers are mocked at packages/rn/testUtils/setup.tsx
136
- if (platform === 'ios') {
136
+ if (mockedPlatform === 'ios') {
137
137
  expect(queryByText('IOS picker')).toBeDefined();
138
138
  } else {
139
139
  expect(queryByText('Android picker')).toBeDefined();
@@ -146,9 +146,9 @@ describe('Calendar', () => {
146
146
  ${'android'}
147
147
  `(
148
148
  'onToggleMonthPicker is called when toggling month year picker on $platform',
149
- ({ platform }) => {
149
+ ({ platform: mockedPlatform }) => {
150
150
  jest.mock('react-native/Libraries/Utilities/Platform', () => ({
151
- OS: platform,
151
+ OS: mockedPlatform,
152
152
  select: () => null,
153
153
  }));
154
154
 
@@ -67,6 +67,10 @@ export interface CardCarouselProps {
67
67
  onLayout?: (event: LayoutChangeEvent) => void;
68
68
  }
69
69
 
70
+ export const getCardCarouselValidIndex = (index: number, length: number) => {
71
+ return Math.min(Math.max(index, 0), length - 1);
72
+ };
73
+
70
74
  export const CardCarousel = forwardRef<CardCarouselHandles, CardCarouselProps>(
71
75
  (
72
76
  {
@@ -92,13 +96,7 @@ export const CardCarousel = forwardRef<CardCarouselHandles, CardCarouselProps>(
92
96
  Platform.OS === 'ios' ? VIEW_POSITION_CENTER : undefined;
93
97
  const snapToIndex = useCallback(
94
98
  (index: number) => {
95
- let validIndex = 0;
96
-
97
- if (index >= items.length) {
98
- validIndex = items.length - 1;
99
- } else if (index >= 0) {
100
- validIndex = index;
101
- }
99
+ const validIndex = getCardCarouselValidIndex(index, items.length);
102
100
 
103
101
  carouselRef.current?.scrollToIndex({
104
102
  index: validIndex,
@@ -106,7 +104,7 @@ export const CardCarousel = forwardRef<CardCarouselHandles, CardCarouselProps>(
106
104
  viewPosition,
107
105
  });
108
106
  },
109
- [carouselRef, itemWidth]
107
+ [items.length, viewPosition]
110
108
  );
111
109
 
112
110
  /*
@@ -122,7 +120,7 @@ export const CardCarousel = forwardRef<CardCarouselHandles, CardCarouselProps>(
122
120
  animated: true,
123
121
  viewPosition,
124
122
  });
125
- }, [carouselRef, currentIndex, itemWidth, items.length]);
123
+ }, [currentIndex, items.length, viewPosition]);
126
124
 
127
125
  React.useImperativeHandle(
128
126
  ref,
@@ -7,6 +7,7 @@ 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';
10
11
 
11
12
  const carouselData = [
12
13
  {
@@ -180,3 +181,16 @@ describe('Carousel', () => {
180
181
  expect(queryByTestId('page-control-indicator1')).toBeFalsy();
181
182
  });
182
183
  });
184
+
185
+ // write test for getCardCarouselValidIndex
186
+ describe('getCardCarouselValidIndex', () => {
187
+ it('should return 0 when index is less than 0', () => {
188
+ expect(getCardCarouselValidIndex(-1, 3)).toBe(0);
189
+ });
190
+ it('should return 2 when index is equal to 2', () => {
191
+ expect(getCardCarouselValidIndex(2, 3)).toBe(2);
192
+ });
193
+ it('should return 2 when index is greater than 2', () => {
194
+ expect(getCardCarouselValidIndex(3, 3)).toBe(2);
195
+ });
196
+ });
@@ -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
  });
@@ -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>
@@ -7,8 +7,18 @@ export type ToastControllerContextType = {
7
7
  clearAll: () => void;
8
8
  };
9
9
 
10
+ export const fallbackToastControlContext: ToastControllerContextType = {
11
+ show: (_: Omit<ToastProps, 'position'>) => '',
12
+ hide: (_: string) => {
13
+ // Fallback empty function
14
+ },
15
+ clearAll: () => {
16
+ // Fallback empty function
17
+ },
18
+ };
19
+
10
20
  export const ToastContext = createContext<ToastControllerContextType>(
11
- {} as ToastControllerContextType
21
+ fallbackToastControlContext
12
22
  );
13
23
 
14
24
  export type ToastConfigContextType = Pick<
@@ -22,4 +32,12 @@ export const ToastConfigContext = createContext<ToastConfigContextType>(
22
32
 
23
33
  export const useToastConfig = () => useContext(ToastConfigContext);
24
34
 
25
- export const useToast = () => useContext(ToastContext);
35
+ export const useToast = () => {
36
+ const context = useContext<ToastControllerContextType>(ToastContext);
37
+ if (!context) {
38
+ // eslint-disable-next-line no-console
39
+ console.warn('Toast was used without ToastProvider');
40
+ return fallbackToastControlContext;
41
+ }
42
+ return context;
43
+ };
@@ -3,7 +3,11 @@ import { View } from 'react-native';
3
3
 
4
4
  import type { ReactNode } from 'react';
5
5
  import ToastContainer from './ToastContainer';
6
- import { ToastConfigContext, ToastContext } from './ToastContext';
6
+ import {
7
+ ToastConfigContext,
8
+ ToastContext,
9
+ fallbackToastControlContext,
10
+ } from './ToastContext';
7
11
  import type { ToastControllerContextType } from './ToastContext';
8
12
  import type { ToastContainerProps } from './types';
9
13
  import { useDeprecation } from '../../utils/hooks';
@@ -29,8 +33,7 @@ const ToastProvider = ({
29
33
  );
30
34
 
31
35
  const toastRef = useRef<ToastControllerContextType>(null);
32
- // @ts-expect-error: TODO: @tungv Fix this type error
33
- const [refState, setRefState] = useState<ToastControllerContextType>(null);
36
+ const [refState, setRefState] = useState<ToastControllerContextType>();
34
37
 
35
38
  useEffect(() => {
36
39
  if (toastRef.current) {
@@ -44,7 +47,7 @@ const ToastProvider = ({
44
47
  );
45
48
 
46
49
  return (
47
- <ToastContext.Provider value={refState}>
50
+ <ToastContext.Provider value={refState || fallbackToastControlContext}>
48
51
  <View style={{ flex: 1 }}>
49
52
  {refState ? children : null}
50
53
  <ToastConfigContext.Provider value={config}>
@@ -38,4 +38,5 @@ export interface CardCarouselProps {
38
38
  ref?: React.Ref<CardCarouselHandles>;
39
39
  onLayout?: (event: LayoutChangeEvent) => void;
40
40
  }
41
+ export declare const getCardCarouselValidIndex: (index: number, length: number) => number;
41
42
  export declare const CardCarousel: React.ForwardRefExoticComponent<Omit<CardCarouselProps, "ref"> & React.RefAttributes<CardCarouselHandles>>;