@utilitywarehouse/hearth-react-native 0.22.0 → 0.23.0-test-list

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 (70) hide show
  1. package/.turbo/turbo-build.log +5 -4
  2. package/CHANGELOG.md +36 -0
  3. package/build/components/List/List.js +2 -2
  4. package/build/components/Modal/Modal.d.ts +1 -1
  5. package/build/components/Modal/Modal.js +17 -5
  6. package/build/components/Modal/Modal.props.d.ts +1 -0
  7. package/build/components/ProgressBar/ProgressBar.d.ts +6 -0
  8. package/build/components/ProgressBar/ProgressBar.js +35 -0
  9. package/build/components/ProgressBar/ProgressBar.props.d.ts +60 -0
  10. package/build/components/ProgressBar/ProgressBar.props.js +1 -0
  11. package/build/components/ProgressBar/ProgressBarCircular.d.ts +6 -0
  12. package/build/components/ProgressBar/ProgressBarCircular.js +115 -0
  13. package/build/components/ProgressBar/ProgressBarLinear.d.ts +6 -0
  14. package/build/components/ProgressBar/ProgressBarLinear.js +79 -0
  15. package/build/components/ProgressBar/index.d.ts +2 -0
  16. package/build/components/ProgressBar/index.js +1 -0
  17. package/build/components/SegmentedControl/SegmentedControl.context.d.ts +14 -0
  18. package/build/components/SegmentedControl/SegmentedControl.context.js +9 -0
  19. package/build/components/SegmentedControl/SegmentedControl.d.ts +6 -0
  20. package/build/components/SegmentedControl/SegmentedControl.js +199 -0
  21. package/build/components/SegmentedControl/SegmentedControl.props.d.ts +18 -0
  22. package/build/components/SegmentedControl/SegmentedControl.props.js +1 -0
  23. package/build/components/SegmentedControl/SegmentedControlOption.d.ts +18 -0
  24. package/build/components/SegmentedControl/SegmentedControlOption.js +144 -0
  25. package/build/components/SegmentedControl/SegmentedControlOption.props.d.ts +14 -0
  26. package/build/components/SegmentedControl/SegmentedControlOption.props.js +1 -0
  27. package/build/components/SegmentedControl/index.d.ts +4 -0
  28. package/build/components/SegmentedControl/index.js +2 -0
  29. package/build/components/TimePicker/TimePicker.d.ts +6 -0
  30. package/build/components/TimePicker/TimePicker.js +78 -0
  31. package/build/components/TimePicker/TimePicker.props.d.ts +45 -0
  32. package/build/components/TimePicker/TimePicker.props.js +1 -0
  33. package/build/components/TimePicker/TimePickerView.d.ts +12 -0
  34. package/build/components/TimePicker/TimePickerView.js +130 -0
  35. package/build/components/TimePicker/TimePickerWheel.d.ts +8 -0
  36. package/build/components/TimePicker/TimePickerWheel.js +86 -0
  37. package/build/components/TimePicker/TimePickerWheel.web.d.ts +8 -0
  38. package/build/components/TimePicker/TimePickerWheel.web.js +122 -0
  39. package/build/components/TimePicker/index.d.ts +6 -0
  40. package/build/components/TimePicker/index.js +3 -0
  41. package/build/components/TimePickerInput/TimePickerInput.d.ts +6 -0
  42. package/build/components/TimePickerInput/TimePickerInput.js +127 -0
  43. package/build/components/TimePickerInput/TimePickerInput.props.d.ts +52 -0
  44. package/build/components/TimePickerInput/TimePickerInput.props.js +1 -0
  45. package/build/components/TimePickerInput/TimePickerInputDoneButton.d.ts +8 -0
  46. package/build/components/TimePickerInput/TimePickerInputDoneButton.js +19 -0
  47. package/build/components/TimePickerInput/TimePickerInputDoneButton.web.d.ts +5 -0
  48. package/build/components/TimePickerInput/TimePickerInputDoneButton.web.js +5 -0
  49. package/build/components/TimePickerInput/index.d.ts +2 -0
  50. package/build/components/TimePickerInput/index.js +1 -0
  51. package/build/components/VerificationInput/VerificationInput.js +182 -20
  52. package/build/components/VerificationInput/VerificationInput.utils.d.ts +8 -0
  53. package/build/components/VerificationInput/VerificationInput.utils.js +17 -0
  54. package/build/components/VerificationInput/VerificationInput.utils.test.d.ts +1 -0
  55. package/build/components/VerificationInput/VerificationInput.utils.test.js +36 -0
  56. package/build/components/VerificationInput/VerificationInputSlot.d.ts +7 -3
  57. package/build/components/VerificationInput/VerificationInputSlot.js +45 -7
  58. package/docs/changelog.mdx +249 -0
  59. package/package.json +3 -3
  60. package/src/components/List/List.tsx +5 -4
  61. package/src/components/Modal/Modal.docs.mdx +1 -0
  62. package/src/components/Modal/Modal.props.ts +1 -0
  63. package/src/components/Modal/Modal.stories.tsx +1 -0
  64. package/src/components/Modal/Modal.tsx +21 -3
  65. package/src/components/VerificationInput/VerificationInput.tsx +218 -29
  66. package/src/components/VerificationInput/VerificationInputSlot.tsx +90 -14
  67. package/.turbo/turbo-lint.log +0 -72
  68. package/build/components/VerificationInput/useVerificationInput.d.ts +0 -15
  69. package/build/components/VerificationInput/useVerificationInput.js +0 -73
  70. package/src/components/VerificationInput/useVerificationInput.ts +0 -88
