@radix-ui/react-one-time-password-field 0.1.0-rc.1744661316162 → 0.1.0-rc.1744831331200

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,4 +1,4 @@
1
- import { Primitive } from '@radix-ui/react-primitive';
1
+ import * as Primitive from '@radix-ui/react-primitive';
2
2
  import { useComposedRefs } from '@radix-ui/react-compose-refs';
3
3
  import { useControllableState } from '@radix-ui/react-use-controllable-state';
4
4
  import { composeEventHandlers } from '@radix-ui/primitive';
@@ -14,35 +14,7 @@ import { useDirection } from '@radix-ui/react-direction';
14
14
  import { clamp } from '@radix-ui/number';
15
15
  import { useEffectEvent } from '@radix-ui/react-use-effect-event';
16
16
 
17
- type InputType = 'password' | 'text';
18
- type AutoComplete = 'off' | 'one-time-code';
19
-
20
- type KeyboardActionDetails =
21
- | {
22
- type: 'keydown';
23
- key: 'Backspace' | 'Delete' | 'Char';
24
- metaKey: boolean;
25
- ctrlKey: boolean;
26
- }
27
- | { type: 'cut' };
28
-
29
- type UpdateAction =
30
- | {
31
- type: 'SET_CHAR';
32
- char: string;
33
- index: number;
34
- event: React.KeyboardEvent | React.ChangeEvent;
35
- }
36
- | { type: 'CLEAR_CHAR'; index: number; reason: 'Backspace' | 'Delete' | 'Cut' }
37
- | { type: 'CLEAR'; reason: 'Reset' | 'Backspace' | 'Delete' }
38
- | { type: 'PASTE'; value: string };
39
- type Dispatcher = React.Dispatch<UpdateAction>;
40
-
41
17
  type InputValidationType = 'alpha' | 'numeric' | 'alphanumeric' | 'none';
42
- type InputValidation = Record<
43
- Exclude<InputValidationType, 'none'>,
44
- { type: InputValidationType; regexp: RegExp; pattern: string; inputMode: string }
45
- >;
46
18
 
