@utilitywarehouse/hearth-react-native 0.21.0 → 0.22.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.
Files changed (35) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +14 -14
  3. package/CHANGELOG.md +38 -0
  4. package/build/components/Card/Card.props.d.ts +4 -8
  5. package/build/components/Card/CardRoot.js +0 -1
  6. package/build/components/Checkbox/Checkbox.d.ts +1 -1
  7. package/build/components/Checkbox/Checkbox.js +2 -2
  8. package/build/components/Checkbox/Checkbox.props.d.ts +2 -0
  9. package/build/components/Modal/Modal.js +1 -1
  10. package/build/components/Radio/Radio.d.ts +1 -1
  11. package/build/components/Radio/Radio.js +2 -2
  12. package/build/components/Radio/Radio.props.d.ts +2 -0
  13. package/build/components/VerificationInput/VerificationInput.js +182 -20
  14. package/build/components/VerificationInput/VerificationInputSlot.d.ts +7 -3
  15. package/build/components/VerificationInput/VerificationInputSlot.js +45 -7
  16. package/docs/changelog.mdx +249 -0
  17. package/docs/components/NextPrevPage.tsx +2 -2
  18. package/package.json +6 -6
  19. package/src/components/Card/Card.props.ts +5 -8
  20. package/src/components/Card/CardRoot.tsx +0 -1
  21. package/src/components/Checkbox/Checkbox.docs.mdx +1 -0
  22. package/src/components/Checkbox/Checkbox.props.ts +2 -0
  23. package/src/components/Checkbox/Checkbox.stories.tsx +26 -0
  24. package/src/components/Checkbox/Checkbox.tsx +2 -0
  25. package/src/components/Modal/Modal.tsx +1 -1
  26. package/src/components/Radio/Radio.docs.mdx +1 -0
  27. package/src/components/Radio/Radio.props.ts +2 -0
  28. package/src/components/Radio/Radio.stories.tsx +22 -0
  29. package/src/components/Radio/Radio.tsx +2 -0
  30. package/src/components/Radio/RadioTile.figma.tsx +4 -0
  31. package/src/components/VerificationInput/VerificationInput.tsx +218 -29
  32. package/src/components/VerificationInput/VerificationInputSlot.tsx +90 -14
  33. package/build/components/VerificationInput/useVerificationInput.d.ts +0 -15
  34. package/build/components/VerificationInput/useVerificationInput.js +0 -73
  35. package/src/components/VerificationInput/useVerificationInput.ts +0 -88
