Skip to content

feat: add "pause" prop #981

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 23 additions & 32 deletions packages/core/src/Controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
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'
import { inferTo } 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'
import {
getCombinedResult,
AnimationResult,
Expand Down Expand Up @@ -58,7 +59,10 @@ export class Controller<State extends Lookup = Lookup>
protected _active = new Set<FrameValue>()

/** State used by the `runAsync` function */
protected _state: RunAsyncState<State> = {}
protected _state: RunAsyncState<State> = {
pauseQueue: new Set(),
resumeQueue: new Set(),
}

/** The event queues that are flushed once per frame maximum */
protected _events = {
Expand Down Expand Up @@ -88,7 +92,7 @@ export class Controller<State extends Lookup = Lookup>
*/
get idle() {
return (
!this._state.promise &&
!this._state.asyncTo &&
Object.values(this.springs as Lookup<SpringValue>).every(
spring => spring.idle
)
Expand Down Expand Up @@ -201,7 +205,7 @@ export class Controller<State extends Lookup = Lookup>
// 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)
})
Expand All @@ -217,19 +221,6 @@ export class Controller<State extends Lookup = Lookup>
}
}

/** Basic helper for clearing a queue after processing it */
function flush<P, T>(
queue: Map<P, T>,
iterator: (value: T, key: P) => void
): void
function flush<T>(queue: Set<T>, iterator: (value: T) => void): void
function flush(queue: any, iterator: any) {
if (queue.size) {
each(queue, iterator)
queue.clear()
}
}

/**
* Warning: Props might be mutated.
*/
Expand Down Expand Up @@ -308,24 +299,24 @@ export function flushUpdate(
scheduleProps(++ctrl['_lastAsyncId'], {
props,
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)
)
)
actions: {
pause: noop,
start(props, resolve) {
props.onRest = onRest as any
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 (hasDefaultProp(props, 'cancel')) {
cancelAsync(state, props.callId)
}
},
},
})
)
}
// 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'])
}
Expand Down
80 changes: 50 additions & 30 deletions packages/core/src/SpringValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ import {
import {
callProp,
computeGoal,
DEFAULT_PROPS,
matchProp,
inferTo,
flush,
mergeDefaultProps,
getDefaultProp,
} from './helpers'
import { FrameValue, isFrameValue } from './FrameValue'
import {
Expand Down Expand Up @@ -84,7 +86,10 @@ export class SpringValue<T = any> extends FrameValue<T> {
protected _phase: SpringPhase = CREATED

/** The state for `runAsync` calls */
protected _state: RunAsyncState<T> = {}
protected _state: RunAsyncState<T> = {
pauseQueue: new Set(),
resumeQueue: new Set(),
}

/** Some props have customizable default values */
protected _defaultProps = {} as SpringDefaultProps<T>
Expand All @@ -107,7 +112,7 @@ export class SpringValue<T = any> extends FrameValue<T> {
}

get idle() {
return !this.is(ACTIVE) && !this._state.promise
return !this.is(ACTIVE) && !this._state.asyncTo
}

get goal() {
Expand Down Expand Up @@ -306,18 +311,18 @@ export class SpringValue<T = any> extends FrameValue<T> {
*/
pause() {
checkDisposed(this, 'pause')
this._phase = PAUSED
if (!this.is(PAUSED)) {
this._phase = PAUSED
flush(this._state.pauseQueue, onPause => onPause())
}
}

/** Resume the animation if paused. */
resume() {
checkDisposed(this, 'resume')
if (this.is(PAUSED)) {
this._start()

if (this._state.asyncTo) {
this._state.unpause!()
}
flush(this._state.resumeQueue, onResume => onResume())
}
}

Expand Down Expand Up @@ -434,7 +439,9 @@ export class SpringValue<T = any> extends FrameValue<T> {
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
Expand Down Expand Up @@ -510,15 +517,34 @@ export class SpringValue<T = any> extends FrameValue<T> {

/** Schedule an animation to run after an optional delay */
protected _update(props: SpringUpdate<T>, isLoop?: boolean): AsyncResult<T> {
type DefaultProps = typeof defaultProps
const defaultProps = this._defaultProps
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)

return scheduleProps<T>(++this._lastCallId, {
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)) {
Expand Down Expand Up @@ -573,6 +599,8 @@ export class SpringValue<T = any> extends FrameValue<T> {
onDelayEnd(props, this)
}

mergeDefaultProps(defaultProps, props, ['pause', 'cancel'])

const { to: prevTo, from: prevFrom } = anim
let { to = prevTo, from = prevFrom } = range

Expand Down Expand Up @@ -608,8 +636,7 @@ export class SpringValue<T = any> extends FrameValue<T> {

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
}
})
Expand All @@ -627,8 +654,10 @@ export class SpringValue<T = any> extends FrameValue<T> {
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
)
}

Expand All @@ -641,11 +670,11 @@ export class SpringValue<T = any> extends FrameValue<T> {

/** 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()
Expand Down Expand Up @@ -774,20 +803,11 @@ export class SpringValue<T = any> extends FrameValue<T> {
}

if (hasAsyncTo) {
return resolve(
runAsync(
props.to as any,
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))
}

if (started) {
// Start an animation
else if (started) {
// Unpause the async animation if one exists.
this.resume()

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('helpers', () => {
ref: undefined,
loop: undefined,
reset: undefined,
pause: undefined,
cancel: undefined,
reverse: undefined,
immediate: undefined,
Expand Down
54 changes: 54 additions & 0 deletions packages/core/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
AnyFn,
OneOrMore,
FluidValue,
Lookup,
} from 'shared'
import * as G from 'shared/globals'
import { ReservedProps, ForwardProps, InferTo } from './types'
Expand Down Expand Up @@ -35,6 +36,9 @@ export const matchProp = (
(is.fun(value) ? value(key) : toArray(value).includes(key))
)

export const concatFn = <T extends AnyFn>(first: T | undefined, last: T) =>
first ? (...args: Parameters<T>) => (first(...args), last(...args)) : last

type AnyProps<T, Arg = never> = OneOrMore<T> | ((i: number, arg: Arg) => T)

export const getProps = <T, Arg = never>(
Expand All @@ -45,8 +49,43 @@ export const getProps = <T, Arg = never>(
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 hasDefaultProp = <T extends Lookup>(props: T, key: keyof T) =>
!is.und(getDefaultProp(props, key))

/** Get the default value being set for the given `key` */
export const getDefaultProp = <T extends Lookup>(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 },
omitKeys: string[] = []
) => {
if (props.default === true) {
each(DEFAULT_PROPS, key => {
const value = props[key]
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) && !omitKeys.includes(key)) {
defaultProps[key] = value as any
}
})
}
}

/** These props can have default values */
export const DEFAULT_PROPS = [
'pause',
'cancel',
'config',
'immediate',
'onDelayEnd',
Expand All @@ -64,6 +103,7 @@ const RESERVED_PROPS: Required<ReservedProps> = {
ref: 1,
loop: 1,
reset: 1,
pause: 1,
cancel: 1,
reverse: 1,
immediate: 1,
Expand Down Expand Up @@ -136,3 +176,17 @@ export function computeGoal<T>(value: T | FluidValue<T>): T {
})(1) as any)
: value
}

/** Basic helper for clearing a queue after processing it */
export function flush<P, T>(
queue: Map<P, T>,
iterator: (entry: [P, T]) => void
): void
export function flush<T>(queue: Set<T>, 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)
}
}
Loading