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
14 changes: 11 additions & 3 deletions packages/framer-motion/src/components/AnimatePresence/PopChild.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ interface Size {
top: number
left: number
right: number
bottom: number
}

interface Props {
children: React.ReactElement
isPresent: boolean
anchorX?: "left" | "right"
anchorY?: "top" | "bottom"
root?: HTMLElement | ShadowRoot
}

Expand All @@ -39,13 +41,17 @@ class PopChildMeasure extends React.Component<MeasureProps> {
const parentWidth = isHTMLElement(parent)
? parent.offsetWidth || 0
: 0
const parentHeight = isHTMLElement(parent)
? parent.offsetHeight || 0
: 0

const size = this.props.sizeRef.current!
size.height = element.offsetHeight || 0
size.width = element.offsetWidth || 0
size.top = element.offsetTop
size.left = element.offsetLeft
size.right = parentWidth - size.width - size.left
size.bottom = parentHeight - size.height - size.top
}

return null
Expand All @@ -61,7 +67,7 @@ class PopChildMeasure extends React.Component<MeasureProps> {
}
}

export function PopChild({ children, isPresent, anchorX, root }: Props) {
export function PopChild({ children, isPresent, anchorX, anchorY, root }: Props) {
const id = useId()
const ref = useRef<HTMLElement>(null)
const size = useRef<Size>({
Expand All @@ -70,6 +76,7 @@ export function PopChild({ children, isPresent, anchorX, root }: Props) {
top: 0,
left: 0,
right: 0,
bottom: 0,
})
const { nonce } = useContext(MotionConfigContext)
/**
Expand All @@ -91,10 +98,11 @@ export function PopChild({ children, isPresent, anchorX, root }: Props) {
* styles set via the style prop.
*/
useInsertionEffect(() => {
const { width, height, top, left, right } = size.current
const { width, height, top, left, right, bottom } = size.current
if (isPresent || !ref.current || !width || !height) return

const x = anchorX === "left" ? `left: ${left}` : `right: ${right}`
const y = anchorY === "bottom" ? `bottom: ${bottom}` : `top: ${top}`

ref.current.dataset.motionPopId = id

Expand All @@ -111,7 +119,7 @@ export function PopChild({ children, isPresent, anchorX, root }: Props) {
width: ${width}px !important;
height: ${height}px !important;
${x}px !important;
top: ${top}px !important;
${y}px !important;
}
`)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface PresenceChildProps {
presenceAffectsLayout: boolean
mode: "sync" | "popLayout" | "wait"
anchorX?: "left" | "right"
anchorY?: "top" | "bottom"
root?: HTMLElement | ShadowRoot
}

Expand All @@ -31,6 +32,7 @@ export const PresenceChild = ({
presenceAffectsLayout,
mode,
anchorX,
anchorY,
root
}: PresenceChildProps) => {
const presenceChildren = useConstant(newChildrenMap)
Expand Down Expand Up @@ -86,7 +88,7 @@ export const PresenceChild = ({

if (mode === "popLayout") {
children = (
<PopChild isPresent={isPresent} anchorX={anchorX} root={root}>
<PopChild isPresent={isPresent} anchorX={anchorX} anchorY={anchorY} root={root}>
{children}
</PopChild>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,63 @@ describe("AnimatePresence", () => {
const result = await promise
return expect(result).toHaveAttribute("data-id", "2")
})

test("popLayout mode with anchorY='bottom' preserves bottom positioning", async () => {
const ref = createRef<HTMLDivElement>()

const Component = ({ isVisible }: { isVisible: boolean }) => {
return (
<div
style={{
position: "relative",
height: "200px",
width: "200px",
}}
>
<AnimatePresence mode="popLayout" anchorY="bottom">
{isVisible && (
<motion.div
ref={ref}
style={{
position: "absolute",
bottom: 0,
width: "50px",
height: "50px",
}}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
/>
)}
</AnimatePresence>
</div>
)
}

const { rerender } = render(<Component isVisible />)
rerender(<Component isVisible />)

await nextFrame()

// Get initial position (should be at bottom)
const initialBottom =
ref.current!.parentElement!.offsetHeight -
ref.current!.offsetTop -
ref.current!.offsetHeight

await act(async () => {
rerender(<Component isVisible={false} />)
})

await nextFrame()

// After popLayout, element should still be at the same bottom position
// Check that the injected style uses bottom positioning
const computedStyle = window.getComputedStyle(ref.current!)
expect(computedStyle.position).toBe("absolute")

// The bottom position should be preserved (approximately 0)
expect(initialBottom).toBeLessThanOrEqual(1)
})
})

describe("AnimatePresence with custom components", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const AnimatePresence = ({
mode = "sync",
propagate = false,
anchorX = "left",
anchorY = "top",
root
}: React.PropsWithChildren<AnimatePresenceProps>) => {
const [isParentPresent, safeToRemove] = usePresence(propagate)
Expand Down Expand Up @@ -226,6 +227,7 @@ export const AnimatePresence = ({
root={root}
onExitComplete={isPresent ? undefined : onExit}
anchorX={anchorX}
anchorY={anchorY}
>
{child}
</PresenceChild>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,11 @@ export interface AnimatePresenceProps {
* when using `mode="popLayout"`.
*/
anchorX?: "left" | "right"

/**
* Internal. Set whether to anchor the y position of the exiting element to the top or bottom
* when using `mode="popLayout"`. Use `"bottom"` for elements originally positioned with
* `bottom: 0` to prevent them from shifting during exit animations.
*/
anchorY?: "top" | "bottom"
}