47
19
  const INPUT_VALIDATION_MAP = {
48
20
  numeric: {
@@ -63,33 +35,38 @@ const INPUT_VALIDATION_MAP = {
63
35
  pattern: '[a-zA-Z0-9]{1}',
64
36
  inputMode: 'text',
65
37
  },
38
+ none: null,
66
39
  } satisfies InputValidation;
67
40
 
41
+ /* -------------------------------------------------------------------------------------------------
42
+ * OneTimePasswordFieldProvider
43
+ * -----------------------------------------------------------------------------------------------*/
44
+
45
+ type RovingFocusGroupProps = RovingFocusGroup.RovingFocusGroupProps;
46
+
68
47
  interface OneTimePasswordFieldContextValue {
69
- value: string[];
70
48
  attemptSubmit: () => void;
71
- hiddenInputRef: React.RefObject<HTMLInputElement | null>;
72
- validationType: InputValidationType;
73
- disabled: boolean;
74
- readOnly: boolean;
75
49
  autoComplete: AutoComplete;
76
50
  autoFocus: boolean;
51
+ disabled: boolean;
52
+ dispatch: Dispatcher;
77
53
  form: string | undefined;
54
+ hiddenInputRef: React.RefObject<HTMLInputElement | null>;
55
+ isHydrated: boolean;
78
56
  name: string | undefined;
57
+ orientation: Exclude<RovingFocusGroupProps['orientation'], undefined>;
79
58
  placeholder: string | undefined;
80
- required: boolean;
59
+ preHydrationIndexTracker: React.RefObject<number>;
60
+ readOnly: boolean;
81
61
  type: InputType;
82
62
  userActionRef: React.RefObject<KeyboardActionDetails | null>;
83
- dispatch: Dispatcher;
84
- orientation: Exclude<RovingFocusGroupProps['orientation'], undefined>;
85
- preHydrationIndexTracker: React.RefObject<number>;
86
- isHydrated: boolean;
63
+ validationType: InputValidationType;
64
+ value: string[];
87
65
  }
88
66
 
89
67
  const ONE_TIME_PASSWORD_FIELD_NAME = 'OneTimePasswordField';
90
- const [Collection, useCollection, createCollectionScope] = createCollection<HTMLInputElement>(
91
- ONE_TIME_PASSWORD_FIELD_NAME
92
- );
68
+ const [Collection, { useCollection, createCollectionScope, useInitCollection }] =
69
+ createCollection<HTMLInputElement>(ONE_TIME_PASSWORD_FIELD_NAME);
93
70
  const [createOneTimePasswordFieldContext] = createContextScope(ONE_TIME_PASSWORD_FIELD_NAME, [
94
71
  createCollectionScope,
95
72
  createRovingFocusGroupScope,
@@ -99,59 +76,131 @@ const useRovingFocusGroupScope = createRovingFocusGroupScope();
99
76
  const [OneTimePasswordFieldContext, useOneTimePasswordFieldContext] =
100
77
  createOneTimePasswordFieldContext<OneTimePasswordFieldContextValue>(ONE_TIME_PASSWORD_FIELD_NAME);
101
78
 
102
- type RovingFocusGroupProps = React.ComponentPropsWithoutRef<typeof RovingFocusGroup.Root>;
79
+ /* -------------------------------------------------------------------------------------------------
80
+ * OneTimePasswordField
81
+ * -----------------------------------------------------------------------------------------------*/
103
82
 
104
83
  interface OneTimePasswordFieldOwnProps {
105
- onValueChange?: (value: string) => void;
106
- id?: string;
107
- value?: string;
108
- defaultValue?: string;
109
- autoSubmit?: boolean;
110
- onAutoSubmit?: (value: string) => void;
111
- validationType?: InputValidationType;
112
- disabled?: boolean;
113
- readOnly?: boolean;
84
+ /**
85
+ * Specifies what—if any—permission the user agent has to provide automated
86
+ * assistance in filling out form field values, as well as guidance to the
87
+ * browser as to the type of information expected in the field. Allows
88
+ * `"one-time-code"` or `"off"`.
89
+ *
90
+ * @defaultValue `"one-time-code"`
91
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete
92
+ */
114
93
  autoComplete?: AutoComplete;
94
+ /**
95
+ * Whether or not the first fillable input should be focused on page-load.
96
+ *
97
+ * @defaultValue `false`
98
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/autofocus
99
+ */
115
100
  autoFocus?: boolean;
101
+ /**
102
+ * Whether or not the component should attempt to automatically submit when
103
+ * all fields are filled. If the field is associated with an HTML `form`
104
+ * element, the form's `requestSubmit` method will be called.
105
+ *
106
+ * @defaultValue `false`
107
+ */
108
+ autoSubmit?: boolean;
109
+ /**
110
+ * The initial value of the uncontrolled field.
111
+ */
112
+ defaultValue?: string;
113
+ /**
114
+ * Indicates the horizontal directionality of the parent element's text.
115
+ *
116
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/dir
117
+ */
118
+ dir?: RovingFocusGroupProps['dir'];
119
+ /**
120
+ * Whether or not the the field's input elements are disabled.
121
+ */
122
+ disabled?: boolean;
123
+ /**
124
+ * A string specifying the `form` element with which the input is associated.
125
+ * This string's value, if present, must match the id of a `form` element in
126
+ * the same document.
127
+ *
128
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form
129
+ */
116
130
  form?: string | undefined;
131
+ /**
132
+ * A string specifying a name for the input control. This name is submitted
133
+ * along with the control's value when the form data is submitted.
134
+ *
135
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#name
136
+ */
117
137
  name?: string | undefined;
138
+ /**
139
+ * When the `autoSubmit` prop is set to `true`, this callback will be fired
140
+ * before attempting to submit the associated form. It will be called whether
141
+ * or not a form is located, or if submission is not allowed.
142
+ */
143
+ onAutoSubmit?: (value: string) => void;
144
+ /**
145
+ * A callback fired when the field's value changes. When the component is
146
+ * controlled, this should update the state passed to the `value` prop.
147
+ */
148
+ onValueChange?: (value: string) => void;
149
+ /**
150
+ * Indicates the vertical directionality of the input elements.
151
+ *
152
+ * @defaultValue `"horizontal"`
153
+ */
154
+ orientation?: RovingFocusGroupProps['orientation'];
155
+ /**
156
+ * Defines the text displayed in a form control when the control has no value.
157
+ *
158
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/placeholder
159
+ */
118
160
  placeholder?: string | undefined;
119
- required?: boolean;
161
+ /**
162
+ * Whether or not the input elements can be updated by the user.
163
+ *
164
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/readonly
165
+ */
166
+ readOnly?: boolean;
167
+ /**
168
+ * Function for custom sanitization when `validationType` is set to `"none"`.
169
+ * This function will be called before updating values in response to user
170
+ * interactions.
171
+ */
172
+ sanitizeValue?: (value: string) => string;
173
+ /**
174
+ * The input type of the field's input elements. Can be `"password"` or `"text"`.
175
+ */
120
176
  type?: InputType;
121
- //
122
- dir?: RovingFocusGroupProps['dir'];
123
- orientation?: RovingFocusGroupProps['orientation'];
177
+ /**
178
+ * Specifies the type of input validation to be used. Can be `"alpha"`,
179
+ * `"numeric"`, `"alphanumeric"` or `"none"`.
180
+ *
181
+ * @defaultValue `"numeric"`
182
+ */
183
+ validationType?: InputValidationType;
184
+ /**
185
+ * The controlled value of the field.
186
+ */
187
+ value?: string;
124
188
  }
125
189
 
126
190
  type ScopedProps<P> = P & { __scopeOneTimePasswordField?: Scope };
127
191
 
128
- const OneTimePasswordFieldCollectionProvider = ({
129
- __scopeOneTimePasswordField,
130
- children,
131
- }: ScopedProps<{ children: React.ReactNode }>) => {
132
- return (
133
- <Collection.Provider scope={__scopeOneTimePasswordField}>
134
- <Collection.Slot scope={__scopeOneTimePasswordField}>{children}</Collection.Slot>
135
- </Collection.Provider>
136
- );
137
- };
138
-
139
192
  interface OneTimePasswordFieldProps
140
193
  extends OneTimePasswordFieldOwnProps,
141
- Omit<
142
- React.ComponentPropsWithoutRef<typeof Primitive.div>,
143
- keyof OneTimePasswordFieldOwnProps
144
- > {}
194
+ Omit<Primitive.PrimitivePropsWithRef<'div'>, keyof OneTimePasswordFieldOwnProps> {}
145
195
 
146
- const OneTimePasswordFieldImpl = React.forwardRef<HTMLDivElement, OneTimePasswordFieldProps>(
196
+ const OneTimePasswordField = React.forwardRef<HTMLDivElement, OneTimePasswordFieldProps>(
147
197
  function OneTimePasswordFieldImpl(
148
198
  {
149
199
  __scopeOneTimePasswordField,
150
- id,
151
200
  defaultValue,
152
201
  value: valueProp,
153
202
  onValueChange,
154
- autoSubmit,
203
+ autoSubmit = false,
155
204
  children,
156
205
  onPaste,
157
206
  onAutoSubmit,
@@ -162,34 +211,58 @@ const OneTimePasswordFieldImpl = React.forwardRef<HTMLDivElement, OneTimePasswor
162
211
  form,
163
212
  name,
164
213
  placeholder,
165
- required = false,
166
214
  type = 'password',
167
215
  // TODO: Change default to vertical when inputs use vertical writing mode
168
216
  orientation = 'horizontal',
169
217
  dir,
170
218
  validationType = 'numeric',
219
+ sanitizeValue: sanitizeValueProp,
171
220
  ...domProps
172
221
  }: ScopedProps<OneTimePasswordFieldProps>,
173
222
  forwardedRef
174
223
  ) {
175
224
  const rovingFocusGroupScope = useRovingFocusGroupScope(__scopeOneTimePasswordField);
176
225
  const direction = useDirection(dir);
177
- const collection = useCollection(__scopeOneTimePasswordField);
226
+ const collectionState = useInitCollection();
227
+ const [collection] = collectionState;
178
228
 
179
- const validation =
180
- validationType in INPUT_VALIDATION_MAP
181
- ? INPUT_VALIDATION_MAP[validationType as keyof InputValidation]
182
- : undefined;
229
+ const validation = INPUT_VALIDATION_MAP[validationType]
230
+ ? INPUT_VALIDATION_MAP[validationType as keyof InputValidation]
231
+ : null;
232
+
233
+ const sanitizeValue = React.useCallback(
234
+ (value: string | string[]) => {
235
+ if (Array.isArray(value)) {
236
+ value = value.map(removeWhitespace).join('');
237
+ } else {
238
+ value = removeWhitespace(value);
239
+ }
240
+
241
+ if (validation) {
242
+ // global regexp is stateful, so we clone it for each call
243
+ const regexp = new RegExp(validation.regexp);
244
+ value = value.replace(regexp, '');
245
+ } else if (sanitizeValueProp) {
246
+ value = sanitizeValueProp(value);
247
+ }
248
+
249
+ return value.split('');
250
+ },
251
+ [validation, sanitizeValueProp]
252
+ );
183
253
 
184
254
  const controlledValue = React.useMemo(() => {
185
- return valueProp != null ? sanitizeValue(valueProp, validation?.regexp) : undefined;
186
- }, [valueProp, validation?.regexp]);
255
+ return valueProp != null ? sanitizeValue(valueProp) : undefined;
256
+ }, [valueProp, sanitizeValue]);
187
257
 
188
258
  const [value, setValue] = useControllableState({
189
259
  caller: 'OneTimePasswordField',
190
260
  prop: controlledValue,
191
- defaultProp: defaultValue != null ? sanitizeValue(defaultValue, validation?.regexp) : [],
192
- onChange: (value) => onValueChange?.(value.filter(Boolean).join('')),
261
+ defaultProp: defaultValue != null ? sanitizeValue(defaultValue) : [],
262
+ onChange: React.useCallback(
263
+ (value: string[]) => onValueChange?.(value.join('')),
264
+ [onValueChange]
265
+ ),
193
266
  });
194
267
 
195
268
  // Update function *specifically* for event handlers.
@@ -229,12 +302,8 @@ const OneTimePasswordFieldImpl = React.forwardRef<HTMLDivElement, OneTimePasswor
229
302
  return;
230
303
  }
231
304
 
232
- const newValue = [
233
- //
234
- ...value.slice(0, index),
235
- char,
236
- ...value.slice(index),
237
- ];
305
+ const newValue = [...value];
306
+ newValue[index] = char;
238
307
 
239
308
  const lastElement = collection.at(-1)?.element;
240
309
  flushSync(() => setValue(newValue));
@@ -282,7 +351,7 @@ const OneTimePasswordFieldImpl = React.forwardRef<HTMLDivElement, OneTimePasswor
282
351
 
283
352
  case 'PASTE': {
284
353
  const { value: pastedValue } = action;
285
- const value = sanitizeValue(pastedValue, validation?.regexp);
354
+ const value = sanitizeValue(pastedValue);
286
355
  if (!value) {
287
356
  return;
288
357
  }
@@ -303,9 +372,9 @@ const OneTimePasswordFieldImpl = React.forwardRef<HTMLDivElement, OneTimePasswor
303
372
 
304
373
  if (validationTypeRef.current?.type !== validation.type) {
305
374
  validationTypeRef.current = validation;
306
- setValue(sanitizeValue(value, validation.regexp));
375
+ setValue(sanitizeValue(value.join('')));
307
376
  }
308
- }, [setValue, validation, value]);
377
+ }, [sanitizeValue, setValue, validation, value]);
309
378
 
