bits-ui 2.16.2 → 2.16.4
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/menu/menu.svelte.js +14 -0
- package/dist/bits/tooltip/tooltip.svelte.js +10 -4
- package/dist/bits/utilities/popper-layer/popper-layer-inner.svelte +8 -3
- package/dist/internal/floating-svelte/use-floating.svelte.js +13 -3
- package/dist/internal/safe-polygon.svelte.d.ts +1 -0
- package/dist/internal/safe-polygon.svelte.js +53 -10
- package/package.json +1 -1
|
@@ -183,6 +183,10 @@ export class MenuContentState {
|
|
|
183
183
|
this.opts.onCloseAutoFocus.current?.(e);
|
|
184
184
|
if (e.defaultPrevented || this.#isSub)
|
|
185
185
|
return;
|
|
186
|
+
if (this.parentMenu.root.ignoreCloseAutoFocus) {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
186
190
|
if (this.parentMenu.triggerNode && isTabbable(this.parentMenu.triggerNode)) {
|
|
187
191
|
e.preventDefault();
|
|
188
192
|
this.parentMenu.triggerNode.focus();
|
|
@@ -306,7 +310,17 @@ export class MenuContentState {
|
|
|
306
310
|
}
|
|
307
311
|
if (e.target.closest(`#${triggerId}`)) {
|
|
308
312
|
e.preventDefault();
|
|
313
|
+
return;
|
|
309
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* when the menu closes due to an outside pointer interaction (for example,
|
|
317
|
+
* clicking another dropdown trigger), avoid focusing this menu's trigger
|
|
318
|
+
* to prevent stealing focus from the new interaction target.
|
|
319
|
+
*/
|
|
320
|
+
this.parentMenu.root.ignoreCloseAutoFocus = true;
|
|
321
|
+
afterTick(() => {
|
|
322
|
+
this.parentMenu.root.ignoreCloseAutoFocus = false;
|
|
323
|
+
});
|
|
310
324
|
}
|
|
311
325
|
get shouldRender() {
|
|
312
326
|
return this.parentMenu.contentPresence.shouldRender;
|
|
@@ -524,14 +524,19 @@ export class TooltipTriggerState {
|
|
|
524
524
|
const relatedTarget = e.relatedTarget;
|
|
525
525
|
// when moving to a sibling trigger and skip delay is active, don't close —
|
|
526
526
|
// the sibling's enter handler will switch the active trigger instantly.
|
|
527
|
-
// if skipDelayDuration is 0 there's no grace period, so
|
|
528
|
-
//
|
|
529
|
-
if (isElement(relatedTarget)
|
|
527
|
+
// if skipDelayDuration is 0 there's no grace period, so close now and let
|
|
528
|
+
// the sibling wait through the full delay (and re-animate).
|
|
529
|
+
if (isElement(relatedTarget)) {
|
|
530
530
|
for (const record of root.registry.triggers.values()) {
|
|
531
|
-
if (record.node
|
|
531
|
+
if (record.node !== relatedTarget)
|
|
532
|
+
continue;
|
|
533
|
+
if (root.provider.opts.skipDelayDuration.current > 0) {
|
|
532
534
|
this.#hasPointerMoveOpened = false;
|
|
533
535
|
return;
|
|
534
536
|
}
|
|
537
|
+
root.handleClose();
|
|
538
|
+
this.#hasPointerMoveOpened = false;
|
|
539
|
+
return;
|
|
535
540
|
}
|
|
536
541
|
}
|
|
537
542
|
root.onTriggerLeave();
|
|
@@ -606,6 +611,7 @@ export class TooltipContentState {
|
|
|
606
611
|
triggerNode: () => this.root.triggerNode,
|
|
607
612
|
contentNode: () => this.root.contentNode,
|
|
608
613
|
enabled: () => this.root.opts.open.current && !this.root.disableHoverableContent,
|
|
614
|
+
transitIntentTimeout: 180,
|
|
609
615
|
ignoredTargets: () => {
|
|
610
616
|
// only skip closing for sibling triggers when there's a skip-delay grace period;
|
|
611
617
|
// with skipDelayDuration=0 the close+reopen is intentional (full delay + re-animation)
|
|
@@ -52,6 +52,11 @@
|
|
|
52
52
|
enabled: boolean;
|
|
53
53
|
contentPointerEvents?: "auto" | "none";
|
|
54
54
|
} = $props();
|
|
55
|
+
|
|
56
|
+
const resolvedPreventScroll = $derived(preventScroll ?? true);
|
|
57
|
+
const effectiveStrategy = $derived(
|
|
58
|
+
strategy ?? (resolvedPreventScroll ? "fixed" : "absolute")
|
|
59
|
+
);
|
|
55
60
|
</script>
|
|
56
61
|
|
|
57
62
|
<PopperContent
|
|
@@ -68,7 +73,7 @@
|
|
|
68
73
|
{sticky}
|
|
69
74
|
{hideWhenDetached}
|
|
70
75
|
{updatePositionStrategy}
|
|
71
|
-
{
|
|
76
|
+
strategy={effectiveStrategy}
|
|
72
77
|
{dir}
|
|
73
78
|
{wrapperId}
|
|
74
79
|
{style}
|
|
@@ -79,9 +84,9 @@
|
|
|
79
84
|
>
|
|
80
85
|
{#snippet content({ props: floatingProps, wrapperProps })}
|
|
81
86
|
{#if restProps.forceMount && enabled}
|
|
82
|
-
<ScrollLock {
|
|
87
|
+
<ScrollLock preventScroll={resolvedPreventScroll} />
|
|
83
88
|
{:else if !restProps.forceMount}
|
|
84
|
-
<ScrollLock {
|
|
89
|
+
<ScrollLock preventScroll={resolvedPreventScroll} />
|
|
85
90
|
{/if}
|
|
86
91
|
<FocusScope
|
|
87
92
|
{onOpenAutoFocus}
|
|
@@ -23,6 +23,7 @@ export function useFloating(options) {
|
|
|
23
23
|
let middlewareData = $state({});
|
|
24
24
|
let isPositioned = $state(false);
|
|
25
25
|
let hasWhileMountedPosition = false;
|
|
26
|
+
let updateRequestId = 0;
|
|
26
27
|
const floatingStyles = $derived.by(() => {
|
|
27
28
|
// preserve last known position when floating ref is null (during transitions)
|
|
28
29
|
const xVal = floating.current ? roundByDPR(floating.current, x) : x;
|
|
@@ -50,12 +51,20 @@ export function useFloating(options) {
|
|
|
50
51
|
function update() {
|
|
51
52
|
if (reference.current === null || floating.current === null)
|
|
52
53
|
return;
|
|
53
|
-
|
|
54
|
+
const referenceNode = reference.current;
|
|
55
|
+
const floatingNode = floating.current;
|
|
56
|
+
const requestId = ++updateRequestId;
|
|
57
|
+
computePosition(referenceNode, floatingNode, {
|
|
54
58
|
middleware: middlewareOption,
|
|
55
59
|
placement: placementOption,
|
|
56
60
|
strategy: strategyOption,
|
|
57
61
|
}).then((position) => {
|
|
58
|
-
|
|
62
|
+
// ignore stale async resolutions when newer updates were requested.
|
|
63
|
+
if (requestId !== updateRequestId)
|
|
64
|
+
return;
|
|
65
|
+
// ignore stale resolutions after ref replacement.
|
|
66
|
+
if (reference.current !== referenceNode || floating.current !== floatingNode)
|
|
67
|
+
return;
|
|
59
68
|
const referenceHidden = isReferenceHidden(referenceNode);
|
|
60
69
|
if (referenceHidden) {
|
|
61
70
|
// keep last good coordinates when the anchor disappears to avoid
|
|
@@ -91,6 +100,7 @@ export function useFloating(options) {
|
|
|
91
100
|
whileElementsMountedCleanup();
|
|
92
101
|
whileElementsMountedCleanup = undefined;
|
|
93
102
|
}
|
|
103
|
+
updateRequestId++;
|
|
94
104
|
}
|
|
95
105
|
function attach() {
|
|
96
106
|
cleanup();
|
|
@@ -105,7 +115,7 @@ export function useFloating(options) {
|
|
|
105
115
|
whileElementsMountedCleanup = whileElementsMountedOption(reference.current, floating.current, update);
|
|
106
116
|
}
|
|
107
117
|
function reset() {
|
|
108
|
-
if (!openOption) {
|
|
118
|
+
if (!openOption && floating.current === null) {
|
|
109
119
|
isPositioned = false;
|
|
110
120
|
}
|
|
111
121
|
}
|
|
@@ -5,6 +5,7 @@ export interface SafePolygonOptions {
|
|
|
5
5
|
contentNode: Getter<HTMLElement | null>;
|
|
6
6
|
onPointerExit: () => void;
|
|
7
7
|
buffer?: number;
|
|
8
|
+
transitIntentTimeout?: number;
|
|
8
9
|
/** nodes that should not trigger a close when they become the relatedTarget on trigger leave (e.g. sibling triggers in singleton mode) */
|
|
9
10
|
ignoredTargets?: Getter<HTMLElement[]>;
|
|
10
11
|
}
|
|
@@ -42,11 +42,15 @@ function getSide(triggerRect, contentRect) {
|
|
|
42
42
|
export class SafePolygon {
|
|
43
43
|
#opts;
|
|
44
44
|
#buffer;
|
|
45
|
+
#transitIntentTimeout;
|
|
45
46
|
// tracks the cursor position when leaving trigger or content
|
|
46
47
|
#exitPoint = null;
|
|
47
48
|
// tracks what we're moving toward: "content" when leaving trigger, "trigger" when leaving content
|
|
48
49
|
#exitTarget = null;
|
|
50
|
+
#transitTargets = [];
|
|
51
|
+
#trackedTriggerNode = null;
|
|
49
52
|
#leaveFallbackRafId = null;
|
|
53
|
+
#transitIntentTimeoutId = null;
|
|
50
54
|
#cancelLeaveFallback() {
|
|
51
55
|
if (this.#leaveFallbackRafId !== null) {
|
|
52
56
|
cancelAnimationFrame(this.#leaveFallbackRafId);
|
|
@@ -63,14 +67,42 @@ export class SafePolygon {
|
|
|
63
67
|
this.#opts.onPointerExit();
|
|
64
68
|
});
|
|
65
69
|
}
|
|
70
|
+
#cancelTransitIntentTimeout() {
|
|
71
|
+
if (this.#transitIntentTimeoutId !== null) {
|
|
72
|
+
clearTimeout(this.#transitIntentTimeoutId);
|
|
73
|
+
this.#transitIntentTimeoutId = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
#scheduleTransitIntentTimeout() {
|
|
77
|
+
if (this.#transitIntentTimeout === null)
|
|
78
|
+
return;
|
|
79
|
+
this.#cancelTransitIntentTimeout();
|
|
80
|
+
this.#transitIntentTimeoutId = window.setTimeout(() => {
|
|
81
|
+
this.#transitIntentTimeoutId = null;
|
|
82
|
+
if (!this.#exitPoint || !this.#exitTarget)
|
|
83
|
+
return;
|
|
84
|
+
this.#clearTracking();
|
|
85
|
+
this.#opts.onPointerExit();
|
|
86
|
+
}, this.#transitIntentTimeout);
|
|
87
|
+
}
|
|
66
88
|
constructor(opts) {
|
|
67
89
|
this.#opts = opts;
|
|
68
90
|
this.#buffer = opts.buffer ?? 1;
|
|
91
|
+
const transitIntentTimeout = opts.transitIntentTimeout;
|
|
92
|
+
this.#transitIntentTimeout =
|
|
93
|
+
typeof transitIntentTimeout === "number" && transitIntentTimeout > 0
|
|
94
|
+
? transitIntentTimeout
|
|
95
|
+
: null;
|
|
69
96
|
watch([opts.triggerNode, opts.contentNode, opts.enabled], ([triggerNode, contentNode, enabled]) => {
|
|
70
97
|
if (!triggerNode || !contentNode || !enabled) {
|
|
98
|
+
this.#trackedTriggerNode = null;
|
|
71
99
|
this.#clearTracking();
|
|
72
100
|
return;
|
|
73
101
|
}
|
|
102
|
+
if (this.#trackedTriggerNode && this.#trackedTriggerNode !== triggerNode) {
|
|
103
|
+
this.#clearTracking();
|
|
104
|
+
}
|
|
105
|
+
this.#trackedTriggerNode = triggerNode;
|
|
74
106
|
const doc = getDocument(triggerNode);
|
|
75
107
|
const handlePointerMove = (e) => {
|
|
76
108
|
this.#onPointerMove([e.clientX, e.clientY], triggerNode, contentNode);
|
|
@@ -89,16 +121,13 @@ export class SafePolygon {
|
|
|
89
121
|
ignoredTargets.some((n) => n === target || n.contains(target))) {
|
|
90
122
|
return;
|
|
91
123
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.#opts.onPointerExit();
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
124
|
+
this.#transitTargets =
|
|
125
|
+
isElement(target) && ignoredTargets.length > 0
|
|
126
|
+
? ignoredTargets.filter((n) => target.contains(n))
|
|
127
|
+
: [];
|
|
128
|
+
// for unrelated elements, defer close decisions to pointer geometry checks.
|
|
129
|
+
// this allows the cursor to pass through intermediate elements on the way
|
|
130
|
+
// to content without immediately closing.
|
|
102
131
|
this.#exitPoint = [e.clientX, e.clientY];
|
|
103
132
|
this.#exitTarget = "content";
|
|
104
133
|
this.#scheduleLeaveFallback();
|
|
@@ -140,6 +169,7 @@ export class SafePolygon {
|
|
|
140
169
|
if (!this.#exitPoint || !this.#exitTarget)
|
|
141
170
|
return;
|
|
142
171
|
this.#cancelLeaveFallback();
|
|
172
|
+
this.#scheduleTransitIntentTimeout();
|
|
143
173
|
const triggerRect = triggerNode.getBoundingClientRect();
|
|
144
174
|
const contentRect = contentNode.getBoundingClientRect();
|
|
145
175
|
// check if pointer reached the target
|
|
@@ -151,6 +181,17 @@ export class SafePolygon {
|
|
|
151
181
|
this.#clearTracking();
|
|
152
182
|
return;
|
|
153
183
|
}
|
|
184
|
+
if (this.#exitTarget === "content" && this.#transitTargets.length > 0) {
|
|
185
|
+
for (const transitTarget of this.#transitTargets) {
|
|
186
|
+
const transitRect = transitTarget.getBoundingClientRect();
|
|
187
|
+
if (isInsideRect(clientPoint, transitRect))
|
|
188
|
+
return;
|
|
189
|
+
const transitSide = getSide(triggerRect, transitRect);
|
|
190
|
+
const transitCorridor = this.#getCorridorPolygon(triggerRect, transitRect, transitSide);
|
|
191
|
+
if (transitCorridor && isPointInPolygon(clientPoint, transitCorridor))
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
154
195
|
// check if pointer is in the rectangular corridor between trigger and content
|
|
155
196
|
const side = getSide(triggerRect, contentRect);
|
|
156
197
|
const corridorPoly = this.#getCorridorPolygon(triggerRect, contentRect, side);
|
|
@@ -170,7 +211,9 @@ export class SafePolygon {
|
|
|
170
211
|
#clearTracking() {
|
|
171
212
|
this.#exitPoint = null;
|
|
172
213
|
this.#exitTarget = null;
|
|
214
|
+
this.#transitTargets = [];
|
|
173
215
|
this.#cancelLeaveFallback();
|
|
216
|
+
this.#cancelTransitIntentTimeout();
|
|
174
217
|
}
|
|
175
218
|
/**
|
|
176
219
|
* Creates a rectangular corridor between trigger and content
|