@funstack/ui 0.1.0-alpha.6 → 0.1.0-alpha.8
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/button/button.mjs +1 -1
- package/dist/button/button.mjs.map +1 -1
- package/dist/combobox/combobox-clear.d.mts +6 -3
- package/dist/combobox/combobox-clear.d.mts.map +1 -1
- package/dist/combobox/combobox-clear.mjs +10 -5
- package/dist/combobox/combobox-clear.mjs.map +1 -1
- package/dist/combobox/combobox-trigger.d.mts +6 -2
- package/dist/combobox/combobox-trigger.d.mts.map +1 -1
- package/dist/combobox/combobox-trigger.mjs +10 -4
- package/dist/combobox/combobox-trigger.mjs.map +1 -1
- package/dist/dialog/dialog-close.d.mts +8 -3
- package/dist/dialog/dialog-close.d.mts.map +1 -1
- package/dist/dialog/dialog-close.mjs +11 -7
- package/dist/dialog/dialog-close.mjs.map +1 -1
- package/dist/dialog/dialog-trigger.d.mts +8 -3
- package/dist/dialog/dialog-trigger.d.mts.map +1 -1
- package/dist/dialog/dialog-trigger.mjs +15 -8
- package/dist/dialog/dialog-trigger.mjs.map +1 -1
- package/dist/form/form-submit.d.mts +4 -1
- package/dist/form/form-submit.d.mts.map +1 -1
- package/dist/form/form-submit.mjs.map +1 -1
- package/dist/internal/anchor-position.mjs +24 -1
- package/dist/internal/anchor-position.mjs.map +1 -1
- package/dist/internal/use-action-trigger.mjs +9 -7
- package/dist/internal/use-action-trigger.mjs.map +1 -1
- package/dist/internal/use-popover-popup.mjs +22 -4
- package/dist/internal/use-popover-popup.mjs.map +1 -1
- package/dist/internal/use-popover-trigger.mjs +6 -5
- package/dist/internal/use-popover-trigger.mjs.map +1 -1
- package/dist/menu/index.d.mts +5 -1
- package/dist/menu/index.d.mts.map +1 -1
- package/dist/menu/index.mjs +5 -1
- package/dist/menu/index.mjs.map +1 -1
- package/dist/menu/menu-context-trigger.d.mts +46 -0
- package/dist/menu/menu-context-trigger.d.mts.map +1 -0
- package/dist/menu/menu-context-trigger.mjs +226 -0
- package/dist/menu/menu-context-trigger.mjs.map +1 -0
- package/dist/menu/menu-context.mjs +34 -1
- package/dist/menu/menu-context.mjs.map +1 -1
- package/dist/menu/menu-dom.mjs +44 -2
- package/dist/menu/menu-dom.mjs.map +1 -1
- package/dist/menu/menu-item.d.mts +6 -2
- package/dist/menu/menu-item.d.mts.map +1 -1
- package/dist/menu/menu-item.mjs +5 -3
- package/dist/menu/menu-item.mjs.map +1 -1
- package/dist/menu/menu-popup.d.mts +18 -4
- package/dist/menu/menu-popup.d.mts.map +1 -1
- package/dist/menu/menu-popup.mjs +116 -17
- package/dist/menu/menu-popup.mjs.map +1 -1
- package/dist/menu/menu-root.d.mts +5 -0
- package/dist/menu/menu-root.d.mts.map +1 -1
- package/dist/menu/menu-root.mjs +48 -6
- package/dist/menu/menu-root.mjs.map +1 -1
- package/dist/menu/menu-trigger.d.mts +6 -3
- package/dist/menu/menu-trigger.d.mts.map +1 -1
- package/dist/menu/menu-trigger.mjs +2 -2
- package/dist/menu/menu-trigger.mjs.map +1 -1
- package/dist/number-field/number-field-decrement.d.mts +4 -3
- package/dist/number-field/number-field-decrement.d.mts.map +1 -1
- package/dist/number-field/number-field-decrement.mjs +4 -3
- package/dist/number-field/number-field-decrement.mjs.map +1 -1
- package/dist/number-field/number-field-increment.d.mts +8 -3
- package/dist/number-field/number-field-increment.d.mts.map +1 -1
- package/dist/number-field/number-field-increment.mjs +8 -5
- package/dist/number-field/number-field-increment.mjs.map +1 -1
- package/dist/number-field/number-field-root.mjs +1 -1
- package/dist/number-field/number-field-root.mjs.map +1 -1
- package/dist/number-field/use-step-button.mjs +6 -1
- package/dist/number-field/use-step-button.mjs.map +1 -1
- package/dist/popover/popover-close.d.mts +9 -3
- package/dist/popover/popover-close.d.mts.map +1 -1
- package/dist/popover/popover-close.mjs +12 -7
- package/dist/popover/popover-close.mjs.map +1 -1
- package/dist/popover/popover-trigger.d.mts +8 -4
- package/dist/popover/popover-trigger.d.mts.map +1 -1
- package/dist/popover/popover-trigger.mjs +3 -3
- package/dist/popover/popover-trigger.mjs.map +1 -1
- package/dist/slider/slider-root.mjs +1 -1
- package/dist/slider/slider-root.mjs.map +1 -1
- package/dist/toast/toast-action.d.mts +4 -3
- package/dist/toast/toast-action.d.mts.map +1 -1
- package/dist/toast/toast-action.mjs +7 -5
- package/dist/toast/toast-action.mjs.map +1 -1
- package/dist/toggle/toggle.d.mts +4 -1
- package/dist/toggle/toggle.d.mts.map +1 -1
- package/dist/toggle/toggle.mjs +14 -8
- package/dist/toggle/toggle.mjs.map +1 -1
- package/dist/toggle-group/toggle-group-item.d.mts +6 -2
- package/dist/toggle-group/toggle-group-item.d.mts.map +1 -1
- package/dist/toggle-group/toggle-group-item.mjs +9 -4
- package/dist/toggle-group/toggle-group-item.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { dataAttr } from "../internal/data-attr.mjs";
|
|
3
|
+
import { useDevWarning } from "../internal/use-dev-warning.mjs";
|
|
4
|
+
import { resolveStateValue } from "../internal/render-element.mjs";
|
|
5
|
+
import { useRender } from "../internal/use-render.mjs";
|
|
6
|
+
import { MenuRegionContext, claimPress, isPressClaimed, markHandled, useMenuContext } from "./menu-context.mjs";
|
|
7
|
+
import { capturePointer, isEditableTarget } from "./menu-dom.mjs";
|
|
8
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
9
|
+
import { jsx } from "react/jsx-runtime";
|
|
10
|
+
//#region src/menu/menu-context-trigger.tsx
|
|
11
|
+
/** Movement that cancels a long-press (Base UI's slop). */
|
|
12
|
+
const LONG_PRESS_SLOP = 10;
|
|
13
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
14
|
+
/**
|
|
15
|
+
* A region whose `contextmenu` (right-click, Ctrl+click, Shift+F10 / the ContextMenu key at a
|
|
16
|
+
* focused descendant) or touch / pen long-press opens the Root's `Menu.Popup` at the pointer
|
|
17
|
+
* (design/components/context-menu.md). A plain element (default `div`) with no role and no ARIA:
|
|
18
|
+
* the region keeps its own semantics (`render={<tr />}`), and keyboard users reach it through
|
|
19
|
+
* focusable content inside it. A second right-click inside the region while the menu is open moves
|
|
20
|
+
* the menu there. Render the `Menu.Popup` as a sibling of the region where the markup allows; in
|
|
21
|
+
* a table row (`render={<tr />}`), inside one of its cells: events from inside the popup are
|
|
22
|
+
* never the region's.
|
|
23
|
+
*
|
|
24
|
+
* Owned keys (conventions 1.2): `onContextMenu`, `onPointerDown`, `onPointerMove`,
|
|
25
|
+
* `onPointerUp`, `onPointerCancel`, `data-open`, `data-pending`, `data-disabled`, and the style
|
|
26
|
+
* key `WebkitTouchCallout` (`"none"`; the consumer's `style` wins).
|
|
27
|
+
*/
|
|
28
|
+
function MenuContextTrigger({ disabled = false, editableTargets = "native", longPressDelay = 500, className, style, children, render, ...rest }) {
|
|
29
|
+
const menu = useMenuContext("Menu.ContextTrigger");
|
|
30
|
+
const { popover } = menu;
|
|
31
|
+
useDevWarning(menu.submenu, "Menu.ContextTrigger: rendered in a submenu. A Menu.Root inside a Menu.Popup is a submenu; a context menu region belongs to a top-level Menu.Root.");
|
|
32
|
+
useDevWarning(longPressDelay < 0, "Menu.ContextTrigger: `longPressDelay` is negative; it is a number of milliseconds.");
|
|
33
|
+
const sequence = useRef(0);
|
|
34
|
+
const lastPointerDown = useRef(-1);
|
|
35
|
+
const lastKeyDown = useRef(-1);
|
|
36
|
+
const longPress = useRef(null);
|
|
37
|
+
const disarmSwallow = useRef(null);
|
|
38
|
+
const warnedNoPopup = useRef(false);
|
|
39
|
+
const cancelLongPress = useCallback(() => {
|
|
40
|
+
if (longPress.current !== null) clearTimeout(longPress.current.timer);
|
|
41
|
+
longPress.current = null;
|
|
42
|
+
}, []);
|
|
43
|
+
useEffect(() => () => {
|
|
44
|
+
cancelLongPress();
|
|
45
|
+
disarmSwallow.current?.();
|
|
46
|
+
}, [cancelLongPress]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (disabled) cancelLongPress();
|
|
49
|
+
}, [disabled, cancelLongPress]);
|
|
50
|
+
const open = popover.optimistic;
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!open) disarmSwallow.current?.();
|
|
53
|
+
}, [open]);
|
|
54
|
+
const modalityRef = useCallback((element) => {
|
|
55
|
+
const onKeyDown = () => {
|
|
56
|
+
lastKeyDown.current = ++sequence.current;
|
|
57
|
+
};
|
|
58
|
+
const onPointerDown = () => {
|
|
59
|
+
lastPointerDown.current = ++sequence.current;
|
|
60
|
+
};
|
|
61
|
+
element.addEventListener("keydown", onKeyDown, true);
|
|
62
|
+
element.addEventListener("pointerdown", onPointerDown, true);
|
|
63
|
+
return () => {
|
|
64
|
+
element.removeEventListener("keydown", onKeyDown, true);
|
|
65
|
+
element.removeEventListener("pointerdown", onPointerDown, true);
|
|
66
|
+
};
|
|
67
|
+
}, []);
|
|
68
|
+
/** The popup element to capture on and open, or `null` (no Popup mounted: nothing opens). */
|
|
69
|
+
const popupElement = () => {
|
|
70
|
+
const popup = popover.popupRef.current;
|
|
71
|
+
if (popup === null && isDev && !warnedNoPopup.current) {
|
|
72
|
+
warnedNoPopup.current = true;
|
|
73
|
+
console.error("Menu.ContextTrigger: a context menu was requested but the Menu.Root has no mounted Menu.Popup; the browser's own menu shows instead.");
|
|
74
|
+
}
|
|
75
|
+
return popup;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* One-shot swallow of the `click` an engine may synthesise when the long-press ends (2.2),
|
|
79
|
+
* scoped to that release (review H1): the click comes in the same task as the `pointerup` of
|
|
80
|
+
* `pointerId`, so the swallow ends one task after that `pointerup` (or `pointercancel`), and at
|
|
81
|
+
* once on any `keydown`, the next `pointerdown`, or the menu closing. iOS synthesises no click
|
|
82
|
+
* after a long-press; a swallow left armed would eat the next keyboard activation
|
|
83
|
+
* (`element.click()`).
|
|
84
|
+
*/
|
|
85
|
+
const armClickSwallow = (doc, pointerId) => {
|
|
86
|
+
disarmSwallow.current?.();
|
|
87
|
+
let released;
|
|
88
|
+
const onClick = (event) => {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
event.stopPropagation();
|
|
91
|
+
disarm();
|
|
92
|
+
};
|
|
93
|
+
const onRelease = (event) => {
|
|
94
|
+
if (event.pointerId !== pointerId) return;
|
|
95
|
+
if (released === void 0) released = setTimeout(disarm, 0);
|
|
96
|
+
};
|
|
97
|
+
const disarm = () => {
|
|
98
|
+
if (released !== void 0) clearTimeout(released);
|
|
99
|
+
doc.removeEventListener("click", onClick, true);
|
|
100
|
+
doc.removeEventListener("pointerdown", disarm, true);
|
|
101
|
+
doc.removeEventListener("keydown", disarm, true);
|
|
102
|
+
doc.removeEventListener("pointerup", onRelease, true);
|
|
103
|
+
doc.removeEventListener("pointercancel", onRelease, true);
|
|
104
|
+
if (disarmSwallow.current === disarm) disarmSwallow.current = null;
|
|
105
|
+
};
|
|
106
|
+
doc.addEventListener("click", onClick, true);
|
|
107
|
+
doc.addEventListener("pointerdown", disarm, true);
|
|
108
|
+
doc.addEventListener("keydown", disarm, true);
|
|
109
|
+
doc.addEventListener("pointerup", onRelease, true);
|
|
110
|
+
doc.addEventListener("pointercancel", onRelease, true);
|
|
111
|
+
disarmSwallow.current = disarm;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* The event comes from this Root's own popup, rendered inside the region (a table row, where a
|
|
115
|
+
* `<div>` cannot be a sibling of the `<tr>`): never a move or a long-press of the region.
|
|
116
|
+
*/
|
|
117
|
+
const fromOwnPopup = (target) => {
|
|
118
|
+
const popup = popover.popupRef.current;
|
|
119
|
+
return popup !== null && target instanceof Node && popup.contains(target);
|
|
120
|
+
};
|
|
121
|
+
const onContextMenu = (event) => {
|
|
122
|
+
const native = event.nativeEvent;
|
|
123
|
+
if (native.defaultPrevented || disabled || fromOwnPopup(native.target)) return;
|
|
124
|
+
if (editableTargets === "native" && isEditableTarget(native.target)) return;
|
|
125
|
+
const popup = popupElement();
|
|
126
|
+
if (popup === null) return;
|
|
127
|
+
native.preventDefault();
|
|
128
|
+
markHandled(native, menu);
|
|
129
|
+
cancelLongPress();
|
|
130
|
+
const keyboard = native.button === -1 || lastKeyDown.current > lastPointerDown.current;
|
|
131
|
+
const pointerId = native.pointerId;
|
|
132
|
+
const id = typeof pointerId === "number" ? pointerId : void 0;
|
|
133
|
+
if (native.buttons !== 0) capturePointer(popup, id);
|
|
134
|
+
menu.openAt({
|
|
135
|
+
x: native.clientX,
|
|
136
|
+
y: native.clientY
|
|
137
|
+
}, {
|
|
138
|
+
source: event.currentTarget,
|
|
139
|
+
event: native,
|
|
140
|
+
pointer: !keyboard,
|
|
141
|
+
pointerId: id
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
const onPointerDown = (event) => {
|
|
145
|
+
const native = event.nativeEvent;
|
|
146
|
+
if (longPress.current !== null) {
|
|
147
|
+
cancelLongPress();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (disabled || native.pointerType !== "touch" && native.pointerType !== "pen") return;
|
|
151
|
+
if (fromOwnPopup(native.target)) return;
|
|
152
|
+
if (!native.isPrimary || isPressClaimed(native)) return;
|
|
153
|
+
if (editableTargets === "native" && isEditableTarget(native.target)) return;
|
|
154
|
+
claimPress(native);
|
|
155
|
+
const source = event.currentTarget;
|
|
156
|
+
const { pointerId, clientX, clientY } = native;
|
|
157
|
+
const timer = setTimeout(() => {
|
|
158
|
+
longPress.current = null;
|
|
159
|
+
const popup = popupElement();
|
|
160
|
+
if (popup === null) return;
|
|
161
|
+
capturePointer(popup, pointerId);
|
|
162
|
+
armClickSwallow(source.ownerDocument, pointerId);
|
|
163
|
+
menu.openAt({
|
|
164
|
+
x: clientX,
|
|
165
|
+
y: clientY
|
|
166
|
+
}, {
|
|
167
|
+
source,
|
|
168
|
+
event: native,
|
|
169
|
+
pointer: true,
|
|
170
|
+
pointerId
|
|
171
|
+
});
|
|
172
|
+
}, Math.max(0, longPressDelay));
|
|
173
|
+
longPress.current = {
|
|
174
|
+
pointerId,
|
|
175
|
+
x: clientX,
|
|
176
|
+
y: clientY,
|
|
177
|
+
timer
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
const onPointerMove = (event) => {
|
|
181
|
+
const press = longPress.current;
|
|
182
|
+
if (press === null || event.pointerId !== press.pointerId) return;
|
|
183
|
+
if (Math.hypot(event.clientX - press.x, event.clientY - press.y) > LONG_PRESS_SLOP) cancelLongPress();
|
|
184
|
+
};
|
|
185
|
+
const onPointerEnd = (event) => {
|
|
186
|
+
const press = longPress.current;
|
|
187
|
+
if (press !== null && event.pointerId === press.pointerId) cancelLongPress();
|
|
188
|
+
};
|
|
189
|
+
const state = {
|
|
190
|
+
open: popover.optimistic,
|
|
191
|
+
pending: popover.pending,
|
|
192
|
+
disabled
|
|
193
|
+
};
|
|
194
|
+
const libraryProps = {
|
|
195
|
+
onContextMenu,
|
|
196
|
+
onPointerDown,
|
|
197
|
+
onPointerMove,
|
|
198
|
+
onPointerUp: onPointerEnd,
|
|
199
|
+
onPointerCancel: onPointerEnd,
|
|
200
|
+
"data-open": dataAttr(state.open),
|
|
201
|
+
"data-pending": dataAttr(state.pending),
|
|
202
|
+
"data-disabled": dataAttr(disabled)
|
|
203
|
+
};
|
|
204
|
+
return useRender({
|
|
205
|
+
defaultElement: "div",
|
|
206
|
+
render,
|
|
207
|
+
state,
|
|
208
|
+
className,
|
|
209
|
+
style: (s) => ({
|
|
210
|
+
WebkitTouchCallout: "none",
|
|
211
|
+
...resolveStateValue(style, s)
|
|
212
|
+
}),
|
|
213
|
+
children: (s) => /* @__PURE__ */ jsx(MenuRegionContext, {
|
|
214
|
+
value: true,
|
|
215
|
+
children: resolveStateValue(children, s)
|
|
216
|
+
}),
|
|
217
|
+
props: rest,
|
|
218
|
+
libraryProps,
|
|
219
|
+
ref: modalityRef
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
MenuContextTrigger.displayName = "MenuContextTrigger";
|
|
223
|
+
//#endregion
|
|
224
|
+
export { MenuContextTrigger };
|
|
225
|
+
|
|
226
|
+
//# sourceMappingURL=menu-context-trigger.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu-context-trigger.mjs","names":[],"sources":["../../src/menu/menu-context-trigger.tsx"],"sourcesContent":["\"use client\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type {\n ComponentPropsWithRef,\n CSSProperties,\n MouseEvent,\n PointerEvent,\n ReactNode,\n} from \"react\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { RenderProp, StateValue } from \"../internal/types.js\";\nimport { useDevWarning } from \"../internal/use-dev-warning.js\";\nimport { resolveStateValue, useRender } from \"../internal/use-render.js\";\nimport type { DataAttributes } from \"../internal/use-render.js\";\nimport {\n claimPress,\n isPressClaimed,\n markHandled,\n MenuRegionContext,\n useMenuContext,\n} from \"./menu-context.js\";\nimport { capturePointer, isEditableTarget } from \"./menu-dom.js\";\n\nexport interface MenuContextTriggerState {\n /** Optimistic open state (the Root's). */\n readonly open: boolean;\n /** The Root's transition is pending (an open or a close in flight). */\n readonly pending: boolean;\n readonly disabled: boolean;\n}\n\nexport interface MenuContextTriggerProps extends Omit<\n ComponentPropsWithRef<\"div\">,\n | \"onContextMenu\"\n | \"onPointerDown\"\n | \"onPointerMove\"\n | \"onPointerUp\"\n | \"onPointerCancel\"\n | \"className\"\n | \"style\"\n | \"children\"\n> {\n /** The region does nothing; a `contextmenu` bubbles on to an outer region. `data-disabled`. */\n disabled?: boolean;\n /**\n * What a `contextmenu` (or long-press) on an editable target inside the region opens: the\n * browser's own menu (`\"native\"`, default: copy and paste keep working) or this menu.\n * Editable: a text-taking `<input>`, a `<textarea>`, an element with `isContentEditable`.\n */\n editableTargets?: \"native\" | \"menu\";\n /** Milliseconds a touch or pen pointer must stay down (within 10 px) to open. Default 500. */\n longPressDelay?: number;\n className?: StateValue<string | undefined, MenuContextTriggerState>;\n style?: StateValue<CSSProperties | undefined, MenuContextTriggerState>;\n children?: StateValue<ReactNode, MenuContextTriggerState>;\n render?: RenderProp<ComponentPropsWithRef<\"div\">, MenuContextTriggerState>;\n}\n\n/** Movement that cancels a long-press (Base UI's slop). */\nconst LONG_PRESS_SLOP = 10;\n\nconst isDev = process.env.NODE_ENV !== \"production\";\n\ninterface LongPress {\n readonly pointerId: number;\n readonly x: number;\n readonly y: number;\n readonly timer: ReturnType<typeof setTimeout>;\n}\n\n/**\n * A region whose `contextmenu` (right-click, Ctrl+click, Shift+F10 / the ContextMenu key at a\n * focused descendant) or touch / pen long-press opens the Root's `Menu.Popup` at the pointer\n * (design/components/context-menu.md). A plain element (default `div`) with no role and no ARIA:\n * the region keeps its own semantics (`render={<tr />}`), and keyboard users reach it through\n * focusable content inside it. A second right-click inside the region while the menu is open moves\n * the menu there. Render the `Menu.Popup` as a sibling of the region where the markup allows; in\n * a table row (`render={<tr />}`), inside one of its cells: events from inside the popup are\n * never the region's.\n *\n * Owned keys (conventions 1.2): `onContextMenu`, `onPointerDown`, `onPointerMove`,\n * `onPointerUp`, `onPointerCancel`, `data-open`, `data-pending`, `data-disabled`, and the style\n * key `WebkitTouchCallout` (`\"none\"`; the consumer's `style` wins).\n */\nexport function MenuContextTrigger({\n disabled = false,\n editableTargets = \"native\",\n longPressDelay = 500,\n className,\n style,\n children,\n render,\n ...rest\n}: MenuContextTriggerProps) {\n const menu = useMenuContext(\"Menu.ContextTrigger\");\n const { popover } = menu;\n useDevWarning(\n menu.submenu,\n \"Menu.ContextTrigger: rendered in a submenu. A Menu.Root inside a Menu.Popup is a submenu; a \" +\n \"context menu region belongs to a top-level Menu.Root.\",\n );\n useDevWarning(\n longPressDelay < 0,\n \"Menu.ContextTrigger: `longPressDelay` is negative; it is a number of milliseconds.\",\n );\n\n // Modality bookkeeping (design 2.1): a sequence number, not a clock, so two events in the same\n // millisecond still order.\n const sequence = useRef(0);\n const lastPointerDown = useRef(-1);\n const lastKeyDown = useRef(-1);\n const longPress = useRef<LongPress | null>(null);\n // Disarms the one-shot click swallow armed by a long-press open.\n const disarmSwallow = useRef<(() => void) | null>(null);\n const warnedNoPopup = useRef(false);\n\n const cancelLongPress = useCallback(() => {\n if (longPress.current !== null) clearTimeout(longPress.current.timer);\n longPress.current = null;\n }, []);\n\n useEffect(\n () => () => {\n cancelLongPress();\n disarmSwallow.current?.();\n },\n [cancelLongPress],\n );\n // A region disabled mid-gesture opens nothing.\n useEffect(() => {\n if (disabled) cancelLongPress();\n }, [disabled, cancelLongPress]);\n\n // A close ends any armed click swallow (review H1).\n const open = popover.optimistic;\n useEffect(() => {\n if (!open) disarmSwallow.current?.();\n }, [open]);\n\n // The modality bookkeeping observes `keydown` and `pointerdown` without owning them (the region\n // is full of the consumer's interactive content), in the capture phase, so a descendant's\n // `stopPropagation()` (a slider, a drag handle) does not hide them (review L2).\n const modalityRef = useCallback((element: HTMLElement) => {\n const onKeyDown = () => {\n lastKeyDown.current = ++sequence.current;\n };\n const onPointerDown = () => {\n lastPointerDown.current = ++sequence.current;\n };\n element.addEventListener(\"keydown\", onKeyDown, true);\n element.addEventListener(\"pointerdown\", onPointerDown, true);\n return () => {\n element.removeEventListener(\"keydown\", onKeyDown, true);\n element.removeEventListener(\"pointerdown\", onPointerDown, true);\n };\n }, []);\n\n /** The popup element to capture on and open, or `null` (no Popup mounted: nothing opens). */\n const popupElement = (): HTMLElement | null => {\n const popup = popover.popupRef.current;\n if (popup === null && isDev && !warnedNoPopup.current) {\n warnedNoPopup.current = true;\n console.error(\n \"Menu.ContextTrigger: a context menu was requested but the Menu.Root has no mounted \" +\n \"Menu.Popup; the browser's own menu shows instead.\",\n );\n }\n return popup;\n };\n\n /**\n * One-shot swallow of the `click` an engine may synthesise when the long-press ends (2.2),\n * scoped to that release (review H1): the click comes in the same task as the `pointerup` of\n * `pointerId`, so the swallow ends one task after that `pointerup` (or `pointercancel`), and at\n * once on any `keydown`, the next `pointerdown`, or the menu closing. iOS synthesises no click\n * after a long-press; a swallow left armed would eat the next keyboard activation\n * (`element.click()`).\n */\n const armClickSwallow = (doc: Document, pointerId: number) => {\n disarmSwallow.current?.();\n let released: ReturnType<typeof setTimeout> | undefined;\n const onClick = (event: Event) => {\n event.preventDefault();\n event.stopPropagation();\n disarm();\n };\n const onRelease = (event: Event) => {\n if ((event as globalThis.PointerEvent).pointerId !== pointerId) return;\n if (released === undefined) released = setTimeout(disarm, 0);\n };\n const disarm = () => {\n if (released !== undefined) clearTimeout(released);\n doc.removeEventListener(\"click\", onClick, true);\n doc.removeEventListener(\"pointerdown\", disarm, true);\n doc.removeEventListener(\"keydown\", disarm, true);\n doc.removeEventListener(\"pointerup\", onRelease, true);\n doc.removeEventListener(\"pointercancel\", onRelease, true);\n if (disarmSwallow.current === disarm) disarmSwallow.current = null;\n };\n doc.addEventListener(\"click\", onClick, true);\n doc.addEventListener(\"pointerdown\", disarm, true);\n doc.addEventListener(\"keydown\", disarm, true);\n doc.addEventListener(\"pointerup\", onRelease, true);\n doc.addEventListener(\"pointercancel\", onRelease, true);\n disarmSwallow.current = disarm;\n };\n\n /**\n * The event comes from this Root's own popup, rendered inside the region (a table row, where a\n * `<div>` cannot be a sibling of the `<tr>`): never a move or a long-press of the region.\n */\n const fromOwnPopup = (target: EventTarget | null): boolean => {\n const popup = popover.popupRef.current;\n return popup !== null && target instanceof Node && popup.contains(target);\n };\n\n const onContextMenu = (event: MouseEvent<HTMLElement>) => {\n const native = event.nativeEvent;\n // An inner region or a consumer handler was first (the Popup's own handler prevents a\n // right-click inside the menu); a disabled region lets the event bubble on to an outer\n // region (design 2.4).\n if (native.defaultPrevented || disabled || fromOwnPopup(native.target)) return;\n // Copy and paste keep the browser's menu (2.3).\n if (editableTargets === \"native\" && isEditableTarget(native.target)) return;\n const popup = popupElement();\n if (popup === null) return;\n native.preventDefault();\n markHandled(native, menu);\n // Android and pen: the platform's own `contextmenu` arrived during our timer; it opens.\n cancelLongPress();\n const keyboard = native.button === -1 || lastKeyDown.current > lastPointerDown.current;\n const pointerId = (native as globalThis.PointerEvent).pointerId;\n const id = typeof pointerId === \"number\" ? pointerId : undefined;\n // whatwg/html#10905: in Firefox and WebKit the pending `pointerup` would light-dismiss a\n // menu opened between `pointerdown` and `pointerup`. Capture now, on the still-hidden popup:\n // the open commit lands at an unpredictable time relative to the `pointerup`.\n if (native.buttons !== 0) capturePointer(popup, id);\n menu.openAt(\n { x: native.clientX, y: native.clientY },\n { source: event.currentTarget, event: native, pointer: !keyboard, pointerId: id },\n );\n };\n\n // The long-press only; the modality bookkeeping is `modalityRef`'s.\n const onPointerDown = (event: PointerEvent<HTMLElement>) => {\n const native = event.nativeEvent;\n if (longPress.current !== null) {\n // A second finger: not a long-press.\n cancelLongPress();\n return;\n }\n if (disabled || (native.pointerType !== \"touch\" && native.pointerType !== \"pen\")) return;\n // A press inside any Menu.Popup (this Root's, or another menu inside the region) is claimed by\n // that popup; a press an inner region already times is claimed by it.\n if (fromOwnPopup(native.target)) return;\n if (!native.isPrimary || isPressClaimed(native)) return;\n if (editableTargets === \"native\" && isEditableTarget(native.target)) return;\n // The innermost enabled region times the press; outer regions leave it (as `contextmenu`'s\n // `defaultPrevented` rule, design 2.4).\n claimPress(native);\n const source = event.currentTarget;\n const { pointerId, clientX, clientY } = native;\n const timer = setTimeout(\n () => {\n longPress.current = null;\n const popup = popupElement();\n if (popup === null) return;\n // The finger is still down: its `pointerup` would light-dismiss in WebKit.\n capturePointer(popup, pointerId);\n armClickSwallow(source.ownerDocument, pointerId);\n menu.openAt(\n { x: clientX, y: clientY },\n { source, event: native, pointer: true, pointerId },\n );\n },\n Math.max(0, longPressDelay),\n );\n longPress.current = { pointerId, x: clientX, y: clientY, timer };\n };\n\n const onPointerMove = (event: PointerEvent<HTMLElement>) => {\n const press = longPress.current;\n if (press === null || event.pointerId !== press.pointerId) return;\n if (Math.hypot(event.clientX - press.x, event.clientY - press.y) > LONG_PRESS_SLOP) {\n cancelLongPress();\n }\n };\n\n // `pointercancel`: the browser took the gesture for panning (the region stays scrollable).\n const onPointerEnd = (event: PointerEvent<HTMLElement>) => {\n const press = longPress.current;\n if (press !== null && event.pointerId === press.pointerId) cancelLongPress();\n };\n\n const state: MenuContextTriggerState = {\n open: popover.optimistic,\n pending: popover.pending,\n disabled,\n };\n\n const libraryProps: ComponentPropsWithRef<\"div\"> & DataAttributes = {\n onContextMenu,\n onPointerDown,\n onPointerMove,\n onPointerUp: onPointerEnd,\n onPointerCancel: onPointerEnd,\n \"data-open\": dataAttr(state.open),\n \"data-pending\": dataAttr(state.pending),\n \"data-disabled\": dataAttr(disabled),\n };\n\n return useRender<ComponentPropsWithRef<\"div\">, MenuContextTriggerState>({\n defaultElement: \"div\",\n render,\n state,\n className,\n // iOS link and image previews appear at about the long-press time (2.2).\n style: (s) => ({ WebkitTouchCallout: \"none\", ...resolveStateValue(style, s) }),\n // A Menu.Root inside is a nested region's menu (no \"outside its Popup\" warning).\n children: (s) => (\n <MenuRegionContext value={true}>{resolveStateValue(children, s)}</MenuRegionContext>\n ),\n props: rest,\n libraryProps,\n ref: modalityRef,\n });\n}\n\nMenuContextTrigger.displayName = \"MenuContextTrigger\";\n"],"mappings":";;;;;;;;;;;AA2DA,MAAM,kBAAkB;AAExB,MAAM,QAAQ,QAAQ,IAAI,aAAa;;;;;;;;;;;;;;;AAuBvC,SAAgB,mBAAmB,EACjC,WAAW,OACX,kBAAkB,UAClB,iBAAiB,KACjB,WACA,OACA,UACA,QACA,GAAG,QACuB;CAC1B,MAAM,OAAO,eAAe,qBAAqB;CACjD,MAAM,EAAE,YAAY;CACpB,cACE,KAAK,SACL,mJAEF;CACA,cACE,iBAAiB,GACjB,oFACF;CAIA,MAAM,WAAW,OAAO,CAAC;CACzB,MAAM,kBAAkB,OAAO,EAAE;CACjC,MAAM,cAAc,OAAO,EAAE;CAC7B,MAAM,YAAY,OAAyB,IAAI;CAE/C,MAAM,gBAAgB,OAA4B,IAAI;CACtD,MAAM,gBAAgB,OAAO,KAAK;CAElC,MAAM,kBAAkB,kBAAkB;EACxC,IAAI,UAAU,YAAY,MAAM,aAAa,UAAU,QAAQ,KAAK;EACpE,UAAU,UAAU;CACtB,GAAG,CAAC,CAAC;CAEL,sBACc;EACV,gBAAgB;EAChB,cAAc,UAAU;CAC1B,GACA,CAAC,eAAe,CAClB;CAEA,gBAAgB;EACd,IAAI,UAAU,gBAAgB;CAChC,GAAG,CAAC,UAAU,eAAe,CAAC;CAG9B,MAAM,OAAO,QAAQ;CACrB,gBAAgB;EACd,IAAI,CAAC,MAAM,cAAc,UAAU;CACrC,GAAG,CAAC,IAAI,CAAC;CAKT,MAAM,cAAc,aAAa,YAAyB;EACxD,MAAM,kBAAkB;GACtB,YAAY,UAAU,EAAE,SAAS;EACnC;EACA,MAAM,sBAAsB;GAC1B,gBAAgB,UAAU,EAAE,SAAS;EACvC;EACA,QAAQ,iBAAiB,WAAW,WAAW,IAAI;EACnD,QAAQ,iBAAiB,eAAe,eAAe,IAAI;EAC3D,aAAa;GACX,QAAQ,oBAAoB,WAAW,WAAW,IAAI;GACtD,QAAQ,oBAAoB,eAAe,eAAe,IAAI;EAChE;CACF,GAAG,CAAC,CAAC;;CAGL,MAAM,qBAAyC;EAC7C,MAAM,QAAQ,QAAQ,SAAS;EAC/B,IAAI,UAAU,QAAQ,SAAS,CAAC,cAAc,SAAS;GACrD,cAAc,UAAU;GACxB,QAAQ,MACN,sIAEF;EACF;EACA,OAAO;CACT;;;;;;;;;CAUA,MAAM,mBAAmB,KAAe,cAAsB;EAC5D,cAAc,UAAU;EACxB,IAAI;EACJ,MAAM,WAAW,UAAiB;GAChC,MAAM,eAAe;GACrB,MAAM,gBAAgB;GACtB,OAAO;EACT;EACA,MAAM,aAAa,UAAiB;GAClC,IAAK,MAAkC,cAAc,WAAW;GAChE,IAAI,aAAa,KAAA,GAAW,WAAW,WAAW,QAAQ,CAAC;EAC7D;EACA,MAAM,eAAe;GACnB,IAAI,aAAa,KAAA,GAAW,aAAa,QAAQ;GACjD,IAAI,oBAAoB,SAAS,SAAS,IAAI;GAC9C,IAAI,oBAAoB,eAAe,QAAQ,IAAI;GACnD,IAAI,oBAAoB,WAAW,QAAQ,IAAI;GAC/C,IAAI,oBAAoB,aAAa,WAAW,IAAI;GACpD,IAAI,oBAAoB,iBAAiB,WAAW,IAAI;GACxD,IAAI,cAAc,YAAY,QAAQ,cAAc,UAAU;EAChE;EACA,IAAI,iBAAiB,SAAS,SAAS,IAAI;EAC3C,IAAI,iBAAiB,eAAe,QAAQ,IAAI;EAChD,IAAI,iBAAiB,WAAW,QAAQ,IAAI;EAC5C,IAAI,iBAAiB,aAAa,WAAW,IAAI;EACjD,IAAI,iBAAiB,iBAAiB,WAAW,IAAI;EACrD,cAAc,UAAU;CAC1B;;;;;CAMA,MAAM,gBAAgB,WAAwC;EAC5D,MAAM,QAAQ,QAAQ,SAAS;EAC/B,OAAO,UAAU,QAAQ,kBAAkB,QAAQ,MAAM,SAAS,MAAM;CAC1E;CAEA,MAAM,iBAAiB,UAAmC;EACxD,MAAM,SAAS,MAAM;EAIrB,IAAI,OAAO,oBAAoB,YAAY,aAAa,OAAO,MAAM,GAAG;EAExE,IAAI,oBAAoB,YAAY,iBAAiB,OAAO,MAAM,GAAG;EACrE,MAAM,QAAQ,aAAa;EAC3B,IAAI,UAAU,MAAM;EACpB,OAAO,eAAe;EACtB,YAAY,QAAQ,IAAI;EAExB,gBAAgB;EAChB,MAAM,WAAW,OAAO,WAAW,MAAM,YAAY,UAAU,gBAAgB;EAC/E,MAAM,YAAa,OAAmC;EACtD,MAAM,KAAK,OAAO,cAAc,WAAW,YAAY,KAAA;EAIvD,IAAI,OAAO,YAAY,GAAG,eAAe,OAAO,EAAE;EAClD,KAAK,OACH;GAAE,GAAG,OAAO;GAAS,GAAG,OAAO;EAAQ,GACvC;GAAE,QAAQ,MAAM;GAAe,OAAO;GAAQ,SAAS,CAAC;GAAU,WAAW;EAAG,CAClF;CACF;CAGA,MAAM,iBAAiB,UAAqC;EAC1D,MAAM,SAAS,MAAM;EACrB,IAAI,UAAU,YAAY,MAAM;GAE9B,gBAAgB;GAChB;EACF;EACA,IAAI,YAAa,OAAO,gBAAgB,WAAW,OAAO,gBAAgB,OAAQ;EAGlF,IAAI,aAAa,OAAO,MAAM,GAAG;EACjC,IAAI,CAAC,OAAO,aAAa,eAAe,MAAM,GAAG;EACjD,IAAI,oBAAoB,YAAY,iBAAiB,OAAO,MAAM,GAAG;EAGrE,WAAW,MAAM;EACjB,MAAM,SAAS,MAAM;EACrB,MAAM,EAAE,WAAW,SAAS,YAAY;EACxC,MAAM,QAAQ,iBACN;GACJ,UAAU,UAAU;GACpB,MAAM,QAAQ,aAAa;GAC3B,IAAI,UAAU,MAAM;GAEpB,eAAe,OAAO,SAAS;GAC/B,gBAAgB,OAAO,eAAe,SAAS;GAC/C,KAAK,OACH;IAAE,GAAG;IAAS,GAAG;GAAQ,GACzB;IAAE;IAAQ,OAAO;IAAQ,SAAS;IAAM;GAAU,CACpD;EACF,GACA,KAAK,IAAI,GAAG,cAAc,CAC5B;EACA,UAAU,UAAU;GAAE;GAAW,GAAG;GAAS,GAAG;GAAS;EAAM;CACjE;CAEA,MAAM,iBAAiB,UAAqC;EAC1D,MAAM,QAAQ,UAAU;EACxB,IAAI,UAAU,QAAQ,MAAM,cAAc,MAAM,WAAW;EAC3D,IAAI,KAAK,MAAM,MAAM,UAAU,MAAM,GAAG,MAAM,UAAU,MAAM,CAAC,IAAI,iBACjE,gBAAgB;CAEpB;CAGA,MAAM,gBAAgB,UAAqC;EACzD,MAAM,QAAQ,UAAU;EACxB,IAAI,UAAU,QAAQ,MAAM,cAAc,MAAM,WAAW,gBAAgB;CAC7E;CAEA,MAAM,QAAiC;EACrC,MAAM,QAAQ;EACd,SAAS,QAAQ;EACjB;CACF;CAEA,MAAM,eAA8D;EAClE;EACA;EACA;EACA,aAAa;EACb,iBAAiB;EACjB,aAAa,SAAS,MAAM,IAAI;EAChC,gBAAgB,SAAS,MAAM,OAAO;EACtC,iBAAiB,SAAS,QAAQ;CACpC;CAEA,OAAO,UAAiE;EACtE,gBAAgB;EAChB;EACA;EACA;EAEA,QAAQ,OAAO;GAAE,oBAAoB;GAAQ,GAAG,kBAAkB,OAAO,CAAC;EAAE;EAE5E,WAAW,MACT,oBAAC,mBAAD;GAAmB,OAAO;GAAO,UAAA,kBAAkB,UAAU,CAAC;EAAqB,CAAA;EAErF,OAAO;EACP;EACA,KAAK;CACP,CAAC;AACH;AAEA,mBAAmB,cAAc"}
|
|
@@ -1,8 +1,41 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { createContext, use } from "react";
|
|
3
3
|
//#region src/menu/menu-context.ts
|
|
4
|
+
const ANCHOR_PLACEMENT = { kind: "anchor" };
|
|
4
5
|
const MenuContext = createContext(null);
|
|
5
6
|
/**
|
|
7
|
+
* `true` inside a `Menu.ContextTrigger`: a `Menu.Root` there is a nested context region's own
|
|
8
|
+
* top-level menu (context-menu.md 2.4), not a misplaced submenu.
|
|
9
|
+
*/
|
|
10
|
+
const MenuRegionContext = createContext(false);
|
|
11
|
+
/**
|
|
12
|
+
* `contextmenu` events a ContextTrigger handled (opened or moved its menu), with the top-level
|
|
13
|
+
* menu they belong to: that menu's document listener leaves its menu open for them
|
|
14
|
+
* (context-menu.md 5 and 14).
|
|
15
|
+
*/
|
|
16
|
+
const handledEvents = /* @__PURE__ */ new WeakMap();
|
|
17
|
+
/**
|
|
18
|
+
* `pointerdown` events that are not a long-press of an enclosing region: claimed by the innermost
|
|
19
|
+
* region that started its timer for them (nested regions), and by every `Menu.Popup` for the
|
|
20
|
+
* presses inside it (a menu inside a region, review M1). As `contextmenu`'s `defaultPrevented`
|
|
21
|
+
* rule (context-menu.md 2.4).
|
|
22
|
+
*/
|
|
23
|
+
const claimedPresses = /* @__PURE__ */ new WeakSet();
|
|
24
|
+
function claimPress(event) {
|
|
25
|
+
claimedPresses.add(event);
|
|
26
|
+
}
|
|
27
|
+
function isPressClaimed(event) {
|
|
28
|
+
return claimedPresses.has(event);
|
|
29
|
+
}
|
|
30
|
+
/** The top-level menu's identity: its popup ref object, stable for the Root's lifetime. */
|
|
31
|
+
const menuKey = (menu) => menu.root.popover.popupRef;
|
|
32
|
+
function markHandled(event, menu) {
|
|
33
|
+
handledEvents.set(event, menuKey(menu));
|
|
34
|
+
}
|
|
35
|
+
function isHandledBy(event, menu) {
|
|
36
|
+
return handledEvents.get(event) === menuKey(menu);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
6
39
|
* The close an activated item runs once its action settled: the whole stack, through the
|
|
7
40
|
* top-level Root (design 2.2). Recorded at activation: if the menu was dismissed and opened again
|
|
8
41
|
* meanwhile, the close belongs to the previous opening and does nothing (as `useActionTrigger`).
|
|
@@ -23,6 +56,6 @@ const MenuListContext = createContext(null);
|
|
|
23
56
|
const MenuGroupContext = createContext(null);
|
|
24
57
|
const MenuRadioGroupContext = createContext(null);
|
|
25
58
|
//#endregion
|
|
26
|
-
export { MenuContext, MenuGroupContext, MenuListContext, MenuRadioGroupContext, closeStackLater, useMenuContext };
|
|
59
|
+
export { ANCHOR_PLACEMENT, MenuContext, MenuGroupContext, MenuListContext, MenuRadioGroupContext, MenuRegionContext, claimPress, closeStackLater, isHandledBy, isPressClaimed, markHandled, useMenuContext };
|
|
27
60
|
|
|
28
61
|
//# sourceMappingURL=menu-context.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu-context.mjs","names":[],"sources":["../../src/menu/menu-context.ts"],"sourcesContent":["\"use client\";\nimport { createContext, use } from \"react\";\nimport type { DelayedPair } from \"../internal/use-delayed-pair.js\";\nimport type { PopoverRootValue } from \"../internal/use-popover-root.js\";\n\n/**\n * Where focus goes when the menu opens (design/components/menu.md 6.1): the first enabled item,\n * the last one (ArrowUp on the Trigger), or nowhere (a hover-opened submenu keeps focus on its\n * SubmenuTrigger).\n */\nexport type MenuOpenCause = \"first\" | \"last\" | \"none\";\n\n/** How the open being shown should place focus (design 6.1; review H1). */\nexport interface MenuOpenFocus {\n readonly cause: MenuOpenCause;\n /**\n * The open came from a pointer click (`detail > 0`): the item is focused without the focus\n * ring where the engine supports `focus({ focusVisible: false })`.\n */\n readonly pointer: boolean;\n}\n\n/** One menu level: what its Root provides to its Trigger, Popup and items. */\nexport interface MenuContextValue {\n /** The shared popover machinery (`usePopoverRoot`). */\n readonly popover: PopoverRootValue;\n /** This Root is nested in a `Menu.Popup`. */\n readonly submenu: boolean;\n /** The top-level menu: item activation closes the whole stack through it (design 2.2). */\n readonly root: MenuContextValue;\n /** The enclosing menu of a submenu; `null` at the top level. */\n readonly parent: MenuContextValue | null;\n readonly loop: boolean;\n readonly delay: number;\n readonly closeDelay: number;\n /** The Trigger's id (`aria-labelledby` of the Popup). */\n readonly triggerId: string;\n /** Record the focus target of the next open (and whether a pointer asked for it). */\n readonly setOpenCause: (cause: MenuOpenCause, pointer?: boolean) => void;\n /** Read and clear the focus target of the open being shown (default `\"first\"`, keyboard). */\n readonly takeOpenCause: () => MenuOpenFocus;\n /** Submenu hover-intent timers, shared by the SubmenuTrigger and the Popup. */\n readonly timers: DelayedPair;\n}\n\nexport const MenuContext = createContext<MenuContextValue | null>(null);\n\n/**\n * The close an activated item runs once its action settled: the whole stack, through the\n * top-level Root (design 2.2). Recorded at activation: if the menu was dismissed and opened again\n * meanwhile, the close belongs to the previous opening and does nothing (as `useActionTrigger`).\n */\nexport function closeStackLater(menu: MenuContextValue, event: Event): () => void {\n const { popover } = menu.root;\n const generation = popover.openGeneration();\n return () => {\n if (popover.openGeneration() === generation) popover.requestClose(event, \"activate\");\n };\n}\n\nexport function useMenuContext(part: string): MenuContextValue {\n const context = use(MenuContext);\n if (context === null) {\n throw new Error(`${part} must be rendered inside a Menu.Root.`);\n }\n return context;\n}\n\n/** What a `Menu.Popup` tells the items rendered in it. */\nexport interface MenuListContextValue {\n /** The menu this popup belongs to. */\n readonly menu: MenuContextValue;\n /**\n * `experimental_focusgroup` is on and the engine implements it: items render no `tabindex`\n * (Chromium leaves `tabindex=\"-1\"` elements out of a focusgroup) and the JS arrow keys step\n * aside.\n */\n readonly nativeFocusgroup: boolean;\n /** A typeahead buffer is active: Space is a character, not an activation (design 5.1). */\n readonly typingAhead: () => boolean;\n}\n\nexport const MenuListContext = createContext<MenuListContextValue | null>(null);\n\n/** `Menu.Group` / `Menu.RadioGroup`: the id their `Menu.GroupLabel` renders. */\nexport interface MenuGroupContextValue {\n readonly labelId: string;\n}\n\nexport const MenuGroupContext = createContext<MenuGroupContextValue | null>(null);\n\n/** `Menu.RadioGroup`'s state for its `Menu.RadioItem`s. */\nexport interface MenuRadioGroupContextValue {\n /** Optimistic value. */\n readonly value: string | null;\n readonly pending: boolean;\n /**\n * Dispatch `value` through the group's transition; `then` runs once the change action\n * resolved (an item's `closeOnActivate`).\n */\n readonly select: (value: string, event: Event, then: (() => void) | undefined) => void;\n}\n\nexport const MenuRadioGroupContext = createContext<MenuRadioGroupContextValue | null>(null);\n"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"menu-context.mjs","names":[],"sources":["../../src/menu/menu-context.ts"],"sourcesContent":["\"use client\";\nimport { createContext, use } from \"react\";\nimport type { DelayedPair } from \"../internal/use-delayed-pair.js\";\nimport type { PopoverRootValue } from \"../internal/use-popover-root.js\";\n\n/**\n * Where focus goes when the menu opens (design/components/menu.md 6.1): the first enabled item,\n * the last one (ArrowUp on the Trigger), or nowhere (a hover-opened submenu keeps focus on its\n * SubmenuTrigger).\n */\nexport type MenuOpenCause = \"first\" | \"last\" | \"none\";\n\n/** How the open being shown should place focus (design 6.1; review H1). */\nexport interface MenuOpenFocus {\n readonly cause: MenuOpenCause;\n /**\n * The open came from a pointer click (`detail > 0`): the item is focused without the focus\n * ring where the engine supports `focus({ focusVisible: false })`.\n */\n readonly pointer: boolean;\n}\n\n/**\n * How the current open is placed (design/components/context-menu.md 3.1): against the Trigger\n * (`anchor`: a Menu.Trigger, a foreign invoker, a controlled or platform open), or at a point\n * (`point`: a Menu.ContextTrigger). `generation` grows with every point open or move, so a second\n * right-click at a new point is a state change while the menu is open (4.3).\n */\nexport type MenuPlacement =\n | { readonly kind: \"anchor\" }\n | {\n readonly kind: \"point\";\n readonly x: number;\n readonly y: number;\n readonly generation: number;\n /** The region that opened: the `source` of `showPopover({ source })` (nesting). */\n readonly source: HTMLElement;\n /** The pointer held down at the open, re-captured after a move's re-show (2.1). */\n readonly pointerId: number | undefined;\n };\n\nexport const ANCHOR_PLACEMENT: MenuPlacement = { kind: \"anchor\" };\n\n/** What a ContextTrigger passes to `openAt`. */\nexport interface MenuOpenAtOptions {\n readonly source: HTMLElement;\n /** The `contextmenu` event, or the long-press `pointerdown` (`details.event`). */\n readonly event: Event;\n /** A pointer opened (quiet focus); `false` for the keyboard (focus ring). */\n readonly pointer: boolean;\n readonly pointerId: number | undefined;\n}\n\n/** One menu level: what its Root provides to its Trigger, Popup and items. */\nexport interface MenuContextValue {\n /** The shared popover machinery (`usePopoverRoot`). */\n readonly popover: PopoverRootValue;\n /** This Root is nested in a `Menu.Popup`. */\n readonly submenu: boolean;\n /** The top-level menu: item activation closes the whole stack through it (design 2.2). */\n readonly root: MenuContextValue;\n /** The enclosing menu of a submenu; `null` at the top level. */\n readonly parent: MenuContextValue | null;\n readonly loop: boolean;\n readonly delay: number;\n readonly closeDelay: number;\n /** The Trigger's id (`aria-labelledby` of the Popup). */\n readonly triggerId: string;\n /** Record the focus target of the next open (and whether a pointer asked for it). */\n readonly setOpenCause: (cause: MenuOpenCause, pointer?: boolean) => void;\n /** Read and clear the focus target of the open being shown (default `\"first\"`, keyboard). */\n readonly takeOpenCause: () => MenuOpenFocus;\n /** Submenu hover-intent timers, shared by the SubmenuTrigger and the Popup. */\n readonly timers: DelayedPair;\n /** Placement of the current (or last) open (context-menu.md 3.1). */\n readonly placement: MenuPlacement;\n /**\n * A ContextTrigger opens the menu at `point`, or moves it there when it is already open (no\n * dispatch, 4.3).\n */\n readonly openAt: (\n point: { readonly x: number; readonly y: number },\n options: MenuOpenAtOptions,\n ) => void;\n /**\n * Back to the anchored placement once a point open has been shown and hidden (the popup is no\n * longer visible): the next open that is not a ContextTrigger's (controlled, platform, foreign\n * invoker) places against the Trigger. Ignored when a newer point was recorded meanwhile.\n */\n readonly resetPlacement: (generation: number) => void;\n}\n\nexport const MenuContext = createContext<MenuContextValue | null>(null);\n\n/**\n * `true` inside a `Menu.ContextTrigger`: a `Menu.Root` there is a nested context region's own\n * top-level menu (context-menu.md 2.4), not a misplaced submenu.\n */\nexport const MenuRegionContext = createContext(false);\n\n/**\n * `contextmenu` events a ContextTrigger handled (opened or moved its menu), with the top-level\n * menu they belong to: that menu's document listener leaves its menu open for them\n * (context-menu.md 5 and 14).\n */\nconst handledEvents = new WeakMap<Event, object>();\n\n/**\n * `pointerdown` events that are not a long-press of an enclosing region: claimed by the innermost\n * region that started its timer for them (nested regions), and by every `Menu.Popup` for the\n * presses inside it (a menu inside a region, review M1). As `contextmenu`'s `defaultPrevented`\n * rule (context-menu.md 2.4).\n */\nconst claimedPresses = new WeakSet<Event>();\n\nexport function claimPress(event: Event): void {\n claimedPresses.add(event);\n}\n\nexport function isPressClaimed(event: Event): boolean {\n return claimedPresses.has(event);\n}\n\n/** The top-level menu's identity: its popup ref object, stable for the Root's lifetime. */\nconst menuKey = (menu: MenuContextValue): object => menu.root.popover.popupRef;\n\nexport function markHandled(event: Event, menu: MenuContextValue): void {\n handledEvents.set(event, menuKey(menu));\n}\n\nexport function isHandledBy(event: Event, menu: MenuContextValue): boolean {\n return handledEvents.get(event) === menuKey(menu);\n}\n\n/**\n * The close an activated item runs once its action settled: the whole stack, through the\n * top-level Root (design 2.2). Recorded at activation: if the menu was dismissed and opened again\n * meanwhile, the close belongs to the previous opening and does nothing (as `useActionTrigger`).\n */\nexport function closeStackLater(menu: MenuContextValue, event: Event): () => void {\n const { popover } = menu.root;\n const generation = popover.openGeneration();\n return () => {\n if (popover.openGeneration() === generation) popover.requestClose(event, \"activate\");\n };\n}\n\nexport function useMenuContext(part: string): MenuContextValue {\n const context = use(MenuContext);\n if (context === null) {\n throw new Error(`${part} must be rendered inside a Menu.Root.`);\n }\n return context;\n}\n\n/** What a `Menu.Popup` tells the items rendered in it. */\nexport interface MenuListContextValue {\n /** The menu this popup belongs to. */\n readonly menu: MenuContextValue;\n /**\n * `experimental_focusgroup` is on and the engine implements it: items render no `tabindex`\n * (Chromium leaves `tabindex=\"-1\"` elements out of a focusgroup) and the JS arrow keys step\n * aside.\n */\n readonly nativeFocusgroup: boolean;\n /** A typeahead buffer is active: Space is a character, not an activation (design 5.1). */\n readonly typingAhead: () => boolean;\n}\n\nexport const MenuListContext = createContext<MenuListContextValue | null>(null);\n\n/** `Menu.Group` / `Menu.RadioGroup`: the id their `Menu.GroupLabel` renders. */\nexport interface MenuGroupContextValue {\n readonly labelId: string;\n}\n\nexport const MenuGroupContext = createContext<MenuGroupContextValue | null>(null);\n\n/** `Menu.RadioGroup`'s state for its `Menu.RadioItem`s. */\nexport interface MenuRadioGroupContextValue {\n /** Optimistic value. */\n readonly value: string | null;\n readonly pending: boolean;\n /**\n * Dispatch `value` through the group's transition; `then` runs once the change action\n * resolved (an item's `closeOnActivate`).\n */\n readonly select: (value: string, event: Event, then: (() => void) | undefined) => void;\n}\n\nexport const MenuRadioGroupContext = createContext<MenuRadioGroupContextValue | null>(null);\n"],"mappings":";;;AAyCA,MAAa,mBAAkC,EAAE,MAAM,SAAS;AAmDhE,MAAa,cAAc,cAAuC,IAAI;;;;;AAMtE,MAAa,oBAAoB,cAAc,KAAK;;;;;;AAOpD,MAAM,gCAAgB,IAAI,QAAuB;;;;;;;AAQjD,MAAM,iCAAiB,IAAI,QAAe;AAE1C,SAAgB,WAAW,OAAoB;CAC7C,eAAe,IAAI,KAAK;AAC1B;AAEA,SAAgB,eAAe,OAAuB;CACpD,OAAO,eAAe,IAAI,KAAK;AACjC;;AAGA,MAAM,WAAW,SAAmC,KAAK,KAAK,QAAQ;AAEtE,SAAgB,YAAY,OAAc,MAA8B;CACtE,cAAc,IAAI,OAAO,QAAQ,IAAI,CAAC;AACxC;AAEA,SAAgB,YAAY,OAAc,MAAiC;CACzE,OAAO,cAAc,IAAI,KAAK,MAAM,QAAQ,IAAI;AAClD;;;;;;AAOA,SAAgB,gBAAgB,MAAwB,OAA0B;CAChF,MAAM,EAAE,YAAY,KAAK;CACzB,MAAM,aAAa,QAAQ,eAAe;CAC1C,aAAa;EACX,IAAI,QAAQ,eAAe,MAAM,YAAY,QAAQ,aAAa,OAAO,UAAU;CACrF;AACF;AAEA,SAAgB,eAAe,MAAgC;CAC7D,MAAM,UAAU,IAAI,WAAW;CAC/B,IAAI,YAAY,MACd,MAAM,IAAI,MAAM,GAAG,KAAK,sCAAsC;CAEhE,OAAO;AACT;AAgBA,MAAa,kBAAkB,cAA2C,IAAI;AAO9E,MAAa,mBAAmB,cAA4C,IAAI;AAchF,MAAa,wBAAwB,cAAiD,IAAI"}
|
package/dist/menu/menu-dom.mjs
CHANGED
|
@@ -8,8 +8,14 @@ function menuItems(popup) {
|
|
|
8
8
|
function isMenuItem(element, popup) {
|
|
9
9
|
return element.matches(ITEM_SELECTOR) && element.closest("[role=\"menu\"]") === popup;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Disabled by its `disabled` prop. Keys on `data-disabled`, not `aria-disabled`: an item is also
|
|
13
|
+
* `aria-disabled` while its own action is pending (#81). A pending item is an ordinary item for
|
|
14
|
+
* navigation; only the open focus (`edgeItem`) skips items with the `disabled` prop. Arrow keys,
|
|
15
|
+
* Home / End and typeahead skip nothing.
|
|
16
|
+
*/
|
|
11
17
|
function isDisabledItem(item) {
|
|
12
|
-
return item.
|
|
18
|
+
return item.hasAttribute("data-disabled");
|
|
13
19
|
}
|
|
14
20
|
/** The first (or last) enabled item; the first (or last) item if all are disabled. */
|
|
15
21
|
function edgeItem(popup, which) {
|
|
@@ -28,10 +34,46 @@ function typeaheadText(item) {
|
|
|
28
34
|
function isRtl(element) {
|
|
29
35
|
return getComputedStyle(element).direction === "rtl";
|
|
30
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* `setPointerCapture`, which throws `NotFoundError` for a pointer the engine no longer tracks
|
|
39
|
+
* (context-menu.md 2.1). A silent no-op when no button is down.
|
|
40
|
+
*/
|
|
41
|
+
function capturePointer(element, pointerId) {
|
|
42
|
+
if (element === null || pointerId === void 0) return;
|
|
43
|
+
try {
|
|
44
|
+
element.setPointerCapture(pointerId);
|
|
45
|
+
} catch {}
|
|
46
|
+
}
|
|
47
|
+
/** `<input>` types that take no text: their `contextmenu` is not an editing one. */
|
|
48
|
+
const NON_TEXT_INPUTS = /* @__PURE__ */ new Set([
|
|
49
|
+
"button",
|
|
50
|
+
"submit",
|
|
51
|
+
"reset",
|
|
52
|
+
"checkbox",
|
|
53
|
+
"radio",
|
|
54
|
+
"range",
|
|
55
|
+
"color",
|
|
56
|
+
"file",
|
|
57
|
+
"image",
|
|
58
|
+
"hidden"
|
|
59
|
+
]);
|
|
60
|
+
/**
|
|
61
|
+
* An editable target keeps the browser's own context menu by default (context-menu.md 2.3): a
|
|
62
|
+
* text-taking `<input>`, a `<textarea>`, or editable content.
|
|
63
|
+
*/
|
|
64
|
+
function isEditableTarget(target) {
|
|
65
|
+
if (target === null || target.nodeType !== 1) return false;
|
|
66
|
+
const element = target;
|
|
67
|
+
if (element.closest("textarea") !== null) return true;
|
|
68
|
+
const input = element.closest("input");
|
|
69
|
+
if (input !== null && !NON_TEXT_INPUTS.has(input.type)) return true;
|
|
70
|
+
if (typeof element.isContentEditable === "boolean") return element.isContentEditable;
|
|
71
|
+
return element.closest("[contenteditable]:not([contenteditable=\"false\"])") !== null;
|
|
72
|
+
}
|
|
31
73
|
function hasModifier(event) {
|
|
32
74
|
return event.altKey || event.ctrlKey || event.metaKey;
|
|
33
75
|
}
|
|
34
76
|
//#endregion
|
|
35
|
-
export { edgeItem, focusItem, hasModifier, isDisabledItem, isMenuItem, isRtl, menuItems, typeaheadText };
|
|
77
|
+
export { capturePointer, edgeItem, focusItem, hasModifier, isDisabledItem, isEditableTarget, isMenuItem, isRtl, menuItems, typeaheadText };
|
|
36
78
|
|
|
37
79
|
//# sourceMappingURL=menu-dom.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu-dom.mjs","names":[],"sources":["../../src/menu/menu-dom.ts"],"sourcesContent":["\"use client\";\n// DOM helpers shared by the Menu parts. Items are read from the DOM when they are needed (a key\n// press, an open), never registered (design/components/menu.md 13 and 14).\n\nconst ITEM_SELECTOR = '[role=\"menuitem\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"]';\n\n/** The items of `popup` in DOM order, without the items of its submenus. Disabled included. */\nexport function menuItems(popup: Element): HTMLElement[] {\n return Array.from(popup.querySelectorAll<HTMLElement>(ITEM_SELECTOR)).filter(\n (item) => item.closest('[role=\"menu\"]') === popup,\n );\n}\n\nexport function isMenuItem(element: Element, popup: Element): boolean {\n return element.matches(ITEM_SELECTOR) && element.closest('[role=\"menu\"]') === popup;\n}\n\nexport function isDisabledItem(item: Element): boolean {\n return item.
|
|
1
|
+
{"version":3,"file":"menu-dom.mjs","names":[],"sources":["../../src/menu/menu-dom.ts"],"sourcesContent":["\"use client\";\n// DOM helpers shared by the Menu parts. Items are read from the DOM when they are needed (a key\n// press, an open), never registered (design/components/menu.md 13 and 14).\n\nconst ITEM_SELECTOR = '[role=\"menuitem\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"]';\n\n/** The items of `popup` in DOM order, without the items of its submenus. Disabled included. */\nexport function menuItems(popup: Element): HTMLElement[] {\n return Array.from(popup.querySelectorAll<HTMLElement>(ITEM_SELECTOR)).filter(\n (item) => item.closest('[role=\"menu\"]') === popup,\n );\n}\n\nexport function isMenuItem(element: Element, popup: Element): boolean {\n return element.matches(ITEM_SELECTOR) && element.closest('[role=\"menu\"]') === popup;\n}\n\n/**\n * Disabled by its `disabled` prop. Keys on `data-disabled`, not `aria-disabled`: an item is also\n * `aria-disabled` while its own action is pending (#81). A pending item is an ordinary item for\n * navigation; only the open focus (`edgeItem`) skips items with the `disabled` prop. Arrow keys,\n * Home / End and typeahead skip nothing.\n */\nexport function isDisabledItem(item: Element): boolean {\n return item.hasAttribute(\"data-disabled\");\n}\n\n/** The first (or last) enabled item; the first (or last) item if all are disabled. */\nexport function edgeItem(popup: Element, which: \"first\" | \"last\"): HTMLElement | null {\n const items = menuItems(popup);\n const ordered = which === \"first\" ? items : [...items].reverse();\n return ordered.find((item) => !isDisabledItem(item)) ?? ordered[0] ?? null;\n}\n\n/** Keyboard focus: the ring shows even where the engine's heuristic would not (as Tabs). */\nexport function focusItem(item: HTMLElement): void {\n item.focus({ focusVisible: true } as FocusOptions);\n}\n\n/** The text typeahead matches: the item's `label` prop, else its text content. */\nexport function typeaheadText(item: Element): string {\n return (item.getAttribute(\"data-typeahead\") ?? item.textContent ?? \"\").trim().toLowerCase();\n}\n\nexport function isRtl(element: Element): boolean {\n return getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * `setPointerCapture`, which throws `NotFoundError` for a pointer the engine no longer tracks\n * (context-menu.md 2.1). A silent no-op when no button is down.\n */\nexport function capturePointer(element: Element | null, pointerId: number | undefined): void {\n if (element === null || pointerId === undefined) return;\n try {\n element.setPointerCapture(pointerId);\n } catch {\n // Not an active pointer (a synthetic event, a released button): nothing to capture.\n }\n}\n\n/** `<input>` types that take no text: their `contextmenu` is not an editing one. */\nconst NON_TEXT_INPUTS = new Set([\n \"button\",\n \"submit\",\n \"reset\",\n \"checkbox\",\n \"radio\",\n \"range\",\n \"color\",\n \"file\",\n \"image\",\n \"hidden\",\n]);\n\n/**\n * An editable target keeps the browser's own context menu by default (context-menu.md 2.3): a\n * text-taking `<input>`, a `<textarea>`, or editable content.\n */\nexport function isEditableTarget(target: EventTarget | null): boolean {\n if (target === null || (target as Node).nodeType !== 1) return false;\n const element = target as HTMLElement;\n if (element.closest(\"textarea\") !== null) return true;\n const input = element.closest(\"input\");\n if (input !== null && !NON_TEXT_INPUTS.has(input.type)) return true;\n // `isContentEditable` where implemented (every engine; not jsdom).\n if (typeof element.isContentEditable === \"boolean\") return element.isContentEditable;\n return element.closest('[contenteditable]:not([contenteditable=\"false\"])') !== null;\n}\n\nexport function hasModifier(event: { altKey: boolean; ctrlKey: boolean; metaKey: boolean }) {\n return event.altKey || event.ctrlKey || event.metaKey;\n}\n"],"mappings":";;AAIA,MAAM,gBAAgB;;AAGtB,SAAgB,UAAU,OAA+B;CACvD,OAAO,MAAM,KAAK,MAAM,iBAA8B,aAAa,CAAC,CAAC,CAAC,QACnE,SAAS,KAAK,QAAQ,iBAAe,MAAM,KAC9C;AACF;AAEA,SAAgB,WAAW,SAAkB,OAAyB;CACpE,OAAO,QAAQ,QAAQ,aAAa,KAAK,QAAQ,QAAQ,iBAAe,MAAM;AAChF;;;;;;;AAQA,SAAgB,eAAe,MAAwB;CACrD,OAAO,KAAK,aAAa,eAAe;AAC1C;;AAGA,SAAgB,SAAS,OAAgB,OAA6C;CACpF,MAAM,QAAQ,UAAU,KAAK;CAC7B,MAAM,UAAU,UAAU,UAAU,QAAQ,CAAC,GAAG,KAAK,CAAC,CAAC,QAAQ;CAC/D,OAAO,QAAQ,MAAM,SAAS,CAAC,eAAe,IAAI,CAAC,KAAK,QAAQ,MAAM;AACxE;;AAGA,SAAgB,UAAU,MAAyB;CACjD,KAAK,MAAM,EAAE,cAAc,KAAK,CAAiB;AACnD;;AAGA,SAAgB,cAAc,MAAuB;CACnD,QAAQ,KAAK,aAAa,gBAAgB,KAAK,KAAK,eAAe,GAAA,CAAI,KAAK,CAAC,CAAC,YAAY;AAC5F;AAEA,SAAgB,MAAM,SAA2B;CAC/C,OAAO,iBAAiB,OAAO,CAAC,CAAC,cAAc;AACjD;;;;;AAMA,SAAgB,eAAe,SAAyB,WAAqC;CAC3F,IAAI,YAAY,QAAQ,cAAc,KAAA,GAAW;CACjD,IAAI;EACF,QAAQ,kBAAkB,SAAS;CACrC,QAAQ,CAER;AACF;;AAGA,MAAM,kCAAkB,IAAI,IAAI;CAC9B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;AAMD,SAAgB,iBAAiB,QAAqC;CACpE,IAAI,WAAW,QAAS,OAAgB,aAAa,GAAG,OAAO;CAC/D,MAAM,UAAU;CAChB,IAAI,QAAQ,QAAQ,UAAU,MAAM,MAAM,OAAO;CACjD,MAAM,QAAQ,QAAQ,QAAQ,OAAO;CACrC,IAAI,UAAU,QAAQ,CAAC,gBAAgB,IAAI,MAAM,IAAI,GAAG,OAAO;CAE/D,IAAI,OAAO,QAAQ,sBAAsB,WAAW,OAAO,QAAQ;CACnE,OAAO,QAAQ,QAAQ,oDAAkD,MAAM;AACjF;AAEA,SAAgB,YAAY,OAAgE;CAC1F,OAAO,MAAM,UAAU,MAAM,WAAW,MAAM;AAChD"}
|
|
@@ -18,7 +18,10 @@ export interface MenuItemProps extends Omit<ComponentPropsWithRef<"button">, "on
|
|
|
18
18
|
action?: Action<[details: MenuItemDetails]>;
|
|
19
19
|
/** Close the whole menu once `action` settles. Default `true`. */
|
|
20
20
|
closeOnActivate?: boolean;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Renders `aria-disabled` and `data-disabled`: the item stays focusable and never activates.
|
|
23
|
+
* It is an ordinary item for navigation; only the open focus skips items with `disabled`.
|
|
24
|
+
*/
|
|
22
25
|
disabled?: boolean;
|
|
23
26
|
/** Text typeahead matches when the item's text content is not representative (an icon). */
|
|
24
27
|
label?: string;
|
|
@@ -29,7 +32,8 @@ export interface MenuItemProps extends Omit<ComponentPropsWithRef<"button">, "on
|
|
|
29
32
|
}
|
|
30
33
|
/**
|
|
31
34
|
* A menu command: `<button type="button" role="menuitem" tabindex="-1">`. Click, Enter or Space
|
|
32
|
-
* runs `action` in the item's own transition
|
|
35
|
+
* runs `action` in the item's own transition (`data-pending` and `aria-disabled` on the item; a
|
|
36
|
+
* second activation meanwhile is ignored); once it resolves the whole menu stack closes
|
|
33
37
|
* through the top-level Root (`openChangeAction(false, { reason: "activate" })` awaited, view
|
|
34
38
|
* transition if set). Hovering the item focuses it.
|
|
35
39
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu-item.d.mts","names":[],"sources":["../../src/menu/menu-item.tsx"],"mappings":";;;;iBAWiB,wBAAwB;iBAExB;;WAEN;WACA;;iBAGM,sBAAsB,KACrC;;;;;;;EASA,SAAS,QAAQ,SAAS;;EAE1B;;EAEA
|
|
1
|
+
{"version":3,"file":"menu-item.d.mts","names":[],"sources":["../../src/menu/menu-item.tsx"],"mappings":";;;;iBAWiB,wBAAwB;iBAExB;;WAEN;WACA;;iBAGM,sBAAsB,KACrC;;;;;;;EASA,SAAS,QAAQ,SAAS;;EAE1B;;;;;EAKA;;EAEA;EACA,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;EACjC,SAAS,WAAW,iCAAiC;;;;;;;;;;;;;wBAcvC,WACd,QACA,iBACA,UACA,OACA,WACA,OACA,UACA,WACG,QACF,gBAAa;yBAVA"}
|
package/dist/menu/menu-item.mjs
CHANGED
|
@@ -9,7 +9,8 @@ import { useMenuItem } from "./use-menu-item.mjs";
|
|
|
9
9
|
//#region src/menu/menu-item.tsx
|
|
10
10
|
/**
|
|
11
11
|
* A menu command: `<button type="button" role="menuitem" tabindex="-1">`. Click, Enter or Space
|
|
12
|
-
* runs `action` in the item's own transition
|
|
12
|
+
* runs `action` in the item's own transition (`data-pending` and `aria-disabled` on the item; a
|
|
13
|
+
* second activation meanwhile is ignored); once it resolves the whole menu stack closes
|
|
13
14
|
* through the top-level Root (`openChangeAction(false, { reason: "activate" })` awaited, view
|
|
14
15
|
* transition if set). Hovering the item focuses it.
|
|
15
16
|
*
|
|
@@ -19,7 +20,7 @@ import { useMenuItem } from "./use-menu-item.mjs";
|
|
|
19
20
|
*/
|
|
20
21
|
function MenuItem({ action, closeOnActivate = true, disabled = false, label, className, style, children, render, ...rest }) {
|
|
21
22
|
const menu = useMenuContext("Menu.Item");
|
|
22
|
-
const { pending, run } = useActionTrigger(menu.root.popover.openGeneration);
|
|
23
|
+
const { pending, run } = useActionTrigger({ openGeneration: menu.root.popover.openGeneration });
|
|
23
24
|
const { libraryProps } = useMenuItem({
|
|
24
25
|
part: "Menu.Item",
|
|
25
26
|
role: "menuitem",
|
|
@@ -27,7 +28,7 @@ function MenuItem({ action, closeOnActivate = true, disabled = false, label, cla
|
|
|
27
28
|
label,
|
|
28
29
|
activate: (event) => {
|
|
29
30
|
const details = createActionDetails(event, {});
|
|
30
|
-
run(() => action?.(details), closeOnActivate ? () => menu.root.popover.requestClose(event, "activate") : void 0);
|
|
31
|
+
if (!run(() => action?.(details), closeOnActivate ? () => menu.root.popover.requestClose(event, "activate") : void 0)) event.preventDefault();
|
|
31
32
|
}
|
|
32
33
|
});
|
|
33
34
|
return useRender({
|
|
@@ -44,6 +45,7 @@ function MenuItem({ action, closeOnActivate = true, disabled = false, label, cla
|
|
|
44
45
|
libraryProps: {
|
|
45
46
|
...rendersButton(render) && { type: "button" },
|
|
46
47
|
...libraryProps,
|
|
48
|
+
"aria-disabled": disabled || pending ? true : void 0,
|
|
47
49
|
"data-pending": dataAttr(pending)
|
|
48
50
|
}
|
|
49
51
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu-item.mjs","names":[],"sources":["../../src/menu/menu-item.tsx"],"sourcesContent":["\"use client\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode } from \"react\";\nimport { createActionDetails } from \"../internal/action-details.js\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { Action, ActionDetails, RenderProp, StateValue } from \"../internal/types.js\";\nimport { useActionTrigger } from \"../internal/use-action-trigger.js\";\nimport { rendersButton, useRender } from \"../internal/use-render.js\";\nimport { useMenuContext } from \"./menu-context.js\";\nimport { useMenuItem } from \"./use-menu-item.js\";\n\n/** Details passed to `Menu.Item`'s `action`. */\nexport interface MenuItemDetails extends ActionDetails {}\n\nexport interface MenuItemState {\n /** This item's `action` is in flight. */\n readonly pending: boolean;\n readonly disabled: boolean;\n}\n\nexport interface MenuItemProps extends Omit<\n ComponentPropsWithRef<\"button\">,\n \"onClick\" | \"onKeyDown\" | \"onPointerMove\" | \"disabled\" | \"className\" | \"style\" | \"children\"\n> {\n /**\n * Runs inside this item's own transition (`data-pending` on the item); the menu stays open\n * until it settles, then the whole menu closes (`closeOnActivate`). May be a Server Function\n * (`details.event` is virtual). A rejection leaves the menu open and reaches the nearest Error\n * Boundary above the item. Without `action`, activation only closes.\n */\n action?: Action<[details: MenuItemDetails]>;\n /** Close the whole menu once `action` settles. Default `true`. */\n closeOnActivate?: boolean;\n
|
|
1
|
+
{"version":3,"file":"menu-item.mjs","names":[],"sources":["../../src/menu/menu-item.tsx"],"sourcesContent":["\"use client\";\nimport type { ComponentPropsWithRef, CSSProperties, ReactNode } from \"react\";\nimport { createActionDetails } from \"../internal/action-details.js\";\nimport { dataAttr } from \"../internal/data-attr.js\";\nimport type { Action, ActionDetails, RenderProp, StateValue } from \"../internal/types.js\";\nimport { useActionTrigger } from \"../internal/use-action-trigger.js\";\nimport { rendersButton, useRender } from \"../internal/use-render.js\";\nimport { useMenuContext } from \"./menu-context.js\";\nimport { useMenuItem } from \"./use-menu-item.js\";\n\n/** Details passed to `Menu.Item`'s `action`. */\nexport interface MenuItemDetails extends ActionDetails {}\n\nexport interface MenuItemState {\n /** This item's `action` is in flight. */\n readonly pending: boolean;\n readonly disabled: boolean;\n}\n\nexport interface MenuItemProps extends Omit<\n ComponentPropsWithRef<\"button\">,\n \"onClick\" | \"onKeyDown\" | \"onPointerMove\" | \"disabled\" | \"className\" | \"style\" | \"children\"\n> {\n /**\n * Runs inside this item's own transition (`data-pending` on the item); the menu stays open\n * until it settles, then the whole menu closes (`closeOnActivate`). May be a Server Function\n * (`details.event` is virtual). A rejection leaves the menu open and reaches the nearest Error\n * Boundary above the item. Without `action`, activation only closes.\n */\n action?: Action<[details: MenuItemDetails]>;\n /** Close the whole menu once `action` settles. Default `true`. */\n closeOnActivate?: boolean;\n /**\n * Renders `aria-disabled` and `data-disabled`: the item stays focusable and never activates.\n * It is an ordinary item for navigation; only the open focus skips items with `disabled`.\n */\n disabled?: boolean;\n /** Text typeahead matches when the item's text content is not representative (an icon). */\n label?: string;\n className?: StateValue<string | undefined, MenuItemState>;\n style?: StateValue<CSSProperties | undefined, MenuItemState>;\n children?: StateValue<ReactNode, MenuItemState>;\n render?: RenderProp<ComponentPropsWithRef<\"button\">, MenuItemState>;\n}\n\n/**\n * A menu command: `<button type=\"button\" role=\"menuitem\" tabindex=\"-1\">`. Click, Enter or Space\n * runs `action` in the item's own transition (`data-pending` and `aria-disabled` on the item; a\n * second activation meanwhile is ignored); once it resolves the whole menu stack closes\n * through the top-level Root (`openChangeAction(false, { reason: \"activate\" })` awaited, view\n * transition if set). Hovering the item focuses it.\n *\n * Owned keys (conventions 1.2): `type` (on a `<button>`), `role`, `tabIndex`, `disabled`\n * (always absent), `aria-disabled`, `onClick`, `onKeyDown`, `onPointerMove`, `data-pending`,\n * `data-disabled`, `data-typeahead`.\n */\nexport function MenuItem({\n action,\n closeOnActivate = true,\n disabled = false,\n label,\n className,\n style,\n children,\n render,\n ...rest\n}: MenuItemProps) {\n const menu = useMenuContext(\"Menu.Item\");\n // The close belongs to the top-level menu, so its open generation guards it.\n const { pending, run } = useActionTrigger({ openGeneration: menu.root.popover.openGeneration });\n\n const { libraryProps } = useMenuItem({\n part: \"Menu.Item\",\n role: \"menuitem\",\n disabled,\n label,\n activate: (event) => {\n const details: MenuItemDetails = createActionDetails(event, {});\n // A second activation while this item's own run is in flight starts nothing and is\n // cancelled (the hook's guard, #81): a link does not navigate, a composed command does not\n // run. Enter / Space reach here through the element's click, their keydown is cancelled.\n const started = run(\n () => action?.(details),\n closeOnActivate ? () => menu.root.popover.requestClose(event, \"activate\") : undefined,\n );\n if (!started) event.preventDefault();\n },\n });\n\n const state: MenuItemState = { pending, disabled };\n\n return useRender<ComponentPropsWithRef<\"button\">, MenuItemState>({\n defaultElement: \"button\",\n render,\n state,\n className,\n style,\n children,\n props: rest,\n libraryProps: {\n ...(rendersButton(render) && { type: \"button\" as const }),\n ...libraryProps,\n // Also while pending (#81): an item that will not act is unavailable to AT. Navigation keys\n // on `data-disabled` (the `disabled` prop), so a pending item is still an enabled item.\n \"aria-disabled\": disabled || pending ? true : undefined,\n \"data-pending\": dataAttr(pending),\n },\n });\n}\n\nMenuItem.displayName = \"MenuItem\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAwDA,SAAgB,SAAS,EACvB,QACA,kBAAkB,MAClB,WAAW,OACX,OACA,WACA,OACA,UACA,QACA,GAAG,QACa;CAChB,MAAM,OAAO,eAAe,WAAW;CAEvC,MAAM,EAAE,SAAS,QAAQ,iBAAiB,EAAE,gBAAgB,KAAK,KAAK,QAAQ,eAAe,CAAC;CAE9F,MAAM,EAAE,iBAAiB,YAAY;EACnC,MAAM;EACN,MAAM;EACN;EACA;EACA,WAAW,UAAU;GACnB,MAAM,UAA2B,oBAAoB,OAAO,CAAC,CAAC;GAQ9D,IAAI,CAJY,UACR,SAAS,OAAO,GACtB,wBAAwB,KAAK,KAAK,QAAQ,aAAa,OAAO,UAAU,IAAI,KAAA,CAEnE,GAAG,MAAM,eAAe;EACrC;CACF,CAAC;CAID,OAAO,UAA0D;EAC/D,gBAAgB;EAChB;EACA,OAAA;GAL6B;GAAS;EAKlC;EACJ;EACA;EACA;EACA,OAAO;EACP,cAAc;GACZ,GAAI,cAAc,MAAM,KAAK,EAAE,MAAM,SAAkB;GACvD,GAAG;GAGH,iBAAiB,YAAY,UAAU,OAAO,KAAA;GAC9C,gBAAgB,SAAS,OAAO;EAClC;CACF,CAAC;AACH;AAEA,SAAS,cAAc"}
|
|
@@ -7,17 +7,26 @@ export type MenuAlign = AnchorAlign;
|
|
|
7
7
|
export interface MenuPopupState {
|
|
8
8
|
/** Committed open state. Read `:popover-open` for the DOM truth. */
|
|
9
9
|
readonly open: boolean;
|
|
10
|
+
/** `null` with `side={null}`, and while the menu is placed at a point (`context`). */
|
|
10
11
|
readonly side: MenuSide | null;
|
|
11
12
|
readonly align: MenuAlign;
|
|
12
13
|
/** This popup belongs to a submenu. */
|
|
13
14
|
readonly submenu: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The current open came from a `Menu.ContextTrigger`: the popup is placed at the pointer, with
|
|
17
|
+
* `--menu-point-x` / `--menu-point-y` set inline (`data-context`).
|
|
18
|
+
*/
|
|
19
|
+
readonly context: boolean;
|
|
14
20
|
}
|
|
15
|
-
export interface MenuPopupProps extends Omit<ComponentPropsWithRef<"div">, "id" | "popover" | "role" | "aria-labelledby" | "onKeyDown" | "onFocus" | "onBlur" | "onMouseDown" | "onPointerEnter" | "onPointerLeave" | "onToggle" | "onBeforeToggle" | "className" | "style" | "children"> {
|
|
21
|
+
export interface MenuPopupProps extends Omit<ComponentPropsWithRef<"div">, "id" | "popover" | "role" | "aria-labelledby" | "onKeyDown" | "onFocus" | "onBlur" | "onMouseDown" | "onContextMenu" | "onPointerEnter" | "onPointerLeave" | "onToggle" | "onBeforeToggle" | "className" | "style" | "children"> {
|
|
16
22
|
/**
|
|
17
23
|
* Placement against the Trigger through inline `position-area`, with `position-try-fallbacks`
|
|
18
24
|
* flipping it when it does not fit. Default `"bottom"` for a top-level menu, `"right"` for a
|
|
19
25
|
* submenu (whose fallback is `flip-inline` only). `null` emits no inline positioning (and no
|
|
20
|
-
* `data-side` / `data-align`): the stylesheet owns placement.
|
|
26
|
+
* `data-side` / `data-align`): the stylesheet owns placement. Ignored for an open from a
|
|
27
|
+
* `Menu.ContextTrigger`, which is placed at the pointer; there `null` also opts out of the
|
|
28
|
+
* inline point placement and the library's `@position-try` rules, leaving only
|
|
29
|
+
* `--menu-point-x` / `--menu-point-y`.
|
|
21
30
|
*/
|
|
22
31
|
side?: MenuSide | null;
|
|
23
32
|
/** Alignment along the side. Default `"start"`. Ignored when `side` is `null`. */
|
|
@@ -58,8 +67,13 @@ export interface MenuPopupProps extends Omit<ComponentPropsWithRef<"div">, "id"
|
|
|
58
67
|
*
|
|
59
68
|
* Owned keys (conventions 1.2): `id`, `popover`, `role`, `aria-labelledby` (token-merged),
|
|
60
69
|
* `tabIndex`, `focusgroup` (with the flag), `onKeyDown`, `onBlur`, `onMouseDown` (a press on
|
|
61
|
-
* non-item space keeps focus on the current item), `
|
|
62
|
-
* `data-side` / `data-align` unless `side` is `null
|
|
70
|
+
* non-item space keeps focus on the current item), `onContextMenu` (no native menu over this
|
|
71
|
+
* one), `onPointerEnter` / `onPointerLeave`, `data-side` / `data-align` unless `side` is `null`
|
|
72
|
+
* or the menu is placed at a point, and `data-context` for an open from a `Menu.ContextTrigger`
|
|
73
|
+
* (placed at the pointer: `--menu-point-x` / `--menu-point-y` inline, and unless `side` is
|
|
74
|
+
* `null`, `left` / `top` at the point with the library's `@position-try` fallbacks, whose rules
|
|
75
|
+
* it renders as a hoisted `<style>`). A top-level menu closes at once on a right- or
|
|
76
|
+
* middle-click outside it.
|
|
63
77
|
*/
|
|
64
78
|
export declare function MenuPopup({ side: sideProp, align, keepMounted, experimental_focusgroup, transition, className, style, children, render, ...rest }: MenuPopupProps): ReactNode;
|
|
65
79
|
export declare namespace MenuPopup {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu-popup.d.mts","names":[],"sources":["../../src/menu/menu-popup.tsx"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"menu-popup.d.mts","names":[],"sources":["../../src/menu/menu-popup.tsx"],"mappings":";;;;YAoDY,WAAW;YACX,YAAY;iBAEP;;WAEN;;WAEA,MAAM;WACN,OAAO;;WAEP;;;;;WAKA;;iBAGM,uBAAuB,KACtC;;;;;;;;;;EA2BA,OAAO;;EAEP,QAAQ;;;;;;EAMR;;;;;;;EAOA;;;;;;;EAOA,aAAa;EACb,YAAY,+BAA+B;EAC3C,QAAQ,WAAW,2BAA2B;EAC9C,WAAW,WAAW,WAAW;;EAEjC,SAAS,WAAW,8BAA8B;;;;;;;;;;;;;;;;;;;;wBAsFpC,YACd,MAAM,UACN,OACA,aACA,yBACA,YACA,WACA,OACA,UACA,WACG,QACF,iBAAc;yBAXD"}
|