Skip to content

Commit e5e46c5

Browse files
authored
test: integrations (#1791)
1 parent c42cabe commit e5e46c5

File tree

9 files changed

+184
-47
lines changed

9 files changed

+184
-47
lines changed

.changeset/thin-places-check.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): dont eagerly start timer
Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,38 @@
11
<script lang="ts">
2-
import DateField from "./date-field.svelte";
2+
import { Tooltip, Dialog } from "bits-ui";
3+
4+
let open = $state(false);
35
</script>
46

5-
<DateField />
7+
<Tooltip.Provider>
8+
<Tooltip.Root delayDuration={200} disableCloseOnTriggerClick={true}>
9+
<Tooltip.Trigger
10+
class="inline-flex size-fit items-center justify-center"
11+
onclick={() => (open = true)}
12+
>
13+
Hover Me & Then Click
14+
</Tooltip.Trigger>
15+
<Tooltip.Content sideOffset={8} side="bottom">
16+
<div
17+
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"
18+
>
19+
Tooltip Content
20+
</div>
21+
</Tooltip.Content>
22+
</Tooltip.Root>
23+
24+
<Dialog.Root bind:open>
25+
<Dialog.Portal>
26+
<Dialog.Content
27+
class="rounded-input border-dark-10 bg-background shadow-popover outline-hidden z-0 border p-3 text-sm font-medium"
28+
>
29+
<p>Dialog Content</p>
30+
<p>
31+
Click "Close" to close dialog and hover tooltip again. The tooltip will not
32+
appear.
33+
</p>
34+
<Dialog.Close class="block">Close</Dialog.Close>
35+
</Dialog.Content>
36+
</Dialog.Portal>
37+
</Dialog.Root>
38+
</Tooltip.Provider>

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

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,9 @@ export class TooltipProviderState {
4646

4747
constructor(opts: TooltipProviderStateOpts) {
4848
this.opts = opts;
49-
this.#timerFn = new TimeoutFn(
50-
() => {
51-
this.isOpenDelayed = true;
52-
},
53-
this.opts.skipDelayDuration.current,
54-
{ immediate: false }
55-
);
49+
this.#timerFn = new TimeoutFn(() => {
50+
this.isOpenDelayed = true;
51+
}, this.opts.skipDelayDuration.current);
5652
}
5753

5854
#startTimer = () => {
@@ -143,14 +139,10 @@ export class TooltipRootState {
143139
constructor(opts: TooltipRootStateOpts, provider: TooltipProviderState) {
144140
this.opts = opts;
145141
this.provider = provider;
146-
this.#timerFn = new TimeoutFn(
147-
() => {
148-
this.#wasOpenDelayed = true;
149-
this.opts.open.current = true;
150-
},
151-
this.delayDuration ?? 0,
152-
{ immediate: false }
153-
);
142+
this.#timerFn = new TimeoutFn(() => {
143+
this.#wasOpenDelayed = true;
144+
this.opts.open.current = true;
145+
}, this.delayDuration ?? 0);
154146

155147
new OpenChangeComplete({
156148
open: this.opts.open,
@@ -164,14 +156,10 @@ export class TooltipRootState {
164156
() => this.delayDuration,
165157
() => {
166158
if (this.delayDuration === undefined) return;
167-
this.#timerFn = new TimeoutFn(
168-
() => {
169-
this.#wasOpenDelayed = true;
170-
this.opts.open.current = true;
171-
},
172-
this.delayDuration,
173-
{ immediate: false }
174-
);
159+
this.#timerFn = new TimeoutFn(() => {
160+
this.#wasOpenDelayed = true;
161+
this.opts.open.current = true;
162+
}, this.delayDuration);
175163
}
176164
);
177165

@@ -183,7 +171,8 @@ export class TooltipRootState {
183171
} else {
184172
this.provider.onClose(this);
185173
}
186-
}
174+
},
175+
{ lazy: true }
187176
);
188177
}
189178

packages/bits-ui/src/lib/internal/timeout-fn.ts

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,18 @@
11
import { onDestroyEffect } from "svelte-toolbelt";
22
import type { AnyFn } from "./types.js";
3-
import { BROWSER } from "esm-env";
4-
5-
type TimeoutFnOptions = {
6-
/**
7-
* Start the timer immediate after calling this function
8-
*
9-
* @default true
10-
*/
11-
immediate?: boolean;
12-
};
13-
14-
const defaultOpts: TimeoutFnOptions = {
15-
immediate: true,
16-
};
173

