digitojs 1.2.2 → 1.2.5

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.
@@ -1,648 +0,0 @@
1
- /**
2
- * digito/react
3
- * ─────────────────────────────────────────────────────────────────────────────
4
- * React adapter — useOTP hook + HiddenOTPInput component (single hidden-input architecture)
5
- *
6
- * @author Olawale Balo — Product Designer + Design Engineer
7
- * @license MIT
8
- */
9
-
10
- import {
11
- useState,
12
- useEffect,
13
- useRef,
14
- useCallback,
15
- forwardRef,
16
- type RefObject,
17
- type KeyboardEvent,
18
- type ChangeEvent,
19
- type ClipboardEvent,
20
- type CSSProperties,
21
- } from 'react'
22
-
23
- import {
24
- createDigito,
25
- createTimer,
26
- filterString,
27
- type DigitoOptions,
28
- type DigitoState,
29
- type InputType,
30
- } from '../core/index.js'
31
-
32
-
33
- // ─────────────────────────────────────────────────────────────────────────────
34
- // TYPES
35
- // ─────────────────────────────────────────────────────────────────────────────
36
-
37
- /**
38
- * Extended options for the React useOTP hook.
39
- * Adds controlled-input, separator, and disabled support on top of DigitoOptions.
40
- *
41
- * Controlled pattern (react-hook-form compatible):
42
- * Pass value + onChange together. onChange fires exactly once per user
43
- * interaction with the current joined code string (partial or complete).
44
- *
45
- * @example — uncontrolled (most common)
46
- * const otp = useOTP({ length: 6, onComplete: (code) => verify(code) })
47
- *
48
- * @example — controlled / react-hook-form
49
- * const { control } = useForm()
50
- * <Controller name="otp" control={control} render={({ field }) => (
51
- * <OTPInput value={field.value} onChange={field.onChange} length={6} />
52
- * )} />
53
- *
54
- * @example — disabled during async verification
55
- * const otp = useOTP({ length: 6, disabled: isVerifying })
56
- */
57
- export type ReactOTPOptions = DigitoOptions & {
58
- /**
59
- * Controlled value — drives the slot state from outside the hook.
60
- * Pass a string of up to length characters to pre-fill or sync the field.
61
- * Compatible with react-hook-form via <Controller>.
62
- */
63
- value?: string
64
- /**
65
- * Uncontrolled initial value. Applied once on mount when `value` is undefined.
66
- * Does not trigger `onComplete` or `onChange`.
67
- */
68
- defaultValue?: string
69
- /** When `true`, mutations are blocked; focus/navigation/copy remain allowed. */
70
- readOnly?: boolean
71
- /**
72
- * Fires exactly ONCE per user interaction with the current joined code string.
73
- * Receives partial values too — not just when the code is complete.
74
- * Use alongside value for a fully controlled pattern.
75
- */
76
- onChange?: (code: string) => void
77
- /**
78
- * Insert a purely visual separator after this slot index (0-based).
79
- * Accepts a single position or an array for multiple separators.
80
- * aria-hidden, never part of the value, no effect on the state machine.
81
- * Default: 0 (no separator).
82
- * @example separatorAfter: 3 -> [*][*][*] — [*][*][*]
83
- * @example separatorAfter: [2, 4] -> [*][*] — [*][*] — [*][*]
84
- */
85
- separatorAfter?: number | number[]
86
- /**
87
- * The character or string to render as the separator.
88
- * Default: '—'
89
- */
90
- separator?: string
91
- /**
92
- * When `true`, each filled slot should display a mask glyph instead of the
93
- * real character. The hidden input switches to `type="password"` for correct
94
- * mobile keyboard and browser autocomplete behavior.
95
- *
96
- * `getCode()` and `onComplete` always return real characters — masking is visual only.
97
- * Use for PIN entry or any sensitive input flow.
98
- *
99
- * Default: `false`.
100
- */
101
- masked?: boolean
102
- /**
103
- * The glyph displayed in filled slots when `masked` is `true`.
104
- * Passed through `SlotRenderProps.maskChar` so headless slot components can
105
- * render the correct character without needing to re-read the option.
106
- *
107
- * Default: `'●'` (U+25CF BLACK CIRCLE).
108
- * @example maskChar: '*'
109
- */
110
- maskChar?: string
111
- }
112
-
113
- /**
114
- * Per-slot render props returned by getSlotProps(index).
115
- * Spread onto a custom slot component for full structural control.
116
- *
117
- * @example
118
- * ```tsx
119
- * function MySlot(props: SlotRenderProps) {
120
- * return (
121
- * <div className={['slot', props.isActive ? 'active' : ''].join(' ')}>
122
- * {props.hasFakeCaret && <span className="caret" />}
123
- * {props.char || <span className="placeholder">·</span>}
124
- * </div>
125
- * )
126
- * }
127
- *
128
- * // In JSX:
129
- * {otp.slotValues.map((_, i) => <MySlot key={i} {...otp.getSlotProps(i)} />)}
130
- * ```
131
- */
132
- export type SlotRenderProps = {
133
- /** The character value of this slot. Empty string when unfilled. */
134
- char: string
135
- /** Zero-based slot index. */
136
- index: number
137
- /** Whether this slot is the active/focused slot. */
138
- isActive: boolean
139
- /** Whether this slot contains a character. */
140
- isFilled: boolean
141
- /** Whether the field is in error state. */
142
- isError: boolean
143
- /** Whether all slots are filled. */
144
- isComplete: boolean
145
- /** Whether the field is disabled. */
146
- isDisabled: boolean
147
- /** Whether the hidden input currently has browser focus. */
148
- isFocused: boolean
149
- /**
150
- * True when this slot is active, empty, and the hidden input has focus —
151
- * i.e. the fake blinking caret should be rendered in this slot.
152
- * Equivalent to: isActive && !isFilled && isFocused
153
- */
154
- hasFakeCaret: boolean
155
- /**
156
- * True when the `masked` option is enabled.
157
- * When true, render `maskChar` instead of `char` for filled slots.
158
- * `char` always holds the real character regardless of this flag.
159
- */
160
- masked: boolean
161
- /**
162
- * The configured mask glyph (from the `maskChar` option).
163
- * Render this instead of `char` when `masked && isFilled`.
164
- * @example {props.masked && props.isFilled ? props.maskChar : props.char}
165
- */
166
- maskChar: string
167
- /**
168
- * The placeholder character for empty slots (from the `placeholder` option).
169
- * Render this when `!isFilled` to show a hint glyph such as `'○'` or `'_'`.
170
- * Empty string when the option is not set.
171
- */
172
- placeholder: string
173
- }
174
-
175
- /** Props to spread onto the single hidden input element. */
176
- export type HiddenInputProps = {
177
- ref: RefObject<HTMLInputElement>
178
- type: 'text' | 'password'
179
- inputMode: 'numeric' | 'text'
180
- autoComplete: 'one-time-code'
181
- maxLength: number
182
- disabled: boolean
183
- /** The `name` attribute for native form submission / FormData. */
184
- name?: string
185
- /** Whether the input auto-focuses on mount. */
186
- autoFocus?: boolean
187
- 'aria-label': string
188
- spellCheck: false
189
- autoCorrect: 'off'
190
- autoCapitalize: 'off'
191
- onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void
192
- onChange: (e: ChangeEvent<HTMLInputElement>) => void
193
- onPaste: (e: ClipboardEvent<HTMLInputElement>) => void
194
- onFocus: () => void
195
- onBlur: () => void
196
- }
197
-
198
- export type UseOTPResult = {
199
- /** Current value of each slot. Empty string = unfilled. */
200
- slotValues: string[]
201
- /** Index of the currently active slot. */
202
- activeSlot: number
203
- /** True when every slot is filled. */
204
- isComplete: boolean
205
- /** True when error state is active. */
206
- hasError: boolean
207
- /** True when the field is disabled. Mirrors the disabled option. */
208
- isDisabled: boolean
209
- /** Remaining timer seconds. 0 when expired or no timer configured. */
210
- timerSeconds: number
211
- /** True while the hidden input has browser focus. */
212
- isFocused: boolean
213
- /** Returns the current joined code string. */
214
- getCode: () => string
215
- /** Clear all slots, restart timer, return focus to input. */
216
- reset: () => void
217
- /** Apply or clear the error state. */
218
- setError: (isError: boolean) => void
219
- /** Programmatically move focus to a specific slot index. */
220
- focus: (slotIndex: number) => void
221
- /** Spread onto the wrapper element to expose state as data attributes for CSS targeting. */
222
- wrapperProps: Record<string, string | undefined>
223
- /**
224
- * The separator slot index/indices for JSX rendering.
225
- * Insert a visual divider AFTER each position. `0` / empty array = no separator.
226
- */
227
- separatorAfter: number | number[]
228
- /** The separator character/string to render. */
229
- separator: string
230
- /** Spread onto the single hidden input element. */
231
- hiddenInputProps: HiddenInputProps
232
- /**
233
- * Returns render props for a single slot — spread onto a custom slot component
234
- * for full structural control over the slot markup.
235
- * @example
236
- * ```tsx
237
- * {otp.slotValues.map((_, i) => <MySlot key={i} {...otp.getSlotProps(i)} />)}
238
- * ```
239
- */
240
- getSlotProps: (index: number) => SlotRenderProps
241
- }
242
-
243
-
244
- // ─────────────────────────────────────────────────────────────────────────────
245
- // HOOK
246
- // ─────────────────────────────────────────────────────────────────────────────
247
-
248
- /**
249
- * React hook for OTP input — single hidden-input architecture.
250
- *
251
- * You render the visual slot divs; the hook handles all state, focus, and events.
252
- *
253
- * @example
254
- * ```tsx
255
- * const otp = useOTP({ length: 6, onComplete: (code) => verify(code) })
256
- *
257
- * <div style={{ position: 'relative', display: 'inline-flex', gap: 8 }}>
258
- * <HiddenOTPInput {...otp.hiddenInputProps} />
259
- * {otp.slotValues.map((_, i) => (
260
- * <MySlot key={i} {...otp.getSlotProps(i)} />
261
- * ))}
262
- * </div>
263
- * ```
264
- */
265
- export function useOTP(options: ReactOTPOptions = {}): UseOTPResult {
266
- const {
267
- length = 6,
268
- type = 'numeric' as InputType,
269
- timer: timerSecs = 0,
270
- disabled = false,
271
- onComplete,
272
- onExpire,
273
- onResend,
274
- haptic = true,
275
- sound = false,
276
- pattern,
277
- pasteTransformer,
278
- onInvalidChar,
279
- value: controlledValue,
280
- defaultValue,
281
- readOnly: readOnlyProp = false,
282
- onChange: onChangeProp,
283
- onFocus: onFocusProp,
284
- onBlur: onBlurProp,
285
- separatorAfter = 0,
286
- separator = '—',
287
- masked = false,
288
- maskChar = '\u25CF',
289
- autoFocus = true,
290
- name: inputName,
291
- placeholder = '',
292
- selectOnFocus = false,
293
- blurOnComplete = false,
294
- } = options
295
-
296
- // ── Stable callback refs ───────────────────────────────────────────────────
297
- const onCompleteRef = useRef(onComplete)
298
- const onExpireRef = useRef(onExpire)
299
- const onResendRef = useRef(onResend)
300
- const onChangeRef = useRef(onChangeProp)
301
- const onFocusRef = useRef(onFocusProp)
302
- const onBlurRef = useRef(onBlurProp)
303
- const onInvalidCharRef = useRef(onInvalidChar)
304
- // Keep pattern and pasteTransformer in refs so callbacks always use the latest
305
- // value without needing to be recreated on every render.
306
- const patternRef = useRef(pattern)
307
- const pasteTransformerRef = useRef(pasteTransformer)
308
- useEffect(() => { onCompleteRef.current = onComplete }, [onComplete])
309
- useEffect(() => { onExpireRef.current = onExpire }, [onExpire])
310
- useEffect(() => { onResendRef.current = onResend }, [onResend])
311
- useEffect(() => { onChangeRef.current = onChangeProp }, [onChangeProp])
312
- useEffect(() => { onFocusRef.current = onFocusProp }, [onFocusProp])
313
- useEffect(() => { onBlurRef.current = onBlurProp }, [onBlurProp])
314
- useEffect(() => { onInvalidCharRef.current = onInvalidChar }, [onInvalidChar])
315
- useEffect(() => { patternRef.current = pattern }, [pattern])
316
- useEffect(() => { pasteTransformerRef.current = pasteTransformer }, [pasteTransformer])
317
-
318
- // ── Core instance ──────────────────────────────────────────────────────────
319
- const digitoRef = useRef(
320
- createDigito({
321
- length, type, haptic, sound, pattern, pasteTransformer,
322
- readOnly: readOnlyProp,
323
- onComplete: (code) => onCompleteRef.current?.(code),
324
- onExpire: () => onExpireRef.current?.(),
325
- onResend: () => onResendRef.current?.(),
326
- onInvalidChar: (char, index) => onInvalidCharRef.current?.(char, index),
327
- })
328
- )
329
- const digito = digitoRef.current
330
-
331
- // ── Disabled / readOnly refs ────────────────────────────────────────────────
332
- // Stored in refs so memoized callbacks (useCallback with [] deps) always read
333
- // the latest value without needing to be recreated on every render.
334
- const disabledRef = useRef(disabled)
335
- const readOnlyRef = useRef(readOnlyProp)
336
- useEffect(() => { disabledRef.current = disabled }, [disabled])
337
- useEffect(() => { readOnlyRef.current = readOnlyProp }, [readOnlyProp])
338
-
339
- // ── State ──────────────────────────────────────────────────────────────────
340
- const [state, setState] = useState<DigitoState>(digito.state)
341
- const [timerSeconds, setTimer] = useState(timerSecs)
342
- const [timerTrigger, setTimerTrigger] = useState(0)
343
- const [isFocused, setIsFocused] = useState(false)
344
- const inputRef = useRef<HTMLInputElement>(null)
345
-
346
- // ── sync() ─────────────────────────────────────────────────────────────────
347
- function sync(suppressOnChange = false): void {
348
- const next = { ...digito.state }
349
- setState(next)
350
- if (!suppressOnChange) {
351
- onChangeRef.current?.(next.slotValues.join(''))
352
- }
353
- }
354
-
355
- // ── Controlled value sync ──────────────────────────────────────────────────
356
- useEffect(() => {
357
- if (controlledValue === undefined) return
358
-
359
- const incoming = filterString(controlledValue.slice(0, length), type, pattern)
360
- const current = digito.state.slotValues.join('')
361
-
362
- if (incoming === current) return
363
-
364
- digito.resetState()
365
- for (let i = 0; i < incoming.length; i++) {
366
- digito.inputChar(i, incoming[i])
367
- }
368
-
369
- digito.cancelPendingComplete()
370
-
371
- setState({ ...digito.state })
372
-
373
- if (inputRef.current) {
374
- inputRef.current.value = incoming
375
- inputRef.current.setSelectionRange(incoming.length, incoming.length)
376
- }
377
-
378
- onChangeRef.current?.(incoming)
379
-
380
- // eslint-disable-next-line react-hooks/exhaustive-deps
381
- }, [controlledValue, length])
382
-
383
- // ── defaultValue — applied once on mount when no controlled value is present ─
384
- useEffect(() => {
385
- if (controlledValue !== undefined || !defaultValue) return
386
- const filtered = filterString(defaultValue.slice(0, length), type, pattern)
387
- if (!filtered) return
388
- digito.resetState()
389
- for (let i = 0; i < filtered.length; i++) digito.inputChar(i, filtered[i])
390
- digito.cancelPendingComplete()
391
- setState({ ...digito.state })
392
- if (inputRef.current) { inputRef.current.value = filtered; inputRef.current.setSelectionRange(filtered.length, filtered.length) }
393
- // eslint-disable-next-line react-hooks/exhaustive-deps
394
- }, [])
395
-
396
- // ── Timer ──────────────────────────────────────────────────────────────────
397
- useEffect(() => {
398
- if (!timerSecs) return
399
- setTimer(timerSecs)
400
- const t = createTimer({
401
- totalSeconds: timerSecs,
402
- onTick: (r) => setTimer(r),
403
- onExpire: () => { setTimer(0); onExpireRef.current?.() },
404
- })
405
- t.start()
406
- return () => t.stop()
407
- }, [timerSecs, timerTrigger])
408
-
409
- // ── Event handlers ─────────────────────────────────────────────────────────
410
-
411
- // eslint-disable-next-line react-hooks/exhaustive-deps
412
- const onKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
413
- if (disabledRef.current) return
414
- const pos = inputRef.current?.selectionStart ?? 0
415
- if (e.key === 'Backspace') {
416
- e.preventDefault()
417
- if (readOnlyRef.current) return
418
- digito.deleteChar(pos)
419
- sync()
420
- const next = digito.state.activeSlot
421
- requestAnimationFrame(() => inputRef.current?.setSelectionRange(next, next))
422
- } else if (e.key === 'Delete') {
423
- e.preventDefault()
424
- if (readOnlyRef.current) return
425
- digito.clearSlot(pos)
426
- sync()
427
- requestAnimationFrame(() => inputRef.current?.setSelectionRange(pos, pos))
428
- } else if (e.key === 'ArrowLeft') {
429
- e.preventDefault()
430
- digito.moveFocusLeft(pos)
431
- sync()
432
- const next = digito.state.activeSlot
433
- requestAnimationFrame(() => inputRef.current?.setSelectionRange(next, next))
434
- } else if (e.key === 'ArrowRight') {
435
- e.preventDefault()
436
- digito.moveFocusRight(pos)
437
- sync()
438
- const next = digito.state.activeSlot
439
- requestAnimationFrame(() => inputRef.current?.setSelectionRange(next, next))
440
- } else if (e.key === 'Tab') {
441
- if (e.shiftKey) {
442
- if (pos === 0) return
443
- e.preventDefault()
444
- digito.moveFocusLeft(pos)
445
- } else {
446
- if (!digito.state.slotValues[pos]) return
447
- if (pos >= length - 1) return
448
- e.preventDefault()
449
- digito.moveFocusRight(pos)
450
- }
451
- sync()
452
- const next = digito.state.activeSlot
453
- requestAnimationFrame(() => inputRef.current?.setSelectionRange(next, next))
454
- }
455
- }, [])
456
-
457
- const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
458
- if (disabledRef.current || readOnlyRef.current) return
459
- const raw = e.target.value
460
- if (!raw) {
461
- digito.resetState()
462
- if (inputRef.current) { inputRef.current.value = ''; inputRef.current.setSelectionRange(0, 0) }
463
- sync()
464
- return
465
- }
466
- const valid = filterString(raw, type, patternRef.current).slice(0, length)
467
- digito.resetState()
468
- for (let i = 0; i < valid.length; i++) digito.inputChar(i, valid[i])
469
- const next = Math.min(valid.length, length - 1)
470
- if (inputRef.current) { inputRef.current.value = valid; inputRef.current.setSelectionRange(next, next) }
471
- digito.moveFocusTo(next)
472
- sync()
473
- if (blurOnComplete && digito.state.isComplete) {
474
- requestAnimationFrame(() => inputRef.current?.blur())
475
- }
476
- }, [type, length, blurOnComplete])
477
-
478
- const onPaste = useCallback((e: ClipboardEvent<HTMLInputElement>) => {
479
- if (disabledRef.current || readOnlyRef.current) return
480
- e.preventDefault()
481
- const text = e.clipboardData.getData('text')
482
- const pos = inputRef.current?.selectionStart ?? 0
483
- digito.pasteString(pos, text)
484
- const { slotValues, activeSlot } = digito.state
485
- if (inputRef.current) { inputRef.current.value = slotValues.join(''); inputRef.current.setSelectionRange(activeSlot, activeSlot) }
486
- sync()
487
- if (blurOnComplete && digito.state.isComplete) {
488
- requestAnimationFrame(() => inputRef.current?.blur())
489
- }
490
- }, [blurOnComplete])
491
-
492
- const onFocus = useCallback(() => {
493
- setIsFocused(true)
494
- onFocusRef.current?.()
495
- const pos = digito.state.activeSlot
496
- requestAnimationFrame(() => {
497
- const char = digito.state.slotValues[pos]
498
- if (selectOnFocus && char) {
499
- inputRef.current?.setSelectionRange(pos, pos + 1)
500
- } else {
501
- inputRef.current?.setSelectionRange(pos, pos)
502
- }
503
- })
504
- }, [selectOnFocus])
505
-
506
- const onBlur = useCallback(() => {
507
- setIsFocused(false)
508
- onBlurRef.current?.()
509
- }, [])
510
-
511
- // ── Public API ─────────────────────────────────────────────────────────────
512
-
513
- const reset = useCallback(() => {
514
- digito.resetState()
515
- if (inputRef.current) { inputRef.current.value = ''; inputRef.current.focus(); inputRef.current.setSelectionRange(0, 0) }
516
- setTimer(timerSecs)
517
- setTimerTrigger((n: number) => n + 1)
518
- sync(true)
519
- }, [timerSecs])
520
-
521
- const setError = useCallback((isError: boolean) => { digito.setError(isError); sync() }, [])
522
-
523
- const focus = useCallback((slotIndex: number) => {
524
- digito.moveFocusTo(slotIndex)
525
- inputRef.current?.focus()
526
- requestAnimationFrame(() => inputRef.current?.setSelectionRange(slotIndex, slotIndex))
527
- sync()
528
- }, [])
529
-
530
- const getCode = useCallback(() => digito.getCode(), [])
531
-
532
- function getSlotProps(index: number): SlotRenderProps {
533
- const char = state.slotValues[index] ?? ''
534
- const isActive = index === state.activeSlot && isFocused
535
- return {
536
- char,
537
- index,
538
- isActive,
539
- isFilled: char.length === 1,
540
- isError: state.hasError,
541
- isComplete: state.isComplete,
542
- isDisabled: disabled,
543
- isFocused,
544
- hasFakeCaret: isActive && char.length === 0,
545
- masked,
546
- maskChar,
547
- placeholder,
548
- }
549
- }
550
-
551
- const hiddenInputProps: HiddenInputProps = {
552
- ref: inputRef,
553
- type: masked ? 'password' : 'text',
554
- inputMode: type === 'numeric' ? 'numeric' : 'text',
555
- autoComplete: 'one-time-code',
556
- maxLength: length,
557
- disabled,
558
- ...(inputName ? { name: inputName } : {}),
559
- ...(autoFocus ? { autoFocus: true } : {}),
560
- 'aria-label': `Enter your ${length}-${type === 'numeric' ? 'digit' : 'character'} code`,
561
- spellCheck: false,
562
- autoCorrect: 'off',
563
- autoCapitalize: 'off',
564
- ...(readOnlyProp ? { 'aria-readonly': 'true' as const } : {}),
565
- onKeyDown,
566
- onChange,
567
- onPaste,
568
- onFocus,
569
- onBlur,
570
- }
571
-
572
- const wrapperProps: Record<string, string | undefined> = {
573
- ...(state.isComplete ? { 'data-complete': '' } : {}),
574
- ...(state.hasError ? { 'data-invalid': '' } : {}),
575
- ...(disabled ? { 'data-disabled': '' } : {}),
576
- ...(readOnlyProp ? { 'data-readonly': '' } : {}),
577
- }
578
-
579
- return {
580
- slotValues: state.slotValues,
581
- activeSlot: state.activeSlot,
582
- isComplete: state.isComplete,
583
- hasError: state.hasError,
584
- isDisabled: disabled,
585
- timerSeconds,
586
- isFocused,
587
- getCode,
588
- reset,
589
- setError,
590
- focus,
591
- separatorAfter,
592
- separator,
593
- hiddenInputProps,
594
- getSlotProps,
595
- wrapperProps,
596
- }
597
- }
598
-
599
-
600
- // ─────────────────────────────────────────────────────────────────────────────
601
- // HIDDEN OTP INPUT COMPONENT
602
- // ─────────────────────────────────────────────────────────────────────────────
603
-
604
- /**
605
- * Convenience wrapper around the hidden real <input> element.
606
- * Applies the correct absolute-positioning styles so it sits invisibly
607
- * on top of the slot row and captures all keyboard input + native autofill.
608
- *
609
- * Forward the ref from useOTP's hiddenInputProps, then spread the rest:
610
- *
611
- * @example
612
- * ```tsx
613
- * const otp = useOTP({ length: 6 })
614
- *
615
- * <div style={{ position: 'relative', display: 'inline-flex', gap: 8 }}>
616
- * <HiddenOTPInput {...otp.hiddenInputProps} />
617
- * {otp.slotValues.map((_, i) => <Slot key={i} {...otp.getSlotProps(i)} />)}
618
- * </div>
619
- * ```
620
- */
621
- const HIDDEN_INPUT_STYLE: CSSProperties = {
622
- position: 'absolute',
623
- inset: 0,
624
- width: '100%',
625
- height: '100%',
626
- opacity: 0,
627
- border: 'none',
628
- outline: 'none',
629
- background: 'transparent',
630
- color: 'transparent',
631
- caretColor: 'transparent',
632
- zIndex: 1,
633
- cursor: 'text',
634
- fontSize: 1,
635
- }
636
-
637
- export const HiddenOTPInput = forwardRef<
638
- HTMLInputElement,
639
- Omit<HiddenInputProps, 'ref'>
640
- >((props, ref) => (
641
- <input
642
- ref={ref}
643
- style={HIDDEN_INPUT_STYLE}
644
- {...props}
645
- />
646
- ))
647
-
648
- HiddenOTPInput.displayName = 'HiddenOTPInput'