Skip to content

Commit

Permalink
feat(useTransition): support for delayed transitions (#386)
Browse files Browse the repository at this point in the history
  • Loading branch information
scottbedard authored and antfu committed May 11, 2021
1 parent 734c388 commit b5d5dec
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 13 deletions.
15 changes: 8 additions & 7 deletions packages/core/useTransition/index.md
Expand Up @@ -8,20 +8,21 @@ Transition between values

## Usage

For simple transitions, provide a numeric source value. When this changes, a new transition will begin. If the source changes while a transition is in progress, a new transition will begin from where the previous one was interrupted.
For simple transitions, provide a numeric source to watch. When changed, the output will transition to the new value. If the source changes while a transition is in progress, a new transition will begin from where the previous one was interrupted.

```js
import { ref } from 'vue'
import { useTransition } from '@vueuse/core'
import { useTransition, TransitionPresets } from '@vueuse/core'

const source = ref(0)

const output = useTransition(source, {
duration: 1000,
transition: TransitionPresets.easeInOutCubic,
})
```

To synchronize transitions, use an array of values. To demonstrate this, here we'll transition between color values.
To synchronize transitions, use an array of numbers. As an example, here is how we could transition between colors.

```js
const source = ref([0, 0, 0])
Expand All @@ -42,7 +43,7 @@ useTransition(source, {
})
```

The following common transitions are available via the `TransitionPresets` constant.
The following transitions are available via the `TransitionPresets` constant.

- [`linear`](https://cubic-bezier.com/#0,0,1,1)
- [`easeInSine`](https://cubic-bezier.com/#.12,0,.39,0)
Expand All @@ -66,7 +67,7 @@ The following common transitions are available via the `TransitionPresets` const
- [`easeInCirc`](https://cubic-bezier.com/#.55,0,1,.45)
- [`easeOutCirc`](https://cubic-bezier.com/#0,.55,.45,1)
- [`easeInOutCirc`](https://cubic-bezier.com/#.85,0,.15,1)
- [`easeInBack`](https://cubic-bezier.com/#0.12,0,0.39,0)
- [`easeInBack`](https://cubic-bezier.com/#.36,0,.66,-.56)
- [`easeOutBack`](https://cubic-bezier.com/#.34,1.56,.64,1)
- [`easeInOutBack`](https://cubic-bezier.com/#.68,-.6,.32,1.6)

Expand All @@ -86,10 +87,11 @@ useTransition(source, {
})
```

To choreograph behavior around a transition, define `onStarted` or `onFinished` callbacks.
To control when a transition starts, set a `delay` value. To choreograph behavior around a transition, define `onStarted` or `onFinished` callbacks.

```js
useTransition(source, {
delay: 1000,
onStarted() {
// called after the transition starts
},
Expand All @@ -99,7 +101,6 @@ useTransition(source, {
})
```


<!--FOOTER_STARTS-->
## Type Declarations

Expand Down
39 changes: 39 additions & 0 deletions packages/core/useTransition/index.spec.ts
Expand Up @@ -86,6 +86,23 @@ describe('useTransition', () => {
expect(transition.value).toBe(1)
})

it('supports delayed transitions', async() => {
const source = ref(0)

const transition = useTransition(source, {
delay: 100,
duration: 100,
})

source.value = 1

await promiseTimeout(50)
expect(transition.value).toBe(0)

await promiseTimeout(100)
expectBetween(transition.value, 0, 1)
})

it('supports dynamic transitions', async() => {
const source = ref(0)
const first = jest.fn(n => n)
Expand Down Expand Up @@ -167,4 +184,26 @@ describe('useTransition', () => {
expect(onStarted).not.toHaveBeenCalled()
expect(onFinished).toHaveBeenCalled()
})

it('clears pending transitions before starting a new one', async() => {
const source = ref(0)
const onStarted = jest.fn()
const onFinished = jest.fn()

useTransition(source, {
delay: 100,
duration: 100,
onFinished,
onStarted,
})

await promiseTimeout(150)
expect(onStarted).not.toHaveBeenCalled()
source.value = 1
await promiseTimeout(50)
source.value = 2
await promiseTimeout(250)
expect(onStarted).toHaveBeenCalledTimes(1)
expect(onFinished).toHaveBeenCalledTimes(1)
})
})
18 changes: 16 additions & 2 deletions packages/core/useTransition/index.ts
@@ -1,6 +1,7 @@
import { computed, ComputedRef, ref, Ref, unref, watch } from 'vue-demi'
import { clamp, identity as linear, isFunction, isNumber, MaybeRef, noop } from '@vueuse/shared'
import { useRafFn } from '../useRafFn'
import { useTimeoutFn } from '@vueuse/core'

/**
* Cubic bezier points
Expand All @@ -16,6 +17,11 @@ type EasingFunction = (n: number) => number
* Transition options
*/
export type TransitionOptions = {
/**
* Milliseconds to wait before starting transition
*/
delay?: MaybeRef<number>

/**
* Transition duration in milliseconds
*/
Expand Down Expand Up @@ -118,8 +124,9 @@ export function useTransition<T extends Ref<number[]>>(source: T, options?: Tran
export function useTransition(
source: Ref<number | number[]> | MaybeRef<number>[],
options: TransitionOptions = {},
): ComputedRef<number | { [K in keyof typeof source]: number } | number[]> {
): ComputedRef<number | number[] | { [K in keyof typeof source]: number }> {
const {
delay = 0,
duration = 1000,
onFinished = noop,
onStarted = noop,
Expand Down Expand Up @@ -165,7 +172,7 @@ export function useTransition(
}, { immediate: false })

// start the animation loop when source vector changes
watch(sourceVector, () => {
const start = () => {
pause()

currentDuration = unref(duration)
Expand All @@ -176,6 +183,13 @@ export function useTransition(

resume()
onStarted()
}

const timeout = useTimeoutFn(start, delay, false)

watch(sourceVector, () => {
if (unref(delay) <= 0) start()
else timeout.start()
}, { deep: true })

return computed(() => isNumber(sourceValue.value) ? outputVector.value[0] : outputVector.value)
Expand Down
24 changes: 24 additions & 0 deletions packages/shared/useTimeoutFn/index.spec.ts
@@ -0,0 +1,24 @@
import { promiseTimeout } from '@vueuse/core'
import { ref } from 'vue-demi'
import { useTimeoutFn } from '.'

describe('useTimeoutFn', () => {
it('supports reactive intervals', async() => {
const callback = jest.fn()
const interval = ref(0)
const { start } = useTimeoutFn(callback, interval)

start()
await promiseTimeout(1)
expect(callback).toHaveBeenCalled()

callback.mockReset()
interval.value = 50

start()
await promiseTimeout(1)
expect(callback).not.toHaveBeenCalled()
await promiseTimeout(100)
expect(callback).toHaveBeenCalled()
})
})
8 changes: 4 additions & 4 deletions packages/shared/useTimeoutFn/index.ts
@@ -1,5 +1,5 @@
import { Fn } from '@vueuse/shared'
import { Ref, ref } from 'vue-demi'
import { Fn, MaybeRef } from '@vueuse/shared'
import { Ref, ref, unref } from 'vue-demi'
import { tryOnUnmounted } from '../tryOnUnmounted'
import { isClient } from '../utils'

Expand All @@ -22,7 +22,7 @@ export interface TimeoutFnResult {
*/
export function useTimeoutFn(
cb: (...args: unknown[]) => any,
interval?: number,
interval?: MaybeRef<number>,
immediate = true,
): TimeoutFnResult {
const isPending = ref(false)
Expand All @@ -49,7 +49,7 @@ export function useTimeoutFn(
timer = null
// eslint-disable-next-line node/no-callback-literal
cb(...args)
}, interval) as unknown as number
}, unref(interval)) as unknown as number
}

if (immediate) {
Expand Down

0 comments on commit b5d5dec

Please sign in to comment.