From 7f36d19140070c204919cae2f225d812a6b98433 Mon Sep 17 00:00:00 2001 From: InventingWithMonster Date: Mon, 15 Jul 2019 16:17:54 +0200 Subject: [PATCH 1/5] Fixing variant propagation for newly-added children --- CHANGELOG.md | 2 + src/animation/use-value-animation-controls.ts | 7 +- src/animation/use-variants.ts | 41 +- src/motion/__tests__/variant.test.tsx | 793 +++++++++--------- src/motion/context/MotionContext.ts | 25 +- src/motion/functionality/animation.ts | 4 +- 6 files changed, 463 insertions(+), 409 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb9a23249e..77d07a8772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Framer Motion adheres to [Semantic Versioning](http://semver.org/). - Fixing unit type conversions when non-positional transforms are applied. - Fixing variant propagation via `useAnimation()` when the parent component has no `variants` prop set. - Fixing unsetting `whileHover` and `whileTap` if they contain `transitionEnd` values. +- Child components within variant trees now animate to `animate` as set by their parent. +- Checking animation props for array variants as well as strings. ## [1.2.4] 2019-07-15 diff --git a/src/animation/use-value-animation-controls.ts b/src/animation/use-value-animation-controls.ts index 375a1e78cf..b0fb4eb406 100644 --- a/src/animation/use-value-animation-controls.ts +++ b/src/animation/use-value-animation-controls.ts @@ -30,6 +30,9 @@ export function useValueAnimationControls

( // Reset and resubscribe children every render to ensure stagger order is correct controls.resetChildren() + controls.setProps(props) + controls.setVariants(variants) + controls.setDefaultTransition(transition) if (inheritVariantChanges && parentControls) { parentControls.addChild(controls) @@ -40,9 +43,5 @@ export function useValueAnimationControls

( [] ) - controls.setProps(props) - controls.setVariants(variants) - controls.setDefaultTransition(transition) - return controls } diff --git a/src/animation/use-variants.ts b/src/animation/use-variants.ts index 001ae0317d..95a8936103 100644 --- a/src/animation/use-variants.ts +++ b/src/animation/use-variants.ts @@ -4,7 +4,8 @@ import { resolveVariantLabels, asDependencyList, } from "./utils/variant-resolvers" -import { useEffect, useRef } from "react" +import { useEffect, useRef, useContext } from "react" +import { MotionContext } from "../motion" const hasVariantChanged = (oldVariant: string[], newVariant: string[]) => { return oldVariant.join(",") !== newVariant.join(",") @@ -13,34 +14,38 @@ const hasVariantChanged = (oldVariant: string[], newVariant: string[]) => { /** * Handle variants and the `animate` prop when its set as variant labels. * - * @param targetVariant - * @param inherit - * @param controls - * @param initialVariant + * @param initial - Initial variant(s) + * @param animate - Variant(s) to animate to + * @param inherit - `true` is inheriting animations from parent + * @param controls - Animation controls * * @internal */ export function useVariants( - targetVariant: VariantLabels, + initial: VariantLabels, + animate: VariantLabels, inherit: boolean, - controls: ValueAnimationControls, - initialVariant: VariantLabels + controls: ValueAnimationControls ) { - const variantList = resolveVariantLabels(targetVariant) + const targetVariants = resolveVariantLabels(animate) + const context = useContext(MotionContext) + // const parentAlreadyMounted = + // context.hasMounted && context.hasMounted.current const hasMounted = useRef(false) - // Fire animations when poses change + console.log(inherit, animate, context.hasMounted) useEffect(() => { - // TODO: This logic might mean we don't need to load this hook at all - if (inherit) return + let shouldAnimate = false - if ( - hasMounted.current || - hasVariantChanged(resolveVariantLabels(initialVariant), variantList) - ) { - controls.start(variantList) + if (inherit) { + } else { + shouldAnimate = + hasMounted.current || + hasVariantChanged(resolveVariantLabels(initial), targetVariants) } + shouldAnimate && controls.start(targetVariants) + hasMounted.current = true - }, asDependencyList(variantList)) + }, asDependencyList(targetVariants)) } diff --git a/src/motion/__tests__/variant.test.tsx b/src/motion/__tests__/variant.test.tsx index 844e8c6bb4..995052a735 100644 --- a/src/motion/__tests__/variant.test.tsx +++ b/src/motion/__tests__/variant.test.tsx @@ -10,396 +10,425 @@ describe("animate prop as variant", () => { hidden: { opacity: 0, x: -100, transition: { type: false } }, visible: { opacity: 1, x: 100, transition: { type: false } }, } - const childVariants: Variants = { - hidden: { opacity: 0, x: -100, transition: { type: false } }, - visible: { opacity: 1, x: 50, transition: { type: false } }, - } - - test("animates to set variant", async () => { - const promise = new Promise(resolve => { - const x = motionValue(0) - const onComplete = () => resolve(x.get()) - const { rerender } = render( - - ) - rerender( - - ) - }) - - return expect(promise).resolves.toBe(100) - }) - - test("child animates to set variant", async () => { - const promise = new Promise(resolve => { - const x = motionValue(0) - const onComplete = () => resolve(x.get()) - const Component = () => ( - - - - ) - - const { rerender } = render() - rerender() - }) - - return expect(promise).resolves.toBe(50) - }) - - test("child animates to set variant even if variants are not found on parent", async () => { - const promise = new Promise(resolve => { - const x = motionValue(0) - const onComplete = () => resolve(x.get()) - const Component = () => ( - - - - ) - - const { rerender } = render() - rerender() - }) - - return expect(promise).resolves.toBe(50) - }) - - test("applies applyOnEnd if set on initial", () => { - const variants: Variants = { - visible: { - background: "#f00", - transitionEnd: { display: "none" }, - }, - } - - const { container } = render( - - ) - expect(container.firstChild).toHaveStyle("display: none") - }) - - test("applies applyOnEnd and end of animation", async () => { - const promise = new Promise(resolve => { - const variants: Variants = { - hidden: { background: "#00f" }, - visible: { - background: "#f00", - transitionEnd: { display: "none" }, - }, - } - const display = motionValue("block") - const onComplete = () => resolve(display.get()) - const Component = () => ( - - ) - - const { rerender } = render() - rerender() - }) - - return expect(promise).resolves.toBe("none") - }) - - test("accepts custom transition", async () => { - const promise = new Promise(resolve => { - const variants: Variants = { - hidden: { background: "#00f" }, - visible: { - background: "#f00", - transition: { to: "#555" }, - }, - } - const background = motionValue("#00f") - const onComplete = () => resolve(background.get()) - const Component = () => ( - - ) - - const { rerender } = render() - rerender() - }) - - return expect(promise).resolves.toBe("rgba(85, 85, 85, 1)") - }) - - test("respects orchestration props in transition prop", async () => { - const promise = new Promise(resolve => { - const opacity = motionValue(0) - const variants: Variants = { - visible: { - opacity: 1, - }, - hidden: { - opacity: 0, - }, - } - - render( - + // const childVariants: Variants = { + // hidden: { opacity: 0, x: -100, transition: { type: false } }, + // visible: { opacity: 1, x: 50, transition: { type: false } }, + // } + + // test("animates to set variant", async () => { + // const promise = new Promise(resolve => { + // const x = motionValue(0) + // const onComplete = () => resolve(x.get()) + // const { rerender } = render( + // + // ) + // rerender( + // + // ) + // }) + + // return expect(promise).resolves.toBe(100) + // }) + + // test("child animates to set variant", async () => { + // const promise = new Promise(resolve => { + // const x = motionValue(0) + // const onComplete = () => resolve(x.get()) + // const Component = () => ( + // + // + // + // ) + + // const { rerender } = render() + // rerender() + // }) + + // return expect(promise).resolves.toBe(50) + // }) + + // test("child animates to set variant even if variants are not found on parent", async () => { + // const promise = new Promise(resolve => { + // const x = motionValue(0) + // const onComplete = () => resolve(x.get()) + // const Component = () => ( + // + // + // + // ) + + // const { rerender } = render() + // rerender() + // }) + + // return expect(promise).resolves.toBe(50) + // }) + + // test("applies applyOnEnd if set on initial", () => { + // const variants: Variants = { + // visible: { + // background: "#f00", + // transitionEnd: { display: "none" }, + // }, + // } + + // const { container } = render( + // + // ) + // expect(container.firstChild).toHaveStyle("display: none") + // }) + + // test("applies applyOnEnd and end of animation", async () => { + // const promise = new Promise(resolve => { + // const variants: Variants = { + // hidden: { background: "#00f" }, + // visible: { + // background: "#f00", + // transitionEnd: { display: "none" }, + // }, + // } + // const display = motionValue("block") + // const onComplete = () => resolve(display.get()) + // const Component = () => ( + // + // ) + + // const { rerender } = render() + // rerender() + // }) + + // return expect(promise).resolves.toBe("none") + // }) + + // test("accepts custom transition", async () => { + // const promise = new Promise(resolve => { + // const variants: Variants = { + // hidden: { background: "#00f" }, + // visible: { + // background: "#f00", + // transition: { to: "#555" }, + // }, + // } + // const background = motionValue("#00f") + // const onComplete = () => resolve(background.get()) + // const Component = () => ( + // + // ) + + // const { rerender } = render() + // rerender() + // }) + + // return expect(promise).resolves.toBe("rgba(85, 85, 85, 1)") + // }) + + // test("respects orchestration props in transition prop", async () => { + // const promise = new Promise(resolve => { + // const opacity = motionValue(0) + // const variants: Variants = { + // visible: { + // opacity: 1, + // }, + // hidden: { + // opacity: 0, + // }, + // } + + // render( + // + // + // + // ) + + // requestAnimationFrame(() => resolve(opacity.get())) + // }) + + // return expect(promise).resolves.toBe(0) + // }) + + // test("propagates through components with no `animate` prop", async () => { + // const promise = new Promise(resolve => { + // const opacity = motionValue(0) + // const variants: Variants = { + // visible: { + // opacity: 1, + // }, + // } + + // render( + // + // + // + // + // + // ) + + // requestAnimationFrame(() => resolve(opacity.get())) + // }) + + // return expect(promise).resolves.toBe(1) + // }) + + // test("components without variants are transparent to stagger order", async () => { + // const [recordedOrder, staggeredEqually] = await new Promise(resolve => { + // const order: number[] = [] + // const delayedBy: number[] = [] + // const staggerDuration = 0.1 + + // const updateDelayedBy = (i: number) => { + // if (delayedBy[i]) return + // delayedBy[i] = performance.now() + // } + + // // Checking a rough equidistance between stagger times allows us to see + // // if any of the supposedly invisible interim `motion.div`s were considered part of the + // // stagger order (which would mess up the timings) + // const checkStaggerEquidistance = () => { + // let isEquidistant = true + // let prev = 0 + // for (let i = 0; i < delayedBy.length; i++) { + // if (prev) { + // const timeSincePrev = prev - delayedBy[i] + // if ( + // Math.round(timeSincePrev / 100) * 100 !== + // staggerDuration * 1000 + // ) { + // isEquidistant = false + // } + // } + // prev = delayedBy[i] + // } + + // return isEquidistant + // } + + // const parentVariants: Variants = { + // visible: { + // transition: { + // staggerChildren: staggerDuration, + // staggerDirection: -1, + // }, + // }, + // } + + // const variants: Variants = { + // hidden: { opacity: 0 }, + // visible: { + // opacity: 1, + // transition: { + // duration: 0.01, + // }, + // }, + // } + + // render( + // + // requestAnimationFrame(() => + // resolve([order, checkStaggerEquidistance()]) + // ) + // } + // > + // + // + // { + // updateDelayedBy(0) + // order.push(1) + // }} + // /> + // { + // updateDelayedBy(1) + // order.push(2) + // }} + // /> + // + // + // { + // updateDelayedBy(2) + // order.push(3) + // }} + // /> + // { + // updateDelayedBy(3) + // order.push(4) + // }} + // /> + // + // + // ) + // }) + + // expect(recordedOrder).toEqual([4, 3, 2, 1]) + // expect(staggeredEqually).toEqual(true) + // }) + + // test("onUpdate", async () => { + // const promise = new Promise(resolve => { + // let latest = {} + + // const onUpdate = (l: { [key: string]: number | string }) => { + // latest = l + // } + + // const Component = () => ( + // resolve(latest)} + // /> + // ) + + // const { rerender } = render() + // rerender() + // }) + + // return expect(promise).resolves.toEqual({ x: 100, y: 100 }) + // }) + + // test("onUpdate doesnt fire if no values have changed", async () => { + // const onUpdate = jest.fn() + + // await new Promise(resolve => { + // const x = motionValue(0) + // const Component = ({ xTarget = 0 }) => ( + // + // ) + + // const { rerender } = render() + // setTimeout(() => rerender(), 30) + // setTimeout(() => rerender(), 60) + // setTimeout(() => resolve(), 90) + // }) + + // expect(onUpdate).toHaveBeenCalledTimes(1) + // }) + + // test("accepts variants without being typed", () => { + // expect(() => { + // const variants = { + // withoutTransition: { opacity: 0 }, + // withJustDefaultTransitionType: { + // opacity: 0, + // transition: { + // duration: 1, + // }, + // }, + // withTransitionIndividual: { + // transition: { + // when: "beforeChildren", + // opacity: { type: "spring" }, + // }, + // }, + // withTransitionType: { + // transition: { + // type: "spring", + // }, + // }, + // asResolver: () => ({ + // opacity: 0, + // transition: { + // type: "physics", + // delay: 10, + // }, + // }), + // withTransitionEnd: { + // transitionEnd: { opacity: 0 }, + // }, + // } + // render() + // }).not.toThrowError() + // }) + + test("new child items animate from initial to animate", () => { + const x = motionValue(0) + const Component = ({ length }: { length: number }) => { + const items = [] + for (let i = 0; i < length; i++) { + items.push( - - ) - - requestAnimationFrame(() => resolve(opacity.get())) - }) - - return expect(promise).resolves.toBe(0) - }) - - test("propagates through components with no `animate` prop", async () => { - const promise = new Promise(resolve => { - const opacity = motionValue(0) - const variants: Variants = { - visible: { - opacity: 1, - }, + ) } - render( - - - - + return ( + + {items} ) + } - requestAnimationFrame(() => resolve(opacity.get())) - }) - - return expect(promise).resolves.toBe(1) - }) - - test("components without variants are transparent to stagger order", async () => { - const [recordedOrder, staggeredEqually] = await new Promise(resolve => { - const order: number[] = [] - const delayedBy: number[] = [] - const staggerDuration = 0.1 - - const updateDelayedBy = (i: number) => { - if (delayedBy[i]) return - delayedBy[i] = performance.now() - } - - // Checking a rough equidistance between stagger times allows us to see - // if any of the supposedly invisible interim `motion.div`s were considered part of the - // stagger order (which would mess up the timings) - const checkStaggerEquidistance = () => { - let isEquidistant = true - let prev = 0 - for (let i = 0; i < delayedBy.length; i++) { - if (prev) { - const timeSincePrev = prev - delayedBy[i] - if ( - Math.round(timeSincePrev / 100) * 100 !== - staggerDuration * 1000 - ) { - isEquidistant = false - } - } - prev = delayedBy[i] - } - - return isEquidistant - } - - const parentVariants: Variants = { - visible: { - transition: { - staggerChildren: staggerDuration, - staggerDirection: -1, - }, - }, - } - - const variants: Variants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - duration: 0.01, - }, - }, - } - - render( - - requestAnimationFrame(() => - resolve([order, checkStaggerEquidistance()]) - ) - } - > - - - { - updateDelayedBy(0) - order.push(1) - }} - /> - { - updateDelayedBy(1) - order.push(2) - }} - /> - - - { - updateDelayedBy(2) - order.push(3) - }} - /> - { - updateDelayedBy(3) - order.push(4) - }} - /> - - - ) - }) - - expect(recordedOrder).toEqual([4, 3, 2, 1]) - expect(staggeredEqually).toEqual(true) - }) - - test("onUpdate", async () => { - const promise = new Promise(resolve => { - let latest = {} - - const onUpdate = (l: { [key: string]: number | string }) => { - latest = l - } - - const Component = () => ( - resolve(latest)} - /> - ) - - const { rerender } = render() - rerender() - }) - - return expect(promise).resolves.toEqual({ x: 100, y: 100 }) - }) - - test("onUpdate doesnt fire if no values have changed", async () => { - const onUpdate = jest.fn() - - await new Promise(resolve => { - const x = motionValue(0) - const Component = ({ xTarget = 0 }) => ( - - ) - - const { rerender } = render() - setTimeout(() => rerender(), 30) - setTimeout(() => rerender(), 60) - setTimeout(() => resolve(), 90) - }) - - expect(onUpdate).toHaveBeenCalledTimes(1) - }) + const { rerender } = render() + rerender() + rerender() + rerender() - test("accepts variants without being typed", () => { - expect(() => { - const variants = { - withoutTransition: { opacity: 0 }, - withJustDefaultTransitionType: { - opacity: 0, - transition: { - duration: 1, - }, - }, - withTransitionIndividual: { - transition: { - when: "beforeChildren", - opacity: { type: "spring" }, - }, - }, - withTransitionType: { - transition: { - type: "spring", - }, - }, - asResolver: () => ({ - opacity: 0, - transition: { - type: "physics", - delay: 10, - }, - }), - withTransitionEnd: { - transitionEnd: { opacity: 0 }, - }, - } - render() - }).not.toThrowError() + expect(x.get()).toBe(100) }) }) diff --git a/src/motion/context/MotionContext.ts b/src/motion/context/MotionContext.ts index a80ca4d375..1a77ead76a 100644 --- a/src/motion/context/MotionContext.ts +++ b/src/motion/context/MotionContext.ts @@ -1,14 +1,17 @@ import * as React from "react" +import { useMemo, useRef, useEffect, RefObject } from "react" import { ValueAnimationControls } from "../../animation/ValueAnimationControls" import { VariantLabels, MotionProps } from "../types" import { useMaxTimes } from "../../utils/use-max-times" import { AnimationControls } from "../../animation/AnimationControls" -import { Target } from "types" +import { Target } from "../../types" type MotionContextProps = { controls?: ValueAnimationControls initial?: false | VariantLabels + animate?: VariantLabels static?: boolean + hasMounted?: RefObject } /** @@ -47,11 +50,15 @@ export const useMotionContext = ( initialState = initial } + // Track mounted status so children can detect whether they were present during their + // parent's first render + const hasMounted = useRef(false) + // We propagate this component's ValueAnimationControls *if* we're being provided variants, // if we're being used to control variants, or if we're being passed animation controls. // Otherwise this component should be "invisible" to variant propagation. This is a slight concession // to Framer X where every `Frame` is a `motion` component and it might be if we change that in the future - // that this restruction is remvoed. + // that this restriction is removed. const shouldPropagateControls = variants || isVariantLabel(animate) || @@ -64,6 +71,12 @@ export const useMotionContext = ( ? initialState : parentContext.initial + // If this is a variant tree we need to propagate the `animate` prop in case new children are added after + // the tree initially animates. + const targetAnimate = isVariantLabel(animate) + ? animate + : parentContext.animate + // Only allow `initial` to trigger context re-renders if this is a `static` component (ie we're on the Framer canvas) // or in another non-animation/interaction environment. const initialDependency = isStatic ? targetInitial : null @@ -77,12 +90,14 @@ export const useMotionContext = ( // The context to provide to the child. We `useMemo` because although `controls` and `initial` are // unlikely to change, by making the context an object it'll be considered a new value every render. // So all child motion components will re-render as a result. - const context: MotionContextProps = React.useMemo( + const context: MotionContextProps = useMemo( () => ({ controls: shouldPropagateControls ? controls : parentContext.controls, initial: targetInitial, + animate: targetAnimate, + hasMounted, }), [initialDependency, animateDependency] ) @@ -100,5 +115,9 @@ export const useMotionContext = ( isStatic ? Infinity : 1 ) + useEffect(() => { + hasMounted.current = true + }, []) + return context } diff --git a/src/motion/functionality/animation.ts b/src/motion/functionality/animation.ts index b90550f3fb..8397fc8e75 100644 --- a/src/motion/functionality/animation.ts +++ b/src/motion/functionality/animation.ts @@ -40,10 +40,10 @@ export const AnimatePropComponents = { initial, }: AnimationFunctionalProps) => { return useVariants( + initial as VariantLabels, animate as VariantLabels, inherit, - controls, - initial as VariantLabels + controls ) } ), From 4530334ba7b94f6846cc398485dd1aef0004c711 Mon Sep 17 00:00:00 2001 From: InventingWithMonster Date: Mon, 15 Jul 2019 16:39:28 +0200 Subject: [PATCH 2/5] refactoring functionality component loading strategy --- src/animation/use-value-animation-controls.ts | 6 +-- src/motion/component.tsx | 47 ++++++++++--------- src/motion/functionality/dom.tsx | 21 +++++++++ src/motion/functionality/types.ts | 12 ++++- 4 files changed, 59 insertions(+), 27 deletions(-) diff --git a/src/animation/use-value-animation-controls.ts b/src/animation/use-value-animation-controls.ts index b0fb4eb406..f6ba1b77b5 100644 --- a/src/animation/use-value-animation-controls.ts +++ b/src/animation/use-value-animation-controls.ts @@ -15,14 +15,14 @@ import { useConstant } from "../utils/use-constant" * @param values * @param props * @param ref - * @param inheritVariantChanges + * @param subscribeToParentControls * * @internal */ export function useValueAnimationControls

( config: ValueAnimationConfig, props: P & MotionProps, - inheritVariantChanges: boolean + subscribeToParentControls: boolean ) { const { variants, transition } = props const parentControls = useContext(MotionContext).controls @@ -34,7 +34,7 @@ export function useValueAnimationControls

( controls.setVariants(variants) controls.setDefaultTransition(transition) - if (inheritVariantChanges && parentControls) { + if (subscribeToParentControls && parentControls) { parentControls.addChild(controls) } diff --git a/src/motion/component.tsx b/src/motion/component.tsx index e666a3420d..20787ea240 100644 --- a/src/motion/component.tsx +++ b/src/motion/component.tsx @@ -10,15 +10,18 @@ import { useMotionStyles } from "./utils/use-styles" import { useValueAnimationControls } from "../animation/use-value-animation-controls" import { MotionContext, useMotionContext } from "./context/MotionContext" import { MotionProps } from "./types" -import { UseFunctionalityComponents } from "./functionality/types" +import { + UseFunctionalityComponents, + UseRenderComponent, +} from "./functionality/types" import { checkShouldInheritVariant } from "./utils/should-inherit-variant" -import { getAnimateComponent } from "./functionality/animation" import { ValueAnimationConfig } from "../animation/ValueAnimationControls" import { useConstant } from "../utils/use-constant" export { MotionProps } export interface MotionComponentConfig { useFunctionalityComponents: UseFunctionalityComponents + useRenderComponent: UseRenderComponent getValueControlsConfig: ( ref: RefObject, values: MotionValuesMap @@ -29,8 +32,9 @@ export interface MotionComponentConfig { * @internal */ export const createMotionComponent =

({ - useFunctionalityComponents, getValueControlsConfig, + useFunctionalityComponents, + useRenderComponent, }: MotionComponentConfig) => { function MotionComponent( props: P & MotionProps, @@ -55,44 +59,43 @@ export const createMotionComponent =

({ props, shouldInheritVariant ) + const context = useMotionContext( parentContext, controls, isStatic, props ) - // Add functionality - const Animate = getAnimateComponent(props, context.static) - const handleAnimate = Animate && ( - - ) - - const handleActiveFunctionality = useFunctionalityComponents( - props, + const functionality = useFunctionalityComponents( + ref, + style, values, + props, controls, + isStatic + ) + + const renderComponent = useRenderComponent( ref, style, - context.static + values, + props, + isStatic ) return ( - + <> - {handleAnimate} - {handleActiveFunctionality} - + {functionality} + + {renderComponent} + + ) } diff --git a/src/motion/functionality/dom.tsx b/src/motion/functionality/dom.tsx index 57459c645e..9385f111fa 100644 --- a/src/motion/functionality/dom.tsx +++ b/src/motion/functionality/dom.tsx @@ -107,6 +107,27 @@ export function createDomMotionConfig

( ) => { const activeComponents: JSX.Element[] = [] + // const Animate = getAnimateComponent(props, context.static) + + // const handleAnimate = Animate && ( + // + // ) + + // const handleActiveFunctionality = useFunctionalityComponents( + // props, + // values, + // controls, + // ref, + // style, + // context.static + // ) + // TODO: Refactor the following loading strategy into something more dynamic // This is also a good target for filesize reduction by making these present externally. // It might be possible to code-split these out and useState to re-render children when the diff --git a/src/motion/functionality/types.ts b/src/motion/functionality/types.ts index 5c18e8e5ab..56ceedf038 100644 --- a/src/motion/functionality/types.ts +++ b/src/motion/functionality/types.ts @@ -15,10 +15,18 @@ export interface FunctionalComponentDefinition { } export type UseFunctionalityComponents

= ( - props: P & MotionProps, + ref: RefObject, + style: CSSProperties, values: MotionValuesMap, + props: P & MotionProps, controls: ValueAnimationControls

, + isStatic?: boolean +) => ReactElement[] + +export type UseRenderComponent

= ( ref: RefObject, style: CSSProperties, + values: MotionValuesMap, + props: P, isStatic?: boolean -) => ReactElement

[] +) => ReactElement From 8de5200595b2f34d9a8796e091aba7eec62365dc Mon Sep 17 00:00:00 2001 From: InventingWithMonster Date: Tue, 16 Jul 2019 10:13:23 +0200 Subject: [PATCH 3/5] Latest refactor --- src/motion/component.tsx | 21 ++-- src/motion/functionality/animation.ts | 11 +- src/motion/functionality/dom.tsx | 159 +++++++++++++------------- src/motion/functionality/types.ts | 7 +- 4 files changed, 102 insertions(+), 96 deletions(-) diff --git a/src/motion/component.tsx b/src/motion/component.tsx index 20787ea240..70bcafe902 100644 --- a/src/motion/component.tsx +++ b/src/motion/component.tsx @@ -11,8 +11,8 @@ import { useValueAnimationControls } from "../animation/use-value-animation-cont import { MotionContext, useMotionContext } from "./context/MotionContext" import { MotionProps } from "./types" import { - UseFunctionalityComponents, - UseRenderComponent, + LoadFunctionalityComponents, + RenderComponent, } from "./functionality/types" import { checkShouldInheritVariant } from "./utils/should-inherit-variant" import { ValueAnimationConfig } from "../animation/ValueAnimationControls" @@ -20,8 +20,8 @@ import { useConstant } from "../utils/use-constant" export { MotionProps } export interface MotionComponentConfig { - useFunctionalityComponents: UseFunctionalityComponents - useRenderComponent: UseRenderComponent + loadFunctionalityComponents: LoadFunctionalityComponents + renderComponent: RenderComponent getValueControlsConfig: ( ref: RefObject, values: MotionValuesMap @@ -33,8 +33,8 @@ export interface MotionComponentConfig { */ export const createMotionComponent =

({ getValueControlsConfig, - useFunctionalityComponents, - useRenderComponent, + loadFunctionalityComponents, + renderComponent, }: MotionComponentConfig) => { function MotionComponent( props: P & MotionProps, @@ -67,16 +67,15 @@ export const createMotionComponent =

({ props ) - const functionality = useFunctionalityComponents( - ref, - style, + const functionality = loadFunctionalityComponents( values, props, controls, + shouldInheritVariant, isStatic ) - const renderComponent = useRenderComponent( + const renderedComponent = renderComponent( ref, style, values, @@ -93,7 +92,7 @@ export const createMotionComponent =

({ /> {functionality} - {renderComponent} + {renderedComponent} ) diff --git a/src/motion/functionality/animation.ts b/src/motion/functionality/animation.ts index 8397fc8e75..a94680edce 100644 --- a/src/motion/functionality/animation.ts +++ b/src/motion/functionality/animation.ts @@ -1,4 +1,4 @@ -import { ComponentType, RefObject } from "react" +import { ComponentType } from "react" import { MotionProps, AnimatePropType, VariantLabels } from "../types" import { makeHookComponent } from "../utils/make-hook-component" import { useAnimateProp } from "../../animation/use-animate-prop" @@ -9,10 +9,13 @@ import { ValueAnimationControls } from "../../animation/ValueAnimationControls" import { MotionValuesMap } from "../../motion/utils/use-motion-values" import { TargetAndTransition } from "../../types" -interface AnimationFunctionalProps extends MotionProps { +interface AnimationFunctionalProps { + initial: MotionProps["initial"] + animate: MotionProps["animate"] + transition: MotionProps["transition"] + variants: MotionProps["variants"] controls: ValueAnimationControls values: MotionValuesMap - innerRef: RefObject inherit: boolean } @@ -82,7 +85,7 @@ const animatePropTypeTests = { [AnimatePropType.AnimationSubscription]: isAnimationSubscription, } -export const getAnimateComponent = ( +export const getAnimationComponent = ( props: MotionProps, isStatic: boolean = false ): ComponentType | undefined => { diff --git a/src/motion/functionality/dom.tsx b/src/motion/functionality/dom.tsx index 9385f111fa..3093157f63 100644 --- a/src/motion/functionality/dom.tsx +++ b/src/motion/functionality/dom.tsx @@ -14,6 +14,7 @@ import { MotionValuesMap } from "../../motion/utils/use-motion-values" import { resolveCurrent } from "../../value/utils/resolve-values" import { Position } from "./position" import { isValidMotionProp } from "../utils/valid-prop" +import { getAnimationComponent } from "./animation" type RenderProps = FunctionalProps & { componentProps: MotionProps @@ -51,33 +52,20 @@ export function createDomMotionConfig

( const isDOM = typeof Component === "string" const isSVG = isDOM && svgElements.indexOf(Component as any) !== -1 - /** - * Create a component that renders the DOM element. This step of indirection - * could probably be removed at this point, and `createElement` could be moved - * to the `activeComponents.push`. - */ - const RenderComponent = ({ - innerRef, - style, - values, - isStatic, - componentProps, - }: RenderProps) => { - const forwardProps = isDOM - ? stripMotionProps(componentProps) - : componentProps - const staticVisualStyles = isSVG - ? buildSVGProps(values, style) - : { style: buildStyleAttr(values, style, isStatic) } - - return createElement(Component, { - ...forwardProps, - ref: innerRef, - ...staticVisualStyles, - }) - } - return { + renderComponent: (ref, style, values, props, isStatic) => { + const forwardProps = isDOM ? stripMotionProps(props) : props + const staticVisualStyles = isSVG + ? buildSVGProps(values, style) + : { style: buildStyleAttr(values, style, isStatic) } + + return createElement(Component, { + ...forwardProps, + ref, + ...staticVisualStyles, + }) + }, + /** * The useFunctionalityComponents hook gets used by the `motion` component * @@ -97,16 +85,33 @@ export function createDomMotionConfig

( * 1) User-defined prop typing (extending `P`) * 2) User-defined "clean props" function that removes their plugin's props before being passed to the DOM. */ - useFunctionalityComponents: ( - props, - values, - controls, + loadFunctionalityComponents: ( ref, style, + values, + props, + controls, + inherit, isStatic ) => { const activeComponents: JSX.Element[] = [] + const Animation = getAnimationComponent(props, isStatic) + + if (Animation) { + activeComponents.push( + + ) + } + // const Animate = getAnimateComponent(props, context.static) // const handleAnimate = Animate && ( @@ -133,53 +138,53 @@ export function createDomMotionConfig

( // It might be possible to code-split these out and useState to re-render children when the // functionality within becomes available, or Suspense. - if (!isStatic && Position.shouldRender(props)) { - activeComponents.push( - - ) - } - - if (!isStatic && Drag.shouldRender(props)) { - activeComponents.push( - - ) - } - - if (!isStatic && Gestures.shouldRender(props)) { - activeComponents.push( - - ) - } - - activeComponents.push( - - ) + // if (!isStatic && Position.shouldRender(props)) { + // activeComponents.push( + // + // ) + // } + + // if (!isStatic && Drag.shouldRender(props)) { + // activeComponents.push( + // + // ) + // } + + // if (!isStatic && Gestures.shouldRender(props)) { + // activeComponents.push( + // + // ) + // } + + // activeComponents.push( + // + // ) return activeComponents }, diff --git a/src/motion/functionality/types.ts b/src/motion/functionality/types.ts index 56ceedf038..730ad67e55 100644 --- a/src/motion/functionality/types.ts +++ b/src/motion/functionality/types.ts @@ -14,16 +14,15 @@ export interface FunctionalComponentDefinition { component: ComponentType } -export type UseFunctionalityComponents

= ( - ref: RefObject, - style: CSSProperties, +export type LoadFunctionalityComponents

= ( values: MotionValuesMap, props: P & MotionProps, controls: ValueAnimationControls

, + inherit: boolean, isStatic?: boolean ) => ReactElement[] -export type UseRenderComponent

= ( +export type RenderComponent

= ( ref: RefObject, style: CSSProperties, values: MotionValuesMap, From a887e55513f858a62d669516c438de5e86d20c09 Mon Sep 17 00:00:00 2001 From: InventingWithMonster Date: Tue, 16 Jul 2019 10:14:10 +0200 Subject: [PATCH 4/5] adding animate functionality --- src/motion/functionality/dom.tsx | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/motion/functionality/dom.tsx b/src/motion/functionality/dom.tsx index 3093157f63..131023c963 100644 --- a/src/motion/functionality/dom.tsx +++ b/src/motion/functionality/dom.tsx @@ -86,8 +86,6 @@ export function createDomMotionConfig

( * 2) User-defined "clean props" function that removes their plugin's props before being passed to the DOM. */ loadFunctionalityComponents: ( - ref, - style, values, props, controls, @@ -112,18 +110,6 @@ export function createDomMotionConfig

( ) } - // const Animate = getAnimateComponent(props, context.static) - - // const handleAnimate = Animate && ( - // - // ) - // const handleActiveFunctionality = useFunctionalityComponents( // props, // values, @@ -174,18 +160,6 @@ export function createDomMotionConfig

( // ) // } - // activeComponents.push( - // - // ) - return activeComponents }, getValueControlsConfig(ref, values) { From 453712e08aa85bd8617d9806426dbd85f94ae295 Mon Sep 17 00:00:00 2001 From: InventingWithMonster Date: Tue, 16 Jul 2019 12:22:40 +0200 Subject: [PATCH 5/5] Refactoring functionality loading --- api/framer-motion.api.md | 2 +- src/animation/use-variants.ts | 12 +- src/events/use-event.ts | 2 - src/motion/__tests__/variant.test.tsx | 786 +++++++++++++------------- src/motion/component.tsx | 16 +- src/motion/functionality/animation.ts | 7 +- src/motion/functionality/dom.tsx | 88 +-- src/motion/functionality/drag.ts | 3 +- src/motion/functionality/gestures.ts | 3 +- src/motion/functionality/position.ts | 3 +- src/motion/functionality/types.ts | 7 +- 11 files changed, 451 insertions(+), 478 deletions(-) diff --git a/api/framer-motion.api.md b/api/framer-motion.api.md index c8e882de61..8b925ac7d9 100644 --- a/api/framer-motion.api.md +++ b/api/framer-motion.api.md @@ -70,7 +70,7 @@ export interface AnimationProps { // Warning: (ae-internal-missing-underscore) The name "createMotionComponent" should be prefixed with an underscore because the declaration is marked as @internal // // @internal (undocumented) -export const createMotionComponent:

({ useFunctionalityComponents, getValueControlsConfig, }: MotionComponentConfig) => React.ForwardRefExoticComponent & React.RefAttributes>; +export const createMotionComponent:

({ getValueControlsConfig, loadFunctionalityComponents, renderComponent, }: MotionComponentConfig) => React.ForwardRefExoticComponent & React.RefAttributes>; // @public (undocumented) export interface CustomValueType { diff --git a/src/animation/use-variants.ts b/src/animation/use-variants.ts index 95a8936103..305eb28133 100644 --- a/src/animation/use-variants.ts +++ b/src/animation/use-variants.ts @@ -27,17 +27,21 @@ export function useVariants( inherit: boolean, controls: ValueAnimationControls ) { - const targetVariants = resolveVariantLabels(animate) + let targetVariants = resolveVariantLabels(animate) const context = useContext(MotionContext) - // const parentAlreadyMounted = - // context.hasMounted && context.hasMounted.current + const parentAlreadyMounted = + context.hasMounted && context.hasMounted.current const hasMounted = useRef(false) - console.log(inherit, animate, context.hasMounted) useEffect(() => { let shouldAnimate = false if (inherit) { + // If we're inheriting variant changes and the parent has already + // mounted when this component loads, we need to manually trigger + // this animation. + shouldAnimate = !!parentAlreadyMounted + targetVariants = resolveVariantLabels(context.animate) } else { shouldAnimate = hasMounted.current || diff --git a/src/events/use-event.ts b/src/events/use-event.ts index 29a484eba4..78893c4d5f 100644 --- a/src/events/use-event.ts +++ b/src/events/use-event.ts @@ -19,8 +19,6 @@ export const eventListener = ( return } - // TODO: Pointer events are stacking after every pan start - // https://github.com/framer/company/issues/12814 target.addEventListener(name, handler, options) } const stopListening = () => { diff --git a/src/motion/__tests__/variant.test.tsx b/src/motion/__tests__/variant.test.tsx index 995052a735..179af7873f 100644 --- a/src/motion/__tests__/variant.test.tsx +++ b/src/motion/__tests__/variant.test.tsx @@ -10,398 +10,398 @@ describe("animate prop as variant", () => { hidden: { opacity: 0, x: -100, transition: { type: false } }, visible: { opacity: 1, x: 100, transition: { type: false } }, } - // const childVariants: Variants = { - // hidden: { opacity: 0, x: -100, transition: { type: false } }, - // visible: { opacity: 1, x: 50, transition: { type: false } }, - // } - - // test("animates to set variant", async () => { - // const promise = new Promise(resolve => { - // const x = motionValue(0) - // const onComplete = () => resolve(x.get()) - // const { rerender } = render( - // - // ) - // rerender( - // - // ) - // }) - - // return expect(promise).resolves.toBe(100) - // }) - - // test("child animates to set variant", async () => { - // const promise = new Promise(resolve => { - // const x = motionValue(0) - // const onComplete = () => resolve(x.get()) - // const Component = () => ( - // - // - // - // ) - - // const { rerender } = render() - // rerender() - // }) - - // return expect(promise).resolves.toBe(50) - // }) - - // test("child animates to set variant even if variants are not found on parent", async () => { - // const promise = new Promise(resolve => { - // const x = motionValue(0) - // const onComplete = () => resolve(x.get()) - // const Component = () => ( - // - // - // - // ) - - // const { rerender } = render() - // rerender() - // }) - - // return expect(promise).resolves.toBe(50) - // }) - - // test("applies applyOnEnd if set on initial", () => { - // const variants: Variants = { - // visible: { - // background: "#f00", - // transitionEnd: { display: "none" }, - // }, - // } - - // const { container } = render( - // - // ) - // expect(container.firstChild).toHaveStyle("display: none") - // }) - - // test("applies applyOnEnd and end of animation", async () => { - // const promise = new Promise(resolve => { - // const variants: Variants = { - // hidden: { background: "#00f" }, - // visible: { - // background: "#f00", - // transitionEnd: { display: "none" }, - // }, - // } - // const display = motionValue("block") - // const onComplete = () => resolve(display.get()) - // const Component = () => ( - // - // ) - - // const { rerender } = render() - // rerender() - // }) - - // return expect(promise).resolves.toBe("none") - // }) - - // test("accepts custom transition", async () => { - // const promise = new Promise(resolve => { - // const variants: Variants = { - // hidden: { background: "#00f" }, - // visible: { - // background: "#f00", - // transition: { to: "#555" }, - // }, - // } - // const background = motionValue("#00f") - // const onComplete = () => resolve(background.get()) - // const Component = () => ( - // - // ) - - // const { rerender } = render() - // rerender() - // }) - - // return expect(promise).resolves.toBe("rgba(85, 85, 85, 1)") - // }) - - // test("respects orchestration props in transition prop", async () => { - // const promise = new Promise(resolve => { - // const opacity = motionValue(0) - // const variants: Variants = { - // visible: { - // opacity: 1, - // }, - // hidden: { - // opacity: 0, - // }, - // } - - // render( - // - // - // - // ) - - // requestAnimationFrame(() => resolve(opacity.get())) - // }) - - // return expect(promise).resolves.toBe(0) - // }) - - // test("propagates through components with no `animate` prop", async () => { - // const promise = new Promise(resolve => { - // const opacity = motionValue(0) - // const variants: Variants = { - // visible: { - // opacity: 1, - // }, - // } - - // render( - // - // - // - // - // - // ) - - // requestAnimationFrame(() => resolve(opacity.get())) - // }) - - // return expect(promise).resolves.toBe(1) - // }) - - // test("components without variants are transparent to stagger order", async () => { - // const [recordedOrder, staggeredEqually] = await new Promise(resolve => { - // const order: number[] = [] - // const delayedBy: number[] = [] - // const staggerDuration = 0.1 - - // const updateDelayedBy = (i: number) => { - // if (delayedBy[i]) return - // delayedBy[i] = performance.now() - // } - - // // Checking a rough equidistance between stagger times allows us to see - // // if any of the supposedly invisible interim `motion.div`s were considered part of the - // // stagger order (which would mess up the timings) - // const checkStaggerEquidistance = () => { - // let isEquidistant = true - // let prev = 0 - // for (let i = 0; i < delayedBy.length; i++) { - // if (prev) { - // const timeSincePrev = prev - delayedBy[i] - // if ( - // Math.round(timeSincePrev / 100) * 100 !== - // staggerDuration * 1000 - // ) { - // isEquidistant = false - // } - // } - // prev = delayedBy[i] - // } - - // return isEquidistant - // } - - // const parentVariants: Variants = { - // visible: { - // transition: { - // staggerChildren: staggerDuration, - // staggerDirection: -1, - // }, - // }, - // } - - // const variants: Variants = { - // hidden: { opacity: 0 }, - // visible: { - // opacity: 1, - // transition: { - // duration: 0.01, - // }, - // }, - // } - - // render( - // - // requestAnimationFrame(() => - // resolve([order, checkStaggerEquidistance()]) - // ) - // } - // > - // - // - // { - // updateDelayedBy(0) - // order.push(1) - // }} - // /> - // { - // updateDelayedBy(1) - // order.push(2) - // }} - // /> - // - // - // { - // updateDelayedBy(2) - // order.push(3) - // }} - // /> - // { - // updateDelayedBy(3) - // order.push(4) - // }} - // /> - // - // - // ) - // }) - - // expect(recordedOrder).toEqual([4, 3, 2, 1]) - // expect(staggeredEqually).toEqual(true) - // }) - - // test("onUpdate", async () => { - // const promise = new Promise(resolve => { - // let latest = {} - - // const onUpdate = (l: { [key: string]: number | string }) => { - // latest = l - // } - - // const Component = () => ( - // resolve(latest)} - // /> - // ) - - // const { rerender } = render() - // rerender() - // }) - - // return expect(promise).resolves.toEqual({ x: 100, y: 100 }) - // }) - - // test("onUpdate doesnt fire if no values have changed", async () => { - // const onUpdate = jest.fn() - - // await new Promise(resolve => { - // const x = motionValue(0) - // const Component = ({ xTarget = 0 }) => ( - // - // ) - - // const { rerender } = render() - // setTimeout(() => rerender(), 30) - // setTimeout(() => rerender(), 60) - // setTimeout(() => resolve(), 90) - // }) - - // expect(onUpdate).toHaveBeenCalledTimes(1) - // }) - - // test("accepts variants without being typed", () => { - // expect(() => { - // const variants = { - // withoutTransition: { opacity: 0 }, - // withJustDefaultTransitionType: { - // opacity: 0, - // transition: { - // duration: 1, - // }, - // }, - // withTransitionIndividual: { - // transition: { - // when: "beforeChildren", - // opacity: { type: "spring" }, - // }, - // }, - // withTransitionType: { - // transition: { - // type: "spring", - // }, - // }, - // asResolver: () => ({ - // opacity: 0, - // transition: { - // type: "physics", - // delay: 10, - // }, - // }), - // withTransitionEnd: { - // transitionEnd: { opacity: 0 }, - // }, - // } - // render() - // }).not.toThrowError() - // }) + const childVariants: Variants = { + hidden: { opacity: 0, x: -100, transition: { type: false } }, + visible: { opacity: 1, x: 50, transition: { type: false } }, + } + + test("animates to set variant", async () => { + const promise = new Promise(resolve => { + const x = motionValue(0) + const onComplete = () => resolve(x.get()) + const { rerender } = render( + + ) + rerender( + + ) + }) + + return expect(promise).resolves.toBe(100) + }) + + test("child animates to set variant", async () => { + const promise = new Promise(resolve => { + const x = motionValue(0) + const onComplete = () => resolve(x.get()) + const Component = () => ( + + + + ) + + const { rerender } = render() + rerender() + }) + + return expect(promise).resolves.toBe(50) + }) + + test("child animates to set variant even if variants are not found on parent", async () => { + const promise = new Promise(resolve => { + const x = motionValue(0) + const onComplete = () => resolve(x.get()) + const Component = () => ( + + + + ) + + const { rerender } = render() + rerender() + }) + + return expect(promise).resolves.toBe(50) + }) + + test("applies applyOnEnd if set on initial", () => { + const variants: Variants = { + visible: { + background: "#f00", + transitionEnd: { display: "none" }, + }, + } + + const { container } = render( + + ) + expect(container.firstChild).toHaveStyle("display: none") + }) + + test("applies applyOnEnd and end of animation", async () => { + const promise = new Promise(resolve => { + const variants: Variants = { + hidden: { background: "#00f" }, + visible: { + background: "#f00", + transitionEnd: { display: "none" }, + }, + } + const display = motionValue("block") + const onComplete = () => resolve(display.get()) + const Component = () => ( + + ) + + const { rerender } = render() + rerender() + }) + + return expect(promise).resolves.toBe("none") + }) + + test("accepts custom transition", async () => { + const promise = new Promise(resolve => { + const variants: Variants = { + hidden: { background: "#00f" }, + visible: { + background: "#f00", + transition: { to: "#555" }, + }, + } + const background = motionValue("#00f") + const onComplete = () => resolve(background.get()) + const Component = () => ( + + ) + + const { rerender } = render() + rerender() + }) + + return expect(promise).resolves.toBe("rgba(85, 85, 85, 1)") + }) + + test("respects orchestration props in transition prop", async () => { + const promise = new Promise(resolve => { + const opacity = motionValue(0) + const variants: Variants = { + visible: { + opacity: 1, + }, + hidden: { + opacity: 0, + }, + } + + render( + + + + ) + + requestAnimationFrame(() => resolve(opacity.get())) + }) + + return expect(promise).resolves.toBe(0) + }) + + test("propagates through components with no `animate` prop", async () => { + const promise = new Promise(resolve => { + const opacity = motionValue(0) + const variants: Variants = { + visible: { + opacity: 1, + }, + } + + render( + + + + + + ) + + requestAnimationFrame(() => resolve(opacity.get())) + }) + + return expect(promise).resolves.toBe(1) + }) + + test("components without variants are transparent to stagger order", async () => { + const [recordedOrder, staggeredEqually] = await new Promise(resolve => { + const order: number[] = [] + const delayedBy: number[] = [] + const staggerDuration = 0.1 + + const updateDelayedBy = (i: number) => { + if (delayedBy[i]) return + delayedBy[i] = performance.now() + } + + // Checking a rough equidistance between stagger times allows us to see + // if any of the supposedly invisible interim `motion.div`s were considered part of the + // stagger order (which would mess up the timings) + const checkStaggerEquidistance = () => { + let isEquidistant = true + let prev = 0 + for (let i = 0; i < delayedBy.length; i++) { + if (prev) { + const timeSincePrev = prev - delayedBy[i] + if ( + Math.round(timeSincePrev / 100) * 100 !== + staggerDuration * 1000 + ) { + isEquidistant = false + } + } + prev = delayedBy[i] + } + + return isEquidistant + } + + const parentVariants: Variants = { + visible: { + transition: { + staggerChildren: staggerDuration, + staggerDirection: -1, + }, + }, + } + + const variants: Variants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + duration: 0.01, + }, + }, + } + + render( + + requestAnimationFrame(() => + resolve([order, checkStaggerEquidistance()]) + ) + } + > + + + { + updateDelayedBy(0) + order.push(1) + }} + /> + { + updateDelayedBy(1) + order.push(2) + }} + /> + + + { + updateDelayedBy(2) + order.push(3) + }} + /> + { + updateDelayedBy(3) + order.push(4) + }} + /> + + + ) + }) + + expect(recordedOrder).toEqual([4, 3, 2, 1]) + expect(staggeredEqually).toEqual(true) + }) + + test("onUpdate", async () => { + const promise = new Promise(resolve => { + let latest = {} + + const onUpdate = (l: { [key: string]: number | string }) => { + latest = l + } + + const Component = () => ( + resolve(latest)} + /> + ) + + const { rerender } = render() + rerender() + }) + + return expect(promise).resolves.toEqual({ x: 100, y: 100 }) + }) + + test("onUpdate doesnt fire if no values have changed", async () => { + const onUpdate = jest.fn() + + await new Promise(resolve => { + const x = motionValue(0) + const Component = ({ xTarget = 0 }) => ( + + ) + + const { rerender } = render() + setTimeout(() => rerender(), 30) + setTimeout(() => rerender(), 60) + setTimeout(() => resolve(), 90) + }) + + expect(onUpdate).toHaveBeenCalledTimes(1) + }) + + test("accepts variants without being typed", () => { + expect(() => { + const variants = { + withoutTransition: { opacity: 0 }, + withJustDefaultTransitionType: { + opacity: 0, + transition: { + duration: 1, + }, + }, + withTransitionIndividual: { + transition: { + when: "beforeChildren", + opacity: { type: "spring" }, + }, + }, + withTransitionType: { + transition: { + type: "spring", + }, + }, + asResolver: () => ({ + opacity: 0, + transition: { + type: "physics", + delay: 10, + }, + }), + withTransitionEnd: { + transitionEnd: { opacity: 0 }, + }, + } + render() + }).not.toThrowError() + }) test("new child items animate from initial to animate", () => { const x = motionValue(0) @@ -419,7 +419,7 @@ describe("animate prop as variant", () => { return ( - {items} + {items} ) } diff --git a/src/motion/component.tsx b/src/motion/component.tsx index 70bcafe902..ce2ffd7e87 100644 --- a/src/motion/component.tsx +++ b/src/motion/component.tsx @@ -67,13 +67,15 @@ export const createMotionComponent =

({ props ) - const functionality = loadFunctionalityComponents( - values, - props, - controls, - shouldInheritVariant, - isStatic - ) + const functionality = isStatic + ? null + : loadFunctionalityComponents( + ref, + values, + props, + controls, + shouldInheritVariant + ) const renderedComponent = renderComponent( ref, diff --git a/src/motion/functionality/animation.ts b/src/motion/functionality/animation.ts index a94680edce..8373283364 100644 --- a/src/motion/functionality/animation.ts +++ b/src/motion/functionality/animation.ts @@ -86,8 +86,7 @@ const animatePropTypeTests = { } export const getAnimationComponent = ( - props: MotionProps, - isStatic: boolean = false + props: MotionProps ): ComponentType | undefined => { let animatePropType: AnimatePropType | undefined = undefined @@ -97,7 +96,5 @@ export const getAnimationComponent = ( } } - return !isStatic && animatePropType - ? AnimatePropComponents[animatePropType] - : undefined + return animatePropType ? AnimatePropComponents[animatePropType] : undefined } diff --git a/src/motion/functionality/dom.tsx b/src/motion/functionality/dom.tsx index 131023c963..d20610e0dd 100644 --- a/src/motion/functionality/dom.tsx +++ b/src/motion/functionality/dom.tsx @@ -7,7 +7,6 @@ import { svgElements } from "../utils/supported-elements" import { Gestures } from "./gestures" import { MotionComponentConfig } from "../component" import { Drag } from "./drag" -import { FunctionalProps } from "./types" import styler, { buildSVGAttrs } from "stylefire" import { parseDomVariant } from "../../dom/parse-dom-variant" import { MotionValuesMap } from "../../motion/utils/use-motion-values" @@ -16,12 +15,6 @@ import { Position } from "./position" import { isValidMotionProp } from "../utils/valid-prop" import { getAnimationComponent } from "./animation" -type RenderProps = FunctionalProps & { - componentProps: MotionProps - style: CSSProperties - isStatic: boolean | undefined -} - function stripMotionProps(props: MotionProps) { const domProps = {} @@ -41,6 +34,9 @@ const buildSVGProps = (values: MotionValuesMap, style: CSSProperties) => { return props } +const functionalityComponents = [Position, Drag, Gestures] +const numFunctionalityComponents = functionalityComponents.length + /** * Create a configuration for `motion` components that provides DOM-specific functionality. * @@ -67,7 +63,7 @@ export function createDomMotionConfig

( }, /** - * The useFunctionalityComponents hook gets used by the `motion` component + * loadFunctionalityComponents gets used by the `motion` component * * Each functionality component gets provided the `ref`, animation controls and the `MotionValuesMap` * generated for that component, as well as all the `props` passed to it by the user. @@ -86,19 +82,21 @@ export function createDomMotionConfig

( * 2) User-defined "clean props" function that removes their plugin's props before being passed to the DOM. */ loadFunctionalityComponents: ( + ref, values, props, controls, - inherit, - isStatic + inherit ) => { const activeComponents: JSX.Element[] = [] - const Animation = getAnimationComponent(props, isStatic) + // TODO: Consolidate Animation functionality loading strategy with other functionality components + const Animation = getAnimationComponent(props) if (Animation) { activeComponents.push( ( ) } - // const handleActiveFunctionality = useFunctionalityComponents( - // props, - // values, - // controls, - // ref, - // style, - // context.static - // ) - - // TODO: Refactor the following loading strategy into something more dynamic - // This is also a good target for filesize reduction by making these present externally. - // It might be possible to code-split these out and useState to re-render children when the - // functionality within becomes available, or Suspense. - - // if (!isStatic && Position.shouldRender(props)) { - // activeComponents.push( - // - // ) - // } - - // if (!isStatic && Drag.shouldRender(props)) { - // activeComponents.push( - // - // ) - // } - - // if (!isStatic && Gestures.shouldRender(props)) { - // activeComponents.push( - // - // ) - // } + for (let i = 0; i < numFunctionalityComponents; i++) { + const { + shouldRender, + key, + Component, + } = functionalityComponents[i] + + if (shouldRender(props)) { + activeComponents.push( + + ) + } + } return activeComponents }, diff --git a/src/motion/functionality/drag.ts b/src/motion/functionality/drag.ts index 4c653fb4b8..19b8652950 100644 --- a/src/motion/functionality/drag.ts +++ b/src/motion/functionality/drag.ts @@ -4,8 +4,9 @@ import { makeHookComponent } from "../utils/make-hook-component" import { FunctionalProps, FunctionalComponentDefinition } from "./types" export const Drag: FunctionalComponentDefinition = { + key: "drag", shouldRender: (props: MotionProps) => !!props.drag, - component: makeHookComponent( + Component: makeHookComponent( ({ innerRef, values, controls, ...props }: FunctionalProps) => { return useDraggable(props, innerRef, values, controls) } diff --git a/src/motion/functionality/gestures.ts b/src/motion/functionality/gestures.ts index b9cd6a6ef1..7ba9d18a45 100644 --- a/src/motion/functionality/gestures.ts +++ b/src/motion/functionality/gestures.ts @@ -19,10 +19,11 @@ export const gestureProps = [ ] export const Gestures: FunctionalComponentDefinition = { + key: "gestures", shouldRender: (props: MotionProps) => { return gestureProps.some(key => props.hasOwnProperty(key)) }, - component: makeHookComponent(({ innerRef, ...props }: FunctionalProps) => { + Component: makeHookComponent(({ innerRef, ...props }: FunctionalProps) => { useGestures(props, innerRef) }), } diff --git a/src/motion/functionality/position.ts b/src/motion/functionality/position.ts index 077978a695..d6880de1c0 100644 --- a/src/motion/functionality/position.ts +++ b/src/motion/functionality/position.ts @@ -93,9 +93,10 @@ function usePositionAnimation( } export const Position: FunctionalComponentDefinition = { + key: "position", shouldRender: (props: MotionProps) => typeof window !== "undefined" && !!props.positionTransition, - component: makeHookComponent( + Component: makeHookComponent( ({ innerRef, controls, diff --git a/src/motion/functionality/types.ts b/src/motion/functionality/types.ts index 730ad67e55..db8db50038 100644 --- a/src/motion/functionality/types.ts +++ b/src/motion/functionality/types.ts @@ -10,16 +10,17 @@ export interface FunctionalProps extends MotionProps { } export interface FunctionalComponentDefinition { + key: string shouldRender: (props: MotionProps) => boolean - component: ComponentType + Component: ComponentType } export type LoadFunctionalityComponents

= ( + ref: RefObject, values: MotionValuesMap, props: P & MotionProps, controls: ValueAnimationControls

, - inherit: boolean, - isStatic?: boolean + inherit: boolean ) => ReactElement[] export type RenderComponent

= (