@octanejs/spring 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/UPSTREAM.md +103 -0
- package/package.json +73 -0
- package/src/animated/Animated.ts +24 -0
- package/src/animated/AnimatedArray.ts +26 -0
- package/src/animated/index.ts +3 -0
- package/src/browser.ts +246 -0
- package/src/components.tsrx +60 -0
- package/src/components.tsrx.d.ts +33 -0
- package/src/context.ts +13 -0
- package/src/core/AnimationConfig.ts +60 -0
- package/src/core/AnimationResult.ts +19 -0
- package/src/core/Controller.ts +200 -0
- package/src/core/FrameValue.ts +16 -0
- package/src/core/Interpolation.ts +46 -0
- package/src/core/SpringRef.ts +26 -0
- package/src/core/SpringValue.ts +384 -0
- package/src/core/index.ts +9 -0
- package/src/core/runAsync.ts +17 -0
- package/src/core/scheduleProps.ts +34 -0
- package/src/core/valueInterpolation.ts +54 -0
- package/src/engine.ts +9 -0
- package/src/hooks.ts +610 -0
- package/src/index.ts +26 -0
- package/src/parallax.ts +8 -0
- package/src/parallax.tsrx +321 -0
- package/src/parallax.tsrx.d.ts +60 -0
- package/src/shared/FrameLoop.ts +75 -0
- package/src/shared/createInterpolator.ts +85 -0
- package/src/shared/globals.ts +7 -0
- package/src/shared/index.ts +4 -0
- package/src/shared/stringInterpolation.ts +56 -0
- package/src/spring-component.ts +36 -0
- package/src/types/animated.ts +10 -0
- package/src/types/common.ts +29 -0
- package/src/types/fluid.ts +9 -0
- package/src/types/index.ts +9 -0
- package/src/types/interpolate.ts +50 -0
- package/src/types/interpolation.ts +18 -0
- package/src/types/objects.ts +17 -0
- package/src/types/props.ts +275 -0
- package/src/types/utils.ts +57 -0
- package/src/upstream-compat.ts +164 -0
- package/src/web/AnimatedStyle.ts +51 -0
- package/src/web/animated.ts +118 -0
- package/src/web/applyAnimatedValues.ts +92 -0
- package/src/web/index.ts +3 -0
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
// Octane hook adapter for react-spring/packages/core/src/hooks at v10.1.2.
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from 'octane';
|
|
3
|
+
import {
|
|
4
|
+
Controller,
|
|
5
|
+
type ControllerUpdate as RuntimeControllerUpdate,
|
|
6
|
+
SpringRef,
|
|
7
|
+
SpringValue,
|
|
8
|
+
type SpringUpdate,
|
|
9
|
+
} from './engine';
|
|
10
|
+
import { inferTo } from './upstream-compat';
|
|
11
|
+
import { type SpringContextValue, useSpringContext } from './context';
|
|
12
|
+
import type { Valid } from './types/common';
|
|
13
|
+
import type { SpringValues } from './types/objects';
|
|
14
|
+
import type { ControllerUpdate, Lookup, PickAnimated, Remap } from './types/index';
|
|
15
|
+
|
|
16
|
+
export type TransitionPhase = 'mount' | 'enter' | 'update' | 'leave';
|
|
17
|
+
|
|
18
|
+
export type UseSpringProps<Props extends object = any> = unknown &
|
|
19
|
+
PickAnimated<Props> extends infer State
|
|
20
|
+
? State extends Lookup
|
|
21
|
+
? Remap<
|
|
22
|
+
ControllerUpdate<State> & {
|
|
23
|
+
ref?: SpringRef<State>;
|
|
24
|
+
}
|
|
25
|
+
>
|
|
26
|
+
: never
|
|
27
|
+
: never;
|
|
28
|
+
|
|
29
|
+
export type UseTransitionProps<Item = any, State extends Lookup = Lookup> = Omit<
|
|
30
|
+
ControllerUpdate<State, Item>,
|
|
31
|
+
'from' | 'onResolve'
|
|
32
|
+
> & {
|
|
33
|
+
keys?: ((item: Item) => string | number) | Array<string | number> | null;
|
|
34
|
+
from?:
|
|
35
|
+
| Partial<State>
|
|
36
|
+
| null
|
|
37
|
+
| false
|
|
38
|
+
| ((item: Item, index: number) => Partial<State> | null | false | undefined)
|
|
39
|
+
| Function;
|
|
40
|
+
initial?:
|
|
41
|
+
| Partial<State>
|
|
42
|
+
| null
|
|
43
|
+
| false
|
|
44
|
+
| ((item: Item, index: number) => Partial<State> | null | false | undefined);
|
|
45
|
+
enter?: Partial<State> | ((item: Item, index: number) => Partial<State> | undefined) | Function;
|
|
46
|
+
update?: Partial<State> | ((item: Item, index: number) => Partial<State> | undefined) | Function;
|
|
47
|
+
leave?: Partial<State> | ((item: Item, index: number) => Partial<State> | undefined) | Function;
|
|
48
|
+
sort?: (a: Item, b: Item) => number;
|
|
49
|
+
expires?: boolean | number | ((item: Item) => boolean | number);
|
|
50
|
+
exitBeforeEnter?: boolean;
|
|
51
|
+
trail?: number;
|
|
52
|
+
reverse?: boolean;
|
|
53
|
+
onDestroyed?: (item: Item, key: string | number) => void;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
interface TransitionRecord<Item, State extends Record<string, any>> {
|
|
57
|
+
key: string | number;
|
|
58
|
+
item: Item;
|
|
59
|
+
phase: TransitionPhase;
|
|
60
|
+
controller: Controller<State>;
|
|
61
|
+
lastUpdate?: RuntimeControllerUpdate<State>;
|
|
62
|
+
expiration?: ReturnType<typeof setTimeout>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function equalDeps(previous: any[] | undefined, next: any[] | undefined): boolean {
|
|
66
|
+
return (
|
|
67
|
+
previous !== undefined &&
|
|
68
|
+
next !== undefined &&
|
|
69
|
+
previous.length === next.length &&
|
|
70
|
+
previous.every((value, index) => Object.is(value, next[index]))
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function equalValue(previous: any, next: any): boolean {
|
|
75
|
+
if (Object.is(previous, next)) return true;
|
|
76
|
+
if (
|
|
77
|
+
previous === null ||
|
|
78
|
+
next === null ||
|
|
79
|
+
typeof previous !== 'object' ||
|
|
80
|
+
typeof next !== 'object' ||
|
|
81
|
+
Array.isArray(previous) !== Array.isArray(next)
|
|
82
|
+
)
|
|
83
|
+
return false;
|
|
84
|
+
const previousKeys = Object.keys(previous);
|
|
85
|
+
const nextKeys = Object.keys(next);
|
|
86
|
+
return (
|
|
87
|
+
previousKeys.length === nextKeys.length &&
|
|
88
|
+
previousKeys.every((key) => Object.hasOwn(next, key) && equalValue(previous[key], next[key]))
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const subCache = new Map<symbol, Map<string, symbol>>();
|
|
93
|
+
|
|
94
|
+
function sub(slot: symbol | undefined, tag: string): symbol | undefined {
|
|
95
|
+
if (slot === undefined) return undefined;
|
|
96
|
+
let tags = subCache.get(slot);
|
|
97
|
+
if (tags === undefined) subCache.set(slot, (tags = new Map()));
|
|
98
|
+
let value = tags.get(tag);
|
|
99
|
+
if (value === undefined) {
|
|
100
|
+
value = Symbol.for(`${slot.description ?? 'react-spring'}:${tag}`);
|
|
101
|
+
tags.set(tag, value);
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function trailingSlot(args: any[]): symbol | undefined {
|
|
107
|
+
const tail = args[args.length - 1];
|
|
108
|
+
return typeof tail === 'symbol' ? tail : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function updateFrom<State extends Record<string, any>>(
|
|
112
|
+
props: RuntimeControllerUpdate<State> | (() => RuntimeControllerUpdate<State>),
|
|
113
|
+
): RuntimeControllerUpdate<State> {
|
|
114
|
+
return typeof props === 'function' ? props() : props;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function withContext<State extends Record<string, any>>(
|
|
118
|
+
update: RuntimeControllerUpdate<State>,
|
|
119
|
+
context: SpringContextValue,
|
|
120
|
+
): RuntimeControllerUpdate<State> {
|
|
121
|
+
return { ...update, ...context } as RuntimeControllerUpdate<State>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function sameLookup(left: unknown, right: unknown): boolean {
|
|
125
|
+
if (Object.is(left, right)) return true;
|
|
126
|
+
if (
|
|
127
|
+
left === null ||
|
|
128
|
+
right === null ||
|
|
129
|
+
typeof left !== 'object' ||
|
|
130
|
+
typeof right !== 'object' ||
|
|
131
|
+
typeof left === 'function' ||
|
|
132
|
+
typeof right === 'function'
|
|
133
|
+
) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const leftKeys = Object.keys(left as object);
|
|
137
|
+
const rightKeys = Object.keys(right as object);
|
|
138
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
139
|
+
return leftKeys.every(function sameKey(key) {
|
|
140
|
+
return Object.is(
|
|
141
|
+
(left as Record<string, unknown>)[key],
|
|
142
|
+
(right as Record<string, unknown>)[key],
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function sameSpringUpdate(
|
|
148
|
+
left: RuntimeControllerUpdate<any>,
|
|
149
|
+
right: RuntimeControllerUpdate<any>,
|
|
150
|
+
): boolean {
|
|
151
|
+
return (
|
|
152
|
+
sameLookup(left.to, right.to) &&
|
|
153
|
+
sameLookup(left.from, right.from) &&
|
|
154
|
+
sameLookup(left.config, right.config) &&
|
|
155
|
+
Object.is(left.immediate, right.immediate) &&
|
|
156
|
+
Object.is(left.delay, right.delay) &&
|
|
157
|
+
Object.is(left.pause, right.pause) &&
|
|
158
|
+
Object.is(left.cancel, right.cancel) &&
|
|
159
|
+
Object.is(left.reset, right.reset) &&
|
|
160
|
+
Object.is(left.loop, right.loop)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function useSpring<State extends Record<string, any>>(
|
|
165
|
+
props: RuntimeControllerUpdate<State>,
|
|
166
|
+
): { [Key in keyof State]: SpringValue<State[Key]> };
|
|
167
|
+
export function useSpring<State extends Record<string, any>>(
|
|
168
|
+
props: RuntimeControllerUpdate<State> | (() => RuntimeControllerUpdate<State>),
|
|
169
|
+
deps: readonly any[] | undefined,
|
|
170
|
+
): [{ [Key in keyof State]: SpringValue<State[Key]> }, Controller<State>];
|
|
171
|
+
export function useSpring<Props extends object>(
|
|
172
|
+
props: Function | (() => (Props & Valid<Props, UseSpringProps<Props>>) | UseSpringProps),
|
|
173
|
+
deps?: readonly any[] | undefined,
|
|
174
|
+
): PickAnimated<Props> extends infer State
|
|
175
|
+
? State extends Lookup
|
|
176
|
+
? [SpringValues<State>, Controller<State>]
|
|
177
|
+
: never
|
|
178
|
+
: never;
|
|
179
|
+
export function useSpring<Props extends object>(
|
|
180
|
+
props: (Props & Valid<Props, UseSpringProps<Props>>) | UseSpringProps,
|
|
181
|
+
): SpringValues<PickAnimated<Props>>;
|
|
182
|
+
export function useSpring<Props extends object>(
|
|
183
|
+
props: (Props & Valid<Props, UseSpringProps<Props>>) | UseSpringProps,
|
|
184
|
+
deps: readonly any[] | undefined,
|
|
185
|
+
): PickAnimated<Props> extends infer State
|
|
186
|
+
? State extends Lookup
|
|
187
|
+
? [SpringValues<State>, Controller<State>]
|
|
188
|
+
: never
|
|
189
|
+
: never;
|
|
190
|
+
export function useSpring(props: any, ...args: any[]): any {
|
|
191
|
+
const slot = trailingSlot(args);
|
|
192
|
+
const context = useSpringContext();
|
|
193
|
+
const update = withContext(
|
|
194
|
+
inferTo(updateFrom(props) as object) as RuntimeControllerUpdate<any>,
|
|
195
|
+
context,
|
|
196
|
+
);
|
|
197
|
+
const [controller] = useState(
|
|
198
|
+
function createController() {
|
|
199
|
+
return new Controller(update as never);
|
|
200
|
+
},
|
|
201
|
+
sub(slot, 'controller'),
|
|
202
|
+
);
|
|
203
|
+
const deps = args.find(Array.isArray) as any[] | undefined;
|
|
204
|
+
const functionProps = typeof props === 'function';
|
|
205
|
+
const cached = useRef(update, sub(slot, 'cached'));
|
|
206
|
+
if (functionProps || deps !== undefined || !sameSpringUpdate(cached.current, update)) {
|
|
207
|
+
cached.current = update;
|
|
208
|
+
}
|
|
209
|
+
const effectUpdate = cached.current;
|
|
210
|
+
useLayoutEffect(
|
|
211
|
+
() => {
|
|
212
|
+
void controller.start(effectUpdate);
|
|
213
|
+
},
|
|
214
|
+
deps === undefined
|
|
215
|
+
? functionProps
|
|
216
|
+
? [update, context]
|
|
217
|
+
: [effectUpdate, context]
|
|
218
|
+
: [...deps, context],
|
|
219
|
+
sub(slot, 'effect'),
|
|
220
|
+
);
|
|
221
|
+
useLayoutEffect(
|
|
222
|
+
() => () => {
|
|
223
|
+
controller.stop(true);
|
|
224
|
+
},
|
|
225
|
+
[],
|
|
226
|
+
sub(slot, 'cleanup'),
|
|
227
|
+
);
|
|
228
|
+
return functionProps || deps !== undefined
|
|
229
|
+
? [controller.springs, controller]
|
|
230
|
+
: controller.springs;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function useSpringValue<T>(
|
|
234
|
+
initial: T,
|
|
235
|
+
propsOrSlot?: Omit<SpringUpdate<T>, 'to'> | symbol,
|
|
236
|
+
...args: any[]
|
|
237
|
+
): SpringValue<T> {
|
|
238
|
+
const slot = typeof propsOrSlot === 'symbol' ? propsOrSlot : trailingSlot(args);
|
|
239
|
+
const props = typeof propsOrSlot === 'symbol' ? undefined : propsOrSlot;
|
|
240
|
+
// Upstream: `new SpringValue(initial, props)` with constructor `default: true`.
|
|
241
|
+
const [value] = useState(
|
|
242
|
+
function createSpringValue() {
|
|
243
|
+
return new SpringValue(initial, props);
|
|
244
|
+
},
|
|
245
|
+
sub(slot, 'value'),
|
|
246
|
+
);
|
|
247
|
+
useLayoutEffect(
|
|
248
|
+
() => () => {
|
|
249
|
+
value.stop(true);
|
|
250
|
+
},
|
|
251
|
+
[],
|
|
252
|
+
sub(slot, 'cleanup'),
|
|
253
|
+
);
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function useSprings<State extends Record<string, any>>(
|
|
258
|
+
length: number,
|
|
259
|
+
props:
|
|
260
|
+
| RuntimeControllerUpdate<State>[]
|
|
261
|
+
| ((index: number, controller: Controller<State>) => RuntimeControllerUpdate<State>),
|
|
262
|
+
...args: any[]
|
|
263
|
+
): [{ [Key in keyof State]: SpringValue<State[Key]> }[], SpringRef<State>] {
|
|
264
|
+
const slot = trailingSlot(args);
|
|
265
|
+
const context = useSpringContext();
|
|
266
|
+
const [state] = useState(
|
|
267
|
+
() => ({
|
|
268
|
+
controllers: [] as Controller<State>[],
|
|
269
|
+
ref: new SpringRef<State>(),
|
|
270
|
+
updates: [] as RuntimeControllerUpdate<State>[],
|
|
271
|
+
deps: undefined as any[] | undefined,
|
|
272
|
+
}),
|
|
273
|
+
sub(slot, 'state'),
|
|
274
|
+
);
|
|
275
|
+
const deps = args.find(Array.isArray) as any[] | undefined;
|
|
276
|
+
const previousLength = state.controllers.length;
|
|
277
|
+
while (state.controllers.length < length) {
|
|
278
|
+
const controller = new Controller<State>();
|
|
279
|
+
state.controllers.push(controller);
|
|
280
|
+
state.ref.add(controller);
|
|
281
|
+
}
|
|
282
|
+
while (state.controllers.length > length) {
|
|
283
|
+
const controller = state.controllers.pop()!;
|
|
284
|
+
controller.stop(true);
|
|
285
|
+
state.ref.delete(controller);
|
|
286
|
+
}
|
|
287
|
+
const functionProps = typeof props === 'function';
|
|
288
|
+
const depsChanged = deps !== undefined && !equalDeps(state.deps, deps);
|
|
289
|
+
if (!functionProps || depsChanged) {
|
|
290
|
+
state.updates = state.controllers.map((controller, index) =>
|
|
291
|
+
withContext(functionProps ? props(index, controller) : (props[index] ?? {}), context),
|
|
292
|
+
);
|
|
293
|
+
} else if (length > previousLength) {
|
|
294
|
+
for (let index = previousLength; index < length; index++) {
|
|
295
|
+
state.updates[index] = withContext(props(index, state.controllers[index]), context);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
state.updates.length = length;
|
|
299
|
+
state.deps = deps?.slice();
|
|
300
|
+
const updates = state.updates;
|
|
301
|
+
useLayoutEffect(
|
|
302
|
+
() => {
|
|
303
|
+
updates.forEach((update, index) => void state.controllers[index].start(update));
|
|
304
|
+
},
|
|
305
|
+
[updates, context, length],
|
|
306
|
+
sub(slot, 'effect'),
|
|
307
|
+
);
|
|
308
|
+
useLayoutEffect(
|
|
309
|
+
() => () => state.controllers.forEach((controller) => controller.stop(true)),
|
|
310
|
+
[],
|
|
311
|
+
sub(slot, 'cleanup'),
|
|
312
|
+
);
|
|
313
|
+
return [state.controllers.map((controller) => controller.springs), state.ref];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function useTrail<State extends Record<string, any>>(
|
|
317
|
+
length: number,
|
|
318
|
+
props: RuntimeControllerUpdate<State>,
|
|
319
|
+
): Array<{ [Key in keyof State]: SpringValue<State[Key]> }>;
|
|
320
|
+
export function useTrail<State extends Record<string, any>>(
|
|
321
|
+
length: number,
|
|
322
|
+
props: RuntimeControllerUpdate<State> | (() => RuntimeControllerUpdate<State>),
|
|
323
|
+
deps: any[],
|
|
324
|
+
): [Array<{ [Key in keyof State]: SpringValue<State[Key]> }>, SpringRef<State>];
|
|
325
|
+
export function useTrail<State extends Record<string, any>>(
|
|
326
|
+
length: number,
|
|
327
|
+
props: () => RuntimeControllerUpdate<State>,
|
|
328
|
+
): [Array<{ [Key in keyof State]: SpringValue<State[Key]> }>, SpringRef<State>];
|
|
329
|
+
export function useTrail<State extends Record<string, any>>(
|
|
330
|
+
length: number,
|
|
331
|
+
props: RuntimeControllerUpdate<State> | (() => RuntimeControllerUpdate<State>),
|
|
332
|
+
...args: any[]
|
|
333
|
+
):
|
|
334
|
+
| Array<{ [Key in keyof State]: SpringValue<State[Key]> }>
|
|
335
|
+
| [Array<{ [Key in keyof State]: SpringValue<State[Key]> }>, SpringRef<State>] {
|
|
336
|
+
const slot = trailingSlot(args);
|
|
337
|
+
const deps = args.find(Array.isArray) as any[] | undefined;
|
|
338
|
+
const functionProps = typeof props === 'function';
|
|
339
|
+
const nextUpdate = updateFrom(props);
|
|
340
|
+
const [trailState] = useState(
|
|
341
|
+
() => ({ update: nextUpdate, revision: {} }),
|
|
342
|
+
sub(slot, 'trail-state'),
|
|
343
|
+
);
|
|
344
|
+
if (!equalValue(trailState.update, nextUpdate)) {
|
|
345
|
+
trailState.update = nextUpdate;
|
|
346
|
+
trailState.revision = {};
|
|
347
|
+
}
|
|
348
|
+
const result = useSprings(
|
|
349
|
+
length,
|
|
350
|
+
(index) => {
|
|
351
|
+
const update = trailState.update;
|
|
352
|
+
return {
|
|
353
|
+
...update,
|
|
354
|
+
delay: (update.delay ?? 0) + index * 16,
|
|
355
|
+
};
|
|
356
|
+
},
|
|
357
|
+
deps ?? (functionProps ? [] : [trailState.revision]),
|
|
358
|
+
slot,
|
|
359
|
+
);
|
|
360
|
+
return functionProps || deps !== undefined ? result : result[0];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function useSpringRef<State extends Record<string, any>>(...args: any[]): SpringRef<State> {
|
|
364
|
+
const slot = trailingSlot(args);
|
|
365
|
+
return useState(() => new SpringRef<State>(), sub(slot, 'ref'))[0];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function resolveTransitionValue<Item, State extends Record<string, any>>(
|
|
369
|
+
value: Partial<State> | ((item: Item, index?: number) => Partial<State>) | Function | undefined,
|
|
370
|
+
item: Item,
|
|
371
|
+
index = 0,
|
|
372
|
+
): Partial<State> | undefined {
|
|
373
|
+
return typeof value === 'function'
|
|
374
|
+
? (value as (item: Item, index: number) => Partial<State>)(item, index)
|
|
375
|
+
: value;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function transitionTarget<State extends Record<string, any>>(
|
|
379
|
+
value: Partial<State> | undefined,
|
|
380
|
+
): { to: Partial<State> | undefined; update: Partial<RuntimeControllerUpdate<State>> } {
|
|
381
|
+
if (value === undefined) return { to: undefined, update: {} };
|
|
382
|
+
const to: Record<string, any> = {};
|
|
383
|
+
const update: Record<string, any> = {};
|
|
384
|
+
const updateKeys = new Set([
|
|
385
|
+
'cancel',
|
|
386
|
+
'config',
|
|
387
|
+
'delay',
|
|
388
|
+
'immediate',
|
|
389
|
+
'onChange',
|
|
390
|
+
'onResolve',
|
|
391
|
+
'onRest',
|
|
392
|
+
'onStart',
|
|
393
|
+
'pause',
|
|
394
|
+
'reset',
|
|
395
|
+
]);
|
|
396
|
+
for (const [key, item] of Object.entries(value)) {
|
|
397
|
+
(updateKeys.has(key) ? update : to)[key] = item;
|
|
398
|
+
}
|
|
399
|
+
return { to: to as Partial<State>, update };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function useTransition<Item, Props extends object>(
|
|
403
|
+
items: Item | readonly Item[],
|
|
404
|
+
props: UseTransitionProps<Item> | (Props & Valid<Props, UseTransitionProps<Item>>),
|
|
405
|
+
...args: any[]
|
|
406
|
+
): PickAnimated<Props> extends infer State
|
|
407
|
+
? State extends Lookup
|
|
408
|
+
? (
|
|
409
|
+
render: (
|
|
410
|
+
values: SpringValues<State>,
|
|
411
|
+
item: Item,
|
|
412
|
+
transition: { key: string | number; phase: TransitionPhase },
|
|
413
|
+
index: number,
|
|
414
|
+
) => unknown,
|
|
415
|
+
) => unknown[]
|
|
416
|
+
: never
|
|
417
|
+
: never;
|
|
418
|
+
export function useTransition<Item = any, State extends Lookup = Lookup>(
|
|
419
|
+
items: any,
|
|
420
|
+
props: any,
|
|
421
|
+
...args: any[]
|
|
422
|
+
): any {
|
|
423
|
+
const slot = trailingSlot(args);
|
|
424
|
+
const context = useSpringContext();
|
|
425
|
+
const list = (Array.isArray(items) ? items : [items]).filter(
|
|
426
|
+
(item): item is Item => item !== undefined && item !== null,
|
|
427
|
+
);
|
|
428
|
+
const [, forceUpdate] = useState(0, sub(slot, 'version'));
|
|
429
|
+
const [transitionState] = useState(
|
|
430
|
+
() => ({ props, context, revision: 0 }),
|
|
431
|
+
sub(slot, 'transition-state'),
|
|
432
|
+
);
|
|
433
|
+
const transitionChanged =
|
|
434
|
+
!equalValue(transitionState.props, props) || !equalValue(transitionState.context, context);
|
|
435
|
+
if (transitionChanged) {
|
|
436
|
+
transitionState.props = props;
|
|
437
|
+
transitionState.context = context;
|
|
438
|
+
transitionState.revision++;
|
|
439
|
+
}
|
|
440
|
+
const [records] = useState(
|
|
441
|
+
() => [] as Array<TransitionRecord<Item, State>>,
|
|
442
|
+
sub(slot, 'records'),
|
|
443
|
+
);
|
|
444
|
+
const nextKeys = list.map((item, index) =>
|
|
445
|
+
typeof props.keys === 'function'
|
|
446
|
+
? props.keys(item)
|
|
447
|
+
: Array.isArray(props.keys)
|
|
448
|
+
? props.keys[index]
|
|
449
|
+
: ((item as any)?.key ?? (item as any)?.id ?? (item as any)),
|
|
450
|
+
);
|
|
451
|
+
const nextKeySet = new Set(nextKeys);
|
|
452
|
+
|
|
453
|
+
for (const record of records) {
|
|
454
|
+
if (!nextKeySet.has(record.key) && record.phase !== 'leave') record.phase = 'leave';
|
|
455
|
+
}
|
|
456
|
+
const hasLeaving = records.some((record) => record.phase === 'leave');
|
|
457
|
+
list.forEach((item, index) => {
|
|
458
|
+
const key = nextKeys[index]!;
|
|
459
|
+
const existing = records.find((record) => record.key === key);
|
|
460
|
+
if (existing !== undefined) {
|
|
461
|
+
if (existing.expiration !== undefined) {
|
|
462
|
+
clearTimeout(existing.expiration);
|
|
463
|
+
existing.expiration = undefined;
|
|
464
|
+
}
|
|
465
|
+
const itemChanged = !equalValue(existing.item, item);
|
|
466
|
+
existing.item = item;
|
|
467
|
+
if (existing.phase === 'leave') existing.phase = 'enter';
|
|
468
|
+
else if (itemChanged || transitionChanged) existing.phase = 'update';
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (props.exitBeforeEnter && hasLeaving) return;
|
|
472
|
+
const from = (props.initial === null ? props.from : (props.initial ?? props.from)) as
|
|
473
|
+
Partial<State> | undefined;
|
|
474
|
+
records.push({
|
|
475
|
+
key,
|
|
476
|
+
item,
|
|
477
|
+
phase: 'enter',
|
|
478
|
+
controller: new Controller<State>({ from }),
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
if (props.sort !== undefined) {
|
|
482
|
+
records.sort((a, b) => props.sort!(a.item, b.item));
|
|
483
|
+
} else {
|
|
484
|
+
const active = nextKeys
|
|
485
|
+
.map((key) => records.find((record) => record.key === key))
|
|
486
|
+
.filter((record): record is TransitionRecord<Item, State> => record !== undefined);
|
|
487
|
+
const leaving = records.filter((record) => !nextKeySet.has(record.key));
|
|
488
|
+
records.splice(0, records.length, ...active, ...leaving);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const signature = `${transitionState.revision}|${records
|
|
492
|
+
.map((record) => `${String(record.key)}:${record.phase}`)
|
|
493
|
+
.join('|')}`;
|
|
494
|
+
useLayoutEffect(
|
|
495
|
+
() => {
|
|
496
|
+
let active = true;
|
|
497
|
+
for (const record of [...records]) {
|
|
498
|
+
const target =
|
|
499
|
+
record.phase === 'leave'
|
|
500
|
+
? resolveTransitionValue(props.leave, record.item)
|
|
501
|
+
: record.phase === 'update'
|
|
502
|
+
? resolveTransitionValue(props.update ?? props.enter, record.item)
|
|
503
|
+
: resolveTransitionValue(props.enter, record.item);
|
|
504
|
+
const phase = record.phase;
|
|
505
|
+
const phaseRecords = records.filter((candidate) => candidate.phase === phase);
|
|
506
|
+
const phaseIndex = phaseRecords.indexOf(record);
|
|
507
|
+
const trailIndex =
|
|
508
|
+
phase === 'leave' && props.reverse ? phaseRecords.length - phaseIndex - 1 : phaseIndex;
|
|
509
|
+
const payload = transitionTarget(target);
|
|
510
|
+
const update = withContext(
|
|
511
|
+
{
|
|
512
|
+
to: payload.to,
|
|
513
|
+
config: props.config,
|
|
514
|
+
immediate: props.immediate,
|
|
515
|
+
delay: (props.delay ?? 0) + (props.trail ?? 0) * trailIndex,
|
|
516
|
+
cancel: props.cancel,
|
|
517
|
+
pause: props.pause,
|
|
518
|
+
onStart: props.onStart,
|
|
519
|
+
onChange: props.onChange,
|
|
520
|
+
onRest: props.onRest,
|
|
521
|
+
onResolve: props.onResolve,
|
|
522
|
+
...payload.update,
|
|
523
|
+
},
|
|
524
|
+
context,
|
|
525
|
+
);
|
|
526
|
+
if (equalValue(record.lastUpdate, update as RuntimeControllerUpdate<State>)) continue;
|
|
527
|
+
record.lastUpdate = update as RuntimeControllerUpdate<State>;
|
|
528
|
+
void record.controller.start(update as RuntimeControllerUpdate<State>).then(() => {
|
|
529
|
+
if (!active || phase !== 'leave' || record.phase !== 'leave') return;
|
|
530
|
+
const expiration =
|
|
531
|
+
typeof props.expires === 'function'
|
|
532
|
+
? props.expires(record.item)
|
|
533
|
+
: (props.expires ?? true);
|
|
534
|
+
const remove = () => {
|
|
535
|
+
const index = records.indexOf(record);
|
|
536
|
+
if (index >= 0) {
|
|
537
|
+
records.splice(index, 1);
|
|
538
|
+
props.onDestroyed?.(record.item, record.key);
|
|
539
|
+
forceUpdate((value) => value + 1);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
if (typeof expiration === 'number' && expiration > 0)
|
|
543
|
+
record.expiration = setTimeout(remove, expiration);
|
|
544
|
+
else if (expiration !== false) remove();
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
return () => {
|
|
548
|
+
active = false;
|
|
549
|
+
};
|
|
550
|
+
},
|
|
551
|
+
[signature],
|
|
552
|
+
sub(slot, 'transition-effect'),
|
|
553
|
+
);
|
|
554
|
+
useLayoutEffect(
|
|
555
|
+
() => () => {
|
|
556
|
+
records.forEach((record) => {
|
|
557
|
+
if (record.expiration !== undefined) clearTimeout(record.expiration);
|
|
558
|
+
record.controller.stop(true);
|
|
559
|
+
});
|
|
560
|
+
records.length = 0;
|
|
561
|
+
},
|
|
562
|
+
[],
|
|
563
|
+
sub(slot, 'transition-cleanup'),
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
return function renderTransitions(
|
|
567
|
+
render: (
|
|
568
|
+
values: SpringValues<State>,
|
|
569
|
+
item: Item,
|
|
570
|
+
transition: { key: string | number; phase: TransitionPhase },
|
|
571
|
+
index: number,
|
|
572
|
+
) => unknown,
|
|
573
|
+
) {
|
|
574
|
+
return records.map((record, index) =>
|
|
575
|
+
render(
|
|
576
|
+
record.controller.springs as SpringValues<State>,
|
|
577
|
+
record.item,
|
|
578
|
+
{ key: record.key, phase: record.phase },
|
|
579
|
+
index,
|
|
580
|
+
),
|
|
581
|
+
);
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function useChain(refs: SpringRef<any>[], ...args: any[]): void {
|
|
586
|
+
const slot = trailingSlot(args);
|
|
587
|
+
const rest = slot !== undefined ? args.slice(0, -1) : args;
|
|
588
|
+
const timeSteps = Array.isArray(rest[0]) ? (rest[0] as number[]) : undefined;
|
|
589
|
+
const timeFrame = typeof rest[1] === 'number' ? (rest[1] as number) : 1000;
|
|
590
|
+
useLayoutEffect(
|
|
591
|
+
() => {
|
|
592
|
+
let active = true;
|
|
593
|
+
if (timeSteps === undefined) {
|
|
594
|
+
void refs.reduce(
|
|
595
|
+
(sequence, ref) => sequence.then(() => (active ? ref.start() : undefined)),
|
|
596
|
+
Promise.resolve<unknown>(undefined),
|
|
597
|
+
);
|
|
598
|
+
return () => {
|
|
599
|
+
active = false;
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const timers = refs.map((ref, index) =>
|
|
603
|
+
setTimeout(() => void ref.start(), (timeSteps[index] ?? 0) * timeFrame),
|
|
604
|
+
);
|
|
605
|
+
return () => timers.forEach(clearTimeout);
|
|
606
|
+
},
|
|
607
|
+
[refs, timeSteps, timeFrame],
|
|
608
|
+
sub(slot, 'chain'),
|
|
609
|
+
);
|
|
610
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export * from './engine';
|
|
2
|
+
export * from './hooks';
|
|
3
|
+
export * from './browser';
|
|
4
|
+
export { createInterpolator } from './shared/createInterpolator';
|
|
5
|
+
export {
|
|
6
|
+
Any,
|
|
7
|
+
BailSignal,
|
|
8
|
+
Globals,
|
|
9
|
+
easings,
|
|
10
|
+
inferTo,
|
|
11
|
+
update,
|
|
12
|
+
type InterpolatorConfig,
|
|
13
|
+
} from './upstream-compat';
|
|
14
|
+
export { FrameValue } from './core/FrameValue';
|
|
15
|
+
export type { SpringContextValue } from './context';
|
|
16
|
+
export { SpringContext, Trail, Transition } from './components.tsrx';
|
|
17
|
+
export { Spring } from './spring-component';
|
|
18
|
+
export { animated, a } from './web/animated';
|
|
19
|
+
export type {
|
|
20
|
+
ForwardProps,
|
|
21
|
+
Lookup,
|
|
22
|
+
PickAnimated,
|
|
23
|
+
ReservedProps,
|
|
24
|
+
SpringUpdateFn,
|
|
25
|
+
ControllerUpdate as TypedControllerUpdate,
|
|
26
|
+
} from './types/index';
|