framer-motion
Version:
An open source and production-ready motion library for React on the web
652 lines (537 loc) • 18.2 kB
text/typescript
import { axisBox } from "../../../utils/geometry"
import { ResolvedValues, VisualElement } from "../../types"
import {
AnimationState,
AnimationType,
createAnimationState,
} from "../animation-state"
import { checkTargetForNewValues, getOrigin } from "../setters"
import { visualElement } from "../../"
import { MotionProps } from "../../../motion"
const stateVisualElement = visualElement<
ResolvedValues,
{},
{ initialState: ResolvedValues }
>({
createRenderState: () => ({}),
build() {},
measureViewportBox: axisBox,
resetTransform() {},
restoreTransform() {},
removeValueFromMutableState() {},
render() {},
scrapeMotionValuesFromProps() {
return {}
},
readValueFromInstance(_state, key, options) {
return options.initialState[key] || 0
},
makeTargetAnimatable(element, { transition, transitionEnd, ...target }) {
const origin = getOrigin(target as any, transition || {}, element)
checkTargetForNewValues(element, target, origin as any)
return { transition, transitionEnd, ...target }
},
})
function createTest(
props: MotionProps = {},
parent: VisualElement<any> | undefined = undefined
): { element: VisualElement; state: any } {
const element = stateVisualElement(
{ props, parent },
{
initialState: {},
}
)
element.animationState = createAnimationState(element)
return {
element: element,
state: {
...element.animationState,
setProps(newProps: any, options: any, type: any): any {
element.setProps(newProps)
return element.animationState?.animateChanges(options, type)
},
},
}
}
function mockAnimate(state: AnimationState) {
const mocked = jest.fn()
state.setAnimateFunction(() => {
return (animations: any) => {
mocked(animations.map(({ animation }: any) => animation))
}
})
return mocked
}
describe("Animation state - Initiating props", () => {
test("Initial animation", () => {
const { state } = createTest()
const animate = mockAnimate(state)
state.setProps({
animate: { opacity: 1 },
})
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
expect(animate).toBeCalledWith([{ opacity: 1 }])
})
test("Initial animation with prop as variant", () => {
const { state } = createTest()
const animate = mockAnimate(state)
state.setProps({
animate: "test",
variants: {
test: { opacity: 1 },
},
})
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
expect(animate).toBeCalledWith(["test"])
})
test("Initial animation with prop as variant list", () => {
const { state } = createTest()
const animate = mockAnimate(state)
state.setProps({
animate: ["test", "heyoo"],
variants: {
test: { opacity: 1 },
},
})
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
expect(animate).toBeCalledWith(["test", "heyoo"])
})
test("Initial animation with prop as inherited variant", () => {
const { element: parent } = createTest({
animate: "test",
})
const { state } = createTest({}, parent)
const animate = mockAnimate(state)
state.setProps({
variants: {
test: { opacity: 1 },
},
})
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
expect(animate).not.toBeCalled()
})
test("Initial animation with initial=false", () => {
const { state } = createTest()
const animate = mockAnimate(state)
state.setProps({
initial: false,
animate: { opacity: 1 },
})
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
expect(animate).not.toBeCalled()
})
test("Initial animation with prop as variant with initial=false", () => {
const { state } = createTest()
const animate = mockAnimate(state)
state.setProps({
initial: false,
animate: "test",
variants: {
test: { opacity: 1 },
},
})
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
expect(animate).not.toBeCalled()
})
test("Initial animation with prop as variant list with initial=false", () => {
const { state } = createTest()
const animate = mockAnimate(state)
state.setProps({
initial: false,
animate: ["test", "heyoo"],
variants: {
test: { opacity: 1 },
},
})
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
expect(animate).not.toBeCalled()
})
})
describe("Animation state - Setting props", () => {
test("No change, target", () => {
const { state } = createTest()
state.setProps({
animate: { opacity: 1 },
})
const animate = mockAnimate(state)
state.setProps({
animate: { opacity: 1 },
})
expect(animate).not.toBeCalled()
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
opacity: true,
})
})
test("No change, target keyframes", () => {
const { state } = createTest()
state.setProps({
animate: { opacity: [0, 1] },
})
const animate = mockAnimate(state)
state.setProps({
animate: { opacity: [0, 1] },
})
expect(animate).not.toBeCalled()
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
opacity: true,
})
})
test("No change, variant", () => {
const { state } = createTest()
state.setProps({
animate: "test",
variants: {
test: { opacity: 1 },
},
})
const animate = mockAnimate(state)
state.setProps({
animate: "test",
variants: {
test: { opacity: 1 },
},
})
expect(animate).not.toBeCalled()
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
opacity: true,
})
})
test("No change, variant list", () => {
const { state } = createTest()
state.setProps({
animate: ["test", "test2"],
variants: {
test: { opacity: 1 },
},
})
const animate = mockAnimate(state)
state.setProps({
animate: ["test", "test2"],
variants: {
test: { opacity: 1 },
},
})
expect(animate).not.toBeCalled()
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
opacity: true,
})
})
test("Change single value, target", () => {
const { state } = createTest()
state.setProps({
animate: { opacity: 1 },
})
const animate = mockAnimate(state)
state.setProps({
animate: { opacity: 0 },
})
expect(animate).toBeCalledWith([{ opacity: 0 }])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
})
test("Change single value, keyframes", () => {
const { state } = createTest()
state.setProps({
animate: { opacity: [0, 1] },
})
const animate = mockAnimate(state)
state.setProps({
animate: { opacity: [0.5, 1] },
})
expect(animate).toBeCalledWith([{ opacity: [0.5, 1] }])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
})
test("Change single value, variant", () => {
const { state } = createTest()
state.setProps({
animate: "a",
variants: {
a: { opacity: 0 },
b: { opacity: 1 },
},
})
const animate = mockAnimate(state)
state.setProps({
animate: "b",
variants: {
a: { opacity: 0 },
b: { opacity: 1 },
},
})
expect(animate).toBeCalledWith(["b"])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
})
test("Change single value, variant list", () => {
const { state } = createTest()
state.setProps({
animate: ["a"],
variants: {
a: { opacity: 0 },
b: { opacity: 1 },
},
})
const animate = mockAnimate(state)
state.setProps({
animate: ["b"],
variants: {
a: { opacity: 0 },
b: { opacity: 1 },
},
})
expect(animate).toBeCalledWith(["b"])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
})
test("Swap between value in target and transitionEnd, target", () => {
const { state } = createTest()
state.setProps({
style: { opacity: 0.1 },
animate: { opacity: 0.2 },
})
let animate = mockAnimate(state)
state.setProps({
style: { opacity: 0.1 },
animate: { transitionEnd: { opacity: 0.3 } },
})
expect(animate).toBeCalledWith([{ transitionEnd: { opacity: 0.3 } }])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
animate = mockAnimate(state)
state.setProps({
style: { opacity: 0.1 },
animate: { opacity: 0.2 },
})
expect(animate).toBeCalledWith([{ opacity: 0.2 }])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
})
test("Change single value, target, with unchanging values", () => {
const { state } = createTest()
state.setProps({
animate: { opacity: 1, x: 0 },
})
let animate = mockAnimate(state)
state.setProps({
animate: { opacity: 0, x: 0 },
})
expect(animate).toBeCalledWith([{ opacity: 0, x: 0 }])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
x: true,
})
animate = mockAnimate(state)
state.setProps({
animate: { opacity: 0, x: 100 },
})
expect(animate).toBeCalledWith([{ opacity: 0, x: 100 }])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual({
opacity: true,
})
})
test("Removing values, target changed", () => {
const { state } = createTest()
state.setProps({
animate: { opacity: 1 },
})
const animate = mockAnimate(state)
state.setProps({
style: { opacity: 0 },
animate: {},
})
expect(animate).toBeCalledWith([{ opacity: 0 }])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
})
test("Removing values, target undefined", () => {
const { state } = createTest()
state.setProps({
animate: { opacity: 1 },
})
const animate = mockAnimate(state)
state.setProps({
style: { opacity: 0 },
animate: undefined,
})
expect(animate).toBeCalledWith([{ opacity: 0 }])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
})
/**
* TODO: Un-skip these and validate before merge
*/
test.skip("Removing values, variant changed", () => {
const { state } = createTest()
state.setProps({
animate: "a",
variants: {
a: { opacity: 0 },
b: { x: 1 },
},
})
const animate = mockAnimate(state)
state.setProps({
style: { opacity: 1 },
animate: "b",
variants: {
a: { opacity: 0 },
b: { x: 1 },
},
})
expect(animate).toBeCalledWith(["b", { opacity: 1 }])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
})
test.skip("Removing values, inherited variant changed", () => {
const { element: parent } = createTest({ animate: "a" })
const { state } = createTest({}, parent)
state.setProps({
variants: {
a: { opacity: 0 },
b: { x: 1 },
},
})
const animate = mockAnimate(state)
parent.setProps({ animate: "b" })
state.setProps({
style: { opacity: 1 },
variants: {
a: { opacity: 0 },
b: { x: 1 },
},
})
expect(animate).toBeCalledWith([{ opacity: 1 }])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
})
})
describe("Animation state - Set active", () => {
test("Change active state while props are the same", () => {
const { state } = createTest()
state.setProps({
style: { opacity: 0 },
animate: { opacity: 1 },
whileHover: { opacity: 0.5 },
whileTap: { opacity: 0.8 },
})
// Set hover to true
let animate = mockAnimate(state)
state.setActive(AnimationType.Hover, true)
expect(animate).toBeCalledWith([{ opacity: 0.5 }])
expect(
state.getState()[AnimationType.Animate].protectedKeys
).toHaveProperty("opacity")
expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})
// Set hover to false
animate = mockAnimate(state)
state.setActive(AnimationType.Hover, false)
expect(animate).toBeCalledWith([{ opacity: 1 }])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})
// Set hover to true
animate = mockAnimate(state)
state.setActive(AnimationType.Hover, true)
expect(animate).toBeCalledWith([{ opacity: 0.5 }])
expect(
state.getState()[AnimationType.Animate].protectedKeys
).toHaveProperty("opacity")
expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})
// Set hover to false
animate = mockAnimate(state)
state.setActive(AnimationType.Hover, false)
expect(animate).toBeCalledWith([{ opacity: 1 }])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})
// Set hover to true
animate = mockAnimate(state)
state.setActive(AnimationType.Hover, true)
expect(animate).toBeCalledWith([{ opacity: 0.5 }])
expect(
state.getState()[AnimationType.Animate].protectedKeys
).toHaveProperty("opacity")
expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})
// Set press to true
animate = mockAnimate(state)
state.setActive(AnimationType.Tap, true)
expect(animate).toBeCalledWith([{ opacity: 0.8 }])
expect(
state.getState()[AnimationType.Animate].protectedKeys
).toHaveProperty("opacity")
expect(
state.getState()[AnimationType.Hover].protectedKeys
).toHaveProperty("opacity")
expect(state.getState()[AnimationType.Tap].protectedKeys).toEqual({})
// Set hover to false
animate = mockAnimate(state)
state.setActive(AnimationType.Hover, false)
expect(
state.getState()[AnimationType.Animate].protectedKeys
).toHaveProperty("opacity")
expect(
state.getState()[AnimationType.Tap].protectedKeys
).toHaveProperty("opacity")
expect(animate).not.toBeCalled()
// Set press to false
animate = mockAnimate(state)
state.setActive(AnimationType.Tap, false)
expect(animate).toBeCalledWith([{ opacity: 1 }])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
})
test("Change active variant where no variants are defined", () => {
/**
* We should still animate back to lower-priority variants in case children need animating to
*/
const { state } = createTest()
state.setProps({
animate: "a",
whileHover: "b",
})
// Set hover to true
let animate = mockAnimate(state)
state.setActive(AnimationType.Hover, true)
expect(animate).toBeCalledWith(["b"])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})
// Set hover to false
animate = mockAnimate(state)
state.setActive(AnimationType.Hover, false)
expect(animate).toBeCalledWith(["a"])
expect(state.getState()[AnimationType.Animate].protectedKeys).toEqual(
{}
)
expect(state.getState()[AnimationType.Hover].protectedKeys).toEqual({})
})
})