184
export class TimeoutFn<T extends AnyFn> {
19-
readonly #opts: TimeoutFnOptions;
205
readonly #interval: number;
216
readonly #cb: T;
227
#timer: number | null = null;
238

24-
constructor(cb: T, interval: number, opts: TimeoutFnOptions = {}) {
9+
constructor(cb: T, interval: number) {
2510
this.#cb = cb;
2611
this.#interval = interval;
27-
this.#opts = { ...defaultOpts, ...opts };
2812

2913
this.stop = this.stop.bind(this);
3014
this.start = this.start.bind(this);
3115

32-
if (this.#opts.immediate && BROWSER) {
33-
this.start();
34-
}
35-
3616
onDestroyEffect(this.stop);
3717
}
3818

@@ -44,10 +24,12 @@ export class TimeoutFn<T extends AnyFn> {
4424
}
4525

4626
stop() {
27+
console.log("stopping timeout");
4728
this.#clear();
4829
}
4930

5031
start(...args: Parameters<T> | []) {
32+
console.log("starting timeout");
5133
this.#clear();
5234
this.#timer = window.setTimeout(() => {
5335
this.#timer = null;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<script lang="ts">
2+
import { Dialog } from "bits-ui";
3+
import { DropdownMenu } from "bits-ui";
4+
import { Popover } from "bits-ui";
5+
</script>
6+
7+
<main>
8+
<Dialog.Root>
9+
<Dialog.Trigger data-testid="dialog-trigger">open</Dialog.Trigger>
10+
<Dialog.Portal>
11+
<Dialog.Content data-testid="dialog-content">
12+
<DropdownMenu.Root>
13+
<DropdownMenu.Trigger data-testid="dropdown-trigger">open</DropdownMenu.Trigger>
14+
<DropdownMenu.Portal>
15+
<DropdownMenu.Content data-testid="dropdown-content">
16+
<DropdownMenu.Item>item</DropdownMenu.Item>
17+
</DropdownMenu.Content>
18+
</DropdownMenu.Portal>
19+
</DropdownMenu.Root>
20+
<Popover.Root>
21+
<Popover.Trigger data-testid="popover-trigger">open</Popover.Trigger>
22+
<Popover.Portal>
23+
<Popover.Content data-testid="popover-content">content</Popover.Content>
24+
</Popover.Portal>
25+
</Popover.Root>
26+
content
27+
</Dialog.Content>
28+
</Dialog.Portal>
29+
</Dialog.Root>
30+
</main>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script lang="ts">
2+
import { Tooltip, Dialog } from "bits-ui";
3+
4+
let open = $state(false);
5+
</script>
6+
7+
<Tooltip.Provider>
8+
<Tooltip.Root delayDuration={200} disableCloseOnTriggerClick={true}>
9+
<Tooltip.Trigger data-testid="trigger" onclick={() => (open = true)}>
10+
Hover Me & Then Click
11+
</Tooltip.Trigger>
12+
<Tooltip.Content data-testid="tooltip-content">
13+
<div>Tooltip Content</div>
14+
</Tooltip.Content>
15+
</Tooltip.Root>
16+
17+
<Dialog.Root bind:open>
18+
<Dialog.Portal>
19+
<Dialog.Content data-testid="dialog-content">
20+
<p>Dialog Content</p>
21+
<p>
22+
Click "Close" to close dialog and hover tooltip again. The tooltip will not
23+
appear.
24+
</p>
25+
<Dialog.Close data-testid="dialog-close">Close</Dialog.Close>
26+
</Dialog.Content>
27+
</Dialog.Portal>
28+
</Dialog.Root>
29+
</Tooltip.Provider>

tests/src/tests/dialog/dialog.browser.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import DialogTest, { type DialogTestProps } from "./dialog-test.svelte";
77
import DialogNestedTest from "./dialog-nested-test.svelte";
88
import { expectExists, expectNotExists, setupBrowserUserEvents } from "../browser-utils";
99
import DialogForceMountTest from "./dialog-force-mount-test.svelte";
10+
import DialogIntegrationTest from "./dialog-integration-test.svelte";
11+
import DialogTooltipTest from "./dialog-tooltip-test.svelte";
1012

1113
const kbd = getTestKbd();
1214

@@ -384,3 +386,39 @@ describe("Nested Dialogs", () => {
384386
await expect.element(page.getByTestId("second-open")).toHaveFocus();
385387
});
386388
});
389+
390+
describe("Integration with other components", () => {
391+
it("should allow opening nested floating components within the dialog", async () => {
392+
render(DialogIntegrationTest);
393+
await page.getByTestId("dialog-trigger").click();
394+
await expectExists(page.getByTestId("dialog-content"));
395+
await page.getByTestId("dropdown-trigger").click();
396+
await expectExists(page.getByTestId("dropdown-content"));
397+
await userEvent.keyboard(kbd.ESCAPE);
398+
await expectNotExists(page.getByTestId("dropdown-content"));
399+
await expectExists(page.getByTestId("dialog-content"));
400+
await page.getByTestId("popover-trigger").click();
401+
await expectExists(page.getByTestId("popover-content"));
402+
await userEvent.keyboard(kbd.ESCAPE);
403+
await expectNotExists(page.getByTestId("popover-content"));
404+
await expectExists(page.getByTestId("dialog-content"));
405+
await userEvent.keyboard(kbd.ESCAPE);
406+
await expectNotExists(page.getByTestId("dialog-content"));
407+
});
408+
409+
it("should not break tooltip when opened from tooltip trigger and disableCloseOnTriggerClick is true", async () => {
410+
// https://github.com/huntabyte/bits-ui/issues/1666
411+
render(DialogTooltipTest);
412+
const trigger = page.getByTestId("trigger");
413+
await trigger.hover();
414+
await expectExists(page.getByTestId("tooltip-content"));
415+
await trigger.click();
416+
await expectExists(page.getByTestId("dialog-content"));
417+
await expectNotExists(page.getByTestId("tooltip-content"));
418+
await page.getByTestId("dialog-close").click();
419+
420+
await expectNotExists(page.getByTestId("dialog-content"));
421+
await trigger.hover();
422+
await expectExists(page.getByTestId("tooltip-content"));
423+
});
424+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script lang="ts">
2+
import { Popover } from "bits-ui";
3+
</script>
4+
5+
<Popover.Root>
6+
<Popover.Trigger data-testid="trigger-1">trigger-1</Popover.Trigger>
7+
<Popover.Trigger data-testid="trigger-2">trigger-2</Popover.Trigger>
8+
<Popover.Trigger data-testid="trigger-3">trigger-3</Popover.Trigger>
9+
<Popover.Portal>
10+
<Popover.Content data-testid="content">content</Popover.Content>
11+
</Popover.Portal>
12+
</Popover.Root>

tests/src/tests/popover/popover.browser.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import PopoverForceMountTest, {
99
import PopoverSiblingsTest from "./popover-siblings-test.svelte";
1010
import { expectExists, expectNotExists } from "../browser-utils";
1111
import { page, userEvent } from "@vitest/browser/context";
12+
import PopoverMultipleTriggersTest from "./popover-multiple-triggers-test.svelte";
1213

1314
const kbd = getTestKbd();
1415

@@ -198,3 +199,21 @@ it("should correctly handle focus when closing one popover by clicking another p
198199
await t.getByTestId("close-3").click();
199200
await expectNotExists(t.getByTestId("content-3"));
200201
});
202+
203+
it("should restore focus to the trigger that opened the popover", async () => {
204+
render(PopoverMultipleTriggersTest);
205+
await page.getByTestId("trigger-1").click();
206+
await expectExists(page.getByTestId("content"));
207+
await userEvent.keyboard(kbd.ESCAPE);
208+
await expect.element(page.getByTestId("trigger-1")).toHaveFocus();
209+
210+
await page.getByTestId("trigger-2").click();
211+
await expectExists(page.getByTestId("content"));
212+
await userEvent.keyboard(kbd.ESCAPE);
213+
await expect.element(page.getByTestId("trigger-2")).toHaveFocus();
214+
215+
await page.getByTestId("trigger-3").click();
216+
await expectExists(page.getByTestId("content"));
217+
await userEvent.keyboard(kbd.ESCAPE);
218+
await expect.element(page.getByTestId("trigger-3")).toHaveFocus();
219+
});

0 commit comments

Comments
 (0)