@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.
- package/.turbo/turbo-build.log +5 -4
- package/CHANGELOG.md +36 -0
- package/build/components/List/List.js +2 -2
- package/build/components/Modal/Modal.d.ts +1 -1
- package/build/components/Modal/Modal.js +17 -5
- package/build/components/Modal/Modal.props.d.ts +1 -0
- package/build/components/ProgressBar/ProgressBar.d.ts +6 -0
- package/build/components/ProgressBar/ProgressBar.js +35 -0
- package/build/components/ProgressBar/ProgressBar.props.d.ts +60 -0
- package/build/components/ProgressBar/ProgressBar.props.js +1 -0
- package/build/components/ProgressBar/ProgressBarCircular.d.ts +6 -0
- package/build/components/ProgressBar/ProgressBarCircular.js +115 -0
- package/build/components/ProgressBar/ProgressBarLinear.d.ts +6 -0
- package/build/components/ProgressBar/ProgressBarLinear.js +79 -0
- package/build/components/ProgressBar/index.d.ts +2 -0
- package/build/components/ProgressBar/index.js +1 -0
- package/build/components/SegmentedControl/SegmentedControl.context.d.ts +14 -0
- package/build/components/SegmentedControl/SegmentedControl.context.js +9 -0
- package/build/components/SegmentedControl/SegmentedControl.d.ts +6 -0
- package/build/components/SegmentedControl/SegmentedControl.js +199 -0
- package/build/components/SegmentedControl/SegmentedControl.props.d.ts +18 -0
- package/build/components/SegmentedControl/SegmentedControl.props.js +1 -0
- package/build/components/SegmentedControl/SegmentedControlOption.d.ts +18 -0
- package/build/components/SegmentedControl/SegmentedControlOption.js +144 -0
- package/build/components/SegmentedControl/SegmentedControlOption.props.d.ts +14 -0
- package/build/components/SegmentedControl/SegmentedControlOption.props.js +1 -0
- package/build/components/SegmentedControl/index.d.ts +4 -0
- package/build/components/SegmentedControl/index.js +2 -0
- package/build/components/TimePicker/TimePicker.d.ts +6 -0
- package/build/components/TimePicker/TimePicker.js +78 -0
- package/build/components/TimePicker/TimePicker.props.d.ts +45 -0
- package/build/components/TimePicker/TimePicker.props.js +1 -0
- package/build/components/TimePicker/TimePickerView.d.ts +12 -0
- package/build/components/TimePicker/TimePickerView.js +130 -0
- package/build/components/TimePicker/TimePickerWheel.d.ts +8 -0
- package/build/components/TimePicker/TimePickerWheel.js +86 -0
- package/build/components/TimePicker/TimePickerWheel.web.d.ts +8 -0
- package/build/components/TimePicker/TimePickerWheel.web.js +122 -0
- package/build/components/TimePicker/index.d.ts +6 -0
- package/build/components/TimePicker/index.js +3 -0
- package/build/components/TimePickerInput/TimePickerInput.d.ts +6 -0
- package/build/components/TimePickerInput/TimePickerInput.js +127 -0
- package/build/components/TimePickerInput/TimePickerInput.props.d.ts +52 -0
- package/build/components/TimePickerInput/TimePickerInput.props.js +1 -0
- package/build/components/TimePickerInput/TimePickerInputDoneButton.d.ts +8 -0
- package/build/components/TimePickerInput/TimePickerInputDoneButton.js +19 -0
- package/build/components/TimePickerInput/TimePickerInputDoneButton.web.d.ts +5 -0
- package/build/components/TimePickerInput/TimePickerInputDoneButton.web.js +5 -0
- package/build/components/TimePickerInput/index.d.ts +2 -0
- package/build/components/TimePickerInput/index.js +1 -0
- package/build/components/VerificationInput/VerificationInput.js +182 -20
- package/build/components/VerificationInput/VerificationInput.utils.d.ts +8 -0
- package/build/components/VerificationInput/VerificationInput.utils.js +17 -0
- package/build/components/VerificationInput/VerificationInput.utils.test.d.ts +1 -0
- package/build/components/VerificationInput/VerificationInput.utils.test.js +36 -0
- package/build/components/VerificationInput/VerificationInputSlot.d.ts +7 -3
- package/build/components/VerificationInput/VerificationInputSlot.js +45 -7
- package/docs/changelog.mdx +249 -0
- package/package.json +3 -3
- package/src/components/List/List.tsx +5 -4
- package/src/components/Modal/Modal.docs.mdx +1 -0
- package/src/components/Modal/Modal.props.ts +1 -0
- package/src/components/Modal/Modal.stories.tsx +1 -0
- package/src/components/Modal/Modal.tsx +21 -3
- package/src/components/VerificationInput/VerificationInput.tsx +218 -29
- package/src/components/VerificationInput/VerificationInputSlot.tsx +90 -14
- package/.turbo/turbo-lint.log +0 -72
- package/build/components/VerificationInput/useVerificationInput.d.ts +0 -15
- package/build/components/VerificationInput/useVerificationInput.js +0 -73
- 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 {...
|
|
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;
|
|
@@ -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
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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: () =>
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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 {
|
|
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
|
|
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<
|
|
14
|
-
(
|
|
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
|
-
<
|
|
66
|
+
<Pressable
|
|
24
67
|
ref={ref}
|
|
25
|
-
{
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}));
|
package/.turbo/turbo-lint.log
DELETED
|
@@ -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%
|