@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 digits, 3–8. @default 6 */
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 without splitting the browser's input into cells. */
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@passui/react",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Passui: native React controls, design tokens, presentation and appearance.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,8 +1,16 @@
1
- import { type CSSProperties, type InputHTMLAttributes, useEffect, useId, useRef } from "react"
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 digits, 3–8. @default 6 */
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 without splitting the browser's input into cells. */
24
- export function normalizeCode(value: string, length: number) {
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
- const normalized = normalizeCode(next, count)
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={input}
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
- pattern="[0-9]*"
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={statusLabel ? `${id}-status` : props["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}