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
768 changes: 0 additions & 768 deletions .yarn/releases/yarn-3.1.0.cjs

This file was deleted.

874 changes: 874 additions & 0 deletions .yarn/releases/yarn-3.6.4.cjs

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
yarnPath: .yarn/releases/yarn-3.1.0.cjs
nodeLinker: node-modules
enableGlobalCache: true

nodeLinker: node-modules

yarnPath: .yarn/releases/yarn-3.6.4.cjs
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ test-e2e: test-nextjs test-html test-react test-react-19
yarn test-playwright

test-single: build test-mkdir
yarn start-server-and-test "yarn dev-server" http://localhost:9991 "cd packages/framer-motion && cypress run --config-file=cypress.react-19.json --headed --spec cypress/integration/scroll.ts"
yarn start-server-and-test "yarn dev-server" http://localhost:9991 "cd packages/framer-motion && cypress run --config-file=cypress.react-19.json --headed --spec cypress/integration/layout-group.ts"

lint: bootstrap
yarn lint
Expand Down
11 changes: 11 additions & 0 deletions dev/html/public/projection/sticky-child-scroll-change-offset.html
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@
bottom: 100,
right: 50,
})

// overlayProjection.willUpdate()
// boxProjection.willUpdate()
// overlayProjection.root.didUpdate()

// matchViewportBox(box, {
// top: 0,
// left: 0,
// bottom: 200,
// right: 200,
// })
})
</script>
</body>
Expand Down
114 changes: 114 additions & 0 deletions dev/react/src/tests/layout-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { LayoutGroup, motion, MotionConfig, Transition } from "framer-motion"
import { Fragment, useId, useState } from "react"

const transition: Transition = {
layout: {
type: "tween",
duration: 0.2,
},
}

export function App() {
const [visible, setVisible] = useState(false)
return (
<div
style={{
display: "flex",
justifyContent: "center",
height: "100vh",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 10,
alignItems: "center",
height: "100vh",
width: "500px",
}}
>
{visible && (
<div
style={{
backgroundColor: "green",
width: 100,
height: 100,
}}
/>
)}
<LayoutGroup>
<MotionConfig transition={transition}>
<motion.div id="expander-wrapper" layout="position">
<Expander />
</motion.div>
<motion.div
id="text-wrapper"
layout="position"
style={{
display: "flex",
gap: 4,
alignItems: "center",
}}
>
some text
<LayoutGroup inherit="id">
<Button
onClick={() =>
setVisible((current) => !current)
}
/>
</LayoutGroup>
</motion.div>
</MotionConfig>
</LayoutGroup>
</div>
</div>
)
}

const Variants = motion.create(Fragment)

function Expander() {
const [expanded, setExpanded] = useState(false)
const id = useId()
return (
<Variants>
<motion.div
id="expander"
layoutId={id}
onClick={() => setExpanded((current) => !current)}
style={{
height: expanded ? 100 : 25,
backgroundColor: "red",
marginBottom: 4,
cursor: "pointer",
}}
transition={{ type: "tween" }}
>
{expanded ? "collapse" : "expand"} me
</motion.div>
</Variants>
)
}

