Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a646a8a
Fix drag gesture triggering on interactive elements inside draggables
mattgperry Jan 5, 2026
bdd34d8
Refactor drag to use shared isElementKeyboardAccessible utility
mattgperry Jan 6, 2026
ce45b63
Add E2E tests for interactive elements inside draggables
mattgperry Jan 6, 2026
72831bc
Merge branch 'main' into drag-input-propagation
mattgperry Jan 6, 2026
c5e9870
Fix drag constraints getting stuck when clicked during animation
mattgperry Jan 6, 2026
7152f04
Fix snapToCursor animation interference
mattgperry Jan 7, 2026
d6772da
Fix AnimatePresence exit animations with Radix UI asChild
mattgperry Jan 6, 2026
12608b2
Update is-keyboard-accessible.ts
mattgperry Jan 7, 2026
609508d
Fix element jump when dragging during constraint animation
mattgperry Jan 7, 2026
4660ab1
Merge pull request #3457 from motiondivision/animate-presence-ref
mattgperry Jan 7, 2026
8c924db
Updating changelog
mattgperry Jan 7, 2026
5e7f2ce
v12.24.9
mattgperry Jan 7, 2026
19e11cd
Revert "Fix AnimatePresence exit animations with Radix UI asChild"
mattgperry Jan 7, 2026
605dbcd
Merge pull request #3459 from motiondivision/revert-3457-animate-pres…
mattgperry Jan 7, 2026
f671238
Fix isElementKeyboardAccessible to not block drag on motion elements …
mattgperry Jan 7, 2026
210f72b
Merge pull request #3456 from motiondivision/drag-constraints-stuck
mattgperry Jan 7, 2026
8ba5f13
Fix AnimatePresence exit animations with Radix UI asChild
mattgperry Jan 7, 2026
cb8e1a8
Merge branch 'main' into drag-input-propagation
mattgperry Jan 7, 2026
04a2a00
Merge pull request #3448 from motiondivision/drag-input-propagation
mattgperry Jan 7, 2026
542f2c9
Updating changelog
mattgperry Jan 7, 2026
ff003a6
Updating changelog
mattgperry Jan 7, 2026
40e0632
Merge pull request #3460 from motiondivision/fix-animate-presence-rad…
mattgperry Jan 7, 2026
10e3c81
v12.24.10
mattgperry Jan 7, 2026
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ Motion adheres to [Semantic Versioning](http://semver.org/).

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

## [12.24.9] 2026-01-07

### Fixed

- Fixing Radix `Dialog` with `AnimatePresence`.
- Ensure drag constraints animation resumes after press interruption.
- Prevent drag gesture from triggering when pressing focusable elements.

## [12.24.8] 2026-01-07

### Fixed
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ motion (public API)

## Writing Tests

**IMPORTANT: Always write a failing test FIRST before implementing any bug fix or feature.** This ensures the issue is reproducible and the fix is verified. For UI interaction bugs (like gesture handling), prefer E2E tests using Playwright or Cypress.

When waiting for the next frame in async tests:

```javascript
Expand Down
8 changes: 4 additions & 4 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.24.8",
"version": "12.24.10",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -10,9 +10,9 @@
"preview": "vite preview"
},
"dependencies": {
"framer-motion": "^12.24.8",
"motion": "^12.24.8",
"motion-dom": "^12.24.8",
"framer-motion": "^12.24.10",
"motion": "^12.24.10",
"motion-dom": "^12.24.10",
"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.24.8",
"version": "12.24.10",
"type": "module",
"scripts": {
"dev": "next dev",
Expand All @@ -10,7 +10,7 @@
"build": "next build"
},
"dependencies": {
"motion": "^12.24.8",
"motion": "^12.24.10",
"next": "15.4.10",
"react": "19.0.0",
"react-dom": "19.0.0"
Expand Down
5 changes: 3 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.24.8",
"version": "12.24.10",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -11,11 +11,12 @@
"preview": "vite preview"
},
"dependencies": {
"motion": "^12.24.8",
"motion": "^12.24.10",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@radix-ui/react-dialog": "^1.1.15",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@typescript-eslint/eslint-plugin": "^7.2.0",
Expand Down
5 changes: 3 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.24.8",
"version": "12.24.10",
"type": "module",
"scripts": {
"dev": "yarn vite",
Expand All @@ -11,11 +11,12 @@
"preview": "yarn vite preview"
},
"dependencies": {
"framer-motion": "^12.24.8",
"framer-motion": "^12.24.10",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@radix-ui/react-dialog": "^1.1.15",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.2.0",
Expand Down
120 changes: 120 additions & 0 deletions dev/react/src/tests/animate-presence-radix-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { AnimatePresence, motion } from "framer-motion"
import { useId, useState } from "react"

/**
* Test for AnimatePresence with Radix UI Dialog
* This reproduces issue #3455 where exit animations break
* when using asChild with motion components inside AnimatePresence.
*
* The issue occurs because Radix UI's asChild prop creates new callback refs
* on each render, and when externalRef is in the useMotionRef dependency array,
* this causes the callback to be recreated, triggering remounts that break
* exit animations.
*/
export const App = () => {
const id = useId()
const [isOpen, setIsOpen] = useState(false)
const [exitComplete, setExitComplete] = useState(false)
const [exitStarted, setExitStarted] = useState(false)

return (
<div className="App" style={{ padding: 20 }}>
<style>{`
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
}
.dialog {
position: fixed;
background: white;
padding: 20px;
border-radius: 8px;
width: 300px;
}
`}</style>

<DialogPrimitive.Root onOpenChange={setIsOpen} open={isOpen}>
<DialogPrimitive.Trigger id="trigger">
{isOpen ? "Close" : "Open"}
</DialogPrimitive.Trigger>
<AnimatePresence
onExitComplete={() => setExitComplete(true)}
>
{isOpen ? (
<DialogPrimitive.Portal key={id} forceMount>
<DialogPrimitive.Overlay asChild>
<motion.div
id="overlay"
className="overlay"
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
initial={{ opacity: 0 }}
transition={{ duration: 0.3 }}
onAnimationStart={(definition) => {
if (
definition === "exit" ||
(typeof definition === "object" &&
"opacity" in definition &&
definition.opacity === 0)
) {
setExitStarted(true)
}
}}
/>
</DialogPrimitive.Overlay>

<DialogPrimitive.Content asChild>
<motion.div
id="dialog"
className="dialog"
animate={{
left: "50%",
bottom: "50%",
y: "50%",
x: "-50%",
}}
exit={{
left: "50%",
bottom: 0,
y: "100%",
x: "-50%",
}}
initial={{
left: "50%",
bottom: 0,
y: "100%",
x: "-50%",
}}
transition={{ duration: 0.3 }}
>
<DialogPrimitive.Title>
Dialog Title
</DialogPrimitive.Title>

<DialogPrimitive.Description>
Dialog content here
</DialogPrimitive.Description>

<DialogPrimitive.Close id="close">
Close
</DialogPrimitive.Close>
</motion.div>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
) : null}
</AnimatePresence>
</DialogPrimitive.Root>

<div id="status" style={{ marginTop: 20 }}>
<div id="exit-started" data-value={exitStarted.toString()}>
Exit started: {exitStarted.toString()}
</div>
<div id="exit-complete" data-value={exitComplete.toString()}>
Exit complete: {exitComplete.toString()}
</div>
</div>
</div>
)
}
35 changes: 35 additions & 0 deletions dev/react/src/tests/drag-constraints-return.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { motion } from "framer-motion"
import { useRef } from "react"

export const App = () => {
const constraintsRef = useRef(null)
const params = new URLSearchParams(window.location.search)
const layout = params.get("layout") || undefined

return (
<motion.div
id="constraints"
ref={constraintsRef}
style={{
width: 300,
height: 300,
background: "rgba(0, 0, 255, 0.2)",
}}
>
<motion.div
id="box"
data-testid="draggable"
drag
dragConstraints={constraintsRef}
dragElastic={1}
dragMomentum={false}
layout={layout}
style={{
width: 100,
height: 100,
background: "red",
}}
/>
</motion.div>
)
}
112 changes: 112 additions & 0 deletions dev/react/src/tests/drag-input-propagation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { motion } from "framer-motion"

/**
* Test page for issue #1674: Interactive elements inside draggable elements
* should not trigger drag when clicked/interacted with.
*/
export const App = () => {
return (
<div style={{ padding: 100 }}>
<motion.div
id="draggable"
data-testid="draggable"
drag
dragElastic={0}
dragMomentum={false}
style={{
width: 400,
height: 200,
background: "red",
display: "flex",
flexWrap: "wrap",
alignItems: "center",
justifyContent: "center",
gap: 10,
padding: 10,
}}
>
<input
type="text"
data-testid="input"
defaultValue="Select me"
style={{
width: 80,
height: 30,
padding: 5,
}}
/>
<textarea
data-testid="textarea"
defaultValue="Text"
style={{
width: 60,
height: 30,
padding: 5,
}}
/>
<button
data-testid="button"
style={{
width: 60,
height: 30,
padding: 5,
}}
>
Click
</button>
<a
href="#test"
data-testid="link"
style={{
display: "inline-block",
width: 60,
height: 30,
padding: 5,
background: "white",
}}
>
Link
</a>
<select
data-testid="select"
style={{
width: 80,
height: 30,
}}
>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>
<label
data-testid="label"
style={{
display: "flex",
alignItems: "center",
gap: 5,
background: "white",
padding: 5,
}}
>
<input
type="checkbox"
data-testid="checkbox"
/>
Check
</label>
<div
contentEditable
data-testid="contenteditable"
style={{
width: 80,
height: 30,
padding: 5,
background: "white",
}}
>
Edit me
</div>
</motion.div>
</div>
)
}
Loading
Loading