Skip to content
Open
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
47 changes: 47 additions & 0 deletions dev/react/src/tests/animate-presence-pop-rtl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { AnimatePresence, motion } from "framer-motion"
import { useState } from "react"

export const App = () => {
const [state, setState] = useState(true)

return (
<div dir="rtl">
<div
id="container"
style={{
display: "flex",
width: "fit-content",
position: "relative",
}}
onClick={() => setState(!state)}
>
<AnimatePresence mode="popLayout">
<motion.div
key="a"
id="a"
style={{
width: 100,
height: 100,
backgroundColor: "red",
}}
/>
{state ? (
<motion.div
key="b"
id="b"
exit={{
opacity: 0,
transition: { duration: 10 },
}}
style={{
width: 100,
height: 100,
backgroundColor: "green",
}}
/>
) : null}
</AnimatePresence>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
describe("AnimatePresence popLayout RTL", () => {
it("correctly pops exiting elements in RTL direction without shifting", () => {
let initialLeft: number

cy.visit("?test=animate-presence-pop-rtl")
.wait(50)
.get("#b")
.then(([$b]: any) => {
initialLeft = $b.getBoundingClientRect().left
})
.get("#container")
.trigger("click", 60, 60, { force: true })
.wait(100)
.get("#b")
.should(([$b]: any) => {
const bbox = $b.getBoundingClientRect()
expect(bbox.left).to.equal(initialLeft)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface Size {
left: number
right: number
bottom: number
direction: string
}

interface Props {
Expand Down Expand Up @@ -54,6 +55,7 @@ class PopChildMeasure extends React.Component<MeasureProps> {
size.left = element.offsetLeft
size.right = parentWidth - size.width - size.left
size.bottom = parentHeight - size.height - size.top
size.direction = computedStyle.direction
}

return null
Expand All @@ -79,6 +81,7 @@ export function PopChild({ children, isPresent, anchorX, anchorY, root, pop }: P
left: 0,
right: 0,
bottom: 0,
direction: "ltr",
})
const { nonce } = useContext(MotionConfigContext)
/**
Expand All @@ -100,10 +103,13 @@ export function PopChild({ children, isPresent, anchorX, anchorY, root, pop }: P
* styles set via the style prop.
*/
useInsertionEffect(() => {
const { width, height, top, left, right, bottom } = size.current
const { width, height, top, left, right, bottom, direction } = size.current
if (isPresent || pop === false || !ref.current || !width || !height) return

const x = anchorX === "left" ? `left: ${left}` : `right: ${right}`
const isRTL = direction === "rtl"
const x = anchorX === "left"
? (isRTL ? `right: ${right}` : `left: ${left}`)
: (isRTL ? `left: ${left}` : `right: ${right}`)
Comment on lines +110 to +112
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 RTL positioning for anchorX="right" may still shift

When anchorX is explicitly set to "right" in an RTL fit-content container, the fix now uses left: ${left} (physical left) which is the unstable edge in RTL. In a right-to-left fit-content container, the right edge remains fixed when the container shrinks — so both anchorX="left" and anchorX="right" would need to use right: ${right} to stay visually anchored.

The new behavior:

// anchorX === "right", isRTL === true
`left: ${left}` // left edge is unstable in RTL fit-content → element shifts

The pre-fix behavior for anchorX="right" in RTL happened to be correct:

// anchorX !== "left" → `right: ${right}` (stable edge in RTL fit-content) ✓

This PR only fixes the default anchorX="left" case. If anchorX="right" is used in RTL, the same positional shifting bug can still occur. Consider whether the swap should always use the stable edge (right) in RTL regardless of anchorX, or document that anchorX="right" is unsupported in RTL contexts.

const y = anchorY === "bottom" ? `bottom: ${bottom}` : `top: ${top}`

ref.current.dataset.motionPopId = id
Expand Down