diff --git a/CHANGELOG.md b/CHANGELOG.md index d697b28104..359915f5ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ Motion adheres to [Semantic Versioning](http://semver.org/). Undocumented APIs should be considered internal and may change without warning. +## [12.30.1] 2026-02-03 + +### Fixed + +- Allow drag to be initiated by child `a` and `button` elements. + ## [12.30.0] 2026-02-02 ### Added diff --git a/Makefile b/Makefile index 3f9992ad27..ad1c34dc0a 100644 --- a/Makefile +++ b/Makefile @@ -95,7 +95,7 @@ test-e2e: test-nextjs test-html test-react test-react-19 yarn test-playwright test-single: build test-mkdir - yarn start-server-and-test "yarn dev-server" http://localhost:9991 "cd packages/framer-motion && cypress run --config-file=cypress.react-19.json --headed --spec cypress/integration/unit-conversion.ts" + yarn start-server-and-test "yarn dev-server" http://localhost:9991 "cd packages/framer-motion && cypress run --config-file=cypress.react.json --headed --spec cypress/integration/drag-nested.ts" lint: bootstrap yarn lint diff --git a/dev/html/package.json b/dev/html/package.json index d58ab3e796..d7d43cdd73 100644 --- a/dev/html/package.json +++ b/dev/html/package.json @@ -1,7 +1,7 @@ { "name": "html-env", "private": true, - "version": "12.30.0", + "version": "12.30.1", "type": "module", "scripts": { "dev": "vite", @@ -10,9 +10,9 @@ "preview": "vite preview" }, "dependencies": { - "framer-motion": "^12.30.0", - "motion": "^12.30.0", - "motion-dom": "^12.30.0", + "framer-motion": "^12.30.1", + "motion": "^12.30.1", + "motion-dom": "^12.30.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/dev/next/package.json b/dev/next/package.json index 215b096d23..26c138a4a3 100644 --- a/dev/next/package.json +++ b/dev/next/package.json @@ -1,7 +1,7 @@ { "name": "next-env", "private": true, - "version": "12.30.0", + "version": "12.30.1", "type": "module", "scripts": { "dev": "next dev", @@ -10,7 +10,7 @@ "build": "next build" }, "dependencies": { - "motion": "^12.30.0", + "motion": "^12.30.1", "next": "15.4.10", "react": "19.0.0", "react-dom": "19.0.0" diff --git a/dev/react-19/package.json b/dev/react-19/package.json index c2c1bbff12..e258ab4910 100644 --- a/dev/react-19/package.json +++ b/dev/react-19/package.json @@ -1,7 +1,7 @@ { "name": "react-19-env", "private": true, - "version": "12.30.0", + "version": "12.30.1", "type": "module", "scripts": { "dev": "vite", @@ -11,7 +11,7 @@ "preview": "vite preview" }, "dependencies": { - "motion": "^12.30.0", + "motion": "^12.30.1", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/dev/react/package.json b/dev/react/package.json index 7b7e188e8b..aa89f7b533 100644 --- a/dev/react/package.json +++ b/dev/react/package.json @@ -1,7 +1,7 @@ { "name": "react-env", "private": true, - "version": "12.30.0", + "version": "12.30.1", "type": "module", "scripts": { "dev": "yarn vite", @@ -11,7 +11,7 @@ "preview": "yarn vite preview" }, "dependencies": { - "framer-motion": "^12.30.0", + "framer-motion": "^12.30.1", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/lerna.json b/lerna.json index 8342924aa4..5ac1a87dcd 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "12.30.0", + "version": "12.30.1", "packages": [ "packages/*", "dev/*" diff --git a/packages/framer-motion/cypress/integration/drag-input-propagation.ts b/packages/framer-motion/cypress/integration/drag-input-propagation.ts index 0fafbb74e3..26dfe60725 100644 --- a/packages/framer-motion/cypress/integration/drag-input-propagation.ts +++ b/packages/framer-motion/cypress/integration/drag-input-propagation.ts @@ -1,6 +1,11 @@ /** - * Tests for issue #1674: Interactive elements inside draggable elements should not trigger drag - * https://github.com/motiondivision/motion/issues/1674 + * Tests for drag behavior with interactive elements inside draggable elements. + * + * Form controls where text selection or direct interaction is expected + * (input, textarea, select, contenteditable) should NOT trigger drag. + * + * Buttons and links SHOULD allow drag since they don't have click-and-move + * actions of their own. */ describe("Drag Input Propagation", () => { it("Should not drag when clicking and dragging on an input inside draggable", () => { @@ -60,7 +65,7 @@ describe("Drag Input Propagation", () => { }) }) - it("Should not drag when clicking and dragging on a button inside draggable", () => { + it("Should drag when clicking and dragging on a button inside draggable", () => { cy.visit("?test=drag-input-propagation") .wait(200) .get("[data-testid='draggable']") @@ -70,7 +75,7 @@ describe("Drag Input Propagation", () => { expect(top).to.equal(100) }) - // Attempt to drag by clicking on the button + // Drag by clicking on the button - buttons don't have click-and-move actions cy.get("[data-testid='button']") .trigger("pointerdown", 5, 5) .trigger("pointermove", 10, 10) @@ -79,15 +84,16 @@ describe("Drag Input Propagation", () => { .wait(50) .trigger("pointerup", { force: true }) - // Verify the draggable element did NOT move + // Verify the draggable element DID move cy.get("[data-testid='draggable']").should(($draggable) => { const { left, top } = $draggable[0].getBoundingClientRect() - expect(left).to.equal(100) - expect(top).to.equal(100) + // Element should have moved + expect(left).to.be.greaterThan(200) + expect(top).to.be.greaterThan(200) }) }) - it("Should not drag when clicking and dragging on a link inside draggable", () => { + it("Should drag when clicking and dragging on a link inside draggable", () => { cy.visit("?test=drag-input-propagation") .wait(200) .get("[data-testid='draggable']") @@ -97,7 +103,7 @@ describe("Drag Input Propagation", () => { expect(top).to.equal(100) }) - // Attempt to drag by clicking on the link + // Drag by clicking on the link - links don't have click-and-move actions cy.get("[data-testid='link']") .trigger("pointerdown", 5, 5) .trigger("pointermove", 10, 10) @@ -106,11 +112,12 @@ describe("Drag Input Propagation", () => { .wait(50) .trigger("pointerup", { force: true }) - // Verify the draggable element did NOT move + // Verify the draggable element DID move cy.get("[data-testid='draggable']").should(($draggable) => { const { left, top } = $draggable[0].getBoundingClientRect() - expect(left).to.equal(100) - expect(top).to.equal(100) + // Element should have moved + expect(left).to.be.greaterThan(200) + expect(top).to.be.greaterThan(200) }) }) diff --git a/packages/framer-motion/cypress/integration/drag-nested.ts b/packages/framer-motion/cypress/integration/drag-nested.ts index d619134f2f..c0b6d3999b 100644 --- a/packages/framer-motion/cypress/integration/drag-nested.ts +++ b/packages/framer-motion/cypress/integration/drag-nested.ts @@ -372,9 +372,9 @@ function testAlternateAxes(parentLayout: boolean, childLayout: boolean) { .wait(200) .get("#child") .trigger("pointerdown", 5, 5, { force: true }) - .wait(50) + .wait(80) .trigger("pointermove", 10, 10, { force: true }) - .wait(50) + .wait(80) .trigger("pointermove", 100, 100, { force: true }) .wait(80) .should(([$child]: any) => { diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index a73f08dde4..ca60828950 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -1,6 +1,6 @@ { "name": "framer-motion", - "version": "12.30.0", + "version": "12.30.1", "description": "A simple and powerful JavaScript animation library", "main": "dist/cjs/index.js", "module": "dist/es/index.mjs", @@ -88,7 +88,7 @@ "measure": "rollup -c ./rollup.size.config.mjs" }, "dependencies": { - "motion-dom": "^12.30.0", + "motion-dom": "^12.30.1", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, diff --git a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts index 5a2001996b..d19b302c4c 100644 --- a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts +++ b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts @@ -7,7 +7,7 @@ import { createBox, eachAxis, frame, - isElementKeyboardAccessible, + isElementTextInput, measurePageBox, mixNumber, PanInfo, @@ -655,15 +655,17 @@ export class VisualElementDragControls { const target = event.target as Element /** - * Only block drag if clicking on a keyboard-accessible child element. - * If the draggable element itself is keyboard-accessible (e.g., motion.button), - * dragging should still work when clicking directly on it. + * Only block drag if clicking on a text input child element + * (input, textarea, select, contenteditable) where users might + * want to select text or interact with the control. + * + * Buttons and links don't block drag since they don't have + * click-and-move actions of their own. */ - const isClickingKeyboardAccessibleChild = - target !== element && - isElementKeyboardAccessible(target) + const isClickingTextInputChild = + target !== element && isElementTextInput(target) - if (drag && dragListener && !isClickingKeyboardAccessibleChild) { + if (drag && dragListener && !isClickingTextInputChild) { this.start(event) } } diff --git a/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx b/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx index f2607d493c..75aea1ea18 100644 --- a/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx +++ b/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx @@ -1004,7 +1004,60 @@ describe("keyboard accessible elements", () => { expect(x.get()).toBeGreaterThanOrEqual(100) }) - test("drag gesture does not start when clicking a child button", async () => { + test("drag gesture starts on a motion.input with drag prop", async () => { + const onDragStart = jest.fn() + const x = motionValue(0) + const Component = () => ( + + + + ) + + const { getByTestId, rerender } = render() + rerender() + + const pointer = await drag(getByTestId("draggable-input")).to(100, 100) + pointer.end() + + await nextFrame() + + expect(onDragStart).toBeCalledTimes(1) + expect(x.get()).toBeGreaterThanOrEqual(100) + }) + + test("drag gesture starts on a motion.a with drag prop", async () => { + const onDragStart = jest.fn() + const x = motionValue(0) + const Component = () => ( + + + + ) + + const { getByTestId, rerender } = render() + rerender() + + const pointer = await drag(getByTestId("draggable-link")).to(100, 100) + pointer.end() + + await nextFrame() + + expect(onDragStart).toBeCalledTimes(1) + expect(x.get()).toBeGreaterThanOrEqual(100) + }) + + test("drag gesture does not start when clicking a child input", async () => { const onDragStart = jest.fn() const x = motionValue(0) const Component = () => ( @@ -1015,7 +1068,7 @@ describe("keyboard accessible elements", () => { onDragStart={onDragStart} style={{ x }} > - + ) @@ -1025,7 +1078,7 @@ describe("keyboard accessible elements", () => { const pointer = await drag( getByTestId("draggable"), - getByTestId("child-button") + getByTestId("child-input") ).to(100, 100) pointer.end() @@ -1035,51 +1088,93 @@ describe("keyboard accessible elements", () => { expect(x.get()).toBe(0) }) - test("drag gesture starts on a motion.input with drag prop", async () => { + test("drag gesture does not start when clicking a child textarea", async () => { const onDragStart = jest.fn() const x = motionValue(0) const Component = () => ( - + > +