feat(vue): expand @robonen/vue composable collection
Composables, tests, category barrels, and README for @robonen/vue.
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { effectScope, nextTick, ref } from 'vue';
|
||||
import {
|
||||
TransitionPresets,
|
||||
useTransition,
|
||||
} from '@/composables/animation/useTransition';
|
||||
|
||||
// A controllable window stub: requestAnimationFrame frames are flushed
|
||||
// manually via `frame()`, and timers are driven by vitest fake timers.
|
||||
function createWindowStub() {
|
||||
let rafId = 0;
|
||||
const callbacks = new Map<number, FrameRequestCallback>();
|
||||
|
||||
const win = {
|
||||
requestAnimationFrame: (cb: FrameRequestCallback) => {
|
||||
const id = ++rafId;
|
||||
callbacks.set(id, cb);
|
||||
return id;
|
||||
},
|
||||
cancelAnimationFrame: (id: number) => {
|
||||
callbacks.delete(id);
|
||||
},
|
||||
setTimeout: (fn: (...args: unknown[]) => void, ms?: number) =>
|
||||
setTimeout(fn, ms) as unknown as number,
|
||||
clearTimeout: (id: number) => clearTimeout(id),
|
||||
} as unknown as Window;
|
||||
|
||||
function frame() {
|
||||
const pending = [...callbacks.entries()];
|
||||
callbacks.clear();
|
||||
for (const [, cb] of pending)
|
||||
cb(Date.now());
|
||||
}
|
||||
|
||||
return { win, frame, get pending() {
|
||||
return callbacks.size;
|
||||
} };
|
||||
}
|
||||
|
||||
describe(useTransition, () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('seeds output with the initial source value', () => {
|
||||
const { win } = createWindowStub();
|
||||
const scope = effectScope();
|
||||
|
||||
scope.run(() => {
|
||||
const output = useTransition(ref(5), { window: win });
|
||||
expect(output.value).toBe(5);
|
||||
});
|
||||
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('transitions a number from old to new over the duration', async () => {
|
||||
const { win, frame } = createWindowStub();
|
||||
vi.setSystemTime(0);
|
||||
const scope = effectScope();
|
||||
const source = ref(0);
|
||||
let output!: ReturnType<typeof useTransition<number>>;
|
||||
|
||||
scope.run(() => {
|
||||
output = useTransition(source, { duration: 100, window: win });
|
||||
});
|
||||
|
||||
source.value = 100;
|
||||
await nextTick();
|
||||
|
||||
// Halfway through.
|
||||
vi.setSystemTime(50);
|
||||
frame();
|
||||
expect(output.value).toBeCloseTo(50, 5);
|
||||
|
||||
// End.
|
||||
vi.setSystemTime(100);
|
||||
frame();
|
||||
expect(output.value).toBe(100);
|
||||
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('fires onStarted and onFinished', async () => {
|
||||
const { win, frame } = createWindowStub();
|
||||
vi.setSystemTime(0);
|
||||
const onStarted = vi.fn();
|
||||
const onFinished = vi.fn();
|
||||
const scope = effectScope();
|
||||
const source = ref(0);
|
||||
|
||||
scope.run(() => {
|
||||
useTransition(source, { duration: 100, window: win, onStarted, onFinished });
|
||||
});
|
||||
|
||||
source.value = 10;
|
||||
await nextTick();
|
||||
|
||||
expect(onStarted).toHaveBeenCalledTimes(1);
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
|
||||
vi.setSystemTime(100);
|
||||
frame();
|
||||
|
||||
expect(onFinished).toHaveBeenCalledTimes(1);
|
||||
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('transitions numeric arrays element-wise', async () => {
|
||||
const { win, frame } = createWindowStub();
|
||||
vi.setSystemTime(0);
|
||||
const scope = effectScope();
|
||||
const source = ref([0, 100]);
|
||||
let output!: ReturnType<typeof useTransition<number[]>>;
|
||||
|
||||
scope.run(() => {
|
||||
output = useTransition(source, { duration: 100, window: win });
|
||||
});
|
||||
|
||||
source.value = [100, 0];
|
||||
await nextTick();
|
||||
|
||||
vi.setSystemTime(50);
|
||||
frame();
|
||||
expect(output.value[0]).toBeCloseTo(50, 5);
|
||||
expect(output.value[1]).toBeCloseTo(50, 5);
|
||||
|
||||
vi.setSystemTime(100);
|
||||
frame();
|
||||
expect(output.value).toEqual([100, 0]);
|
||||
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('applies an easing preset (eased value differs from linear midpoint)', async () => {
|
||||
const { win, frame } = createWindowStub();
|
||||
vi.setSystemTime(0);
|
||||
const scope = effectScope();
|
||||
const source = ref(0);
|
||||
let output!: ReturnType<typeof useTransition<number>>;
|
||||
|
||||
scope.run(() => {
|
||||
output = useTransition(source, {
|
||||
duration: 100,
|
||||
transition: TransitionPresets.easeInCubic,
|
||||
window: win,
|
||||
});
|
||||
});
|
||||
|
||||
source.value = 100;
|
||||
await nextTick();
|
||||
|
||||
vi.setSystemTime(50);
|
||||
frame();
|
||||
// easeInCubic at t=0.5 is well below the linear midpoint of 50.
|
||||
expect(output.value).toBeLessThan(50);
|
||||
expect(output.value).toBeGreaterThan(0);
|
||||
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('accepts a custom easing function', async () => {
|
||||
const { win, frame } = createWindowStub();
|
||||
vi.setSystemTime(0);
|
||||
const scope = effectScope();
|
||||
const source = ref(0);
|
||||
const easing = vi.fn((n: number) => n);
|
||||
let output!: ReturnType<typeof useTransition<number>>;
|
||||
|
||||
scope.run(() => {
|
||||
output = useTransition(source, { duration: 100, transition: easing, window: win });
|
||||
});
|
||||
|
||||
source.value = 100;
|
||||
await nextTick();
|
||||
|
||||
vi.setSystemTime(50);
|
||||
frame();
|
||||
|
||||
expect(easing).toHaveBeenCalled();
|
||||
expect(output.value).toBeCloseTo(50, 5);
|
||||
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('delays the start of a transition', async () => {
|
||||
const { win, frame } = createWindowStub();
|
||||
vi.setSystemTime(0);
|
||||
const onStarted = vi.fn();
|
||||
const scope = effectScope();
|
||||
const source = ref(0);
|
||||
|
||||
scope.run(() => {
|
||||
useTransition(source, { duration: 100, delay: 200, window: win, onStarted });
|
||||
});
|
||||
|
||||
source.value = 100;
|
||||
await nextTick();
|
||||
|
||||
// Still in the delay window: not started yet.
|
||||
expect(onStarted).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(200);
|
||||
expect(onStarted).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.setSystemTime(200);
|
||||
frame();
|
||||
expect(onStarted).toHaveBeenCalledTimes(1);
|
||||
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('snaps instantly when disabled and tracks the source', async () => {
|
||||
const { win, frame, pending } = createWindowStub();
|
||||
const scope = effectScope();
|
||||
const source = ref(0);
|
||||
let output!: ReturnType<typeof useTransition<number>>;
|
||||
|
||||
scope.run(() => {
|
||||
output = useTransition(source, { duration: 100, disabled: true, window: win });
|
||||
});
|
||||
|
||||
source.value = 42;
|
||||
await nextTick();
|
||||
|
||||
expect(output.value).toBe(42);
|
||||
expect(pending).toBe(0);
|
||||
frame();
|
||||
expect(output.value).toBe(42);
|
||||
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('disabling mid-transition snaps to the source', async () => {
|
||||
const { win, frame } = createWindowStub();
|
||||
vi.setSystemTime(0);
|
||||
const scope = effectScope();
|
||||
const source = ref(0);
|
||||
const disabled = ref(false);
|
||||
let output!: ReturnType<typeof useTransition<number>>;
|
||||
|
||||
scope.run(() => {
|
||||
output = useTransition(source, { duration: 100, disabled, window: win });
|
||||
});
|
||||
|
||||
source.value = 100;
|
||||
await nextTick();
|
||||
|
||||
vi.setSystemTime(50);
|
||||
frame();
|
||||
expect(output.value).toBeCloseTo(50, 5);
|
||||
|
||||
disabled.value = true;
|
||||
await nextTick();
|
||||
|
||||
expect(output.value).toBe(100);
|
||||
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('jumps immediately when duration is zero', async () => {
|
||||
const { win } = createWindowStub();
|
||||
vi.setSystemTime(0);
|
||||
const onFinished = vi.fn();
|
||||
const scope = effectScope();
|
||||
const source = ref(0);
|
||||
let output!: ReturnType<typeof useTransition<number>>;
|
||||
|
||||
scope.run(() => {
|
||||
output = useTransition(source, { duration: 0, window: win, onFinished });
|
||||
});
|
||||
|
||||
source.value = 100;
|
||||
await nextTick();
|
||||
|
||||
expect(output.value).toBe(100);
|
||||
expect(onFinished).toHaveBeenCalledTimes(1);
|
||||
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('is SSR-safe: without a window it mirrors the source synchronously', async () => {
|
||||
const onFinished = vi.fn();
|
||||
const scope = effectScope();
|
||||
const source = ref(0);
|
||||
let output!: ReturnType<typeof useTransition<number>>;
|
||||
|
||||
scope.run(() => {
|
||||
output = useTransition(source, { duration: 100, window: undefined, onFinished });
|
||||
});
|
||||
|
||||
expect(output.value).toBe(0);
|
||||
|
||||
source.value = 100;
|
||||
await nextTick();
|
||||
|
||||
// No RAF available: transition resolves immediately to the target.
|
||||
expect(output.value).toBe(100);
|
||||
expect(onFinished).toHaveBeenCalledTimes(1);
|
||||
|
||||
scope.stop();
|
||||
});
|
||||
|
||||
it('exposes the documented easing presets', () => {
|
||||
expect(TransitionPresets.linear).toEqual([0, 0, 1, 1]);
|
||||
expect(TransitionPresets.easeInOutCubic).toEqual([0.65, 0, 0.35, 1]);
|
||||
expect(Object.keys(TransitionPresets)).toContain('easeOutBack');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,360 @@
|
||||
import { computed, ref, toValue, watch } from 'vue';
|
||||
import type { MaybeRefOrGetter, Ref } from 'vue';
|
||||
import { clamp, isFunction, isNumber, lerp, noop } from '@robonen/stdlib';
|
||||
import { defaultWindow } from '@/types';
|
||||
import type { ConfigurableWindow } from '@/types';
|
||||
import { useRafFn } from '@/composables/animation/useRafFn';
|
||||
|
||||
/**
|
||||
* Cubic bezier control points `[x1, y1, x2, y2]` (the implied endpoints are
|
||||
* `(0, 0)` and `(1, 1)`), matching the CSS `cubic-bezier()` argument order.
|
||||
*/
|
||||
export type CubicBezierPoints = [number, number, number, number];
|
||||
|
||||
/**
|
||||
* An easing function mapping linear progress in `[0, 1]` to eased progress.
|
||||
*/
|
||||
export type EasingFunction = (n: number) => number;
|
||||
|
||||
/**
|
||||
* Interpolates between two values of `T` given an eased progress `alpha`.
|
||||
*/
|
||||
export type TransitionInterpolation<T> = (from: T, to: T, alpha: number) => T;
|
||||
|
||||
/**
|
||||
* The transition easing: either a cubic bezier tuple or a custom easing function.
|
||||
*/
|
||||
export type TransitionEasing = CubicBezierPoints | EasingFunction;
|
||||
|
||||
/**
|
||||
* Values that can be transitioned: a single number or a fixed-length number array.
|
||||
*/
|
||||
export type TransitionValue = number | number[];
|
||||
|
||||
/**
|
||||
* Common cubic bezier easing presets (same curves as CSS / VueUse).
|
||||
*/
|
||||
export const TransitionPresets = {
|
||||
linear: [0, 0, 1, 1],
|
||||
easeInSine: [0.12, 0, 0.39, 0],
|
||||
easeOutSine: [0.61, 1, 0.88, 1],
|
||||
easeInOutSine: [0.37, 0, 0.63, 1],
|
||||
easeInQuad: [0.11, 0, 0.5, 0],
|
||||
easeOutQuad: [0.5, 1, 0.89, 1],
|
||||
easeInOutQuad: [0.45, 0, 0.55, 1],
|
||||
easeInCubic: [0.32, 0, 0.67, 0],
|
||||
easeOutCubic: [0.33, 1, 0.68, 1],
|
||||
easeInOutCubic: [0.65, 0, 0.35, 1],
|
||||
easeInQuart: [0.5, 0, 0.75, 0],
|
||||
easeOutQuart: [0.25, 1, 0.5, 1],
|
||||
easeInOutQuart: [0.76, 0, 0.24, 1],
|
||||
easeInQuint: [0.64, 0, 0.78, 0],
|
||||
easeOutQuint: [0.22, 1, 0.36, 1],
|
||||
easeInOutQuint: [0.83, 0, 0.17, 1],
|
||||
easeInExpo: [0.7, 0, 0.84, 0],
|
||||
easeOutExpo: [0.16, 1, 0.3, 1],
|
||||
easeInOutExpo: [0.87, 0, 0.13, 1],
|
||||
easeInCirc: [0.55, 0, 1, 0.45],
|
||||
easeOutCirc: [0, 0.55, 0.45, 1],
|
||||
easeInOutCirc: [0.85, 0, 0.15, 1],
|
||||
easeInBack: [0.36, 0, 0.66, -0.56],
|
||||
easeOutBack: [0.34, 1.56, 0.64, 1],
|
||||
easeInOutBack: [0.68, -0.6, 0.32, 1.6],
|
||||
} satisfies Record<string, CubicBezierPoints>;
|
||||
|
||||
export interface UseTransitionOptions {
|
||||
/**
|
||||
* Transition duration in milliseconds. Accepts a reactive value (resolved
|
||||
* at the start of each transition).
|
||||
*
|
||||
* @default 1000
|
||||
*/
|
||||
duration?: MaybeRefOrGetter<number>;
|
||||
|
||||
/**
|
||||
* Easing applied to the progress: a cubic bezier tuple (e.g. one of
|
||||
* {@link TransitionPresets}) or a custom easing function.
|
||||
*
|
||||
* @default identity (linear)
|
||||
*/
|
||||
transition?: MaybeRefOrGetter<TransitionEasing>;
|
||||
|
||||
/**
|
||||
* Delay in milliseconds before a transition begins after the source changes.
|
||||
*
|
||||
* @default 0
|
||||
*/
|
||||
delay?: MaybeRefOrGetter<number>;
|
||||
|
||||
/**
|
||||
* When `true`, transitions are skipped and the output tracks the source
|
||||
* value directly (no animation). Reactive.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
disabled?: MaybeRefOrGetter<boolean>;
|
||||
|
||||
/**
|
||||
* Called when a transition starts.
|
||||
*/
|
||||
onStarted?: () => void;
|
||||
|
||||
/**
|
||||
* Called when a transition finishes (not called when aborted by a new change).
|
||||
*/
|
||||
onFinished?: () => void;
|
||||
}
|
||||
|
||||
export type UseTransitionReturn<T> = Readonly<Ref<T>>;
|
||||
|
||||
const identity: EasingFunction = n => n;
|
||||
|
||||
interface BezierCoefficients {
|
||||
a: (a1: number, a2: number) => number;
|
||||
b: (a1: number, a2: number) => number;
|
||||
c: (a1: number) => number;
|
||||
}
|
||||
|
||||
function createEasingFunction(points: CubicBezierPoints): EasingFunction {
|
||||
const [p0, p1, p2, p3] = points;
|
||||
|
||||
const coeffs: BezierCoefficients = {
|
||||
a: (a1, a2) => 1 - 3 * a2 + 3 * a1,
|
||||
b: (a1, a2) => 3 * a2 - 6 * a1,
|
||||
c: a1 => 3 * a1,
|
||||
};
|
||||
|
||||
const calcBezier = (t: number, a1: number, a2: number): number =>
|
||||
((coeffs.a(a1, a2) * t + coeffs.b(a1, a2)) * t + coeffs.c(a1)) * t;
|
||||
|
||||
const getSlope = (t: number, a1: number, a2: number): number =>
|
||||
3 * coeffs.a(a1, a2) * t * t + 2 * coeffs.b(a1, a2) * t + coeffs.c(a1);
|
||||
|
||||
const getTForX = (x: number): number => {
|
||||
let guess = x;
|
||||
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
const slope = getSlope(guess, p0, p2);
|
||||
|
||||
if (slope === 0)
|
||||
return guess;
|
||||
|
||||
const currentX = calcBezier(guess, p0, p2) - x;
|
||||
guess -= currentX / slope;
|
||||
}
|
||||
|
||||
return guess;
|
||||
};
|
||||
|
||||
return n => (p0 === p1 && p2 === p3) ? n : calcBezier(getTForX(n), p1, p3);
|
||||
}
|
||||
|
||||
function resolveEasing(transition: TransitionEasing | undefined): EasingFunction {
|
||||
if (!transition)
|
||||
return identity;
|
||||
|
||||
if (isFunction(transition))
|
||||
return transition;
|
||||
|
||||
return createEasingFunction(transition);
|
||||
}
|
||||
|
||||
// Interpolate a single number or a (fixed-length) numeric array.
|
||||
function interpolate(from: TransitionValue, to: TransitionValue, alpha: number): TransitionValue {
|
||||
if (isNumber(from) && isNumber(to))
|
||||
return lerp(from, to, alpha);
|
||||
|
||||
const source = from as number[];
|
||||
const target = to as number[];
|
||||
|
||||
return source.map((value, index) => lerp(value, target[index] ?? value, alpha));
|
||||
}
|
||||
|
||||
function snapshot<T extends TransitionValue>(value: T): T {
|
||||
return (isNumber(value) ? value : (value as number[]).slice()) as T;
|
||||
}
|
||||
|
||||
function valuesEqual(a: TransitionValue, b: TransitionValue): boolean {
|
||||
if (isNumber(a) && isNumber(b))
|
||||
return a === b;
|
||||
|
||||
if (isNumber(a) || isNumber(b))
|
||||
return false;
|
||||
|
||||
if (a.length !== b.length)
|
||||
return false;
|
||||
|
||||
for (let i = 0; i < a.length; ++i) {
|
||||
if (a[i] !== b[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name useTransition
|
||||
* @category Animation
|
||||
* @description Reactively transition between numeric values (or numeric arrays)
|
||||
* over a duration with configurable easing. Wraps a single, paused
|
||||
* `requestAnimationFrame` loop that only runs while a transition is in flight,
|
||||
* so it is cheaper than re-creating an RAF loop per change. SSR-safe: without a
|
||||
* `window` the output tracks the source synchronously.
|
||||
*
|
||||
* @param {MaybeRefOrGetter<T>} source The reactive source value (number or number[])
|
||||
* @param {UseTransitionOptions & ConfigurableWindow} [options={}] Transition options
|
||||
* @returns {UseTransitionReturn<T>} A readonly ref of the transitioned value
|
||||
*
|
||||
* @example
|
||||
* const progress = ref(0);
|
||||
* const output = useTransition(progress, {
|
||||
* duration: 500,
|
||||
* transition: TransitionPresets.easeOutCubic,
|
||||
* });
|
||||
*
|
||||
* @example
|
||||
* // Transition a tuple (e.g. an RGB color)
|
||||
* const color = ref([0, 0, 0]);
|
||||
* const animated = useTransition(color, { duration: 1000 });
|
||||
*
|
||||
* @since 0.0.15
|
||||
*/
|
||||
export function useTransition<T extends TransitionValue>(
|
||||
source: MaybeRefOrGetter<T>,
|
||||
options: UseTransitionOptions & ConfigurableWindow = {},
|
||||
): UseTransitionReturn<T> {
|
||||
const {
|
||||
duration = 1000,
|
||||
transition = identity,
|
||||
delay = 0,
|
||||
disabled = false,
|
||||
onStarted = noop,
|
||||
onFinished = noop,
|
||||
} = options;
|
||||
|
||||
const window = 'window' in options ? options.window : defaultWindow;
|
||||
|
||||
// The animated output. Seeded with a snapshot of the current source.
|
||||
const outputRef = ref(snapshot(toValue(source))) as Ref<T>;
|
||||
|
||||
// Active-transition state. `endpoints` are detached snapshots so that later
|
||||
// source mutations cannot bleed into an in-flight transition.
|
||||
let fromValue: T = outputRef.value;
|
||||
let toValue_: T = outputRef.value;
|
||||
let startedAt = 0;
|
||||
let durationMs = 0;
|
||||
let easing: EasingFunction = identity;
|
||||
let delayTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let finishPending = false;
|
||||
|
||||
const { pause, resume, isActive } = useRafFn(tick, { immediate: false, window });
|
||||
|
||||
function clearDelay() {
|
||||
if (delayTimer !== null && window) {
|
||||
window.clearTimeout(delayTimer);
|
||||
delayTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function settle(value: T) {
|
||||
outputRef.value = snapshot(value);
|
||||
|
||||
if (isActive.value)
|
||||
pause();
|
||||
|
||||
if (finishPending) {
|
||||
finishPending = false;
|
||||
onFinished();
|
||||
}
|
||||
}
|
||||
|
||||
function tick() {
|
||||
const now = Date.now();
|
||||
const alpha = durationMs <= 0 ? 1 : clamp((now - startedAt) / durationMs, 0, 1);
|
||||
|
||||
outputRef.value = interpolate(fromValue, toValue_, easing(alpha)) as T;
|
||||
|
||||
if (alpha >= 1)
|
||||
settle(toValue_);
|
||||
}
|
||||
|
||||
function begin(target: T) {
|
||||
fromValue = snapshot(outputRef.value);
|
||||
toValue_ = snapshot(target);
|
||||
|
||||
durationMs = Math.max(0, toValue(duration));
|
||||
easing = resolveEasing(toValue(transition));
|
||||
startedAt = Date.now();
|
||||
finishPending = true;
|
||||
|
||||
onStarted();
|
||||
|
||||
if (durationMs <= 0 || !window) {
|
||||
settle(toValue_);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isActive.value)
|
||||
resume();
|
||||
}
|
||||
|
||||
function start(target: T) {
|
||||
clearDelay();
|
||||
|
||||
const delayMs = Math.max(0, toValue(delay));
|
||||
|
||||
if (delayMs > 0 && window) {
|
||||
delayTimer = window.setTimeout(() => {
|
||||
delayTimer = null;
|
||||
begin(target);
|
||||
}, delayMs);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
begin(target);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => toValue(source),
|
||||
(value) => {
|
||||
// When disabled, mirror the source instantly and abort any animation.
|
||||
if (toValue(disabled)) {
|
||||
clearDelay();
|
||||
|
||||
if (isActive.value)
|
||||
pause();
|
||||
|
||||
finishPending = false;
|
||||
outputRef.value = snapshot(value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip no-op changes so we don't restart an identical transition.
|
||||
if (valuesEqual(value, outputRef.value) && !isActive.value && delayTimer === null)
|
||||
return;
|
||||
|
||||
start(value);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// Reacting to `disabled` flipping to true mid-transition: snap to source.
|
||||
watch(
|
||||
() => toValue(disabled),
|
||||
(off) => {
|
||||
if (off) {
|
||||
clearDelay();
|
||||
|
||||
if (isActive.value)
|
||||
pause();
|
||||
|
||||
finishPending = false;
|
||||
outputRef.value = snapshot(toValue(source));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return computed(() => outputRef.value);
|
||||
}
|
||||
Reference in New Issue
Block a user