Skip to content

Commit

Permalink
Anchor popLayout x (#3021)
Browse files Browse the repository at this point in the history
* Latest

* Latest
  • Loading branch information
mattgperry authored Jan 23, 2025
1 parent 605d0ae commit fbdccaf
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 8 deletions.
7 changes: 4 additions & 3 deletions dev/react/src/tests/animate-presence-pop.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AnimatePresence, motion, animate } from "framer-motion"
import { useState, useRef, useEffect } from "react"
import { animate, AnimatePresence, motion } from "framer-motion"
import { useEffect, useRef, useState } from "react"
import styled from "styled-components"

const Container = styled.section`
Expand All @@ -19,6 +19,7 @@ export const App = () => {
const [state, setState] = useState(true)
const params = new URLSearchParams(window.location.search)
const position = params.get("position") || ("static" as any)
const anchorX = params.get("anchor-x") || ("left" as any)
const itemStyle =
position === "relative" ? { position, top: 100, left: 100 } : {}

Expand All @@ -33,7 +34,7 @@ export const App = () => {

return (
<Container onClick={() => setState(!state)}>
<AnimatePresence mode="popLayout">
<AnimatePresence anchorX={anchorX} mode="popLayout">
<motion.div
key="a"
id="a"
Expand Down
63 changes: 63 additions & 0 deletions packages/framer-motion/cypress/integration/animate-presence-pop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,67 @@ describe("AnimatePresence popLayout", () => {
})
})
})

it("correctly pops exiting elements out of the DOM when anchorX is set to right", () => {
cy.visit("?test=animate-presence-pop&anchor-x=right")
.wait(50)
.get("#b")
.should(([$a]: any) => {
expectBbox($a, {
top: 200,
left: 100,
width: 100,
height: 100,
})
})
.get("#c")
.should(([$a]: any) => {
expectBbox($a, {
top: 300,
left: 100,
width: 100,
height: 100,
})
})
.trigger("click", 60, 60, { force: true })
.wait(100)
.get("#b")
.should(([$a]: any) => {
expectBbox($a, {
top: 200,
left: 100,
width: 100,
height: 100,
})
})
.get("#c")
.should(([$a]: any) => {
expectBbox($a, {
top: 200,
left: 100,
width: 100,
height: 100,
})
})
.trigger("click", 60, 60, { force: true })
.wait(100)
.get("#b")
.should(([$a]: any) => {
expectBbox($a, {
top: 200,
left: 100,
width: 100,
height: 100,
})
})
.get("#c")
.should(([$a]: any) => {
expectBbox($a, {
top: 300,
left: 100,
width: 100,
height: 100,
})
})
})
})
16 changes: 13 additions & 3 deletions packages/framer-motion/src/components/AnimatePresence/PopChild.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ interface Size {
height: number
top: number
left: number
right: number
}

interface Props {
children: React.ReactElement
isPresent: boolean
anchorX?: "left" | "right"
}

interface MeasureProps extends Props {
Expand All @@ -30,11 +32,16 @@ class PopChildMeasure extends React.Component<MeasureProps> {
getSnapshotBeforeUpdate(prevProps: MeasureProps) {
const element = this.props.childRef.current
if (element && prevProps.isPresent && !this.props.isPresent) {
const parent = element.offsetParent
const parentWidth =
parent instanceof HTMLElement ? parent.offsetWidth || 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
}

return null
Expand All @@ -50,14 +57,15 @@ class PopChildMeasure extends React.Component<MeasureProps> {
}
}

export function PopChild({ children, isPresent }: Props) {
export function PopChild({ children, isPresent, anchorX }: Props) {
const id = useId()
const ref = useRef<HTMLElement>(null)
const size = useRef<Size>({
width: 0,
height: 0,
top: 0,
left: 0,
right: 0,
})
const { nonce } = useContext(MotionConfigContext)

Expand All @@ -71,9 +79,11 @@ export function PopChild({ children, isPresent }: Props) {
* styles set via the style prop.
*/
useInsertionEffect(() => {
const { width, height, top, left } = size.current
const { width, height, top, left, right } = size.current
if (isPresent || !ref.current || !width || !height) return

const x = anchorX === "left" ? `left: ${left}` : `right: ${right}`

ref.current.dataset.motionPopId = id

const style = document.createElement("style")
Expand All @@ -85,8 +95,8 @@ export function PopChild({ children, isPresent }: Props) {
position: absolute !important;
width: ${width}px !important;
height: ${height}px !important;
${x}px !important;
top: ${top}px !important;
left: ${left}px !important;
}
`)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client"

import * as React from "react"
import { useId, useMemo, useCallback } from "react"
import { useCallback, useId, useMemo } from "react"
import {
PresenceContext,
PresenceContextProps,
Expand All @@ -18,6 +18,7 @@ interface PresenceChildProps {
custom?: any
presenceAffectsLayout: boolean
mode: "sync" | "popLayout" | "wait"
anchorX?: "left" | "right"
}

export const PresenceChild = ({
Expand All @@ -28,6 +29,7 @@ export const PresenceChild = ({
custom,
presenceAffectsLayout,
mode,
anchorX,
}: PresenceChildProps) => {
const presenceChildren = useConstant(newChildrenMap)
const id = useId()
Expand Down Expand Up @@ -83,7 +85,11 @@ export const PresenceChild = ({
}, [isPresent])

if (mode === "popLayout") {
children = <PopChild isPresent={isPresent}>{children}</PopChild>
children = (
<PopChild isPresent={isPresent} anchorX={anchorX}>
{children}
</PopChild>
)
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const AnimatePresence = ({
presenceAffectsLayout = true,
mode = "sync",
propagate = false,
anchorX = "left",
}: React.PropsWithChildren<AnimatePresenceProps>) => {
const [isParentPresent, safeToRemove] = usePresence(propagate)

Expand Down Expand Up @@ -211,6 +212,7 @@ export const AnimatePresence = ({
presenceAffectsLayout={presenceAffectsLayout}
mode={mode}
onExitComplete={isPresent ? undefined : onExit}
anchorX={anchorX}
>
{child}
</PresenceChild>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,10 @@ export interface AnimatePresenceProps {
* to its children.
*/
propagate?: boolean

/**
* Internal. Set whether to anchor the x position of the exiting element to the left or right
* when using `mode="popLayout"`.
*/
anchorX?: "left" | "right"
}

0 comments on commit fbdccaf

Please sign in to comment.