310
379
  const hiddenInputRef = React.useRef<HTMLInputElement>(null);
311
380
 
@@ -380,7 +449,6 @@ const OneTimePasswordFieldImpl = React.forwardRef<HTMLDivElement, OneTimePasswor
380
449
  form={form}
381
450
  name={name}
382
451
  placeholder={placeholder}
383
- required={required}
384
452
  type={type}
385
453
  hiddenInputRef={hiddenInputRef}
386
454
  userActionRef={userActionRef}
@@ -390,46 +458,44 @@ const OneTimePasswordFieldImpl = React.forwardRef<HTMLDivElement, OneTimePasswor
390
458
  preHydrationIndexTracker={preHydrationIndexTracker}
391
459
  isHydrated={isHydrated}
392
460
  >
393
- <RovingFocusGroup.Root
394
- asChild
395
- {...rovingFocusGroupScope}
396
- orientation={orientation}
397
- dir={direction}
398
- >
399
- <Primitive.div
400
- {...domProps}
401
- role="group"
402
- ref={composedRefs}
403
- onPaste={composeEventHandlers(
404
- onPaste,
405
- (event: React.ClipboardEvent<HTMLDivElement>) => {
406
- event.preventDefault();
407
- const pastedValue = event.clipboardData.getData('Text');
408
- const value = sanitizeValue(pastedValue, validation?.regexp);
409
- if (!value) {
410
- return;
411
- }
412
- dispatch({ type: 'PASTE', value: pastedValue });
413
- }
414
- )}
415
- >
416
- {children}
417
- </Primitive.div>
418
- </RovingFocusGroup.Root>
461
+ <Collection.Provider scope={__scopeOneTimePasswordField} state={collectionState}>
462
+ <Collection.Slot scope={__scopeOneTimePasswordField}>
463
+ <RovingFocusGroup.Root
464
+ asChild
465
+ {...rovingFocusGroupScope}
466
+ orientation={orientation}
467
+ dir={direction}
468
+ >
469
+ <Primitive.Root.div
470
+ {...domProps}
471
+ role="group"
472
+ ref={composedRefs}
473
+ onPaste={composeEventHandlers(
474
+ onPaste,
475
+ (event: React.ClipboardEvent<HTMLDivElement>) => {
476
+ event.preventDefault();
477
+ const pastedValue = event.clipboardData.getData('Text');
478
+ const value = sanitizeValue(pastedValue);
479
+ if (!value) {
480
+ return;
481
+ }
482
+ dispatch({ type: 'PASTE', value: pastedValue });
483
+ }
484
+ )}
485
+ >
486
+ {children}
487
+ </Primitive.Root.div>
488
+ </RovingFocusGroup.Root>
489
+ </Collection.Slot>
490
+ </Collection.Provider>
419
491
  </OneTimePasswordFieldContext>
