Skip to content

Commit

Permalink
Updating
Browse files Browse the repository at this point in the history
  • Loading branch information
mattgperry committed May 13, 2024
1 parent 366fb52 commit 9dc6e6a
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 4 deletions.
32 changes: 32 additions & 0 deletions dev/examples/Animation-display-visibility.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState } from "react"
import { MotionConfig, motion } from "framer-motion"

/**
* An example of the tween transition type
*/

const style = {
width: 100,
height: 100,
background: "white",
}

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

return (
<MotionConfig transition={{ duration: 1 }}>
<motion.div
initial={{ display: "block" }}
animate={{
display: state ? "block" : "none",
visibility: state ? "visible" : "hidden",
opacity: state ? 1 : 0.2,
}}
onUpdate={console.log}
style={style}
/>
<button onClick={() => setState(!state)}>Toggle</button>
</MotionConfig>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export function canAnimate(
const originKeyframe = keyframes[0]
if (originKeyframe === null) return false

console.log(keyframes, name)
if (name === "display" || name === "visibility") return true

const targetKeyframe = keyframes[keyframes.length - 1]
const isOriginAnimatable = isAnimatable(originKeyframe, name)
const isTargetAnimatable = isAnimatable(targetKeyframe, name)
Expand Down
126 changes: 123 additions & 3 deletions packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
useMotionValue,
useMotionValueEvent,
} from "../../"
import { useRef, createRef } from "react";
import { useRef, createRef } from "react"
import { nextFrame } from "../../gestures/__tests__/utils"

describe("animate prop as object", () => {
Expand Down Expand Up @@ -223,7 +223,7 @@ describe("animate prop as object", () => {
const { container, rerender } = render(<Component />)
rerender(<Component />)
})
expect(promise).resolves.toHaveStyle(
return expect(promise).resolves.toHaveStyle(
"transform: translateX(30px) translateX(30px) translateZ(0)"
)
})
Expand All @@ -242,8 +242,128 @@ describe("animate prop as object", () => {
const { rerender } = render(<Component />)
rerender(<Component />)
})
expect(promise).resolves.toEqual(true)
return expect(promise).resolves.toEqual(true)
})

test("animate display none => block immediately switches to block", async () => {
const promise = new Promise((resolve) => {
const Component = () => {
const display = useMotionValue("block")
let hasChecked = false

return (
<motion.div
initial={{ display: "none", opacity: 0 }}
animate={{ display: "block", opacity: 1 }}
style={{ display }}
transition={{ duration: 0.1 }}
onUpdate={(latest) => {
if (!hasChecked) {
expect(latest.display).toBe("block")
hasChecked = true
}
}}
onAnimationComplete={() => {
resolve([hasChecked, display.get()])
}}
/>
)
}
const { rerender } = render(<Component />)
rerender(<Component />)
})
return expect(promise).resolves.toEqual([true, "block"])
})

test.only("animate display block => none switches to none on animation end", async () => {
const promise = new Promise((resolve) => {
let hasChecked = false
const Component = () => {
const display = useMotionValue("block")

return (
<motion.div
initial={{ display: "block", opacity: 1 }}
animate={{ display: "none", opacity: 0 }}
style={{ display }}
transition={{ duration: 0.1 }}
onUpdate={(latest) => {
if (!hasChecked) {
expect(latest.display).toBe("block")
hasChecked = true
}
}}
onAnimationComplete={() => {
resolve([hasChecked, display.get()])
}}
/>
)
}
render(<Component />)
})
return expect(promise).resolves.toEqual([true, "none"])
})

test("animate visibility hidden => visible immediately switches to visible", async () => {
const promise = new Promise((resolve) => {
const Component = () => {
const visibility = useMotionValue("visible")
let hasChecked = false

return (
<motion.div
initial={{ visibility: "hidden", opacity: 0 }}
animate={{ visibility: "visible", opacity: 1 }}
style={{ visibility }}
transition={{ duration: 0.1 }}
onUpdate={(latest) => {
if (!hasChecked) {
expect(latest.visibility).toBe("visible")
hasChecked = true
}
}}
onAnimationComplete={() => {
resolve([hasChecked, visibility.get()])
}}
/>
)
}
const { rerender } = render(<Component />)
rerender(<Component />)
})
return expect(promise).resolves.toEqual([true, "visible"])
})

