diff --git a/dev/react/src/tests/drag-variant-transform.tsx b/dev/react/src/tests/drag-variant-transform.tsx new file mode 100644 index 0000000000..47a4994625 --- /dev/null +++ b/dev/react/src/tests/drag-variant-transform.tsx @@ -0,0 +1,48 @@ +import { motion, useDragControls, Variants } from "framer-motion" + +const variants: Variants = { + none: (custom?: string) => ({ + width: "fit-content", + height: "fit-content", + transform: custom || undefined, + }), +} + +// Regression test for #2807: a variant that sets a literal `transform` string +// should not prevent drag from moving the element. +export const App = () => { + const dragControls = useDragControls() + + return ( +
+
+ ) +} diff --git a/packages/framer-motion/cypress/integration/drag-variant-transform.ts b/packages/framer-motion/cypress/integration/drag-variant-transform.ts new file mode 100644 index 0000000000..84db720b5f --- /dev/null +++ b/packages/framer-motion/cypress/integration/drag-variant-transform.ts @@ -0,0 +1,36 @@ +/** + * Regression test for #2807: a variant that sets a literal `transform` string + * should not prevent drag from moving the element. + */ +describe("Drag with variant-set transform", () => { + it("Drags the element even when a variant sets a literal transform string", () => { + cy.visit("?test=drag-variant-transform") + + // The variant applies `transform: "translate(50px, 60px)"` so the + // initial bounding rect reflects that translation (padding 100 + 50). + cy.get("[data-testid='draggable']") + .wait(200) + .then(($el: any) => { + expect( + $el[0].getBoundingClientRect().left, + "initial transform from variant is applied" + ).to.equal(150) + }) + + cy.get("[data-testid='handle']") + .trigger("pointerdown", 5, 5) + .trigger("pointermove", 10, 10) + .wait(50) + .trigger("pointermove", 200, 250, { force: true }) + .wait(50) + .trigger("pointerup", { force: true }) + + cy.get("[data-testid='draggable']").should(($el: any) => { + const { left, top } = $el[0].getBoundingClientRect() + // Drag moved the pointer by ~195/245 px. The element should have + // moved by the same amount from its initial position. + expect(left, "left after drag").to.be.greaterThan(250) + expect(top, "top after drag").to.be.greaterThan(250) + }) + }) +}) diff --git a/packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts b/packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts index 643e91767a..ef116f16ee 100644 --- a/packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts +++ b/packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts @@ -110,6 +110,16 @@ describe("buildHTMLStyles", () => { transform: "translateY(2) translateX(1px)", }) }) + + test("Individual transform props take precedence over transform string", () => { + const latest = { x: 50, transform: "translateX(100px)" } + const style = {} + build(latest, { style }) + + expect(style).toEqual({ + transform: "translateX(50px)", + }) + }) }) interface BuildProps { diff --git a/packages/motion-dom/src/render/html/utils/build-styles.ts b/packages/motion-dom/src/render/html/utils/build-styles.ts index 5914df7876..d6627b2233 100644 --- a/packages/motion-dom/src/render/html/utils/build-styles.ts +++ b/packages/motion-dom/src/render/html/utils/build-styles.ts @@ -49,14 +49,23 @@ export function buildHTMLStyles( } } - if (!latestValues.transform) { + /** + * If x or y motion values are present they take precedence over any + * `transform` string in latestValues. This allows drag (and other gestures + * that drive x/y) to keep working when a variant or style sets a literal + * `transform` string. See #2807. + */ + const hasTranslate = + latestValues.x !== undefined || latestValues.y !== undefined + + if (!latestValues.transform || hasTranslate) { if (hasTransform || transformTemplate) { style.transform = buildTransform( latestValues, state.transform, transformTemplate ) - } else if (style.transform) { + } else if (!latestValues.transform && style.transform) { /** * If we have previously created a transform but currently don't have any, * reset transform style to none.