function Button({ onClick }: { onClick?: VoidFunction }) {
const id = useId()

return (
<motion.div
id="button"
layoutId={id}
style={{
background: "blue",
color: "white",
borderRadius: 8,
padding: 10,
cursor: "pointer",
}}
onClick={onClick}
>
Add child
</motion.div>
)
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,5 @@
"@types/react-dom": "^19.0.0"
}
},
"packageManager": "yarn@3.1.0"
"packageManager": "yarn@3.6.4"
}
117 changes: 117 additions & 0 deletions packages/framer-motion/cypress/integration/layout-group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
describe(`LayoutGroup inherit="id"`, () => {
it("relative children should not instantly jump to new layout", () => {
cy.viewport(500, 500).visit("?test=layout-group").wait(250)

// Measure initial position
let initialTop: number
cy.get("#button").then(($button) => {
initialTop = Math.round($button[0].getBoundingClientRect().top)
})

// Click expander
cy.get("#expander").click()

// Measure position after 100ms
let top100ms: number
cy.wait(100).then(() => {
cy.get("#button").then(($button) => {
top100ms = Math.round($button[0].getBoundingClientRect().top)
// Should not be in original or final position
expect(top100ms).to.not.equal(104)
expect(top100ms).to.not.equal(initialTop)
})
})

// Measure position after another 200ms for animation to finish
cy.wait(200).then(() => {
cy.get("#button").then(($button) => {
const top200ms = Math.round(
$button[0].getBoundingClientRect().top
)
expect(top200ms).to.equal(104)
expect(top200ms).to.not.equal(initialTop)
expect(top200ms).to.not.equal(top100ms)
})
})
})

it("relative children should not instantly jump to new layout, after performing their own layout animation", () => {
cy.viewport(500, 500).visit("?test=layout-group").wait(250)

// Click button first, then wait 50ms
cy.get("#button").click()
cy.wait(50)

// Measure initial position
let initialTop: number
cy.get("#button").then(($button) => {
initialTop = Math.round($button[0].getBoundingClientRect().top)
})

// Click expander
cy.wait(300).get("#expander").click()

// Measure position after 100ms
let top100ms: number
cy.wait(100).then(() => {
cy.get("#button").then(($button) => {
top100ms = Math.round($button[0].getBoundingClientRect().top)

// Don't be in final or original position
expect(top100ms).to.not.equal(204)
expect(top100ms).to.not.equal(initialTop)
})
})

// Measure position after another 100ms (200ms total)
cy.wait(200).then(() => {
cy.get("#button").then(($button) => {
const top200ms = Math.round(
$button[0].getBoundingClientRect().top
)
// Should not be in either previous measurement
expect(top200ms).to.equal(204)
expect(top200ms).to.not.equal(initialTop)
expect(top200ms).to.not.equal(top100ms)
})
})
})

it("should return to original state when expander is clicked twice with delay", () => {
cy.viewport(500, 500).visit("?test=layout-group").wait(250)

// Measure initial position
let initialTop: number
cy.get("#button").then(($button) => {
initialTop = Math.round($button[0].getBoundingClientRect().top)
})

// Click expander
cy.get("#expander").click()

// Measure position after 100ms
let top100ms: number
cy.wait(100).then(() => {
cy.get("#button").then(($button) => {
top100ms = Math.round($button[0].getBoundingClientRect().top)
// Should not be in original or final position
expect(top100ms).to.not.equal(104)
expect(top100ms).to.not.equal(initialTop)
})
})

// Wait 50ms, then click expander again
cy.wait(50).get("#expander").click()

// Measure position after animation finishes
cy.wait(300).then(() => {
cy.get("#button").then(($button) => {
const finalTop = Math.round(
$button[0].getBoundingClientRect().top
)
// Should be back to original state
expect(finalTop).to.equal(initialTop)
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,8 @@ export function createProjectionNode<I>({

hasTreeAnimated = false

layoutVersion: number = 0

constructor(
latestValues: ResolvedValues = {},
parent: IProjectionNode | undefined = defaultParent?.()
Expand Down Expand Up @@ -893,6 +895,7 @@ export function createProjectionNode<I>({

const prevLayout = this.layout
this.layout = this.measure(false)
this.layoutVersion++
this.layoutCorrected = createBox()
this.isLayoutDirty = false
this.projectionDelta = undefined
Expand Down Expand Up @@ -1185,31 +1188,30 @@ export function createProjectionNode<I>({

this.resolvedRelativeTargetAt = frameData.timestamp

const relativeParent = this.getClosestProjectingParent()

if (
relativeParent &&
this.linkedParentVersion !== relativeParent.layoutVersion &&
!relativeParent.options.layoutRoot
) {
this.removeRelativeTarget()
}

/**
* If we don't have a targetDelta but do have a layout, we can attempt to resolve
* a relativeParent. This will allow a component to perform scale correction
* even if no animation has started.
*/
if (!this.targetDelta && !this.relativeTarget) {
const relativeParent = this.getClosestProjectingParent()
if (
relativeParent &&
relativeParent.layout &&
this.animationProgress !== 1
) {
this.relativeParent = relativeParent
this.forceRelativeParentToResolveTarget()
this.relativeTarget = createBox()
this.relativeTargetOrigin = createBox()
calcRelativePosition(
this.relativeTargetOrigin,
if (relativeParent && relativeParent.layout) {
this.createRelativeTarget(
relativeParent,
this.layout.layoutBox,
relativeParent.layout.layoutBox
)

copyBoxInto(this.relativeTarget, this.relativeTargetOrigin)
} else {
this.relativeParent = this.relativeTarget = undefined
this.removeRelativeTarget()
}
}

Expand Down Expand Up @@ -1268,7 +1270,6 @@ export function createProjectionNode<I>({
*/
if (this.attemptToResolveRelativeTarget) {
this.attemptToResolveRelativeTarget = false
const relativeParent = this.getClosestProjectingParent()

if (
relativeParent &&
Expand All @@ -1278,18 +1279,11 @@ export function createProjectionNode<I>({
relativeParent.target &&
this.animationProgress !== 1
) {
this.relativeParent = relativeParent
this.forceRelativeParentToResolveTarget()
this.relativeTarget = createBox()
this.relativeTargetOrigin = createBox()

calcRelativePosition(
this.relativeTargetOrigin,
this.createRelativeTarget(
relativeParent,
this.target,
relativeParent.target
)

copyBoxInto(this.relativeTarget, this.relativeTargetOrigin)
} else {
this.relativeParent = this.relativeTarget = undefined
}
Expand Down Expand Up @@ -1328,6 +1322,30 @@ export function createProjectionNode<I>({
)
}

linkedParentVersion: number = 0
createRelativeTarget(
relativeParent: IProjectionNode,
layout: Box,
parentLayout: Box
) {
this.relativeParent = relativeParent
this.linkedParentVersion = relativeParent.layoutVersion
this.forceRelativeParentToResolveTarget()
this.relativeTarget = createBox()
this.relativeTargetOrigin = createBox()
calcRelativePosition(
this.relativeTargetOrigin,
layout,
parentLayout
)

copyBoxInto(this.relativeTarget, this.relativeTargetOrigin)
}

removeRelativeTarget() {
this.relativeParent = this.relativeTarget = undefined
}

hasProjected: boolean = false

calcProjection() {
Expand Down
Loading