From ad327eae6e2caa0d37cc30c5416ed2f915447c95 Mon Sep 17 00:00:00 2001 From: madou Date: Sun, 16 Dec 2018 09:36:05 +1100 Subject: [PATCH 1/3] chore: adds profiler hooks to baba component --- packages/yubaba/src/Baba/stories.tsx | 142 ++++++++++++++++++ packages/yubaba/src/animations/Noop/index.tsx | 2 +- 2 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 packages/yubaba/src/Baba/stories.tsx diff --git a/packages/yubaba/src/Baba/stories.tsx b/packages/yubaba/src/Baba/stories.tsx new file mode 100644 index 0000000..47384c6 --- /dev/null +++ b/packages/yubaba/src/Baba/stories.tsx @@ -0,0 +1,142 @@ +import * as React from 'react'; +import { storiesOf } from '@storybook/react'; +import Baba from './index'; +import Noop from '../animations/Noop'; + +interface BabaProfilerProps { + iterations: number; +} + +interface BabaProfilerState { + start: boolean; + profiling: boolean; + finished: boolean; +} + +class BabaProfiler extends React.Component { + state: BabaProfilerState = BabaProfiler.getDefaultState(); + + curStart: number = -1; + + curEnd: number = -1; + + results: number[] = []; + + iteration: number = 1; + + onNextIteration = () => { + this.iteration += 1; + + this.setState( + { + start: false, + }, + () => { + if (this.iteration < this.props.iterations) { + this.curEnd = Date.now(); + this.results.push(this.curEnd - this.curStart); + this.curStart = Date.now(); + + this.setState({ + start: true, + }); + } else { + this.curEnd = Date.now(); + this.results.push(this.curEnd - this.curStart); + + this.setState({ + finished: true, + }); + } + } + ); + }; + + start = () => { + this.curStart = Date.now(); + + this.setState( + { + profiling: true, + }, + () => { + this.setState({ + start: true, + }); + } + ); + }; + + reset = () => { + this.curStart = -1; + this.curEnd = -1; + this.results = []; + this.iteration = 1; + this.setState(BabaProfiler.getDefaultState(), this.start); + }; + + static getDefaultState() { + return { + start: false, + profiling: false, + finished: false, + }; + } + + getAverage() { + return Math.ceil(this.results.reduce((val, total) => val + total, 0) / this.results.length); + } + + render() { + if (this.state.finished) { + return ( + + {`avg: ${this.getAverage()}ms`} + + + ); + } + + if (!this.state.profiling) { + return ( + + ); + } + + return ( +
+ {!this.state.start ? ( + + + {baba => ( +
+ {this.iteration} +
+ )} +
+
+ ) : ( +
+ + {baba => ( +
+ {this.iteration} +
+ )} +
+
+ )} +
+ ); + } +} + +storiesOf('yubaba/Baba', module) + .add('profiler (1)', () => ) + .add('profiler (10)', () => ) + .add('profiler (100)', () => ) + .add('profiler (1000)', () => ); diff --git a/packages/yubaba/src/animations/Noop/index.tsx b/packages/yubaba/src/animations/Noop/index.tsx index a3b09de..e3da673 100644 --- a/packages/yubaba/src/animations/Noop/index.tsx +++ b/packages/yubaba/src/animations/Noop/index.tsx @@ -10,7 +10,7 @@ interface NoopProps extends CollectorChildrenProps { */ export default class Noop extends React.Component { static defaultProps = { - duration: 1, + duration: 0, }; render() { From fedddff2ea2207bb554cb329a4fad3df413a9792 Mon Sep 17 00:00:00 2001 From: madou Date: Sun, 16 Dec 2018 10:22:45 +1100 Subject: [PATCH 2/3] chore: rename internals --- packages/yubaba/src/Collector/index.tsx | 6 ++-- .../src/animations/CircleExpand/index.tsx | 6 ++-- .../src/animations/ConcealMove/index.tsx | 6 ++-- .../yubaba/src/animations/FadeMove/index.tsx | 6 ++-- packages/yubaba/src/animations/Move/index.tsx | 4 +-- packages/yubaba/src/lib/dom.tsx | 30 +++++++++---------- 6 files changed, 32 insertions(+), 26 deletions(-) diff --git a/packages/yubaba/src/Collector/index.tsx b/packages/yubaba/src/Collector/index.tsx index 0e2eadc..40f0d59 100644 --- a/packages/yubaba/src/Collector/index.tsx +++ b/packages/yubaba/src/Collector/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { GetElementSizeLocationReturnValue } from '../lib/dom'; +import { ElementBoundingBox } from '../lib/dom'; export interface TargetProps { style: InlineStyles; @@ -84,9 +84,9 @@ export interface InlineStyles { */ export interface ElementData { element: HTMLElement; - elementBoundingBox: GetElementSizeLocationReturnValue; + elementBoundingBox: ElementBoundingBox; focalTargetElement: HTMLElement | null | undefined; - focalTargetElementBoundingBox: GetElementSizeLocationReturnValue | undefined; + focalTargetElementBoundingBox: ElementBoundingBox | undefined; render: CollectorChildrenAsFunction; } diff --git a/packages/yubaba/src/animations/CircleExpand/index.tsx b/packages/yubaba/src/animations/CircleExpand/index.tsx index d754ae8..23d9321 100644 --- a/packages/yubaba/src/animations/CircleExpand/index.tsx +++ b/packages/yubaba/src/animations/CircleExpand/index.tsx @@ -9,7 +9,7 @@ import { calculateHypotenuse } from '../../lib/math'; import { calculateWindowCentre, calculateElementCenterInViewport, - recalculateLocationFromScroll, + recalculateElementBoundingBoxFromScroll, } from '../../lib/dom'; import SimpleKeyframe from '../SimpleKeyframe'; import { standard, accelerate } from '../../lib/curves'; @@ -51,7 +51,9 @@ export default class CircleExpand extends React.Component { const { duration, background, zIndex } = this.props; // Scroll could have changed between unmount and this prepare step, let's recalculate just in case. - const fromTargetSizeLocation = recalculateLocationFromScroll(data.origin.elementBoundingBox); + const fromTargetSizeLocation = recalculateElementBoundingBoxFromScroll( + data.origin.elementBoundingBox + ); const minSize = Math.min(fromTargetSizeLocation.size.width, fromTargetSizeLocation.size.height); const fromTargetHypotenuse = calculateHypotenuse(fromTargetSizeLocation.size); const fromTargetCenterInViewport = calculateElementCenterInViewport(fromTargetSizeLocation); diff --git a/packages/yubaba/src/animations/ConcealMove/index.tsx b/packages/yubaba/src/animations/ConcealMove/index.tsx index b1e3392..8999994 100644 --- a/packages/yubaba/src/animations/ConcealMove/index.tsx +++ b/packages/yubaba/src/animations/ConcealMove/index.tsx @@ -6,7 +6,7 @@ import Collector, { CollectorActions, AnimationData, } from '../../Collector'; -import { recalculateLocationFromScroll } from '../../lib/dom'; +import { recalculateElementBoundingBoxFromScroll } from '../../lib/dom'; import noop from '../../lib/noop'; import { standard } from '../../lib/curves'; import { zIndexStack } from '../../lib/style'; @@ -53,7 +53,9 @@ targetElement was missing.`); const { duration, timingFunction, zIndex } = this.props; // Scroll could have changed between unmount and this prepare step. - const fromTargetSizeLocation = recalculateLocationFromScroll(data.origin.elementBoundingBox); + const fromTargetSizeLocation = recalculateElementBoundingBoxFromScroll( + data.origin.elementBoundingBox + ); return data.origin.render({ ref: noop, diff --git a/packages/yubaba/src/animations/FadeMove/index.tsx b/packages/yubaba/src/animations/FadeMove/index.tsx index 8a37d11..82d2a51 100644 --- a/packages/yubaba/src/animations/FadeMove/index.tsx +++ b/packages/yubaba/src/animations/FadeMove/index.tsx @@ -6,7 +6,7 @@ import Collector, { AnimationData, } from '../../Collector'; import * as math from '../../lib/math'; -import { recalculateLocationFromScroll } from '../../lib/dom'; +import { recalculateElementBoundingBoxFromScroll } from '../../lib/dom'; import noop from '../../lib/noop'; import { standard } from '../../lib/curves'; import { zIndexStack } from '../../lib/style'; @@ -51,7 +51,9 @@ export default class FadeMove extends React.Component { const { timingFunction, duration, zIndex } = this.props; // Scroll could have changed between unmount and this prepare step, let's recalculate // just in case. - const fromTargetSizeLocation = recalculateLocationFromScroll(data.origin.elementBoundingBox); + const fromTargetSizeLocation = recalculateElementBoundingBoxFromScroll( + data.origin.elementBoundingBox + ); const fromEndXOffset = data.destination.elementBoundingBox.location.left - fromTargetSizeLocation.location.left; const fromEndYOffset = diff --git a/packages/yubaba/src/animations/Move/index.tsx b/packages/yubaba/src/animations/Move/index.tsx index 393a651..a4277da 100644 --- a/packages/yubaba/src/animations/Move/index.tsx +++ b/packages/yubaba/src/animations/Move/index.tsx @@ -5,7 +5,7 @@ import Collector, { CollectorActions, } from '../../Collector'; import * as math from '../../lib/math'; -import { recalculateLocationFromScroll } from '../../lib/dom'; +import { recalculateElementBoundingBoxFromScroll } from '../../lib/dom'; import { standard } from '../../lib/curves'; import { combine, zIndexStack } from '../../lib/style'; @@ -75,7 +75,7 @@ targetElement was missing.`); } // Scroll could have changed between unmount and this prepare step. - const originTarget = recalculateLocationFromScroll(data.origin.elementBoundingBox); + const originTarget = recalculateElementBoundingBoxFromScroll(data.origin.elementBoundingBox); const destinationTarget = useFocalTarget && data.destination.focalTargetElementBoundingBox ? data.destination.focalTargetElementBoundingBox diff --git a/packages/yubaba/src/lib/dom.tsx b/packages/yubaba/src/lib/dom.tsx index 453d000..c42fe9b 100644 --- a/packages/yubaba/src/lib/dom.tsx +++ b/packages/yubaba/src/lib/dom.tsx @@ -21,14 +21,14 @@ export function getDocumentScroll() { /** * @hidden */ -export interface GetElementSizeLocationOptions { +export interface ElementBoundingBoxOpts { useOffsetSize?: boolean; } /** * @hidden */ -export interface GetElementSizeLocationReturnValue { +export interface ElementBoundingBox { size: { width: number; height: number; @@ -49,8 +49,8 @@ export interface GetElementSizeLocationReturnValue { */ export function getElementBoundingBox( element: HTMLElement, - options: GetElementSizeLocationOptions = {} -): GetElementSizeLocationReturnValue { + options: ElementBoundingBoxOpts = {} +): ElementBoundingBox { const rect = element.getBoundingClientRect(); const { scrollLeft, scrollTop } = getDocumentScroll(); const topOffset = (rect.height - element.offsetHeight) / 2; @@ -76,10 +76,10 @@ export function getElementBoundingBox( /** * @hidden */ -export function calculateElementCenterInViewport(sizeLocation: GetElementSizeLocationReturnValue) { +export function calculateElementCenterInViewport(elementBoundingBox: ElementBoundingBox) { return { - top: sizeLocation.location.top + Math.ceil(sizeLocation.size.width / 2), - left: sizeLocation.location.left - Math.ceil(sizeLocation.size.height / 2), + top: elementBoundingBox.location.top + Math.ceil(elementBoundingBox.size.width / 2), + left: elementBoundingBox.location.left - Math.ceil(elementBoundingBox.size.height / 2), }; } @@ -96,18 +96,18 @@ export function calculateWindowCentre() { /** * @hidden */ -export function recalculateLocationFromScroll( - sizeLocation: GetElementSizeLocationReturnValue -): GetElementSizeLocationReturnValue { +export function recalculateElementBoundingBoxFromScroll( + elementBoundingBox: ElementBoundingBox +): ElementBoundingBox { const { scrollTop, scrollLeft } = getDocumentScroll(); - const scrollTopDiff = scrollTop - sizeLocation.raw.scrollTop; - const scrollLeftDiff = scrollLeft - sizeLocation.raw.scrollLeft; + const scrollTopDiff = scrollTop - elementBoundingBox.raw.scrollTop; + const scrollLeftDiff = scrollLeft - elementBoundingBox.raw.scrollLeft; return { - ...sizeLocation, + ...elementBoundingBox, location: { - top: sizeLocation.location.top + scrollTopDiff, - left: sizeLocation.location.left + scrollLeftDiff, + top: elementBoundingBox.location.top + scrollTopDiff, + left: elementBoundingBox.location.left + scrollLeftDiff, }, }; } From 29bde44417dee1691a43898e4b74e1265728b81f Mon Sep 17 00:00:00 2001 From: madou Date: Sat, 29 Dec 2018 12:54:43 +1100 Subject: [PATCH 3/3] chore: split work over multiple frames, don't set state if we don't need to --- packages/yubaba/src/Baba/index.tsx | 437 +++++++++++++++-------------- test/setup.js | 1 + 2 files changed, 225 insertions(+), 213 deletions(-) diff --git a/packages/yubaba/src/Baba/index.tsx b/packages/yubaba/src/Baba/index.tsx index fd61ee6..f5f712d 100644 --- a/packages/yubaba/src/Baba/index.tsx +++ b/packages/yubaba/src/Baba/index.tsx @@ -166,8 +166,8 @@ export class Baba extends React.PureComponent { const { in: isIn } = this.props; if (prevProps.in === false && isIn === true) { // We're being removed from "in". Let's recalculate our DOM position. - this.storeDOMData(); - this.delayedClearBabaStore(); + this.snapshotDOM(); + this.delayedClearDOMSnapshot(); this.abortAnimations(); } } @@ -199,8 +199,8 @@ You're switching between controlled and uncontrolled, don't do this. Either alwa } componentWillUnmount() { - this.storeDOMData(); - this.delayedClearBabaStore(); + this.snapshotDOM(); + this.delayedClearDOMSnapshot(); this.abortAnimations(); this.unmounting = true; } @@ -208,9 +208,7 @@ You're switching between controlled and uncontrolled, don't do this. Either alwa showSelfAndNotifyManager() { const { context, name } = this.props; - this.setState({ - shown: true, - }); + this.showSelf(); // If a BabaManager is a parent up the tree context will be available. // Notify them that we're finished getting ready. @@ -219,13 +217,21 @@ You're switching between controlled and uncontrolled, don't do this. Either alwa } } - delayedClearBabaStore() { + delayedClearDOMSnapshot() { const { name, timeToWaitForNextBaba } = this.props; setTimeout(() => babaStore.remove(name), timeToWaitForNextBaba); } - storeDOMData() { + showSelf() { + if (!this.state.shown) { + this.setState({ + shown: true, + }); + } + } + + snapshotDOM() { if (this.unmounting) { return; } @@ -264,86 +270,109 @@ If it's an image, try and have the image loaded before mounting, or set a static } } - executeAnimations = () => { - const { name } = this.props; - const fromTarget = babaStore.get(name); - - if (fromTarget) { - const { collectorData, elementData } = fromTarget; - this.animating = true; - - // Calculate DOM data for the executing element to then be passed to the animation/s. - const animationData: AnimationData = { - origin: elementData, - destination: { - render: this.renderChildren, - element: this.element as HTMLElement, - elementBoundingBox: getElementBoundingBox(this.element as HTMLElement), - focalTargetElement: this.focalTargetElement, - focalTargetElementBoundingBox: this.focalTargetElement - ? getElementBoundingBox(this.focalTargetElement) - : undefined, - }, - }; - - // Loads each action up in an easy-to-execute format. - const actions = collectorData.map(targetData => { - if (targetData.action === CollectorActions.animation) { - // Element will be lazily instantiated if we need to add something to the DOM. - let elementToMountChildren: HTMLElement; - - const mount = (jsx: React.ReactNode) => { - if (!elementToMountChildren) { - elementToMountChildren = document.createElement('div'); - // We insert the new element at the beginning of the body to ensure correct - // stacking context. - document.body.insertBefore(elementToMountChildren, document.body.firstChild); - } - - // This ensures that if there was an update to the jsx that is animating, - // it changes next frame. Resulting in the transition _actually_ happening. - requestAnimationFrame(() => - renderSubtreeIntoContainer( - this, - jsx as React.ReactElement<{}>, - elementToMountChildren - ) - ); - }; + /** + * executeAnimations() + * Will always execute on the next animation to spread out CPU usage. + */ + executeAnimations = () => + requestAnimationFrame(() => { + const { name } = this.props; + const fromTarget = babaStore.get(name); + + if (fromTarget) { + const { collectorData, elementData } = fromTarget; + this.animating = true; + + // Calculate DOM data for the executing element to then be passed to the animation/s. + const animationData: AnimationData = { + origin: elementData, + destination: { + render: this.renderChildren, + element: this.element as HTMLElement, + elementBoundingBox: getElementBoundingBox(this.element as HTMLElement), + focalTargetElement: this.focalTargetElement, + focalTargetElementBoundingBox: this.focalTargetElement + ? getElementBoundingBox(this.focalTargetElement) + : undefined, + }, + }; + + // Loads each action up in an easy-to-execute format. + const actions = collectorData.map(targetData => { + if (targetData.action === CollectorActions.animation) { + // Element will be lazily instantiated if we need to add something to the DOM. + let elementToMountChildren: HTMLElement; + + const mount = (jsx: React.ReactNode) => { + if (!elementToMountChildren) { + elementToMountChildren = document.createElement('div'); + // We insert the new element at the beginning of the body to ensure correct + // stacking context. + document.body.insertBefore(elementToMountChildren, document.body.firstChild); + } + + // This ensures that if there was an update to the jsx that is animating, + // it changes next frame. Resulting in the transition _actually_ happening. + requestAnimationFrame(() => + renderSubtreeIntoContainer( + this, + jsx as React.ReactElement<{}>, + elementToMountChildren + ) + ); + }; + + const unmount = () => { + if (elementToMountChildren) { + unmountComponentAtNode(elementToMountChildren); + document.body.removeChild(elementToMountChildren); + } + }; + + const setChildProps = (props: TargetPropsFunc | null) => { + if (props) { + this.setState(prevState => ({ + childProps: { + style: props.style + ? props.style(prevState.childProps.style || {}) + : prevState.childProps.style, + className: props.className + ? props.className(prevState.childProps.className) + : prevState.childProps.className, + }, + })); + } else if (this.state.childProps.style || this.state.childProps.className) { + // We only clear if something was set, else it's wasted CPU. + this.setState({ + childProps: {}, + }); + } + }; + + return { + action: CollectorActions.animation, + payload: { + beforeAnimate: () => { + if (targetData.payload.beforeAnimate) { + const deferred = defer(); + const jsx = targetData.payload.beforeAnimate( + animationData, + deferred.resolve, + setChildProps + ); + + if (jsx) { + mount(jsx); + } + + return deferred.promise; + } - const unmount = () => { - if (elementToMountChildren) { - unmountComponentAtNode(elementToMountChildren); - document.body.removeChild(elementToMountChildren); - } - }; - - const setChildProps = (props: TargetPropsFunc | null) => { - if (props) { - this.setState(prevState => ({ - childProps: { - style: props.style - ? props.style(prevState.childProps.style || {}) - : prevState.childProps.style, - className: props.className - ? props.className(prevState.childProps.className) - : prevState.childProps.className, + return Promise.resolve(); }, - })); - } else { - this.setState({ - childProps: {}, - }); - } - }; - - return { - action: CollectorActions.animation, - payload: { - beforeAnimate: () => { - if (targetData.payload.beforeAnimate) { + animate: () => { const deferred = defer(); - const jsx = targetData.payload.beforeAnimate( + const jsx = targetData.payload.animate( animationData, deferred.resolve, setChildProps @@ -354,143 +383,125 @@ If it's an image, try and have the image loaded before mounting, or set a static } return deferred.promise; - } - - return Promise.resolve(); - }, - animate: () => { - const deferred = defer(); - const jsx = targetData.payload.animate( - animationData, - deferred.resolve, - setChildProps - ); - - if (jsx) { - mount(jsx); - } - - return deferred.promise; - }, - afterAnimate: () => { - if (targetData.payload.afterAnimate) { - const deferred = defer(); - const jsx = targetData.payload.afterAnimate( - animationData, - deferred.resolve, - setChildProps - ); - - if (jsx) { - mount(jsx); + }, + afterAnimate: () => { + if (targetData.payload.afterAnimate) { + const deferred = defer(); + const jsx = targetData.payload.afterAnimate( + animationData, + deferred.resolve, + setChildProps + ); + + if (jsx) { + mount(jsx); + } + + return deferred.promise; } - return deferred.promise; - } - - return Promise.resolve(); - }, - cleanup: () => { - unmount(); - setChildProps(null); + return Promise.resolve(); + }, + cleanup: () => { + unmount(); + setChildProps(null); + }, }, - }, - }; - } - - return targetData; - }); - - const blocks = actions.reduce( - (arr, targetData) => { - switch (targetData.action) { - case CollectorActions.animation: { - // Add to the last block in the array. - arr[arr.length - 1].push(targetData.payload); - return arr; - } + }; + } - case CollectorActions.wait: { - // Found a wait action, start a new block. - arr.push([]); - return arr; - } + return targetData; + }); - default: { - return arr; + const blocks = actions.reduce( + (arr, targetData) => { + switch (targetData.action) { + case CollectorActions.animation: { + // Add to the last block in the array. + arr[arr.length - 1].push(targetData.payload); + return arr; + } + + case CollectorActions.wait: { + // Found a wait action, start a new block. + arr.push([]); + return arr; + } + + default: { + return arr; + } } + }, + [[]] + ); + + this.abortAnimations = () => { + if (this.animating) { + this.animating = false; + blocks.forEach(block => block.forEach(anim => anim.cleanup())); } - }, - [[]] - ); - - this.abortAnimations = () => { - if (this.animating) { - this.animating = false; - blocks.forEach(block => block.forEach(anim => anim.cleanup())); - } - }; - - const beforeAnimatePromises = actions.map(targetData => - targetData.action === CollectorActions.animation - ? targetData.payload.beforeAnimate() - : Promise.resolve() - ); - - Promise.all(beforeAnimatePromises) - .then(() => { - // Wait two animation frames before triggering animations. - // This makes sure state set inside animate don't happen in the same animation frame as beforeAnimate. - const deferred = defer(); - requestAnimationFrame(() => { - requestAnimationFrame(() => deferred.resolve()); - }); - return deferred.promise; - }) - .then(() => { - // Trigger each blocks animations, one block at a time. - return ( - blocks - // We don't care what the promises return. - .reduce>( - (promise, block) => - promise.then(() => Promise.all(block.map(anim => anim.animate()))), - Promise.resolve() - ) - .then(() => { - // We're finished all the transitions! Show the child element. - this.setState({ - shown: true, - }); + }; + + const beforeAnimatePromises = actions.map(targetData => + targetData.action === CollectorActions.animation + ? targetData.payload.beforeAnimate() + : Promise.resolve() + ); + + Promise.all(beforeAnimatePromises) + .then(() => { + // Wait two animation frames before triggering animations. + // This makes sure state set inside animate don't happen in the same animation frame as beforeAnimate. + const deferred = defer(); + requestAnimationFrame(() => { + requestAnimationFrame(() => deferred.resolve()); + }); + return deferred.promise; + }) + .then(() => { + // Trigger each blocks animations, one block at a time. + return ( + blocks + // We don't care what the promises return. + .reduce>( + (promise, block) => + promise.then(() => Promise.all(block.map(anim => anim.animate()))), + Promise.resolve() + ) + .then(() => { + // We're finished all the transitions! Show the child element. + this.showSelf(); - const { context } = this.props; + const { context } = this.props; - // If a BabaManager is a parent somewhere, notify them that we're finished animating. - if (context) { - context.onFinish({ name }); - } + // If a BabaManager is a parent somewhere, notify them that we're finished animating. + if (context) { + context.onFinish({ name }); + } - // Run through all after animates. - return blocks.reduce( - (promise, block) => - promise.then(() => - Promise.all(block.map(anim => anim.afterAnimate())).then(() => undefined) - ), - Promise.resolve() - ); - }) - .then(() => { - blocks.forEach(block => block.forEach(anim => anim.cleanup())); - }) - .then(() => { - this.animating = false; - const { onFinish } = this.props; - onFinish(); - }) - ); - }); - } - }; + // Run through all after animates. + return blocks.reduce( + (promise, block) => + promise.then(() => + Promise.all(block.map(anim => anim.afterAnimate())).then(() => undefined) + ), + Promise.resolve() + ); + }) + .then(() => { + blocks.forEach(block => block.forEach(anim => anim.cleanup())); + }) + .then(() => { + this.animating = false; + const { onFinish } = this.props; + // So we don't run everything on the same stack, call this in the next frame. + requestAnimationFrame(onFinish); + }) + ); + }); + } + }); setRef: SupplyRefHandler = ref => { this.element = ref; diff --git a/test/setup.js b/test/setup.js index 0520d05..1bf28e8 100644 --- a/test/setup.js +++ b/test/setup.js @@ -5,3 +5,4 @@ require('jest-enzyme'); enzyme.configure({ adapter: new Adapter() }); expect.addSnapshotSerializer(createSerializer({ mode: 'deep' })); +window.requestAnimationFrame = cb => cb();