panelui-native 0.16.0 → 0.20.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.
- package/lib/module/components/bottom-sheet/index.js +38 -10
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/checkbox/index.js +24 -11
- package/lib/module/components/checkbox/index.js.map +1 -1
- package/lib/module/components/field/index.js +337 -0
- package/lib/module/components/field/index.js.map +1 -0
- package/lib/module/components/flow/flow-paths.js +378 -0
- package/lib/module/components/flow/flow-paths.js.map +1 -0
- package/lib/module/components/flow/index.js +1598 -0
- package/lib/module/components/flow/index.js.map +1 -0
- package/lib/module/components/form/index.js +77 -0
- package/lib/module/components/form/index.js.map +1 -0
- package/lib/module/components/form/use-field.js +50 -0
- package/lib/module/components/form/use-field.js.map +1 -0
- package/lib/module/components/form/use-form.js +217 -0
- package/lib/module/components/form/use-form.js.map +1 -0
- package/lib/module/components/frame/index.js +124 -22
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/otp-input/index.js +303 -0
- package/lib/module/components/otp-input/index.js.map +1 -0
- package/lib/module/components/rating/index.js +339 -0
- package/lib/module/components/rating/index.js.map +1 -0
- package/lib/module/components/signature/index.js +500 -0
- package/lib/module/components/signature/index.js.map +1 -0
- package/lib/module/components/slider/index.js +14 -1
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/icons/index.js +247 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +7 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +15 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/checkbox/index.d.ts +8 -0
- package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
- package/lib/typescript/src/components/field/index.d.ts +94 -0
- package/lib/typescript/src/components/field/index.d.ts.map +1 -0
- package/lib/typescript/src/components/flow/flow-paths.d.ts +89 -0
- package/lib/typescript/src/components/flow/flow-paths.d.ts.map +1 -0
- package/lib/typescript/src/components/flow/index.d.ts +310 -0
- package/lib/typescript/src/components/flow/index.d.ts.map +1 -0
- package/lib/typescript/src/components/form/index.d.ts +64 -0
- package/lib/typescript/src/components/form/index.d.ts.map +1 -0
- package/lib/typescript/src/components/form/use-field.d.ts +16 -0
- package/lib/typescript/src/components/form/use-field.d.ts.map +1 -0
- package/lib/typescript/src/components/form/use-form.d.ts +37 -0
- package/lib/typescript/src/components/form/use-form.d.ts.map +1 -0
- package/lib/typescript/src/components/frame/index.d.ts +39 -2
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
- package/lib/typescript/src/components/otp-input/index.d.ts +141 -0
- package/lib/typescript/src/components/otp-input/index.d.ts.map +1 -0
- package/lib/typescript/src/components/rating/index.d.ts +122 -0
- package/lib/typescript/src/components/rating/index.d.ts.map +1 -0
- package/lib/typescript/src/components/signature/index.d.ts +254 -0
- package/lib/typescript/src/components/signature/index.d.ts.map +1 -0
- package/lib/typescript/src/components/slider/index.d.ts +6 -0
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/icons/index.d.ts +25 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +8 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +9 -1
- package/src/components/bottom-sheet/index.tsx +50 -12
- package/src/components/checkbox/index.tsx +40 -12
- package/src/components/field/index.tsx +329 -0
- package/src/components/flow/flow-paths.ts +348 -0
- package/src/components/flow/index.tsx +1924 -0
- package/src/components/form/index.tsx +85 -0
- package/src/components/form/use-field.ts +65 -0
- package/src/components/form/use-form.ts +252 -0
- package/src/components/frame/index.tsx +162 -14
- package/src/components/otp-input/index.tsx +396 -0
- package/src/components/rating/index.tsx +416 -0
- package/src/components/signature/index.tsx +681 -0
- package/src/components/slider/index.tsx +20 -1
- package/src/icons/index.tsx +185 -0
- package/src/index.ts +78 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OtpInput — a one-time-code field drawn as a row of separate cells.
|
|
3
|
+
*
|
|
4
|
+
* The problem this solves is that a code is read and typed one character at a
|
|
5
|
+
* time, and a single text box hides that: you cannot see, at a glance, how
|
|
6
|
+
* many digits are left. So the value is spread across `length` cells, each its
|
|
7
|
+
* own box, and the caret sits in the cell you are about to fill.
|
|
8
|
+
*
|
|
9
|
+
* There is still exactly one text field underneath — a transparent input laid
|
|
10
|
+
* over the whole row — because the software keyboard, autofill and paste all
|
|
11
|
+
* belong to a real `TextInput`, and splitting the value across N inputs fights
|
|
12
|
+
* every one of them. The cells are drawn from the value; the input owns it.
|
|
13
|
+
* Tapping any cell focuses that one input, so the platform never sees the seam.
|
|
14
|
+
*
|
|
15
|
+
* The active cell's border crosses to the focus colour on the UI thread rather
|
|
16
|
+
* than snapping, matching the rest of the field family, and a caret blinks in
|
|
17
|
+
* the cell awaiting input. Each digit zooms in as it lands, so a fast type or a
|
|
18
|
+
* one-tap autofill reads as the code arriving rather than the box redrawing.
|
|
19
|
+
*
|
|
20
|
+
* ```tsx
|
|
21
|
+
* const [code, setCode] = useState('');
|
|
22
|
+
* <OtpInput value={code} onChangeText={setCode} onComplete={submit} />
|
|
23
|
+
* <OtpInput length={4} mask groupEvery={2} type="numeric" />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
27
|
+
import { Pressable, TextInput, View, type TextInputProps } from 'react-native';
|
|
28
|
+
import Animated, {
|
|
29
|
+
cancelAnimation,
|
|
30
|
+
interpolateColor,
|
|
31
|
+
useAnimatedStyle,
|
|
32
|
+
useSharedValue,
|
|
33
|
+
withRepeat,
|
|
34
|
+
withSequence,
|
|
35
|
+
withTiming,
|
|
36
|
+
ZoomIn,
|
|
37
|
+
} from 'react-native-reanimated';
|
|
38
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
39
|
+
import { useCSSVariable } from 'uniwind';
|
|
40
|
+
import { Text } from '../../primitives/text';
|
|
41
|
+
|
|
42
|
+
/** Long enough to read as a transition, short enough not to lag a fast type. */
|
|
43
|
+
const FOCUS_DURATION = 150;
|
|
44
|
+
/** A caret that is on for the same time it is off reads as a resting cursor. */
|
|
45
|
+
const CARET_PERIOD = 560;
|
|
46
|
+
|
|
47
|
+
const otpVariants = tv({
|
|
48
|
+
slots: {
|
|
49
|
+
container: 'gap-1.5',
|
|
50
|
+
row: 'flex-row items-center',
|
|
51
|
+
cell: 'items-center justify-center rounded-lg border border-input bg-background',
|
|
52
|
+
char: 'text-foreground',
|
|
53
|
+
caret: 'w-0.5 rounded-full bg-foreground',
|
|
54
|
+
separator: 'h-0.5 rounded-full bg-border',
|
|
55
|
+
error: 'text-sm text-destructive',
|
|
56
|
+
},
|
|
57
|
+
variants: {
|
|
58
|
+
size: {
|
|
59
|
+
sm: { cell: 'h-10 w-9', char: 'text-base', caret: 'h-5', separator: 'w-2' },
|
|
60
|
+
md: { cell: 'h-12 w-11', char: 'text-lg', caret: 'h-6', separator: 'w-2.5' },
|
|
61
|
+
lg: { cell: 'h-14 w-12', char: 'text-xl', caret: 'h-7', separator: 'w-3' },
|
|
62
|
+
},
|
|
63
|
+
disabled: {
|
|
64
|
+
true: { row: 'opacity-[0.64]' },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
defaultVariants: {
|
|
68
|
+
size: 'md',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
type OtpVariantProps = VariantProps<typeof otpVariants>;
|
|
73
|
+
|
|
74
|
+
export interface OtpInputProps
|
|
75
|
+
extends Omit<
|
|
76
|
+
TextInputProps,
|
|
77
|
+
'value' | 'defaultValue' | 'onChangeText' | 'maxLength' | 'children'
|
|
78
|
+
>,
|
|
79
|
+
Omit<OtpVariantProps, 'disabled'> {
|
|
80
|
+
className?: string;
|
|
81
|
+
/** Class applied to every cell — for a taller box or a different radius. */
|
|
82
|
+
cellClassName?: string;
|
|
83
|
+
/** How many cells, i.e. the length of the code. */
|
|
84
|
+
length?: number;
|
|
85
|
+
/** Controlled value. Longer strings and stray characters are trimmed to fit. */
|
|
86
|
+
value?: string;
|
|
87
|
+
/** Starting value when the field manages its own state. */
|
|
88
|
+
defaultValue?: string;
|
|
89
|
+
onChangeText?: (value: string) => void;
|
|
90
|
+
/** Fires once, the moment the last cell is filled. */
|
|
91
|
+
onComplete?: (value: string) => void;
|
|
92
|
+
/**
|
|
93
|
+
* What the keyboard offers and what the field accepts. `numeric` keeps
|
|
94
|
+
* digits only and asks for the number pad; `text` accepts any character.
|
|
95
|
+
*/
|
|
96
|
+
type?: 'numeric' | 'text';
|
|
97
|
+
/** Hide each filled character behind a dot, the way a passcode field does. */
|
|
98
|
+
mask?: boolean;
|
|
99
|
+
/** A single character shown, dimmed, in every cell still waiting for input. */
|
|
100
|
+
placeholder?: string;
|
|
101
|
+
/** Draw a separator between groups of this many cells — 3 gives `xxx — xxx`. */
|
|
102
|
+
groupEvery?: number;
|
|
103
|
+
disabled?: boolean;
|
|
104
|
+
/** Tint the field in its error colour and announce it as invalid. */
|
|
105
|
+
isInvalid?: boolean;
|
|
106
|
+
/** Error line under the field. Setting it also puts the field in its invalid state. */
|
|
107
|
+
errorMessage?: string;
|
|
108
|
+
/** Announced by a screen reader as the field's name. */
|
|
109
|
+
accessibilityLabel?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface CellProps {
|
|
113
|
+
char: string;
|
|
114
|
+
active: boolean;
|
|
115
|
+
focused: boolean;
|
|
116
|
+
mask: boolean;
|
|
117
|
+
placeholder?: string;
|
|
118
|
+
invalid: boolean;
|
|
119
|
+
className: string;
|
|
120
|
+
charClassName: string;
|
|
121
|
+
caretClassName: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* One box. It owns three animations, all on the UI thread: the border crossing
|
|
126
|
+
* to the focus (or error) colour when it becomes the active cell, the caret
|
|
127
|
+
* blinking while it waits, and the entrance of the character that lands in it.
|
|
128
|
+
*/
|
|
129
|
+
function OtpCell({
|
|
130
|
+
char,
|
|
131
|
+
active,
|
|
132
|
+
focused,
|
|
133
|
+
mask,
|
|
134
|
+
placeholder,
|
|
135
|
+
invalid,
|
|
136
|
+
className,
|
|
137
|
+
charClassName,
|
|
138
|
+
caretClassName,
|
|
139
|
+
}: CellProps) {
|
|
140
|
+
const restColor = useCSSVariable('--color-input');
|
|
141
|
+
const focusColor = useCSSVariable('--color-ring');
|
|
142
|
+
const errorColor = useCSSVariable('--color-destructive');
|
|
143
|
+
const placeholderColor = useCSSVariable('--color-muted-foreground');
|
|
144
|
+
|
|
145
|
+
const resting = typeof restColor === 'string' ? restColor : '#e5e5e5';
|
|
146
|
+
const activeColor = invalid
|
|
147
|
+
? typeof errorColor === 'string'
|
|
148
|
+
? errorColor
|
|
149
|
+
: '#ef4444'
|
|
150
|
+
: typeof focusColor === 'string'
|
|
151
|
+
? focusColor
|
|
152
|
+
: '#a3a3a3';
|
|
153
|
+
// An invalid field carries its tint even when it is not the active cell —
|
|
154
|
+
// the error is a fact about the value, not about where the caret happens to be.
|
|
155
|
+
const idle = invalid ? activeColor : resting;
|
|
156
|
+
|
|
157
|
+
const lit = active && focused;
|
|
158
|
+
|
|
159
|
+
const border = useSharedValue(lit ? 1 : 0);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
border.value = withTiming(lit ? 1 : 0, { duration: FOCUS_DURATION });
|
|
162
|
+
}, [lit, border]);
|
|
163
|
+
|
|
164
|
+
const borderStyle = useAnimatedStyle(() => ({
|
|
165
|
+
borderColor: interpolateColor(border.value, [0, 1], [idle, activeColor]),
|
|
166
|
+
// A ring only when lit — the resting cells keep their hairline.
|
|
167
|
+
borderWidth: 1 + border.value,
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
const caret = useSharedValue(0);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (lit && char.length === 0) {
|
|
173
|
+
caret.value = withRepeat(
|
|
174
|
+
withSequence(
|
|
175
|
+
withTiming(1, { duration: 1 }),
|
|
176
|
+
withTiming(1, { duration: CARET_PERIOD }),
|
|
177
|
+
withTiming(0, { duration: CARET_PERIOD })
|
|
178
|
+
),
|
|
179
|
+
-1,
|
|
180
|
+
false
|
|
181
|
+
);
|
|
182
|
+
} else {
|
|
183
|
+
cancelAnimation(caret);
|
|
184
|
+
caret.value = 0;
|
|
185
|
+
}
|
|
186
|
+
return () => cancelAnimation(caret);
|
|
187
|
+
}, [lit, char.length, caret]);
|
|
188
|
+
|
|
189
|
+
const caretStyle = useAnimatedStyle(() => ({ opacity: caret.value }));
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<Animated.View style={borderStyle} className={className}>
|
|
193
|
+
{char.length > 0 ? (
|
|
194
|
+
<Animated.Text
|
|
195
|
+
// Keyed by the character so a change re-mounts and replays the pop.
|
|
196
|
+
key={char}
|
|
197
|
+
entering={ZoomIn.duration(140)}
|
|
198
|
+
className={charClassName}
|
|
199
|
+
>
|
|
200
|
+
{mask ? '•' : char}
|
|
201
|
+
</Animated.Text>
|
|
202
|
+
) : lit ? (
|
|
203
|
+
<Animated.View
|
|
204
|
+
style={caretStyle}
|
|
205
|
+
className={caretClassName}
|
|
206
|
+
pointerEvents="none"
|
|
207
|
+
/>
|
|
208
|
+
) : placeholder ? (
|
|
209
|
+
<Text
|
|
210
|
+
className={charClassName}
|
|
211
|
+
style={{
|
|
212
|
+
color:
|
|
213
|
+
typeof placeholderColor === 'string' ? placeholderColor : '#a3a3a3',
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
{placeholder}
|
|
217
|
+
</Text>
|
|
218
|
+
) : null}
|
|
219
|
+
</Animated.View>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Clamp and filter a raw string down to what this field will hold. */
|
|
224
|
+
function sanitize(raw: string, length: number, type: 'numeric' | 'text'): string {
|
|
225
|
+
const filtered = type === 'numeric' ? raw.replace(/[^0-9]/g, '') : raw;
|
|
226
|
+
return filtered.slice(0, length);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const OtpInput = forwardRef<TextInput, OtpInputProps>(
|
|
230
|
+
(
|
|
231
|
+
{
|
|
232
|
+
className,
|
|
233
|
+
cellClassName,
|
|
234
|
+
length = 6,
|
|
235
|
+
value: valueProp,
|
|
236
|
+
defaultValue = '',
|
|
237
|
+
onChangeText,
|
|
238
|
+
onComplete,
|
|
239
|
+
type = 'numeric',
|
|
240
|
+
mask = false,
|
|
241
|
+
placeholder,
|
|
242
|
+
groupEvery = 0,
|
|
243
|
+
size,
|
|
244
|
+
disabled,
|
|
245
|
+
isInvalid,
|
|
246
|
+
errorMessage,
|
|
247
|
+
accessibilityLabel,
|
|
248
|
+
onFocus,
|
|
249
|
+
onBlur,
|
|
250
|
+
style,
|
|
251
|
+
...props
|
|
252
|
+
},
|
|
253
|
+
ref
|
|
254
|
+
) => {
|
|
255
|
+
const controlled = valueProp != null;
|
|
256
|
+
const [internal, setInternal] = useState(() =>
|
|
257
|
+
sanitize(defaultValue, length, type)
|
|
258
|
+
);
|
|
259
|
+
const value = sanitize(controlled ? valueProp! : internal, length, type);
|
|
260
|
+
const invalid = isInvalid || !!errorMessage;
|
|
261
|
+
|
|
262
|
+
const [focused, setFocused] = useState(false);
|
|
263
|
+
const inputRef = useRef<TextInput | null>(null);
|
|
264
|
+
|
|
265
|
+
// A one-shot guard so onComplete fires on the transition to full, not on
|
|
266
|
+
// every keystroke that leaves the field full (e.g. a trailing focus event).
|
|
267
|
+
const completedRef = useRef(false);
|
|
268
|
+
|
|
269
|
+
const slots = otpVariants({ size, disabled: !!disabled });
|
|
270
|
+
|
|
271
|
+
const setRef = useCallback(
|
|
272
|
+
(node: TextInput | null) => {
|
|
273
|
+
inputRef.current = node;
|
|
274
|
+
if (typeof ref === 'function') ref(node);
|
|
275
|
+
else if (ref) ref.current = node;
|
|
276
|
+
},
|
|
277
|
+
[ref]
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const handleChange = useCallback(
|
|
281
|
+
(raw: string) => {
|
|
282
|
+
const next = sanitize(raw, length, type);
|
|
283
|
+
if (!controlled) setInternal(next);
|
|
284
|
+
onChangeText?.(next);
|
|
285
|
+
if (next.length === length) {
|
|
286
|
+
if (!completedRef.current) {
|
|
287
|
+
completedRef.current = true;
|
|
288
|
+
onComplete?.(next);
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
completedRef.current = false;
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
[controlled, length, type, onChangeText, onComplete]
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// Keep the guard honest when the value is driven from outside (a reset, a
|
|
298
|
+
// paste through the controlled prop) rather than through the keyboard.
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
completedRef.current = value.length === length;
|
|
301
|
+
}, [value.length, length]);
|
|
302
|
+
|
|
303
|
+
const focus = useCallback(() => {
|
|
304
|
+
if (!disabled) inputRef.current?.focus();
|
|
305
|
+
}, [disabled]);
|
|
306
|
+
|
|
307
|
+
const activeIndex = Math.min(value.length, length - 1);
|
|
308
|
+
|
|
309
|
+
const cells = useMemo(() => Array.from({ length }, (_, i) => i), [length]);
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<View className={slots.container({ className })}>
|
|
313
|
+
{/* The row is one big hit target; the field underneath is the a11y node. */}
|
|
314
|
+
<Pressable onPress={focus} disabled={disabled} accessible={false}>
|
|
315
|
+
<View className={slots.row()}>
|
|
316
|
+
{cells.map((i) => (
|
|
317
|
+
<View key={i} className="flex-row items-center">
|
|
318
|
+
<OtpCell
|
|
319
|
+
char={value[i] ?? ''}
|
|
320
|
+
active={i === activeIndex}
|
|
321
|
+
focused={focused}
|
|
322
|
+
mask={mask}
|
|
323
|
+
placeholder={placeholder}
|
|
324
|
+
invalid={invalid}
|
|
325
|
+
className={slots.cell({
|
|
326
|
+
className: `${i > 0 ? 'ml-2' : ''} ${cellClassName ?? ''}`,
|
|
327
|
+
})}
|
|
328
|
+
charClassName={slots.char()}
|
|
329
|
+
caretClassName={slots.caret()}
|
|
330
|
+
/>
|
|
331
|
+
{groupEvery > 0 &&
|
|
332
|
+
(i + 1) % groupEvery === 0 &&
|
|
333
|
+
i < length - 1 ? (
|
|
334
|
+
<View className={`${slots.separator()} ml-2`} />
|
|
335
|
+
) : null}
|
|
336
|
+
</View>
|
|
337
|
+
))}
|
|
338
|
+
</View>
|
|
339
|
+
|
|
340
|
+
{/*
|
|
341
|
+
* The real field: one transparent input stretched over the whole row.
|
|
342
|
+
* It carries the keyboard, autofill and paste, and its value is the
|
|
343
|
+
* source the cells are drawn from — there is no per-cell input to keep
|
|
344
|
+
* in sync. `pointerEvents` off so taps fall through to the Pressable,
|
|
345
|
+
* which focuses this exact input.
|
|
346
|
+
*/}
|
|
347
|
+
<TextInput
|
|
348
|
+
ref={setRef}
|
|
349
|
+
value={value}
|
|
350
|
+
onChangeText={handleChange}
|
|
351
|
+
editable={!disabled}
|
|
352
|
+
keyboardType={type === 'numeric' ? 'number-pad' : 'default'}
|
|
353
|
+
// The OS one-time-code affordances: iOS surfaces the SMS code above
|
|
354
|
+
// the keyboard, Android offers to autofill it.
|
|
355
|
+
textContentType="oneTimeCode"
|
|
356
|
+
autoComplete={type === 'numeric' ? 'sms-otp' : 'one-time-code'}
|
|
357
|
+
maxLength={length}
|
|
358
|
+
caretHidden
|
|
359
|
+
accessibilityLabel={accessibilityLabel}
|
|
360
|
+
accessibilityState={{ disabled: !!disabled }}
|
|
361
|
+
aria-invalid={invalid}
|
|
362
|
+
onFocus={(e) => {
|
|
363
|
+
setFocused(true);
|
|
364
|
+
onFocus?.(e);
|
|
365
|
+
}}
|
|
366
|
+
onBlur={(e) => {
|
|
367
|
+
setFocused(false);
|
|
368
|
+
onBlur?.(e);
|
|
369
|
+
}}
|
|
370
|
+
style={[
|
|
371
|
+
{
|
|
372
|
+
position: 'absolute',
|
|
373
|
+
top: 0,
|
|
374
|
+
left: 0,
|
|
375
|
+
right: 0,
|
|
376
|
+
bottom: 0,
|
|
377
|
+
opacity: 0,
|
|
378
|
+
// Taps fall through to the Pressable, which focuses this input —
|
|
379
|
+
// so the caret lands wherever the row is touched.
|
|
380
|
+
pointerEvents: 'none',
|
|
381
|
+
},
|
|
382
|
+
style,
|
|
383
|
+
]}
|
|
384
|
+
{...props}
|
|
385
|
+
/>
|
|
386
|
+
</Pressable>
|
|
387
|
+
|
|
388
|
+
{errorMessage ? (
|
|
389
|
+
<Text className={slots.error()}>{errorMessage}</Text>
|
|
390
|
+
) : null}
|
|
391
|
+
</View>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
OtpInput.displayName = 'OtpInput';
|