bits-ui 2.16.4 → 2.17.0
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/bits/accordion/accordion.svelte.d.ts +4 -2
- package/dist/bits/accordion/accordion.svelte.js +2 -1
- package/dist/bits/collapsible/collapsible.svelte.d.ts +3 -1
- package/dist/bits/collapsible/collapsible.svelte.js +2 -1
- package/dist/bits/context-menu/components/context-menu.svelte +3 -1
- package/dist/bits/date-field/date-field.svelte.d.ts +5 -0
- package/dist/bits/date-field/date-field.svelte.js +5 -0
- package/dist/bits/dialog/dialog.svelte.d.ts +4 -0
- package/dist/bits/dialog/dialog.svelte.js +3 -1
- package/dist/bits/link-preview/link-preview.svelte.d.ts +5 -3
- package/dist/bits/link-preview/link-preview.svelte.js +2 -1
- package/dist/bits/menu/components/menu-sub-content-static.svelte +12 -3
- package/dist/bits/menu/components/menu-sub-content.svelte +12 -3
- package/dist/bits/menu/components/menu-sub-trigger.svelte +1 -1
- package/dist/bits/menu/components/menu.svelte +5 -0
- package/dist/bits/menu/components/menu.svelte.d.ts +1 -0
- package/dist/bits/menu/menu.svelte.d.ts +8 -4
- package/dist/bits/menu/menu.svelte.js +636 -12
- package/dist/bits/menubar/components/menubar-menu.svelte +3 -0
- package/dist/bits/menubar/menubar.svelte.d.ts +2 -0
- package/dist/bits/menubar/menubar.svelte.js +22 -2
- package/dist/bits/navigation-menu/components/navigation-menu-content.svelte +7 -2
- package/dist/bits/navigation-menu/components/navigation-menu-indicator.svelte +9 -2
- package/dist/bits/navigation-menu/components/navigation-menu-viewport.svelte +5 -3
- package/dist/bits/navigation-menu/navigation-menu.svelte.js +1 -1
- package/dist/bits/popover/popover.svelte.d.ts +7 -3
- package/dist/bits/popover/popover.svelte.js +3 -1
- package/dist/bits/select/select.svelte.d.ts +8 -4
- package/dist/bits/select/select.svelte.js +20 -3
- package/dist/bits/tooltip/tooltip.svelte.d.ts +5 -3
- package/dist/bits/tooltip/tooltip.svelte.js +3 -2
- package/dist/bits/utilities/floating-layer/use-floating-layer.svelte.d.ts +5 -5
- package/dist/bits/utilities/presence-layer/presence-layer.svelte +5 -1
- package/dist/bits/utilities/presence-layer/presence.svelte.d.ts +3 -38
- package/dist/bits/utilities/presence-layer/presence.svelte.js +49 -146
- package/dist/bits/utilities/presence-layer/types.d.ts +7 -3
- package/dist/internal/animations-complete.js +64 -9
- package/dist/internal/attrs.d.ts +5 -0
- package/dist/internal/attrs.js +8 -2
- package/dist/internal/presence-manager.svelte.d.ts +4 -1
- package/dist/internal/presence-manager.svelte.js +42 -1
- package/package.json +1 -1
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import { afterTick, mergeProps, onDestroyEffect, attachRef, DOMContext, getWindow, simpleBox, boxWith, } from "svelte-toolbelt";
|
|
1
|
+
import { afterTick, mergeProps, onDestroyEffect, attachRef, DOMContext, getDocument, getWindow, simpleBox, boxWith, } from "svelte-toolbelt";
|
|
2
2
|
import { Context, watch } from "runed";
|
|
3
3
|
import { FIRST_LAST_KEYS, LAST_KEYS, SELECTION_KEYS, SUB_OPEN_KEYS, getCheckedState, isMouseEvent, } from "./utils.js";
|
|
4
4
|
import { focusFirst } from "../../internal/focus.js";
|
|
5
5
|
import { CustomEventDispatcher } from "../../internal/events.js";
|
|
6
6
|
import { isElement, isElementOrSVGElement, isHTMLElement } from "../../internal/is.js";
|
|
7
7
|
import { kbd } from "../../internal/kbd.js";
|
|
8
|
-
import { createBitsAttrs, getAriaChecked, boolToStr, getDataOpenClosed, boolToEmptyStrOrUndef, } from "../../internal/attrs.js";
|
|
8
|
+
import { createBitsAttrs, getAriaChecked, boolToStr, getDataOpenClosed, boolToEmptyStrOrUndef, getDataTransitionAttrs, } from "../../internal/attrs.js";
|
|
9
9
|
import { IsUsingKeyboard } from "../utilities/is-using-keyboard/is-using-keyboard.svelte.js";
|
|
10
10
|
import { getTabbableFrom } from "../../internal/tabbable.js";
|
|
11
11
|
import { isTabbable } from "tabbable";
|
|
12
12
|
import { DOMTypeahead } from "../../internal/dom-typeahead.svelte.js";
|
|
13
13
|
import { RovingFocusGroup } from "../../internal/roving-focus-group.js";
|
|
14
|
-
import { GraceArea } from "../../internal/grace-area.svelte.js";
|
|
15
14
|
import { PresenceManager } from "../../internal/presence-manager.svelte.js";
|
|
16
15
|
import { arraysAreEqual } from "../../internal/arrays.js";
|
|
17
16
|
export const CONTEXT_MENU_TRIGGER_ATTR = "data-context-menu-trigger";
|
|
@@ -44,6 +43,593 @@ export const menuAttrs = createBitsAttrs({
|
|
|
44
43
|
"arrow",
|
|
45
44
|
],
|
|
46
45
|
});
|
|
46
|
+
/*
|
|
47
|
+
interface MenuIntentDebugSnapshot {
|
|
48
|
+
active: boolean;
|
|
49
|
+
target: IntentTarget | null;
|
|
50
|
+
exitPoint: Point | null;
|
|
51
|
+
pointerPoint: Point | null;
|
|
52
|
+
corridor: Polygon | null;
|
|
53
|
+
intentPolygon: Polygon | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class MenuIntentDebugOverlay {
|
|
57
|
+
readonly #enabled: () => boolean;
|
|
58
|
+
readonly #getDocument: () => Document | null;
|
|
59
|
+
#root: HTMLDivElement | null = null;
|
|
60
|
+
#corridorPolygon: SVGPolygonElement | null = null;
|
|
61
|
+
#intentPolygon: SVGPolygonElement | null = null;
|
|
62
|
+
#exitPoint: SVGCircleElement | null = null;
|
|
63
|
+
#pointerPoint: SVGCircleElement | null = null;
|
|
64
|
+
|
|
65
|
+
constructor(opts: { enabled: () => boolean; getDocument: () => Document | null }) {
|
|
66
|
+
this.#enabled = opts.enabled;
|
|
67
|
+
this.#getDocument = opts.getDocument;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
update(state: MenuIntentDebugSnapshot) {
|
|
71
|
+
if (!this.#enabled()) {
|
|
72
|
+
this.#detach();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
this.#ensureRoot();
|
|
76
|
+
if (!this.#root) return;
|
|
77
|
+
|
|
78
|
+
if (!state.active || !state.corridor || !state.intentPolygon || !state.exitPoint) {
|
|
79
|
+
this.#root.style.display = "none";
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const color = state.target === "trigger" ? "16 185 129" : "59 130 246";
|
|
84
|
+
const strokeColor = `rgb(${color})`;
|
|
85
|
+
const fillColor = `rgb(${color} / 0.18)`;
|
|
86
|
+
const corridorFill = `rgb(${color} / 0.1)`;
|
|
87
|
+
|
|
88
|
+
this.#root.style.display = "block";
|
|
89
|
+
this.#setPolygon(this.#corridorPolygon, state.corridor, corridorFill, strokeColor, 1.5);
|
|
90
|
+
this.#setPolygon(this.#intentPolygon, state.intentPolygon, fillColor, strokeColor, 2);
|
|
91
|
+
this.#setPoint(this.#exitPoint, state.exitPoint, strokeColor, 5);
|
|
92
|
+
this.#setPoint(
|
|
93
|
+
this.#pointerPoint,
|
|
94
|
+
state.pointerPoint,
|
|
95
|
+
"rgb(15 23 42 / 0.9)",
|
|
96
|
+
4,
|
|
97
|
+
"rgb(255 255 255 / 0.95)"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
destroy() {
|
|
102
|
+
this.#detach();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#setPolygon(
|
|
106
|
+
node: SVGPolygonElement | null,
|
|
107
|
+
points: Polygon | null,
|
|
108
|
+
fill: string,
|
|
109
|
+
stroke: string,
|
|
110
|
+
strokeWidth: number
|
|
111
|
+
) {
|
|
112
|
+
if (!node || !points || points.length === 0) return;
|
|
113
|
+
node.setAttribute("points", polygonToSvgPoints(points));
|
|
114
|
+
node.setAttribute("fill", fill);
|
|
115
|
+
node.setAttribute("stroke", stroke);
|
|
116
|
+
node.setAttribute("stroke-width", `${strokeWidth}`);
|
|
117
|
+
node.setAttribute("stroke-dasharray", "6 4");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#setPoint(
|
|
121
|
+
node: SVGCircleElement | null,
|
|
122
|
+
point: Point | null,
|
|
123
|
+
fill: string,
|
|
124
|
+
radius: number,
|
|
125
|
+
stroke = "transparent"
|
|
126
|
+
) {
|
|
127
|
+
if (!node || !point) return;
|
|
128
|
+
node.setAttribute("cx", `${point.x}`);
|
|
129
|
+
node.setAttribute("cy", `${point.y}`);
|
|
130
|
+
node.setAttribute("r", `${radius}`);
|
|
131
|
+
node.setAttribute("fill", fill);
|
|
132
|
+
node.setAttribute("stroke", stroke);
|
|
133
|
+
node.setAttribute("stroke-width", "1.5");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#ensureRoot() {
|
|
137
|
+
if (this.#root) return;
|
|
138
|
+
const doc = this.#getDocument();
|
|
139
|
+
if (!doc?.body) return;
|
|
140
|
+
|
|
141
|
+
const root = doc.createElement("div");
|
|
142
|
+
root.setAttribute("aria-hidden", "true");
|
|
143
|
+
root.style.position = "fixed";
|
|
144
|
+
root.style.inset = "0";
|
|
145
|
+
root.style.pointerEvents = "none";
|
|
146
|
+
root.style.zIndex = "2147483647";
|
|
147
|
+
root.style.display = "none";
|
|
148
|
+
|
|
149
|
+
const svg = doc.createElementNS(SVG_NS, "svg");
|
|
150
|
+
svg.setAttribute("width", "100%");
|
|
151
|
+
svg.setAttribute("height", "100%");
|
|
152
|
+
svg.style.overflow = "visible";
|
|
153
|
+
|
|
154
|
+
const corridorPolygon = doc.createElementNS(SVG_NS, "polygon");
|
|
155
|
+
const intentPolygon = doc.createElementNS(SVG_NS, "polygon");
|
|
156
|
+
const exitPoint = doc.createElementNS(SVG_NS, "circle");
|
|
157
|
+
const pointerPoint = doc.createElementNS(SVG_NS, "circle");
|
|
158
|
+
|
|
159
|
+
svg.append(corridorPolygon, intentPolygon, exitPoint, pointerPoint);
|
|
160
|
+
root.append(svg);
|
|
161
|
+
doc.body.append(root);
|
|
162
|
+
|
|
163
|
+
this.#root = root;
|
|
164
|
+
this.#corridorPolygon = corridorPolygon;
|
|
165
|
+
this.#intentPolygon = intentPolygon;
|
|
166
|
+
this.#exitPoint = exitPoint;
|
|
167
|
+
this.#pointerPoint = pointerPoint;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#detach() {
|
|
171
|
+
this.#root?.remove();
|
|
172
|
+
this.#root = null;
|
|
173
|
+
this.#corridorPolygon = null;
|
|
174
|
+
this.#intentPolygon = null;
|
|
175
|
+
this.#exitPoint = null;
|
|
176
|
+
this.#pointerPoint = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
*/
|
|
180
|
+
class MenuSubmenuIntent {
|
|
181
|
+
#opts;
|
|
182
|
+
// readonly #debugOverlay: MenuIntentDebugOverlay;
|
|
183
|
+
#cleanupDocMove = null;
|
|
184
|
+
#fallbackTimer = null;
|
|
185
|
+
#active = false;
|
|
186
|
+
#target = null;
|
|
187
|
+
#apex = null;
|
|
188
|
+
#pointerPoint = null;
|
|
189
|
+
// #corridor: Polygon | null = null;
|
|
190
|
+
// #intentPolygon: Polygon | null = null;
|
|
191
|
+
#launchPoint = null;
|
|
192
|
+
constructor(opts) {
|
|
193
|
+
this.#opts = opts;
|
|
194
|
+
// this.#debugOverlay = new MenuIntentDebugOverlay({
|
|
195
|
+
// enabled: () => this.#opts.debugMode(),
|
|
196
|
+
// getDocument: () => getDocument(this.#opts.triggerNode() ?? this.#opts.contentNode()),
|
|
197
|
+
// });
|
|
198
|
+
watch([opts.triggerNode, opts.contentNode, opts.enabled], ([triggerNode, contentNode, enabled]) => {
|
|
199
|
+
this.#reset();
|
|
200
|
+
if (!triggerNode || !contentNode || !enabled)
|
|
201
|
+
return;
|
|
202
|
+
const onTriggerMove = (e) => {
|
|
203
|
+
if (!isMouseEvent(e))
|
|
204
|
+
return;
|
|
205
|
+
this.#launchPoint = { x: e.clientX, y: e.clientY };
|
|
206
|
+
if (!this.#active)
|
|
207
|
+
this.#preview(e, "content");
|
|
208
|
+
};
|
|
209
|
+
const onTriggerLeave = (e) => {
|
|
210
|
+
if (!isMouseEvent(e))
|
|
211
|
+
return;
|
|
212
|
+
this.#engage(e, "content");
|
|
213
|
+
};
|
|
214
|
+
const onContentMove = (e) => {
|
|
215
|
+
if (!isMouseEvent(e))
|
|
216
|
+
return;
|
|
217
|
+
if (!this.#active)
|
|
218
|
+
this.#preview(e, "trigger");
|
|
219
|
+
};
|
|
220
|
+
const onContentLeave = (e) => {
|
|
221
|
+
if (!isMouseEvent(e))
|
|
222
|
+
return;
|
|
223
|
+
if (isElement(e.relatedTarget)) {
|
|
224
|
+
const selector = this.#opts.subContentSelector();
|
|
225
|
+
const matchedSubContent = e.relatedTarget.closest(selector);
|
|
226
|
+
if (matchedSubContent &&
|
|
227
|
+
matchedSubContent !== contentNode &&
|
|
228
|
+
matchedSubContent.id) {
|
|
229
|
+
const isChild = !!contentNode.querySelector(`[aria-controls="${matchedSubContent.id}"]`);
|
|
230
|
+
if (isChild) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
this.#engage(e, "trigger");
|
|
236
|
+
};
|
|
237
|
+
const onTriggerEnter = (e) => {
|
|
238
|
+
if (!isMouseEvent(e))
|
|
239
|
+
return;
|
|
240
|
+
this.#disengage();
|
|
241
|
+
};
|
|
242
|
+
const onContentEnter = (e) => {
|
|
243
|
+
if (!isMouseEvent(e))
|
|
244
|
+
return;
|
|
245
|
+
this.#disengage();
|
|
246
|
+
};
|
|
247
|
+
triggerNode.addEventListener("pointermove", onTriggerMove);
|
|
248
|
+
triggerNode.addEventListener("pointerleave", onTriggerLeave);
|
|
249
|
+
triggerNode.addEventListener("pointerenter", onTriggerEnter);
|
|
250
|
+
contentNode.addEventListener("pointermove", onContentMove);
|
|
251
|
+
contentNode.addEventListener("pointerleave", onContentLeave);
|
|
252
|
+
contentNode.addEventListener("pointerenter", onContentEnter);
|
|
253
|
+
return () => {
|
|
254
|
+
triggerNode.removeEventListener("pointermove", onTriggerMove);
|
|
255
|
+
triggerNode.removeEventListener("pointerleave", onTriggerLeave);
|
|
256
|
+
triggerNode.removeEventListener("pointerenter", onTriggerEnter);
|
|
257
|
+
contentNode.removeEventListener("pointermove", onContentMove);
|
|
258
|
+
contentNode.removeEventListener("pointerleave", onContentLeave);
|
|
259
|
+
contentNode.removeEventListener("pointerenter", onContentEnter);
|
|
260
|
+
this.#reset();
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
onDestroyEffect(() => {
|
|
264
|
+
this.#reset();
|
|
265
|
+
// this.#debugOverlay.destroy();
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
#parentTargetRect() {
|
|
269
|
+
const parent = this.#opts.parentContentNode();
|
|
270
|
+
if (parent)
|
|
271
|
+
return parent.getBoundingClientRect();
|
|
272
|
+
return this.#opts.triggerNode()?.getBoundingClientRect() ?? null;
|
|
273
|
+
}
|
|
274
|
+
#computePolygons(pointerPt, target) {
|
|
275
|
+
const triggerNode = this.#opts.triggerNode();
|
|
276
|
+
const contentNode = this.#opts.contentNode();
|
|
277
|
+
if (!triggerNode || !contentNode)
|
|
278
|
+
return null;
|
|
279
|
+
const triggerRect = triggerNode.getBoundingClientRect();
|
|
280
|
+
const contentRect = contentNode.getBoundingClientRect();
|
|
281
|
+
const side = getSide(triggerRect, contentRect);
|
|
282
|
+
let apex;
|
|
283
|
+
let targetRect;
|
|
284
|
+
let sourceRect;
|
|
285
|
+
if (target === "content") {
|
|
286
|
+
apex = this.#active ? (this.#apex ?? pointerPt) : pointerPt;
|
|
287
|
+
targetRect = contentRect;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
apex = this.#launchPoint ?? pointerPt;
|
|
291
|
+
targetRect = this.#parentTargetRect() ?? triggerRect;
|
|
292
|
+
sourceRect = contentRect;
|
|
293
|
+
}
|
|
294
|
+
this.#apex = apex;
|
|
295
|
+
return {
|
|
296
|
+
corridor: getCorridorPolygon(triggerRect, contentRect, side),
|
|
297
|
+
intent: getIntentPolygon(apex, targetRect, side, target, sourceRect),
|
|
298
|
+
targetRect,
|
|
299
|
+
side,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
#isInSafeZone(pt, corridor, intent) {
|
|
303
|
+
return isPointInPolygon(pt, corridor) || isPointInPolygon(pt, intent);
|
|
304
|
+
}
|
|
305
|
+
#preview(e, target) {
|
|
306
|
+
const pt = { x: e.clientX, y: e.clientY };
|
|
307
|
+
const geo = this.#computePolygons(pt, target);
|
|
308
|
+
if (!geo)
|
|
309
|
+
return;
|
|
310
|
+
this.#target = target;
|
|
311
|
+
this.#pointerPoint = pt;
|
|
312
|
+
// this.#corridor = geo.corridor;
|
|
313
|
+
// this.#intentPolygon = geo.intent;
|
|
314
|
+
// this.#syncDebug();
|
|
315
|
+
}
|
|
316
|
+
#engage(e, target) {
|
|
317
|
+
if (!this.#opts.enabled())
|
|
318
|
+
return;
|
|
319
|
+
const triggerNode = this.#opts.triggerNode();
|
|
320
|
+
const contentNode = this.#opts.contentNode();
|
|
321
|
+
if (!triggerNode || !contentNode)
|
|
322
|
+
return;
|
|
323
|
+
const related = e.relatedTarget;
|
|
324
|
+
if (isElement(related)) {
|
|
325
|
+
if (target === "content" && contentNode.contains(related))
|
|
326
|
+
return;
|
|
327
|
+
if (target === "trigger" && triggerNode.contains(related))
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const pt = { x: e.clientX, y: e.clientY };
|
|
331
|
+
const geo = this.#computePolygons(pt, target);
|
|
332
|
+
if (!geo)
|
|
333
|
+
return;
|
|
334
|
+
if (!isInsideRect(pt, geo.targetRect) &&
|
|
335
|
+
!this.#isInSafeZone(pt, geo.corridor, geo.intent)) {
|
|
336
|
+
this.#clearVisuals();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
this.#active = true;
|
|
340
|
+
this.#target = target;
|
|
341
|
+
this.#pointerPoint = pt;
|
|
342
|
+
// this.#corridor = geo.corridor;
|
|
343
|
+
// this.#intentPolygon = geo.intent;
|
|
344
|
+
this.#opts.setIsPointerInTransit(true);
|
|
345
|
+
this.#attachDocMove();
|
|
346
|
+
this.#startFallback();
|
|
347
|
+
// this.#syncDebug();
|
|
348
|
+
}
|
|
349
|
+
#disengageTimer = null;
|
|
350
|
+
#disengage() {
|
|
351
|
+
if (!this.#active)
|
|
352
|
+
return;
|
|
353
|
+
const wasReturning = this.#target === "trigger";
|
|
354
|
+
this.#detachDocMove();
|
|
355
|
+
this.#clearFallback();
|
|
356
|
+
this.#active = false;
|
|
357
|
+
this.#clearVisuals();
|
|
358
|
+
if (wasReturning) {
|
|
359
|
+
this.#clearDisengageTimer();
|
|
360
|
+
this.#disengageTimer = setTimeout(() => {
|
|
361
|
+
this.#disengageTimer = null;
|
|
362
|
+
this.#opts.setIsPointerInTransit(false);
|
|
363
|
+
}, 100);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
this.#opts.setIsPointerInTransit(false);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
#clearDisengageTimer() {
|
|
370
|
+
if (this.#disengageTimer === null)
|
|
371
|
+
return;
|
|
372
|
+
clearTimeout(this.#disengageTimer);
|
|
373
|
+
this.#disengageTimer = null;
|
|
374
|
+
}
|
|
375
|
+
#intentExit() {
|
|
376
|
+
const pointerPoint = this.#pointerPoint;
|
|
377
|
+
this.#detachDocMove();
|
|
378
|
+
this.#clearFallback();
|
|
379
|
+
this.#clearDisengageTimer();
|
|
380
|
+
this.#active = false;
|
|
381
|
+
this.#opts.setIsPointerInTransit(false);
|
|
382
|
+
this.#clearVisuals();
|
|
383
|
+
this.#opts.onIntentExit(pointerPoint);
|
|
384
|
+
}
|
|
385
|
+
#reset() {
|
|
386
|
+
this.#detachDocMove();
|
|
387
|
+
this.#clearFallback();
|
|
388
|
+
this.#clearDisengageTimer();
|
|
389
|
+
if (this.#active)
|
|
390
|
+
this.#opts.setIsPointerInTransit(false);
|
|
391
|
+
this.#active = false;
|
|
392
|
+
this.#target = null;
|
|
393
|
+
this.#apex = null;
|
|
394
|
+
this.#pointerPoint = null;
|
|
395
|
+
// this.#corridor = null;
|
|
396
|
+
// this.#intentPolygon = null;
|
|
397
|
+
this.#launchPoint = null;
|
|
398
|
+
// this.#syncDebug();
|
|
399
|
+
}
|
|
400
|
+
#isPointerInDescendantSubContent(pt) {
|
|
401
|
+
const contentNode = this.#opts.contentNode();
|
|
402
|
+
if (!contentNode)
|
|
403
|
+
return false;
|
|
404
|
+
const doc = contentNode.ownerDocument;
|
|
405
|
+
const el = doc.elementFromPoint(pt.x, pt.y);
|
|
406
|
+
if (!el)
|
|
407
|
+
return false;
|
|
408
|
+
const selector = this.#opts.subContentSelector();
|
|
409
|
+
const subContent = el.closest(selector);
|
|
410
|
+
if (!subContent || subContent === contentNode)
|
|
411
|
+
return false;
|
|
412
|
+
if (subContent.id)
|
|
413
|
+
return !!contentNode.querySelector(`[aria-controls="${subContent.id}"]`);
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
#onDocMove = (e) => {
|
|
417
|
+
if (!this.#active || !this.#target)
|
|
418
|
+
return;
|
|
419
|
+
if (!isMouseEvent(e))
|
|
420
|
+
return;
|
|
421
|
+
const triggerNode = this.#opts.triggerNode();
|
|
422
|
+
const contentNode = this.#opts.contentNode();
|
|
423
|
+
if (!triggerNode || !contentNode) {
|
|
424
|
+
this.#intentExit();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
this.#clearFallback();
|
|
428
|
+
const pt = { x: e.clientX, y: e.clientY };
|
|
429
|
+
this.#pointerPoint = pt;
|
|
430
|
+
const triggerRect = triggerNode.getBoundingClientRect();
|
|
431
|
+
const contentRect = contentNode.getBoundingClientRect();
|
|
432
|
+
if (this.#target === "content" && isInsideRect(pt, contentRect)) {
|
|
433
|
+
this.#disengage();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (this.#target === "trigger" && isInsideInsetRect(pt, triggerRect, 4)) {
|
|
437
|
+
this.#disengage();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (this.#isPointerInDescendantSubContent(pt)) {
|
|
441
|
+
this.#startFallback();
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const geo = this.#computePolygons(pt, this.#target);
|
|
445
|
+
if (!geo) {
|
|
446
|
+
this.#intentExit();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
// this.#corridor = geo.corridor;
|
|
450
|
+
// this.#intentPolygon = geo.intent;
|
|
451
|
+
// this.#syncDebug();
|
|
452
|
+
if (this.#isInSafeZone(pt, geo.corridor, geo.intent)) {
|
|
453
|
+
this.#startFallback();
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
this.#intentExit();
|
|
457
|
+
};
|
|
458
|
+
#attachDocMove() {
|
|
459
|
+
if (this.#cleanupDocMove)
|
|
460
|
+
return;
|
|
461
|
+
const doc = getDocument(this.#opts.triggerNode() ?? this.#opts.contentNode());
|
|
462
|
+
if (!doc)
|
|
463
|
+
return;
|
|
464
|
+
doc.addEventListener("pointermove", this.#onDocMove, true);
|
|
465
|
+
this.#cleanupDocMove = () => {
|
|
466
|
+
doc.removeEventListener("pointermove", this.#onDocMove, true);
|
|
467
|
+
this.#cleanupDocMove = null;
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
#detachDocMove() {
|
|
471
|
+
this.#cleanupDocMove?.();
|
|
472
|
+
}
|
|
473
|
+
#startFallback() {
|
|
474
|
+
this.#clearFallback();
|
|
475
|
+
this.#fallbackTimer = setTimeout(() => {
|
|
476
|
+
this.#fallbackTimer = null;
|
|
477
|
+
if (this.#active)
|
|
478
|
+
this.#intentExit();
|
|
479
|
+
}, 500);
|
|
480
|
+
}
|
|
481
|
+
#clearFallback() {
|
|
482
|
+
if (this.#fallbackTimer === null)
|
|
483
|
+
return;
|
|
484
|
+
clearTimeout(this.#fallbackTimer);
|
|
485
|
+
this.#fallbackTimer = null;
|
|
486
|
+
}
|
|
487
|
+
#clearVisuals() {
|
|
488
|
+
this.#target = null;
|
|
489
|
+
this.#apex = null;
|
|
490
|
+
this.#pointerPoint = null;
|
|
491
|
+
// this.#corridor = null;
|
|
492
|
+
// this.#intentPolygon = null;
|
|
493
|
+
// this.#syncDebug();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/*
|
|
497
|
+
function polygonToSvgPoints(points: Polygon): string {
|
|
498
|
+
return points.map((point) => `${point.x},${point.y}`).join(" ");
|
|
499
|
+
}
|
|
500
|
+
*/
|
|
501
|
+
function isPointInPolygon(point, polygon) {
|
|
502
|
+
const { x, y } = point;
|
|
503
|
+
let inside = false;
|
|
504
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
505
|
+
const xi = polygon[i].x;
|
|
506
|
+
const yi = polygon[i].y;
|
|
507
|
+
const xj = polygon[j].x;
|
|
508
|
+
const yj = polygon[j].y;
|
|
509
|
+
// prettier-ignore
|
|
510
|
+
const intersect = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);
|
|
511
|
+
if (intersect)
|
|
512
|
+
inside = !inside;
|
|
513
|
+
}
|
|
514
|
+
return inside;
|
|
515
|
+
}
|
|
516
|
+
function isInsideRect(point, rect) {
|
|
517
|
+
return (point.x >= rect.left &&
|
|
518
|
+
point.x <= rect.right &&
|
|
519
|
+
point.y >= rect.top &&
|
|
520
|
+
point.y <= rect.bottom);
|
|
521
|
+
}
|
|
522
|
+
function isInsideInsetRect(point, rect, inset) {
|
|
523
|
+
return (point.x >= rect.left + inset &&
|
|
524
|
+
point.x <= rect.right - inset &&
|
|
525
|
+
point.y >= rect.top + inset &&
|
|
526
|
+
point.y <= rect.bottom - inset);
|
|
527
|
+
}
|
|
528
|
+
function getSide(triggerRect, contentRect) {
|
|
529
|
+
const triggerCenterX = triggerRect.left + triggerRect.width / 2;
|
|
530
|
+
const triggerCenterY = triggerRect.top + triggerRect.height / 2;
|
|
531
|
+
const contentCenterX = contentRect.left + contentRect.width / 2;
|
|
532
|
+
const contentCenterY = contentRect.top + contentRect.height / 2;
|
|
533
|
+
const deltaX = contentCenterX - triggerCenterX;
|
|
534
|
+
const deltaY = contentCenterY - triggerCenterY;
|
|
535
|
+
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
536
|
+
return deltaX > 0 ? "right" : "left";
|
|
537
|
+
}
|
|
538
|
+
return deltaY > 0 ? "bottom" : "top";
|
|
539
|
+
}
|
|
540
|
+
function getCorridorPolygon(triggerRect, contentRect, side) {
|
|
541
|
+
const buffer = 2;
|
|
542
|
+
switch (side) {
|
|
543
|
+
case "top":
|
|
544
|
+
return [
|
|
545
|
+
{ x: Math.min(triggerRect.left, contentRect.left) - buffer, y: triggerRect.top },
|
|
546
|
+
{ x: Math.min(triggerRect.left, contentRect.left) - buffer, y: contentRect.bottom },
|
|
547
|
+
{
|
|
548
|
+
x: Math.max(triggerRect.right, contentRect.right) + buffer,
|
|
549
|
+
y: contentRect.bottom,
|
|
550
|
+
},
|
|
551
|
+
{ x: Math.max(triggerRect.right, contentRect.right) + buffer, y: triggerRect.top },
|
|
552
|
+
];
|
|
553
|
+
case "bottom":
|
|
554
|
+
return [
|
|
555
|
+
{ x: Math.min(triggerRect.left, contentRect.left) - buffer, y: triggerRect.bottom },
|
|
556
|
+
{ x: Math.min(triggerRect.left, contentRect.left) - buffer, y: contentRect.top },
|
|
557
|
+
{ x: Math.max(triggerRect.right, contentRect.right) + buffer, y: contentRect.top },
|
|
558
|
+
{
|
|
559
|
+
x: Math.max(triggerRect.right, contentRect.right) + buffer,
|
|
560
|
+
y: triggerRect.bottom,
|
|
561
|
+
},
|
|
562
|
+
];
|
|
563
|
+
case "left":
|
|
564
|
+
return [
|
|
565
|
+
{ x: triggerRect.left, y: Math.min(triggerRect.top, contentRect.top) - buffer },
|
|
566
|
+
{ x: contentRect.right, y: Math.min(triggerRect.top, contentRect.top) - buffer },
|
|
567
|
+
{
|
|
568
|
+
x: contentRect.right,
|
|
569
|
+
y: Math.max(triggerRect.bottom, contentRect.bottom) + buffer,
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
x: triggerRect.left,
|
|
573
|
+
y: Math.max(triggerRect.bottom, contentRect.bottom) + buffer,
|
|
574
|
+
},
|
|
575
|
+
];
|
|
576
|
+
case "right":
|
|
577
|
+
return [
|
|
578
|
+
{ x: triggerRect.right, y: Math.min(triggerRect.top, contentRect.top) - buffer },
|
|
579
|
+
{ x: contentRect.left, y: Math.min(triggerRect.top, contentRect.top) - buffer },
|
|
580
|
+
{
|
|
581
|
+
x: contentRect.left,
|
|
582
|
+
y: Math.max(triggerRect.bottom, contentRect.bottom) + buffer,
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
x: triggerRect.right,
|
|
586
|
+
y: Math.max(triggerRect.bottom, contentRect.bottom) + buffer,
|
|
587
|
+
},
|
|
588
|
+
];
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function getIntentPolygon(exitPoint, targetRect, side, target, sourceRect) {
|
|
592
|
+
const edgeBuffer = 8;
|
|
593
|
+
const effectiveSide = target === "trigger" ? flipSide(side) : side;
|
|
594
|
+
const top = sourceRect
|
|
595
|
+
? Math.min(targetRect.top, sourceRect.top) - edgeBuffer
|
|
596
|
+
: targetRect.top - edgeBuffer;
|
|
597
|
+
const bottom = sourceRect
|
|
598
|
+
? Math.max(targetRect.bottom, sourceRect.bottom) + edgeBuffer
|
|
599
|
+
: targetRect.bottom + edgeBuffer;
|
|
600
|
+
const left = sourceRect
|
|
601
|
+
? Math.min(targetRect.left, sourceRect.left) - edgeBuffer
|
|
602
|
+
: targetRect.left - edgeBuffer;
|
|
603
|
+
const right = sourceRect
|
|
604
|
+
? Math.max(targetRect.right, sourceRect.right) + edgeBuffer
|
|
605
|
+
: targetRect.right + edgeBuffer;
|
|
606
|
+
switch (effectiveSide) {
|
|
607
|
+
case "right":
|
|
608
|
+
return [exitPoint, { x: targetRect.left, y: top }, { x: targetRect.left, y: bottom }];
|
|
609
|
+
case "left":
|
|
610
|
+
return [exitPoint, { x: targetRect.right, y: top }, { x: targetRect.right, y: bottom }];
|
|
611
|
+
case "bottom":
|
|
612
|
+
return [exitPoint, { x: left, y: targetRect.top }, { x: right, y: targetRect.top }];
|
|
613
|
+
case "top":
|
|
614
|
+
return [
|
|
615
|
+
exitPoint,
|
|
616
|
+
{ x: left, y: targetRect.bottom },
|
|
617
|
+
{ x: right, y: targetRect.bottom },
|
|
618
|
+
];
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function flipSide(side) {
|
|
622
|
+
switch (side) {
|
|
623
|
+
case "top":
|
|
624
|
+
return "bottom";
|
|
625
|
+
case "bottom":
|
|
626
|
+
return "top";
|
|
627
|
+
case "left":
|
|
628
|
+
return "right";
|
|
629
|
+
case "right":
|
|
630
|
+
return "left";
|
|
631
|
+
}
|
|
632
|
+
}
|
|
47
633
|
export class MenuRootState {
|
|
48
634
|
static create(opts) {
|
|
49
635
|
const root = new MenuRootState(opts);
|
|
@@ -81,6 +667,12 @@ export class MenuMenuState {
|
|
|
81
667
|
onComplete: () => {
|
|
82
668
|
this.opts.onOpenChangeComplete.current(this.opts.open.current);
|
|
83
669
|
},
|
|
670
|
+
shouldSkipExitAnimation: () => {
|
|
671
|
+
if (this.root.opts.variant.current !== "menubar" || this.parentMenu !== null) {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
return this.root.opts.shouldSkipExitAnimation?.() ?? false;
|
|
675
|
+
},
|
|
84
676
|
});
|
|
85
677
|
if (parentMenu) {
|
|
86
678
|
watch(() => parentMenu.opts.open.current, () => {
|
|
@@ -129,13 +721,17 @@ export class MenuContentState {
|
|
|
129
721
|
this.onblur = this.onblur.bind(this);
|
|
130
722
|
this.onfocus = this.onfocus.bind(this);
|
|
131
723
|
this.handleInteractOutside = this.handleInteractOutside.bind(this);
|
|
132
|
-
new
|
|
724
|
+
new MenuSubmenuIntent({
|
|
133
725
|
contentNode: () => this.parentMenu.contentNode,
|
|
134
726
|
triggerNode: () => this.parentMenu.triggerNode,
|
|
727
|
+
parentContentNode: () => this.parentMenu.parentMenu?.contentNode ?? null,
|
|
728
|
+
subContentSelector: () => `[${this.parentMenu.root.getBitsAttr("sub-content")}]`,
|
|
729
|
+
// debugMode: () => this.parentMenu.root.opts.debugMode.current,
|
|
135
730
|
enabled: () => this.parentMenu.opts.open.current &&
|
|
136
731
|
Boolean(this.parentMenu.triggerNode?.hasAttribute(this.parentMenu.root.getBitsAttr("sub-trigger"))),
|
|
137
|
-
|
|
732
|
+
onIntentExit: (pointerPoint) => {
|
|
138
733
|
this.parentMenu.opts.open.current = false;
|
|
734
|
+
this.#dispatchPointerMoveToHoveredSubTrigger(pointerPoint);
|
|
139
735
|
},
|
|
140
736
|
setIsPointerInTransit: (value) => {
|
|
141
737
|
this.parentMenu.root.isPointerInTransit = value;
|
|
@@ -179,6 +775,30 @@ export class MenuContentState {
|
|
|
179
775
|
#isPointerMovingToSubmenu() {
|
|
180
776
|
return this.parentMenu.root.isPointerInTransit;
|
|
181
777
|
}
|
|
778
|
+
#dispatchPointerMoveToHoveredSubTrigger(pointerPoint) {
|
|
779
|
+
if (!pointerPoint)
|
|
780
|
+
return;
|
|
781
|
+
const parentContentNode = this.parentMenu.parentMenu?.contentNode;
|
|
782
|
+
if (!parentContentNode)
|
|
783
|
+
return;
|
|
784
|
+
const hoveredNode = this.domContext
|
|
785
|
+
.getDocument()
|
|
786
|
+
.elementFromPoint(pointerPoint.x, pointerPoint.y);
|
|
787
|
+
if (!isElement(hoveredNode))
|
|
788
|
+
return;
|
|
789
|
+
const hoveredSubTrigger = hoveredNode.closest(`[${this.parentMenu.root.getBitsAttr("sub-trigger")}]`);
|
|
790
|
+
if (!hoveredSubTrigger || !parentContentNode.contains(hoveredSubTrigger))
|
|
791
|
+
return;
|
|
792
|
+
if (hoveredSubTrigger === this.parentMenu.triggerNode)
|
|
793
|
+
return;
|
|
794
|
+
hoveredSubTrigger.dispatchEvent(new PointerEvent("pointermove", {
|
|
795
|
+
bubbles: true,
|
|
796
|
+
cancelable: true,
|
|
797
|
+
pointerType: "mouse",
|
|
798
|
+
clientX: pointerPoint.x,
|
|
799
|
+
clientY: pointerPoint.y,
|
|
800
|
+
}));
|
|
801
|
+
}
|
|
182
802
|
onCloseAutoFocus = (e) => {
|
|
183
803
|
this.opts.onCloseAutoFocus.current?.(e);
|
|
184
804
|
if (e.defaultPrevented || this.#isSub)
|
|
@@ -202,12 +822,9 @@ export class MenuContentState {
|
|
|
202
822
|
while (rootMenu.parentMenu !== null) {
|
|
203
823
|
rootMenu = rootMenu.parentMenu;
|
|
204
824
|
}
|
|
205
|
-
// if for some unforeseen reason the root menu has no trigger, we bail
|
|
206
825
|
if (!rootMenu.triggerNode)
|
|
207
826
|
return;
|
|
208
|
-
// cancel default tab behavior
|
|
209
827
|
e.preventDefault();
|
|
210
|
-
// find the next/previous tabbable
|
|
211
828
|
const nodeToFocus = getTabbableFrom(rootMenu.triggerNode, e.shiftKey ? "prev" : "next");
|
|
212
829
|
if (nodeToFocus) {
|
|
213
830
|
/**
|
|
@@ -332,13 +949,13 @@ export class MenuContentState {
|
|
|
332
949
|
"aria-orientation": "vertical",
|
|
333
950
|
[this.parentMenu.root.getBitsAttr("content")]: "",
|
|
334
951
|
"data-state": getDataOpenClosed(this.parentMenu.opts.open.current),
|
|
952
|
+
...getDataTransitionAttrs(this.parentMenu.contentPresence.transitionStatus),
|
|
335
953
|
onkeydown: this.onkeydown,
|
|
336
954
|
onblur: this.onblur,
|
|
337
955
|
onfocus: this.onfocus,
|
|
338
956
|
dir: this.parentMenu.root.opts.dir.current,
|
|
339
957
|
style: {
|
|
340
958
|
pointerEvents: "auto",
|
|
341
|
-
// CSS containment isolates style/layout/paint calculations from the rest of the page
|
|
342
959
|
contain: "layout style",
|
|
343
960
|
},
|
|
344
961
|
...this.attachment,
|
|
@@ -524,11 +1141,19 @@ export class MenuSubTriggerState {
|
|
|
524
1141
|
onpointermove(e) {
|
|
525
1142
|
if (!isMouseEvent(e))
|
|
526
1143
|
return;
|
|
1144
|
+
if (this.submenu.root.isPointerInTransit) {
|
|
1145
|
+
if (this.#openTimer !== null)
|
|
1146
|
+
this.#clearOpenTimer();
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
527
1149
|
if (!this.item.opts.disabled.current &&
|
|
528
1150
|
!this.submenu.opts.open.current &&
|
|
529
|
-
!this.#openTimer
|
|
530
|
-
!this.content.parentMenu.root.isPointerInTransit) {
|
|
1151
|
+
!this.#openTimer) {
|
|
531
1152
|
this.#openTimer = this.content.domContext.setTimeout(() => {
|
|
1153
|
+
if (this.submenu.root.isPointerInTransit) {
|
|
1154
|
+
this.#clearOpenTimer();
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
532
1157
|
this.submenu.onOpen();
|
|
533
1158
|
this.#clearOpenTimer();
|
|
534
1159
|
}, this.opts.openDelay.current);
|
|
@@ -929,7 +1554,6 @@ export class ContextMenuTriggerState {
|
|
|
929
1554
|
"data-state": getDataOpenClosed(this.parentMenu.opts.open.current),
|
|
930
1555
|
[CONTEXT_MENU_TRIGGER_ATTR]: "",
|
|
931
1556
|
tabindex: -1,
|
|
932
|
-
//
|
|
933
1557
|
onpointerdown: this.onpointerdown,
|
|
934
1558
|
onpointermove: this.onpointermove,
|
|
935
1559
|
onpointercancel: this.onpointercancel,
|