From e7d160b7097116d6dfbc661daac4b17dcf411f46 Mon Sep 17 00:00:00 2001 From: Dinh-Van Colomban Date: Wed, 15 Jan 2025 08:52:37 +0100 Subject: [PATCH 1/2] feat(use-floating): adds a translate option to the useFloating hook To not interfere with other transform animation, this allows to set the position state through the 'translate' property instead of 'transform' --- .../src/hooks/use-floating.svelte.ts | 16 +++++ .../test/hooks/use-floating.svelte.ts | 59 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/packages/floating-ui-svelte/src/hooks/use-floating.svelte.ts b/packages/floating-ui-svelte/src/hooks/use-floating.svelte.ts index dfe3f1e0..ccd6efa6 100644 --- a/packages/floating-ui-svelte/src/hooks/use-floating.svelte.ts +++ b/packages/floating-ui-svelte/src/hooks/use-floating.svelte.ts @@ -68,6 +68,13 @@ interface UseFloatingOptions { */ transform?: boolean; + /** + * Whether to use 'translate' instead of 'transform' to position the floating element. + * + * @default false + */ + translate?: boolean; + /** * Object containing the floating and reference elements. * @default {} @@ -218,6 +225,7 @@ function useFloating(options: UseFloatingOptions = {}): UseFloatingReturn { strategy = "absolute", middleware = [], transform = true, + translate = false, open = true, onOpenChange: unstableOnOpenChange = noop, whileElementsMounted, @@ -237,6 +245,14 @@ function useFloating(options: UseFloatingOptions = {}): UseFloatingReturn { const x = roundByDPR(elements.floating, state.x); const y = roundByDPR(elements.floating, state.y); + if (translate) { + return styleObjectToString({ + ...initialStyles, + translate: `${x}px ${y}px`, + ...(getDPR(elements.floating) >= 1.5 && { willChange: "transform" }), + }); + } + if (transform) { return styleObjectToString({ ...initialStyles, diff --git a/packages/floating-ui-svelte/test/hooks/use-floating.svelte.ts b/packages/floating-ui-svelte/test/hooks/use-floating.svelte.ts index 70175865..10d48fff 100644 --- a/packages/floating-ui-svelte/test/hooks/use-floating.svelte.ts +++ b/packages/floating-ui-svelte/test/hooks/use-floating.svelte.ts @@ -162,6 +162,65 @@ describe("useFloating", () => { ); }); + describe("translate", () => { + it( + "can be set", + withRunes(async () => { + const translate = true; + const floating = useFloating({ + elements: createElements(), + translate, + }); + + await vi.waitFor(() => { + expect(floating.floatingStyles).contain( + "translate: 0px 0px", + ); + }); + }), + ); + it( + 'defaults to "false"', + withRunes(async () => { + const floating = useFloating({ + elements: createElements(), + }); + await vi.waitFor(() => { + expect(floating.floatingStyles).contain( + "transform: translate(0px, 0px)", + ); + }); + }), + ); + it( + "is reactive", + withRunes(async () => { + let translate = $state(true); + + const floating = useFloating({ + elements: createElements(), + get translate() { + return translate; + }, + }); + + await vi.waitFor(() => { + expect(floating.floatingStyles).contain( + "translate: 0px 0px", + ); + }); + + translate = false; + + await vi.waitFor(() => { + expect(floating.floatingStyles).not.contain( + "translate: 0px 0px", + ); + }); + }), + ); + }); + describe("strategy", () => { it( "can be set", From 3c1c0256b50c9eaecde495df5f0454359d1daf0f Mon Sep 17 00:00:00 2001 From: Dinh-Van Colomban Date: Wed, 15 Jan 2025 09:10:03 +0100 Subject: [PATCH 2/2] feat(use-floating): adds transform-origin to the floatingStyles To facilitate transition and animation, includes the transition-origin in the floating styles when using translate or transform --- .../src/hooks/use-floating.svelte.ts | 33 +++++++++++---- .../test/hooks/use-floating.svelte.ts | 42 +++++++++++++++---- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/packages/floating-ui-svelte/src/hooks/use-floating.svelte.ts b/packages/floating-ui-svelte/src/hooks/use-floating.svelte.ts index ccd6efa6..db57199d 100644 --- a/packages/floating-ui-svelte/src/hooks/use-floating.svelte.ts +++ b/packages/floating-ui-svelte/src/hooks/use-floating.svelte.ts @@ -231,6 +231,28 @@ function useFloating(options: UseFloatingOptions = {}): UseFloatingReturn { whileElementsMounted, nodeId, } = $derived(options); + + const state: UseFloatingData = $state({ + x: 0, + y: 0, + strategy, + placement, + middlewareData: {}, + isPositioned: false, + }); + + const origin = $derived.by(() => { + if (state.placement.startsWith("top")) + return state.placement.replace("top", "bottom"); + if (state.placement.startsWith("bottom")) + return state.placement.replace("bottom", "top"); + if (state.placement.startsWith("left")) + return state.placement.replace("left", "right"); + if (state.placement.startsWith("right")) + return state.placement.replace("right", "left"); + return state.placement; + }); + const floatingStyles = $derived.by(() => { const initialStyles = { position: strategy, @@ -249,6 +271,7 @@ function useFloating(options: UseFloatingOptions = {}): UseFloatingReturn { return styleObjectToString({ ...initialStyles, translate: `${x}px ${y}px`, + "transform-origin": origin, ...(getDPR(elements.floating) >= 1.5 && { willChange: "transform" }), }); } @@ -257,6 +280,7 @@ function useFloating(options: UseFloatingOptions = {}): UseFloatingReturn { return styleObjectToString({ ...initialStyles, transform: `translate(${x}px, ${y}px)`, + "transform-origin": origin, ...(getDPR(elements.floating) >= 1.5 && { willChange: "transform" }), }); } @@ -281,15 +305,6 @@ function useFloating(options: UseFloatingOptions = {}): UseFloatingReturn { unstableOnOpenChange(open, event, reason); }; - const state: UseFloatingData = $state({ - x: 0, - y: 0, - strategy, - placement, - middlewareData: {}, - isPositioned: false, - }); - const context: FloatingContext = $state({ data, events, diff --git a/packages/floating-ui-svelte/test/hooks/use-floating.svelte.ts b/packages/floating-ui-svelte/test/hooks/use-floating.svelte.ts index 10d48fff..811313e8 100644 --- a/packages/floating-ui-svelte/test/hooks/use-floating.svelte.ts +++ b/packages/floating-ui-svelte/test/hooks/use-floating.svelte.ts @@ -160,6 +160,35 @@ describe("useFloating", () => { }); }), ); + it.each<{ placement: Placement; expected: Placement }>([ + { placement: "top", expected: "bottom" }, + { placement: "right", expected: "left" }, + { placement: "bottom", expected: "top" }, + { placement: "left", expected: "right" }, + + { placement: "top-start", expected: "bottom-start" }, + { placement: "right-start", expected: "left-start" }, + { placement: "bottom-end", expected: "top-end" }, + { placement: "left-end", expected: "right-end" }, + ])("can be set to $placement", async ({ placement, expected }) => { + await withRunes(async () => { + const floating = useFloating({ + placement, + elements: createElements(), + }); + + await vi.waitFor(() => { + expect(floating.floatingStyles).contain( + "transform: translate(0px, 0px)", + ); + }); + + expect(floating.placement).toBe(placement); + expect(floating.floatingStyles).toContain( + `transform-origin: ${expected};`, + ); + })(); + }); }); describe("translate", () => { @@ -173,9 +202,7 @@ describe("useFloating", () => { }); await vi.waitFor(() => { - expect(floating.floatingStyles).contain( - "translate: 0px 0px", - ); + expect(floating.floatingStyles).contain("translate: 0px 0px"); }); }), ); @@ -205,17 +232,13 @@ describe("useFloating", () => { }); await vi.waitFor(() => { - expect(floating.floatingStyles).contain( - "translate: 0px 0px", - ); + expect(floating.floatingStyles).contain("translate: 0px 0px"); }); translate = false; await vi.waitFor(() => { - expect(floating.floatingStyles).not.contain( - "translate: 0px 0px", - ); + expect(floating.floatingStyles).not.contain("translate: 0px 0px"); }); }), ); @@ -302,6 +325,7 @@ describe("useFloating", () => { withRunes(() => { const floating = useFloating(); expect(floating.placement).toBe("bottom"); + expect(floating.floatingStyles).toContain("transform-origin: top;"); }), ); it(