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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Motion adheres to [Semantic Versioning](http://semver.org/).

Undocumented APIs should be considered internal and may change without warning.

## [12.31.1] 2026-02-04

### Added

- Drag constraints updated even when draggable or constraints resize outside of React renders.

## [12.31.0] 2026-02-03

### Added
Expand Down
6 changes: 3 additions & 3 deletions dev/html/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "html-env",
"private": true,
"version": "12.31.0",
"version": "12.31.1",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -10,8 +10,8 @@
"preview": "vite preview"
},
"dependencies": {
"framer-motion": "^12.31.0",
"motion": "^12.31.0",
"framer-motion": "^12.31.1",
"motion": "^12.31.1",
"motion-dom": "^12.30.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
Expand Down
4 changes: 2 additions & 2 deletions dev/next/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "next-env",
"private": true,
"version": "12.31.0",
"version": "12.31.1",
"type": "module",
"scripts": {
"dev": "next dev",
Expand All @@ -10,7 +10,7 @@
"build": "next build"
},
"dependencies": {
"motion": "^12.31.0",
"motion": "^12.31.1",
"next": "15.4.10",
"react": "19.0.0",
"react-dom": "19.0.0"
Expand Down
4 changes: 2 additions & 2 deletions dev/react-19/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "react-19-env",
"private": true,
"version": "12.31.0",
"version": "12.31.1",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -11,7 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"motion": "^12.31.0",
"motion": "^12.31.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
Expand Down
4 changes: 2 additions & 2 deletions dev/react/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "react-env",
"private": true,
"version": "12.31.0",
"version": "12.31.1",
"type": "module",
"scripts": {
"dev": "yarn vite",
Expand All @@ -11,7 +11,7 @@
"preview": "yarn vite preview"
},
"dependencies": {
"framer-motion": "^12.31.0",
"framer-motion": "^12.31.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
Expand Down
61 changes: 61 additions & 0 deletions dev/react/src/tests/drag-ref-constraints-element-resize.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { motion, useMotionValue, useTransform } from "framer-motion"
import { useRef, useCallback } from "react"

/**
* Test page for issue #2458: Drag constraints should update when
* the draggable element's dimensions change.
*
* Container: 500x500, positioned at top-left
* Draggable: starts at 100x100, can be resized to 300x300
*
* Before resize: constraints allow 400px of travel (500 - 100)
* After resize: constraints should allow 200px of travel (500 - 300)
*/
export const App = () => {
const constraintsRef = useRef<HTMLDivElement>(null)
const widthMV = useMotionValue(100)
const heightMV = useMotionValue(100)
const width = useTransform(widthMV, (v) => `${v}px`)
const height = useTransform(heightMV, (v) => `${v}px`)

const handleResize = useCallback(() => {
widthMV.set(300)
heightMV.set(300)
}, [widthMV, heightMV])

return (
<div style={{ padding: 0, margin: 0 }}>
<button
id="resize-trigger"
onClick={handleResize}
style={{ position: "fixed", top: 10, right: 10, zIndex: 10 }}
>
Resize to 300x300
</button>
<motion.div
id="constraints"
ref={constraintsRef}
style={{
width: 500,
height: 500,
background: "rgba(0, 0, 255, 0.1)",
position: "relative",
}}
>
<motion.div
id="box"
data-testid="draggable"
drag
dragConstraints={constraintsRef}
dragElastic={0}
dragMomentum={false}
style={{
width,
height,
background: "red",
}}
/>
</motion.div>
</div>
)
}
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "12.31.0",
"version": "12.31.1",
"packages": [
"packages/*",
"dev/*"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Tests for issue #2458: Drag constraints should update when the
* draggable element or constraint container resizes.
*
* The test page has:
* - Container (#constraints): 500x500
* - Draggable (#box): starts 100x100, resizable to 300x300 via button
*
* Before resize: max travel = 400px (500 - 100)
* After resize: max travel = 200px (500 - 300)
*/
describe("Drag Constraints Update on Element Resize", () => {
it("Constrains drag correctly before resize", () => {
cy.visit("?test=drag-ref-constraints-element-resize")
.wait(200)
.get("#box")
.trigger("pointerdown", 5, 5)
.trigger("pointermove", 10, 10)
.wait(50)
.trigger("pointermove", 600, 600, { force: true })
.wait(50)
.trigger("pointerup", { force: true })
.wait(50)
.should(($box: any) => {
const box = $box[0] as HTMLDivElement
const { right, bottom } = box.getBoundingClientRect()
// 100x100 box in 500x500 container: max right/bottom = 500
expect(right).to.be.at.most(502)
expect(bottom).to.be.at.most(502)
})
})

it("Updates drag constraints after draggable element is resized", () => {
cy.visit("?test=drag-ref-constraints-element-resize")
.wait(200)

// Click resize button to resize draggable from 100x100 to 300x300
cy.get("#resize-trigger")
.click()
.wait(200)

// Verify the box is now 300x300
cy.get("#box").should(($box: any) => {
const box = $box[0] as HTMLDivElement
const { width, height } = box.getBoundingClientRect()
expect(width).to.equal(300)
expect(height).to.equal(300)
})

// Now drag to the far bottom-right
cy.get("#box")
.trigger("pointerdown", 5, 5)
.trigger("pointermove", 10, 10)
.wait(50)
.trigger("pointermove", 600, 600, { force: true })
.wait(50)
.trigger("pointerup", { force: true })
.wait(50)
.should(($box: any) => {
const box = $box[0] as HTMLDivElement
const { right, bottom } = box.getBoundingClientRect()
// 300x300 box in 500x500 container: max right/bottom = 500
// Without the fix, right/bottom would be ~700 (300 + 400 old constraint)
expect(right).to.be.at.most(502)
expect(bottom).to.be.at.most(502)
})
})

it("Updates drag constraints after draggable element is resized, with existing drag offset", () => {
cy.visit("?test=drag-ref-constraints-element-resize")
.wait(200)

// First drag to an intermediate position
cy.get("#box")
.trigger("pointerdown", 5, 5)
.trigger("pointermove", 10, 10)
.wait(50)
.trigger("pointermove", 100, 100, { force: true })
.wait(50)
.trigger("pointerup", { force: true })
.wait(50)

// Resize the element from 100x100 to 300x300
cy.get("#resize-trigger")
.click()
.wait(200)

// Now drag far to the bottom-right
cy.get("#box")
.trigger("pointerdown", 5, 5)
.trigger("pointermove", 10, 10)
.wait(50)
.trigger("pointermove", 600, 600, { force: true })
.wait(50)
.trigger("pointerup", { force: true })
.wait(50)
.should(($box: any) => {
const box = $box[0] as HTMLDivElement
const { right, bottom } = box.getBoundingClientRect()
// Even after a prior drag + resize, box must stay within container
expect(right).to.be.at.most(502)
expect(bottom).to.be.at.most(502)
})
})
})
2 changes: 1 addition & 1 deletion packages/framer-motion/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "framer-motion",
"version": "12.31.0",
"version": "12.31.1",
"description": "A simple and powerful JavaScript animation library",
"main": "dist/cjs/index.js",
"module": "dist/es/index.mjs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
PanInfo,
percent,
ResolvedConstraints,
resize,
setDragLock,
Transition,
type VisualElement,
Expand Down Expand Up @@ -375,10 +376,13 @@ export class VisualElementDragControls {

/**
* If we're outputting to external MotionValues, we want to rebase the measured constraints
* from viewport-relative to component-relative.
* from viewport-relative to component-relative. This only applies to relative (non-ref)
* constraints, as ref-based constraints from calcViewportConstraints are already in the
* correct coordinate space for the motion value transform offset.
*/
if (
prevConstraints !== this.constraints &&
!isRefObject(dragConstraints) &&
layout &&
this.constraints &&
!this.hasMutatedConstraints
Expand Down Expand Up @@ -621,6 +625,12 @@ export class VisualElementDragControls {
: "none"
projection.root && projection.root.updateScroll()
projection.updateLayout()

/**
* Reset constraints so resolveConstraints() will recalculate them
* with the freshly measured layout rather than returning the cached value.
*/
this.constraints = false
this.resolveConstraints()

/**
Expand All @@ -639,6 +649,13 @@ export class VisualElementDragControls {
] as Axis
axisValue.set(mixNumber(min, max, boxProgress[axis]))
})

/**
* Flush the updated transform to the DOM synchronously to prevent
* a visual flash at the element's CSS layout position (0,0) when
* the transform was stripped for measurement.
*/
this.visualElement.render()
}

addListeners() {
Expand Down Expand Up @@ -673,10 +690,26 @@ export class VisualElementDragControls {
}
)

/**
* If using ref-based constraints, observe both the draggable element
* and the constraint container for size changes via ResizeObserver.
* Setup is deferred because dragConstraints.current is null when
* addListeners first runs (React hasn't committed the ref yet).
*/
let stopResizeObservers: VoidFunction | undefined

const measureDragConstraints = () => {
const { dragConstraints } = this.getProps()
if (isRefObject(dragConstraints) && dragConstraints.current) {
this.constraints = this.resolveRefConstraints()

if (!stopResizeObservers) {
stopResizeObservers = startResizeObservers(
element,
dragConstraints.current as HTMLElement,
() => this.scalePositionWithinConstraints()
)
}
}
}

Expand Down Expand Up @@ -730,6 +763,7 @@ export class VisualElementDragControls {
stopPointerListener()
stopMeasureLayoutListener()
stopLayoutUpdateListener && stopLayoutUpdateListener()
stopResizeObservers && stopResizeObservers()
}
}

Expand All @@ -755,6 +789,30 @@ export class VisualElementDragControls {
}
}

function skipFirstCall(callback: VoidFunction): VoidFunction {
let isFirst = true
return () => {
if (isFirst) {
isFirst = false
return
}
callback()
}
}

function startResizeObservers(
element: HTMLElement,
constraintsElement: HTMLElement,
onResize: VoidFunction
): VoidFunction {
const stopElement = resize(element, skipFirstCall(onResize))
const stopContainer = resize(constraintsElement, skipFirstCall(onResize))
return () => {
stopElement()
stopContainer()
}
}

function shouldDrag(
direction: DragDirection,
drag: boolean | DragDirection | undefined,
Expand Down
Loading
Loading