Skip to content

Commit bacb86d

Browse files
committed
useMergeRefs
1 parent 3b647ee commit bacb86d

File tree

20 files changed

+233
-114
lines changed

20 files changed

+233
-114
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { ReferenceType } from "../types.js";
2+
import { FloatingState } from "./use-floating.svelte.js";
3+
4+
interface BoxedRef {
5+
current: ReferenceType | null;
6+
}
7+
8+
/**
9+
* Merges the references of either floating instances or refs into a single reference
10+
* that can be accessed and set via the `.current` property.
11+
*/
12+
class MergeRefs {
13+
#current = $state<ReferenceType | null>(null);
14+
constructor(
15+
private readonly floatingOrRef: Array<FloatingState | BoxedRef>,
16+
) {}
17+
18+
get current() {
19+
return this.#current;
20+
}
21+
22+
set current(node: ReferenceType | null) {
23+
for (const arg of this.floatingOrRef) {
24+
if (arg instanceof FloatingState) {
25+
arg.reference = node;
26+
continue;
27+
}
28+
arg.current = node;
29+
}
30+
this.#current = node;
31+
}
32+
}
33+
34+
/**
35+
* Use the same reference for multiple floating instances at once.
36+
*
37+
* @example
38+
* ```svelte
39+
* <script lang="ts">
40+
* import { useMergeRefs, useFloating, type BoxedRef } from "@skeletonlabs/floating-ui-svelte";
41+
* const tooltip = useFloating();
42+
* const menu = useFloating()
43+
*
44+
* let someOtherRef: BoxedRef = $state({ current: null })
45+
*
46+
* const tooltipInt = useInteractions([])
47+
* const menuInt = useInteractions([])
48+
*
49+
* const ref = useMergeRefs([tooltip, menu, someOtherRef])
50+
* const props = $derived(tooltipInt.getReferenceProps(menuInt.getReferenceProps()))
51+
* </script>
52+
*
53+
* <button bind:this={ref.current} {...props}>
54+
* <!-- ... -->
55+
* </button>
56+
*```
57+
*
58+
*
59+
* @param floatingInstances
60+
* @returns
61+
*/
62+
function useMergeRefs(refLikes: Array<FloatingState | BoxedRef>) {
63+
return new MergeRefs(refLikes);
64+
}
65+
66+
export { MergeRefs, useMergeRefs };
67+
export type { BoxedRef };
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import { useFloating } from "../../src/index.js";
3+
import { withRunes } from "../internal/with-runes.svelte.js";
4+
import {
5+
useMergeRefs,
6+
type BoxedRef,
7+
} from "../../src/hooks/use-merge-refs.svelte.js";
8+
9+
describe("useMergeRefs", () => {
10+
vi.mock(import("svelte"), async (importOriginal) => {
11+
const actual = await importOriginal();
12+
return {
13+
...actual,
14+
getContext: vi.fn().mockReturnValue(null),
15+
};
16+
});
17+
18+
it(
19+
"merges the references of multiple floating instances or other boxed elements",
20+
withRunes(() => {
21+
const ref1 = useFloating();
22+
const ref2 = useFloating();
23+
const ref3: BoxedRef = $state({ current: null });
24+
25+
const mergedRef = useMergeRefs([ref1, ref2, ref3]);
26+
expect(mergedRef.current).toBe(null);
27+
expect(ref1.reference).toBe(null);
28+
expect(ref2.reference).toBe(null);
29+
expect(ref3.current).toBe(null);
30+
31+
const node = document.createElement("div");
32+
mergedRef.current = node;
33+
expect(mergedRef.current).toBe(node);
34+
expect(ref1.reference).toBe(node);
35+
expect(ref2.reference).toBe(node);
36+
expect(ref3.current).toBe(node);
37+
mergedRef.current = null;
38+
expect(mergedRef.current).toBe(null);
39+
expect(ref1.reference).toBe(null);
40+
expect(ref2.reference).toBe(null);
41+
expect(ref3.current).toBe(null);
42+
}),
43+
);
44+
});

