mirror of
https://github.com/hex248/ob248.com.git
synced 2026-02-08 18:53:02 +00:00
merge new into master
This commit is contained in:
21
node_modules/@radix-ui/react-one-time-password-field/LICENSE
generated
vendored
Normal file
21
node_modules/@radix-ui/react-one-time-password-field/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 WorkOS
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
13
node_modules/@radix-ui/react-one-time-password-field/README.md
generated
vendored
Normal file
13
node_modules/@radix-ui/react-one-time-password-field/README.md
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# `react-one-time-password-field`
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
$ yarn add radix-ui
|
||||
# or
|
||||
$ npm install radix-ui
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
View docs [here](https://radix-ui.com/primitives/docs/components/one-time-password-field).
|
||||
136
node_modules/@radix-ui/react-one-time-password-field/dist/index.d.mts
generated
vendored
Normal file
136
node_modules/@radix-ui/react-one-time-password-field/dist/index.d.mts
generated
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
import * as Primitive from '@radix-ui/react-primitive';
|
||||
import * as RovingFocusGroup from '@radix-ui/react-roving-focus';
|
||||
import * as React from 'react';
|
||||
|
||||
type InputValidationType = 'alpha' | 'numeric' | 'alphanumeric' | 'none';
|
||||
type RovingFocusGroupProps = RovingFocusGroup.RovingFocusGroupProps;
|
||||
interface OneTimePasswordFieldOwnProps {
|
||||
/**
|
||||
* Specifies what—if any—permission the user agent has to provide automated
|
||||
* assistance in filling out form field values, as well as guidance to the
|
||||
* browser as to the type of information expected in the field. Allows
|
||||
* `"one-time-code"` or `"off"`.
|
||||
*
|
||||
* @defaultValue `"one-time-code"`
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete
|
||||
*/
|
||||
autoComplete?: AutoComplete;
|
||||
/**
|
||||
* Whether or not the first fillable input should be focused on page-load.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/autofocus
|
||||
*/
|
||||
autoFocus?: boolean;
|
||||
/**
|
||||
* Whether or not the component should attempt to automatically submit when
|
||||
* all fields are filled. If the field is associated with an HTML `form`
|
||||
* element, the form's `requestSubmit` method will be called.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
autoSubmit?: boolean;
|
||||
/**
|
||||
* The initial value of the uncontrolled field.
|
||||
*/
|
||||
defaultValue?: string;
|
||||
/**
|
||||
* Indicates the horizontal directionality of the parent element's text.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/dir
|
||||
*/
|
||||
dir?: RovingFocusGroupProps['dir'];
|
||||
/**
|
||||
* Whether or not the the field's input elements are disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* A string specifying the `form` element with which the input is associated.
|
||||
* This string's value, if present, must match the id of a `form` element in
|
||||
* the same document.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form
|
||||
*/
|
||||
form?: string | undefined;
|
||||
/**
|
||||
* A string specifying a name for the input control. This name is submitted
|
||||
* along with the control's value when the form data is submitted.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#name
|
||||
*/
|
||||
name?: string | undefined;
|
||||
/**
|
||||
* When the `autoSubmit` prop is set to `true`, this callback will be fired
|
||||
* before attempting to submit the associated form. It will be called whether
|
||||
* or not a form is located, or if submission is not allowed.
|
||||
*/
|
||||
onAutoSubmit?: (value: string) => void;
|
||||
/**
|
||||
* A callback fired when the field's value changes. When the component is
|
||||
* controlled, this should update the state passed to the `value` prop.
|
||||
*/
|
||||
onValueChange?: (value: string) => void;
|
||||
/**
|
||||
* Indicates the vertical directionality of the input elements.
|
||||
*
|
||||
* @defaultValue `"horizontal"`
|
||||
*/
|
||||
orientation?: RovingFocusGroupProps['orientation'];
|
||||
/**
|
||||
* Defines the text displayed in a form control when the control has no value.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/placeholder
|
||||
*/
|
||||
placeholder?: string | undefined;
|
||||
/**
|
||||
* Whether or not the input elements can be updated by the user.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/readonly
|
||||
*/
|
||||
readOnly?: boolean;
|
||||
/**
|
||||
* Function for custom sanitization when `validationType` is set to `"none"`.
|
||||
* This function will be called before updating values in response to user
|
||||
* interactions.
|
||||
*/
|
||||
sanitizeValue?: (value: string) => string;
|
||||
/**
|
||||
* The input type of the field's input elements. Can be `"password"` or `"text"`.
|
||||
*/
|
||||
type?: InputType;
|
||||
/**
|
||||
* Specifies the type of input validation to be used. Can be `"alpha"`,
|
||||
* `"numeric"`, `"alphanumeric"` or `"none"`.
|
||||
*
|
||||
* @defaultValue `"numeric"`
|
||||
*/
|
||||
validationType?: InputValidationType;
|
||||
/**
|
||||
* The controlled value of the field.
|
||||
*/
|
||||
value?: string;
|
||||
}
|
||||
interface OneTimePasswordFieldProps extends OneTimePasswordFieldOwnProps, Omit<Primitive.PrimitivePropsWithRef<'div'>, keyof OneTimePasswordFieldOwnProps> {
|
||||
}
|
||||
declare const OneTimePasswordField: React.ForwardRefExoticComponent<Omit<OneTimePasswordFieldProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
||||
interface OneTimePasswordFieldHiddenInputProps extends Omit<React.ComponentProps<'input'>, keyof 'value' | 'defaultValue' | 'type' | 'onChange' | 'readOnly' | 'disabled' | 'autoComplete' | 'autoFocus'> {
|
||||
}
|
||||
declare const OneTimePasswordFieldHiddenInput: React.ForwardRefExoticComponent<Omit<OneTimePasswordFieldHiddenInputProps, "ref"> & React.RefAttributes<HTMLInputElement>>;
|
||||
interface OneTimePasswordFieldInputProps extends Omit<Primitive.PrimitivePropsWithRef<'input'>, 'value' | 'defaultValue' | 'disabled' | 'readOnly' | 'autoComplete' | 'autoFocus' | 'form' | 'name' | 'placeholder' | 'type'> {
|
||||
/**
|
||||
* Callback fired when the user input fails native HTML input validation.
|
||||
*/
|
||||
onInvalidChange?: (character: string) => void;
|
||||
/**
|
||||
* User-provided index to determine the order of the inputs. This is useful if
|
||||
* you need certain index-based attributes to be set on the initial render,
|
||||
* often to prevent flickering after hydration.
|
||||
*/
|
||||
index?: number;
|
||||
}
|
||||
declare const OneTimePasswordFieldInput: React.ForwardRefExoticComponent<Omit<OneTimePasswordFieldInputProps, "ref"> & React.RefAttributes<HTMLInputElement>>;
|
||||
|
||||
type InputType = 'password' | 'text';
|
||||
type AutoComplete = 'off' | 'one-time-code';
|
||||
|
||||
export { OneTimePasswordFieldHiddenInput as HiddenInput, OneTimePasswordFieldInput as Input, type InputValidationType, OneTimePasswordField, OneTimePasswordFieldHiddenInput, type OneTimePasswordFieldHiddenInputProps, OneTimePasswordFieldInput, type OneTimePasswordFieldInputProps, type OneTimePasswordFieldProps, OneTimePasswordField as Root };
|
||||
136
node_modules/@radix-ui/react-one-time-password-field/dist/index.d.ts
generated
vendored
Normal file
136
node_modules/@radix-ui/react-one-time-password-field/dist/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
import * as Primitive from '@radix-ui/react-primitive';
|
||||
import * as RovingFocusGroup from '@radix-ui/react-roving-focus';
|
||||
import * as React from 'react';
|
||||
|
||||
type InputValidationType = 'alpha' | 'numeric' | 'alphanumeric' | 'none';
|
||||
type RovingFocusGroupProps = RovingFocusGroup.RovingFocusGroupProps;
|
||||
interface OneTimePasswordFieldOwnProps {
|
||||
/**
|
||||
* Specifies what—if any—permission the user agent has to provide automated
|
||||
* assistance in filling out form field values, as well as guidance to the
|
||||
* browser as to the type of information expected in the field. Allows
|
||||
* `"one-time-code"` or `"off"`.
|
||||
*
|
||||
* @defaultValue `"one-time-code"`
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete
|
||||
*/
|
||||
autoComplete?: AutoComplete;
|
||||
/**
|
||||
* Whether or not the first fillable input should be focused on page-load.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/autofocus
|
||||
*/
|
||||
autoFocus?: boolean;
|
||||
/**
|
||||
* Whether or not the component should attempt to automatically submit when
|
||||
* all fields are filled. If the field is associated with an HTML `form`
|
||||
* element, the form's `requestSubmit` method will be called.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
autoSubmit?: boolean;
|
||||
/**
|
||||
* The initial value of the uncontrolled field.
|
||||
*/
|
||||
defaultValue?: string;
|
||||
/**
|
||||
* Indicates the horizontal directionality of the parent element's text.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/dir
|
||||
*/
|
||||
dir?: RovingFocusGroupProps['dir'];
|
||||
/**
|
||||
* Whether or not the the field's input elements are disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* A string specifying the `form` element with which the input is associated.
|
||||
* This string's value, if present, must match the id of a `form` element in
|
||||
* the same document.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form
|
||||
*/
|
||||
form?: string | undefined;
|
||||
/**
|
||||
* A string specifying a name for the input control. This name is submitted
|
||||
* along with the control's value when the form data is submitted.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#name
|
||||
*/
|
||||
name?: string | undefined;
|
||||
/**
|
||||
* When the `autoSubmit` prop is set to `true`, this callback will be fired
|
||||
* before attempting to submit the associated form. It will be called whether
|
||||
* or not a form is located, or if submission is not allowed.
|
||||
*/
|
||||
onAutoSubmit?: (value: string) => void;
|
||||
/**
|
||||
* A callback fired when the field's value changes. When the component is
|
||||
* controlled, this should update the state passed to the `value` prop.
|
||||
*/
|
||||
onValueChange?: (value: string) => void;
|
||||
/**
|
||||
* Indicates the vertical directionality of the input elements.
|
||||
*
|
||||
* @defaultValue `"horizontal"`
|
||||
*/
|
||||
orientation?: RovingFocusGroupProps['orientation'];
|
||||
/**
|
||||
* Defines the text displayed in a form control when the control has no value.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/placeholder
|
||||
*/
|
||||
placeholder?: string | undefined;
|
||||
/**
|
||||
* Whether or not the input elements can be updated by the user.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/readonly
|
||||
*/
|
||||
readOnly?: boolean;
|
||||
/**
|
||||
* Function for custom sanitization when `validationType` is set to `"none"`.
|
||||
* This function will be called before updating values in response to user
|
||||
* interactions.
|
||||
*/
|
||||
sanitizeValue?: (value: string) => string;
|
||||
/**
|
||||
* The input type of the field's input elements. Can be `"password"` or `"text"`.
|
||||
*/
|
||||
type?: InputType;
|
||||
/**
|
||||
* Specifies the type of input validation to be used. Can be `"alpha"`,
|
||||
* `"numeric"`, `"alphanumeric"` or `"none"`.
|
||||
*
|
||||
* @defaultValue `"numeric"`
|
||||
*/
|
||||
validationType?: InputValidationType;
|
||||
/**
|
||||
* The controlled value of the field.
|
||||
*/
|
||||
value?: string;
|
||||
}
|
||||
interface OneTimePasswordFieldProps extends OneTimePasswordFieldOwnProps, Omit<Primitive.PrimitivePropsWithRef<'div'>, keyof OneTimePasswordFieldOwnProps> {
|
||||
}
|
||||
declare const OneTimePasswordField: React.ForwardRefExoticComponent<Omit<OneTimePasswordFieldProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
||||
interface OneTimePasswordFieldHiddenInputProps extends Omit<React.ComponentProps<'input'>, keyof 'value' | 'defaultValue' | 'type' | 'onChange' | 'readOnly' | 'disabled' | 'autoComplete' | 'autoFocus'> {
|
||||
}
|
||||
declare const OneTimePasswordFieldHiddenInput: React.ForwardRefExoticComponent<Omit<OneTimePasswordFieldHiddenInputProps, "ref"> & React.RefAttributes<HTMLInputElement>>;
|
||||
interface OneTimePasswordFieldInputProps extends Omit<Primitive.PrimitivePropsWithRef<'input'>, 'value' | 'defaultValue' | 'disabled' | 'readOnly' | 'autoComplete' | 'autoFocus' | 'form' | 'name' | 'placeholder' | 'type'> {
|
||||
/**
|
||||
* Callback fired when the user input fails native HTML input validation.
|
||||
*/
|
||||
onInvalidChange?: (character: string) => void;
|
||||
/**
|
||||
* User-provided index to determine the order of the inputs. This is useful if
|
||||
* you need certain index-based attributes to be set on the initial render,
|
||||
* often to prevent flickering after hydration.
|
||||
*/
|
||||
index?: number;
|
||||
}
|
||||
declare const OneTimePasswordFieldInput: React.ForwardRefExoticComponent<Omit<OneTimePasswordFieldInputProps, "ref"> & React.RefAttributes<HTMLInputElement>>;
|
||||
|
||||
type InputType = 'password' | 'text';
|
||||
type AutoComplete = 'off' | 'one-time-code';
|
||||
|
||||
export { OneTimePasswordFieldHiddenInput as HiddenInput, OneTimePasswordFieldInput as Input, type InputValidationType, OneTimePasswordField, OneTimePasswordFieldHiddenInput, type OneTimePasswordFieldHiddenInputProps, OneTimePasswordFieldInput, type OneTimePasswordFieldInputProps, type OneTimePasswordFieldProps, OneTimePasswordField as Root };
|
||||
623
node_modules/@radix-ui/react-one-time-password-field/dist/index.js
generated
vendored
Normal file
623
node_modules/@radix-ui/react-one-time-password-field/dist/index.js
generated
vendored
Normal file
@@ -0,0 +1,623 @@
|
||||
"use strict";
|
||||
"use client";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/index.ts
|
||||
var index_exports = {};
|
||||
__export(index_exports, {
|
||||
HiddenInput: () => OneTimePasswordFieldHiddenInput,
|
||||
Input: () => OneTimePasswordFieldInput,
|
||||
OneTimePasswordField: () => OneTimePasswordField,
|
||||
OneTimePasswordFieldHiddenInput: () => OneTimePasswordFieldHiddenInput,
|
||||
OneTimePasswordFieldInput: () => OneTimePasswordFieldInput,
|
||||
Root: () => OneTimePasswordField
|
||||
});
|
||||
module.exports = __toCommonJS(index_exports);
|
||||
|
||||
// src/one-time-password-field.tsx
|
||||
var Primitive = __toESM(require("@radix-ui/react-primitive"));
|
||||
var import_react_compose_refs = require("@radix-ui/react-compose-refs");
|
||||
var import_react_use_controllable_state = require("@radix-ui/react-use-controllable-state");
|
||||
var import_primitive = require("@radix-ui/primitive");
|
||||
var import_react_collection = require("@radix-ui/react-collection");
|
||||
var RovingFocusGroup = __toESM(require("@radix-ui/react-roving-focus"));
|
||||
var import_react_roving_focus = require("@radix-ui/react-roving-focus");
|
||||
var import_react_use_is_hydrated = require("@radix-ui/react-use-is-hydrated");
|
||||
var React = __toESM(require("react"));
|
||||
var import_react_dom = require("react-dom");
|
||||
var import_react_context = require("@radix-ui/react-context");
|
||||
var import_react_direction = require("@radix-ui/react-direction");
|
||||
var import_number = require("@radix-ui/number");
|
||||
var import_react_use_effect_event = require("@radix-ui/react-use-effect-event");
|
||||
var import_jsx_runtime = require("react/jsx-runtime");
|
||||
var INPUT_VALIDATION_MAP = {
|
||||
numeric: {
|
||||
type: "numeric",
|
||||
regexp: /[^\d]/g,
|
||||
pattern: "\\d{1}",
|
||||
inputMode: "numeric"
|
||||
},
|
||||
alpha: {
|
||||
type: "alpha",
|
||||
regexp: /[^a-zA-Z]/g,
|
||||
pattern: "[a-zA-Z]{1}",
|
||||
inputMode: "text"
|
||||
},
|
||||
alphanumeric: {
|
||||
type: "alphanumeric",
|
||||
regexp: /[^a-zA-Z0-9]/g,
|
||||
pattern: "[a-zA-Z0-9]{1}",
|
||||
inputMode: "text"
|
||||
},
|
||||
none: null
|
||||
};
|
||||
var ONE_TIME_PASSWORD_FIELD_NAME = "OneTimePasswordField";
|
||||
var [Collection, { useCollection, createCollectionScope, useInitCollection }] = (0, import_react_collection.unstable_createCollection)(ONE_TIME_PASSWORD_FIELD_NAME);
|
||||
var [createOneTimePasswordFieldContext] = (0, import_react_context.createContextScope)(ONE_TIME_PASSWORD_FIELD_NAME, [
|
||||
createCollectionScope,
|
||||
import_react_roving_focus.createRovingFocusGroupScope
|
||||
]);
|
||||
var useRovingFocusGroupScope = (0, import_react_roving_focus.createRovingFocusGroupScope)();
|
||||
var [OneTimePasswordFieldContext, useOneTimePasswordFieldContext] = createOneTimePasswordFieldContext(ONE_TIME_PASSWORD_FIELD_NAME);
|
||||
var OneTimePasswordField = React.forwardRef(
|
||||
function OneTimePasswordFieldImpl({
|
||||
__scopeOneTimePasswordField,
|
||||
defaultValue,
|
||||
value: valueProp,
|
||||
onValueChange,
|
||||
autoSubmit = false,
|
||||
children,
|
||||
onPaste,
|
||||
onAutoSubmit,
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
autoComplete = "one-time-code",
|
||||
autoFocus = false,
|
||||
form,
|
||||
name,
|
||||
placeholder,
|
||||
type = "text",
|
||||
// TODO: Change default to vertical when inputs use vertical writing mode
|
||||
orientation = "horizontal",
|
||||
dir,
|
||||
validationType = "numeric",
|
||||
sanitizeValue: sanitizeValueProp,
|
||||
...domProps
|
||||
}, forwardedRef) {
|
||||
const rovingFocusGroupScope = useRovingFocusGroupScope(__scopeOneTimePasswordField);
|
||||
const direction = (0, import_react_direction.useDirection)(dir);
|
||||
const collectionState = useInitCollection();
|
||||
const [collection] = collectionState;
|
||||
const validation = INPUT_VALIDATION_MAP[validationType] ? INPUT_VALIDATION_MAP[validationType] : null;
|
||||
const sanitizeValue = React.useCallback(
|
||||
(value2) => {
|
||||
if (Array.isArray(value2)) {
|
||||
value2 = value2.map(removeWhitespace).join("");
|
||||
} else {
|
||||
value2 = removeWhitespace(value2);
|
||||
}
|
||||
if (validation) {
|
||||
const regexp = new RegExp(validation.regexp);
|
||||
value2 = value2.replace(regexp, "");
|
||||
} else if (sanitizeValueProp) {
|
||||
value2 = sanitizeValueProp(value2);
|
||||
}
|
||||
return value2.split("");
|
||||
},
|
||||
[validation, sanitizeValueProp]
|
||||
);
|
||||
const controlledValue = React.useMemo(() => {
|
||||
return valueProp != null ? sanitizeValue(valueProp) : void 0;
|
||||
}, [valueProp, sanitizeValue]);
|
||||
const [value, setValue] = (0, import_react_use_controllable_state.useControllableState)({
|
||||
caller: "OneTimePasswordField",
|
||||
prop: controlledValue,
|
||||
defaultProp: defaultValue != null ? sanitizeValue(defaultValue) : [],
|
||||
onChange: React.useCallback(
|
||||
(value2) => onValueChange?.(value2.join("")),
|
||||
[onValueChange]
|
||||
)
|
||||
});
|
||||
const dispatch = (0, import_react_use_effect_event.useEffectEvent)((action) => {
|
||||
switch (action.type) {
|
||||
case "SET_CHAR": {
|
||||
const { index, char } = action;
|
||||
const currentTarget = collection.at(index)?.element;
|
||||
if (value[index] === char) {
|
||||
const next = currentTarget && collection.from(currentTarget, 1)?.element;
|
||||
focusInput(next);
|
||||
return;
|
||||
}
|
||||
if (char === "") {
|
||||
return;
|
||||
}
|
||||
if (validation) {
|
||||
const regexp = new RegExp(validation.regexp);
|
||||
const clean = char.replace(regexp, "");
|
||||
if (clean !== char) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (value.length >= collection.size) {
|
||||
const newValue2 = [...value];
|
||||
newValue2[index] = char;
|
||||
(0, import_react_dom.flushSync)(() => setValue(newValue2));
|
||||
const next = currentTarget && collection.from(currentTarget, 1)?.element;
|
||||
focusInput(next);
|
||||
return;
|
||||
}
|
||||
const newValue = [...value];
|
||||
newValue[index] = char;
|
||||
const lastElement = collection.at(-1)?.element;
|
||||
(0, import_react_dom.flushSync)(() => setValue(newValue));
|
||||
if (currentTarget !== lastElement) {
|
||||
const next = currentTarget && collection.from(currentTarget, 1)?.element;
|
||||
focusInput(next);
|
||||
} else {
|
||||
currentTarget?.select();
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "CLEAR_CHAR": {
|
||||
const { index, reason } = action;
|
||||
if (!value[index]) {
|
||||
return;
|
||||
}
|
||||
const newValue = value.filter((_, i) => i !== index);
|
||||
const currentTarget = collection.at(index)?.element;
|
||||
const previous = currentTarget && collection.from(currentTarget, -1)?.element;
|
||||
(0, import_react_dom.flushSync)(() => setValue(newValue));
|
||||
if (reason === "Backspace") {
|
||||
focusInput(previous);
|
||||
} else if (reason === "Delete" || reason === "Cut") {
|
||||
focusInput(currentTarget);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "CLEAR": {
|
||||
if (value.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (action.reason === "Backspace" || action.reason === "Delete") {
|
||||
(0, import_react_dom.flushSync)(() => setValue([]));
|
||||
focusInput(collection.at(0)?.element);
|
||||
} else {
|
||||
setValue([]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "PASTE": {
|
||||
const { value: pastedValue } = action;
|
||||
const value2 = sanitizeValue(pastedValue);
|
||||
if (!value2) {
|
||||
return;
|
||||
}
|
||||
(0, import_react_dom.flushSync)(() => setValue(value2));
|
||||
focusInput(collection.at(value2.length - 1)?.element);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
const validationTypeRef = React.useRef(validation);
|
||||
React.useEffect(() => {
|
||||
if (!validation) {
|
||||
return;
|
||||
}
|
||||
if (validationTypeRef.current?.type !== validation.type) {
|
||||
validationTypeRef.current = validation;
|
||||
setValue(sanitizeValue(value.join("")));
|
||||
}
|
||||
}, [sanitizeValue, setValue, validation, value]);
|
||||
const hiddenInputRef = React.useRef(null);
|
||||
const userActionRef = React.useRef(null);
|
||||
const rootRef = React.useRef(null);
|
||||
const composedRefs = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, rootRef);
|
||||
const firstInput = collection.at(0)?.element;
|
||||
const locateForm = React.useCallback(() => {
|
||||
let formElement;
|
||||
if (form) {
|
||||
const associatedElement = (rootRef.current?.ownerDocument ?? document).getElementById(form);
|
||||
if (isFormElement(associatedElement)) {
|
||||
formElement = associatedElement;
|
||||
}
|
||||
} else if (hiddenInputRef.current) {
|
||||
formElement = hiddenInputRef.current.form;
|
||||
} else if (firstInput) {
|
||||
formElement = firstInput.form;
|
||||
}
|
||||
return formElement ?? null;
|
||||
}, [form, firstInput]);
|
||||
const attemptSubmit = React.useCallback(() => {
|
||||
const formElement = locateForm();
|
||||
formElement?.requestSubmit();
|
||||
}, [locateForm]);
|
||||
React.useEffect(() => {
|
||||
const form2 = locateForm();
|
||||
if (form2) {
|
||||
const reset = () => dispatch({ type: "CLEAR", reason: "Reset" });
|
||||
form2.addEventListener("reset", reset);
|
||||
return () => form2.removeEventListener("reset", reset);
|
||||
}
|
||||
}, [dispatch, locateForm]);
|
||||
const currentValue = value.join("");
|
||||
const valueRef = React.useRef(currentValue);
|
||||
const length = collection.size;
|
||||
React.useEffect(() => {
|
||||
const previousValue = valueRef.current;
|
||||
valueRef.current = currentValue;
|
||||
if (previousValue === currentValue) {
|
||||
return;
|
||||
}
|
||||
if (autoSubmit && value.every((char) => char !== "") && value.length === length) {
|
||||
onAutoSubmit?.(value.join(""));
|
||||
attemptSubmit();
|
||||
}
|
||||
}, [attemptSubmit, autoSubmit, currentValue, length, onAutoSubmit, value]);
|
||||
const isHydrated = (0, import_react_use_is_hydrated.useIsHydrated)();
|
||||
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
||||
OneTimePasswordFieldContext,
|
||||
{
|
||||
scope: __scopeOneTimePasswordField,
|
||||
value,
|
||||
attemptSubmit,
|
||||
disabled,
|
||||
readOnly,
|
||||
autoComplete,
|
||||
autoFocus,
|
||||
form,
|
||||
name,
|
||||
placeholder,
|
||||
type,
|
||||
hiddenInputRef,
|
||||
userActionRef,
|
||||
dispatch,
|
||||
validationType,
|
||||
orientation,
|
||||
isHydrated,
|
||||
sanitizeValue,
|
||||
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Collection.Provider, { scope: __scopeOneTimePasswordField, state: collectionState, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Collection.Slot, { scope: __scopeOneTimePasswordField, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
||||
RovingFocusGroup.Root,
|
||||
{
|
||||
asChild: true,
|
||||
...rovingFocusGroupScope,
|
||||
orientation,
|
||||
dir: direction,
|
||||
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
||||
Primitive.Root.div,
|
||||
{
|
||||
...domProps,
|
||||
role: "group",
|
||||
ref: composedRefs,
|
||||
onPaste: (0, import_primitive.composeEventHandlers)(
|
||||
onPaste,
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
const pastedValue = event.clipboardData.getData("Text");
|
||||
dispatch({ type: "PASTE", value: pastedValue });
|
||||
}
|
||||
),
|
||||
children
|
||||
}
|
||||
)
|
||||
}
|
||||
) }) })
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
var OneTimePasswordFieldHiddenInput = React.forwardRef(function OneTimePasswordFieldHiddenInput2({ __scopeOneTimePasswordField, ...props }, forwardedRef) {
|
||||
const { value, hiddenInputRef, name } = useOneTimePasswordFieldContext(
|
||||
"OneTimePasswordFieldHiddenInput",
|
||||
__scopeOneTimePasswordField
|
||||
);
|
||||
const ref = (0, import_react_compose_refs.useComposedRefs)(hiddenInputRef, forwardedRef);
|
||||
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
||||
"input",
|
||||
{
|
||||
ref,
|
||||
name,
|
||||
value: value.join("").trim(),
|
||||
autoComplete: "off",
|
||||
autoFocus: false,
|
||||
autoCapitalize: "off",
|
||||
autoCorrect: "off",
|
||||
autoSave: "off",
|
||||
spellCheck: false,
|
||||
...props,
|
||||
type: "hidden",
|
||||
readOnly: true
|
||||
}
|
||||
);
|
||||
});
|
||||
var OneTimePasswordFieldInput = React.forwardRef(function OneTimePasswordFieldInput2({
|
||||
__scopeOneTimePasswordField,
|
||||
onInvalidChange,
|
||||
index: indexProp,
|
||||
...props
|
||||
}, forwardedRef) {
|
||||
const {
|
||||
value: _value,
|
||||
defaultValue: _defaultValue,
|
||||
disabled: _disabled,
|
||||
readOnly: _readOnly,
|
||||
autoComplete: _autoComplete,
|
||||
autoFocus: _autoFocus,
|
||||
form: _form,
|
||||
name: _name,
|
||||
placeholder: _placeholder,
|
||||
type: _type,
|
||||
...domProps
|
||||
} = props;
|
||||
const context = useOneTimePasswordFieldContext(
|
||||
"OneTimePasswordFieldInput",
|
||||
__scopeOneTimePasswordField
|
||||
);
|
||||
const { dispatch, userActionRef, validationType, isHydrated, disabled } = context;
|
||||
const collection = useCollection(__scopeOneTimePasswordField);
|
||||
const rovingFocusGroupScope = useRovingFocusGroupScope(__scopeOneTimePasswordField);
|
||||
const inputRef = React.useRef(null);
|
||||
const [element, setElement] = React.useState(null);
|
||||
const index = indexProp ?? (element ? collection.indexOf(element) : -1);
|
||||
const canSetPlaceholder = indexProp != null || isHydrated;
|
||||
let placeholder;
|
||||
if (canSetPlaceholder && context.placeholder && context.value.length === 0) {
|
||||
placeholder = context.placeholder[index];
|
||||
}
|
||||
const composedInputRef = (0, import_react_compose_refs.useComposedRefs)(forwardedRef, inputRef, setElement);
|
||||
const char = context.value[index] ?? "";
|
||||
const keyboardActionTimeoutRef = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
window.clearTimeout(keyboardActionTimeoutRef.current);
|
||||
};
|
||||
}, []);
|
||||
const totalValue = context.value.join("").trim();
|
||||
const lastSelectableIndex = (0, import_number.clamp)(totalValue.length, [0, collection.size - 1]);
|
||||
const isFocusable = index <= lastSelectableIndex;
|
||||
const validation = validationType in INPUT_VALIDATION_MAP ? INPUT_VALIDATION_MAP[validationType] : void 0;
|
||||
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Collection.ItemSlot, { scope: __scopeOneTimePasswordField, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
||||
RovingFocusGroup.Item,
|
||||
{
|
||||
...rovingFocusGroupScope,
|
||||
asChild: true,
|
||||
focusable: !context.disabled && isFocusable,
|
||||
active: index === lastSelectableIndex,
|
||||
children: ({ hasTabStop, isCurrentTabStop }) => {
|
||||
const supportsAutoComplete = hasTabStop ? isCurrentTabStop : index === 0;
|
||||
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
||||
Primitive.Root.input,
|
||||
{
|
||||
ref: composedInputRef,
|
||||
type: context.type,
|
||||
disabled,
|
||||
"aria-label": `Character ${index + 1} of ${collection.size}`,
|
||||
autoComplete: supportsAutoComplete ? context.autoComplete : "off",
|
||||
"data-1p-ignore": supportsAutoComplete ? void 0 : "true",
|
||||
"data-lpignore": supportsAutoComplete ? void 0 : "true",
|
||||
"data-protonpass-ignore": supportsAutoComplete ? void 0 : "true",
|
||||
"data-bwignore": supportsAutoComplete ? void 0 : "true",
|
||||
inputMode: validation?.inputMode,
|
||||
maxLength: 1,
|
||||
pattern: validation?.pattern,
|
||||
readOnly: context.readOnly,
|
||||
value: char,
|
||||
placeholder,
|
||||
"data-radix-otp-input": "",
|
||||
"data-radix-index": index,
|
||||
...domProps,
|
||||
onFocus: (0, import_primitive.composeEventHandlers)(props.onFocus, (event) => {
|
||||
event.currentTarget.select();
|
||||
}),
|
||||
onCut: (0, import_primitive.composeEventHandlers)(props.onCut, (event) => {
|
||||
const currentValue = event.currentTarget.value;
|
||||
if (currentValue !== "") {
|
||||
userActionRef.current = {
|
||||
type: "cut"
|
||||
};
|
||||
keyboardActionTimeoutRef.current = window.setTimeout(() => {
|
||||
userActionRef.current = null;
|
||||
}, 10);
|
||||
}
|
||||
}),
|
||||
onInput: (0, import_primitive.composeEventHandlers)(props.onInput, (event) => {
|
||||
const value = event.currentTarget.value;
|
||||
if (value.length > 1) {
|
||||
event.preventDefault();
|
||||
dispatch({ type: "PASTE", value });
|
||||
}
|
||||
}),
|
||||
onChange: (0, import_primitive.composeEventHandlers)(props.onChange, (event) => {
|
||||
const value = event.target.value;
|
||||
event.preventDefault();
|
||||
const action = userActionRef.current;
|
||||
userActionRef.current = null;
|
||||
if (action) {
|
||||
switch (action.type) {
|
||||
case "cut":
|
||||
dispatch({ type: "CLEAR_CHAR", index, reason: "Cut" });
|
||||
return;
|
||||
case "keydown": {
|
||||
if (action.key === "Char") {
|
||||
return;
|
||||
}
|
||||
const isClearing = action.key === "Backspace" && (action.metaKey || action.ctrlKey);
|
||||
if (action.key === "Clear" || isClearing) {
|
||||
dispatch({ type: "CLEAR", reason: "Backspace" });
|
||||
} else {
|
||||
dispatch({ type: "CLEAR_CHAR", index, reason: action.key });
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (event.target.validity.valid) {
|
||||
if (value === "") {
|
||||
let reason = "Backspace";
|
||||
if (isInputEvent(event.nativeEvent)) {
|
||||
const inputType = event.nativeEvent.inputType;
|
||||
if (inputType === "deleteContentBackward") {
|
||||
reason = "Backspace";
|
||||
} else if (inputType === "deleteByCut") {
|
||||
reason = "Cut";
|
||||
}
|
||||
}
|
||||
dispatch({ type: "CLEAR_CHAR", index, reason });
|
||||
} else {
|
||||
dispatch({ type: "SET_CHAR", char: value, index, event });
|
||||
}
|
||||
} else {
|
||||
const element2 = event.target;
|
||||
onInvalidChange?.(element2.value);
|
||||
requestAnimationFrame(() => {
|
||||
if (element2.ownerDocument.activeElement === element2) {
|
||||
element2.select();
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
onKeyDown: (0, import_primitive.composeEventHandlers)(props.onKeyDown, (event) => {
|
||||
switch (event.key) {
|
||||
case "Clear":
|
||||
case "Delete":
|
||||
case "Backspace": {
|
||||
const currentValue = event.currentTarget.value;
|
||||
if (currentValue === "") {
|
||||
if (event.key === "Delete") return;
|
||||
const isClearing = event.key === "Clear" || event.metaKey || event.ctrlKey;
|
||||
if (isClearing) {
|
||||
dispatch({ type: "CLEAR", reason: "Backspace" });
|
||||
} else {
|
||||
const element2 = event.currentTarget;
|
||||
requestAnimationFrame(() => {
|
||||
focusInput(collection.from(element2, -1)?.element);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
userActionRef.current = {
|
||||
type: "keydown",
|
||||
key: event.key,
|
||||
metaKey: event.metaKey,
|
||||
ctrlKey: event.ctrlKey
|
||||
};
|
||||
keyboardActionTimeoutRef.current = window.setTimeout(() => {
|
||||
userActionRef.current = null;
|
||||
}, 10);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "Enter": {
|
||||
event.preventDefault();
|
||||
context.attemptSubmit();
|
||||
return;
|
||||
}
|
||||
case "ArrowDown":
|
||||
case "ArrowUp": {
|
||||
if (context.orientation === "horizontal") {
|
||||
event.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// TODO: Handle left/right arrow keys in vertical writing mode
|
||||
default: {
|
||||
if (event.currentTarget.value === event.key) {
|
||||
const element2 = event.currentTarget;
|
||||
event.preventDefault();
|
||||
focusInput(collection.from(element2, 1)?.element);
|
||||
return;
|
||||
} else if (
|
||||
// input already has a value, but...
|
||||
event.currentTarget.value && // the value is not selected
|
||||
!(event.currentTarget.selectionStart === 0 && event.currentTarget.selectionEnd != null && event.currentTarget.selectionEnd > 0)
|
||||
) {
|
||||
const attemptedValue = event.key;
|
||||
if (event.key.length > 1 || event.key === " ") {
|
||||
return;
|
||||
} else {
|
||||
const nextInput = collection.from(event.currentTarget, 1)?.element;
|
||||
const lastInput = collection.at(-1)?.element;
|
||||
if (nextInput !== lastInput && event.currentTarget !== lastInput) {
|
||||
if (event.currentTarget.selectionStart === 0) {
|
||||
dispatch({ type: "SET_CHAR", char: attemptedValue, index, event });
|
||||
} else {
|
||||
dispatch({
|
||||
type: "SET_CHAR",
|
||||
char: attemptedValue,
|
||||
index: index + 1,
|
||||
event
|
||||
});
|
||||
}
|
||||
userActionRef.current = {
|
||||
type: "keydown",
|
||||
key: "Char",
|
||||
metaKey: event.metaKey,
|
||||
ctrlKey: event.ctrlKey
|
||||
};
|
||||
keyboardActionTimeoutRef.current = window.setTimeout(() => {
|
||||
userActionRef.current = null;
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
onPointerDown: (0, import_primitive.composeEventHandlers)(props.onPointerDown, (event) => {
|
||||
event.preventDefault();
|
||||
const indexToFocus = Math.min(index, lastSelectableIndex);
|
||||
const element2 = collection.at(indexToFocus)?.element;
|
||||
focusInput(element2);
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
) });
|
||||
});
|
||||
function isFormElement(element) {
|
||||
return element?.tagName === "FORM";
|
||||
}
|
||||
function removeWhitespace(value) {
|
||||
return value.replace(/\s/g, "");
|
||||
}
|
||||
function focusInput(element) {
|
||||
if (!element) return;
|
||||
if (element.ownerDocument.activeElement === element) {
|
||||
window.requestAnimationFrame(() => {
|
||||
element.select?.();
|
||||
});
|
||||
} else {
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
function isInputEvent(event) {
|
||||
return event.type === "input";
|
||||
}
|
||||
//# sourceMappingURL=index.js.map
|
||||
7
node_modules/@radix-ui/react-one-time-password-field/dist/index.js.map
generated
vendored
Normal file
7
node_modules/@radix-ui/react-one-time-password-field/dist/index.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
591
node_modules/@radix-ui/react-one-time-password-field/dist/index.mjs
generated
vendored
Normal file
591
node_modules/@radix-ui/react-one-time-password-field/dist/index.mjs
generated
vendored
Normal file
@@ -0,0 +1,591 @@
|
||||
"use client";
|
||||
|
||||
// src/one-time-password-field.tsx
|
||||
import * as Primitive from "@radix-ui/react-primitive";
|
||||
import { useComposedRefs } from "@radix-ui/react-compose-refs";
|
||||
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
||||
import { composeEventHandlers } from "@radix-ui/primitive";
|
||||
import { unstable_createCollection as createCollection } from "@radix-ui/react-collection";
|
||||
import * as RovingFocusGroup from "@radix-ui/react-roving-focus";
|
||||
import { createRovingFocusGroupScope } from "@radix-ui/react-roving-focus";
|
||||
import { useIsHydrated } from "@radix-ui/react-use-is-hydrated";
|
||||
import * as React from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import { createContextScope } from "@radix-ui/react-context";
|
||||
import { useDirection } from "@radix-ui/react-direction";
|
||||
import { clamp } from "@radix-ui/number";
|
||||
import { useEffectEvent } from "@radix-ui/react-use-effect-event";
|
||||
import { jsx } from "react/jsx-runtime";
|
||||
var INPUT_VALIDATION_MAP = {
|
||||
numeric: {
|
||||
type: "numeric",
|
||||
regexp: /[^\d]/g,
|
||||
pattern: "\\d{1}",
|
||||
inputMode: "numeric"
|
||||
},
|
||||
alpha: {
|
||||
type: "alpha",
|
||||
regexp: /[^a-zA-Z]/g,
|
||||
pattern: "[a-zA-Z]{1}",
|
||||
inputMode: "text"
|
||||
},
|
||||
alphanumeric: {
|
||||
type: "alphanumeric",
|
||||
regexp: /[^a-zA-Z0-9]/g,
|
||||
pattern: "[a-zA-Z0-9]{1}",
|
||||
inputMode: "text"
|
||||
},
|
||||
none: null
|
||||
};
|
||||
var ONE_TIME_PASSWORD_FIELD_NAME = "OneTimePasswordField";
|
||||
var [Collection, { useCollection, createCollectionScope, useInitCollection }] = createCollection(ONE_TIME_PASSWORD_FIELD_NAME);
|
||||
var [createOneTimePasswordFieldContext] = createContextScope(ONE_TIME_PASSWORD_FIELD_NAME, [
|
||||
createCollectionScope,
|
||||
createRovingFocusGroupScope
|
||||
]);
|
||||
var useRovingFocusGroupScope = createRovingFocusGroupScope();
|
||||
var [OneTimePasswordFieldContext, useOneTimePasswordFieldContext] = createOneTimePasswordFieldContext(ONE_TIME_PASSWORD_FIELD_NAME);
|
||||
var OneTimePasswordField = React.forwardRef(
|
||||
function OneTimePasswordFieldImpl({
|
||||
__scopeOneTimePasswordField,
|
||||
defaultValue,
|
||||
value: valueProp,
|
||||
onValueChange,
|
||||
autoSubmit = false,
|
||||
children,
|
||||
onPaste,
|
||||
onAutoSubmit,
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
autoComplete = "one-time-code",
|
||||
autoFocus = false,
|
||||
form,
|
||||
name,
|
||||
placeholder,
|
||||
type = "text",
|
||||
// TODO: Change default to vertical when inputs use vertical writing mode
|
||||
orientation = "horizontal",
|
||||
dir,
|
||||
validationType = "numeric",
|
||||
sanitizeValue: sanitizeValueProp,
|
||||
...domProps
|
||||
}, forwardedRef) {
|
||||
const rovingFocusGroupScope = useRovingFocusGroupScope(__scopeOneTimePasswordField);
|
||||
const direction = useDirection(dir);
|
||||
const collectionState = useInitCollection();
|
||||
const [collection] = collectionState;
|
||||
const validation = INPUT_VALIDATION_MAP[validationType] ? INPUT_VALIDATION_MAP[validationType] : null;
|
||||
const sanitizeValue = React.useCallback(
|
||||
(value2) => {
|
||||
if (Array.isArray(value2)) {
|
||||
value2 = value2.map(removeWhitespace).join("");
|
||||
} else {
|
||||
value2 = removeWhitespace(value2);
|
||||
}
|
||||
if (validation) {
|
||||
const regexp = new RegExp(validation.regexp);
|
||||
value2 = value2.replace(regexp, "");
|
||||
} else if (sanitizeValueProp) {
|
||||
value2 = sanitizeValueProp(value2);
|
||||
}
|
||||
return value2.split("");
|
||||
},
|
||||
[validation, sanitizeValueProp]
|
||||
);
|
||||
const controlledValue = React.useMemo(() => {
|
||||
return valueProp != null ? sanitizeValue(valueProp) : void 0;
|
||||
}, [valueProp, sanitizeValue]);
|
||||
const [value, setValue] = useControllableState({
|
||||
caller: "OneTimePasswordField",
|
||||
prop: controlledValue,
|
||||
defaultProp: defaultValue != null ? sanitizeValue(defaultValue) : [],
|
||||
onChange: React.useCallback(
|
||||
(value2) => onValueChange?.(value2.join("")),
|
||||
[onValueChange]
|
||||
)
|
||||
});
|
||||
const dispatch = useEffectEvent((action) => {
|
||||
switch (action.type) {
|
||||
case "SET_CHAR": {
|
||||
const { index, char } = action;
|
||||
const currentTarget = collection.at(index)?.element;
|
||||
if (value[index] === char) {
|
||||
const next = currentTarget && collection.from(currentTarget, 1)?.element;
|
||||
focusInput(next);
|
||||
return;
|
||||
}
|
||||
if (char === "") {
|
||||
return;
|
||||
}
|
||||
if (validation) {
|
||||
const regexp = new RegExp(validation.regexp);
|
||||
const clean = char.replace(regexp, "");
|
||||
if (clean !== char) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (value.length >= collection.size) {
|
||||
const newValue2 = [...value];
|
||||
newValue2[index] = char;
|
||||
flushSync(() => setValue(newValue2));
|
||||
const next = currentTarget && collection.from(currentTarget, 1)?.element;
|
||||
focusInput(next);
|
||||
return;
|
||||
}
|
||||
const newValue = [...value];
|
||||
newValue[index] = char;
|
||||
const lastElement = collection.at(-1)?.element;
|
||||
flushSync(() => setValue(newValue));
|
||||
if (currentTarget !== lastElement) {
|
||||
const next = currentTarget && collection.from(currentTarget, 1)?.element;
|
||||
focusInput(next);
|
||||
} else {
|
||||
currentTarget?.select();
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "CLEAR_CHAR": {
|
||||
const { index, reason } = action;
|
||||
if (!value[index]) {
|
||||
return;
|
||||
}
|
||||
const newValue = value.filter((_, i) => i !== index);
|
||||
const currentTarget = collection.at(index)?.element;
|
||||
const previous = currentTarget && collection.from(currentTarget, -1)?.element;
|
||||
flushSync(() => setValue(newValue));
|
||||
if (reason === "Backspace") {
|
||||
focusInput(previous);
|
||||
} else if (reason === "Delete" || reason === "Cut") {
|
||||
focusInput(currentTarget);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "CLEAR": {
|
||||
if (value.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (action.reason === "Backspace" || action.reason === "Delete") {
|
||||
flushSync(() => setValue([]));
|
||||
focusInput(collection.at(0)?.element);
|
||||
} else {
|
||||
setValue([]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "PASTE": {
|
||||
const { value: pastedValue } = action;
|
||||
const value2 = sanitizeValue(pastedValue);
|
||||
if (!value2) {
|
||||
return;
|
||||
}
|
||||
flushSync(() => setValue(value2));
|
||||
focusInput(collection.at(value2.length - 1)?.element);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
const validationTypeRef = React.useRef(validation);
|
||||
React.useEffect(() => {
|
||||
if (!validation) {
|
||||
return;
|
||||
}
|
||||
if (validationTypeRef.current?.type !== validation.type) {
|
||||
validationTypeRef.current = validation;
|
||||
setValue(sanitizeValue(value.join("")));
|
||||
}
|
||||
}, [sanitizeValue, setValue, validation, value]);
|
||||
const hiddenInputRef = React.useRef(null);
|
||||
const userActionRef = React.useRef(null);
|
||||
const rootRef = React.useRef(null);
|
||||
const composedRefs = useComposedRefs(forwardedRef, rootRef);
|
||||
const firstInput = collection.at(0)?.element;
|
||||
const locateForm = React.useCallback(() => {
|
||||
let formElement;
|
||||
if (form) {
|
||||
const associatedElement = (rootRef.current?.ownerDocument ?? document).getElementById(form);
|
||||
if (isFormElement(associatedElement)) {
|
||||
formElement = associatedElement;
|
||||
}
|
||||
} else if (hiddenInputRef.current) {
|
||||
formElement = hiddenInputRef.current.form;
|
||||
} else if (firstInput) {
|
||||
formElement = firstInput.form;
|
||||
}
|
||||
return formElement ?? null;
|
||||
}, [form, firstInput]);
|
||||
const attemptSubmit = React.useCallback(() => {
|
||||
const formElement = locateForm();
|
||||
formElement?.requestSubmit();
|
||||
}, [locateForm]);
|
||||
React.useEffect(() => {
|
||||
const form2 = locateForm();
|
||||
if (form2) {
|
||||
const reset = () => dispatch({ type: "CLEAR", reason: "Reset" });
|
||||
form2.addEventListener("reset", reset);
|
||||
return () => form2.removeEventListener("reset", reset);
|
||||
}
|
||||
}, [dispatch, locateForm]);
|
||||
const currentValue = value.join("");
|
||||
const valueRef = React.useRef(currentValue);
|
||||
const length = collection.size;
|
||||
React.useEffect(() => {
|
||||
const previousValue = valueRef.current;
|
||||
valueRef.current = currentValue;
|
||||
if (previousValue === currentValue) {
|
||||
return;
|
||||
}
|
||||
if (autoSubmit && value.every((char) => char !== "") && value.length === length) {
|
||||
onAutoSubmit?.(value.join(""));
|
||||
attemptSubmit();
|
||||
}
|
||||
}, [attemptSubmit, autoSubmit, currentValue, length, onAutoSubmit, value]);
|
||||
const isHydrated = useIsHydrated();
|
||||
return /* @__PURE__ */ jsx(
|
||||
OneTimePasswordFieldContext,
|
||||
{
|
||||
scope: __scopeOneTimePasswordField,
|
||||
value,
|
||||
attemptSubmit,
|
||||
disabled,
|
||||
readOnly,
|
||||
autoComplete,
|
||||
autoFocus,
|
||||
form,
|
||||
name,
|
||||
placeholder,
|
||||
type,
|
||||
hiddenInputRef,
|
||||
userActionRef,
|
||||
dispatch,
|
||||
validationType,
|
||||
orientation,
|
||||
isHydrated,
|
||||
sanitizeValue,
|
||||
children: /* @__PURE__ */ jsx(Collection.Provider, { scope: __scopeOneTimePasswordField, state: collectionState, children: /* @__PURE__ */ jsx(Collection.Slot, { scope: __scopeOneTimePasswordField, children: /* @__PURE__ */ jsx(
|
||||
RovingFocusGroup.Root,
|
||||
{
|
||||
asChild: true,
|
||||
...rovingFocusGroupScope,
|
||||
orientation,
|
||||
dir: direction,
|
||||
children: /* @__PURE__ */ jsx(
|
||||
Primitive.Root.div,
|
||||
{
|
||||
...domProps,
|
||||
role: "group",
|
||||
ref: composedRefs,
|
||||
onPaste: composeEventHandlers(
|
||||
onPaste,
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
const pastedValue = event.clipboardData.getData("Text");
|
||||
dispatch({ type: "PASTE", value: pastedValue });
|
||||
}
|
||||
),
|
||||
children
|
||||
}
|
||||
)
|
||||
}
|
||||
) }) })
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
var OneTimePasswordFieldHiddenInput = React.forwardRef(function OneTimePasswordFieldHiddenInput2({ __scopeOneTimePasswordField, ...props }, forwardedRef) {
|
||||
const { value, hiddenInputRef, name } = useOneTimePasswordFieldContext(
|
||||
"OneTimePasswordFieldHiddenInput",
|
||||
__scopeOneTimePasswordField
|
||||
);
|
||||
const ref = useComposedRefs(hiddenInputRef, forwardedRef);
|
||||
return /* @__PURE__ */ jsx(
|
||||
"input",
|
||||
{
|
||||
ref,
|
||||
name,
|
||||
value: value.join("").trim(),
|
||||
autoComplete: "off",
|
||||
autoFocus: false,
|
||||
autoCapitalize: "off",
|
||||
autoCorrect: "off",
|
||||
autoSave: "off",
|
||||
spellCheck: false,
|
||||
...props,
|
||||
type: "hidden",
|
||||
readOnly: true
|
||||
}
|
||||
);
|
||||
});
|
||||
var OneTimePasswordFieldInput = React.forwardRef(function OneTimePasswordFieldInput2({
|
||||
__scopeOneTimePasswordField,
|
||||
onInvalidChange,
|
||||
index: indexProp,
|
||||
...props
|
||||
}, forwardedRef) {
|
||||
const {
|
||||
value: _value,
|
||||
defaultValue: _defaultValue,
|
||||
disabled: _disabled,
|
||||
readOnly: _readOnly,
|
||||
autoComplete: _autoComplete,
|
||||
autoFocus: _autoFocus,
|
||||
form: _form,
|
||||
name: _name,
|
||||
placeholder: _placeholder,
|
||||
type: _type,
|
||||
...domProps
|
||||
} = props;
|
||||
const context = useOneTimePasswordFieldContext(
|
||||
"OneTimePasswordFieldInput",
|
||||
__scopeOneTimePasswordField
|
||||
);
|
||||
const { dispatch, userActionRef, validationType, isHydrated, disabled } = context;
|
||||
const collection = useCollection(__scopeOneTimePasswordField);
|
||||
const rovingFocusGroupScope = useRovingFocusGroupScope(__scopeOneTimePasswordField);
|
||||
const inputRef = React.useRef(null);
|
||||
const [element, setElement] = React.useState(null);
|
||||
const index = indexProp ?? (element ? collection.indexOf(element) : -1);
|
||||
const canSetPlaceholder = indexProp != null || isHydrated;
|
||||
let placeholder;
|
||||
if (canSetPlaceholder && context.placeholder && context.value.length === 0) {
|
||||
placeholder = context.placeholder[index];
|
||||
}
|
||||
const composedInputRef = useComposedRefs(forwardedRef, inputRef, setElement);
|
||||
const char = context.value[index] ?? "";
|
||||
const keyboardActionTimeoutRef = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
window.clearTimeout(keyboardActionTimeoutRef.current);
|
||||
};
|
||||
}, []);
|
||||
const totalValue = context.value.join("").trim();
|
||||
const lastSelectableIndex = clamp(totalValue.length, [0, collection.size - 1]);
|
||||
const isFocusable = index <= lastSelectableIndex;
|
||||
const validation = validationType in INPUT_VALIDATION_MAP ? INPUT_VALIDATION_MAP[validationType] : void 0;
|
||||
return /* @__PURE__ */ jsx(Collection.ItemSlot, { scope: __scopeOneTimePasswordField, children: /* @__PURE__ */ jsx(
|
||||
RovingFocusGroup.Item,
|
||||
{
|
||||
...rovingFocusGroupScope,
|
||||
asChild: true,
|
||||
focusable: !context.disabled && isFocusable,
|
||||
active: index === lastSelectableIndex,
|
||||
children: ({ hasTabStop, isCurrentTabStop }) => {
|
||||
const supportsAutoComplete = hasTabStop ? isCurrentTabStop : index === 0;
|
||||
return /* @__PURE__ */ jsx(
|
||||
Primitive.Root.input,
|
||||
{
|
||||
ref: composedInputRef,
|
||||
type: context.type,
|
||||
disabled,
|
||||
"aria-label": `Character ${index + 1} of ${collection.size}`,
|
||||
autoComplete: supportsAutoComplete ? context.autoComplete : "off",
|
||||
"data-1p-ignore": supportsAutoComplete ? void 0 : "true",
|
||||
"data-lpignore": supportsAutoComplete ? void 0 : "true",
|
||||
"data-protonpass-ignore": supportsAutoComplete ? void 0 : "true",
|
||||
"data-bwignore": supportsAutoComplete ? void 0 : "true",
|
||||
inputMode: validation?.inputMode,
|
||||
maxLength: 1,
|
||||
pattern: validation?.pattern,
|
||||
readOnly: context.readOnly,
|
||||
value: char,
|
||||
placeholder,
|
||||
"data-radix-otp-input": "",
|
||||
"data-radix-index": index,
|
||||
...domProps,
|
||||
onFocus: composeEventHandlers(props.onFocus, (event) => {
|
||||
event.currentTarget.select();
|
||||
}),
|
||||
onCut: composeEventHandlers(props.onCut, (event) => {
|
||||
const currentValue = event.currentTarget.value;
|
||||
if (currentValue !== "") {
|
||||
userActionRef.current = {
|
||||
type: "cut"
|
||||
};
|
||||
keyboardActionTimeoutRef.current = window.setTimeout(() => {
|
||||
userActionRef.current = null;
|
||||
}, 10);
|
||||
}
|
||||
}),
|
||||
onInput: composeEventHandlers(props.onInput, (event) => {
|
||||
const value = event.currentTarget.value;
|
||||
if (value.length > 1) {
|
||||
event.preventDefault();
|
||||
dispatch({ type: "PASTE", value });
|
||||
}
|
||||
}),
|
||||
onChange: composeEventHandlers(props.onChange, (event) => {
|
||||
const value = event.target.value;
|
||||
event.preventDefault();
|
||||
const action = userActionRef.current;
|
||||
userActionRef.current = null;
|
||||
if (action) {
|
||||
switch (action.type) {
|
||||
case "cut":
|
||||
dispatch({ type: "CLEAR_CHAR", index, reason: "Cut" });
|
||||
return;
|
||||
case "keydown": {
|
||||
if (action.key === "Char") {
|
||||
return;
|
||||
}
|
||||
const isClearing = action.key === "Backspace" && (action.metaKey || action.ctrlKey);
|
||||
if (action.key === "Clear" || isClearing) {
|
||||
dispatch({ type: "CLEAR", reason: "Backspace" });
|
||||
} else {
|
||||
dispatch({ type: "CLEAR_CHAR", index, reason: action.key });
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (event.target.validity.valid) {
|
||||
if (value === "") {
|
||||
let reason = "Backspace";
|
||||
if (isInputEvent(event.nativeEvent)) {
|
||||
const inputType = event.nativeEvent.inputType;
|
||||
if (inputType === "deleteContentBackward") {
|
||||
reason = "Backspace";
|
||||
} else if (inputType === "deleteByCut") {
|
||||
reason = "Cut";
|
||||
}
|
||||
}
|
||||
dispatch({ type: "CLEAR_CHAR", index, reason });
|
||||
} else {
|
||||
dispatch({ type: "SET_CHAR", char: value, index, event });
|
||||
}
|
||||
} else {
|
||||
const element2 = event.target;
|
||||
onInvalidChange?.(element2.value);
|
||||
requestAnimationFrame(() => {
|
||||
if (element2.ownerDocument.activeElement === element2) {
|
||||
element2.select();
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
onKeyDown: composeEventHandlers(props.onKeyDown, (event) => {
|
||||
switch (event.key) {
|
||||
case "Clear":
|
||||
case "Delete":
|
||||
case "Backspace": {
|
||||
const currentValue = event.currentTarget.value;
|
||||
if (currentValue === "") {
|
||||
if (event.key === "Delete") return;
|
||||
const isClearing = event.key === "Clear" || event.metaKey || event.ctrlKey;
|
||||
if (isClearing) {
|
||||
dispatch({ type: "CLEAR", reason: "Backspace" });
|
||||
} else {
|
||||
const element2 = event.currentTarget;
|
||||
requestAnimationFrame(() => {
|
||||
focusInput(collection.from(element2, -1)?.element);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
userActionRef.current = {
|
||||
type: "keydown",
|
||||
key: event.key,
|
||||
metaKey: event.metaKey,
|
||||
ctrlKey: event.ctrlKey
|
||||
};
|
||||
keyboardActionTimeoutRef.current = window.setTimeout(() => {
|
||||
userActionRef.current = null;
|
||||
}, 10);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "Enter": {
|
||||
event.preventDefault();
|
||||
context.attemptSubmit();
|
||||
return;
|
||||
}
|
||||
case "ArrowDown":
|
||||
case "ArrowUp": {
|
||||
if (context.orientation === "horizontal") {
|
||||
event.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// TODO: Handle left/right arrow keys in vertical writing mode
|
||||
default: {
|
||||
if (event.currentTarget.value === event.key) {
|
||||
const element2 = event.currentTarget;
|
||||
event.preventDefault();
|
||||
focusInput(collection.from(element2, 1)?.element);
|
||||
return;
|
||||
} else if (
|
||||
// input already has a value, but...
|
||||
event.currentTarget.value && // the value is not selected
|
||||
!(event.currentTarget.selectionStart === 0 && event.currentTarget.selectionEnd != null && event.currentTarget.selectionEnd > 0)
|
||||
) {
|
||||
const attemptedValue = event.key;
|
||||
if (event.key.length > 1 || event.key === " ") {
|
||||
return;
|
||||
} else {
|
||||
const nextInput = collection.from(event.currentTarget, 1)?.element;
|
||||
const lastInput = collection.at(-1)?.element;
|
||||
if (nextInput !== lastInput && event.currentTarget !== lastInput) {
|
||||
if (event.currentTarget.selectionStart === 0) {
|
||||
dispatch({ type: "SET_CHAR", char: attemptedValue, index, event });
|
||||
} else {
|
||||
dispatch({
|
||||
type: "SET_CHAR",
|
||||
char: attemptedValue,
|
||||
index: index + 1,
|
||||
event
|
||||
});
|
||||
}
|
||||
userActionRef.current = {
|
||||
type: "keydown",
|
||||
key: "Char",
|
||||
metaKey: event.metaKey,
|
||||
ctrlKey: event.ctrlKey
|
||||
};
|
||||
keyboardActionTimeoutRef.current = window.setTimeout(() => {
|
||||
userActionRef.current = null;
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
onPointerDown: composeEventHandlers(props.onPointerDown, (event) => {
|
||||
event.preventDefault();
|
||||
const indexToFocus = Math.min(index, lastSelectableIndex);
|
||||
const element2 = collection.at(indexToFocus)?.element;
|
||||
focusInput(element2);
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
) });
|
||||
});
|
||||
function isFormElement(element) {
|
||||
return element?.tagName === "FORM";
|
||||
}
|
||||
function removeWhitespace(value) {
|
||||
return value.replace(/\s/g, "");
|
||||
}
|
||||
function focusInput(element) {
|
||||
if (!element) return;
|
||||
if (element.ownerDocument.activeElement === element) {
|
||||
window.requestAnimationFrame(() => {
|
||||
element.select?.();
|
||||
});
|
||||
} else {
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
function isInputEvent(event) {
|
||||
return event.type === "input";
|
||||
}
|
||||
export {
|
||||
OneTimePasswordFieldHiddenInput as HiddenInput,
|
||||
OneTimePasswordFieldInput as Input,
|
||||
OneTimePasswordField,
|
||||
OneTimePasswordFieldHiddenInput,
|
||||
OneTimePasswordFieldInput,
|
||||
OneTimePasswordField as Root
|
||||
};
|
||||
//# sourceMappingURL=index.mjs.map
|
||||
7
node_modules/@radix-ui/react-one-time-password-field/dist/index.mjs.map
generated
vendored
Normal file
7
node_modules/@radix-ui/react-one-time-password-field/dist/index.mjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
80
node_modules/@radix-ui/react-one-time-password-field/package.json
generated
vendored
Normal file
80
node_modules/@radix-ui/react-one-time-password-field/package.json
generated
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"name": "@radix-ui/react-one-time-password-field",
|
||||
"version": "0.1.8",
|
||||
"license": "MIT",
|
||||
"source": "./src/index.ts",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/number": "1.1.1",
|
||||
"@radix-ui/react-roving-focus": "1.1.11",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||
"@radix-ui/react-use-is-hydrated": "0.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.7",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"eslint": "^9.18.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"typescript": "^5.7.3",
|
||||
"@repo/eslint-config": "0.0.0",
|
||||
"@repo/builder": "0.0.0",
|
||||
"@repo/typescript-config": "0.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"homepage": "https://radix-ui.com/primitives",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/radix-ui/primitives.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/radix-ui/primitives/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint --max-warnings 0 src",
|
||||
"clean": "rm -rf dist",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "radix-build"
|
||||
},
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
node_modules/@radix-ui/react-one-time-password-field/src/index.ts
generated
vendored
Normal file
16
node_modules/@radix-ui/react-one-time-password-field/src/index.ts
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
export type {
|
||||
OneTimePasswordFieldProps,
|
||||
OneTimePasswordFieldInputProps,
|
||||
OneTimePasswordFieldHiddenInputProps,
|
||||
InputValidationType,
|
||||
} from './one-time-password-field';
|
||||
export {
|
||||
OneTimePasswordField,
|
||||
OneTimePasswordFieldInput,
|
||||
OneTimePasswordFieldHiddenInput,
|
||||
//
|
||||
Root,
|
||||
Input,
|
||||
HiddenInput,
|
||||
} from './one-time-password-field';
|
||||
86
node_modules/@radix-ui/react-one-time-password-field/src/one-time-password-field.test.tsx
generated
vendored
Normal file
86
node_modules/@radix-ui/react-one-time-password-field/src/one-time-password-field.test.tsx
generated
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
import { axe } from 'vitest-axe';
|
||||
import type { RenderResult } from '@testing-library/react';
|
||||
import { act, cleanup, render, screen, fireEvent } from '@testing-library/react';
|
||||
import * as OneTimePasswordField from './one-time-password-field';
|
||||
import { afterEach, describe, it, beforeEach, expect } from 'vitest';
|
||||
import { userEvent, type UserEvent } from '@testing-library/user-event';
|
||||
|
||||
describe('given a default OneTimePasswordField', () => {
|
||||
let rendered: RenderResult;
|
||||
let user: UserEvent;
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
beforeEach(() => {
|
||||
user = userEvent.setup();
|
||||
rendered = render(
|
||||
<OneTimePasswordField.Root>
|
||||
<OneTimePasswordField.Input />
|
||||
<OneTimePasswordField.Input />
|
||||
<OneTimePasswordField.Input />
|
||||
<OneTimePasswordField.Input />
|
||||
<OneTimePasswordField.Input />
|
||||
<OneTimePasswordField.Input />
|
||||
<OneTimePasswordField.HiddenInput name="code" />
|
||||
</OneTimePasswordField.Root>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
it('should have no accessibility violations', async () => {
|
||||
expect(await axe(rendered.container)).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should mask input value when type is password', async () => {
|
||||
rendered.rerender(
|
||||
<OneTimePasswordField.Root type="password">
|
||||
<OneTimePasswordField.Input />
|
||||
<OneTimePasswordField.HiddenInput name="code" />
|
||||
</OneTimePasswordField.Root>
|
||||
);
|
||||
|
||||
const input = rendered.container.querySelector(
|
||||
'input:not([type="hidden"])'
|
||||
) as HTMLInputElement;
|
||||
|
||||
await userEvent.type(input, '1');
|
||||
expect(input.type).toBe('password');
|
||||
|
||||
const hiddenInput = rendered.container.querySelector(
|
||||
'input[type="hidden"]'
|
||||
) as HTMLInputElement;
|
||||
expect(hiddenInput.value).toBe('1');
|
||||
});
|
||||
|
||||
it('should disable all inputs when Root is disabled', () => {
|
||||
rendered.rerender(
|
||||
<OneTimePasswordField.Root disabled>
|
||||
<OneTimePasswordField.Input />
|
||||
<OneTimePasswordField.Input />
|
||||
<OneTimePasswordField.HiddenInput name="code" />
|
||||
</OneTimePasswordField.Root>
|
||||
);
|
||||
|
||||
const inputs = rendered.container.querySelectorAll('input:not([type="hidden"])');
|
||||
inputs.forEach(input => {
|
||||
expect(input).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: userEvent paste not behaving as expected. Debug and unskip.
|
||||
// Replicated in storybook for now.
|
||||
it.todo('pastes the code into the input', async () => {
|
||||
const inputs = screen.getAllByRole<HTMLInputElement>('textbox', {
|
||||
hidden: false,
|
||||
});
|
||||
const firstInput = inputs[0]!;
|
||||
fireEvent.click(firstInput);
|
||||
await act(async () => await user.paste('1,2,3,4,5,6'));
|
||||
expect(getInputValues(inputs)).toBe('1,2,3,4,5,6');
|
||||
});
|
||||
});
|
||||
|
||||
function getInputValues(inputs: HTMLInputElement[]) {
|
||||
return inputs.map((input) => input.value).join(',');
|
||||
}
|
||||
953
node_modules/@radix-ui/react-one-time-password-field/src/one-time-password-field.tsx
generated
vendored
Normal file
953
node_modules/@radix-ui/react-one-time-password-field/src/one-time-password-field.tsx
generated
vendored
Normal file
@@ -0,0 +1,953 @@
|
||||
import * as Primitive from '@radix-ui/react-primitive';
|
||||
import { useComposedRefs } from '@radix-ui/react-compose-refs';
|
||||
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
||||
import { composeEventHandlers } from '@radix-ui/primitive';
|
||||
import { unstable_createCollection as createCollection } from '@radix-ui/react-collection';
|
||||
import * as RovingFocusGroup from '@radix-ui/react-roving-focus';
|
||||
import { createRovingFocusGroupScope } from '@radix-ui/react-roving-focus';
|
||||
import { useIsHydrated } from '@radix-ui/react-use-is-hydrated';
|
||||
import * as React from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import type { Scope } from '@radix-ui/react-context';
|
||||
import { createContextScope } from '@radix-ui/react-context';
|
||||
import { useDirection } from '@radix-ui/react-direction';
|
||||
import { clamp } from '@radix-ui/number';
|
||||
import { useEffectEvent } from '@radix-ui/react-use-effect-event';
|
||||
|
||||
type InputValidationType = 'alpha' | 'numeric' | 'alphanumeric' | 'none';
|
||||
|
||||
const INPUT_VALIDATION_MAP = {
|
||||
numeric: {
|
||||
type: 'numeric',
|
||||
regexp: /[^\d]/g,
|
||||
pattern: '\\d{1}',
|
||||
inputMode: 'numeric',
|
||||
},
|
||||
alpha: {
|
||||
type: 'alpha',
|
||||
regexp: /[^a-zA-Z]/g,
|
||||
pattern: '[a-zA-Z]{1}',
|
||||
inputMode: 'text',
|
||||
},
|
||||
alphanumeric: {
|
||||
type: 'alphanumeric',
|
||||
regexp: /[^a-zA-Z0-9]/g,
|
||||
pattern: '[a-zA-Z0-9]{1}',
|
||||
inputMode: 'text',
|
||||
},
|
||||
none: null,
|
||||
} satisfies InputValidation;
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* OneTimePasswordFieldProvider
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
type RovingFocusGroupProps = RovingFocusGroup.RovingFocusGroupProps;
|
||||
|
||||
interface OneTimePasswordFieldContextValue {
|
||||
attemptSubmit: () => void;
|
||||
autoComplete: AutoComplete;
|
||||
autoFocus: boolean;
|
||||
disabled: boolean;
|
||||
dispatch: Dispatcher;
|
||||
form: string | undefined;
|
||||
hiddenInputRef: React.RefObject<HTMLInputElement | null>;
|
||||
isHydrated: boolean;
|
||||
name: string | undefined;
|
||||
orientation: Exclude<RovingFocusGroupProps['orientation'], undefined>;
|
||||
placeholder: string | undefined;
|
||||
readOnly: boolean;
|
||||
type: InputType;
|
||||
userActionRef: React.RefObject<KeyboardActionDetails | null>;
|
||||
validationType: InputValidationType;
|
||||
value: string[];
|
||||
sanitizeValue: (arg: string | string[]) => string[];
|
||||
}
|
||||
|
||||
const ONE_TIME_PASSWORD_FIELD_NAME = 'OneTimePasswordField';
|
||||
const [Collection, { useCollection, createCollectionScope, useInitCollection }] =
|
||||
createCollection<HTMLInputElement>(ONE_TIME_PASSWORD_FIELD_NAME);
|
||||
const [createOneTimePasswordFieldContext] = createContextScope(ONE_TIME_PASSWORD_FIELD_NAME, [
|
||||
createCollectionScope,
|
||||
createRovingFocusGroupScope,
|
||||
]);
|
||||
const useRovingFocusGroupScope = createRovingFocusGroupScope();
|
||||
|
||||
const [OneTimePasswordFieldContext, useOneTimePasswordFieldContext] =
|
||||
createOneTimePasswordFieldContext<OneTimePasswordFieldContextValue>(ONE_TIME_PASSWORD_FIELD_NAME);
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* OneTimePasswordField
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
interface OneTimePasswordFieldOwnProps {
|
||||
/**
|
||||
* Specifies what—if any—permission the user agent has to provide automated
|
||||
* assistance in filling out form field values, as well as guidance to the
|
||||
* browser as to the type of information expected in the field. Allows
|
||||
* `"one-time-code"` or `"off"`.
|
||||
*
|
||||
* @defaultValue `"one-time-code"`
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete
|
||||
*/
|
||||
autoComplete?: AutoComplete;
|
||||
/**
|
||||
* Whether or not the first fillable input should be focused on page-load.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/autofocus
|
||||
*/
|
||||
autoFocus?: boolean;
|
||||
/**
|
||||
* Whether or not the component should attempt to automatically submit when
|
||||
* all fields are filled. If the field is associated with an HTML `form`
|
||||
* element, the form's `requestSubmit` method will be called.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
autoSubmit?: boolean;
|
||||
/**
|
||||
* The initial value of the uncontrolled field.
|
||||
*/
|
||||
defaultValue?: string;
|
||||
/**
|
||||
* Indicates the horizontal directionality of the parent element's text.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/dir
|
||||
*/
|
||||
dir?: RovingFocusGroupProps['dir'];
|
||||
/**
|
||||
* Whether or not the the field's input elements are disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* A string specifying the `form` element with which the input is associated.
|
||||
* This string's value, if present, must match the id of a `form` element in
|
||||
* the same document.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#form
|
||||
*/
|
||||
form?: string | undefined;
|
||||
/**
|
||||
* A string specifying a name for the input control. This name is submitted
|
||||
* along with the control's value when the form data is submitted.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#name
|
||||
*/
|
||||
name?: string | undefined;
|
||||
/**
|
||||
* When the `autoSubmit` prop is set to `true`, this callback will be fired
|
||||
* before attempting to submit the associated form. It will be called whether
|
||||
* or not a form is located, or if submission is not allowed.
|
||||
*/
|
||||
onAutoSubmit?: (value: string) => void;
|
||||
/**
|
||||
* A callback fired when the field's value changes. When the component is
|
||||
* controlled, this should update the state passed to the `value` prop.
|
||||
*/
|
||||
onValueChange?: (value: string) => void;
|
||||
/**
|
||||
* Indicates the vertical directionality of the input elements.
|
||||
*
|
||||
* @defaultValue `"horizontal"`
|
||||
*/
|
||||
orientation?: RovingFocusGroupProps['orientation'];
|
||||
/**
|
||||
* Defines the text displayed in a form control when the control has no value.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/placeholder
|
||||
*/
|
||||
placeholder?: string | undefined;
|
||||
/**
|
||||
* Whether or not the input elements can be updated by the user.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/readonly
|
||||
*/
|
||||
readOnly?: boolean;
|
||||
/**
|
||||
* Function for custom sanitization when `validationType` is set to `"none"`.
|
||||
* This function will be called before updating values in response to user
|
||||
* interactions.
|
||||
*/
|
||||
sanitizeValue?: (value: string) => string;
|
||||
/**
|
||||
* The input type of the field's input elements. Can be `"password"` or `"text"`.
|
||||
*/
|
||||
type?: InputType;
|
||||
/**
|
||||
* Specifies the type of input validation to be used. Can be `"alpha"`,
|
||||
* `"numeric"`, `"alphanumeric"` or `"none"`.
|
||||
*
|
||||
* @defaultValue `"numeric"`
|
||||
*/
|
||||
validationType?: InputValidationType;
|
||||
/**
|
||||
* The controlled value of the field.
|
||||
*/
|
||||
value?: string;
|
||||
}
|
||||
|
||||
type ScopedProps<P> = P & { __scopeOneTimePasswordField?: Scope };
|
||||
|
||||
interface OneTimePasswordFieldProps
|
||||
extends OneTimePasswordFieldOwnProps,
|
||||
Omit<Primitive.PrimitivePropsWithRef<'div'>, keyof OneTimePasswordFieldOwnProps> {}
|
||||
|
||||
const OneTimePasswordField = React.forwardRef<HTMLDivElement, OneTimePasswordFieldProps>(
|
||||
function OneTimePasswordFieldImpl(
|
||||
{
|
||||
__scopeOneTimePasswordField,
|
||||
defaultValue,
|
||||
value: valueProp,
|
||||
onValueChange,
|
||||
autoSubmit = false,
|
||||
children,
|
||||
onPaste,
|
||||
onAutoSubmit,
|
||||
disabled = false,
|
||||
readOnly = false,
|
||||
autoComplete = 'one-time-code',
|
||||
autoFocus = false,
|
||||
form,
|
||||
name,
|
||||
placeholder,
|
||||
type = 'text',
|
||||
// TODO: Change default to vertical when inputs use vertical writing mode
|
||||
orientation = 'horizontal',
|
||||
dir,
|
||||
validationType = 'numeric',
|
||||
sanitizeValue: sanitizeValueProp,
|
||||
...domProps
|
||||
}: ScopedProps<OneTimePasswordFieldProps>,
|
||||
forwardedRef
|
||||
) {
|
||||
const rovingFocusGroupScope = useRovingFocusGroupScope(__scopeOneTimePasswordField);
|
||||
const direction = useDirection(dir);
|
||||
const collectionState = useInitCollection();
|
||||
const [collection] = collectionState;
|
||||
|
||||
const validation = INPUT_VALIDATION_MAP[validationType]
|
||||
? INPUT_VALIDATION_MAP[validationType as keyof InputValidation]
|
||||
: null;
|
||||
|
||||
const sanitizeValue = React.useCallback(
|
||||
(value: string | string[]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value = value.map(removeWhitespace).join('');
|
||||
} else {
|
||||
value = removeWhitespace(value);
|
||||
}
|
||||
|
||||
if (validation) {
|
||||
// global regexp is stateful, so we clone it for each call
|
||||
const regexp = new RegExp(validation.regexp);
|
||||
value = value.replace(regexp, '');
|
||||
} else if (sanitizeValueProp) {
|
||||
value = sanitizeValueProp(value);
|
||||
}
|
||||
|
||||
return value.split('');
|
||||
},
|
||||
[validation, sanitizeValueProp]
|
||||
);
|
||||
|
||||
const controlledValue = React.useMemo(() => {
|
||||
return valueProp != null ? sanitizeValue(valueProp) : undefined;
|
||||
}, [valueProp, sanitizeValue]);
|
||||
|
||||
const [value, setValue] = useControllableState({
|
||||
caller: 'OneTimePasswordField',
|
||||
prop: controlledValue,
|
||||
defaultProp: defaultValue != null ? sanitizeValue(defaultValue) : [],
|
||||
onChange: React.useCallback(
|
||||
(value: string[]) => onValueChange?.(value.join('')),
|
||||
[onValueChange]
|
||||
),
|
||||
});
|
||||
|
||||
// Update function *specifically* for event handlers.
|
||||
const dispatch = useEffectEvent<Dispatcher>((action) => {
|
||||
switch (action.type) {
|
||||
case 'SET_CHAR': {
|
||||
const { index, char } = action;
|
||||
const currentTarget = collection.at(index)?.element;
|
||||
if (value[index] === char) {
|
||||
const next = currentTarget && collection.from(currentTarget, 1)?.element;
|
||||
focusInput(next);
|
||||
return;
|
||||
}
|
||||
|
||||
// empty values should be handled in the CLEAR_CHAR action
|
||||
if (char === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (validation) {
|
||||
const regexp = new RegExp(validation.regexp);
|
||||
const clean = char.replace(regexp, '');
|
||||
if (clean !== char) {
|
||||
// not valid; ignore
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// no more space
|
||||
if (value.length >= collection.size) {
|
||||
// replace current value; move to next input
|
||||
const newValue = [...value];
|
||||
newValue[index] = char;
|
||||
flushSync(() => setValue(newValue));
|
||||
const next = currentTarget && collection.from(currentTarget, 1)?.element;
|
||||
focusInput(next);
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = [...value];
|
||||
newValue[index] = char;
|
||||
|
||||
const lastElement = collection.at(-1)?.element;
|
||||
flushSync(() => setValue(newValue));
|
||||
if (currentTarget !== lastElement) {
|
||||
const next = currentTarget && collection.from(currentTarget, 1)?.element;
|
||||
focusInput(next);
|
||||
} else {
|
||||
currentTarget?.select();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case 'CLEAR_CHAR': {
|
||||
const { index, reason } = action;
|
||||
if (!value[index]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = value.filter((_, i) => i !== index);
|
||||
const currentTarget = collection.at(index)?.element;
|
||||
const previous = currentTarget && collection.from(currentTarget, -1)?.element;
|
||||
|
||||
flushSync(() => setValue(newValue));
|
||||
if (reason === 'Backspace') {
|
||||
focusInput(previous);
|
||||
} else if (reason === 'Delete' || reason === 'Cut') {
|
||||
focusInput(currentTarget);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case 'CLEAR': {
|
||||
if (value.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.reason === 'Backspace' || action.reason === 'Delete') {
|
||||
flushSync(() => setValue([]));
|
||||
focusInput(collection.at(0)?.element);
|
||||
} else {
|
||||
setValue([]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case 'PASTE': {
|
||||
const { value: pastedValue } = action;
|
||||
const value = sanitizeValue(pastedValue);
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
flushSync(() => setValue(value));
|
||||
focusInput(collection.at(value.length - 1)?.element);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// re-validate when the validation type changes
|
||||
const validationTypeRef = React.useRef(validation);
|
||||
React.useEffect(() => {
|
||||
if (!validation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (validationTypeRef.current?.type !== validation.type) {
|
||||
validationTypeRef.current = validation;
|
||||
setValue(sanitizeValue(value.join('')));
|
||||
}
|
||||
}, [sanitizeValue, setValue, validation, value]);
|
||||
|
||||
const hiddenInputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const userActionRef = React.useRef<KeyboardActionDetails | null>(null);
|
||||
const rootRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const composedRefs = useComposedRefs(forwardedRef, rootRef);
|
||||
|
||||
const firstInput = collection.at(0)?.element;
|
||||
const locateForm = React.useCallback(() => {
|
||||
let formElement: HTMLFormElement | null | undefined;
|
||||
if (form) {
|
||||
const associatedElement = (rootRef.current?.ownerDocument ?? document).getElementById(form);
|
||||
if (isFormElement(associatedElement)) {
|
||||
formElement = associatedElement;
|
||||
}
|
||||
} else if (hiddenInputRef.current) {
|
||||
formElement = hiddenInputRef.current.form;
|
||||
} else if (firstInput) {
|
||||
formElement = firstInput.form;
|
||||
}
|
||||
|
||||
return formElement ?? null;
|
||||
}, [form, firstInput]);
|
||||
|
||||
const attemptSubmit = React.useCallback(() => {
|
||||
const formElement = locateForm();
|
||||
formElement?.requestSubmit();
|
||||
}, [locateForm]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const form = locateForm();
|
||||
if (form) {
|
||||
const reset = () => dispatch({ type: 'CLEAR', reason: 'Reset' });
|
||||
form.addEventListener('reset', reset);
|
||||
return () => form.removeEventListener('reset', reset);
|
||||
}
|
||||
}, [dispatch, locateForm]);
|
||||
|
||||
const currentValue = value.join('');
|
||||
const valueRef = React.useRef(currentValue);
|
||||
const length = collection.size;
|
||||
React.useEffect(() => {
|
||||
const previousValue = valueRef.current;
|
||||
valueRef.current = currentValue;
|
||||
if (previousValue === currentValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoSubmit && value.every((char) => char !== '') && value.length === length) {
|
||||
onAutoSubmit?.(value.join(''));
|
||||
attemptSubmit();
|
||||
}
|
||||
}, [attemptSubmit, autoSubmit, currentValue, length, onAutoSubmit, value]);
|
||||
const isHydrated = useIsHydrated();
|
||||
|
||||
return (
|
||||
<OneTimePasswordFieldContext
|
||||
scope={__scopeOneTimePasswordField}
|
||||
value={value}
|
||||
attemptSubmit={attemptSubmit}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
autoComplete={autoComplete}
|
||||
autoFocus={autoFocus}
|
||||
form={form}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
hiddenInputRef={hiddenInputRef}
|
||||
userActionRef={userActionRef}
|
||||
dispatch={dispatch}
|
||||
validationType={validationType}
|
||||
orientation={orientation}
|
||||
isHydrated={isHydrated}
|
||||
sanitizeValue={sanitizeValue}
|
||||
>
|
||||
<Collection.Provider scope={__scopeOneTimePasswordField} state={collectionState}>
|
||||
<Collection.Slot scope={__scopeOneTimePasswordField}>
|
||||
<RovingFocusGroup.Root
|
||||
asChild
|
||||
{...rovingFocusGroupScope}
|
||||
orientation={orientation}
|
||||
dir={direction}
|
||||
>
|
||||
<Primitive.Root.div
|
||||
{...domProps}
|
||||
role="group"
|
||||
ref={composedRefs}
|
||||
onPaste={composeEventHandlers(
|
||||
onPaste,
|
||||
(event: React.ClipboardEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
const pastedValue = event.clipboardData.getData('Text');
|
||||
dispatch({ type: 'PASTE', value: pastedValue });
|
||||
}
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Primitive.Root.div>
|
||||
</RovingFocusGroup.Root>
|
||||
</Collection.Slot>
|
||||
</Collection.Provider>
|
||||
</OneTimePasswordFieldContext>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* OneTimePasswordFieldHiddenInput
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
interface OneTimePasswordFieldHiddenInputProps
|
||||
extends Omit<
|
||||
React.ComponentProps<'input'>,
|
||||
| keyof 'value'
|
||||
| 'defaultValue'
|
||||
| 'type'
|
||||
| 'onChange'
|
||||
| 'readOnly'
|
||||
| 'disabled'
|
||||
| 'autoComplete'
|
||||
| 'autoFocus'
|
||||
> {}
|
||||
|
||||
const OneTimePasswordFieldHiddenInput = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
OneTimePasswordFieldHiddenInputProps
|
||||
>(function OneTimePasswordFieldHiddenInput(
|
||||
{ __scopeOneTimePasswordField, ...props }: ScopedProps<OneTimePasswordFieldHiddenInputProps>,
|
||||
forwardedRef
|
||||
) {
|
||||
const { value, hiddenInputRef, name } = useOneTimePasswordFieldContext(
|
||||
'OneTimePasswordFieldHiddenInput',
|
||||
__scopeOneTimePasswordField
|
||||
);
|
||||
const ref = useComposedRefs(hiddenInputRef, forwardedRef);
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
name={name}
|
||||
value={value.join('').trim()}
|
||||
autoComplete="off"
|
||||
autoFocus={false}
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
autoSave="off"
|
||||
spellCheck={false}
|
||||
{...props}
|
||||
type="hidden"
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* OneTimePasswordFieldInput
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
interface OneTimePasswordFieldInputProps
|
||||
extends Omit<
|
||||
Primitive.PrimitivePropsWithRef<'input'>,
|
||||
| 'value'
|
||||
| 'defaultValue'
|
||||
| 'disabled'
|
||||
| 'readOnly'
|
||||
| 'autoComplete'
|
||||
| 'autoFocus'
|
||||
| 'form'
|
||||
| 'name'
|
||||
| 'placeholder'
|
||||
| 'type'
|
||||
> {
|
||||
/**
|
||||
* Callback fired when the user input fails native HTML input validation.
|
||||
*/
|
||||
onInvalidChange?: (character: string) => void;
|
||||
/**
|
||||
* User-provided index to determine the order of the inputs. This is useful if
|
||||
* you need certain index-based attributes to be set on the initial render,
|
||||
* often to prevent flickering after hydration.
|
||||
*/
|
||||
index?: number;
|
||||
}
|
||||
|
||||
const OneTimePasswordFieldInput = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
OneTimePasswordFieldInputProps
|
||||
>(function OneTimePasswordFieldInput(
|
||||
{
|
||||
__scopeOneTimePasswordField,
|
||||
onInvalidChange,
|
||||
index: indexProp,
|
||||
...props
|
||||
}: ScopedProps<OneTimePasswordFieldInputProps>,
|
||||
forwardedRef
|
||||
) {
|
||||
// TODO: warn if these values are passed
|
||||
const {
|
||||
value: _value,
|
||||
defaultValue: _defaultValue,
|
||||
disabled: _disabled,
|
||||
readOnly: _readOnly,
|
||||
autoComplete: _autoComplete,
|
||||
autoFocus: _autoFocus,
|
||||
form: _form,
|
||||
name: _name,
|
||||
placeholder: _placeholder,
|
||||
type: _type,
|
||||
...domProps
|
||||
} = props as Primitive.PrimitivePropsWithRef<'input'>;
|
||||
|
||||
const context = useOneTimePasswordFieldContext(
|
||||
'OneTimePasswordFieldInput',
|
||||
__scopeOneTimePasswordField
|
||||
);
|
||||
const { dispatch, userActionRef, validationType, isHydrated, disabled } = context;
|
||||
const collection = useCollection(__scopeOneTimePasswordField);
|
||||
const rovingFocusGroupScope = useRovingFocusGroupScope(__scopeOneTimePasswordField);
|
||||
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const [element, setElement] = React.useState<HTMLInputElement | null>(null);
|
||||
|
||||
const index = indexProp ?? (element ? collection.indexOf(element) : -1);
|
||||
const canSetPlaceholder = indexProp != null || isHydrated;
|
||||
let placeholder: string | undefined;
|
||||
if (canSetPlaceholder && context.placeholder && context.value.length === 0) {
|
||||
// only set placeholder after hydration to prevent flickering when indices
|
||||
// are re-calculated
|
||||
placeholder = context.placeholder[index];
|
||||
}
|
||||
|
||||
const composedInputRef = useComposedRefs(forwardedRef, inputRef, setElement);
|
||||
const char = context.value[index] ?? '';
|
||||
|
||||
const keyboardActionTimeoutRef = React.useRef<number | null>(null);
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
window.clearTimeout(keyboardActionTimeoutRef.current!);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const totalValue = context.value.join('').trim();
|
||||
const lastSelectableIndex = clamp(totalValue.length, [0, collection.size - 1]);
|
||||
const isFocusable = index <= lastSelectableIndex;
|
||||
|
||||
const validation =
|
||||
validationType in INPUT_VALIDATION_MAP
|
||||
? INPUT_VALIDATION_MAP[validationType as keyof InputValidation]
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Collection.ItemSlot scope={__scopeOneTimePasswordField}>
|
||||
<RovingFocusGroup.Item
|
||||
{...rovingFocusGroupScope}
|
||||
asChild
|
||||
focusable={!context.disabled && isFocusable}
|
||||
active={index === lastSelectableIndex}
|
||||
>
|
||||
{({ hasTabStop, isCurrentTabStop }) => {
|
||||
const supportsAutoComplete = hasTabStop ? isCurrentTabStop : index === 0;
|
||||
return (
|
||||
<Primitive.Root.input
|
||||
ref={composedInputRef}
|
||||
type={context.type}
|
||||
disabled={disabled}
|
||||
aria-label={`Character ${index + 1} of ${collection.size}`}
|
||||
autoComplete={supportsAutoComplete ? context.autoComplete : 'off'}
|
||||
data-1p-ignore={supportsAutoComplete ? undefined : 'true'}
|
||||
data-lpignore={supportsAutoComplete ? undefined : 'true'}
|
||||
data-protonpass-ignore={supportsAutoComplete ? undefined : 'true'}
|
||||
data-bwignore={supportsAutoComplete ? undefined : 'true'}
|
||||
inputMode={validation?.inputMode}
|
||||
maxLength={1}
|
||||
pattern={validation?.pattern}
|
||||
readOnly={context.readOnly}
|
||||
value={char}
|
||||
placeholder={placeholder}
|
||||
data-radix-otp-input=""
|
||||
data-radix-index={index}
|
||||
{...domProps}
|
||||
onFocus={composeEventHandlers(props.onFocus, (event) => {
|
||||
event.currentTarget.select();
|
||||
})}
|
||||
onCut={composeEventHandlers(props.onCut, (event) => {
|
||||
const currentValue = event.currentTarget.value;
|
||||
if (currentValue !== '') {
|
||||
// In this case the value will be cleared, but we don't want to
|
||||
// set it directly because the user may want to prevent default
|
||||
// behavior in the onChange handler. The userActionRef will
|
||||
// is set temporarily so the change handler can behave correctly
|
||||
// in response to the action.
|
||||
userActionRef.current = {
|
||||
type: 'cut',
|
||||
};
|
||||
// Set a short timeout to clear the action tracker after the change
|
||||
// handler has had time to complete.
|
||||
keyboardActionTimeoutRef.current = window.setTimeout(() => {
|
||||
userActionRef.current = null;
|
||||
}, 10);
|
||||
}
|
||||
})}
|
||||
onInput={composeEventHandlers(props.onInput, (event) => {
|
||||
const value = event.currentTarget.value;
|
||||
if (value.length > 1) {
|
||||
// Password managers may try to insert the code into a single
|
||||
// input, in which case form validation will fail to prevent
|
||||
// additional input. Handle this the same as if a user were
|
||||
// pasting a value.
|
||||
event.preventDefault();
|
||||
dispatch({ type: 'PASTE', value });
|
||||
}
|
||||
})}
|
||||
onChange={composeEventHandlers(props.onChange, (event) => {
|
||||
const value = event.target.value;
|
||||
event.preventDefault();
|
||||
const action = userActionRef.current;
|
||||
userActionRef.current = null;
|
||||
|
||||
if (action) {
|
||||
switch (action.type) {
|
||||
case 'cut':
|
||||
// TODO: do we want to assume the user wantt to clear the
|
||||
// entire value here and copy the code to the clipboard instead
|
||||
// of just the value of the given input?
|
||||
dispatch({ type: 'CLEAR_CHAR', index, reason: 'Cut' });
|
||||
return;
|
||||
case 'keydown': {
|
||||
if (action.key === 'Char') {
|
||||
// update resulting from a keydown event that set a value
|
||||
// directly. Ignore.
|
||||
return;
|
||||
}
|
||||
|
||||
const isClearing =
|
||||
action.key === 'Backspace' && (action.metaKey || action.ctrlKey);
|
||||
if (action.key === 'Clear' || isClearing) {
|
||||
dispatch({ type: 'CLEAR', reason: 'Backspace' });
|
||||
} else {
|
||||
dispatch({ type: 'CLEAR_CHAR', index, reason: action.key });
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Only update the value if it matches the input pattern
|
||||
if (event.target.validity.valid) {
|
||||
if (value === '') {
|
||||
let reason: 'Backspace' | 'Delete' | 'Cut' = 'Backspace';
|
||||
if (isInputEvent(event.nativeEvent)) {
|
||||
const inputType = event.nativeEvent.inputType;
|
||||
if (inputType === 'deleteContentBackward') {
|
||||
reason = 'Backspace';
|
||||
} else if (inputType === 'deleteByCut') {
|
||||
reason = 'Cut';
|
||||
}
|
||||
}
|
||||
dispatch({ type: 'CLEAR_CHAR', index, reason });
|
||||
} else {
|
||||
dispatch({ type: 'SET_CHAR', char: value, index, event });
|
||||
}
|
||||
} else {
|
||||
const element = event.target;
|
||||
onInvalidChange?.(element.value);
|
||||
requestAnimationFrame(() => {
|
||||
if (element.ownerDocument.activeElement === element) {
|
||||
element.select();
|
||||
}
|
||||
});
|
||||
}
|
||||
})}
|
||||
onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {
|
||||
switch (event.key) {
|
||||
case 'Clear':
|
||||
case 'Delete':
|
||||
case 'Backspace': {
|
||||
const currentValue = event.currentTarget.value;
|
||||
// if current value is empty, no change event will fire
|
||||
if (currentValue === '') {
|
||||
// if the user presses delete when there is no value, noop
|
||||
if (event.key === 'Delete') return;
|
||||
|
||||
const isClearing = event.key === 'Clear' || event.metaKey || event.ctrlKey;
|
||||
if (isClearing) {
|
||||
dispatch({ type: 'CLEAR', reason: 'Backspace' });
|
||||
} else {
|
||||
const element = event.currentTarget;
|
||||
requestAnimationFrame(() => {
|
||||
focusInput(collection.from(element, -1)?.element);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// In this case the value will be cleared, but we don't want
|
||||
// to set it directly because the user may want to prevent
|
||||
// default behavior in the onChange handler. The userActionRef
|
||||
// will is set temporarily so the change handler can behave
|
||||
// correctly in response to the key vs. clearing the value by
|
||||
// setting state externally.
|
||||
userActionRef.current = {
|
||||
type: 'keydown',
|
||||
key: event.key,
|
||||
metaKey: event.metaKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
};
|
||||
// Set a short timeout to clear the action tracker after the change
|
||||
// handler has had time to complete.
|
||||
keyboardActionTimeoutRef.current = window.setTimeout(() => {
|
||||
userActionRef.current = null;
|
||||
}, 10);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
case 'Enter': {
|
||||
event.preventDefault();
|
||||
context.attemptSubmit();
|
||||
return;
|
||||
}
|
||||
case 'ArrowDown':
|
||||
case 'ArrowUp': {
|
||||
if (context.orientation === 'horizontal') {
|
||||
// in horizontal orientation, the up/down will de-select the
|
||||
// input instead of moving focus
|
||||
event.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// TODO: Handle left/right arrow keys in vertical writing mode
|
||||
default: {
|
||||
if (event.currentTarget.value === event.key) {
|
||||
// if current value is same as the key press, no change event
|
||||
// will fire. Focus the next input.
|
||||
const element = event.currentTarget;
|
||||
event.preventDefault();
|
||||
focusInput(collection.from(element, 1)?.element);
|
||||
return;
|
||||
} else if (
|
||||
// input already has a value, but...
|
||||
event.currentTarget.value &&
|
||||
// the value is not selected
|
||||
!(
|
||||
event.currentTarget.selectionStart === 0 &&
|
||||
event.currentTarget.selectionEnd != null &&
|
||||
event.currentTarget.selectionEnd > 0
|
||||
)
|
||||
) {
|
||||
const attemptedValue = event.key;
|
||||
if (event.key.length > 1 || event.key === ' ') {
|
||||
// not a character; do nothing
|
||||
return;
|
||||
} else {
|
||||
// user is attempting to enter a character, but the input
|
||||
// will not update by default since it's limited to a single
|
||||
// character.
|
||||
const nextInput = collection.from(event.currentTarget, 1)?.element;
|
||||
const lastInput = collection.at(-1)?.element;
|
||||
if (nextInput !== lastInput && event.currentTarget !== lastInput) {
|
||||
// if selection is before the value, set the value of the
|
||||
// current input. Otherwise set the value of the next
|
||||
// input.
|
||||
if (event.currentTarget.selectionStart === 0) {
|
||||
dispatch({ type: 'SET_CHAR', char: attemptedValue, index, event });
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'SET_CHAR',
|
||||
char: attemptedValue,
|
||||
index: index + 1,
|
||||
event,
|
||||
});
|
||||
}
|
||||
|
||||
userActionRef.current = {
|
||||
type: 'keydown',
|
||||
key: 'Char',
|
||||
metaKey: event.metaKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
};
|
||||
keyboardActionTimeoutRef.current = window.setTimeout(() => {
|
||||
userActionRef.current = null;
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})}
|
||||
onPointerDown={composeEventHandlers(props.onPointerDown, (event) => {
|
||||
event.preventDefault();
|
||||
const indexToFocus = Math.min(index, lastSelectableIndex);
|
||||
const element = collection.at(indexToFocus)?.element;
|
||||
focusInput(element);
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</RovingFocusGroup.Item>
|
||||
</Collection.ItemSlot>
|
||||
);
|
||||
});
|
||||
|
||||
export {
|
||||
OneTimePasswordField,
|
||||
OneTimePasswordFieldInput,
|
||||
OneTimePasswordFieldHiddenInput,
|
||||
//
|
||||
OneTimePasswordField as Root,
|
||||
OneTimePasswordFieldInput as Input,
|
||||
OneTimePasswordFieldHiddenInput as HiddenInput,
|
||||
};
|
||||
export type {
|
||||
OneTimePasswordFieldProps,
|
||||
OneTimePasswordFieldInputProps,
|
||||
OneTimePasswordFieldHiddenInputProps,
|
||||
InputValidationType,
|
||||
};
|
||||
|
||||
/* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
function isFormElement(element: Element | null | undefined): element is HTMLFormElement {
|
||||
return element?.tagName === 'FORM';
|
||||
}
|
||||
|
||||
function removeWhitespace(value: string) {
|
||||
return value.replace(/\s/g, '');
|
||||
}
|
||||
|
||||
function focusInput(element: HTMLInputElement | null | undefined) {
|
||||
if (!element) return;
|
||||
if (element.ownerDocument.activeElement === element) {
|
||||
// if the element is already focused, select the value in the next
|
||||
// animation frame
|
||||
window.requestAnimationFrame(() => {
|
||||
element.select?.();
|
||||
});
|
||||
} else {
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function isInputEvent(event: Event): event is InputEvent {
|
||||
return event.type === 'input';
|
||||
}
|
||||
|
||||
type InputType = 'password' | 'text';
|
||||
type AutoComplete = 'off' | 'one-time-code';
|
||||
type KeyboardActionDetails =
|
||||
| {
|
||||
type: 'keydown';
|
||||
key: 'Backspace' | 'Delete' | 'Clear' | 'Char';
|
||||
metaKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
}
|
||||
| { type: 'cut' };
|
||||
|
||||
type UpdateAction =
|
||||
| {
|
||||
type: 'SET_CHAR';
|
||||
char: string;
|
||||
index: number;
|
||||
event: React.KeyboardEvent | React.ChangeEvent;
|
||||
}
|
||||
| { type: 'CLEAR_CHAR'; index: number; reason: 'Backspace' | 'Delete' | 'Cut' }
|
||||
| { type: 'CLEAR'; reason: 'Reset' | 'Backspace' | 'Delete' | 'Clear' }
|
||||
| { type: 'PASTE'; value: string };
|
||||
type Dispatcher = React.Dispatch<UpdateAction>;
|
||||
type InputValidation = Record<
|
||||
InputValidationType,
|
||||
{
|
||||
type: InputValidationType;
|
||||
regexp: RegExp;
|
||||
pattern: string;
|
||||
inputMode: 'text' | 'numeric';
|
||||
} | null
|
||||
>;
|
||||
Reference in New Issue
Block a user