Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,15 @@ export class VisualElementDragControls {
if (projection && projection.layout) {
const { min, max } = projection.layout.layoutBox[axis]

axisValue.set(point[axis] - mixNumber(min, max, 0.5))
/**
* The layout measurement includes the current transform value,
* so we need to add it back to get the correct snap position.
* This fixes an issue where elements with initial coordinates
* would snap to the wrong position on the first drag.
*/
const current = axisValue.get() || 0

axisValue.set(point[axis] - mixNumber(min, max, 0.5) + current)
}
})
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from "react"
import { motion, useDragControls, DragControls } from "../../../"
import { motion, useDragControls, DragControls, motionValue } from "../../../"
import { render } from "../../../jest.setup"
import { nextFrame } from "../../__tests__/utils"
import { MockDrag, drag } from "./utils"
Expand Down Expand Up @@ -140,4 +140,72 @@ describe("useDragControls", () => {
await nextFrame()
expect(onDragStart).toBeCalledTimes(2)
})

test("snapToCursor works correctly with initial coordinates", async () => {
const x = motionValue(0)
const y = motionValue(0)
const Component = () => {
const dragControls = useDragControls()
return (
<MockDrag>
<div
onPointerDown={(e) =>
dragControls.start(e, { snapToCursor: true })
}
data-testid="drag-handle"
/>
<motion.div
drag
dragControls={dragControls}
initial={{ x: 100, y: 100 }}
style={{ x, y }}
data-testid="draggable"
/>
</MockDrag>
)
}

const { rerender, getByTestId } = render(<Component />)
rerender(<Component />)

// Wait for initial values to be applied
await nextFrame()

// The element should start at x=100, y=100
expect(x.get()).toBe(100)
expect(y.get()).toBe(100)

// Drag to position (50, 50) with snapToCursor
const pointer = await drag(
getByTestId("draggable"),
getByTestId("drag-handle")
).to(50, 50)

await nextFrame()

// With snapToCursor, the element should snap to the cursor position
// The x and y values should reflect the cursor position relative to the element's center
// The key is that the values should be consistent regardless of initial position
const xAfterFirstSnap = x.get()
const yAfterFirstSnap = y.get()

pointer.end()
await nextFrame()

// Now do a second drag to the same position to verify behavior is consistent
const pointer2 = await drag(
getByTestId("draggable"),
getByTestId("drag-handle")
).to(50, 50)

await nextFrame()

// The snap behavior should be the same on the second drag
// Before the fix, first drag was different from second drag due to
// not accounting for the initial coordinates in the layout measurement
expect(x.get()).toBeCloseTo(xAfterFirstSnap, 0)
expect(y.get()).toBeCloseTo(yAfterFirstSnap, 0)

pointer2.end()
})
})