Skip to content
Open
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 @@ -657,7 +657,11 @@ export class VisualElementDragControls {
element,
"pointerdown",
(event) => {
const { drag, dragListener = true } = this.getProps()
const {
drag,
dragListener = true,
dragSnapToCursor,
} = this.getProps()
const target = event.target as Element

/**
Expand All @@ -672,7 +676,7 @@ export class VisualElementDragControls {
target !== element && isElementTextInput(target)

if (drag && dragListener && !isClickingTextInputChild) {
this.start(event)
this.start(event, { snapToCursor: dragSnapToCursor })
}
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState } from "react"
import { motion, useDragControls, DragControls, motionValue } from "../../../"
import { render } from "../../../jest.setup"
import { nextFrame } from "../../__tests__/utils"
import { VisualElementDragControls } from "../VisualElementDragControls"
import { MockDrag, drag } from "./utils"

describe("useDragControls", () => {
Expand Down Expand Up @@ -209,3 +210,67 @@ describe("useDragControls", () => {
pointer2.end()
})
})

describe("dragSnapToCursor prop", () => {
test("forwards snapToCursor option to start when prop is true", async () => {
const startSpy = jest.spyOn(
VisualElementDragControls.prototype,
"start"
)

const Component = () => (
<MockDrag>
<motion.div
drag
dragSnapToCursor
data-testid="draggable"
/>
</MockDrag>
)

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

startSpy.mockClear()

const pointer = await drag(getByTestId("draggable")).to(50, 50)
pointer.end()
await nextFrame()

expect(startSpy).toHaveBeenCalled()
const [, options] = startSpy.mock.calls[0]
expect(options).toEqual(
expect.objectContaining({ snapToCursor: true })
)

startSpy.mockRestore()
})

test("does not forward snapToCursor option when prop is not set", async () => {
const startSpy = jest.spyOn(
VisualElementDragControls.prototype,
"start"
)

const Component = () => (
<MockDrag>
<motion.div drag data-testid="draggable" />
</MockDrag>
)

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

startSpy.mockClear()

const pointer = await drag(getByTestId("draggable")).to(50, 50)
pointer.end()
await nextFrame()

expect(startSpy).toHaveBeenCalled()
const [, options] = startSpy.mock.calls[0]
expect(options?.snapToCursor).toBeFalsy()

startSpy.mockRestore()
})
})
12 changes: 12 additions & 0 deletions packages/motion-dom/src/node/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,18 @@ export interface MotionNodeDraggableOptions {
*/
dragSnapToOrigin?: boolean | "x" | "y"

/**
* If `true`, the element will snap so that the cursor is centered on it
* when a drag gesture starts. This is the equivalent of passing
* `{ snapToCursor: true }` to a `dragControls.start()` call but works
* for any drag-enabled motion component.
*
* ```jsx
* <motion.div drag dragSnapToCursor />
* ```
*/
dragSnapToCursor?: boolean
Comment on lines +736 to +746
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The JSDoc states this prop "works for any drag-enabled motion component," but it is silently ignored when dragListener={false} is set — a pattern commonly used together with dragControls. A user who combines dragSnapToCursor with dragControls + dragListener={false} will see no snap behaviour and get no warning.

Suggested change
/**
* If `true`, the element will snap so that the cursor is centered on it
* when a drag gesture starts. This is the equivalent of passing
* `{ snapToCursor: true }` to a `dragControls.start()` call but works
* for any drag-enabled motion component.
*
* ```jsx
* <motion.div drag dragSnapToCursor />
* ```
*/
dragSnapToCursor?: boolean
/**
* If `true`, the element will snap so that the cursor is centered on it
* when a drag gesture starts. This is the equivalent of passing
* `{ snapToCursor: true }` to a `dragControls.start()` call but works
* for any drag-enabled motion component.
*
* Note: has no effect when `dragListener` is `false`, in which case pass
* `{ snapToCursor: true }` directly to `dragControls.start()`.
*
* ```jsx
* <motion.div drag dragSnapToCursor />
* ```
*/
dragSnapToCursor?: boolean


/**
* By default, if `drag` is defined on a component then an event listener will be attached
* to automatically initiate dragging when a user presses down on it.
Expand Down