sites/floating-ui-svelte.vercel.app/src/routes/(inner)/api/floating-arrow/+page.svelte

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
<script lang="ts">
2-
import CodeBlock from "$lib/components/CodeBlock/CodeBlock.svelte";
3-
import Table from "$lib/components/Table/Table.svelte";
4-
import { tableProps } from "./data.js";
2+
import CodeBlock from "$lib/components/CodeBlock/CodeBlock.svelte";
3+
import Table from "$lib/components/Table/Table.svelte";
4+
import { tableProps } from "./data.js";
55
</script>
66

77
<div class="space-y-10">
88
<!-- Header -->
99
<header class="card card-gradient space-y-8">
1010
<h1 class="h1"><span>FloatingArrow</span></h1>
1111
<p>
12-
Renders a customizable <code class="code">{'<svg>'}</code> pointing arrow triangle inside the floating
13-
element that gets automatically positioned.
12+
Renders a customizable <code class="code">{"<svg>"}</code> pointing arrow
13+
triangle inside the floating element that gets automatically positioned.
1414
</p>
1515
<CodeBlock
1616
lang="ts"
17-
code={`import { FloatingArrow } from '@skeletonlabs/floating-ui-svelte';`}
18-
/>
17+
code={`import { FloatingArrow } from '@skeletonlabs/floating-ui-svelte';`} />
1918
</header>
2019
<!-- Usage -->
2120
<section class="space-y-8">
@@ -35,8 +34,7 @@ const floating = useFloating({
3534
];
3635
}
3736
});
38-
`}
39-
/>
37+
`} />
4038
<CodeBlock
4139
lang="svelte"
4240
code={`
@@ -57,8 +55,7 @@ const floating = useFloating({
5755
context="{floating.context}"
5856
/>
5957
</div>
60-
`}
61-
/>
58+
`} />
6259
</section>
6360
<!-- Table: Props -->
6461
<section class="space-y-8">
@@ -69,7 +66,9 @@ const floating = useFloating({
6966
<section class="space-y-8">
7067
<h2 class="h2">Utility Classes and Styles</h2>
7168
<p>Provide arbitrary utility classes using the standard attribute.</p>
72-
<CodeBlock lang="svelte" code={`<FloatingArrow class="fill-white" />`} />
69+
<CodeBlock
70+
lang="svelte"
71+
code={`<FloatingArrow class="fill-white" />`} />
7372
</section>
7473
<!-- Compare -->
7574
<section class="space-y-8">
Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
11
<script lang="ts">
2-
import {
3-
useClick,
4-
useFloating,
5-
useInteractions,
6-
} from "@skeletonlabs/floating-ui-svelte";
2+
import {
3+
useClick,
4+
useFloating,
5+
useInteractions,
6+
} from "@skeletonlabs/floating-ui-svelte";
77
8-
const floating = useFloating();
9-
const click = useClick(floating.context);
10-
const interactions = useInteractions([click]);
8+
const floating = useFloating();
9+
const click = useClick(floating.context);
10+
const interactions = useInteractions([click]);
1111
</script>
1212

1313
<!-- Reference Element -->
14-
<button bind:this={floating.elements.reference} {...interactions.getReferenceProps()}>
14+
<button bind:this={floating.reference} {...interactions.getReferenceProps()}>
1515
Reference
1616
</button>
1717

1818
<!-- Floating Element -->
1919
<div
20-
bind:this={floating.elements.floating}
20+
bind:this={floating.floating}
2121
style={floating.floatingStyles}
22-
{...interactions.getFloatingProps()}
23-
>
22+
{...interactions.getFloatingProps()}>
2423
Floating
2524
</div>
Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
11
<script lang="ts">
2-
import {
3-
useDismiss,
4-
useFloating,
5-
useInteractions,
6-
} from "@skeletonlabs/floating-ui-svelte";
2+
import {
3+
useDismiss,
4+
useFloating,
5+
useInteractions,
6+
} from "@skeletonlabs/floating-ui-svelte";
77
8-
const floating = useFloating();
9-
const dismiss = useDismiss(floating.context);
10-
const interactions = useInteractions([dismiss]);
8+
const floating = useFloating();
9+
const dismiss = useDismiss(floating.context);
10+
const interactions = useInteractions([dismiss]);
1111
</script>
1212

1313
<!-- Reference Element -->
14-
<button bind:this={floating.elements.reference} {...interactions.getReferenceProps()}>
14+
<button bind:this={floating.reference} {...interactions.getReferenceProps()}>
1515
Reference
1616
</button>
1717

1818
<!-- Floating Element -->
1919
<div
20-
bind:this={floating.elements.floating}
20+
bind:this={floating.floating}
2121
style={floating.floatingStyles}
22-
{...interactions.getFloatingProps()}
23-
>
22+
{...interactions.getFloatingProps()}>
2423
Floating
2524
</div>

sites/floating-ui-svelte.vercel.app/src/routes/(inner)/api/use-floating/+page.svelte

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,34 @@
11
<script lang="ts">
2-
import CodeBlock from "$lib/components/CodeBlock/CodeBlock.svelte";
3-
import Table from "$lib/components/Table/Table.svelte";
4-
import { tableOptions, tableReturns } from "./data.js";
2+
import CodeBlock from "$lib/components/CodeBlock/CodeBlock.svelte";
3+
import Table from "$lib/components/Table/Table.svelte";
4+
import { tableOptions, tableReturns } from "./data.js";
55
</script>
66

77
<div class="space-y-10">
88
<!-- Header -->
99
<header class="card card-gradient space-y-8">
1010
<h1 class="h1"><span>useFloating</span></h1>
1111
<p>
12-
The main Hook of the library that acts as a controller for all other Hooks and components.
12+
The main Hook of the library that acts as a controller for all other
13+
Hooks and components.
1314
</p>
14-
<CodeBlock lang="ts" code={`import { useFloating } from '@skeletonlabs/floating-ui-svelte';`} />
15+
<CodeBlock
16+
lang="ts"
17+
code={`import { useFloating } from '@skeletonlabs/floating-ui-svelte';`} />
1518
</header>
1619
<!-- Usage -->
1720
<section class="space-y-8">
1821
<h2 class="h2">Usage</h2>
1922
<p>
20-
The <code class="code">useFloating</code> Svelte hook acts as a controller for all other Floating
21-
UI Svelte features. It handles positioning your floating elements (tooltips, popovers, etc.) relative
22-
to an anchored element. Automatically calculates the best placement and updates it as needed, providing
23-
access to properties for position and style.
23+
The <code class="code">useFloating</code> Svelte hook acts as a controller
24+
for all other Floating UI Svelte features. It handles positioning your
25+
floating elements (tooltips, popovers, etc.) relative to an anchored
26+
element. Automatically calculates the best placement and updates it as
27+
needed, providing access to properties for position and style.
2428
</p>
25-
<CodeBlock lang="ts" code={`const floating = useFloating({\n\t/* options */\n});`} />
29+
<CodeBlock
30+
lang="ts"
31+
code={`const floating = useFloating({\n\t/* options */\n});`} />
2632
<CodeBlock
2733
lang="html"
2834
code={`
@@ -38,13 +44,15 @@ import { tableOptions, tableReturns } from "./data.js";
3844
>
3945
Floating
4046
</div>
41-
`}
42-
/>
47+
`} />
4348
</section>
4449
<!-- Alert: Destructuring -->
4550
<div class="alert">
4651
<h3 class="h3">Note</h3>
47-
<p>Destructured variables are not supported as this would break reactivity.</p>
52+
<p>
53+
Destructured variables are not supported as this would break
54+
reactivity.
55+
</p>
4856
</div>
4957
<!-- Table: Options -->
5058
<section class="space-y-8">

