From 3acbf0bdc3b6608b2f516bc31402dc14a79a92a8 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 19 Jun 2024 12:31:57 +0200 Subject: [PATCH 1/3] Fixing drag constraints with layout animations --- .../src/examples/Drag-constraints-ref.tsx | 50 ++++++++--- dev/react/src/tests/drag-ref-constraints.tsx | 82 +++++++++++++------ .../framer-motion/cypress/integration/drag.ts | 4 +- .../projection/node/create-projection-node.ts | 6 +- 4 files changed, 102 insertions(+), 40 deletions(-) diff --git a/dev/react/src/examples/Drag-constraints-ref.tsx b/dev/react/src/examples/Drag-constraints-ref.tsx index 28e5454680..b2dfbf0bc5 100644 --- a/dev/react/src/examples/Drag-constraints-ref.tsx +++ b/dev/react/src/examples/Drag-constraints-ref.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from "react" +import { useEffect, useRef, useState } from "react" import { motion } from "framer-motion" const container = { @@ -18,20 +18,46 @@ const child = { borderRadius: 20, } +const SiblingLayoutAnimation = () => { + const [state, setState] = useState(false) + + useEffect(() => { + const timer = setTimeout(() => setState(!state), 500) + + return () => clearTimeout(timer) + }, [state]) + + return ( + + ) +} + export const App = () => { const ref = useRef() const [count, setCount] = useState(0) return ( -
- setCount(count + 1)} - /> -
+ <> +
+ setCount(count + 1)} + id="draggable" + /> +
+ + ) } diff --git a/dev/react/src/tests/drag-ref-constraints.tsx b/dev/react/src/tests/drag-ref-constraints.tsx index 97f02f0c19..4953a9c5c3 100644 --- a/dev/react/src/tests/drag-ref-constraints.tsx +++ b/dev/react/src/tests/drag-ref-constraints.tsx @@ -1,5 +1,5 @@ import { motion, useMotionValue } from "framer-motion" -import { useRef, useState, useLayoutEffect } from "react"; +import { useRef, useState, useLayoutEffect, useEffect } from "react" // It's important for this test to only trigger a single rerender while dragging (in response to onDragStart) of draggable component. @@ -16,30 +16,62 @@ export const App = () => { const x = useMotionValue("100%") return ( -
- + <> +
setDragging(true)} - onDragEnd={() => setDragging(false)} - /> - -
+ data-testid="constraint" + style={{ width: 200, height: 200, background: "blue" }} + ref={containerRef} + > + setDragging(true)} + onDragEnd={() => setDragging(false)} + /> + +
+ + + ) +} + +/** + * This sibling layout animation is designed to fuzz/stress the drag constraints + * measurements. Remeasuring the constraints during drag would previously mess + * up the position of the draggable element. + */ +const SiblingLayoutAnimation = () => { + const [state, setState] = useState(false) + + useEffect(() => { + const timer = setTimeout(() => setState(!state), 200) + + return () => clearTimeout(timer) + }, [state]) + + return ( + ) } diff --git a/packages/framer-motion/cypress/integration/drag.ts b/packages/framer-motion/cypress/integration/drag.ts index af7d7e563b..125740e80f 100644 --- a/packages/framer-motion/cypress/integration/drag.ts +++ b/packages/framer-motion/cypress/integration/drag.ts @@ -234,9 +234,9 @@ describe("Drag", () => { .get("[data-testid='draggable']") .trigger("pointerdown", 10, 10) .trigger("pointermove", 15, 15) - .wait(50) + .wait(200) .trigger("pointermove", 300, 300, { force: true }) - .wait(50) + .wait(200) .trigger("pointerup", { force: true }) .should(($draggable: any) => { const draggable = $draggable[0] as HTMLDivElement diff --git a/packages/framer-motion/src/projection/node/create-projection-node.ts b/packages/framer-motion/src/projection/node/create-projection-node.ts index c750abe2b0..246b0f6293 100644 --- a/packages/framer-motion/src/projection/node/create-projection-node.ts +++ b/packages/framer-motion/src/projection/node/create-projection-node.ts @@ -59,6 +59,7 @@ import { time } from "../../frameloop/sync-time" import { microtask } from "../../frameloop/microtask" import { VisualElement } from "../../render/VisualElement" import { getOptimisedAppearId } from "../../animation/optimized-appear/get-appear-id" +import { isDragActive } from "../../gestures/drag/utils/lock" const transformAxes = ["", "X", "Y", "Z"] @@ -872,8 +873,11 @@ export function createProjectionNode({ resetTransform() { if (!resetTransform) return + const isResetRequested = - this.isLayoutDirty || this.shouldResetTransform + this.isLayoutDirty || + this.shouldResetTransform || + this.options.visualElement?.getProps().drag const hasProjection = this.projectionDelta && !isDeltaZero(this.projectionDelta) From 1a9ba5c2e3413fdb4343d29533b8a2b9add40fdc Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 19 Jun 2024 12:34:23 +0200 Subject: [PATCH 2/3] Updating PR --- dev/react/src/examples/Drag-constraints-ref.tsx | 6 +++++- .../src/projection/node/create-projection-node.ts | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dev/react/src/examples/Drag-constraints-ref.tsx b/dev/react/src/examples/Drag-constraints-ref.tsx index b2dfbf0bc5..5577f9184b 100644 --- a/dev/react/src/examples/Drag-constraints-ref.tsx +++ b/dev/react/src/examples/Drag-constraints-ref.tsx @@ -18,6 +18,11 @@ const child = { borderRadius: 20, } +/** + * This sibling layout animation is designed to fuzz/stress the drag constraints + * measurements. Remeasuring the constraints during drag would previously mess + * up the position of the draggable element. + */ const SiblingLayoutAnimation = () => { const [state, setState] = useState(false) @@ -48,7 +53,6 @@ export const App = () => {
Date: Wed, 19 Jun 2024 12:43:04 +0200 Subject: [PATCH 3/3] Always remove transform on draggable elements --- .../framer-motion/src/projection/node/create-projection-node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/framer-motion/src/projection/node/create-projection-node.ts b/packages/framer-motion/src/projection/node/create-projection-node.ts index 00b1c63ce9..ae06f41c6e 100644 --- a/packages/framer-motion/src/projection/node/create-projection-node.ts +++ b/packages/framer-motion/src/projection/node/create-projection-node.ts @@ -876,7 +876,7 @@ export function createProjectionNode({ const isResetRequested = this.isLayoutDirty || this.shouldResetTransform || - this.options.visualElement?.getProps().drag + this.options.alwaysMeasureLayout const hasProjection = this.projectionDelta && !isDeltaZero(this.projectionDelta)