Skip to content

Commit

Permalink
Merge pull request #201 from framer/fix/new-item-variant-animate
Browse files Browse the repository at this point in the history
Animating existing variants on newly-added variant children
  • Loading branch information
mergetron[bot] committed Jul 19, 2019
2 parents 22358ab + 453712e commit a595bf5
Show file tree
Hide file tree
Showing 14 changed files with 193 additions and 154 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion api/framer-motion.api.md
Expand Up @@ -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: <P extends {}>({ useFunctionalityComponents, getValueControlsConfig, }: MotionComponentConfig) => React.ForwardRefExoticComponent<React.PropsWithoutRef<P & MotionProps> & React.RefAttributes<Element>>;
export const createMotionComponent: <P extends {}>({ getValueControlsConfig, loadFunctionalityComponents, renderComponent, }: MotionComponentConfig) => React.ForwardRefExoticComponent<React.PropsWithoutRef<P & MotionProps> & React.RefAttributes<Element>>;

// @public (undocumented)
export interface CustomValueType {
Expand Down
13 changes: 6 additions & 7 deletions src/animation/use-value-animation-controls.ts
Expand Up @@ -15,23 +15,26 @@ import { useConstant } from "../utils/use-constant"
* @param values
* @param props
* @param ref
* @param inheritVariantChanges
* @param subscribeToParentControls
*
* @internal
*/
export function useValueAnimationControls<P>(
config: ValueAnimationConfig,
props: P & MotionProps,
inheritVariantChanges: boolean
subscribeToParentControls: boolean
) {
const { variants, transition } = props
const parentControls = useContext(MotionContext).controls
const controls = useConstant(() => new ValueAnimationControls<P>(config))

// 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) {
if (subscribeToParentControls && parentControls) {
parentControls.addChild(controls)
}

Expand All @@ -40,9 +43,5 @@ export function useValueAnimationControls<P>(
[]
)

controls.setProps(props)
controls.setVariants(variants)
controls.setDefaultTransition(transition)

return controls
}
45 changes: 27 additions & 18 deletions src/animation/use-variants.ts
Expand Up @@ -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(",")
Expand All @@ -13,34 +14,42 @@ 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)
let targetVariants = resolveVariantLabels(animate)
const context = useContext(MotionContext)
const parentAlreadyMounted =
context.hasMounted && context.hasMounted.current
const hasMounted = useRef(false)

// Fire animations when poses change
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) {
// 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 ||
hasVariantChanged(resolveVariantLabels(initial), targetVariants)
}

shouldAnimate && controls.start(targetVariants)

hasMounted.current = true
}, asDependencyList(variantList))
}, asDependencyList(targetVariants))
}
2 changes: 0 additions & 2 deletions src/events/use-event.ts
Expand Up @@ -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 = () => {
Expand Down
29 changes: 29 additions & 0 deletions src/motion/__tests__/variant.test.tsx
Expand Up @@ -402,4 +402,33 @@ describe("animate prop as variant", () => {
render(<motion.div variants={variants} />)
}).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(
<motion.div
key={i}
variants={variants}
style={{ x: i === 1 ? x : 0 }}
/>
)
}

return (
<motion.div initial="hidden" animate="visible">
<motion.div>{items}</motion.div>
</motion.div>
)
}

const { rerender } = render(<Component length={1} />)
rerender(<Component length={1} />)
rerender(<Component length={2} />)
rerender(<Component length={2} />)

expect(x.get()).toBe(100)
})
})
52 changes: 28 additions & 24 deletions src/motion/component.tsx
Expand Up @@ -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 {
LoadFunctionalityComponents,
RenderComponent,
} 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
loadFunctionalityComponents: LoadFunctionalityComponents
renderComponent: RenderComponent
getValueControlsConfig: (
ref: RefObject<any>,
values: MotionValuesMap
Expand All @@ -29,8 +32,9 @@ export interface MotionComponentConfig {
* @internal
*/
export const createMotionComponent = <P extends {}>({
useFunctionalityComponents,
getValueControlsConfig,
loadFunctionalityComponents,
renderComponent,
}: MotionComponentConfig) => {
function MotionComponent(
props: P & MotionProps,
Expand All @@ -55,44 +59,44 @@ export const createMotionComponent = <P extends {}>({
props,
shouldInheritVariant
)

const context = useMotionContext(
parentContext,
controls,
isStatic,
props
)
// Add functionality
const Animate = getAnimateComponent(props, context.static)

const handleAnimate = Animate && (
<Animate
{...props}
inherit={shouldInheritVariant}
innerRef={ref}
values={values}
controls={controls}
/>
)
const functionality = isStatic
? null
: loadFunctionalityComponents(
ref,
values,
props,
controls,
shouldInheritVariant
)

const handleActiveFunctionality = useFunctionalityComponents(
props,
values,
controls,
const renderedComponent = renderComponent(
ref,
style,
context.static
values,
props,
isStatic
)

return (
<MotionContext.Provider value={context}>
<>
<MountMotionValues
ref={ref}
values={values}
isStatic={isStatic}
/>
{handleAnimate}
{handleActiveFunctionality}
</MotionContext.Provider>
{functionality}
<MotionContext.Provider value={context}>
{renderedComponent}
</MotionContext.Provider>
</>
)
}

Expand Down
25 changes: 22 additions & 3 deletions 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<boolean>
}

/**
Expand Down Expand Up @@ -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) ||
Expand All @@ -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
Expand All @@ -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]
)
Expand All @@ -100,5 +115,9 @@ export const useMotionContext = (
isStatic ? Infinity : 1
)

useEffect(() => {
hasMounted.current = true
}, [])

return context
}
22 changes: 11 additions & 11 deletions 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"
Expand All @@ -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<Element | null>
inherit: boolean
}

Expand Down Expand Up @@ -40,10 +43,10 @@ export const AnimatePropComponents = {
initial,
}: AnimationFunctionalProps) => {
return useVariants(
initial as VariantLabels,
animate as VariantLabels,
inherit,
controls,
initial as VariantLabels
controls
)
}
),
Expand Down Expand Up @@ -82,9 +85,8 @@ const animatePropTypeTests = {
[AnimatePropType.AnimationSubscription]: isAnimationSubscription,
}

export const getAnimateComponent = (
props: MotionProps,
isStatic: boolean = false
export const getAnimationComponent = (
props: MotionProps
): ComponentType<AnimationFunctionalProps> | undefined => {
let animatePropType: AnimatePropType | undefined = undefined

Expand All @@ -94,7 +96,5 @@ export const getAnimateComponent = (
}
}

return !isStatic && animatePropType
? AnimatePropComponents[animatePropType]
: undefined
return animatePropType ? AnimatePropComponents[animatePropType] : undefined
}

0 comments on commit a595bf5

Please sign in to comment.