@passui/react 0.1.20 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/src/components/context-menu/menu-reveal.d.ts +9 -0
- package/dist/types/src/components/one-time-code/one-time-code.d.ts +10 -5
- package/dist/types/src/components/sheet/sheet.d.ts +2 -0
- package/dist/types/src/icons/icons.d.ts +4 -0
- package/package.json +1 -1
- package/src/components/context-menu/context-menu-host.tsx +9 -4
- package/src/components/context-menu/menu-reveal.ts +5 -0
- package/src/components/locale-switcher/locale-switcher.tsx +106 -65
- package/src/components/one-time-code/one-time-code.tsx +43 -13
- package/src/components/popover/anchored-surface.tsx +23 -6
- package/src/components/popover/popover.tsx +13 -4
- package/src/components/sheet/sheet.tsx +4 -1
- package/src/icons/icons.tsx +2 -0
|
@@ -4,6 +4,15 @@ export type MenuRect = {
|
|
|
4
4
|
width: number;
|
|
5
5
|
height: number;
|
|
6
6
|
};
|
|
7
|
+
/** Return toward the source while handing its material back to the real trigger. */
|
|
8
|
+
export declare function menuSurfaceExit(source: ReturnType<typeof menuSurfaceReveal>): {
|
|
9
|
+
opacity: number;
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
borderRadius: number;
|
|
15
|
+
};
|
|
7
16
|
/** Animate the material's bounds independently so menu text never stretches. */
|
|
8
17
|
export declare function menuSurfaceReveal(source: MenuRect | undefined, target: MenuRect & {
|
|
9
18
|
radius?: number;
|
|
@@ -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. */
|
|
@@ -152,6 +152,10 @@ export declare const LanguageIcon: {
|
|
|
152
152
|
({ size, strokeWidth, ...props }: IconProps): import("react").JSX.Element;
|
|
153
153
|
displayName: string;
|
|
154
154
|
};
|
|
155
|
+
export declare const LoadingIcon: {
|
|
156
|
+
({ size, strokeWidth, ...props }: IconProps): import("react").JSX.Element;
|
|
157
|
+
displayName: string;
|
|
158
|
+
};
|
|
155
159
|
export declare const SignOutIcon: {
|
|
156
160
|
({ size, strokeWidth, ...props }: IconProps): import("react").JSX.Element;
|
|
157
161
|
displayName: string;
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@ import { spring } from "../../motion/transitions"
|
|
|
9
9
|
import { concealMenuSource } from "./conceal-menu-source"
|
|
10
10
|
import { findMenuTypeaheadIndex, isMenuNavigationKey, nextMenuIndex } from "./menu-navigation"
|
|
11
11
|
import { menuPosition } from "./menu-position"
|
|
12
|
-
import { menuSurfaceReveal } from "./menu-reveal"
|
|
12
|
+
import { menuSurfaceExit, menuSurfaceReveal } from "./menu-reveal"
|
|
13
13
|
import {
|
|
14
14
|
closeContextMenu,
|
|
15
15
|
getContextMenu,
|
|
@@ -44,6 +44,7 @@ function Menu({ menu }: { menu: OpenMenu }) {
|
|
|
44
44
|
|
|
45
45
|
useLayoutEffect(() => {
|
|
46
46
|
if (
|
|
47
|
+
!present ||
|
|
47
48
|
(!menu.source && !menu.toolbar) ||
|
|
48
49
|
!position.ready ||
|
|
49
50
|
!(menu.anchor instanceof HTMLElement)
|
|
@@ -64,7 +65,7 @@ function Menu({ menu }: { menu: OpenMenu }) {
|
|
|
64
65
|
)
|
|
65
66
|
menu.anchor.focus({ preventScroll: true })
|
|
66
67
|
}
|
|
67
|
-
}, [menu, position.ready])
|
|
68
|
+
}, [menu, position.ready, present])
|
|
68
69
|
|
|
69
70
|
useLayoutEffect(() => {
|
|
70
71
|
const surface = ref.current
|
|
@@ -283,8 +284,12 @@ function Menu({ menu }: { menu: OpenMenu }) {
|
|
|
283
284
|
}
|
|
284
285
|
: undefined
|
|
285
286
|
}
|
|
286
|
-
exit={reducedMotion ? { opacity: 0 } : source}
|
|
287
|
-
transition={
|
|
287
|
+
exit={reducedMotion ? { opacity: 0 } : menuSurfaceExit(source)}
|
|
288
|
+
transition={
|
|
289
|
+
reducedMotion
|
|
290
|
+
? { duration: 0 }
|
|
291
|
+
: { ...spring.smooth, opacity: { duration: 0.16 } }
|
|
292
|
+
}
|
|
288
293
|
/>
|
|
289
294
|
)}
|
|
290
295
|
<motion.div
|
|
@@ -5,6 +5,11 @@ export type MenuRect = {
|
|
|
5
5
|
height: number
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/** Return toward the source while handing its material back to the real trigger. */
|
|
9
|
+
export function menuSurfaceExit(source: ReturnType<typeof menuSurfaceReveal>) {
|
|
10
|
+
return { ...source, opacity: 0 }
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
/** Animate the material's bounds independently so menu text never stretches. */
|
|
9
14
|
export function menuSurfaceReveal(
|
|
10
15
|
source: MenuRect | undefined,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { getNativeLanguageName } from "../../lib/language"
|
|
2
2
|
import { cn } from "../../lib/class-names"
|
|
3
|
-
import {
|
|
3
|
+
import { motion, useDragControls, useMotionValue } from "motion/react"
|
|
4
4
|
import { useEffect, useRef, useState } from "react"
|
|
5
|
+
import { useDismiss } from "../../hooks/use-dismiss"
|
|
6
|
+
import { CheckIcon, GlobeIcon } from "../../icons/icons"
|
|
7
|
+
import { Popover } from "../popover/popover"
|
|
5
8
|
|
|
6
9
|
type Side = "left" | "right"
|
|
7
10
|
type Edge = "top" | "bottom"
|
|
@@ -66,6 +69,7 @@ export function LocaleSwitcher<L extends string>({
|
|
|
66
69
|
label,
|
|
67
70
|
}: LocaleSwitcherProps<L>) {
|
|
68
71
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
72
|
+
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
69
73
|
const dragControls = useDragControls()
|
|
70
74
|
const x = useMotionValue(0)
|
|
71
75
|
const y = useMotionValue(0)
|
|
@@ -82,15 +86,12 @@ export function LocaleSwitcher<L extends string>({
|
|
|
82
86
|
if (stored) setAnchor(stored)
|
|
83
87
|
}, [storageKey])
|
|
84
88
|
|
|
89
|
+
useDismiss(containerRef, open, () => setOpen(false))
|
|
85
90
|
useEffect(() => {
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
document.addEventListener("pointerdown", handlePointerDown)
|
|
93
|
-
return () => document.removeEventListener("pointerdown", handlePointerDown)
|
|
91
|
+
if (open)
|
|
92
|
+
containerRef.current
|
|
93
|
+
?.querySelector<HTMLButtonElement>('[role="menuitemradio"][aria-checked="true"]')
|
|
94
|
+
?.focus()
|
|
94
95
|
}, [open])
|
|
95
96
|
|
|
96
97
|
function commitDraggedPosition() {
|
|
@@ -129,74 +130,114 @@ export function LocaleSwitcher<L extends string>({
|
|
|
129
130
|
...(anchor.edge === "top" ? { top: anchor.y } : { bottom: anchor.y }),
|
|
130
131
|
}}
|
|
131
132
|
onDragEnd={commitDraggedPosition}
|
|
132
|
-
|
|
133
|
+
onDragStart={() => setOpen(false)}
|
|
134
|
+
onKeyDown={(event) => {
|
|
135
|
+
if (!open) return
|
|
136
|
+
if (event.key === "Escape") {
|
|
137
|
+
event.preventDefault()
|
|
138
|
+
setOpen(false)
|
|
139
|
+
triggerRef.current?.focus()
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
if (event.key === "Tab") {
|
|
143
|
+
setOpen(false)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return
|
|
147
|
+
const options = Array.from(
|
|
148
|
+
containerRef.current?.querySelectorAll<HTMLButtonElement>(
|
|
149
|
+
'[role="menuitemradio"]'
|
|
150
|
+
) ?? []
|
|
151
|
+
)
|
|
152
|
+
if (!options.length) return
|
|
153
|
+
event.preventDefault()
|
|
154
|
+
const current = options.indexOf(document.activeElement as HTMLButtonElement)
|
|
155
|
+
const next =
|
|
156
|
+
event.key === "Home"
|
|
157
|
+
? 0
|
|
158
|
+
: event.key === "End"
|
|
159
|
+
? options.length - 1
|
|
160
|
+
: (current + (event.key === "ArrowDown" ? 1 : -1) + options.length) %
|
|
161
|
+
options.length
|
|
162
|
+
options[next]?.focus()
|
|
163
|
+
}}
|
|
133
164
|
className="z-60"
|
|
134
165
|
>
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
<button
|
|
167
|
+
ref={triggerRef}
|
|
168
|
+
type="button"
|
|
169
|
+
aria-label={label}
|
|
170
|
+
aria-haspopup="menu"
|
|
171
|
+
aria-expanded={open}
|
|
172
|
+
onPointerDown={(e) => dragControls.start(e)}
|
|
173
|
+
onClick={() => setOpen((prev) => !prev)}
|
|
174
|
+
onKeyDown={(event) => {
|
|
175
|
+
if (!open && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
176
|
+
event.preventDefault()
|
|
177
|
+
setOpen(true)
|
|
178
|
+
}
|
|
141
179
|
}}
|
|
180
|
+
className="glass-menu flex h-12 min-w-20 touch-none cursor-grab items-center justify-center gap-2 rounded-full px-4 text-caption font-semibold text-label active:cursor-grabbing"
|
|
181
|
+
>
|
|
182
|
+
<GlobeIcon aria-hidden="true" className="size-5" />
|
|
183
|
+
{locale.toUpperCase()}
|
|
184
|
+
</button>
|
|
185
|
+
<Popover
|
|
186
|
+
open={open}
|
|
187
|
+
anchorRef={triggerRef}
|
|
188
|
+
origin={anchor.side === "right" ? "top-right" : "top-left"}
|
|
189
|
+
role="menu"
|
|
190
|
+
aria-label={label}
|
|
142
191
|
className={cn(
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"flex"
|
|
146
|
-
growsUp ? "flex-col-reverse" : "flex-col",
|
|
147
|
-
open ? "w-40 gap-px p-1" : "size-10"
|
|
192
|
+
"top-0 flex w-52 max-w-[calc(100vw-2.5rem)] gap-2 p-2",
|
|
193
|
+
anchor.side === "right" ? "right-0" : "left-0",
|
|
194
|
+
growsUp ? "flex-col-reverse" : "flex-col"
|
|
148
195
|
)}
|
|
149
196
|
>
|
|
150
197
|
<button
|
|
151
198
|
type="button"
|
|
152
199
|
aria-label={label}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
"active:cursor-grabbing text-xs font-semibold",
|
|
159
|
-
open ? "h-9 w-full rounded-lg hover:bg-accent" : "size-10 rounded-2xl"
|
|
160
|
-
)}
|
|
200
|
+
onClick={() => {
|
|
201
|
+
setOpen(false)
|
|
202
|
+
triggerRef.current?.focus()
|
|
203
|
+
}}
|
|
204
|
+
className="flex min-h-12 w-full items-center justify-center gap-2 rounded-4xl bg-fill/50 text-caption font-semibold text-label"
|
|
161
205
|
>
|
|
206
|
+
<GlobeIcon aria-hidden="true" className="size-5" />
|
|
162
207
|
{locale.toUpperCase()}
|
|
163
208
|
</button>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
</motion.div>
|
|
197
|
-
)}
|
|
198
|
-
</AnimatePresence>
|
|
199
|
-
</motion.div>
|
|
209
|
+
<div className="flex flex-col gap-1">
|
|
210
|
+
{langs.map((item) => {
|
|
211
|
+
const active = item.value === locale
|
|
212
|
+
return (
|
|
213
|
+
<button
|
|
214
|
+
key={item.value}
|
|
215
|
+
type="button"
|
|
216
|
+
role="menuitemradio"
|
|
217
|
+
aria-checked={active}
|
|
218
|
+
tabIndex={-1}
|
|
219
|
+
onClick={() => {
|
|
220
|
+
setOpen(false)
|
|
221
|
+
triggerRef.current?.focus()
|
|
222
|
+
if (!active) {
|
|
223
|
+
onSelect(item.value)
|
|
224
|
+
}
|
|
225
|
+
}}
|
|
226
|
+
className={cn(
|
|
227
|
+
"flex min-h-11 items-center justify-between gap-3 rounded-4xl px-3.5 py-2.5 text-left text-footnote text-label outline-none",
|
|
228
|
+
"hover:bg-fill/65 focus-visible:bg-fill/65 transition-colors",
|
|
229
|
+
active && "bg-accent/12 font-medium"
|
|
230
|
+
)}
|
|
231
|
+
>
|
|
232
|
+
{item.label}
|
|
233
|
+
{active && (
|
|
234
|
+
<CheckIcon aria-hidden="true" className="size-4 shrink-0" />
|
|
235
|
+
)}
|
|
236
|
+
</button>
|
|
237
|
+
)
|
|
238
|
+
})}
|
|
239
|
+
</div>
|
|
240
|
+
</Popover>
|
|
200
241
|
</motion.div>
|
|
201
242
|
)
|
|
202
243
|
}
|
|
@@ -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)
|
|
@@ -40,9 +40,12 @@ export const AnchoredSurface = forwardRef<HTMLDivElement, AnchoredSurfaceProps>(
|
|
|
40
40
|
const reducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)")
|
|
41
41
|
const material = useRef<HTMLDivElement>(null)
|
|
42
42
|
const content = useRef<HTMLDivElement>(null)
|
|
43
|
-
const animated = useRef<{
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
const animated = useRef<{
|
|
44
|
+
values: number[]
|
|
45
|
+
velocities: number[]
|
|
46
|
+
opacity: number
|
|
47
|
+
materialOpacity: number
|
|
48
|
+
} | null>(null)
|
|
46
49
|
const placement = useAnchoredPlacement(anchor, width, needed, true)
|
|
47
50
|
const [geometry, setGeometry] = useState<{
|
|
48
51
|
source: ReturnType<typeof menuSurfaceReveal>
|
|
@@ -82,12 +85,16 @@ export const AnchoredSurface = forwardRef<HTMLDivElement, AnchoredSurfaceProps>(
|
|
|
82
85
|
useLayoutEffect(() => {
|
|
83
86
|
if (!root.current) return
|
|
84
87
|
const unregister = registerFloatingLayer(root.current, anchor)
|
|
88
|
+
return unregister
|
|
89
|
+
}, [anchor])
|
|
90
|
+
|
|
91
|
+
useLayoutEffect(() => {
|
|
92
|
+
if (!present) return
|
|
85
93
|
const restore = concealMenuSource(anchor, false)
|
|
86
94
|
return () => {
|
|
87
|
-
unregister()
|
|
88
95
|
restore()
|
|
89
96
|
}
|
|
90
|
-
}, [anchor])
|
|
97
|
+
}, [anchor, present])
|
|
91
98
|
|
|
92
99
|
useLayoutEffect(() => {
|
|
93
100
|
if (!geometry || !material.current || !content.current) {
|
|
@@ -110,6 +117,7 @@ export const AnchoredSurface = forwardRef<HTMLDivElement, AnchoredSurfaceProps>(
|
|
|
110
117
|
values: [...source],
|
|
111
118
|
velocities: source.map(() => 0),
|
|
112
119
|
opacity: 0,
|
|
120
|
+
materialOpacity: 1,
|
|
113
121
|
}
|
|
114
122
|
animated.current = state
|
|
115
123
|
let frame = 0,
|
|
@@ -120,6 +128,7 @@ export const AnchoredSurface = forwardRef<HTMLDivElement, AnchoredSurfaceProps>(
|
|
|
120
128
|
pane.style.width = `${Math.max(0, width)}px`
|
|
121
129
|
pane.style.height = `${Math.max(0, height)}px`
|
|
122
130
|
pane.style.borderRadius = `${Math.max(0, radius)}px`
|
|
131
|
+
pane.style.opacity = String(state.materialOpacity)
|
|
123
132
|
body.style.opacity = String(state.opacity)
|
|
124
133
|
}
|
|
125
134
|
const tick = (now: number) => {
|
|
@@ -151,11 +160,18 @@ export const AnchoredSurface = forwardRef<HTMLDivElement, AnchoredSurfaceProps>(
|
|
|
151
160
|
)
|
|
152
161
|
const opacity = present ? Math.max(0, (progress - 0.25) / 0.75) : 0
|
|
153
162
|
state.opacity += (opacity - state.opacity) * (1 - Math.exp(-dt / 0.04))
|
|
163
|
+
state.materialOpacity +=
|
|
164
|
+
((present ? 1 : 0) - state.materialOpacity) * (1 - Math.exp(-dt / 0.04))
|
|
154
165
|
paint()
|
|
155
|
-
if (
|
|
166
|
+
if (
|
|
167
|
+
resting &&
|
|
168
|
+
Math.abs(opacity - state.opacity) < 0.01 &&
|
|
169
|
+
Math.abs((present ? 1 : 0) - state.materialOpacity) < 0.01
|
|
170
|
+
) {
|
|
156
171
|
state.values = [...target]
|
|
157
172
|
state.velocities.fill(0)
|
|
158
173
|
state.opacity = present ? 1 : 0
|
|
174
|
+
state.materialOpacity = present ? 1 : 0
|
|
159
175
|
paint()
|
|
160
176
|
if (!present) remove?.()
|
|
161
177
|
return
|
|
@@ -166,6 +182,7 @@ export const AnchoredSurface = forwardRef<HTMLDivElement, AnchoredSurfaceProps>(
|
|
|
166
182
|
state.values = [...target]
|
|
167
183
|
state.velocities.fill(0)
|
|
168
184
|
state.opacity = present ? 1 : 0
|
|
185
|
+
state.materialOpacity = present ? 1 : 0
|
|
169
186
|
paint()
|
|
170
187
|
if (!present) remove?.()
|
|
171
188
|
} else {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "motion/react"
|
|
8
8
|
import { type ReactNode, type RefObject, useLayoutEffect, useRef, useState } from "react"
|
|
9
9
|
import { concealMenuSource } from "../context-menu/conceal-menu-source"
|
|
10
|
-
import { menuSurfaceReveal } from "../context-menu/menu-reveal"
|
|
10
|
+
import { menuSurfaceExit, menuSurfaceReveal } from "../context-menu/menu-reveal"
|
|
11
11
|
import { cn } from "../../lib/class-names"
|
|
12
12
|
import { screenEdges } from "../../lib/screen-edges"
|
|
13
13
|
import { floatingPanelMotion, spring } from "../../motion/transitions"
|
|
@@ -183,11 +183,16 @@ function OpenPopover({
|
|
|
183
183
|
}
|
|
184
184
|
)
|
|
185
185
|
)
|
|
186
|
+
}, [anchorRef, fit])
|
|
187
|
+
|
|
188
|
+
useLayoutEffect(() => {
|
|
189
|
+
const source = anchorRef?.current
|
|
190
|
+
if (!source || !present) return
|
|
186
191
|
const restoreSource = concealMenuSource(source, false)
|
|
187
192
|
return () => {
|
|
188
193
|
restoreSource()
|
|
189
194
|
}
|
|
190
|
-
}, [anchorRef,
|
|
195
|
+
}, [anchorRef, present])
|
|
191
196
|
|
|
192
197
|
return (
|
|
193
198
|
<motion.div
|
|
@@ -274,8 +279,12 @@ function OpenPopover({
|
|
|
274
279
|
}
|
|
275
280
|
: undefined
|
|
276
281
|
}
|
|
277
|
-
exit={reducedMotion ? { opacity: 0 } : (reveal
|
|
278
|
-
transition={
|
|
282
|
+
exit={reducedMotion || !reveal ? { opacity: 0 } : menuSurfaceExit(reveal)}
|
|
283
|
+
transition={
|
|
284
|
+
reducedMotion
|
|
285
|
+
? { duration: 0 }
|
|
286
|
+
: { ...spring.smooth, opacity: { duration: 0.16 } }
|
|
287
|
+
}
|
|
279
288
|
/>
|
|
280
289
|
)}
|
|
281
290
|
{children}
|
|
@@ -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}
|
package/src/icons/icons.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
LanguagesIcon,
|
|
3
|
+
Loading03Icon,
|
|
3
4
|
Logout01Icon,
|
|
4
5
|
MoreVertical,
|
|
5
6
|
SentIcon,
|
|
@@ -227,6 +228,7 @@ export const catalogIcons = {
|
|
|
227
228
|
|
|
228
229
|
export const NoteIcon = /* @__PURE__ */ icon(Note01Icon, "NoteIcon")
|
|
229
230
|
export const LanguageIcon = /* @__PURE__ */ icon(LanguagesIcon, "LanguageIcon")
|
|
231
|
+
export const LoadingIcon = /* @__PURE__ */ icon(Loading03Icon, "LoadingIcon")
|
|
230
232
|
export const SignOutIcon = /* @__PURE__ */ icon(Logout01Icon, "SignOutIcon")
|
|
231
233
|
export const MoreVerticalIcon = /* @__PURE__ */ icon(MoreVertical, "MoreVerticalIcon")
|
|
232
234
|
export const SendIcon = /* @__PURE__ */ icon(SentIcon, "SendIcon")
|