Skip to content

Commit 842ef47

Browse files
authored
fix: inconsistent grace areas for tooltip/link preview (#1915)
1 parent 42ffe80 commit 842ef47

File tree

8 files changed

+55
-78
lines changed

8 files changed

+55
-78
lines changed

.changeset/legal-webs-hang.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): inconsistent grace area

.changeset/yellow-baths-type.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(LinkPreview): inconsistent grace area

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,6 @@
2323
{...contentProps}
2424
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)"
2525
>
26-
<div>
27-
<Tooltip.Arrow
28-
class="border-dark-10 text-dark-10 rounded-[2px] border-l border-t"
29-
/>
30-
</div>
3126
<div
3227
class="rounded-input border-dark-10 bg-background shadow-popover outline-hidden flex items-center justify-center border p-3 text-sm font-medium"
3328
>

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

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -22,58 +22,4 @@
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>
7925
</Tooltip.Provider>

packages/bits-ui/src/lib/bits/link-preview/link-preview.svelte.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type {
1919
WithRefOpts,
2020
} from "$lib/internal/types.js";
2121
import { getTabbableCandidates } from "$lib/internal/focus.js";
22-
import { GraceArea } from "$lib/internal/grace-area.svelte.js";
22+
import { SafePolygon } from "$lib/internal/safe-polygon.svelte.js";
2323
import { PresenceManager } from "$lib/internal/presence-manager.svelte.js";
2424

2525
const linkPreviewAttrs = createBitsAttrs({
@@ -240,7 +240,7 @@ export class LinkPreviewContentState {
240240
this.onpointerenter = this.onpointerenter.bind(this);
241241
this.onfocusout = this.onfocusout.bind(this);
242242

243-
new GraceArea({
243+
new SafePolygon({
244244
triggerNode: () => this.root.triggerNode,
245245
contentNode: () => this.opts.ref.current,
246246
enabled: () => this.root.opts.open.current,

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { createBitsAttrs, boolToEmptyStrOrUndef } from "$lib/internal/attrs.js";
1414
import type { OnChangeFn, RefAttachment, WithRefOpts } from "$lib/internal/types.js";
1515
import type { FocusEventHandler, MouseEventHandler, PointerEventHandler } from "svelte/elements";
1616
import { TimeoutFn } from "$lib/internal/timeout-fn.js";
17-
import { GraceArea } from "$lib/internal/grace-area.svelte.js";
17+
import { SafePolygon } from "$lib/internal/safe-polygon.svelte.js";
1818
import { PresenceManager } from "$lib/internal/presence-manager.svelte.js";
1919

2020
export const tooltipAttrs = createBitsAttrs({
@@ -378,7 +378,7 @@ export class TooltipContentState {
378378
this.root = root;
379379
this.attachment = attachRef(this.opts.ref, (v) => (this.root.contentNode = v));
380380

381-
new GraceArea({
381+
new SafePolygon({
382382
triggerNode: () => this.root.triggerNode,
383383
contentNode: () => this.root.contentNode,
384384
enabled: () => this.root.opts.open.current && !this.root.disableHoverableContent,
@@ -387,10 +387,6 @@ export class TooltipContentState {
387387
this.root.handleClose();
388388
}
389389
},
390-
setIsPointerInTransit: (value) => {
391-
this.root.provider.isPointerInTransit.current = value;
392-
},
393-
transitTimeout: this.root.provider.opts.skipDelayDuration.current,
394390
});
395391

396392
onMountEffect(() =>

tests/src/tests/link-preview/link-preview.browser.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,31 @@ it("should close on escape keydown", async () => {
5656
expect(mockEsc).toHaveBeenCalledTimes(1);
5757
});
5858

59-
it.skip("closes when pointer moves outside the trigger and content", async () => {
59+
it("closes when pointer moves outside the trigger and content", async () => {
6060
await open();
6161
const outside = page.getByTestId("outside");
6262
await outside.hover();
6363
await expectNotExists(page.getByTestId("content"));
6464
});
6565

66+
it("should stay open when hovering content", async () => {
67+
await open();
68+
const content = page.getByTestId("content");
69+
await content.hover();
70+
await expectExists(page.getByTestId("content"));
71+
});
72+
73+
it("should open on focus and close on blur", async () => {
74+
const t = setup();
75+
await expectNotExists(page.getByTestId("content"));
76+
77+
(t.trigger.element() as HTMLElement).focus();
78+
await expectExists(page.getByTestId("content"));
79+
80+
(t.trigger.element() as HTMLElement).blur();
81+
await expectNotExists(page.getByTestId("content"));
82+
});
83+
6684
it("should portal to the body by default", async () => {
6785
await open();
6886
expect(page.getByTestId("content").element().parentElement?.parentElement).toBe(document.body);

tests/src/tests/tooltip/tooltip.browser.test.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, it, vi } from "vitest";
1+
import { expect, it } from "vitest";
22
import { render } from "vitest-browser-svelte";
33
import type { Component } from "svelte";
44
import { getTestKbd } from "../utils.js";
@@ -56,7 +56,7 @@ it("should close on escape keydown", async () => {
5656
await expectNotExists(page.getByTestId("content"));
5757
});
5858

59-
it.skip("should close when pointer moves outside the trigger and content", async () => {
59+
it("should close when pointer moves outside the trigger and content", async () => {
6060
await open();
6161

6262
const outside = page.getByTestId("outside");
@@ -66,6 +66,23 @@ it.skip("should close when pointer moves outside the trigger and content", async
6666
await expectNotExists(page.getByTestId("content"));
6767
});
6868

69+
it("should stay open when hovering content", async () => {
70+
const t = await open();
71+
await t.content.hover();
72+
await expectExists(page.getByTestId("content"));
73+
});
74+
75+
it("should open on focus and close on blur", async () => {
76+
const t = setup();
77+
await expectNotExists(page.getByTestId("content"));
78+
79+
(t.trigger.element() as HTMLElement).focus();
80+
await expectExists(page.getByTestId("content"));
81+
82+
(t.trigger.element() as HTMLElement).blur();
83+
await expectNotExists(page.getByTestId("content"));
84+
});
85+
6986
it("should portal to the body by default", async () => {
7087
const t = await open();
7188
const contentWrapper = t.content.element().parentElement;
@@ -98,17 +115,12 @@ it("should allow ignoring escapeKeydownBehavior ", async () => {
98115
});
99116

100117
it("should respect binding the open prop", async () => {
101-
await open({
102-
contentProps: {
103-
interactOutsideBehavior: "ignore",
104-
},
105-
});
118+
await open();
106119
const binding = page.getByTestId("binding");
107-
await vi.waitFor(() => expect(binding).toHaveTextContent("true"));
108120
await expect.element(binding).toHaveTextContent("true");
109-
await binding.click();
110-
await expect.element(binding).toHaveTextContent("false");
121+
await userEvent.keyboard(kbd.ESCAPE);
111122
await expectNotExists(page.getByTestId("content"));
123+
await expect.element(binding).toHaveTextContent("false");
112124
await binding.click();
113125
await expect.element(binding).toHaveTextContent("true");
114126
await expectExists(page.getByTestId("content"));

0 commit comments

Comments
 (0)