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..db57199d 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,11 +225,34 @@ function useFloating(options: UseFloatingOptions = {}): UseFloatingReturn { strategy = "absolute", middleware = [], transform = true, + translate = false, open = true, onOpenChange: unstableOnOpenChange = noop, 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, @@ -237,10 +267,20 @@ 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`, + "transform-origin": origin, + ...(getDPR(elements.floating) >= 1.5 && { willChange: "transform" }), + }); + } + if (transform) { return styleObjectToString({ ...initialStyles, transform: `translate(${x}px, ${y}px)`, + "transform-origin": origin, ...(getDPR(elements.floating) >= 1.5 && { willChange: "transform" }), }); } @@ -265,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 70175865..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,88 @@ 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", () => { + 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", () => { @@ -243,6 +325,7 @@ describe("useFloating", () => { withRunes(() => { const floating = useFloating(); expect(floating.placement).toBe("bottom"); + expect(floating.floatingStyles).toContain("transform-origin: top;"); }), ); it(