@@ -14,7 +14,8 @@ const List = ({
14
14
  invalidText,
15
15
  ...props
16
16
  }: ListProps) => {
17
- const { loading, disabled, container = 'none' } = props;
17
+ const { loading, disabled, container = 'none', testID, style, ...rest } = props;
18
+
18
19
  const orderRef = useRef<string[]>([]);
19
20
  const [firstItemId, setFirstItemId] = useState<string | undefined>(undefined);
20
21
  const containerToCard: {
@@ -51,7 +52,7 @@ const List = ({
51
52
  styles.useVariants({ disabled });
52
53
  return (
53
54
  <ListContext.Provider value={value}>
54
- <View {...props} style={[styles.container, props.style]}>
55
+ <View {...rest} style={[styles.container, style]}>
55
56
  {heading ? (
56
57
  <SectionHeader
57
58
  heading={heading}
@@ -61,10 +62,10 @@ const List = ({
61
62
  />
62
63
  ) : null}
63
64
  {container === 'none' ? (
64
- <View>{children}</View>
65
+ <View testID={testID}>{children}</View>
65
66
  ) : (
66
67
  React.Children.count(children) > 0 && (
67
- <Card {...containerToCard} noPadding style={styles.card}>
68
+ <Card {...containerToCard} noPadding style={styles.card} testID={testID}>
68
69
  <>{children}</>
69
70
  </Card>
70
71
  )
@@ -109,6 +109,7 @@ The Modal component extends the `BottomSheetModal` component and accepts all of
109
109
  | `closeButtonProps` | `Omit<UnstyledIconButtonProps, 'children'>` | Additional props to pass to the close button | - |
110
110
  | `fullscreen` | `boolean` | Whether the modal should take up the full screen height | `false` |
111
111
  | `inNavModal` | `boolean` | Renders the modal correctly when used inside a navigation modal | `false` |
112
+ | `background` | `'default' \| 'brand'` | Sets the modal background. Only applies when `inNavModal` is `true` | `'default'` |
112
113
 
113
114
  \* use this to detect if the modal has been opened or closed, index 0 indicates open state and -1 indicates closed state
114
115
 
@@ -25,6 +25,7 @@ interface ModalProps extends Omit<BottomSheetProps, 'children'> {
25
25
  primaryButtonProps?: Omit<ButtonWithoutChildrenProps, 'children'>;
26
26
  secondaryButtonProps?: Omit<ButtonWithoutChildrenProps, 'children'>;
27
27
  closeButtonProps?: Omit<UnstyledIconButtonProps, 'children'>;
28
+ background?: 'default' | 'brand';
28
29
  }
29
30
 
30
31
  export default ModalProps;
@@ -66,6 +66,7 @@ const meta = {
66
66
  onPressCloseButton: () => null,
67
67
  onPressPrimaryButton: () => null,
68
68
  onPressSecondaryButton: () => null,
69
+ background: 'brand',
69
70
  },
70
71
  } satisfies Meta<typeof Modal>;
71
72
 
@@ -7,7 +7,7 @@ import {
7
7
  import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
8
8
  import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
9
9
  import { useCallback, useEffect, useImperativeHandle, useRef } from 'react';
10
- import { AccessibilityInfo, Platform, View, findNodeHandle } from 'react-native';
10
+ import { AccessibilityInfo, Platform, ScrollView, View, findNodeHandle } from 'react-native';
11
11
  import Animated, {
12
12
  Easing,
13
13
  useAnimatedStyle,
@@ -50,6 +50,7 @@ const Modal = ({
50
50
  closeButtonProps,
51
51
  inNavModal = false,
52
52
  stickyFooter = true,
53
+ background = 'default',
53
54
  ...props
54
55
  }: ModalProps) => {
55
56
  const bottomSheetModalRef = useRef<BottomSheetModal>(null);
@@ -170,6 +171,7 @@ const Modal = ({
170
171
  noButtons,
171
172
  stickyFooter,
172
173
  showHandle: props.showHandle,
174
+ background: background === 'brand' ? 'brand' : 'primary',
173
175
  });
174
176
 
175
177
  const footer = (
@@ -178,6 +180,7 @@ const Modal = ({
178
180
  <Button
179
181
  onPress={handlePrimaryButtonPress}
180
182
  text={primaryButtonText}
183
+ inverted={background === 'brand' && inNavModal}
181
184
  {...primaryButtonProps}
182
185
  variant={(primaryButtonProps?.variant as 'solid') ?? 'solid'}
183
186
  colorScheme={(primaryButtonProps?.colorScheme as 'highlight') ?? 'highlight'}
@@ -187,6 +190,7 @@ const Modal = ({
187
190
  <Button
188
191
  onPress={handleSecondaryButtonPress}
189
192
  text={secondaryButtonText}
193
+ inverted={background === 'brand' && inNavModal}
190
194
  {...secondaryButtonProps}
191
195
  variant={(secondaryButtonProps?.variant as 'outline') ?? 'outline'}
192
196
  colorScheme={(secondaryButtonProps?.colorScheme as 'functional') ?? 'functional'}
@@ -232,6 +236,7 @@ const Modal = ({
232
236
  icon={CloseMediumIcon}
233
237
  onPress={handleCloseButtonPress}
234
238
  accessibilityLabel="Close modal"
239
+ inverted={background === 'brand' && inNavModal}
235
240
  {...closeButtonProps}
236
241
  />
237
242
  ) : null}
@@ -253,7 +258,7 @@ const Modal = ({
253
258
  </View>
254
259
  </View>
255
260
  ) : null}
256
- {children}
261
+ {inNavModal ? <ScrollView style={{ flex: 1 }}>{children}</ScrollView> : children}
257
262
  {(!stickyFooter || inNavModal) && !noButtons ? footer : null}
258
263
  </View>
259
264
  )}
@@ -277,7 +282,12 @@ const Modal = ({
277
282
  );
278
283
 
279
284
  return inNavModal ? (
280
- <View style={{ flex: 1, backgroundColor: theme.color.background.primary }}>
285
+ <View
286
+ style={{
287
+ flex: 1,
288
+ backgroundColor: theme.color.background[background === 'brand' ? 'brand' : 'primary'],
289
+ }}
290
+ >
281
291
  {Platform.OS === 'android' ? (
282
292
  <Animated.View style={[styles.androidContainer, animatedBackgroundStyle]}>
283
293
  <Animated.View style={[styles.pretendContent, animatedPretendContentStyle]} />
@@ -420,6 +430,14 @@ const styles = StyleSheet.create((theme, rt) => ({
420
430
  gap: theme.components.modal.gap,
421
431
  padding: theme.components.modal.padding,
422
432
  paddingBottom: theme.components.modal.padding + rt.insets.bottom,
433
+ variants: {
434
+ background: {
435
+ primary: {},
436
+ brand: {
437
+ backgroundColor: theme.color.background.brand,
438
+ },
439
+ },
440
+ },
423
441
  },
424
442
  androidContainer: {
425
443
  height: rt.insets.top + 18,
@@ -1,8 +1,7 @@
1
- import { forwardRef, useImperativeHandle } from 'react';
2
- import { View } from 'react-native';
1
+ import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
2
+ import { Platform, TextInput, View } from 'react-native';
3
3
  import { StyleSheet } from 'react-native-unistyles';
4
4
  import { FormField } from '../FormField';
5
- import { useVerificationInput } from './useVerificationInput';
6
5
  import type { VerificationInputHandle, VerificationInputProps } from './VerificationInput.props';
7
6
  import { VerificationInputSlot } from './VerificationInputSlot';
8
7
 
@@ -28,30 +27,147 @@ const VerificationInput = forwardRef<VerificationInputHandle, VerificationInputP
28
27
  ref
29
28
  ) => {
30
29
  const length = 6;
31
- const {
32
- inputRefs,
33
- displayValue,
34
- focusedIndex,
35
- handleFocus,
36
- handleBlur,
37
- handleChangeText,
38
- handleKeyPress,
39
- } = useVerificationInput({
40
- value,
41
- onChangeText,
42
- });
30
+ const inputRef = useRef<TextInput>(null);
31
+ const latestValueRef = useRef(value);
32
+ const [displayValue, setDisplayValue] = useState(value);
33
+ const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
34
+ const [selection, setSelection] = useState({ start: 0, end: 0 });
35
+ const latestSelectionRef = useRef(selection);
36
+ const ignoreNextSelectionRef = useRef(false);
37
+ const pendingFocusIndexRef = useRef<number | null>(null);
38
+
39
+ useEffect(() => {
40
+ if (value !== latestValueRef.current) {
41
+ const trimmedValue = value.slice(0, length);
42
+ latestValueRef.current = trimmedValue;
43
+ setDisplayValue(trimmedValue);
44
+ const nextPos = Math.min(trimmedValue.length, length);
45
+ const nextSelection = { start: nextPos, end: nextPos };
46
+ ignoreNextSelectionRef.current = true;
47
+ latestSelectionRef.current = nextSelection;
48
+ setSelection(nextSelection);
49
+ }
50
+ }, [length, value]);
51
+
52
+ const updateValue = (nextValue: string) => {
53
+ const trimmedValue = nextValue.slice(0, length);
54
+ latestValueRef.current = trimmedValue;
55
+ setDisplayValue(trimmedValue);
56
+ onChangeText?.(trimmedValue);
57
+ };
58
+
59
+ const setSelectionIndex = (index: number) => {
60
+ const clampedIndex = Math.max(0, Math.min(index, length));
61
+ const hasChar = !!latestValueRef.current[clampedIndex];
62
+ const endIndex = hasChar ? Math.min(clampedIndex + 1, length) : clampedIndex;
63
+ const nextSelection = { start: clampedIndex, end: endIndex };
64
+ ignoreNextSelectionRef.current = true;
65
+ latestSelectionRef.current = nextSelection;
66
+ setSelection(nextSelection);
67
+ setFocusedIndex(Math.min(clampedIndex, length - 1));
68
+ };
69
+
70
+ const setCaretIndex = (index: number) => {
71
+ const clampedIndex = Math.max(0, Math.min(index, length));
72
+ const nextSelection = { start: clampedIndex, end: clampedIndex };
73
+ ignoreNextSelectionRef.current = true;
74
+ latestSelectionRef.current = nextSelection;
75
+ setSelection(nextSelection);
76
+ setFocusedIndex(Math.min(clampedIndex, length - 1));
77
+ };
78
+
79
+ const findDiffIndex = (prevValue: string, nextValue: string) => {
80
+ const minLength = Math.min(prevValue.length, nextValue.length);
81
+ for (let i = 0; i < minLength; i += 1) {
82
+ if (prevValue[i] !== nextValue[i]) {
83
+ return i;
84
+ }
85
+ }
86
+ return minLength;
87
+ };
88
+
89
+ const handleChangeText = (text: string) => {
90
+ const prevValue = latestValueRef.current;
91
+ const nextValue = text.slice(0, length);
92
+ const prevLength = prevValue.length;
93
+ const nextLength = nextValue.length;
94
+ const diff = nextLength - prevLength;
95
+ const isBulkInsert = text.length > 1 && diff > 1;
96
+ const shouldBlur = nextLength >= length;
97
+ let nextIndex = Math.max(
98
+ 0,
99
+ Math.min(latestSelectionRef.current.start + (diff >= 0 ? 1 : diff), length)
100
+ );
101
+ if (Platform.OS === 'android') {
102
+ const editedIndex = findDiffIndex(prevValue, nextValue);
103
+ nextIndex = diff >= 0 ? Math.min(editedIndex + 1, length) : Math.max(editedIndex, 0);
104
+ }
105
+ updateValue(nextValue);
106
+ if (isBulkInsert) {
107
+ setCaretIndex(Math.min(nextLength, length));
108
+ if (shouldBlur) {
109
+ inputRef.current?.blur();
110
+ }
111
+ return;
112
+ }
113
+ if (nextIndex >= length) {
114
+ setCaretIndex(nextIndex);
115
+ if (shouldBlur) {
116
+ inputRef.current?.blur();
117
+ }
118
+ return;
119
+ }
120
+
121
+ if (nextLength >= length) {
122
+ setSelectionIndex(nextIndex);
123
+ inputRef.current?.blur();
124
+ return;
125
+ }
126
+
127
+ const hasNextChar = !!nextValue[nextIndex];
128
+ if (hasNextChar) {
129
+ setSelectionIndex(nextIndex);
130
+ } else {
131
+ setCaretIndex(nextIndex);
132
+ }
133
+ };
134
+
135
+ const handleFocus = () => {
136
+ if (pendingFocusIndexRef.current !== null) {
137
+ setSelectionIndex(pendingFocusIndexRef.current);
138
+ pendingFocusIndexRef.current = null;
139
+ return;
140
+ }
141
+ setFocusedIndex(Math.min(selection.start, length - 1));
142
+ };
143
+
144
+ const handleBlur = () => {
145
+ setFocusedIndex(null);
146
+ };
43
147
 
44
148
  useImperativeHandle(
45
149
  ref,
46
150
  () => ({
47
- focus: () => inputRefs.current[0]?.focus(),
48
- blur: () => {
49
- inputRefs.current.forEach(input => input?.blur());
151
+ focus: () => {
152
+ inputRef.current?.focus();
153
+ const nextIndex = Math.min(latestValueRef.current.length, length - 1);
154
+ if (latestValueRef.current.length >= length) {
155
+ setSelectionIndex(nextIndex);
156
+ } else {
157
+ setCaretIndex(nextIndex);
158
+ }
159
+ },
160
+ blur: () => inputRef.current?.blur(),
161
+ clear: () => {
162
+ updateValue('');
163
+ setSelectionIndex(0);
164
+ inputRef.current?.blur();
165
+ setFocusedIndex(null);
50
166
  },
51
- clear: () => onChangeText?.(''),
52
167
  focusIndex: (index: number) => {
53
168
  if (index >= 0 && index < length) {
54
- inputRefs.current[index]?.focus();
169
+ inputRef.current?.focus();
170
+ setSelectionIndex(index);
55
171
  }
56
172
  },
57
173
  }),
@@ -60,6 +176,29 @@ const VerificationInput = forwardRef<VerificationInputHandle, VerificationInputP
60
176
 
61
177
  const slots = Array.from({ length }, (_, index) => index);
62
178
 
179
+ const getAccessibilityLabel = () => {
180
+ return label || props.accessibilityLabel;
181
+ };
182
+
183
+ const getAccessibilityHint = () => {
184
+ let accessibilityHint = '';
185
+ if (helperText) {
186
+ accessibilityHint = accessibilityHint + helperText;
187
+ }
188
+ if (validationStatus !== 'initial') {
189
+ if (accessibilityHint.length > 0) {
190
+ accessibilityHint = accessibilityHint + ', ';
191
+ }
192
+ if (validationStatus === 'invalid' && invalidText) {
193
+ accessibilityHint = accessibilityHint + invalidText;
194
+ }
195
+ if (validationStatus === 'valid' && validText) {
196
+ accessibilityHint = accessibilityHint + validText;
197
+ }
198
+ }
199
+ return accessibilityHint || props.accessibilityHint;
200
+ };
201
+
63
202
  return (
64
203
  <FormField
65
204
  label={label}
@@ -71,31 +210,70 @@ const VerificationInput = forwardRef<VerificationInputHandle, VerificationInputP
71
210
  invalidText={invalidText}
72
211
  disabled={disabled}
73
212
  readonly={readonly}
213
+ accessibilityHandledByChildren
74
214
  style={[styles.root, style]}
75
215
  {...props}
76
216
  >
77
217
  <View style={styles.slotsContainer}>
218
+ <TextInput
219
+ ref={inputRef}
220
+ value={displayValue}
221
+ autoFocus={autoFocus}
222
+ editable={!disabled && !readonly}
223
+ accessibilityLabel={getAccessibilityLabel()}
224
+ accessibilityHint={getAccessibilityHint()}
225
+ accessibilityState={{ disabled: disabled || readonly }}
226
+ importantForAccessibility="yes"
227
+ onChangeText={handleChangeText}
228
+ onSelectionChange={event => {
229
+ const nextSelection = event.nativeEvent.selection;
230
+ if (
231
+ ignoreNextSelectionRef.current &&
232
+ (nextSelection.start !== latestSelectionRef.current.start ||
233
+ nextSelection.end !== latestSelectionRef.current.end)
234
+ ) {
235
+ return;
236
+ }
237
+ if (pendingFocusIndexRef.current !== null) {
238
+ return;
239
+ }
240
+ ignoreNextSelectionRef.current = false;
241
+ latestSelectionRef.current = nextSelection;
242
+ setSelection(nextSelection);
243
+ setFocusedIndex(Math.min(nextSelection.start, length - 1));
244
+ }}
245
+ onFocus={handleFocus}
246
+ onBlur={handleBlur}
247
+ selection={selection}
248
+ keyboardType="number-pad"
249
+ textContentType="oneTimeCode"
250
+ autoComplete="sms-otp"
251
+ secureTextEntry={secureTextEntry}
252
+ maxLength={length}
253
+ caretHidden
254
+ style={styles.hiddenInput}
255
+ pointerEvents="none"
256
+ />
78
257
  {slots.map(index => {
79
258
  const char = displayValue[index] || '';
80
259
  const isActive = focusedIndex === index;
260
+ const displayChar = secureTextEntry && char ? '*' : char;
81
261
 
82
262
  return (
83
263
  <VerificationInputSlot
84
264
  key={index}
85
- ref={inputRef => {
86
- inputRefs.current[index] = inputRef;
87
- }}
88
- autoFocus={index === 0 && autoFocus}
89
- value={char}
265
+ value={displayChar}
90
266
  isActive={isActive}
267
+ showCaret={isActive && !displayChar}
91
268
  validationStatus={validationStatus}
92
269
  disabled={disabled}
93
270
  readonly={readonly}
94
271
  secureTextEntry={secureTextEntry}
95
- onChangeText={text => handleChangeText(text, index)}
96
- onKeyPress={e => handleKeyPress(e, index)}
97
- onFocus={() => handleFocus(index)}
98
- onBlur={handleBlur}
272
+ onPress={() => {
273
+ pendingFocusIndexRef.current = index;
274
+ inputRef.current?.focus();
275
+ setSelectionIndex(index);
276
+ }}
99
277
  />
100
278
  );
101
279
  })}
@@ -115,6 +293,17 @@ const styles = StyleSheet.create(theme => ({
115
293
  flexDirection: 'row',
116
294
  gap: theme.components.input.verification.gap,
117
295
  width: '100%',
296
+ position: 'relative',
297
+ },
298
+ hiddenInput: {
299
+ position: 'absolute',
300
+ width: '100%',
301
+ height: '100%',
302
+ left: 0,
303
+ top: 0,
304
+ color: 'transparent',
305
+ fontSize: 1,
306
+ opacity: 0.1,
118
307
  },
119
308
  }));
120
309
 
@@ -1,33 +1,84 @@
1
- import { forwardRef } from 'react';
2
- import { TextInput, TextInputProps } from 'react-native';
1
+ import { forwardRef, useEffect } from 'react';
2
+ import { Pressable, Text, View, ViewProps } from 'react-native';
3
+ import Animated, {
4
+ Easing,
5
+ useAnimatedStyle,
6
+ useSharedValue,
7
+ withRepeat,
8
+ withTiming,
9
+ } from 'react-native-reanimated';
3
10
  import { StyleSheet } from 'react-native-unistyles';
4
- import InputField from '../Input/InputField';
5
11
 
6
- interface VerificationInputSlotProps extends TextInputProps {
12
+ interface VerificationInputSlotProps extends ViewProps {
13
+ value: string;
7
14
  isActive: boolean;
15
+ showCaret?: boolean;
8
16
  validationStatus: 'initial' | 'valid' | 'invalid';
9
17
  disabled?: boolean;
10
18
  readonly?: boolean;
19
+ onPress?: () => void;
20
+ secureTextEntry?: boolean;
11
21
  }
12
22
 
13
- export const VerificationInputSlot = forwardRef<TextInput, VerificationInputSlotProps>(
14
- ({ isActive, validationStatus, disabled, readonly, style, ...props }, ref) => {
23
+ export const VerificationInputSlot = forwardRef<View, VerificationInputSlotProps>(
24
+ (
25
+ {
26
+ value,
27
+ isActive,
28
+ showCaret,
29
+ validationStatus,
30
+ disabled,
31
+ readonly,
32
+ style,
33
+ onPress,
34
+ secureTextEntry,
35
+ ...props
36
+ },
37
+ ref
38
+ ) => {
15
39
  styles.useVariants({
16
40
  disabled,
17
41
  readonly,
18
42
  validationStatus,
19
43
  active: isActive,
44
+ secureTextEntry,
20
45
  });
21
46
 
47
+ const caretOpacity = useSharedValue(0);
48
+ const animatedCaretStyle = useAnimatedStyle(() => ({
49
+ opacity: caretOpacity.value,
50
+ }));
51
+
52
+ useEffect(() => {
53
+ if (showCaret && !disabled && !readonly) {
54
+ caretOpacity.value = withRepeat(
55
+ withTiming(1, { duration: 500, easing: Easing.inOut(Easing.ease) }),
56
+ -1,
57
+ true
58
+ );
59
+ return;
60
+ }
61
+
62
+ caretOpacity.value = withTiming(0, { duration: 150, easing: Easing.out(Easing.ease) });
63
+ }, [caretOpacity, disabled, readonly, showCaret]);
64
+
22
65
  return (
23
- <InputField
66
+ <Pressable
24
67
  ref={ref}
25
- {...props}
26
- editable={!disabled && !readonly}
27
- selectTextOnFocus
28
- keyboardType="number-pad"
68
+ onPress={onPress}
69
+ disabled={disabled || readonly}
29
70
  style={[styles.slot, style]}
30
- />
71
+ accessibilityRole="button"
72
+ accessible={false}
73
+ accessibilityElementsHidden
74
+ importantForAccessibility="no-hide-descendants"
75
+ {...props}
76
+ >
77
+ <Text style={styles.slotText}>{value}</Text>
78
+ {showCaret && !disabled && !readonly && (
79
+ <Animated.View style={[styles.caret, animatedCaretStyle]} />
80
+ )}
81
+ </Pressable>
31
82
  );
32
83
  }
33
84
  );
@@ -36,15 +87,20 @@ VerificationInputSlot.displayName = 'VerificationInputSlot';
36
87
 
37
88
  const styles = StyleSheet.create(theme => ({
38
89
  slot: {
39
- flex: 0,
90
+ flexGrow: 0,
91
+ flexShrink: 0,
40
92
  width: theme.components.input.height,
41
93
  height: theme.components.input.height,
94
+ minWidth: theme.components.input.height,
95
+ minHeight: theme.components.input.height,
42
96
  borderWidth: theme.components.input.borderWidth,
43
97
  borderColor: theme.color.border.strong,
44
98
  borderRadius: theme.components.input.borderRadius,
45
99
  backgroundColor: theme.color.surface.neutral.strong,
46
- textAlign: 'center',
100
+ alignItems: 'center',
101
+ justifyContent: 'center',
47
102
  padding: 0,
103
+ position: 'relative',
48
104
  variants: {
49
105
  disabled: {
50
106
  true: {
@@ -91,4 +147,24 @@ const styles = StyleSheet.create(theme => ({
91
147
  },
92
148
  ],
93
149
  },
150
+ slotText: {
151
+ color: theme.color.text.primary,
152
+ fontSize: theme.typography.mobile.bodyText.md.fontSize,
153
+ fontFamily: theme.typography.mobile.bodyText.fontFamily,
154
+ fontWeight: `${theme.typography.mobile.bodyText.fontWeight}`,
155
+ textAlign: 'center',
156
+ variants: {
157
+ secureTextEntry: {
158
+ true: {
159
+ paddingTop: 5,
160
+ },
161
+ },
162
+ },
163
+ },
164
+ caret: {
165
+ position: 'absolute',
166
+ width: 2,
167
+ height: '55%',
168
+ backgroundColor: theme.color.text.brand,
169
+ },
94
170
  }));
@@ -1,72 +0,0 @@
1
-
2
- > @utilitywarehouse/hearth-react-native@0.22.0 lint /home/runner/work/hearth/hearth/packages/react-native
3
- > TIMING=1 eslint .
4
-
5
-
6
- /home/runner/work/hearth/hearth/packages/react-native/src/components/Carousel/Carousel.context.tsx
7
- 6:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
8
-
9
- /home/runner/work/hearth/hearth/packages/react-native/src/components/Carousel/Carousel.tsx
10
- 146:6 warning React Hook useMemo has a missing dependency: 'hasCarouselControlsInTree'. Either include it or remove the dependency array react-hooks/exhaustive-deps
11
-
12
- /home/runner/work/hearth/hearth/packages/react-native/src/components/DatePicker/DatePicker.tsx
13
- 109:6 warning React Hook useCallback has an unnecessary dependency: 'modalRef.current'. Either exclude it or remove the dependency array. Mutable values like 'modalRef.current' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
14
- 259:6 warning React Hook useEffect has a missing dependency: 'initialState'. Either include it or remove the dependency array react-hooks/exhaustive-deps
15
- 346:6 warning React Hook useEffect has a missing dependency: 'onChange'. Either include it or remove the dependency array react-hooks/exhaustive-deps
16
- 468:5 warning React Hook useCallback has a missing dependency: 'onChange'. Either include it or remove the dependency array react-hooks/exhaustive-deps
17
- 536:6 warning React Hook useEffect has a missing dependency: 'onSelectMonth'. Either include it or remove the dependency array react-hooks/exhaustive-deps
18
- 542:6 warning React Hook useEffect has a missing dependency: 'onSelectYear'. Either include it or remove the dependency array react-hooks/exhaustive-deps
19
-
20
- /home/runner/work/hearth/hearth/packages/react-native/src/components/DatePicker/DatePickerDay.tsx
21
- 76:6 warning React Hook useMemo has an unnecessary dependency: 'styles.rangeRoot'. Either exclude it or remove the dependency array. Outer scope values like 'styles.rangeRoot' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
22
- 84:6 warning React Hook useMemo has a missing dependency: 'isSelected'. Either include it or remove the dependency array react-hooks/exhaustive-deps
23
-
24
- /home/runner/work/hearth/hearth/packages/react-native/src/components/DatePicker/DatePickerDays.tsx
25
- 179:6 warning React Hook useMemo has unnecessary dependencies: 'month' and 'year'. Either exclude them or remove the dependency array react-hooks/exhaustive-deps
26
-
27
- /home/runner/work/hearth/hearth/packages/react-native/src/components/DatePicker/DatePickerYears.tsx
28
- 52:6 warning React Hook useCallback has a missing dependency: 'containerHeight'. Either include it or remove the dependency array. Outer scope values like 'styles' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
29
-
30
- /home/runner/work/hearth/hearth/packages/react-native/src/components/Input/Input.tsx
31
- 78:8 warning React Hook useEffect has a missing dependency: 'formFieldContext'. Either include it or remove the dependency array react-hooks/exhaustive-deps
32
-
33
- /home/runner/work/hearth/hearth/packages/react-native/src/components/Modal/Modal.tsx
34
- 73:6 warning React Hook useCallback has an unnecessary dependency: 'Platform.OS'. Either exclude it or remove the dependency array. Outer scope values like 'Platform.OS' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
35
- 269:5 warning React Hook useCallback has a missing dependency: 'footer'. Either include it or remove the dependency array react-hooks/exhaustive-deps
36
-
37
- /home/runner/work/hearth/hearth/packages/react-native/src/components/Modal/Modal.web.tsx
38
- 66:6 warning React Hook useCallback has an unnecessary dependency: 'Platform.OS'. Either exclude it or remove the dependency array. Outer scope values like 'Platform.OS' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
39
-
40
- /home/runner/work/hearth/hearth/packages/react-native/src/components/PillGroup/PillGroup.tsx
41
- 17:9 warning The 'normalizedValue' conditional could make the dependencies of useMemo Hook (at line 33) change on every render. Move it inside the useMemo callback. Alternatively, wrap the initialization of 'normalizedValue' in its own useMemo() Hook react-hooks/exhaustive-deps
42
-
43
- /home/runner/work/hearth/hearth/packages/react-native/src/components/Tabs/Tabs.tsx
44
- 53:6 warning React Hook useEffect has a missing dependency: 'tabValues'. Either include it or remove the dependency array react-hooks/exhaustive-deps
45
- 53:7 warning React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked react-hooks/exhaustive-deps
46
- 104:5 warning React Hook useMemo has an unnecessary dependency: 'tabValues'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
47
- 127:62 warning React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked react-hooks/exhaustive-deps
48
-
49
- /home/runner/work/hearth/hearth/packages/react-native/src/components/Textarea/Textarea.tsx
50
- 45:6 warning React Hook useEffect has a missing dependency: 'formFieldContext'. Either include it or remove the dependency array react-hooks/exhaustive-deps
51
-
52
- /home/runner/work/hearth/hearth/packages/react-native/src/components/Toast/Toast.context.tsx
53
- 14:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
54
- 106:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
55
-
56
- /home/runner/work/hearth/hearth/packages/react-native/src/components/VerificationInput/VerificationInput.tsx
57
- 58:7 warning React Hook useImperativeHandle has a missing dependency: 'inputRefs'. Either include it or remove the dependency array react-hooks/exhaustive-deps
58
-
59
- ✖ 25 problems (0 errors, 25 warnings)
60
-
61
- Rule | Time (ms) | Relative
62
- :-------------------------------------------------|----------:|--------:
63
- @typescript-eslint/no-unused-vars | 1687.263 | 65.5%
64
- react-hooks/rules-of-hooks | 78.867 | 3.1%
65
- react-hooks/exhaustive-deps | 78.504 | 3.0%
66
- no-global-assign | 75.145 | 2.9%
67
- no-unexpected-multiline | 55.153 | 2.1%
68
- no-loss-of-precision | 46.457 | 1.8%
69
- @typescript-eslint/ban-ts-comment | 45.439 | 1.8%
70
- no-misleading-character-class | 37.162 | 1.4%
71
- @typescript-eslint/triple-slash-reference | 28.294 | 1.1%
72
- @typescript-eslint/no-unnecessary-type-constraint | 20.347 | 0.8%