diff --git a/.changeset/eleven-geckos-trade.md b/.changeset/eleven-geckos-trade.md new file mode 100644 index 000000000..23d1e861c --- /dev/null +++ b/.changeset/eleven-geckos-trade.md @@ -0,0 +1,5 @@ +--- +"victory-core": patch +--- + +Convert victory-animation to function component diff --git a/packages/victory-core/src/victory-animation/victory-animation.tsx b/packages/victory-core/src/victory-animation/victory-animation.tsx index d0ab99d57..318c8c73a 100644 --- a/packages/victory-core/src/victory-animation/victory-animation.tsx +++ b/packages/victory-core/src/victory-animation/victory-animation.tsx @@ -1,10 +1,7 @@ -/* global setTimeout:false */ import React from "react"; import * as d3Ease from "victory-vendor/d3-ease"; import { victoryInterpolator } from "./util"; import TimerContext from "../victory-util/timer-context"; -import isEqual from "react-fast-compare"; -import type Timer from "../victory-util/timer"; /** * Single animation object to interpolate @@ -15,6 +12,7 @@ export type AnimationStyle = { [key: string]: string | number }; */ export type AnimationData = AnimationStyle | AnimationStyle[]; + export type AnimationEasing = | "back" | "backIn" @@ -58,17 +56,19 @@ export type AnimationEasing = | "sinInOut"; export interface VictoryAnimationProps { - children: (style: AnimationStyle, info: AnimationInfo) => React.ReactNode; + children: (style: AnimationStyle, info: AnimationInfo) => React.ReactElement; duration?: number; easing?: AnimationEasing; delay?: number; onEnd?: () => void; data: AnimationData; } + export interface VictoryAnimationState { data: AnimationStyle; animationInfo: AnimationInfo; } + export interface AnimationInfo { progress: number; animating: boolean; @@ -79,161 +79,132 @@ export interface VictoryAnimation { context: React.ContextType; } -export class VictoryAnimation extends React.Component< - VictoryAnimationProps, - VictoryAnimationState -> { - static displayName = "VictoryAnimation"; - - static defaultProps = { - data: {}, - delay: 0, - duration: 1000, - easing: "quadInOut", - }; +/** d3-ease changed the naming scheme for ease from "linear" -> "easeLinear" etc. */ +const formatAnimationName = (name: AnimationEasing) => { + const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1); + return `ease${capitalizedName}`; +}; - static contextType = TimerContext; - private interpolator: null | ((value: number) => AnimationStyle); - private queue: AnimationStyle[]; - private ease: any; - private timer: Timer; - private loopID?: number; - - constructor(props, context) { - super(props, context); - /* defaults */ - this.state = { - data: Array.isArray(this.props.data) - ? this.props.data[0] - : this.props.data, - animationInfo: { - progress: 0, - animating: false, - }, - }; - this.interpolator = null; - this.queue = Array.isArray(this.props.data) ? this.props.data.slice(1) : []; - /* build easing function */ - this.ease = d3Ease[this.toNewName(this.props.easing)]; - this.timer = this.context.animationTimer; - } - - componentDidMount() { +const DEFAULT_DURATION = 1000; + +export const VictoryAnimation = ({ + duration = DEFAULT_DURATION, + easing = "quadInOut", + delay = 0, + data, + children, + onEnd, +}: VictoryAnimationProps) => { + const [state, setState] = React.useState({ + data: Array.isArray(data) ? data[0] : data, + animationInfo: { + progress: 0, + animating: false, + }, + }); + + const timer = React.useContext(TimerContext).animationTimer; + const queue = React.useRef( + Array.isArray(data) ? data.slice(1) : [], + ); + const interpolator = React.useRef AnimationStyle)>( + null, + ); + const loopID = React.useRef(undefined); + const ease = d3Ease[formatAnimationName(easing)]; + + React.useEffect(() => { // Length check prevents us from triggering `onEnd` in `traverseQueue`. - if (this.queue.length) { - this.traverseQueue(); + if (queue.current.length) { + traverseQueue(); } - } - - componentDidUpdate(prevProps) { - const equalProps = isEqual(this.props, prevProps); - if (!equalProps) { - /* If the previous animation didn't finish, force it to complete before starting a new one */ - if ( - this.interpolator && - this.state.animationInfo && - this.state.animationInfo.progress < 1 - ) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ - data: this.interpolator(1), - animationInfo: { - progress: 1, - animating: false, - terminating: true, - }, - }); + + // Clean up the animation loop + return () => { + if (loopID.current) { + timer.unsubscribe(loopID.current); } else { - /* cancel existing loop if it exists */ - this.timer.unsubscribe(this.loopID); - /* If an object was supplied */ - if (!Array.isArray(this.props.data)) { - // Replace the tween queue. Could set `this.queue = [nextProps.data]`, - // but let's reuse the same array. - this.queue.length = 0; - this.queue.push(this.props.data); - /* If an array was supplied */ - } else { - /* Extend the tween queue */ - this.queue.push(...this.props.data); - } - /* Start traversing the tween queue */ - this.traverseQueue(); + timer.stop(); } - } - } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - componentWillUnmount() { - if (this.loopID) { - this.timer.unsubscribe(this.loopID); + React.useEffect(() => { + // If the previous animation didn't finish, force it to complete before starting a new one + if ( + interpolator.current && + state.animationInfo && + state.animationInfo.progress < 1 + ) { + setState({ + data: interpolator.current(1), + animationInfo: { + progress: 1, + animating: false, + terminating: true, + }, + }); } else { - this.timer.stop(); + // Cancel existing loop if it exists + timer.unsubscribe(loopID.current); + // Set the tween queue to the new data + queue.current = Array.isArray(data) ? data : [data]; + // Start traversing the tween queue + traverseQueue(); } - } - - toNewName(ease) { - // d3-ease changed the naming scheme for ease from "linear" -> "easeLinear" etc. - const capitalize = (s) => s && s[0].toUpperCase() + s.slice(1); - return `ease${capitalize(ease)}`; - } - - /* Traverse the tween queue */ - traverseQueue() { - if (this.queue.length) { - /* Get the next index */ - const data = this.queue[0]; - /* compare cached version to next props */ - this.interpolator = victoryInterpolator(this.state.data, data); - /* reset step to zero */ - if (this.props.delay) { + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data]); + + const traverseQueue = () => { + if (queue.current.length) { + const nextData = queue.current[0]; + + // Compare cached version to next props + interpolator.current = victoryInterpolator(state.data, nextData); + + // Reset step to zero + if (delay) { setTimeout(() => { - this.loopID = this.timer.subscribe( - this.functionToBeRunEachFrame, - this.props.duration!, - ); - }, this.props.delay); + loopID.current = timer.subscribe(functionToBeRunEachFrame, duration); + }, delay); } else { - this.loopID = this.timer.subscribe( - this.functionToBeRunEachFrame, - this.props.duration!, - ); + loopID.current = timer.subscribe(functionToBeRunEachFrame, duration); } - } else if (this.props.onEnd) { - this.props.onEnd(); + } else if (onEnd) { + onEnd(); } - } - /* every frame we... */ - functionToBeRunEachFrame = (elapsed, duration) => { - /* - step can generate imprecise values, sometimes greater than 1 - if this happens set the state to 1 and return, cancelling the timer - */ - const animationDuration = - duration !== undefined ? duration : this.props.duration; - const step = animationDuration ? elapsed / animationDuration : 1; + }; + + const functionToBeRunEachFrame = (elapsed: number) => { + if (!interpolator.current) return; + + // Step can generate imprecise values, sometimes greater than 1 + // if this happens set the state to 1 and return, cancelling the timer + const step = duration ? elapsed / duration : 1; + if (step >= 1) { - this.setState({ - data: this.interpolator!(1), + setState({ + data: interpolator.current(1), animationInfo: { progress: 1, animating: false, terminating: true, }, }); - if (this.loopID) { - this.timer.unsubscribe(this.loopID); + if (loopID.current) { + timer.unsubscribe(loopID.current); } - this.queue.shift(); - this.traverseQueue(); + queue.current.shift(); + traverseQueue(); return; } - /* - if we're not at the end of the timer, set the state by passing - current step value that's transformed by the ease function to the - interpolator, which is cached for performance whenever props are received - */ - this.setState({ - data: this.interpolator!(this.ease(step)), + + // If we're not at the end of the timer, set the state by passing + // current step value that's transformed by the ease function to the + // interpolator, which is cached for performance whenever props are received + setState({ + data: interpolator.current(ease(step)), animationInfo: { progress: step, animating: step < 1, @@ -241,7 +212,5 @@ export class VictoryAnimation extends React.Component< }); }; - render() { - return this.props.children(this.state.data, this.state.animationInfo); - } -} + return children(state.data, state.animationInfo); +};