diff --git a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts index 1dde45c74d..9a8d8fdaa4 100644 --- a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts +++ b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts @@ -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 /** @@ -672,7 +676,7 @@ export class VisualElementDragControls { target !== element && isElementTextInput(target) if (drag && dragListener && !isClickingTextInputChild) { - this.start(event) + this.start(event, { snapToCursor: dragSnapToCursor }) } } ) diff --git a/packages/framer-motion/src/gestures/drag/__tests__/use-drag-controls.test.tsx b/packages/framer-motion/src/gestures/drag/__tests__/use-drag-controls.test.tsx index 54469272c1..52b06954a4 100644 --- a/packages/framer-motion/src/gestures/drag/__tests__/use-drag-controls.test.tsx +++ b/packages/framer-motion/src/gestures/drag/__tests__/use-drag-controls.test.tsx @@ -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", () => { @@ -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 = () => ( + + + + ) + + const { rerender, getByTestId } = render() + rerender() + + 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 = () => ( + + + + ) + + const { rerender, getByTestId } = render() + rerender() + + 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() + }) +}) diff --git a/packages/motion-dom/src/node/types.ts b/packages/motion-dom/src/node/types.ts index ee0164b448..a150fb6f23 100644 --- a/packages/motion-dom/src/node/types.ts +++ b/packages/motion-dom/src/node/types.ts @@ -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 + * + * ``` + */ + 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.