@passui/react 0.1.45 → 0.1.46
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/README.md +57 -46
- package/dist/types/src/components/accordion/accordion.d.ts +1 -1
- package/dist/types/src/components/breadcrumb/breadcrumb.d.ts +1 -1
- package/dist/types/src/components/inline-disclosure/inline-disclosure.d.ts +1 -1
- package/dist/types/src/components/popover/anchored-surface.d.ts +5 -1
- package/dist/types/src/components/popover/index.d.ts +41 -0
- package/dist/types/src/components/popover/popover-content.d.ts +32 -0
- package/dist/types/src/components/popover/popover-root.d.ts +22 -0
- package/dist/types/src/components/popover/popover-trigger.d.ts +23 -0
- package/dist/types/src/components/stepper/rolling-digit.d.ts +1 -1
- package/dist/types/src/components/table/table.d.ts +1 -1
- package/dist/types/src/components/view-controller/view-controller.d.ts +2 -2
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/machines/popover.machine.d.ts +30 -0
- package/package.json +307 -307
- package/src/appearance/index.ts +2 -2
- package/src/appearance/platform.tsx +76 -60
- package/src/appearance/theme.tsx +409 -377
- package/src/components/accordion/accordion.tsx +58 -49
- package/src/components/ambient/ambient.tsx +25 -25
- package/src/components/ambient/index.ts +1 -1
- package/src/components/aspect-ratio/aspect-ratio.tsx +21 -16
- package/src/components/breadcrumb/breadcrumb.tsx +63 -57
- package/src/components/button/button-styles.ts +39 -32
- package/src/components/button/button.tsx +91 -84
- package/src/components/button/icon-button.tsx +27 -25
- package/src/components/button/pending-spinner.tsx +8 -8
- package/src/components/button/use-pending-indicator.ts +11 -11
- package/src/components/chip/chip.tsx +35 -30
- package/src/components/code-block/code-block.tsx +40 -39
- package/src/components/combo-box/combo-box.tsx +5 -5
- package/src/components/command/command.tsx +124 -94
- package/src/components/compact-date-picker/calendar-month.ts +75 -53
- package/src/components/compact-date-picker/compact-date-picker.tsx +308 -289
- package/src/components/compact-time-picker/compact-time-picker.tsx +288 -281
- package/src/components/compact-time-picker/time-value.ts +32 -29
- package/src/components/content-unavailable-view/content-unavailable-view.tsx +64 -48
- package/src/components/content-view/content-view.tsx +168 -159
- package/src/components/content-view/index.ts +1 -1
- package/src/components/context-menu/conceal-menu-source.ts +25 -25
- package/src/components/context-menu/context-menu-host.tsx +439 -402
- package/src/components/context-menu/context-menu-store.ts +125 -123
- package/src/components/context-menu/menu-navigation.ts +25 -18
- package/src/components/context-menu/menu-position.ts +33 -29
- package/src/components/context-menu/menu-reveal.ts +32 -32
- package/src/components/context-menu/use-context-menu.ts +63 -56
- package/src/components/conversation/conversation.tsx +265 -263
- package/src/components/conversation/index.ts +1 -1
- package/src/components/date-picker/date-picker.tsx +95 -94
- package/src/components/date-picker/date-value.ts +20 -10
- package/src/components/dialog/dialog-host.tsx +105 -103
- package/src/components/dialog/dialog-store.ts +37 -36
- package/src/components/dialog/dialog.tsx +192 -179
- package/src/components/dialog/index.ts +1 -1
- package/src/components/disclosure-group/disclosure-group.tsx +93 -86
- package/src/components/divider/divider.tsx +24 -22
- package/src/components/inline-disclosure/inline-disclosure.tsx +40 -36
- package/src/components/keyboard-shortcut/keyboard-shortcut.tsx +23 -19
- package/src/components/labeled-content/labeled-content.tsx +68 -68
- package/src/components/locale-switcher/index.ts +1 -1
- package/src/components/locale-switcher/locale-switcher.tsx +279 -251
- package/src/components/menu/menu.tsx +75 -58
- package/src/components/morph/index.ts +2 -2
- package/src/components/morph/morph.tsx +227 -201
- package/src/components/morph/use-morph-disclosure.ts +19 -19
- package/src/components/one-time-code/one-time-code.tsx +216 -200
- package/src/components/picker/picker-trigger.ts +16 -13
- package/src/components/picker/picker.tsx +371 -345
- package/src/components/popover/anchored-surface.tsx +253 -229
- package/src/components/popover/index.ts +50 -0
- package/src/components/popover/popover-content.tsx +334 -0
- package/src/components/popover/popover-fit.ts +58 -53
- package/src/components/popover/popover-root.tsx +97 -0
- package/src/components/popover/popover-trigger.tsx +71 -0
- package/src/components/profile-image/profile-image.tsx +80 -64
- package/src/components/progress-view/progress-view.tsx +61 -59
- package/src/components/progress-view/use-liquid-progress.ts +38 -38
- package/src/components/radio-group/radio-group.tsx +71 -68
- package/src/components/search-field/expanding-search-field.tsx +191 -177
- package/src/components/search-field/search-field.tsx +59 -50
- package/src/components/search-list/search-list.tsx +246 -236
- package/src/components/section/section.tsx +58 -58
- package/src/components/secure-field/secure-field.tsx +3 -3
- package/src/components/segmented-control/segmented-control.tsx +136 -136
- package/src/components/sheet/modal-environment.ts +71 -64
- package/src/components/sheet/sheet-portal.tsx +58 -53
- package/src/components/sheet/sheet.tsx +146 -141
- package/src/components/sheet/use-sheet-focus.ts +97 -82
- package/src/components/sidebar/sidebar.tsx +78 -78
- package/src/components/skeleton/skeleton.tsx +30 -21
- package/src/components/slider/slider.tsx +113 -104
- package/src/components/slosh-slider/liquid-step.ts +32 -31
- package/src/components/slosh-slider/slosh-slider.tsx +265 -228
- package/src/components/stat-tile/index.ts +1 -1
- package/src/components/stat-tile/stat-tile.tsx +79 -68
- package/src/components/status-indicator/status-indicator.tsx +40 -37
- package/src/components/stepper/rolling-digit.tsx +82 -68
- package/src/components/stepper/stepper.tsx +122 -113
- package/src/components/swatch-picker/swatch-picker.tsx +120 -120
- package/src/components/tab-bar/tab-bar.tsx +149 -148
- package/src/components/tab-view/tab-view.tsx +140 -137
- package/src/components/table/table.tsx +117 -98
- package/src/components/text-editor/text-editor.tsx +34 -29
- package/src/components/text-field/text-field.tsx +182 -180
- package/src/components/toast/toast-store.ts +127 -115
- package/src/components/toast/toaster.tsx +361 -347
- package/src/components/toggle/toggle.tsx +297 -282
- package/src/components/tooltip/tooltip-host.tsx +157 -150
- package/src/components/view-controller/index.ts +1 -1
- package/src/components/view-controller/view-controller.tsx +64 -53
- package/src/components/wake-slider/wake-slider.tsx +220 -205
- package/src/hooks/scroll/minimize-decision.ts +11 -7
- package/src/hooks/use-anchored-disclosure.ts +19 -19
- package/src/hooks/use-anchored-placement.ts +29 -25
- package/src/hooks/use-dismiss.ts +33 -29
- package/src/hooks/use-floating-dismiss.ts +24 -22
- package/src/hooks/use-media-query.ts +67 -60
- package/src/hooks/use-minimize-on-scroll.ts +69 -60
- package/src/hooks/use-visual-viewport.ts +63 -63
- package/src/hooks/viewport/types.ts +16 -16
- package/src/hooks/viewport/viewport-metrics.ts +16 -10
- package/src/icons/icons.tsx +440 -328
- package/src/index.ts +91 -91
- package/src/lib/anchored-placement.ts +70 -65
- package/src/lib/class-names.ts +14 -13
- package/src/lib/clipboard.ts +8 -7
- package/src/lib/date.ts +61 -60
- package/src/lib/floating-layers.ts +39 -24
- package/src/lib/haptics.ts +10 -10
- package/src/lib/language.ts +9 -9
- package/src/lib/range-value.ts +27 -21
- package/src/lib/screen-edges.ts +33 -30
- package/src/lib/shapes.ts +38 -34
- package/src/machines/popover.machine.ts +51 -0
- package/src/motion/transitions.ts +46 -43
- package/src/styles/code-block.css +69 -43
- package/src/styles/motion-controls.css +393 -191
- package/src/tokens.ts +1 -1
- package/dist/types/src/components/popover/popover.d.ts +0 -21
- package/src/components/popover/popover.tsx +0 -334
|
@@ -1,76 +1,78 @@
|
|
|
1
|
-
import type { ReactNode } from "react"
|
|
2
|
-
import type { MenuRect } from "./menu-reveal"
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { MenuRect } from "./menu-reveal";
|
|
3
3
|
|
|
4
|
-
export type ContextMenuAlignX = "start" | "end"
|
|
5
|
-
export type ContextMenuAlignY = "top" | "bottom"
|
|
6
|
-
export type ContextMenuEdge = "leading" | "trailing"
|
|
7
|
-
export type ContextMenuSize = "sm" | "md" | "lg"
|
|
4
|
+
export type ContextMenuAlignX = "start" | "end";
|
|
5
|
+
export type ContextMenuAlignY = "top" | "bottom";
|
|
6
|
+
export type ContextMenuEdge = "leading" | "trailing";
|
|
7
|
+
export type ContextMenuSize = "sm" | "md" | "lg";
|
|
8
8
|
|
|
9
9
|
/** One row of a context menu, or a divider between groups of rows. */
|
|
10
10
|
export type ContextMenuItem =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
| {
|
|
12
|
+
label: string;
|
|
13
|
+
icon?: ReactNode;
|
|
14
|
+
onSelect: () => void;
|
|
15
|
+
destructive?: boolean;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** Marks the current choice, e.g. a note's kind. */
|
|
18
|
+
checked?: boolean;
|
|
19
|
+
}
|
|
20
|
+
| { heading: string }
|
|
21
|
+
| "divider";
|
|
22
22
|
|
|
23
23
|
export interface OpenMenu {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
24
|
+
id: number;
|
|
25
|
+
anchor: Element | null;
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
items: ContextMenuItem[];
|
|
29
|
+
/** Opened by long-press: bigger rows for fingers. */
|
|
30
|
+
touch: boolean;
|
|
31
|
+
/** Surface, item and heading density. @default "md" */
|
|
32
|
+
size: ContextMenuSize;
|
|
33
|
+
/** Which edge of the menu sits at `x`: its left (`start`) or its right (`end`). */
|
|
34
|
+
alignX: ContextMenuAlignX;
|
|
35
|
+
/** Which edge of the menu sits at `y`: its top (`top`) or its bottom (`bottom`). */
|
|
36
|
+
alignY: ContextMenuAlignY;
|
|
37
|
+
source?: MenuRect;
|
|
38
|
+
toolbar?: HTMLElement;
|
|
39
|
+
toolbarRect?: MenuRect;
|
|
40
|
+
restoreFocus?: boolean;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
let menu: OpenMenu | null = null
|
|
43
|
+
let menu: OpenMenu | null = null;
|
|
44
44
|
/** The menu shown most recently, kept after it closes so a follow-up can stand in its place. */
|
|
45
|
-
let last: OpenMenu | null = null
|
|
46
|
-
let nextId = 1
|
|
47
|
-
const listeners = new Set<() => void>()
|
|
45
|
+
let last: OpenMenu | null = null;
|
|
46
|
+
let nextId = 1;
|
|
47
|
+
const listeners = new Set<() => void>();
|
|
48
48
|
const emit = () => {
|
|
49
|
-
|
|
50
|
-
}
|
|
49
|
+
for (const listener of listeners) listener();
|
|
50
|
+
};
|
|
51
51
|
|
|
52
52
|
/** Opens a menu at a point, such as where a pointer was pressed. */
|
|
53
53
|
export function openContextMenu(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
x: number,
|
|
55
|
+
y: number,
|
|
56
|
+
items: ContextMenuItem[],
|
|
57
|
+
touch = false,
|
|
58
|
+
anchor: Element | null = typeof document === "undefined"
|
|
59
|
+
? null
|
|
60
|
+
: document.activeElement,
|
|
61
|
+
size: ContextMenuSize = "md",
|
|
60
62
|
) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
menu = {
|
|
64
|
+
id: nextId++,
|
|
65
|
+
anchor,
|
|
66
|
+
x,
|
|
67
|
+
y,
|
|
68
|
+
items,
|
|
69
|
+
touch,
|
|
70
|
+
size,
|
|
71
|
+
alignX: "start",
|
|
72
|
+
alignY: "top",
|
|
73
|
+
};
|
|
74
|
+
last = menu;
|
|
75
|
+
emit();
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
/**
|
|
@@ -79,10 +81,10 @@ export function openContextMenu(
|
|
|
79
81
|
* first menu instead of making it tall.
|
|
80
82
|
*/
|
|
81
83
|
export function openFollowUpMenu(items: ContextMenuItem[]) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
if (!last) return;
|
|
85
|
+
menu = { ...last, id: nextId++, items, source: undefined };
|
|
86
|
+
last = menu;
|
|
87
|
+
emit();
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
/**
|
|
@@ -92,74 +94,74 @@ export function openFollowUpMenu(items: ContextMenuItem[]) {
|
|
|
92
94
|
* The menu's real size decides where it goes; nothing is guessed.
|
|
93
95
|
*/
|
|
94
96
|
export function openMenu(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
97
|
+
anchor: Element,
|
|
98
|
+
items: ContextMenuItem[],
|
|
99
|
+
{
|
|
100
|
+
edge = "leading",
|
|
101
|
+
above = false,
|
|
102
|
+
toolbar,
|
|
103
|
+
size = "md",
|
|
104
|
+
}: {
|
|
105
|
+
edge?: ContextMenuEdge;
|
|
106
|
+
above?: boolean;
|
|
107
|
+
/** Replace this action group with a horizontally expanding menu surface. */
|
|
108
|
+
toolbar?: HTMLElement;
|
|
109
|
+
/** Surface, item and heading density. @default "md" */
|
|
110
|
+
size?: ContextMenuSize;
|
|
111
|
+
} = {},
|
|
110
112
|
) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
113
|
+
const rect = anchor.getBoundingClientRect();
|
|
114
|
+
const toolbarRect = toolbar?.getBoundingClientRect();
|
|
115
|
+
const rtl = getComputedStyle(anchor).direction === "rtl";
|
|
116
|
+
// Trailing is the right edge in left-to-right text, the left edge in right-to-left.
|
|
117
|
+
const right = (edge === "trailing") !== rtl;
|
|
118
|
+
menu = {
|
|
119
|
+
id: nextId++,
|
|
120
|
+
anchor,
|
|
121
|
+
x: right ? (toolbarRect ?? rect).right : (toolbarRect ?? rect).left,
|
|
122
|
+
y: toolbar ? rect.top : above ? rect.bottom : rect.top,
|
|
123
|
+
items,
|
|
124
|
+
touch: false,
|
|
125
|
+
size,
|
|
126
|
+
alignX: right ? "end" : "start",
|
|
127
|
+
alignY: above ? "bottom" : "top",
|
|
128
|
+
toolbar,
|
|
129
|
+
toolbarRect,
|
|
130
|
+
source: {
|
|
131
|
+
left: rect.left,
|
|
132
|
+
top: rect.top,
|
|
133
|
+
width: rect.width,
|
|
134
|
+
height: rect.height,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
last = menu;
|
|
138
|
+
emit();
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
export function closeContextMenu(restoreFocus = true) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
142
|
+
if (!menu) return;
|
|
143
|
+
const anchor = menu.anchor;
|
|
144
|
+
const toolbar = menu.toolbar;
|
|
145
|
+
menu.restoreFocus = restoreFocus;
|
|
146
|
+
menu = null;
|
|
147
|
+
if (
|
|
148
|
+
restoreFocus &&
|
|
149
|
+
!toolbar &&
|
|
150
|
+
typeof HTMLElement !== "undefined" &&
|
|
151
|
+
anchor instanceof HTMLElement &&
|
|
152
|
+
anchor.isConnected
|
|
153
|
+
)
|
|
154
|
+
anchor.focus();
|
|
155
|
+
emit();
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
export function subscribeContextMenu(listener: () => void) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
listeners.add(listener);
|
|
160
|
+
return () => {
|
|
161
|
+
listeners.delete(listener);
|
|
162
|
+
};
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
export function getContextMenu(): OpenMenu | null {
|
|
164
|
-
|
|
166
|
+
return menu;
|
|
165
167
|
}
|
|
@@ -1,29 +1,36 @@
|
|
|
1
|
-
export type MenuNavigationKey = "ArrowDown" | "ArrowUp" | "Home" | "End"
|
|
1
|
+
export type MenuNavigationKey = "ArrowDown" | "ArrowUp" | "Home" | "End";
|
|
2
2
|
|
|
3
3
|
export function isMenuNavigationKey(key: string): key is MenuNavigationKey {
|
|
4
|
-
|
|
4
|
+
return (
|
|
5
|
+
key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End"
|
|
6
|
+
);
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
/** Index of the next enabled action in a menu, with wrapping arrow navigation. */
|
|
8
|
-
export function nextMenuIndex(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
export function nextMenuIndex(
|
|
11
|
+
key: MenuNavigationKey,
|
|
12
|
+
current: number,
|
|
13
|
+
count: number,
|
|
14
|
+
): number {
|
|
15
|
+
if (count <= 0) return -1;
|
|
16
|
+
if (key === "Home") return 0;
|
|
17
|
+
if (key === "End") return count - 1;
|
|
18
|
+
if (key === "ArrowDown") return (current + 1) % count;
|
|
19
|
+
return current < 0 ? count - 1 : (current - 1 + count) % count;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
/** Finds a matching label after the current action, wrapping at the end. */
|
|
17
23
|
export function findMenuTypeaheadIndex(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
labels: ReadonlyArray<string>,
|
|
25
|
+
query: string,
|
|
26
|
+
current: number,
|
|
21
27
|
): number {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
if (!labels.length || !query.trim()) return -1;
|
|
29
|
+
const needle = query.toLocaleLowerCase();
|
|
30
|
+
for (let offset = 1; offset <= labels.length; offset++) {
|
|
31
|
+
const index = (current + offset + labels.length) % labels.length;
|
|
32
|
+
if (labels[index]?.trim().toLocaleLowerCase().startsWith(needle))
|
|
33
|
+
return index;
|
|
34
|
+
}
|
|
35
|
+
return -1;
|
|
29
36
|
}
|
|
@@ -1,34 +1,38 @@
|
|
|
1
|
-
import type { OpenMenu } from "./context-menu-store"
|
|
2
|
-
import type { MenuRect } from "./menu-reveal"
|
|
1
|
+
import type { OpenMenu } from "./context-menu-store";
|
|
2
|
+
import type { MenuRect } from "./menu-reveal";
|
|
3
3
|
|
|
4
|
-
type MenuAnchor = Pick<OpenMenu, "x" | "y" | "alignX" | "alignY"
|
|
5
|
-
type MenuSize = Pick<MenuRect, "width" | "height"
|
|
4
|
+
type MenuAnchor = Pick<OpenMenu, "x" | "y" | "alignX" | "alignY">;
|
|
5
|
+
type MenuSize = Pick<MenuRect, "width" | "height">;
|
|
6
6
|
type MenuBounds = {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
7
|
+
left: number;
|
|
8
|
+
top: number;
|
|
9
|
+
right: number;
|
|
10
|
+
bottom: number;
|
|
11
|
+
};
|
|
12
12
|
|
|
13
13
|
/** Fit within the current visible viewport, including a keyboard or Safari pan. */
|
|
14
|
-
export function menuPosition(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
14
|
+
export function menuPosition(
|
|
15
|
+
menu: MenuAnchor,
|
|
16
|
+
size: MenuSize,
|
|
17
|
+
bounds: MenuBounds,
|
|
18
|
+
) {
|
|
19
|
+
const { width, height } = size;
|
|
20
|
+
const { left, top, right, bottom } = bounds;
|
|
21
|
+
const fitsAfterX = menu.x + width <= right;
|
|
22
|
+
const fitsBeforeX = menu.x - width >= left;
|
|
23
|
+
const endX = menu.alignX === "end" ? fitsBeforeX || !fitsAfterX : !fitsAfterX;
|
|
24
|
+
const fitsBelow = menu.y + height <= bottom;
|
|
25
|
+
const fitsAbove = menu.y - height >= top;
|
|
26
|
+
const endY = menu.alignY === "bottom" ? fitsAbove || !fitsBelow : !fitsBelow;
|
|
27
|
+
const clamp = (value: number, min: number, max: number) =>
|
|
28
|
+
Math.min(Math.max(value, min), Math.max(min, max));
|
|
29
|
+
return {
|
|
30
|
+
left: clamp(endX ? menu.x - width : menu.x, left, right - width),
|
|
31
|
+
top: clamp(endY ? menu.y - height : menu.y, top, bottom - height),
|
|
32
|
+
originX: endX ? 1 : 0,
|
|
33
|
+
originY: endY ? 1 : 0,
|
|
34
|
+
maxHeight: Math.max(0, bottom - top),
|
|
35
|
+
width,
|
|
36
|
+
height: Math.min(height, Math.max(0, bottom - top)),
|
|
37
|
+
};
|
|
34
38
|
}
|
|
@@ -1,42 +1,42 @@
|
|
|
1
1
|
export type MenuRect = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
2
|
+
left: number;
|
|
3
|
+
top: number;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
};
|
|
7
7
|
|
|
8
8
|
/** Return toward the source while handing its material back to the real trigger. */
|
|
9
9
|
export function menuSurfaceExit(source: ReturnType<typeof menuSurfaceReveal>) {
|
|
10
|
-
|
|
10
|
+
return { ...source, opacity: 0 };
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/** Animate the material's bounds independently so menu text never stretches. */
|
|
14
14
|
export function menuSurfaceReveal(
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
source: MenuRect | undefined,
|
|
16
|
+
target: MenuRect & { radius?: number },
|
|
17
17
|
) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
18
|
+
if (
|
|
19
|
+
!source ||
|
|
20
|
+
source.width <= 0 ||
|
|
21
|
+
source.height <= 0 ||
|
|
22
|
+
target.width <= 0 ||
|
|
23
|
+
target.height <= 0
|
|
24
|
+
) {
|
|
25
|
+
return {
|
|
26
|
+
x: 0,
|
|
27
|
+
y: -4,
|
|
28
|
+
width: target.width,
|
|
29
|
+
height: target.height,
|
|
30
|
+
opacity: 0,
|
|
31
|
+
borderRadius: target.radius ?? 0,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
x: source.left - target.left,
|
|
36
|
+
y: source.top - target.top,
|
|
37
|
+
width: source.width,
|
|
38
|
+
height: source.height,
|
|
39
|
+
opacity: 1,
|
|
40
|
+
borderRadius: Math.min(source.width, source.height) / 2,
|
|
41
|
+
};
|
|
42
42
|
}
|
|
@@ -1,66 +1,73 @@
|
|
|
1
|
-
import { type MouseEvent, type PointerEvent, useRef } from "react"
|
|
2
|
-
import { haptic } from "../../lib/haptics"
|
|
3
|
-
import { type ContextMenuItem, openContextMenu } from "./context-menu-store"
|
|
1
|
+
import { type MouseEvent, type PointerEvent, useRef } from "react";
|
|
2
|
+
import { haptic } from "../../lib/haptics";
|
|
3
|
+
import { type ContextMenuItem, openContextMenu } from "./context-menu-store";
|
|
4
4
|
|
|
5
|
-
const LONG_PRESS_MS = 480
|
|
6
|
-
const MOVE_TOLERANCE = 10
|
|
5
|
+
const LONG_PRESS_MS = 480;
|
|
6
|
+
const MOVE_TOLERANCE = 10;
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Right-click on desktop, long-press on touch screens: both open the same
|
|
10
10
|
* menu of actions. Spread the returned handlers onto the element.
|
|
11
11
|
*/
|
|
12
12
|
export function useContextMenu(items: () => ContextMenuItem[]) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
const press = useRef<{
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
timer: ReturnType<typeof setTimeout>;
|
|
17
|
+
} | null>(null);
|
|
18
|
+
const suppressClick = useRef(false);
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const cancel = () => {
|
|
21
|
+
if (press.current) clearTimeout(press.current.timer);
|
|
22
|
+
press.current = null;
|
|
23
|
+
};
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
25
|
+
return {
|
|
26
|
+
onContextMenu(event: MouseEvent) {
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
cancel();
|
|
29
|
+
openContextMenu(
|
|
30
|
+
event.clientX,
|
|
31
|
+
event.clientY,
|
|
32
|
+
items(),
|
|
33
|
+
false,
|
|
34
|
+
event.currentTarget,
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
onPointerDown(event: PointerEvent) {
|
|
38
|
+
if (event.pointerType !== "touch") return;
|
|
39
|
+
const { clientX: x, clientY: y } = event;
|
|
40
|
+
const anchor = event.currentTarget;
|
|
41
|
+
cancel();
|
|
42
|
+
press.current = {
|
|
43
|
+
x,
|
|
44
|
+
y,
|
|
45
|
+
timer: setTimeout(() => {
|
|
46
|
+
press.current = null;
|
|
47
|
+
suppressClick.current = true;
|
|
48
|
+
haptic("medium");
|
|
49
|
+
openContextMenu(x, y, items(), true, anchor);
|
|
50
|
+
}, LONG_PRESS_MS),
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
onPointerMove(event: PointerEvent) {
|
|
54
|
+
const start = press.current;
|
|
55
|
+
if (
|
|
56
|
+
start &&
|
|
57
|
+
Math.hypot(event.clientX - start.x, event.clientY - start.y) >
|
|
58
|
+
MOVE_TOLERANCE
|
|
59
|
+
) {
|
|
60
|
+
cancel();
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
onPointerUp: cancel,
|
|
64
|
+
onPointerCancel: cancel,
|
|
65
|
+
// The tap that ends a long-press shouldn't also open the row.
|
|
66
|
+
onClickCapture(event: MouseEvent) {
|
|
67
|
+
if (!suppressClick.current) return;
|
|
68
|
+
suppressClick.current = false;
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
event.stopPropagation();
|
|
71
|
+
},
|
|
72
|
+
};
|
|
66
73
|
}
|