diff --git a/packages/core/src/Controller.test.ts b/packages/core/src/Controller.test.ts index 508d93f024..9e8b96dff9 100644 --- a/packages/core/src/Controller.test.ts +++ b/packages/core/src/Controller.test.ts @@ -256,6 +256,52 @@ describe('Controller', () => { // Since we call `update` twice, frames are generated! expect(global.getFrames(ctrl)).toMatchSnapshot() }) + + describe('when skipAnimations is true', () => { + it('should not run at all', async () => { + const ctrl = new Controller({ from: { x: 0 } }) + let n = 0 + + global.setSkipAnimation(true) + + ctrl.start({ + to: async next => { + while (true) { + n += 1 + await next({ x: 1, reset: true }) + } + }, + }) + + await flushMicroTasks() + expect(n).toBe(0) + }) + + it('should stop running and push the animation to the finished state when called mid animation', async () => { + const ctrl = new Controller({ from: { x: 0 } }) + let n = 0 + + ctrl.start({ + to: async next => { + while (n < 5) { + n++ + await next({ x: 10, reset: true }) + } + }, + }) + + await global.advance() + expect(n).toBe(1) + + global.setSkipAnimation(true) + + await global.advanceUntilIdle() + + const { x } = ctrl.springs + expect(n).toBe(2) + expect(x.get()).toEqual(10) + }) + }) }) describe('when the "onStart" prop is defined', () => { diff --git a/packages/core/src/runAsync.ts b/packages/core/src/runAsync.ts index 2f9ec68843..34e5179f10 100644 --- a/packages/core/src/runAsync.ts +++ b/packages/core/src/runAsync.ts @@ -1,4 +1,11 @@ -import { is, raf, flush, eachProp, Timeout } from '@react-spring/shared' +import { + is, + raf, + flush, + eachProp, + Timeout, + Globals as G, +} from '@react-spring/shared' import { Falsy } from '@react-spring/types' import { getDefaultProps } from './helpers' @@ -88,8 +95,23 @@ export function runAsync( // Create the bail signal outside the returned promise, // so the generated stack trace is relevant. const bailSignal = new BailSignal() + const skipAnimationSignal = new SkipAniamtionSignal() return (async () => { + if (G.skipAnimation) { + /** + * We need to stop animations if `skipAnimation` + * is set in the Globals + * + */ + stopAsync(state) + + // create the rejection error that's handled gracefully + skipAnimationSignal.result = getFinishedResult(target, false) + bail(skipAnimationSignal) + throw skipAnimationSignal + } + bailIfEnded(bailSignal) const props: any = is.obj(arg1) ? { ...arg1 } : { ...arg2, to: arg1 } @@ -115,6 +137,16 @@ export function runAsync( } let result!: AnimationResult + + if (G.skipAnimation) { + /** + * We need to stop animations if `skipAnimation` + * is set in the Globals + */ + stopAsync(state) + return getFinishedResult(target, false) + } + try { let animating!: Promise @@ -139,6 +171,8 @@ export function runAsync( } catch (err) { if (err instanceof BailSignal) { result = err.result + } else if (err instanceof SkipAniamtionSignal) { + result = err.result } else { throw err } @@ -181,3 +215,11 @@ export class BailSignal extends Error { ) } } + +export class SkipAniamtionSignal extends Error { + result!: AnimationResult + + constructor() { + super('SkipAnimationSignal') + } +} diff --git a/packages/core/test/setup.ts b/packages/core/test/setup.ts index abcf16c76f..ab0cd21543 100644 --- a/packages/core/test/setup.ts +++ b/packages/core/test/setup.ts @@ -37,6 +37,8 @@ declare global { // @ts-ignore setTimeout: (handler: Function, ms: number) => number + + setSkipAnimation: (skip: boolean) => void } } } @@ -60,6 +62,7 @@ beforeEach(() => { now: global.mockRaf.now, requestAnimationFrame: global.mockRaf.raf, colors, + skipAnimation: false, // This lets our useTransition hook force its component // to update from within an "onRest" handler. batchedUpdates: act, @@ -184,3 +187,9 @@ global.advanceUntilValue = (spring, value) => { return stop }) } + +global.setSkipAnimation = skip => { + Globals.assign({ + skipAnimation: skip, + }) +}