Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add layout="size" for size-only layout animations #1154

Merged
merged 1 commit into from Jul 2, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,10 @@

Framer Motion adheres to [Semantic Versioning](http://semver.org/).

### Added

- `layout="size"` for size-only layout animations.

## [4.1.17] 2021-05-17

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion api/framer-motion.api.md
Expand Up @@ -432,7 +432,7 @@ export const LayoutGroupContext: import("react").Context<string | null>;

// @public (undocumented)
export interface LayoutProps {
layout?: boolean | "position";
layout?: boolean | "position" | "size";
layoutId?: string;
// @internal
_layoutResetTransform?: boolean;
Expand Down
72 changes: 72 additions & 0 deletions cypress/integration/layout-shared.ts
Expand Up @@ -95,6 +95,42 @@ describe("AnimateSharedLayout: A -> B transition", () => {
})
})
})

it(`It correctly fires layout="size" animations`, () => {
cy.visit("?test=layout-shared-switch-a-b&type=size")
.wait(50)
.get("#a")
.should(([$box]: any) => {
expectBbox($box, {
top: 0,
left: 0,
width: 100,
height: 200,
})
})
.trigger("click")
.wait(50)
.get("#b")
.should(([$box]: any) => {
expectBbox($box, {
top: 50,
left: 100,
width: 300,
height: 300,
})
})
.trigger("click")
.wait(50)
.get("#a")
.should(([$box]: any) => {
expectBbox($box, {
top: 25,
left: 50,
width: 100,
height: 200,
})
})
})
})

describe("AnimateSharedLayout: A -> AB -> A switch transition", () => {
Expand Down Expand Up @@ -345,6 +381,42 @@ describe("AnimateSharedLayout: A -> B crossfade transition", () => {
})
})
})

it(`It correctly fires layout="size" animations`, () => {
cy.visit("?test=layout-shared-switch-a-b&type=size")
.wait(50)
.get("#a")
.should(([$box]: any) => {
expectBbox($box, {
top: 0,
left: 0,
width: 100,
height: 200,
})
})
.trigger("click")
.wait(50)
.get("#b")
.should(([$box]: any) => {
expectBbox($box, {
top: 50,
left: 100,
width: 300,
height: 300,
})
})
.trigger("click")
.wait(50)
.get("#a")
.should(([$box]: any) => {
expectBbox($box, {
top: 25,
left: 50,
width: 100,
height: 200,
})
})
})
})

describe("AnimateSharedLayout: A -> AB -> A crossfade transition", () => {
Expand Down
24 changes: 24 additions & 0 deletions cypress/integration/layout.ts
Expand Up @@ -69,6 +69,30 @@ describe("Layout animation", () => {
})
})

it(`It correctly fires layout="size" animations`, () => {
cy.visit("?test=layout&type=size")
.wait(50)
.get("#box")
.should(([$box]: any) => {
expectBbox($box, {
top: 0,
left: 0,
width: 100,
height: 200,
})
})
.trigger("click")
.wait(50)
.should(([$box]: any) => {
expectBbox($box, {
top: 50,
left: 100,
width: 300,
height: 300,
})
})
})

it("Doesn't initiate a new animation if the viewport box hasn't updated between renders", () => {
cy.visit("?test=layout-interrupt")
.wait(50)
Expand Down
44 changes: 44 additions & 0 deletions dev/examples/Layout-Projection-scale-size.tsx
@@ -0,0 +1,44 @@
import React from "react"
import { useState } from "react"
import { motion } from "@framer"

const textA = `
It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).
`

const textB = `
It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.
`

export function App() {
const [c, setC] = useState(1)

return (
<div
id="parent"
onClick={() => setC((i) => -1 * i)}
style={{
backgroundColor: "#fff",
padding: 40,
overflow: "hidden",
maxWidth: 500,
position: "absolute",
top: c * 10 + 100,
left: c * 10 + 100,
}}
>
<motion.div
layout="size"
id="child"
transition={transition}
style={{ backgroundColor: "#ccc" }}
>
{c === 1 ? textA : textB}
</motion.div>
</div>
)
}

const transition = {
duration: 3,
}
16 changes: 12 additions & 4 deletions src/motion/features/layout/Animate.tsx
Expand Up @@ -170,13 +170,21 @@ class Animate extends React.Component<AnimateProps> {
const boxHasMoved = hasMoved(origin, target)

const animations = eachAxis((axis) => {
/**
* If layout is set to "position", we can resize the origin box based on the target
* box and only animate its position.
*/
if (layout === "position") {
/**
* If layout is set to "position", we can resize the origin box based on the target
* box and only animate its position.
*/
const targetLength = target[axis].max - target[axis].min
origin[axis].max = origin[axis].min + targetLength
} else if (layout === "size") {
/**
* If layout is set to "size", we move the origin to the target box and only animate
* its length.
*/
const originLength = origin[axis].max - origin[axis].min
origin[axis].min = target[axis].min
origin[axis].max = origin[axis].min + originLength
}

if (visualElement.projection.isTargetLocked) {
Expand Down
5 changes: 3 additions & 2 deletions src/motion/features/layout/types.ts
Expand Up @@ -20,11 +20,12 @@ export interface LayoutProps {
* animated on this component. Otherwise, set them directly via the `initial` prop.
*
* If `layout` is set to `"position"`, the size of the component will change instantly and
* only its position will animate.
* only its position will animate. If `layout` is set to `"size"`, the position of the
* component will change instantly but its size will animate.
*
* @public
*/
layout?: boolean | "position"
layout?: boolean | "position" | "size"

/**
* Enable shared layout transitions between components for children of `AnimateSharedLayout`.
Expand Down