420
492
  );
421
493
  }
422
494
  );
423
495
 
424
- const OneTimePasswordField = React.forwardRef<HTMLDivElement, OneTimePasswordFieldProps>(
425
- function OneTimePasswordField(props, ref) {
426
- return (
427
- <OneTimePasswordFieldCollectionProvider>
428
- <OneTimePasswordFieldImpl {...props} ref={ref} />
429
- </OneTimePasswordFieldCollectionProvider>
430
- );
431
- }
432
- );
496
+ /* -------------------------------------------------------------------------------------------------
497
+ * OneTimePasswordFieldHiddenInput
498
+ * -----------------------------------------------------------------------------------------------*/
433
499
 
434
500
  interface OneTimePasswordFieldHiddenInputProps
435
501
  extends Omit<
@@ -451,7 +517,7 @@ const OneTimePasswordFieldHiddenInput = React.forwardRef<
451
517
  { __scopeOneTimePasswordField, ...props }: ScopedProps<OneTimePasswordFieldHiddenInputProps>,
452
518
  forwardedRef
453
519
  ) {
454
- const { value, hiddenInputRef } = useOneTimePasswordFieldContext(
520
+ const { value, hiddenInputRef, name } = useOneTimePasswordFieldContext(
455
521
  'OneTimePasswordFieldHiddenInput',
456
522
  __scopeOneTimePasswordField
457
523
  );
@@ -459,9 +525,7 @@ const OneTimePasswordFieldHiddenInput = React.forwardRef<
459
525
  return (
460
526
  <input
461
527
  ref={ref}
462
- {...props}
463
- type="hidden"
464
- readOnly
528
+ name={name}
465
529
  value={value.join('').trim()}
466
530
  autoComplete="off"
467
531
  autoFocus={false}
@@ -469,23 +533,46 @@ const OneTimePasswordFieldHiddenInput = React.forwardRef<
469
533
  autoCorrect="off"
470
534
  autoSave="off"
471
535
  spellCheck={false}
536
+ {...props}
537
+ type="hidden"
538
+ readOnly
472
539
  />
473
540
  );
474
541
  });
