From 86f0bb01af1bff87abbe3fd60879bb453c57b76a Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Sun, 29 Sep 2019 16:13:50 -0400 Subject: [PATCH 01/14] feat: add "pause" prop --- packages/core/src/SpringValue.ts | 49 ++++++++++++++++------- packages/core/src/helpers.test.ts | 1 + packages/core/src/helpers.ts | 5 +++ packages/core/src/runAsync.ts | 65 ++++++++++++++++++++++++------- packages/core/src/types/props.ts | 6 +++ packages/shared/src/FrameLoop.ts | 25 ++++++++---- 6 files changed, 117 insertions(+), 34 deletions(-) diff --git a/packages/core/src/SpringValue.ts b/packages/core/src/SpringValue.ts index cde69af55..bfad3ef56 100644 --- a/packages/core/src/SpringValue.ts +++ b/packages/core/src/SpringValue.ts @@ -306,7 +306,10 @@ export class SpringValue extends FrameValue { */ pause() { checkDisposed(this, 'pause') - this._phase = PAUSED + if (!this.is(PAUSED)) { + this._phase = PAUSED + callProp(this._state.pause) + } } /** Resume the animation if paused. */ @@ -314,10 +317,7 @@ export class SpringValue extends FrameValue { checkDisposed(this, 'resume') if (this.is(PAUSED)) { this._start() - - if (this._state.asyncTo) { - this._state.unpause!() - } + callProp(this._state.unpause) } } @@ -434,7 +434,9 @@ export class SpringValue extends FrameValue { if (event.type == 'change') { if (!this.is(ACTIVE)) { this._reset() - this._start() + if (!this.is(PAUSED)) { + this._start() + } } } else if (event.type == 'priority') { this.priority = event.priority + 1 @@ -510,6 +512,13 @@ export class SpringValue extends FrameValue { /** Schedule an animation to run after an optional delay */ protected _update(props: SpringUpdate, isLoop?: boolean): AsyncResult { + const defaultProps = this._defaultProps + + // The default `pause` prop overrides all updates. + if (defaultProps.pause) { + props.pause = true + } + // Ensure the initial value can be accessed by animated components. const range = this._prepareNode(props) @@ -608,8 +617,7 @@ export class SpringValue extends FrameValue { if (props.default) { each(DEFAULT_PROPS, prop => { - // Default props can only be null, an object, or a function. - if (/^(function|object)$/.test(typeof props[prop])) { + if (prop in props) { defaultProps[prop] = props[prop] as any } }) @@ -773,10 +781,15 @@ export class SpringValue extends FrameValue { node.setValue(value) } + const paused = props.pause + if (paused) { + this.pause() + } + if (hasAsyncTo) { - return resolve( + resolve( runAsync( - props.to as any, + props.to, props, this._state, () => this.get(), @@ -787,9 +800,12 @@ export class SpringValue extends FrameValue { ) } - if (started) { - // Unpause the async animation if one exists. - this.resume() + // Start an animation + else if (started) { + if (!paused) { + // Unpause the async animation if one exists. + this.resume() + } if (reset) { // Must be idle for "onStart" to be called again. @@ -797,7 +813,12 @@ export class SpringValue extends FrameValue { } this._reset() - this._start() + + if (paused) { + this._phase = PAUSED + } else { + this._start() + } } // Postpone promise resolution until the animation is finished, diff --git a/packages/core/src/helpers.test.ts b/packages/core/src/helpers.test.ts index 08f325e63..b63a2a52e 100644 --- a/packages/core/src/helpers.test.ts +++ b/packages/core/src/helpers.test.ts @@ -19,6 +19,7 @@ describe('helpers', () => { ref: undefined, loop: undefined, reset: undefined, + pause: undefined, cancel: undefined, reverse: undefined, immediate: undefined, diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index 3965b8ce2..3dd5f0a1d 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -35,6 +35,9 @@ export const matchProp = ( (is.fun(value) ? value(key) : toArray(value).includes(key)) ) +export const concatFn = (first: T | undefined, last: T) => + first ? (...args: Parameters) => (first(...args), last(...args)) : last + type AnyProps = OneOrMore | ((i: number, arg: Arg) => T) export const getProps = ( @@ -47,6 +50,7 @@ export const getProps = ( /** These props can have default values */ export const DEFAULT_PROPS = [ + 'pause', 'config', 'immediate', 'onDelayEnd', @@ -64,6 +68,7 @@ const RESERVED_PROPS: Required = { ref: 1, loop: 1, reset: 1, + pause: 1, cancel: 1, reverse: 1, immediate: 1, diff --git a/packages/core/src/runAsync.ts b/packages/core/src/runAsync.ts index c8237dc37..c10c269d5 100644 --- a/packages/core/src/runAsync.ts +++ b/packages/core/src/runAsync.ts @@ -1,7 +1,7 @@ -import { is, each, Pick } from 'shared' +import { is, each, Pick, Timeout } from 'shared' import * as G from 'shared/globals' -import { matchProp, DEFAULT_PROPS, callProp } from './helpers' +import { matchProp, DEFAULT_PROPS, callProp, concatFn } from './helpers' import { AnimationResolver, ControllerUpdate, @@ -16,6 +16,7 @@ import { AnimationResult, AsyncResult } from './AnimationResult' export interface RunAsyncProps extends SpringProps { callId: number cancel: boolean + pause: boolean reset: boolean delay: number to?: any @@ -26,6 +27,8 @@ export interface RunAsyncState { asyncTo?: SpringChain | SpringToFn /** Resolves when the current `asyncTo` finishes or gets cancelled. */ promise?: AsyncResult + /** Call this to pause the `delay` prop */ + pause?: () => void /** Call this to unpause the current `asyncTo` function or array. */ unpause?: () => void /** The last time we saw a matching `cancel` prop. */ @@ -56,8 +59,16 @@ export async function runAsync( cancelled: true, } } + if (props.pause) { + await new Promise(next => { + state.unpause = concatFn(state.unpause, () => { + state.unpause = void 0 + next() + }) + }) + } // Wait for the previous async animation to be cancelled. - else if (props.reset) { + if (props.reset) { await state.promise } // Async animations are only replaced when "props.to" changes @@ -72,7 +83,7 @@ export async function runAsync( const defaultProps: SpringDefaultProps = {} each(DEFAULT_PROPS, prop => { if (prop == 'onRest') return - if (/function|object/.test(typeof props[prop])) { + if (props[prop]) { defaultProps[prop] = props[prop] as any } }) @@ -107,8 +118,11 @@ export async function runAsync( } if (getPaused()) { - state.unpause = await new Promise(resolve => { - state.unpause = resolve + await new Promise(resolve => { + state.unpause = concatFn(state.unpause, () => { + state.unpause = void 0 + resolve() + }) }) } @@ -163,8 +177,8 @@ export function cancelAsync(state: RunAsyncState, callId: number) { interface ScheduledProps { key?: string - props: Pick, 'cancel' | 'reset' | 'delay'> - state: { cancelId?: number } + props: Pick, 'cancel' | 'pause' | 'reset' | 'delay'> + state: RunAsyncState action: (props: RunAsyncProps, resolve: AnimationResolver) => void } @@ -177,11 +191,36 @@ export function scheduleProps( { key, props, state, action }: ScheduledProps ): AsyncResult { return new Promise((resolve, reject) => { - const delay = Math.max(0, callProp(props.delay || 0, key)) - if (delay > 0) G.frameLoop.setTimeout(run, delay) - else run() + const pause = matchProp(props.pause, key) + + let delay = Math.max(0, callProp(props.delay || 0, key)) + if (delay > 0) { + let timeout: Timeout + const onPause = () => { + state.pause = void 0 + state.unpause = concatFn(state.unpause, onResume) + timeout.cancel() + delay = Math.max(0, timeout.time - G.now()) + } + const onResume = () => { + state.unpause = void 0 + state.pause = concatFn(state.pause, onPause) + timeout = G.frameLoop.setTimeout(next, delay) + } + if (pause) { + state.unpause = concatFn(state.unpause, onResume) + } else { + timeout = G.frameLoop.setTimeout(next, delay) + state.pause = concatFn(state.pause, onPause) + } + } else { + next() + } - function run() { + function next() { + if (delay > 0) { + state.pause = void 0 + } let { cancel, reset } = props try { // Might have been cancelled during its delay. @@ -194,7 +233,7 @@ export function scheduleProps( } } reset = !cancel && matchProp(reset, key) - action({ ...props, callId, delay, cancel, reset }, resolve) + action({ ...props, callId, delay, cancel, pause, reset }, resolve) } catch (err) { reject(err) } diff --git a/packages/core/src/types/props.ts b/packages/core/src/types/props.ts index 64d2f3752..1b708689f 100644 --- a/packages/core/src/types/props.ts +++ b/packages/core/src/types/props.ts @@ -225,6 +225,11 @@ export interface AnimationProps { * or an array of keys. */ cancel?: MatchProp> + /** + * Pause all animations by using `true`, or some animations by using a key + * or an array of keys. + */ + pause?: MatchProp> /** * Start the next animations at their values in the `from` prop. */ @@ -267,6 +272,7 @@ export interface ReservedProps { to?: any ref?: any loop?: any + pause?: any reset?: any cancel?: any reverse?: any diff --git a/packages/shared/src/FrameLoop.ts b/packages/shared/src/FrameLoop.ts index 3772a564f..3a86093ce 100644 --- a/packages/shared/src/FrameLoop.ts +++ b/packages/shared/src/FrameLoop.ts @@ -14,6 +14,12 @@ export interface OpaqueAnimation { advance(dt: number): void } +export interface Timeout { + time: number + handler: () => void + cancel: () => void +} + /** * FrameLoop executes its animations in order of lowest priority first. * Animations are released once idle. The loop is paused while no animations @@ -38,7 +44,7 @@ export class FrameLoop { * `ms` delay is completed. When the delay is `<= 0`, the handler is * invoked immediately. */ - setTimeout: (handler: Function, ms: number) => void + setTimeout: (handler: () => void, ms: number) => Timeout /** * Execute a function once after all animations have updated. @@ -110,18 +116,23 @@ export class FrameLoop { } } - interface Timeout { - time: number - handler: Function - } - const timeoutQueue: Timeout[] = [] this.setTimeout = (handler, ms) => { const time = G.now() + ms + const cancel = () => { + const index = timeoutQueue.findIndex(t => t.cancel == cancel) + if (index >= 0) { + timeoutQueue.splice(index, 1) + } + } + const index = findIndex(timeoutQueue, t => t.time > time) - timeoutQueue.splice(index, 0, { time, handler }) + const timeout = { time, handler, cancel } + timeoutQueue.splice(index, 0, timeout) + kickoff() + return timeout } // Process the current frame. From b83b51d00f93848aa6c80b68aca0e3f2e59c0dcc Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Sun, 3 May 2020 21:45:43 -0400 Subject: [PATCH 02/14] fix: call "cancel" function before "delay" timeout --- packages/core/src/runAsync.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/core/src/runAsync.ts b/packages/core/src/runAsync.ts index c10c269d5..8906ad2a8 100644 --- a/packages/core/src/runAsync.ts +++ b/packages/core/src/runAsync.ts @@ -191,9 +191,17 @@ export function scheduleProps( { key, props, state, action }: ScheduledProps ): AsyncResult { return new Promise((resolve, reject) => { - const pause = matchProp(props.pause, key) + let delay = 0 + let pause = false - let delay = Math.max(0, callProp(props.delay || 0, key)) + let cancel = matchProp(props.cancel, key) + if (cancel) { + state.cancelId = callId + return next() + } + + pause = matchProp(props.pause, key) + delay = Math.max(0, callProp(props.delay || 0, key)) if (delay > 0) { let timeout: Timeout const onPause = () => { @@ -221,18 +229,12 @@ export function scheduleProps( if (delay > 0) { state.pause = void 0 } - let { cancel, reset } = props + // Maybe cancelled during its delay. + if (callId <= (state.cancelId || 0)) { + cancel = true + } try { - // Might have been cancelled during its delay. - if (callId <= (state.cancelId || 0)) { - cancel = true - } else { - cancel = matchProp(cancel, key) - if (cancel) { - state.cancelId = callId - } - } - reset = !cancel && matchProp(reset, key) + const reset = !cancel && matchProp(props.reset, key) action({ ...props, callId, delay, cancel, pause, reset }, resolve) } catch (err) { reject(err) From 1844cf99d3eb2d9530251be9389580d3c2a9b64b Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Sun, 3 May 2020 21:47:15 -0400 Subject: [PATCH 03/14] feat: let "cancel" prop have a default value This lets you cancel all animations until { cancel: false, default: true } is merged. --- packages/core/src/SpringValue.ts | 9 +++++++++ packages/core/src/helpers.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/packages/core/src/SpringValue.ts b/packages/core/src/SpringValue.ts index bfad3ef56..ceedfbd90 100644 --- a/packages/core/src/SpringValue.ts +++ b/packages/core/src/SpringValue.ts @@ -514,6 +514,15 @@ export class SpringValue extends FrameValue { protected _update(props: SpringUpdate, isLoop?: boolean): AsyncResult { const defaultProps = this._defaultProps + // Set the default `cancel` prop first, because it prevents other default + // props in this update from being cached. + if (props.default && !is.und(props.cancel)) { + defaultProps.cancel = props.cancel + } + // The default `cancel` prop overrides all updates. + else if (defaultProps.cancel) { + props.cancel = true + } // The default `pause` prop overrides all updates. if (defaultProps.pause) { props.pause = true diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index 3dd5f0a1d..abd7bef8a 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -51,6 +51,7 @@ export const getProps = ( /** These props can have default values */ export const DEFAULT_PROPS = [ 'pause', + 'cancel', 'config', 'immediate', 'onDelayEnd', From 8143d91f4befc0c873639cc53bdbd64635968e7c Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Mon, 4 May 2020 18:25:55 -0400 Subject: [PATCH 04/14] chore: refactor `runAsync` function - pass an `AnimationTarget` object to `runAsync` function - check for invalidated `runAsync` call whenever an `animate` promise is resolved - only set `state.asyncTo` on a default cancel, because using `cancel: true` without `default: true` should only affect the update it's defined in - use `getResult` functions for brevity --- packages/core/src/Controller.ts | 17 +++-------- packages/core/src/SpringValue.ts | 14 ++------- packages/core/src/runAsync.ts | 49 ++++++++++++++++++-------------- 3 files changed, 33 insertions(+), 47 deletions(-) diff --git a/packages/core/src/Controller.ts b/packages/core/src/Controller.ts index 8e1c699c5..cdf691720 100644 --- a/packages/core/src/Controller.ts +++ b/packages/core/src/Controller.ts @@ -5,6 +5,7 @@ import { Lookup, Falsy } from './types/common' import { inferTo } from './helpers' import { FrameValue } from './FrameValue' import { SpringPhase, CREATED, ACTIVE, IDLE } from './SpringPhase' +import { SpringValue, createLoopUpdate, createUpdate } from './SpringValue' import { getCombinedResult, AnimationResult, @@ -88,7 +89,7 @@ export class Controller */ get idle() { return ( - !this._state.promise && + !this._state.asyncTo && Object.values(this.springs as Lookup).every( spring => spring.idle ) @@ -310,22 +311,12 @@ export function flushUpdate( state, action(props, resolve) { props.onRest = onRest as any - resolve( - runAsync( - asyncTo, - props, - state, - ctrl.get.bind(ctrl), - () => false, // TODO: add pausing to Controller - ctrl.start.bind(ctrl) as any, - ctrl.stop.bind(ctrl) - ) - ) + resolve(runAsync(asyncTo, props, state, ctrl)) }, }) ) } - // Cancel an active "asyncTo" if desired. + // Respect the `cancel` prop when no keys are affected. else if (!props.keys && props.cancel === true) { cancelAsync(state, ctrl['_lastAsyncId']) } diff --git a/packages/core/src/SpringValue.ts b/packages/core/src/SpringValue.ts index ceedfbd90..b12aeb888 100644 --- a/packages/core/src/SpringValue.ts +++ b/packages/core/src/SpringValue.ts @@ -107,7 +107,7 @@ export class SpringValue extends FrameValue { } get idle() { - return !this.is(ACTIVE) && !this._state.promise + return !this.is(ACTIVE) && !this._state.asyncTo } get goal() { @@ -796,17 +796,7 @@ export class SpringValue extends FrameValue { } if (hasAsyncTo) { - resolve( - runAsync( - props.to, - props, - this._state, - () => this.get(), - () => this.is(PAUSED), - this.start.bind(this), - this.stop.bind(this) as any - ) - ) + resolve(runAsync(props.to, props, this._state, this)) } // Start an animation diff --git a/packages/core/src/runAsync.ts b/packages/core/src/runAsync.ts index 8906ad2a8..4f479dad2 100644 --- a/packages/core/src/runAsync.ts +++ b/packages/core/src/runAsync.ts @@ -8,10 +8,15 @@ import { SpringChain, SpringDefaultProps, SpringProps, - SpringStopFn, SpringToFn, } from './types' -import { AnimationResult, AsyncResult } from './AnimationResult' +import { + getCancelledResult, + getFinishedResult, + AnimationResult, + AsyncResult, + AnimationTarget, +} from './AnimationResult' export interface RunAsyncProps extends SpringProps { callId: number @@ -47,17 +52,14 @@ export async function runAsync( to: SpringChain | SpringToFn, props: RunAsyncProps, state: RunAsyncState, - getValue: () => T, - getPaused: () => boolean, - update: (props: any) => AsyncResult, - stop: SpringStopFn + target: AnimationTarget ): AsyncResult { if (props.cancel) { - state.asyncTo = undefined - return { - value: getValue(), - cancelled: true, + // Stop the active `asyncTo` only on "default cancel". + if (props.default) { + state.asyncTo = undefined } + return getCancelledResult(target) } if (props.pause) { await new Promise(next => { @@ -89,17 +91,21 @@ export async function runAsync( }) const { callId, onRest } = props - - // Note: This function cannot be async, because `checkFailConditions` must be sync. - const animate: any = (arg1: any, arg2?: any) => { + const throwInvalidated = () => { // Prevent further animation if cancelled. if (callId <= (state.cancelId || 0)) { - throw (result = { value: getValue(), cancelled: true }) + throw (result = getCancelledResult(target)) } // Prevent further animation if another "runAsync" call is active. if (to !== state.asyncTo) { - throw (result = { value: getValue(), finished: false }) + throw (result = getFinishedResult(target, false)) } + } + + // Note: This function cannot use the `async` keyword, because we want the + // `throw` statements to interrupt the caller. + const animate: any = (arg1: any, arg2?: any) => { + throwInvalidated() const props: ControllerUpdate = is.obj(arg1) ? { ...arg1 } @@ -112,12 +118,14 @@ export async function runAsync( }) const parentTo = state.asyncTo - return update(props).then(async result => { + return target.start(props).then(async result => { + throwInvalidated() + if (state.asyncTo == null) { state.asyncTo = parentTo } - if (getPaused()) { + if (target.is('PAUSED')) { await new Promise(resolve => { state.unpause = concatFn(state.unpause, () => { state.unpause = void 0 @@ -139,12 +147,9 @@ export async function runAsync( } // Async script else if (is.fun(to)) { - await to(animate, stop) - } - result = { - value: getValue(), - finished: true, + await to(animate, target.stop.bind(target) as any) } + result = getFinishedResult(target, true) } catch (err) { if (err !== result) { throw err From c47c5475293b8e0d5382907067df5987d57608ad Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Mon, 4 May 2020 22:13:29 -0400 Subject: [PATCH 05/14] refactor: remove `reset` coercion from `scheduleProps` function The `scheduleProps` function is concerned with delays and pausing. The `reset` prop is none of its business. --- packages/core/src/SpringValue.ts | 6 +++--- packages/core/src/runAsync.ts | 6 ++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/core/src/SpringValue.ts b/packages/core/src/SpringValue.ts index b12aeb888..a133491a1 100644 --- a/packages/core/src/SpringValue.ts +++ b/packages/core/src/SpringValue.ts @@ -658,11 +658,11 @@ export class SpringValue extends FrameValue { /** When true, start at the "from" value. */ const reset = - // Can't reset if no "from" value exists. - (props.reset && !is.und(from)) || // We want { from } to imply { reset: true } // unless default values are being set. - (hasFromProp && !props.default) + (hasFromProp && !props.default) || + // Can't reset if no "from" value exists. + (!is.und(from) && matchProp(props.reset, key)) // The current value, where the animation starts from. const value = reset ? (from as T) : this.get() diff --git a/packages/core/src/runAsync.ts b/packages/core/src/runAsync.ts index 4f479dad2..530e2e7d8 100644 --- a/packages/core/src/runAsync.ts +++ b/packages/core/src/runAsync.ts @@ -22,7 +22,6 @@ export interface RunAsyncProps extends SpringProps { callId: number cancel: boolean pause: boolean - reset: boolean delay: number to?: any } @@ -182,7 +181,7 @@ export function cancelAsync(state: RunAsyncState, callId: number) { interface ScheduledProps { key?: string - props: Pick, 'cancel' | 'pause' | 'reset' | 'delay'> + props: Pick, 'cancel' | 'pause' | 'delay'> state: RunAsyncState action: (props: RunAsyncProps, resolve: AnimationResolver) => void } @@ -239,8 +238,7 @@ export function scheduleProps( cancel = true } try { - const reset = !cancel && matchProp(props.reset, key) - action({ ...props, callId, delay, cancel, pause, reset }, resolve) + action({ ...props, callId, delay, cancel, pause }, resolve) } catch (err) { reject(err) } From 3f88545227ec800f425704db9cb039984727b8c7 Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Tue, 5 May 2020 10:36:28 -0400 Subject: [PATCH 06/14] feat: let `default` prop be an object The object contains new values for default props. This commit also moves special handling for default `immediate: true` into the `useSprings` hook, which is what it's really intended for. This means you can use `immediate: true` in a declarative update without inadvertently making future updates immediate by default. If you really want such behavior, you can still use `default: { immediate: true }` in a declarative update. In imperative updates, using `default: true` will still set the default value for the `immediate` prop, if it's defined in the update. --- packages/core/src/SpringValue.ts | 10 +++++++--- packages/core/src/helpers.ts | 21 +++++++++++++++++++++ packages/core/src/hooks/useSprings.ts | 13 +++++++++++-- packages/core/src/hooks/useTransition.tsx | 8 ++------ packages/core/src/runAsync.ts | 12 +++++------- packages/core/src/types/props.ts | 2 +- 6 files changed, 47 insertions(+), 19 deletions(-) diff --git a/packages/core/src/SpringValue.ts b/packages/core/src/SpringValue.ts index a133491a1..e2a201f7e 100644 --- a/packages/core/src/SpringValue.ts +++ b/packages/core/src/SpringValue.ts @@ -34,9 +34,9 @@ import { import { callProp, computeGoal, - DEFAULT_PROPS, matchProp, inferTo, + mergeDefaultProps, } from './helpers' import { FrameValue, isFrameValue } from './FrameValue' import { @@ -591,6 +591,8 @@ export class SpringValue extends FrameValue { onDelayEnd(props, this) } + mergeDefaultProps(defaultProps, props) + const { to: prevTo, from: prevFrom } = anim let { to = prevTo, from = prevFrom } = range @@ -644,8 +646,10 @@ export class SpringValue extends FrameValue { mergeConfig( config, callProp(props.config, key!), - // Avoid calling the "config" prop twice when "default" is true. - props.default ? undefined : callProp(defaultProps.config, key!) + // Avoid calling the same "config" prop twice. + props.config !== defaultProps.config + ? callProp(defaultProps.config, key!) + : void 0 ) } diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index abd7bef8a..99ef2af20 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -8,6 +8,7 @@ import { AnyFn, OneOrMore, FluidValue, + Lookup, } from 'shared' import * as G from 'shared/globals' import { ReservedProps, ForwardProps, InferTo } from './types' @@ -48,6 +49,26 @@ export const getProps = ( props && (is.fun(props) ? props(i, arg) : is.arr(props) ? props[i] : { ...props }) +export const mergeDefaultProps = ( + defaultProps: Lookup, + props: Lookup & { default?: boolean | Lookup } +) => { + if (props.default === true) { + each(DEFAULT_PROPS, key => { + const value = props[key] + if (!is.und(value)) { + defaultProps[key] = value as any + } + }) + } else if (props.default) { + each(props.default, (value, key) => { + if (!is.und(value)) { + defaultProps[key] = value as any + } + }) + } +} + /** These props can have default values */ export const DEFAULT_PROPS = [ 'pause', diff --git a/packages/core/src/hooks/useSprings.ts b/packages/core/src/hooks/useSprings.ts index 36850dc55..77e35a0c9 100644 --- a/packages/core/src/hooks/useSprings.ts +++ b/packages/core/src/hooks/useSprings.ts @@ -27,7 +27,7 @@ import { flushUpdateQueue, setSprings, } from '../Controller' -import { useMemo as useMemoOne } from '../helpers' +import { useMemo as useMemoOne, mergeDefaultProps } from '../helpers' import { SpringHandle } from '../SpringHandle' export type UseSpringsProps = unknown & @@ -150,7 +150,16 @@ export function useSprings( if (update) { update = updates[i] = createUpdate(update) - update.default = true + if (!update.default) { + // Declarative updates always set the default props. + const defaultProps: Lookup = (update.default = {}) + mergeDefaultProps(defaultProps, update) + + // Avoid forcing `immediate: true` onto imperative updates. + if (defaultProps.immediate === true) { + defaultProps.immediate = undefined + } + } if (i == 0) { refProp.current = update.ref update.ref = undefined diff --git a/packages/core/src/hooks/useTransition.tsx b/packages/core/src/hooks/useTransition.tsx index c5e6688fe..61e07987a 100644 --- a/packages/core/src/hooks/useTransition.tsx +++ b/packages/core/src/hooks/useTransition.tsx @@ -24,7 +24,7 @@ import { TransitionDefaultProps, } from '../types' import { Valid } from '../types/common' -import { DEFAULT_PROPS, callProp, inferTo } from '../helpers' +import { callProp, inferTo, mergeDefaultProps } from '../helpers' import { Controller, getSprings, setSprings } from '../Controller' import { SpringHandle } from '../SpringHandle' import { @@ -143,11 +143,7 @@ export function useTransition( const forceUpdate = useForceUpdate() const defaultProps = {} as TransitionDefaultProps - each(DEFAULT_PROPS, prop => { - if (/function|object/.test(typeof props[prop])) { - defaultProps[prop] = props[prop] as any - } - }) + mergeDefaultProps(defaultProps, props) // Generate changes to apply in useEffect. const changes = new Map() diff --git a/packages/core/src/runAsync.ts b/packages/core/src/runAsync.ts index 530e2e7d8..6b4f81e9c 100644 --- a/packages/core/src/runAsync.ts +++ b/packages/core/src/runAsync.ts @@ -1,7 +1,7 @@ import { is, each, Pick, Timeout } from 'shared' import * as G from 'shared/globals' -import { matchProp, DEFAULT_PROPS, callProp, concatFn } from './helpers' +import { matchProp, callProp, mergeDefaultProps, concatFn } from './helpers' import { AnimationResolver, ControllerUpdate, @@ -82,12 +82,10 @@ export async function runAsync( let result!: AnimationResult const defaultProps: SpringDefaultProps = {} - each(DEFAULT_PROPS, prop => { - if (prop == 'onRest') return - if (props[prop]) { - defaultProps[prop] = props[prop] as any - } - }) + mergeDefaultProps(defaultProps, props) + + // The `onRest` prop is for `runAsync` to call. + defaultProps.onRest = undefined const { callId, onRest } = props const throwInvalidated = () => { diff --git a/packages/core/src/types/props.ts b/packages/core/src/types/props.ts index 1b708689f..632529c28 100644 --- a/packages/core/src/types/props.ts +++ b/packages/core/src/types/props.ts @@ -241,7 +241,7 @@ export interface AnimationProps { /** * Override the default props with this update. */ - default?: boolean + default?: boolean | SpringDefaultProps } /** Default props for a `SpringValue` object */ From 4cfa517c27517bda53b3334a5cb29bb2a812b7c2 Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Tue, 5 May 2020 10:48:30 -0400 Subject: [PATCH 07/14] fix: use Set objects for pause/resume queues In contrast with the `concatFn` solution, this lets us remove a handler from the queue. --- packages/core/src/Controller.ts | 22 +++-------- packages/core/src/SpringValue.ts | 10 +++-- packages/core/src/helpers.ts | 14 +++++++ packages/core/src/runAsync.ts | 68 +++++++++++++++----------------- 4 files changed, 59 insertions(+), 55 deletions(-) diff --git a/packages/core/src/Controller.ts b/packages/core/src/Controller.ts index cdf691720..7e26c4df0 100644 --- a/packages/core/src/Controller.ts +++ b/packages/core/src/Controller.ts @@ -2,7 +2,7 @@ import { is, each, OneOrMore, toArray, UnknownProps } from 'shared' import * as G from 'shared/globals' import { Lookup, Falsy } from './types/common' -import { inferTo } from './helpers' +import { inferTo, flush } from './helpers' import { FrameValue } from './FrameValue' import { SpringPhase, CREATED, ACTIVE, IDLE } from './SpringPhase' import { SpringValue, createLoopUpdate, createUpdate } from './SpringValue' @@ -59,7 +59,10 @@ export class Controller protected _active = new Set() /** State used by the `runAsync` function */ - protected _state: RunAsyncState = {} + protected _state: RunAsyncState = { + pauseQueue: new Set(), + resumeQueue: new Set(), + } /** The event queues that are flushed once per frame maximum */ protected _events = { @@ -202,7 +205,7 @@ export class Controller // The "onRest" queue is only flushed when all springs are idle. if (!isActive) { this._phase = IDLE - flush(onRest, (result, onRest) => { + flush(onRest, ([onRest, result]) => { result.value = values onRest(result) }) @@ -218,19 +221,6 @@ export class Controller } } -/** Basic helper for clearing a queue after processing it */ -function flush( - queue: Map, - iterator: (value: T, key: P) => void -): void -function flush(queue: Set, iterator: (value: T) => void): void -function flush(queue: any, iterator: any) { - if (queue.size) { - each(queue, iterator) - queue.clear() - } -} - /** * Warning: Props might be mutated. */ diff --git a/packages/core/src/SpringValue.ts b/packages/core/src/SpringValue.ts index e2a201f7e..22529216f 100644 --- a/packages/core/src/SpringValue.ts +++ b/packages/core/src/SpringValue.ts @@ -36,6 +36,7 @@ import { computeGoal, matchProp, inferTo, + flush, mergeDefaultProps, } from './helpers' import { FrameValue, isFrameValue } from './FrameValue' @@ -84,7 +85,10 @@ export class SpringValue extends FrameValue { protected _phase: SpringPhase = CREATED /** The state for `runAsync` calls */ - protected _state: RunAsyncState = {} + protected _state: RunAsyncState = { + pauseQueue: new Set(), + resumeQueue: new Set(), + } /** Some props have customizable default values */ protected _defaultProps = {} as SpringDefaultProps @@ -308,7 +312,7 @@ export class SpringValue extends FrameValue { checkDisposed(this, 'pause') if (!this.is(PAUSED)) { this._phase = PAUSED - callProp(this._state.pause) + flush(this._state.pauseQueue, onPause => onPause()) } } @@ -317,7 +321,7 @@ export class SpringValue extends FrameValue { checkDisposed(this, 'resume') if (this.is(PAUSED)) { this._start() - callProp(this._state.unpause) + flush(this._state.resumeQueue, onResume => onResume()) } } diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index 99ef2af20..b07081441 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -163,3 +163,17 @@ export function computeGoal(value: T | FluidValue): T { })(1) as any) : value } + +/** Basic helper for clearing a queue after processing it */ +export function flush( + queue: Map, + iterator: (entry: [P, T]) => void +): void +export function flush(queue: Set, iterator: (value: T) => void): void +export function flush(queue: any, iterator: any) { + if (queue.size) { + const items = Array.from(queue) + queue.clear() + each(items, iterator) + } +} diff --git a/packages/core/src/runAsync.ts b/packages/core/src/runAsync.ts index 6b4f81e9c..f38cc5631 100644 --- a/packages/core/src/runAsync.ts +++ b/packages/core/src/runAsync.ts @@ -27,14 +27,14 @@ export interface RunAsyncProps extends SpringProps { } export interface RunAsyncState { + /** Functions to be called once paused */ + pauseQueue: Set + /** Functions to be called once resumed */ + resumeQueue: Set /** The async function or array of spring props */ asyncTo?: SpringChain | SpringToFn /** Resolves when the current `asyncTo` finishes or gets cancelled. */ promise?: AsyncResult - /** Call this to pause the `delay` prop */ - pause?: () => void - /** Call this to unpause the current `asyncTo` function or array. */ - unpause?: () => void /** The last time we saw a matching `cancel` prop. */ cancelId?: number } @@ -61,11 +61,8 @@ export async function runAsync( return getCancelledResult(target) } if (props.pause) { - await new Promise(next => { - state.unpause = concatFn(state.unpause, () => { - state.unpause = void 0 - next() - }) + await new Promise(resume => { + state.resumeQueue.add(resume) }) } // Wait for the previous async animation to be cancelled. @@ -123,11 +120,8 @@ export async function runAsync( } if (target.is('PAUSED')) { - await new Promise(resolve => { - state.unpause = concatFn(state.unpause, () => { - state.unpause = void 0 - resolve() - }) + await new Promise(resume => { + state.resumeQueue.add(resume) }) } @@ -194,47 +188,49 @@ export function scheduleProps( ): AsyncResult { return new Promise((resolve, reject) => { let delay = 0 - let pause = false + let timeout: Timeout + let pause = false let cancel = matchProp(props.cancel, key) + if (cancel) { state.cancelId = callId - return next() + return onStart() } pause = matchProp(props.pause, key) delay = Math.max(0, callProp(props.delay || 0, key)) + if (delay > 0) { - let timeout: Timeout - const onPause = () => { - state.pause = void 0 - state.unpause = concatFn(state.unpause, onResume) - timeout.cancel() - delay = Math.max(0, timeout.time - G.now()) - } - const onResume = () => { - state.unpause = void 0 - state.pause = concatFn(state.pause, onPause) - timeout = G.frameLoop.setTimeout(next, delay) - } if (pause) { - state.unpause = concatFn(state.unpause, onResume) + state.resumeQueue.add(onResume) } else { - timeout = G.frameLoop.setTimeout(next, delay) - state.pause = concatFn(state.pause, onPause) + timeout = G.frameLoop.setTimeout(onStart, delay) + state.pauseQueue.add(onPause) } } else { - next() + onStart() } - function next() { - if (delay > 0) { - state.pause = void 0 - } + function onPause() { + state.resumeQueue.add(onResume) + timeout.cancel() + delay = Math.max(0, timeout.time - G.now()) + } + + function onResume() { + state.pauseQueue.add(onPause) + timeout = G.frameLoop.setTimeout(onStart, delay) + } + + function onStart() { + state.pauseQueue.delete(onPause) + // Maybe cancelled during its delay. if (callId <= (state.cancelId || 0)) { cancel = true } + try { action({ ...props, callId, delay, cancel, pause }, resolve) } catch (err) { From 85b04ee0506f2c98bf161ad377b7e01273e52331 Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Tue, 5 May 2020 10:53:09 -0400 Subject: [PATCH 08/14] fix: pass a `pause` action to `scheduleProps` ..which lets it pause the current animation as well as the next animation. --- packages/core/src/Controller.ts | 11 +++++++---- packages/core/src/SpringValue.ts | 5 +++-- packages/core/src/runAsync.ts | 16 ++++++++++------ 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/core/src/Controller.ts b/packages/core/src/Controller.ts index 7e26c4df0..7d8ad226b 100644 --- a/packages/core/src/Controller.ts +++ b/packages/core/src/Controller.ts @@ -1,4 +1,4 @@ -import { is, each, OneOrMore, toArray, UnknownProps } from 'shared' +import { is, each, OneOrMore, toArray, UnknownProps, noop } from 'shared' import * as G from 'shared/globals' import { Lookup, Falsy } from './types/common' @@ -299,9 +299,12 @@ export function flushUpdate( scheduleProps(++ctrl['_lastAsyncId'], { props, state, - action(props, resolve) { - props.onRest = onRest as any - resolve(runAsync(asyncTo, props, state, ctrl)) + actions: { + pause: noop, + start(props, resolve) { + props.onRest = onRest as any + resolve(runAsync(asyncTo, props, state, ctrl)) + }, }, }) ) diff --git a/packages/core/src/SpringValue.ts b/packages/core/src/SpringValue.ts index 22529216f..746f2d5ba 100644 --- a/packages/core/src/SpringValue.ts +++ b/packages/core/src/SpringValue.ts @@ -539,8 +539,9 @@ export class SpringValue extends FrameValue { key: this.key, props, state: this._state, - action: (props, resolve) => { - this._merge(range, props, resolve) + actions: { + pause: this.pause.bind(this), + start: this._merge.bind(this, range), }, }).then(result => { if (props.loop && result.finished && !(isLoop && result.noop)) { diff --git a/packages/core/src/runAsync.ts b/packages/core/src/runAsync.ts index f38cc5631..6932bcb33 100644 --- a/packages/core/src/runAsync.ts +++ b/packages/core/src/runAsync.ts @@ -168,23 +168,26 @@ export function cancelAsync(state: RunAsyncState, callId: number) { } // -// scheduleProps(props, state, action) +// scheduleProps // interface ScheduledProps { key?: string props: Pick, 'cancel' | 'pause' | 'delay'> state: RunAsyncState - action: (props: RunAsyncProps, resolve: AnimationResolver) => void + actions: { + pause: () => void + start: (props: RunAsyncProps, resolve: AnimationResolver) => void + } } /** - * Pass a copy of the given props to an `action` after any delay is finished - * and the props weren't cancelled before then. + * This function sets a timeout if the `delay` prop exists. The `cancel` and + * `pause` props are both coerced into booleans too. */ export function scheduleProps( callId: number, - { key, props, state, action }: ScheduledProps + { key, props, state, actions }: ScheduledProps ): AsyncResult { return new Promise((resolve, reject) => { let delay = 0 @@ -204,6 +207,7 @@ export function scheduleProps( if (delay > 0) { if (pause) { state.resumeQueue.add(onResume) + actions.pause() } else { timeout = G.frameLoop.setTimeout(onStart, delay) state.pauseQueue.add(onPause) @@ -232,7 +236,7 @@ export function scheduleProps( } try { - action({ ...props, callId, delay, cancel, pause }, resolve) + actions.start({ ...props, callId, delay, cancel, pause }, resolve) } catch (err) { reject(err) } From 66149c5e65e954b6d24e0274a5be000439bd66cf Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Tue, 5 May 2020 10:56:04 -0400 Subject: [PATCH 09/14] nit: check `delay > 0` in the `onResume` function --- packages/core/src/runAsync.ts | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/core/src/runAsync.ts b/packages/core/src/runAsync.ts index 6932bcb33..e3e1338a3 100644 --- a/packages/core/src/runAsync.ts +++ b/packages/core/src/runAsync.ts @@ -190,41 +190,39 @@ export function scheduleProps( { key, props, state, actions }: ScheduledProps ): AsyncResult { return new Promise((resolve, reject) => { - let delay = 0 + let delay: number let timeout: Timeout let pause = false let cancel = matchProp(props.cancel, key) if (cancel) { - state.cancelId = callId - return onStart() - } - - pause = matchProp(props.pause, key) - delay = Math.max(0, callProp(props.delay || 0, key)) - - if (delay > 0) { + onStart() + } else { + delay = Math.max(0, callProp(props.delay || 0, key)) + pause = matchProp(props.pause, key) if (pause) { state.resumeQueue.add(onResume) actions.pause() } else { - timeout = G.frameLoop.setTimeout(onStart, delay) - state.pauseQueue.add(onPause) + onResume() } - } else { - onStart() } function onPause() { state.resumeQueue.add(onResume) timeout.cancel() - delay = Math.max(0, timeout.time - G.now()) + // Cache the remaining delay. + delay = timeout.time - G.now() } function onResume() { - state.pauseQueue.add(onPause) - timeout = G.frameLoop.setTimeout(onStart, delay) + if (delay > 0) { + state.pauseQueue.add(onPause) + timeout = G.frameLoop.setTimeout(onStart, delay) + } else { + onStart() + } } function onStart() { From 78ee1d8ac1e819bd403f71507096e3b2090bfc1a Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Tue, 5 May 2020 12:27:24 -0400 Subject: [PATCH 10/14] fix: stop handling the `pause` prop in SpringValue#_merge The `runAsync` function already handles it for us --- packages/core/src/SpringValue.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/packages/core/src/SpringValue.ts b/packages/core/src/SpringValue.ts index 746f2d5ba..fa1a5fb4b 100644 --- a/packages/core/src/SpringValue.ts +++ b/packages/core/src/SpringValue.ts @@ -799,21 +799,14 @@ export class SpringValue extends FrameValue { node.setValue(value) } - const paused = props.pause - if (paused) { - this.pause() - } - if (hasAsyncTo) { resolve(runAsync(props.to, props, this._state, this)) } // Start an animation else if (started) { - if (!paused) { - // Unpause the async animation if one exists. - this.resume() - } + // Unpause the async animation if one exists. + this.resume() if (reset) { // Must be idle for "onStart" to be called again. @@ -821,12 +814,7 @@ export class SpringValue extends FrameValue { } this._reset() - - if (paused) { - this._phase = PAUSED - } else { - this._start() - } + this._start() } // Postpone promise resolution until the animation is finished, From 995eaaed24e1819d7c3400ef92c3135726f9a0e4 Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Tue, 5 May 2020 12:39:49 -0400 Subject: [PATCH 11/14] nit: remove unnecessary `Math.max` call --- packages/core/src/runAsync.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/runAsync.ts b/packages/core/src/runAsync.ts index e3e1338a3..870a7f6cb 100644 --- a/packages/core/src/runAsync.ts +++ b/packages/core/src/runAsync.ts @@ -199,7 +199,7 @@ export function scheduleProps( if (cancel) { onStart() } else { - delay = Math.max(0, callProp(props.delay || 0, key)) + delay = callProp(props.delay || 0, key) pause = matchProp(props.pause, key) if (pause) { state.resumeQueue.add(onResume) From cbe93403b41ab4e1fcc0aaf5f6d8f5734632109d Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Tue, 5 May 2020 12:39:59 -0400 Subject: [PATCH 12/14] nit: tweak the `scheduleProps` description --- packages/core/src/runAsync.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core/src/runAsync.ts b/packages/core/src/runAsync.ts index 870a7f6cb..4a02e417b 100644 --- a/packages/core/src/runAsync.ts +++ b/packages/core/src/runAsync.ts @@ -182,8 +182,11 @@ interface ScheduledProps { } /** - * This function sets a timeout if the `delay` prop exists. The `cancel` and - * `pause` props are both coerced into booleans too. + * This function sets a timeout if both the `delay` prop exists and + * the `cancel` prop is not `true`. + * + * The `actions.start` function must handle the `cancel` prop itself, + * but the `pause` prop is taken care of. */ export function scheduleProps( callId: number, From 3a9d106240246f3219bf3af38b99c851c5d62f1e Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Tue, 5 May 2020 12:38:37 -0400 Subject: [PATCH 13/14] fix(Controller): handle the `cancel` prop in `runAsync` start callback Remove `cancel` handling from the `runAsync` function, forcing callers to implement cancellation manually. --- packages/core/src/Controller.ts | 11 +++++++++-- packages/core/src/helpers.ts | 6 ++++++ packages/core/src/runAsync.ts | 7 ------- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/core/src/Controller.ts b/packages/core/src/Controller.ts index 7d8ad226b..3ab165b65 100644 --- a/packages/core/src/Controller.ts +++ b/packages/core/src/Controller.ts @@ -2,7 +2,7 @@ import { is, each, OneOrMore, toArray, UnknownProps, noop } from 'shared' import * as G from 'shared/globals' import { Lookup, Falsy } from './types/common' -import { inferTo, flush } from './helpers' +import { inferTo, flush, isDefaultProp } from './helpers' import { FrameValue } from './FrameValue' import { SpringPhase, CREATED, ACTIVE, IDLE } from './SpringPhase' import { SpringValue, createLoopUpdate, createUpdate } from './SpringValue' @@ -303,7 +303,14 @@ export function flushUpdate( pause: noop, start(props, resolve) { props.onRest = onRest as any - resolve(runAsync(asyncTo, props, state, ctrl)) + if (!props.cancel) { + resolve(runAsync(asyncTo, props, state, ctrl)) + } + // Prevent `cancel: true` from ending the current `runAsync` call, + // except when the default `cancel` prop is being set. + else if (isDefaultProp(props, 'cancel')) { + cancelAsync(state, props.callId) + } }, }, }) diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index b07081441..a1da25d82 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -49,6 +49,12 @@ export const getProps = ( props && (is.fun(props) ? props(i, arg) : is.arr(props) ? props[i] : { ...props }) +/** Returns `true` if the given prop is having its default value set. */ +export const isDefaultProp = (props: T, key: keyof T) => + !is.und( + props.default === true ? props[key] : props.default && props.default[key] + ) + export const mergeDefaultProps = ( defaultProps: Lookup, props: Lookup & { default?: boolean | Lookup } diff --git a/packages/core/src/runAsync.ts b/packages/core/src/runAsync.ts index 4a02e417b..05d436348 100644 --- a/packages/core/src/runAsync.ts +++ b/packages/core/src/runAsync.ts @@ -53,13 +53,6 @@ export async function runAsync( state: RunAsyncState, target: AnimationTarget ): AsyncResult { - if (props.cancel) { - // Stop the active `asyncTo` only on "default cancel". - if (props.default) { - state.asyncTo = undefined - } - return getCancelledResult(target) - } if (props.pause) { await new Promise(resume => { state.resumeQueue.add(resume) From 38d29a1b717f8aacc1a7236c41d115e5d5e95c84 Mon Sep 17 00:00:00 2001 From: Alec Larson Date: Tue, 5 May 2020 13:11:28 -0400 Subject: [PATCH 14/14] fix: merging of default `cancel` and `pause` props --- packages/core/src/Controller.ts | 4 ++-- packages/core/src/SpringValue.ts | 31 +++++++++++++++++-------------- packages/core/src/helpers.ts | 21 ++++++++++++++------- packages/core/src/runAsync.ts | 5 +---- 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/packages/core/src/Controller.ts b/packages/core/src/Controller.ts index 3ab165b65..cb258bcba 100644 --- a/packages/core/src/Controller.ts +++ b/packages/core/src/Controller.ts @@ -2,7 +2,7 @@ import { is, each, OneOrMore, toArray, UnknownProps, noop } from 'shared' import * as G from 'shared/globals' import { Lookup, Falsy } from './types/common' -import { inferTo, flush, isDefaultProp } from './helpers' +import { inferTo, flush, hasDefaultProp } from './helpers' import { FrameValue } from './FrameValue' import { SpringPhase, CREATED, ACTIVE, IDLE } from './SpringPhase' import { SpringValue, createLoopUpdate, createUpdate } from './SpringValue' @@ -308,7 +308,7 @@ export function flushUpdate( } // Prevent `cancel: true` from ending the current `runAsync` call, // except when the default `cancel` prop is being set. - else if (isDefaultProp(props, 'cancel')) { + else if (hasDefaultProp(props, 'cancel')) { cancelAsync(state, props.callId) } }, diff --git a/packages/core/src/SpringValue.ts b/packages/core/src/SpringValue.ts index fa1a5fb4b..a275ec9eb 100644 --- a/packages/core/src/SpringValue.ts +++ b/packages/core/src/SpringValue.ts @@ -38,6 +38,7 @@ import { inferTo, flush, mergeDefaultProps, + getDefaultProp, } from './helpers' import { FrameValue, isFrameValue } from './FrameValue' import { @@ -516,22 +517,24 @@ export class SpringValue extends FrameValue { /** Schedule an animation to run after an optional delay */ protected _update(props: SpringUpdate, isLoop?: boolean): AsyncResult { + type DefaultProps = typeof defaultProps const defaultProps = this._defaultProps - - // Set the default `cancel` prop first, because it prevents other default - // props in this update from being cached. - if (props.default && !is.und(props.cancel)) { - defaultProps.cancel = props.cancel - } - // The default `cancel` prop overrides all updates. - else if (defaultProps.cancel) { - props.cancel = true - } - // The default `pause` prop overrides all updates. - if (defaultProps.pause) { - props.pause = true + const mergeDefaultProp = (key: keyof DefaultProps) => { + const value = getDefaultProp(props, key) + if (!is.und(value)) { + defaultProps[key] = value as any + } + // For `cancel` and `pause`, a truthy default always wins. + if (defaultProps[key]) { + props[key] = defaultProps[key] as any + } } + // These props are coerced into booleans by the `scheduleProps` function, + // so they need their default values processed before then. + mergeDefaultProp('cancel') + mergeDefaultProp('pause') + // Ensure the initial value can be accessed by animated components. const range = this._prepareNode(props) @@ -596,7 +599,7 @@ export class SpringValue extends FrameValue { onDelayEnd(props, this) } - mergeDefaultProps(defaultProps, props) + mergeDefaultProps(defaultProps, props, ['pause', 'cancel']) const { to: prevTo, from: prevFrom } = anim let { to = prevTo, from = prevFrom } = range diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index a1da25d82..6fa8fbc2f 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -50,25 +50,32 @@ export const getProps = ( (is.fun(props) ? props(i, arg) : is.arr(props) ? props[i] : { ...props }) /** Returns `true` if the given prop is having its default value set. */ -export const isDefaultProp = (props: T, key: keyof T) => - !is.und( - props.default === true ? props[key] : props.default && props.default[key] - ) +export const hasDefaultProp = (props: T, key: keyof T) => + !is.und(getDefaultProp(props, key)) + +/** Get the default value being set for the given `key` */ +export const getDefaultProp = (props: T, key: keyof T) => + props.default === true + ? props[key] + : props.default + ? props.default[key] + : undefined export const mergeDefaultProps = ( defaultProps: Lookup, - props: Lookup & { default?: boolean | Lookup } + props: Lookup & { default?: boolean | Lookup }, + omitKeys: string[] = [] ) => { if (props.default === true) { each(DEFAULT_PROPS, key => { const value = props[key] - if (!is.und(value)) { + if (!is.und(value) && !omitKeys.includes(key)) { defaultProps[key] = value as any } }) } else if (props.default) { each(props.default, (value, key) => { - if (!is.und(value)) { + if (!is.und(value) && !omitKeys.includes(key)) { defaultProps[key] = value as any } }) diff --git a/packages/core/src/runAsync.ts b/packages/core/src/runAsync.ts index 05d436348..265d3f33a 100644 --- a/packages/core/src/runAsync.ts +++ b/packages/core/src/runAsync.ts @@ -72,10 +72,7 @@ export async function runAsync( let result!: AnimationResult const defaultProps: SpringDefaultProps = {} - mergeDefaultProps(defaultProps, props) - - // The `onRest` prop is for `runAsync` to call. - defaultProps.onRest = undefined + mergeDefaultProps(defaultProps, props, ['onRest']) const { callId, onRest } = props const throwInvalidated = () => {