Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing dragConstraints={ref} with active layout animations #2707

Merged
merged 3 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 42 additions & 12 deletions dev/react/src/examples/Drag-constraints-ref.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef, useState } from "react"
import { useEffect, useRef, useState } from "react"
import { motion } from "framer-motion"

const container = {
Expand All @@ -18,20 +18,50 @@ 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)

useEffect(() => {
const timer = setTimeout(() => setState(!state), 500)

return () => clearTimeout(timer)
}, [state])

return (
<motion.div
layout
style={{
...child,
background: "blue",
position: "relative",
left: state ? "100px" : "0",
}}
/>
)
}

export const App = () => {
const ref = useRef()
const [count, setCount] = useState(0)
return (
<div ref={ref} style={container}>
<motion.div
drag
//dragElastic
dragConstraints={ref}
whileTap={{ scale: 0.95 }}
whileHover={{ scale: 1.1 }}
style={child}
onClick={() => setCount(count + 1)}
/>
</div>
<>
<div ref={ref} style={container}>
<motion.div
drag
dragConstraints={ref}
whileTap={{ scale: 0.95 }}
whileHover={{ scale: 1.1 }}
style={child}
onClick={() => setCount(count + 1)}
id="draggable"
/>
</div>
<SiblingLayoutAnimation />
</>
)
}
82 changes: 57 additions & 25 deletions dev/react/src/tests/drag-ref-constraints.tsx
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -16,30 +16,62 @@ export const App = () => {
const x = useMotionValue("100%")

return (
<div style={{ height: 2000, paddingTop: 100 }}>
<motion.div
data-testid="constraint"
style={{ width: 200, height: 200, background: "blue" }}
ref={containerRef}
>
<>
<div style={{ height: 2000, paddingTop: 100 }}>
<motion.div
id="box"
data-testid="draggable"
drag
dragElastic={0}
dragMomentum={false}
style={{
width: 50,
height: 50,
background: dragging ? "yellow" : "red",
x,
}}
dragConstraints={containerRef}
layout={layout}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}
/>
</motion.div>
</div>
data-testid="constraint"
style={{ width: 200, height: 200, background: "blue" }}
ref={containerRef}
>
<motion.div
id="box"
data-testid="draggable"
drag
dragElastic={0}
dragMomentum={false}
style={{
width: 50,
height: 50,
background: dragging ? "yellow" : "red",
x,
}}
dragConstraints={containerRef}
layout={layout}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}
/>
</motion.div>
</div>
<SiblingLayoutAnimation />
</>
)
}

/**
* 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 (
<motion.div
layout
style={{
width: 200,
height: 200,
borderRadius: 20,
background: "blue",
position: "relative",
left: state ? "100px" : "0",
}}
/>
)
}
4 changes: 2 additions & 2 deletions packages/framer-motion/cypress/integration/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +237 to +239
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this required?

Copy link
Collaborator Author

@mattgperry mattgperry Jun 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The layout animation I added is like 100 - 200ms long so this delay has to be long enough for them to trigger and break the measurements

.trigger("pointerup", { force: true })
.should(($draggable: any) => {
const draggable = $draggable[0] as HTMLDivElement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -872,8 +872,11 @@ export function createProjectionNode<I>({

resetTransform() {
if (!resetTransform) return

const isResetRequested =
this.isLayoutDirty || this.shouldResetTransform
this.isLayoutDirty ||
this.shouldResetTransform ||
this.options.alwaysMeasureLayout

const hasProjection =
this.projectionDelta && !isDeltaZero(this.projectionDelta)
Expand Down