bits-ui 2.16.5 → 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.
Files changed (39) hide show
  1. package/dist/bits/accordion/accordion.svelte.d.ts +4 -2
  2. package/dist/bits/accordion/accordion.svelte.js +2 -1
  3. package/dist/bits/collapsible/collapsible.svelte.d.ts +3 -1
  4. package/dist/bits/collapsible/collapsible.svelte.js +2 -1
  5. package/dist/bits/context-menu/components/context-menu.svelte +3 -1
  6. package/dist/bits/dialog/dialog.svelte.d.ts +4 -0
  7. package/dist/bits/dialog/dialog.svelte.js +3 -1
  8. package/dist/bits/link-preview/link-preview.svelte.d.ts +5 -3
  9. package/dist/bits/link-preview/link-preview.svelte.js +2 -1
  10. package/dist/bits/menu/components/menu-sub-content-static.svelte +12 -3
  11. package/dist/bits/menu/components/menu-sub-content.svelte +12 -3
  12. package/dist/bits/menu/components/menu-sub-trigger.svelte +1 -1
  13. package/dist/bits/menu/components/menu.svelte +5 -0
  14. package/dist/bits/menu/components/menu.svelte.d.ts +1 -0
  15. package/dist/bits/menu/menu.svelte.d.ts +8 -4
  16. package/dist/bits/menu/menu.svelte.js +636 -12
  17. package/dist/bits/menubar/components/menubar-menu.svelte +3 -0
  18. package/dist/bits/menubar/menubar.svelte.d.ts +2 -0
  19. package/dist/bits/menubar/menubar.svelte.js +22 -2
  20. package/dist/bits/navigation-menu/components/navigation-menu-content.svelte +7 -2
  21. package/dist/bits/navigation-menu/components/navigation-menu-indicator.svelte +9 -2
  22. package/dist/bits/navigation-menu/components/navigation-menu-viewport.svelte +5 -3
  23. package/dist/bits/popover/popover.svelte.d.ts +7 -3
  24. package/dist/bits/popover/popover.svelte.js +3 -1
  25. package/dist/bits/select/select.svelte.d.ts +6 -4
  26. package/dist/bits/select/select.svelte.js +2 -1
  27. package/dist/bits/tooltip/tooltip.svelte.d.ts +5 -3
  28. package/dist/bits/tooltip/tooltip.svelte.js +2 -1
  29. package/dist/bits/utilities/floating-layer/use-floating-layer.svelte.d.ts +5 -5
  30. package/dist/bits/utilities/presence-layer/presence-layer.svelte +5 -1
  31. package/dist/bits/utilities/presence-layer/presence.svelte.d.ts +3 -38
  32. package/dist/bits/utilities/presence-layer/presence.svelte.js +49 -146
  33. package/dist/bits/utilities/presence-layer/types.d.ts +7 -3
  34. package/dist/internal/animations-complete.js +64 -9
  35. package/dist/internal/attrs.d.ts +5 -0
  36. package/dist/internal/attrs.js +8 -2
  37. package/dist/internal/presence-manager.svelte.d.ts +4 -1
  38. package/dist/internal/presence-manager.svelte.js +42 -1
  39. 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 GraceArea({
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
- onPointerExit: () => {
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,