475
542
 
476
- interface OneTimePasswordFieldInputOwnProps {
477
- autoComplete?: 'one-time-code' | 'off';
478
- }
543
+ /* -------------------------------------------------------------------------------------------------
544
+ * OneTimePasswordFieldInput
545
+ * -----------------------------------------------------------------------------------------------*/
479
546
 
480
547
  interface OneTimePasswordFieldInputProps
481
- extends OneTimePasswordFieldInputOwnProps,
482
- Omit<React.ComponentProps<typeof Primitive.input>, keyof OneTimePasswordFieldInputOwnProps> {}
548
+ extends Omit<
549
+ Primitive.PrimitivePropsWithRef<'input'>,
550
+ | 'value'
551
+ | 'defaultValue'
552
+ | 'disabled'
553
+ | 'readOnly'
554
+ | 'autoComplete'
555
+ | 'autoFocus'
556
+ | 'form'
557
+ | 'name'
558
+ | 'placeholder'
559
+ | 'type'
560
+ > {
561
+ /**
562
+ * Callback fired when the user input fails native HTML input validation.
563
+ */
564
+ onInvalidChange?: (character: string) => void;
565
+ }
483
566
 
484
567
  const OneTimePasswordFieldInput = React.forwardRef<
485
568
  HTMLInputElement,
