@passui/react 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import { type InputHTMLAttributes } from "react";
|
|
1
|
+
import { type InputHTMLAttributes, type Ref } from "react";
|
|
2
2
|
export type OneTimeCodeStatus = "idle" | "verifying" | "success" | "error";
|
|
3
|
+
export type OneTimeCodeFormat = "numeric" | "alphanumeric";
|
|
3
4
|
export interface OneTimeCodeProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "onChange" | "type" | "maxLength" | "size"> {
|
|
4
5
|
label: string;
|
|
5
6
|
value: string;
|
|
6
7
|
onChange: (value: string) => void;
|
|
7
8
|
/** Called when a complete code is entered, including paste and autofill. */
|
|
8
9
|
onComplete?: (value: string) => void;
|
|
9
|
-
/** Number of
|
|
10
|
+
/** Number of characters, 3–8. @default 6 */
|
|
10
11
|
length?: number;
|
|
12
|
+
/** Accepted characters. Alphanumeric codes use uppercase ASCII letters and digits. @default "numeric" */
|
|
13
|
+
format?: OneTimeCodeFormat;
|
|
14
|
+
/** Ref to the single native input, for focus and selection. */
|
|
15
|
+
ref?: Ref<HTMLInputElement>;
|
|
11
16
|
status?: OneTimeCodeStatus;
|
|
12
17
|
/** Localized verification feedback announced without reading decorative cells. */
|
|
13
18
|
statusLabel?: string;
|
|
14
19
|
}
|
|
15
|
-
/** Normalize common numeric keyboards
|
|
16
|
-
export declare function normalizeCode(value: string, length: number): string;
|
|
20
|
+
/** Normalize common numeric keyboards and optional ASCII letters in one native field. */
|
|
21
|
+
export declare function normalizeCode(value: string, length: number, format?: OneTimeCodeFormat): string;
|
|
17
22
|
/** Six cells, one field: paste, autofill, selection and Backspace stay native. */
|
|
18
|
-
export declare function OneTimeCode({ label, value, onChange, onComplete, length, status, statusLabel, className, disabled, id: suppliedId, ...props }: OneTimeCodeProps): import("react").JSX.Element;
|
|
23
|
+
export declare function OneTimeCode({ label, value, onChange, onComplete, length, format, ref, status, statusLabel, className, disabled, id: suppliedId, ...props }: OneTimeCodeProps): import("react").JSX.Element;
|
|
@@ -5,6 +5,8 @@ export interface SheetProps {
|
|
|
5
5
|
open: boolean;
|
|
6
6
|
/** Called on Escape or an outside press. */
|
|
7
7
|
onClose: () => void;
|
|
8
|
+
/** Allow tapping the backdrop to close. Escape still calls onClose. @default true */
|
|
9
|
+
dismissOnOutsidePress?: boolean;
|
|
8
10
|
/** Read out by screen readers as the sheet's name. */
|
|
9
11
|
label: string;
|
|
10
12
|
/** ID of a visible heading to use instead of label for the accessible name. */
|
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type CSSProperties,
|
|
3
|
+
type InputHTMLAttributes,
|
|
4
|
+
type Ref,
|
|
5
|
+
useEffect,
|
|
6
|
+
useId,
|
|
7
|
+
useRef,
|
|
8
|
+
} from "react"
|
|
2
9
|
import { CheckIcon } from "../../icons/icons"
|
|
3
10
|
import { cn } from "../../lib/class-names"
|
|
4
11
|
|
|
5
12
|
export type OneTimeCodeStatus = "idle" | "verifying" | "success" | "error"
|
|
13
|
+
export type OneTimeCodeFormat = "numeric" | "alphanumeric"
|
|
6
14
|
export interface OneTimeCodeProps
|
|
7
15
|
extends Omit<
|
|
8
16
|
InputHTMLAttributes<HTMLInputElement>,
|
|
@@ -13,21 +21,29 @@ export interface OneTimeCodeProps
|
|
|
13
21
|
onChange: (value: string) => void
|
|
14
22
|
/** Called when a complete code is entered, including paste and autofill. */
|
|
15
23
|
onComplete?: (value: string) => void
|
|
16
|
-
/** Number of
|
|
24
|
+
/** Number of characters, 3–8. @default 6 */
|
|
17
25
|
length?: number
|
|
26
|
+
/** Accepted characters. Alphanumeric codes use uppercase ASCII letters and digits. @default "numeric" */
|
|
27
|
+
format?: OneTimeCodeFormat
|
|
28
|
+
/** Ref to the single native input, for focus and selection. */
|
|
29
|
+
ref?: Ref<HTMLInputElement>
|
|
18
30
|
status?: OneTimeCodeStatus
|
|
19
31
|
/** Localized verification feedback announced without reading decorative cells. */
|
|
20
32
|
statusLabel?: string
|
|
21
33
|
}
|
|
22
34
|
|
|
23
|
-
/** Normalize common numeric keyboards
|
|
24
|
-
export function normalizeCode(
|
|
35
|
+
/** Normalize common numeric keyboards and optional ASCII letters in one native field. */
|
|
36
|
+
export function normalizeCode(
|
|
37
|
+
value: string,
|
|
38
|
+
length: number,
|
|
39
|
+
format: OneTimeCodeFormat = "numeric"
|
|
40
|
+
) {
|
|
25
41
|
return Array.from(value, (digit) => {
|
|
26
42
|
const point = digit.codePointAt(0) ?? 0
|
|
27
43
|
for (const base of [0x30, 0x660, 0x6f0, 0xff10]) {
|
|
28
44
|
if (point >= base && point < base + 10) return String(point - base)
|
|
29
45
|
}
|
|
30
|
-
return ""
|
|
46
|
+
return format === "alphanumeric" && /^[a-z]$/i.test(digit) ? digit.toUpperCase() : ""
|
|
31
47
|
})
|
|
32
48
|
.join("")
|
|
33
49
|
.slice(0, length)
|
|
@@ -40,6 +56,8 @@ export function OneTimeCode({
|
|
|
40
56
|
onChange,
|
|
41
57
|
onComplete,
|
|
42
58
|
length = 6,
|
|
59
|
+
format = "numeric",
|
|
60
|
+
ref,
|
|
43
61
|
status = "idle",
|
|
44
62
|
statusLabel,
|
|
45
63
|
className,
|
|
@@ -52,13 +70,14 @@ export function OneTimeCode({
|
|
|
52
70
|
const filter = `otp-goo-${generated.replace(/:/g, "")}`
|
|
53
71
|
const input = useRef<HTMLInputElement>(null)
|
|
54
72
|
const count = Number.isFinite(length) ? Math.max(3, Math.min(8, Math.round(length))) : 6
|
|
55
|
-
const code = normalizeCode(value, count)
|
|
73
|
+
const code = normalizeCode(value, count, format)
|
|
56
74
|
const locked = disabled || props.readOnly || status === "verifying" || status === "success"
|
|
57
75
|
useEffect(() => {
|
|
58
76
|
if (status === "error") input.current?.select()
|
|
59
77
|
}, [status])
|
|
60
78
|
const update = (next: string) => {
|
|
61
|
-
|
|
79
|
+
if (locked) return
|
|
80
|
+
const normalized = normalizeCode(next, count, format)
|
|
62
81
|
onChange(normalized)
|
|
63
82
|
if (normalized.length === count && normalized !== code) onComplete?.(normalized)
|
|
64
83
|
}
|
|
@@ -123,26 +142,37 @@ export function OneTimeCode({
|
|
|
123
142
|
<CheckIcon aria-hidden="true" className="passui-otp-check" size={24} />
|
|
124
143
|
<input
|
|
125
144
|
{...props}
|
|
126
|
-
ref={
|
|
145
|
+
ref={(node) => {
|
|
146
|
+
input.current = node
|
|
147
|
+
if (typeof ref === "function") return ref(node)
|
|
148
|
+
if (ref) ref.current = node
|
|
149
|
+
}}
|
|
127
150
|
id={id}
|
|
128
151
|
type="text"
|
|
129
|
-
inputMode="numeric"
|
|
130
|
-
autoComplete="one-time-code"
|
|
131
|
-
|
|
152
|
+
inputMode={format === "numeric" ? "numeric" : "text"}
|
|
153
|
+
autoComplete={props.autoComplete ?? "one-time-code"}
|
|
154
|
+
autoCapitalize={format === "alphanumeric" ? "characters" : "off"}
|
|
155
|
+
spellCheck={false}
|
|
156
|
+
pattern={format === "numeric" ? "[0-9]*" : "[A-Za-z0-9]*"}
|
|
132
157
|
maxLength={count}
|
|
158
|
+
minLength={count}
|
|
133
159
|
aria-label={label}
|
|
134
160
|
value={code}
|
|
135
161
|
disabled={disabled}
|
|
136
162
|
readOnly={locked}
|
|
137
163
|
aria-busy={status === "verifying" || undefined}
|
|
138
164
|
aria-invalid={status === "error" || props["aria-invalid"]}
|
|
139
|
-
aria-describedby={
|
|
165
|
+
aria-describedby={
|
|
166
|
+
[props["aria-describedby"], statusLabel && `${id}-status`]
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.join(" ") || undefined
|
|
169
|
+
}
|
|
140
170
|
onChange={(event) => update(event.target.value)}
|
|
141
171
|
onPaste={(event) => {
|
|
142
172
|
props.onPaste?.(event)
|
|
143
173
|
if (event.defaultPrevented || locked) return
|
|
144
174
|
const pasted = event.clipboardData.getData("text")
|
|
145
|
-
const normalized = normalizeCode(pasted, count)
|
|
175
|
+
const normalized = normalizeCode(pasted, count, format)
|
|
146
176
|
if (normalized && normalized !== pasted) {
|
|
147
177
|
event.preventDefault()
|
|
148
178
|
update(normalized)
|
|
@@ -13,6 +13,8 @@ export interface SheetProps {
|
|
|
13
13
|
open: boolean
|
|
14
14
|
/** Called on Escape or an outside press. */
|
|
15
15
|
onClose: () => void
|
|
16
|
+
/** Allow tapping the backdrop to close. Escape still calls onClose. @default true */
|
|
17
|
+
dismissOnOutsidePress?: boolean
|
|
16
18
|
/** Read out by screen readers as the sheet's name. */
|
|
17
19
|
label: string
|
|
18
20
|
/** ID of a visible heading to use instead of label for the accessible name. */
|
|
@@ -41,6 +43,7 @@ export function Sheet({ open, ...props }: SheetProps) {
|
|
|
41
43
|
|
|
42
44
|
function OpenSheet({
|
|
43
45
|
onClose,
|
|
46
|
+
dismissOnOutsidePress = true,
|
|
44
47
|
label,
|
|
45
48
|
labelledBy,
|
|
46
49
|
placement = "adaptive",
|
|
@@ -77,7 +80,7 @@ function OpenSheet({
|
|
|
77
80
|
initial={{ opacity: 0 }}
|
|
78
81
|
animate={{ opacity: 1 }}
|
|
79
82
|
exit={{ opacity: 0 }}
|
|
80
|
-
onClick={onClose}
|
|
83
|
+
onClick={dismissOnOutsidePress ? onClose : undefined}
|
|
81
84
|
/>
|
|
82
85
|
<motion.div
|
|
83
86
|
ref={panel}
|