bits-ui 2.15.6 → 2.15.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/bits/popover/popover.svelte.js +1 -0
- package/dist/bits/select/select.svelte.js +4 -4
- package/dist/bits/tooltip/tooltip.svelte.d.ts +1 -0
- package/dist/bits/tooltip/tooltip.svelte.js +40 -14
- package/dist/bits/utilities/floating-layer/use-floating-layer.svelte.js +14 -2
- package/dist/internal/floating-svelte/use-floating.svelte.js +39 -1
- package/dist/internal/safe-polygon.svelte.d.ts +1 -0
- package/dist/internal/safe-polygon.svelte.js +91 -16
- package/package.json +1 -1
|
@@ -260,6 +260,7 @@ export class PopoverContentState {
|
|
|
260
260
|
enabled: () => this.root.opts.open.current &&
|
|
261
261
|
this.root.openedViaHover &&
|
|
262
262
|
!this.root.hasInteractedWithContent,
|
|
263
|
+
throttlePointerMove: true,
|
|
263
264
|
onPointerExit: () => {
|
|
264
265
|
this.root.handleDelayedHoverClose();
|
|
265
266
|
},
|
|
@@ -108,10 +108,10 @@ class SelectBaseRootState {
|
|
|
108
108
|
if (!this.viewportNode)
|
|
109
109
|
return false;
|
|
110
110
|
const nodeRect = node.getBoundingClientRect();
|
|
111
|
-
const isNodeFullyVisible = nodeRect.right
|
|
112
|
-
nodeRect.left
|
|
113
|
-
nodeRect.bottom
|
|
114
|
-
nodeRect.top
|
|
111
|
+
const isNodeFullyVisible = nodeRect.right <= viewportRect.right &&
|
|
112
|
+
nodeRect.left >= viewportRect.left &&
|
|
113
|
+
nodeRect.bottom <= viewportRect.bottom &&
|
|
114
|
+
nodeRect.top >= viewportRect.top;
|
|
115
115
|
return isNodeFullyVisible;
|
|
116
116
|
});
|
|
117
117
|
}
|
|
@@ -92,6 +92,7 @@ interface TooltipContentStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
|
|
92
92
|
}> {
|
|
93
93
|
}
|
|
94
94
|
export declare class TooltipContentState {
|
|
95
|
+
#private;
|
|
95
96
|
static create(opts: TooltipContentStateOpts): TooltipContentState;
|
|
96
97
|
readonly opts: TooltipContentStateOpts;
|
|
97
98
|
readonly root: TooltipRootState;
|
|
@@ -26,6 +26,20 @@ export class TooltipProviderState {
|
|
|
26
26
|
this.#timerFn = new TimeoutFn(() => {
|
|
27
27
|
this.isOpenDelayed = true;
|
|
28
28
|
}, this.opts.skipDelayDuration.current);
|
|
29
|
+
onMountEffect(() => on(window, "scroll", (e) => {
|
|
30
|
+
const activeTooltip = this.#openTooltip;
|
|
31
|
+
if (!activeTooltip)
|
|
32
|
+
return;
|
|
33
|
+
const triggerNode = activeTooltip.triggerNode;
|
|
34
|
+
if (!triggerNode)
|
|
35
|
+
return;
|
|
36
|
+
const target = e.target;
|
|
37
|
+
if (!(target instanceof Element || target instanceof Document))
|
|
38
|
+
return;
|
|
39
|
+
if (target.contains(triggerNode)) {
|
|
40
|
+
activeTooltip.handleClose();
|
|
41
|
+
}
|
|
42
|
+
}));
|
|
29
43
|
}
|
|
30
44
|
#startTimer = () => {
|
|
31
45
|
const skipDuration = this.opts.skipDelayDuration.current;
|
|
@@ -222,11 +236,21 @@ export class TooltipTriggerState {
|
|
|
222
236
|
this.root.onTriggerEnter();
|
|
223
237
|
this.#hasPointerMoveOpened = true;
|
|
224
238
|
};
|
|
225
|
-
#onpointerleave = () => {
|
|
239
|
+
#onpointerleave = (e) => {
|
|
226
240
|
if (this.#isDisabled)
|
|
227
241
|
return;
|
|
228
242
|
this.#clearTransitCheck();
|
|
229
|
-
|
|
243
|
+
const relatedTarget = e.relatedTarget;
|
|
244
|
+
if (!this.root.disableHoverableContent &&
|
|
245
|
+
this.root.opts.open.current &&
|
|
246
|
+
isElement(relatedTarget) &&
|
|
247
|
+
this.root.contentNode &&
|
|
248
|
+
!this.root.contentNode.contains(relatedTarget)) {
|
|
249
|
+
this.root.handleClose();
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
this.root.onTriggerLeave();
|
|
253
|
+
}
|
|
230
254
|
this.#hasPointerMoveOpened = false;
|
|
231
255
|
};
|
|
232
256
|
#onfocus = (e) => {
|
|
@@ -275,11 +299,11 @@ export class TooltipContentState {
|
|
|
275
299
|
opts;
|
|
276
300
|
root;
|
|
277
301
|
attachment;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
this
|
|
281
|
-
|
|
282
|
-
new SafePolygon({
|
|
302
|
+
#safePolygon = null;
|
|
303
|
+
#createSafePolygon = () => {
|
|
304
|
+
if (this.#safePolygon)
|
|
305
|
+
return;
|
|
306
|
+
this.#safePolygon = new SafePolygon({
|
|
283
307
|
triggerNode: () => this.root.triggerNode,
|
|
284
308
|
contentNode: () => this.root.contentNode,
|
|
285
309
|
enabled: () => this.root.opts.open.current && !this.root.disableHoverableContent,
|
|
@@ -289,14 +313,16 @@ export class TooltipContentState {
|
|
|
289
313
|
}
|
|
290
314
|
},
|
|
291
315
|
});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
316
|
+
};
|
|
317
|
+
constructor(opts, root) {
|
|
318
|
+
this.opts = opts;
|
|
319
|
+
this.root = root;
|
|
320
|
+
this.attachment = attachRef(this.opts.ref, (v) => (this.root.contentNode = v));
|
|
321
|
+
watch(() => this.root.opts.open.current && !this.root.disableHoverableContent, (shouldTrackSafePolygon) => {
|
|
322
|
+
if (!shouldTrackSafePolygon)
|
|
295
323
|
return;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
}));
|
|
324
|
+
this.#createSafePolygon();
|
|
325
|
+
}, { lazy: true });
|
|
300
326
|
}
|
|
301
327
|
onInteractOutside = (e) => {
|
|
302
328
|
if (isElement(e.target) &&
|
|
@@ -181,6 +181,7 @@ export class FloatingContentState {
|
|
|
181
181
|
constructor(opts, root) {
|
|
182
182
|
this.opts = opts;
|
|
183
183
|
this.root = root;
|
|
184
|
+
this.#updatePositionStrategy = opts.updatePositionStrategy;
|
|
184
185
|
if (opts.customAnchor) {
|
|
185
186
|
this.root.customAnchorNode.current = opts.customAnchor.current;
|
|
186
187
|
}
|
|
@@ -208,10 +209,21 @@ export class FloatingContentState {
|
|
|
208
209
|
this.opts.onPlaced?.current();
|
|
209
210
|
});
|
|
210
211
|
watch(() => this.contentRef.current, (contentNode) => {
|
|
211
|
-
if (!contentNode)
|
|
212
|
+
if (!contentNode || !this.opts.enabled.current)
|
|
212
213
|
return;
|
|
213
214
|
const win = getWindow(contentNode);
|
|
214
|
-
|
|
215
|
+
const rafId = win.requestAnimationFrame(() => {
|
|
216
|
+
// avoid applying stale values when refs change quickly
|
|
217
|
+
if (this.contentRef.current !== contentNode || !this.opts.enabled.current)
|
|
218
|
+
return;
|
|
219
|
+
const zIndex = win.getComputedStyle(contentNode).zIndex;
|
|
220
|
+
if (zIndex !== this.contentZIndex) {
|
|
221
|
+
this.contentZIndex = zIndex;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return () => {
|
|
225
|
+
win.cancelAnimationFrame(rafId);
|
|
226
|
+
};
|
|
215
227
|
});
|
|
216
228
|
$effect(() => {
|
|
217
229
|
this.floating.floating.current = this.wrapperRef.current;
|
|
@@ -22,6 +22,7 @@ export function useFloating(options) {
|
|
|
22
22
|
let placement = $state(placementOption);
|
|
23
23
|
let middlewareData = $state({});
|
|
24
24
|
let isPositioned = $state(false);
|
|
25
|
+
let hasWhileMountedPosition = false;
|
|
25
26
|
const floatingStyles = $derived.by(() => {
|
|
26
27
|
// preserve last known position when floating ref is null (during transitions)
|
|
27
28
|
const xVal = floating.current ? roundByDPR(floating.current, x) : x;
|
|
@@ -82,6 +83,8 @@ export function useFloating(options) {
|
|
|
82
83
|
update();
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
86
|
+
if (!openOption)
|
|
87
|
+
return;
|
|
85
88
|
if (reference.current === null || floating.current === null)
|
|
86
89
|
return;
|
|
87
90
|
whileElementsMountedCleanup = whileElementsMountedOption(reference.current, floating.current, update);
|
|
@@ -91,8 +94,43 @@ export function useFloating(options) {
|
|
|
91
94
|
isPositioned = false;
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
|
-
|
|
97
|
+
function trackWhileMountedDeps() {
|
|
98
|
+
return [
|
|
99
|
+
middlewareOption,
|
|
100
|
+
placementOption,
|
|
101
|
+
strategyOption,
|
|
102
|
+
sideOffsetOption,
|
|
103
|
+
alignOffsetOption,
|
|
104
|
+
openOption,
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
$effect(() => {
|
|
108
|
+
if (whileElementsMountedOption !== undefined)
|
|
109
|
+
return;
|
|
110
|
+
if (!openOption)
|
|
111
|
+
return;
|
|
112
|
+
update();
|
|
113
|
+
});
|
|
95
114
|
$effect(attach);
|
|
115
|
+
$effect(() => {
|
|
116
|
+
if (whileElementsMountedOption === undefined)
|
|
117
|
+
return;
|
|
118
|
+
trackWhileMountedDeps();
|
|
119
|
+
if (!openOption) {
|
|
120
|
+
hasWhileMountedPosition = false;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!isPositioned) {
|
|
124
|
+
hasWhileMountedPosition = false;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// skip the first post-position run, since autoUpdate already computed it
|
|
128
|
+
if (!hasWhileMountedPosition) {
|
|
129
|
+
hasWhileMountedPosition = true;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
update();
|
|
133
|
+
});
|
|
96
134
|
$effect(reset);
|
|
97
135
|
$effect(() => cleanup);
|
|
98
136
|
return {
|
|
@@ -46,18 +46,52 @@ export class SafePolygon {
|
|
|
46
46
|
#exitPoint = null;
|
|
47
47
|
// tracks what we're moving toward: "content" when leaving trigger, "trigger" when leaving content
|
|
48
48
|
#exitTarget = null;
|
|
49
|
+
#triggerRect = null;
|
|
50
|
+
#contentRect = null;
|
|
51
|
+
#pointerMoveRafId = null;
|
|
52
|
+
#pendingClientPoint = null;
|
|
53
|
+
#leaveFallbackRafId = null;
|
|
54
|
+
#cancelLeaveFallback() {
|
|
55
|
+
if (this.#leaveFallbackRafId !== null) {
|
|
56
|
+
cancelAnimationFrame(this.#leaveFallbackRafId);
|
|
57
|
+
this.#leaveFallbackRafId = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
#scheduleLeaveFallback() {
|
|
61
|
+
this.#cancelLeaveFallback();
|
|
62
|
+
this.#leaveFallbackRafId = requestAnimationFrame(() => {
|
|
63
|
+
this.#leaveFallbackRafId = null;
|
|
64
|
+
if (!this.#exitPoint || !this.#exitTarget)
|
|
65
|
+
return;
|
|
66
|
+
this.#clearTracking();
|
|
67
|
+
this.#opts.onPointerExit();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
#closeIfPointerEnteredOutside(target, triggerNode, contentNode) {
|
|
71
|
+
if (!isElement(target))
|
|
72
|
+
return false;
|
|
73
|
+
if (triggerNode.contains(target) || contentNode.contains(target))
|
|
74
|
+
return false;
|
|
75
|
+
this.#clearTracking();
|
|
76
|
+
this.#opts.onPointerExit();
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
49
79
|
constructor(opts) {
|
|
50
80
|
this.#opts = opts;
|
|
51
81
|
this.#buffer = opts.buffer ?? 1;
|
|
52
82
|
watch([opts.triggerNode, opts.contentNode, opts.enabled], ([triggerNode, contentNode, enabled]) => {
|
|
53
83
|
if (!triggerNode || !contentNode || !enabled) {
|
|
54
|
-
this.#
|
|
55
|
-
this.#exitTarget = null;
|
|
84
|
+
this.#clearTracking();
|
|
56
85
|
return;
|
|
57
86
|
}
|
|
58
87
|
const doc = getDocument(triggerNode);
|
|
59
88
|
const handlePointerMove = (e) => {
|
|
60
|
-
this.#
|
|
89
|
+
if (this.#opts.throttlePointerMove) {
|
|
90
|
+
this.#onPointerMoveThrottled(e, triggerNode, contentNode);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
this.#onPointerMove([e.clientX, e.clientY], triggerNode, contentNode);
|
|
94
|
+
}
|
|
61
95
|
};
|
|
62
96
|
const handleTriggerLeave = (e) => {
|
|
63
97
|
// when leaving trigger toward content, record exit point
|
|
@@ -66,18 +100,21 @@ export class SafePolygon {
|
|
|
66
100
|
if (isElement(target) && contentNode.contains(target)) {
|
|
67
101
|
return;
|
|
68
102
|
}
|
|
103
|
+
if (this.#closeIfPointerEnteredOutside(target, triggerNode, contentNode)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
69
106
|
this.#exitPoint = [e.clientX, e.clientY];
|
|
70
107
|
this.#exitTarget = "content";
|
|
108
|
+
this.#captureRects(triggerNode, contentNode);
|
|
109
|
+
this.#scheduleLeaveFallback();
|
|
71
110
|
};
|
|
72
111
|
const handleTriggerEnter = () => {
|
|
73
112
|
// reached trigger, clear tracking
|
|
74
|
-
this.#
|
|
75
|
-
this.#exitTarget = null;
|
|
113
|
+
this.#clearTracking();
|
|
76
114
|
};
|
|
77
115
|
const handleContentEnter = () => {
|
|
78
116
|
// reached content, clear tracking
|
|
79
|
-
this.#
|
|
80
|
-
this.#exitTarget = null;
|
|
117
|
+
this.#clearTracking();
|
|
81
118
|
};
|
|
82
119
|
const handleContentLeave = (e) => {
|
|
83
120
|
// when leaving content, check if going directly back to trigger
|
|
@@ -86,9 +123,14 @@ export class SafePolygon {
|
|
|
86
123
|
// going directly to trigger, no polygon tracking needed
|
|
87
124
|
return;
|
|
88
125
|
}
|
|
126
|
+
if (this.#closeIfPointerEnteredOutside(target, triggerNode, contentNode)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
89
129
|
// might be traversing gap back to trigger, set up polygon tracking
|
|
90
130
|
this.#exitPoint = [e.clientX, e.clientY];
|
|
91
131
|
this.#exitTarget = "trigger";
|
|
132
|
+
this.#captureRects(triggerNode, contentNode);
|
|
133
|
+
this.#scheduleLeaveFallback();
|
|
92
134
|
};
|
|
93
135
|
return [
|
|
94
136
|
on(doc, "pointermove", handlePointerMove),
|
|
@@ -102,22 +144,40 @@ export class SafePolygon {
|
|
|
102
144
|
}, () => { });
|
|
103
145
|
});
|
|
104
146
|
}
|
|
105
|
-
#
|
|
147
|
+
#onPointerMoveThrottled(e, triggerNode, contentNode) {
|
|
148
|
+
if (this.#pointerMoveRafId === null) {
|
|
149
|
+
// handle the first move in the frame immediately so close checks
|
|
150
|
+
// are not deferred when only a single pointermove fires.
|
|
151
|
+
this.#pointerMoveRafId = requestAnimationFrame(() => {
|
|
152
|
+
this.#pointerMoveRafId = null;
|
|
153
|
+
const point = this.#pendingClientPoint;
|
|
154
|
+
this.#pendingClientPoint = null;
|
|
155
|
+
if (!point)
|
|
156
|
+
return;
|
|
157
|
+
this.#onPointerMove(point, triggerNode, contentNode);
|
|
158
|
+
});
|
|
159
|
+
this.#onPointerMove([e.clientX, e.clientY], triggerNode, contentNode);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.#pendingClientPoint = [e.clientX, e.clientY];
|
|
163
|
+
}
|
|
164
|
+
#onPointerMove(clientPoint, triggerNode, contentNode) {
|
|
106
165
|
// if no exit point recorded, nothing to check
|
|
107
166
|
if (!this.#exitPoint || !this.#exitTarget)
|
|
108
167
|
return;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
168
|
+
this.#cancelLeaveFallback();
|
|
169
|
+
if (!this.#triggerRect || !this.#contentRect) {
|
|
170
|
+
this.#captureRects(triggerNode, contentNode);
|
|
171
|
+
}
|
|
172
|
+
const triggerRect = this.#triggerRect ?? triggerNode.getBoundingClientRect();
|
|
173
|
+
const contentRect = this.#contentRect ?? contentNode.getBoundingClientRect();
|
|
112
174
|
// check if pointer reached the target
|
|
113
175
|
if (this.#exitTarget === "content" && isInsideRect(clientPoint, contentRect)) {
|
|
114
|
-
this.#
|
|
115
|
-
this.#exitTarget = null;
|
|
176
|
+
this.#clearTracking();
|
|
116
177
|
return;
|
|
117
178
|
}
|
|
118
179
|
if (this.#exitTarget === "trigger" && isInsideRect(clientPoint, triggerRect)) {
|
|
119
|
-
this.#
|
|
120
|
-
this.#exitTarget = null;
|
|
180
|
+
this.#clearTracking();
|
|
121
181
|
return;
|
|
122
182
|
}
|
|
123
183
|
// check if pointer is in the rectangular corridor between trigger and content
|
|
@@ -133,9 +193,24 @@ export class SafePolygon {
|
|
|
133
193
|
return;
|
|
134
194
|
}
|
|
135
195
|
// pointer is outside all safe zones - close
|
|
196
|
+
this.#clearTracking();
|
|
197
|
+
this.#opts.onPointerExit();
|
|
198
|
+
}
|
|
199
|
+
#captureRects(triggerNode, contentNode) {
|
|
200
|
+
this.#triggerRect = triggerNode.getBoundingClientRect();
|
|
201
|
+
this.#contentRect = contentNode.getBoundingClientRect();
|
|
202
|
+
}
|
|
203
|
+
#clearTracking() {
|
|
136
204
|
this.#exitPoint = null;
|
|
137
205
|
this.#exitTarget = null;
|
|
138
|
-
this.#
|
|
206
|
+
this.#triggerRect = null;
|
|
207
|
+
this.#contentRect = null;
|
|
208
|
+
this.#pendingClientPoint = null;
|
|
209
|
+
if (this.#pointerMoveRafId !== null) {
|
|
210
|
+
cancelAnimationFrame(this.#pointerMoveRafId);
|
|
211
|
+
this.#pointerMoveRafId = null;
|
|
212
|
+
}
|
|
213
|
+
this.#cancelLeaveFallback();
|
|
139
214
|
}
|
|
140
215
|
/**
|
|
141
216
|
* Creates a rectangular corridor between trigger and content
|