sites/floating-ui-svelte.vercel.app/src/routes/(inner)/api/use-focus/+page.svelte

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,34 @@
11
<script lang="ts">
2-
import CodeBlock from "$lib/components/CodeBlock/CodeBlock.svelte";
3-
import Table from "$lib/components/Table/Table.svelte";
4-
import ExampleRaw from "./Example.svelte?raw";
5-
import { tableOptions } from "./data.js";
2+
import CodeBlock from "$lib/components/CodeBlock/CodeBlock.svelte";
3+
import Table from "$lib/components/Table/Table.svelte";
4+
import ExampleRaw from "./Example.svelte?raw";
5+
import { tableOptions } from "./data.js";
66
</script>
77

88
<div class="space-y-10">
99
<!-- Header -->
1010
<header class="card card-gradient space-y-8">
1111
<h1 class="h1"><span>useFocus</span></h1>
1212
<p>
13-
Opens the floating element while the reference element has focus, like CSS
13+
Opens the floating element while the reference element has focus,
14+
like CSS
1415
<code class="code">:focus</code>.
1516
</p>
16-
<CodeBlock lang="ts" code={`import { useFocus } from '@skeletonlabs/floating-ui-svelte';`} />
17+
<CodeBlock
18+
lang="ts"
19+
code={`import { useFocus } from '@skeletonlabs/floating-ui-svelte';`} />
1720
<!-- TODO: add when FloatingFocusManager is ready -->
1821
<!-- <p>To manage focus within the floating element itself, use <a class="anchor" href="/api/floating-focus-manager">FloatingFocusManager</a>.</p> -->
1922
</header>
2023
<!-- Usage -->
2124
<section class="space-y-8">
2225
<h2 class="h2">Usage</h2>
2326
<p>
24-
This Hook returns event handler props. To use it, pass it the context object returned from <code
25-
class="code">useFloating()</code
26-
>, and then feed its result into the <code class="code">useInteractions()</code> array. The returned
27-
prop getters are then spread onto the elements for rendering.
27+
This Hook returns event handler props. To use it, pass it the
28+
context object returned from <code class="code">useFloating()</code
29+
>, and then feed its result into the
30+
<code class="code">useInteractions()</code> array. The returned prop
31+
getters are then spread onto the elements for rendering.
2832
</p>
2933
<CodeBlock code={ExampleRaw} lang="svelte" mark={[2, 5, 6]} />
3034
</section>
Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
11
<script lang="ts">
2-
import {
3-
useFloating,
4-
useFocus,
5-
useInteractions,
6-
} from "@skeletonlabs/floating-ui-svelte";
2+
import {
3+
useFloating,
4+
useFocus,
5+
useInteractions,
6+
} from "@skeletonlabs/floating-ui-svelte";
77
8-
const floating = useFloating();
9-
const focus = useFocus(floating.context);
10-
const interactions = useInteractions([focus]);
8+
const floating = useFloating();
9+
const focus = useFocus(floating.context);
10+
const interactions = useInteractions([focus]);
1111
</script>
1212

1313
<!-- Reference Element -->
14-
<button bind:this={floating.elements.reference} {...interactions.getReferenceProps()}>
14+
<button bind:this={floating.reference} {...interactions.getReferenceProps()}>
1515
Reference
1616
</button>
1717

1818
<!-- Floating Element -->
1919
<div
20-
bind:this={floating.elements.floating}
20+
bind:this={floating.floating}
2121
style={floating.floatingStyles}
22-
{...interactions.getFloatingProps()}
23-
>
22+
{...interactions.getFloatingProps()}>
2423
Floating
2524
</div>

0 commit comments

Comments
 (0)