test("animate visibility visible => hidden switches to hidden on animation end", async () => {
const promise = new Promise((resolve) => {
const Component = () => {
const visibility = useMotionValue("hidden")
let hasChecked = false

return (
<motion.div
initial={{ visibility: "visible", opacity: 1 }}
animate={{ visibility: "hidden", opacity: 0 }}
style={{ visibility }}
transition={{ duration: 0.1 }}
onUpdate={(latest) => {
if (!hasChecked) {
expect(latest.visibility).toBe("visible")
hasChecked = true
}
}}
onAnimationComplete={() => {
resolve([hasChecked, visibility.get()])
}}
/>
)
}
const { rerender } = render(<Component />)
rerender(<Component />)
})
return expect(promise).resolves.toEqual([true, "hidden"])
})

test("keyframes - accepts ease as an array", async () => {
const promise = new Promise((resolve) => {
const x = motionValue(0)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { analyseComplexValue } from "../../../value/types/complex"
import { getAnimatableNone } from "../../dom/value-types/animatable-none"
import { UnresolvedKeyframes } from "../../utils/KeyframesResolver"

Expand All @@ -18,7 +19,11 @@ export function makeNoneKeyframesAnimatable(
let animatableTemplate: string | undefined = undefined
while (i < unresolvedKeyframes.length && !animatableTemplate) {
const keyframe = unresolvedKeyframes[i]
if (typeof keyframe === "string" && !invalidTemplates.has(keyframe)) {
if (
typeof keyframe === "string" &&
!invalidTemplates.has(keyframe) &&
analyseComplexValue(keyframe).values.length
) {
animatableTemplate = unresolvedKeyframes[i] as string
}
i++
Expand Down
15 changes: 15 additions & 0 deletions packages/framer-motion/src/utils/mix/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,19 @@ describe("mix", () => {
"var(--test-9) 35px"
)
})

test("mixes binary visibility", () => {
expect(mix("visible", "hidden")(0)).toBe("visible")
expect(mix("visible", "hidden")(0.5)).toBe("visible")
expect(mix("visible", "hidden")(1)).toBe("hidden")
expect(mix("hidden", "visible")(0)).toBe("hidden")
expect(mix("hidden", "visible")(0.5)).toBe("visible")
expect(mix("hidden", "visible")(1)).toBe("visible")
expect(mix("block", "none")(0)).toBe("block")
expect(mix("block", "none")(0.5)).toBe("block")
expect(mix("block", "none")(1)).toBe("none")
expect(mix("none", "block")(0)).toBe("none")
expect(mix("none", "block")(0.5)).toBe("block")
expect(mix("none", "block")(1)).toBe("block")
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { mixVisibility } from "../visibility"

describe("mixVisibility", () => {
test("mixes binary visibility", () => {
expect(mixVisibility("visible", "hidden")(0)).toBe("visible")
expect(mixVisibility("visible", "hidden")(0.5)).toBe("visible")
expect(mixVisibility("visible", "hidden")(1)).toBe("hidden")
expect(mixVisibility("hidden", "visible")(0)).toBe("hidden")
expect(mixVisibility("hidden", "visible")(0.5)).toBe("visible")
expect(mixVisibility("hidden", "visible")(1)).toBe("visible")
expect(mixVisibility("block", "none")(0)).toBe("block")
expect(mixVisibility("block", "none")(0.5)).toBe("block")
expect(mixVisibility("block", "none")(1)).toBe("none")
expect(mixVisibility("none", "block")(0)).toBe("none")
expect(mixVisibility("none", "block")(0.5)).toBe("block")
expect(mixVisibility("none", "block")(1)).toBe("block")
})
})
10 changes: 10 additions & 0 deletions packages/framer-motion/src/utils/mix/complex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
complex,
} from "../../value/types/complex"
import { isCSSVariableToken } from "../../render/dom/utils/is-css-variable"
import { invisibleValues, mixVisibility } from "./visibility"

type MixableArray = Array<number | RGBA | HSLA | string>
type MixableObject = {
Expand Down Expand Up @@ -112,6 +113,15 @@ export const mixComplex = (
originStats.indexes.number.length >= targetStats.indexes.number.length

if (canInterpolate) {
if (
(invisibleValues.has(origin as string) &&
!targetStats.values.length) ||
(invisibleValues.has(target as string) &&
!originStats.values.length)
) {
return mixVisibility(origin as string, target as string)
}

return pipe(
mixArray(matchOrder(originStats, targetStats), targetStats.values),
template
Expand Down
14 changes: 14 additions & 0 deletions packages/framer-motion/src/utils/mix/visibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const invisibleValues = new Set(["none", "hidden"])

/**
* Returns a function that, when provided a progress value between 0 and 1,
* will return the "none" or "hidden" string only when the progress is that of
* the origin or target.
*/
export function mixVisibility(origin: string, target: string) {
if (invisibleValues.has(origin)) {
return (p: number) => (p <= 0 ? origin : target)
} else {
return (p: number) => (p >= 1 ? target : origin)
}
}

0 comments on commit 9dc6e6a

Please sign in to comment.