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.29.3] 2026-02-02

### Fixed

- `Reorder`: Fixed viewport autoscroll.

## [12.29.2] 2026-01-26

### Fixed
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.29.2",
"version": "12.29.3",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -10,8 +10,8 @@
"preview": "vite preview"
},
"dependencies": {
"framer-motion": "^12.29.2",
"motion": "^12.29.2",
"framer-motion": "^12.29.3",
"motion": "^12.29.3",
"motion-dom": "^12.29.2",
"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.29.2",
"version": "12.29.3",
"type": "module",
"scripts": {
"dev": "next dev",
Expand All @@ -10,7 +10,7 @@
"build": "next build"
},
"dependencies": {
"motion": "^12.29.2",
"motion": "^12.29.3",
"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.29.2",
"version": "12.29.3",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -11,7 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"motion": "^12.29.2",
"motion": "^12.29.3",
"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.29.2",
"version": "12.29.3",
"type": "module",
"scripts": {
"dev": "yarn vite",
Expand All @@ -11,7 +11,7 @@
"preview": "yarn vite preview"
},
"dependencies": {
"framer-motion": "^12.29.2",
"framer-motion": "^12.29.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
Expand Down
104 changes: 104 additions & 0 deletions dev/react/src/tests/reorder-auto-scroll-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as React from "react"
import { useState } from "react"
import { Reorder, useMotionValue } from "framer-motion"

const initialItems = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

interface ItemProps {
item: number
}

const Item = ({ item }: ItemProps) => {
const y = useMotionValue(0)
const hue = item * 30

return (
<Reorder.Item
value={item}
id={String(item)}
style={{
y,
backgroundColor: `hsl(${hue}, 70%, 50%)`,
}}
data-testid={String(item)}
/>
)
}

/**
* Test case for auto-scroll when the scrollable container is inside a scrollable page.
*
* This tests the bug where autoscroll wouldn't work because the gesture system
* uses pageX/pageY coordinates but getBoundingClientRect() returns viewport
* coordinates. When the page is scrolled, these coordinate systems don't match.
*/
export const App = () => {
const [items, setItems] = useState(initialItems)

return (
<>
{/* Spacer to make page scrollable */}
<div style={{ height: "300px", background: "#222" }}>
<p style={{ color: "#fff", padding: "20px" }}>
Scroll down to see the reorder list. The container and the page are both scrollable.
</p>
</div>
<div
data-testid="scroll-container"
style={{
height: "300px",
overflow: "auto",
margin: "0 auto",
width: "300px",
background: "#444",
}}
>
<Reorder.Group axis="y" onReorder={setItems} values={items}>
{items.map((item) => (
<Item key={item} item={item} />
))}
</Reorder.Group>
</div>
{/* More spacer to ensure page is scrollable */}
<div style={{ height: "500px", background: "#222" }}>
<p style={{ color: "#fff", padding: "20px" }}>
Bottom spacer
</p>
</div>
<style>{styles}</style>
</>
)
}

const styles = `
html, body {
width: 100%;
min-height: 100%;
background: #333;
padding: 0;
margin: 0;
}

ul,
li {
list-style: none;
padding: 0;
margin: 0;
}

ul {
position: relative;
width: 100%;
}

li {
border-radius: 10px;
margin-bottom: 10px;
width: 100%;
height: 60px;
position: relative;
border-radius: 5px;
flex-shrink: 0;
cursor: grab;
}
`
2 changes: 0 additions & 2 deletions dev/react/src/tests/reorder-auto-scroll-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ export const App = () => {
<div
data-testid="scroll-container"
style={{
height: "300px",
overflow: "auto",
margin: "0 auto",
width: "300px",
background: "#444",
Expand Down
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "12.29.2",
"version": "12.29.3",
"packages": [
"packages/*",
"dev/*"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ describe("Reorder auto-scroll", () => {

describe("with scrollable container inside scrollable page", () => {
it("Auto-scrolls container after page has been scrolled", () => {
cy.visit("?test=reorder-auto-scroll-page")
cy.visit("?test=reorder-auto-scroll-container")
.wait(200)
// Scroll the page down so the container is in view
.window()
Expand Down Expand Up @@ -117,4 +117,40 @@ describe("Reorder auto-scroll", () => {
})
})
})

describe("without scrollable container inside scrollable page", () => {
it("Auto-scrolls page after dragging near the edges", () => {
cy.visit("?test=reorder-auto-scroll-page")
.wait(200)
// Check window position, then scroll
.window()
.then((win) => {
expect(win.scrollY).to.equal(0)
})
.wait(100)
.get("[data-testid='0']")
.then(() => {
cy.window().then((win) => {
const nearBottom =
win.innerHeight - win.screenTop - 20

cy.get("[data-testid='0']")
.trigger("pointerdown", 50, 25)
.wait(50)
.trigger("pointermove", 50, 30, { force: true })
.wait(50)
.trigger("pointermove", 50, nearBottom, {
force: true,
})
.wait(300)
.window()
.then((win) => {
expect(win.scrollY).to.greaterThan(0)
})
.get("[data-testid='0']")
.trigger("pointerup", { force: true })
})
})
})
})
})
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.29.2",
"version": "12.29.3",
"description": "A simple and powerful JavaScript animation library",
"main": "dist/cjs/index.js",
"module": "dist/es/index.mjs",
Expand Down
38 changes: 27 additions & 11 deletions packages/framer-motion/src/components/Reorder/utils/auto-scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ export function resetAutoScrollState(): void {
function isScrollableElement(element: Element, axis: "x" | "y"): boolean {
const style = getComputedStyle(element)
const overflow = axis === "x" ? style.overflowX : style.overflowY
return overflowStyles.has(overflow)

const isDocumentScroll =
element === document.body ||
element === document.documentElement

return overflowStyles.has(overflow) || isDocumentScroll
}

function findScrollableAncestor(
Expand All @@ -63,8 +68,8 @@ function getScrollAmount(
): { amount: number; edge: ActiveEdge } {
const rect = scrollElement.getBoundingClientRect()

const start = axis === "x" ? rect.left : rect.top
const end = axis === "x" ? rect.right : rect.bottom
const start = axis === "x" ? Math.max(0, rect.left) : Math.max(0, rect.top)
const end = axis === "x" ? Math.min(window.innerWidth, rect.right) : Math.min(window.innerHeight, rect.bottom)

const distanceFromStart = pointerPosition - start
const distanceFromEnd = end - pointerPosition
Expand Down Expand Up @@ -115,6 +120,10 @@ export function autoScrollIfNeeded(

const currentActiveEdge = activeScrollEdge.get(scrollableAncestor)

const isDocumentScroll =
scrollableAncestor === document.body ||
scrollableAncestor === document.documentElement

// If not currently scrolling this edge, check velocity to see if we should start
if (currentActiveEdge !== edge) {
// Only start scrolling if velocity is towards the edge
Expand All @@ -129,10 +138,9 @@ export function autoScrollIfNeeded(
// Record initial scroll limit (prevents infinite scroll)
const maxScroll =
axis === "x"
? scrollableAncestor.scrollWidth -
scrollableAncestor.clientWidth
: scrollableAncestor.scrollHeight -
scrollableAncestor.clientHeight
? scrollableAncestor.scrollWidth - (isDocumentScroll ? window.innerWidth : scrollableAncestor.clientWidth)
: scrollableAncestor.scrollHeight - (isDocumentScroll ? window.innerHeight : scrollableAncestor.clientHeight)

initialScrollLimits.set(scrollableAncestor, maxScroll)
}

Expand All @@ -141,15 +149,23 @@ export function autoScrollIfNeeded(
const initialLimit = initialScrollLimits.get(scrollableAncestor)!
const currentScroll =
axis === "x"
? scrollableAncestor.scrollLeft
: scrollableAncestor.scrollTop
? (isDocumentScroll ? window.scrollX : scrollableAncestor.scrollLeft)
: (isDocumentScroll ? window.scrollY : scrollableAncestor.scrollTop)
if (currentScroll >= initialLimit) return
}

// Apply scroll
if (axis === "x") {
scrollableAncestor.scrollLeft += scrollAmount
if (isDocumentScroll) {
window.scrollBy({ left: scrollAmount })
} else {
scrollableAncestor.scrollLeft += scrollAmount
}
} else {
scrollableAncestor.scrollTop += scrollAmount
if (isDocumentScroll) {
window.scrollBy({ top: scrollAmount })
} else {
scrollableAncestor.scrollTop += scrollAmount
}
}
}
4 changes: 2 additions & 2 deletions packages/motion/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "motion",
"version": "12.29.2",
"version": "12.29.3",
"description": "An animation library for JavaScript and React.",
"main": "dist/cjs/index.js",
"module": "dist/es/index.mjs",
Expand Down Expand Up @@ -76,7 +76,7 @@
"postpublish": "git push --tags"
},
"dependencies": {
"framer-motion": "^12.29.2",
"framer-motion": "^12.29.3",
"tslib": "^2.4.0"
},
"peerDependencies": {
Expand Down
Loading
Loading