bits-ui 2.16.2 → 2.16.3
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.
|
@@ -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)
|
|
@@ -105,7 +105,7 @@ export function useFloating(options) {
|
|
|
105
105
|
whileElementsMountedCleanup = whileElementsMountedOption(reference.current, floating.current, update);
|
|
106
106
|
}
|
|
107
107
|
function reset() {
|
|
108
|
-
if (!openOption) {
|
|
108
|
+
if (!openOption && floating.current === null) {
|
|
109
109
|
isPositioned = false;
|
|
110
110
|
}
|
|
111
111
|
}
|
|
@@ -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
|