Skip to content

Commit 5fa9d26

Browse files
authored
fix: tooltip trigger behavior (#1875)
1 parent 4ab8859 commit 5fa9d26

File tree

3 files changed

+94
-1
lines changed

3 files changed

+94
-1
lines changed

.changeset/empty-streets-design.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"bits-ui": patch
3+
---
4+
5+
fix(Tooltip): ensure hovering between triggers of the same provider is smooth

docs/src/lib/components/demos/tooltip-demo.svelte

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,58 @@
2222
</div>
2323
</Tooltip.Content>
2424
</Tooltip.Root>
25+
<Tooltip.Root delayDuration={200}>
26+
<Tooltip.Trigger
27+
class="border-border-input bg-background-alt shadow-btn ring-dark ring-offset-background
28+
hover:bg-muted focus-visible:ring-dark focus-visible:ring-offset-background focus-visible:outline-hidden inline-flex size-10 items-center justify-center rounded-full border focus-visible:ring-2 focus-visible:ring-offset-2"
29+
>
30+
<MagicWand class="size-5" />
31+
</Tooltip.Trigger>
32+
<Tooltip.Content
33+
sideOffset={8}
34+
class="animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-tooltip-content-transform-origin)"
35+
>
36+
<div
37+
class="rounded-input border-dark-10 bg-background shadow-popover outline-hidden z-0 flex items-center justify-center border p-3 text-sm font-medium"
38+
>
39+
Make some magic!
40+
</div>
41+
</Tooltip.Content>
42+
</Tooltip.Root>
43+
<Tooltip.Root delayDuration={200}>
44+
<Tooltip.Trigger
45+
class="border-border-input bg-background-alt shadow-btn ring-dark ring-offset-background
46+
hover:bg-muted focus-visible:ring-dark focus-visible:ring-offset-background focus-visible:outline-hidden inline-flex size-10 items-center justify-center rounded-full border focus-visible:ring-2 focus-visible:ring-offset-2"
47+
>
48+
<MagicWand class="size-5" />
49+
</Tooltip.Trigger>
50+
<Tooltip.Content
51+
sideOffset={8}
52+
class="animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-tooltip-content-transform-origin)"
53+
>
54+
<div
55+
class="rounded-input border-dark-10 bg-background shadow-popover outline-hidden z-0 flex items-center justify-center border p-3 text-sm font-medium"
56+
>
57+
Make some magic!
58+
</div>
59+
</Tooltip.Content>
60+
</Tooltip.Root>
61+
<Tooltip.Root delayDuration={200}>
62+
<Tooltip.Trigger
63+
class="border-border-input bg-background-alt shadow-btn ring-dark ring-offset-background
64+
hover:bg-muted focus-visible:ring-dark focus-visible:ring-offset-background focus-visible:outline-hidden inline-flex size-10 items-center justify-center rounded-full border focus-visible:ring-2 focus-visible:ring-offset-2"
65+
>
66+
<MagicWand class="size-5" />
67+
</Tooltip.Trigger>
68+
<Tooltip.Content
69+
sideOffset={8}
70+
class="animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-tooltip-content-transform-origin)"
71+
>
72+
<div
73+
class="rounded-input border-dark-10 bg-background shadow-popover outline-hidden z-0 flex items-center justify-center border p-3 text-sm font-medium"
74+
>
75+
Make some magic!
76+
</div>
77+
</Tooltip.Content>
78+
</Tooltip.Root>
2579
</Tooltip.Provider>

packages/bits-ui/src/lib/bits/tooltip/tooltip.svelte.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ export class TooltipTriggerState {
235235
#hasPointerMoveOpened = $state(false);
236236
readonly #isDisabled = $derived.by(() => this.opts.disabled.current || this.root.disabled);
237237
domContext: DOMContext;
238+
#transitCheckTimeout: number | null = null;
238239

239240
constructor(opts: TooltipTriggerStateOpts, root: TooltipRootState) {
240241
this.opts = opts;
@@ -243,6 +244,13 @@ export class TooltipTriggerState {
243244
this.attachment = attachRef(this.opts.ref, (v) => (this.root.triggerNode = v));
244245
}
245246

247+
#clearTransitCheck = () => {
248+
if (this.#transitCheckTimeout !== null) {
249+
clearTimeout(this.#transitCheckTimeout);
250+
this.#transitCheckTimeout = null;
251+
}
252+
};
253+
246254
handlePointerUp = () => {
247255
this.#isPointerDown.current = false;
248256
};
@@ -265,19 +273,44 @@ export class TooltipTriggerState {
265273
);
266274
};
267275

276+
#onpointerenter: PointerEventHandler<HTMLElement> = (e) => {
277+
if (this.#isDisabled) return;
278+
if (e.pointerType === "touch") return;
279+
280+
// if in transit, wait briefly to see if user is actually heading to old content or staying here
281+
if (this.root.provider.isPointerInTransit.current) {
282+
this.#clearTransitCheck();
283+
this.#transitCheckTimeout = window.setTimeout(() => {
284+
// if still in transit after delay, user is likely staying on this trigger
285+
if (this.root.provider.isPointerInTransit.current) {
286+
this.root.provider.isPointerInTransit.current = false;
287+
this.root.onTriggerEnter();
288+
this.#hasPointerMoveOpened = true;
289+
}
290+
}, 250);
291+
return;
292+
}
293+
294+
this.root.onTriggerEnter();
295+
this.#hasPointerMoveOpened = true;
296+
};
297+
268298
#onpointermove: PointerEventHandler<HTMLElement> = (e) => {
269299
if (this.#isDisabled) return;
270300
if (e.pointerType === "touch") return;
271301
if (this.#hasPointerMoveOpened) return;
272302

273-
if (this.root.provider.isPointerInTransit.current) return;
303+
// moving within trigger means we're definitely not in transit anymore
304+
this.#clearTransitCheck();
305+
this.root.provider.isPointerInTransit.current = false;
274306

275307
this.root.onTriggerEnter();
276308
this.#hasPointerMoveOpened = true;
277309
};
278310

279311
#onpointerleave: PointerEventHandler<HTMLElement> = () => {
280312
if (this.#isDisabled) return;
313+
this.#clearTransitCheck();
281314
this.root.onTriggerLeave();
282315
this.#hasPointerMoveOpened = false;
283316
};
@@ -314,6 +347,7 @@ export class TooltipTriggerState {
314347
disabled: this.opts.disabled.current,
315348
onpointerup: this.#onpointerup,
316349
onpointerdown: this.#onpointerdown,
350+
onpointerenter: this.#onpointerenter,
317351
onpointermove: this.#onpointermove,
318352
onpointerleave: this.#onpointerleave,
319353
onfocus: this.#onfocus,

0 commit comments

Comments
 (0)