@@ -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,15 +0,0 @@
1
- import { NativeSyntheticEvent, TextInput, TextInputKeyPressEventData } from 'react-native';
2
- interface UseVerificationInputProps {
3
- value: string;
4
- onChangeText?: (text: string) => void;
5
- }
6
- export declare const useVerificationInput: ({ value, onChangeText }: UseVerificationInputProps) => {
7
- inputRefs: import("react").RefObject<(TextInput | null)[]>;
8
- displayValue: string;
9
- focusedIndex: number | null;
10
- handleFocus: (index: number) => void;
11
- handleBlur: () => void;
12
- handleChangeText: (text: string, index: number) => void;
13
- handleKeyPress: (e: NativeSyntheticEvent<TextInputKeyPressEventData>, index: number) => void;
14
- };
15
- export {};
@@ -1,73 +0,0 @@
1
- import { useEffect, useRef, useState } from 'react';
2
- export const useVerificationInput = ({ value, onChangeText }) => {
3
- const length = 6;
4
- const inputRefs = useRef([]);
5
- const latestValueRef = useRef(value);
6
- const [displayValue, setDisplayValue] = useState(value);
7
- const [focusedIndex, setFocusedIndex] = useState(null);
8
- useEffect(() => {
9
- if (value !== latestValueRef.current) {
10
- latestValueRef.current = value;
11
- setDisplayValue(value);
12
- }
13
- }, [value]);
14
- const handleFocus = (index) => {
15
- setFocusedIndex(index);
16
- };
17
- const handleBlur = () => {
18
- setFocusedIndex(null);
19
- };
20
- const updateValue = (nextValue) => {
21
- latestValueRef.current = nextValue;
22
- setDisplayValue(nextValue);
23
- onChangeText?.(nextValue);
24
- };
25
- const handleChangeText = (text, index) => {
26
- const currentValue = latestValueRef.current;
27
- const chars = Array(length).fill('');
28
- for (let i = 0; i < currentValue.length && i < length; i++) {
29
- chars[i] = currentValue[i];
30
- }
31
- if (text.length > 1) {
32
- // Handle paste
33
- const pastedChars = text.slice(0, length - index).split('');
34
- for (let i = 0; i < pastedChars.length; i++) {
35
- chars[index + i] = pastedChars[i];
36
- }
37
- const nextIndex = Math.min(index + pastedChars.length, length - 1);
38
- inputRefs.current[nextIndex]?.focus();
39
- }
40
- else {
41
- // Handle single char input
42
- chars[index] = text;
43
- if (text.length === 1 && index < length - 1) {
44
- inputRefs.current[index + 1]?.focus();
45
- }
46
- }
47
- updateValue(chars.join(''));
48
- };
49
- const handleKeyPress = (e, index) => {
50
- if (e.nativeEvent.key === 'Backspace') {
51
- const currentValue = latestValueRef.current;
52
- if (!currentValue[index] && index > 0) {
53
- e.preventDefault();
54
- inputRefs.current[index - 1]?.focus();
55
- const chars = Array(length).fill('');
56
- for (let i = 0; i < currentValue.length && i < length; i++) {
57
- chars[i] = currentValue[i];
58
- }
59
- chars[index - 1] = '';
60
- updateValue(chars.join(''));
61
- }
62
- }
63
- };
64
- return {
65
- inputRefs,
66
- displayValue,
67
- focusedIndex,
68
- handleFocus,
69
- handleBlur,
70
- handleChangeText,
71
- handleKeyPress,
72
- };
73
- };
@@ -1,88 +0,0 @@
1
- import { useEffect, useRef, useState } from 'react';
2
- import { NativeSyntheticEvent, TextInput, TextInputKeyPressEventData } from 'react-native';
3
-
4
- interface UseVerificationInputProps {
5
- value: string;
6
- onChangeText?: (text: string) => void;
7
- }
8
-
9
- export const useVerificationInput = ({ value, onChangeText }: UseVerificationInputProps) => {
10
- const length = 6;
11
- const inputRefs = useRef<(TextInput | null)[]>([]);
12
- const latestValueRef = useRef(value);
13
- const [displayValue, setDisplayValue] = useState(value);
14
- const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
15
-
16
- useEffect(() => {
17
- if (value !== latestValueRef.current) {
18
- latestValueRef.current = value;
19
- setDisplayValue(value);
20
- }
21
- }, [value]);
22
-
23
- const handleFocus = (index: number) => {
24
- setFocusedIndex(index);
25
- };
26
-
27
- const handleBlur = () => {
28
- setFocusedIndex(null);
29
- };
30
-
31
- const updateValue = (nextValue: string) => {
32
- latestValueRef.current = nextValue;
33
- setDisplayValue(nextValue);
34
- onChangeText?.(nextValue);
35
- };
36
-
37
- const handleChangeText = (text: string, index: number) => {
38
- const currentValue = latestValueRef.current;
39
- const chars = Array(length).fill('');
40
- for (let i = 0; i < currentValue.length && i < length; i++) {
41
- chars[i] = currentValue[i];
42
- }
43
-
44
- if (text.length > 1) {
45
- // Handle paste
46
- const pastedChars = text.slice(0, length - index).split('');
47
- for (let i = 0; i < pastedChars.length; i++) {
48
- chars[index + i] = pastedChars[i];
49
- }
50
- const nextIndex = Math.min(index + pastedChars.length, length - 1);
51
- inputRefs.current[nextIndex]?.focus();
52
- } else {
53
- // Handle single char input
54
- chars[index] = text;
55
- if (text.length === 1 && index < length - 1) {
56
- inputRefs.current[index + 1]?.focus();
57
- }
58
- }
59
- updateValue(chars.join(''));
60
- };
61
-
62
- const handleKeyPress = (e: NativeSyntheticEvent<TextInputKeyPressEventData>, index: number) => {
63
- if (e.nativeEvent.key === 'Backspace') {
64
- const currentValue = latestValueRef.current;
65
- if (!currentValue[index] && index > 0) {
66
- e.preventDefault();
67
- inputRefs.current[index - 1]?.focus();
68
-
69
- const chars = Array(length).fill('');
70
- for (let i = 0; i < currentValue.length && i < length; i++) {
71
- chars[i] = currentValue[i];
72
- }
73
- chars[index - 1] = '';
74
- updateValue(chars.join(''));
75
- }
76
- }
77
- };
78
-
79
- return {
80
- inputRefs,
81
- displayValue,
82
- focusedIndex,
83
- handleFocus,
84
- handleBlur,
85
- handleChangeText,
86
- handleKeyPress,
87
- };
88
- };