@radix-ui/react-one-time-password-field 0.1.0-rc.1744661316162 → 0.1.0-rc.1744830756566
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/dist/index.d.mts +111 -26
- package/dist/index.d.ts +111 -26
- package/dist/index.js +70 -66
- package/dist/index.js.map +3 -3
- package/dist/index.mjs +70 -66
- package/dist/index.mjs.map +3 -3
- package/package.json +8 -8
- package/src/one-time-password-field.tsx +278 -161
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
59
|
+
preHydrationIndexTracker: React.RefObject<number>;
|
|
60
|
+
readOnly: boolean;
|
|
81
61
|
type: InputType;
|
|
82
62
|
userActionRef: React.RefObject<KeyboardActionDetails | null>;
|
|
83
|
-
|
|
84
|
-
|
|
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] =
|
|
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
|
-
|
|
79
|
+
/* -------------------------------------------------------------------------------------------------
|
|
80
|
+
* OneTimePasswordField
|
|
81
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
103
82
|
|
|
104
83
|
interface OneTimePasswordFieldOwnProps {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
|
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
|
|
226
|
+
const collectionState = useInitCollection();
|
|
227
|
+
const [collection] = collectionState;
|
|
178
228
|
|
|
179
|
-
const validation =
|
|
180
|
-
validationType
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
186
|
-
}, [valueProp,
|
|
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
|
|
192
|
-
onChange:
|
|
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
|
|
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
|
|
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
|
-
<
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
543
|
+
/* -------------------------------------------------------------------------------------------------
|
|
544
|
+
* OneTimePasswordFieldInput
|
|
545
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
479
546
|
|
|
480
547
|
interface OneTimePasswordFieldInputProps
|
|
481
|
-
extends
|
|
482
|
-
|
|
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
|
-
{
|
|
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
|
|
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
|
|
797
|
-
|
|
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
|
+
>;
|