486
569
  OneTimePasswordFieldInputProps
487
570
  >(function OneTimePasswordFieldInput(
488
- { __scopeOneTimePasswordField, ...props }: ScopedProps<OneTimePasswordFieldInputProps>,
571
+ {
572
+ __scopeOneTimePasswordField,
573
+ onInvalidChange,
574
+ ...props
575
+ }: ScopedProps<OneTimePasswordFieldInputProps>,
489
576
  forwardedRef
490
577
  ) {
491
578
  // TODO: warn if these values are passed
@@ -499,10 +586,9 @@ const OneTimePasswordFieldInput = React.forwardRef<
499
586
  form: _form,
500
587
  name: _name,
501
588
  placeholder: _placeholder,
502
- required: _required,
503
589
  type: _type,
504
590
  ...domProps
505
- } = props as any;
591
+ } = props as Primitive.PrimitivePropsWithRef<'input'>;
506
592
 
507
593
  const context = useOneTimePasswordFieldContext(
508
594
  'OneTimePasswordFieldInput',
@@ -515,12 +601,18 @@ const OneTimePasswordFieldInput = React.forwardRef<
515
601
  const inputRef = React.useRef<HTMLInputElement>(null);
516
602
  const [element, setElement] = React.useState<HTMLInputElement | null>(null);
517
603
 
604
+ let placeholder: string | undefined;
518
605
  let index: number;
519
606
  if (!isHydrated) {
520
607
  index = preHydrationIndexTracker.current;
521
608
  preHydrationIndexTracker.current++;
522
609
  } else {
523
610
  index = element ? collection.indexOf(element) : -1;
611
+ if (context.placeholder && context.value.length === 0) {
612
+ // only set placeholder after hydration to prevent flickering when indices
613
+ // are re-calculated
614
+ placeholder = context.placeholder[index];
615
+ }
524
616
  }
525
617
 
526
618
  const composedInputRef = useComposedRefs(forwardedRef, inputRef, setElement);
@@ -550,7 +642,7 @@ const OneTimePasswordFieldInput = React.forwardRef<
550
642
  focusable={!context.disabled && isFocusable}
551
643
  active={index === lastSelectableIndex}
552
644
  >
553
- <Primitive.input
645
+ <Primitive.Root.input
554
646
  ref={composedInputRef}
555
647
  type="text"
556
648
  aria-label={`Character ${index + 1} of ${collection.size}`}
@@ -560,6 +652,7 @@ const OneTimePasswordFieldInput = React.forwardRef<
560
652
  pattern={validation?.pattern}
561
653
  readOnly={context.readOnly}
562
654
  value={char}
655
+ placeholder={placeholder}
563
656
  data-radix-otp-input=""
564
657
  data-radix-index={index}
565
658
  {...domProps}
@@ -605,7 +698,7 @@ const OneTimePasswordFieldInput = React.forwardRef<
605
698
 
606
699
  const isClearing =
607
700
  action.key === 'Backspace' && (action.metaKey || action.ctrlKey);
608
- if (isClearing) {
701
+ if (action.key === 'Clear' || isClearing) {
609
702
  dispatch({ type: 'CLEAR', reason: 'Backspace' });
610
703
  } else {
611
704
  dispatch({ type: 'CLEAR_CHAR', index, reason: action.key });
@@ -635,6 +728,7 @@ const OneTimePasswordFieldInput = React.forwardRef<
635
728
  }
636
729
  } else {
637
730
  const element = event.target;
731
+ onInvalidChange?.(element.value);
638
732
  requestAnimationFrame(() => {
639
733
  if (element.ownerDocument.activeElement === element) {
640
734
  element.select();
@@ -644,6 +738,7 @@ const OneTimePasswordFieldInput = React.forwardRef<
644
738
  })}
645
739
  onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {
646
740
  switch (event.key) {
741
+ case 'Clear':
647
742
  case 'Delete':
648
743
  case 'Backspace': {
649
744
  const currentValue = event.currentTarget.value;
@@ -652,7 +747,7 @@ const OneTimePasswordFieldInput = React.forwardRef<
652
747
  // if the user presses delete when there is no value, noop
653
748
  if (event.key === 'Delete') return;
654
749
 
655
- const isClearing = event.metaKey || event.ctrlKey;
750
+ const isClearing = event.key === 'Clear' || event.metaKey || event.ctrlKey;
656
751
  if (isClearing) {
657
752
  dispatch({ type: 'CLEAR', reason: 'Backspace' });
658
753
  } else {
@@ -769,18 +864,14 @@ const OneTimePasswordFieldInput = React.forwardRef<
769
864
  );
770
865
  });
771
866
 
772
- const Root = OneTimePasswordField;
773
- const Input = OneTimePasswordFieldInput;
774
- const HiddenInput = OneTimePasswordFieldHiddenInput;
775
-
776
867
  export {
777
868
  OneTimePasswordField,
778
869
  OneTimePasswordFieldInput,
779
870
  OneTimePasswordFieldHiddenInput,
780
871
  //
781
- Root,
782
- Input,
783
- HiddenInput,
872
+ OneTimePasswordField as Root,
873
+ OneTimePasswordFieldInput as Input,
874
+ OneTimePasswordFieldHiddenInput as HiddenInput,
784
875
  };
785
876
  export type {
786
877
  OneTimePasswordFieldProps,
@@ -789,20 +880,14 @@ export type {
789
880
  InputValidationType,
790
881
  };
791
882
 
883
+ /* -----------------------------------------------------------------------------------------------*/
884
+
792
885
  function isFormElement(element: Element | null | undefined): element is HTMLFormElement {
793
886
  return element?.tagName === 'FORM';
794
887
  }
795
888
 
796
- function sanitizeValue(value: string | string[], regexp: RegExp | undefined | null) {
797
- if (Array.isArray(value)) {
798
- value = value.join('');
799
- }
800
- if (regexp) {
801
- // global regexp is stateful, so we clone it for each call
802
- regexp = new RegExp(regexp);
803
- return value.replace(regexp, '').split('').filter(Boolean);
804
- }
805
- return value.split('').filter(Boolean);
889
+ function removeWhitespace(value: string) {
890
+ return value.replace(/\s/g, '');
806
891
  }
807
892
 
808
893
  function focusInput(element: HTMLInputElement | null | undefined) {
@@ -821,3 +906,35 @@ function focusInput(element: HTMLInputElement | null | undefined) {
821
906
  function isInputEvent(event: Event): event is InputEvent {
822
907
  return event.type === 'input';
823
908
  }
909
+
910
+ type InputType = 'password' | 'text';
911
+ type AutoComplete = 'off' | 'one-time-code';
912
+ type KeyboardActionDetails =
913
+ | {
914
+ type: 'keydown';
915
+ key: 'Backspace' | 'Delete' | 'Clear' | 'Char';
916
+ metaKey: boolean;
917
+ ctrlKey: boolean;
918
+ }
919
+ | { type: 'cut' };
920
+
921
+ type UpdateAction =
922
+ | {
923
+ type: 'SET_CHAR';
924
+ char: string;
925
+ index: number;
926
+ event: React.KeyboardEvent | React.ChangeEvent;
927
+ }
928
+ | { type: 'CLEAR_CHAR'; index: number; reason: 'Backspace' | 'Delete' | 'Cut' }
929
+ | { type: 'CLEAR'; reason: 'Reset' | 'Backspace' | 'Delete' | 'Clear' }
930
+ | { type: 'PASTE'; value: string };
931
+ type Dispatcher = React.Dispatch<UpdateAction>;
932
+ type InputValidation = Record<
933
+ InputValidationType,
934
+ {
935
+ type: InputValidationType;
936
+ regexp: RegExp;
937
+ pattern: string;
938
+ inputMode: 'text' | 'numeric';
939
+ } | null
940
+ >;