@onlynative/inertia 0.0.1-alpha.2 → 0.0.1-alpha.3
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/README.md +29 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +170 -58
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +171 -59
- package/dist/index.mjs.map +1 -1
- package/dist/motion/Image.d.mts +1 -1
- package/dist/motion/Image.d.ts +1 -1
- package/dist/motion/Image.js +170 -58
- package/dist/motion/Image.js.map +1 -1
- package/dist/motion/Image.mjs +171 -59
- package/dist/motion/Image.mjs.map +1 -1
- package/dist/motion/Pressable.d.mts +1 -1
- package/dist/motion/Pressable.d.ts +1 -1
- package/dist/motion/Pressable.js +170 -58
- package/dist/motion/Pressable.js.map +1 -1
- package/dist/motion/Pressable.mjs +171 -59
- package/dist/motion/Pressable.mjs.map +1 -1
- package/dist/motion/ScrollView.d.mts +1 -1
- package/dist/motion/ScrollView.d.ts +1 -1
- package/dist/motion/ScrollView.js +170 -58
- package/dist/motion/ScrollView.js.map +1 -1
- package/dist/motion/ScrollView.mjs +171 -59
- package/dist/motion/ScrollView.mjs.map +1 -1
- package/dist/motion/Text.d.mts +1 -1
- package/dist/motion/Text.d.ts +1 -1
- package/dist/motion/Text.js +170 -58
- package/dist/motion/Text.js.map +1 -1
- package/dist/motion/Text.mjs +171 -59
- package/dist/motion/Text.mjs.map +1 -1
- package/dist/motion/View.d.mts +1 -1
- package/dist/motion/View.d.ts +1 -1
- package/dist/motion/View.js +170 -58
- package/dist/motion/View.js.map +1 -1
- package/dist/motion/View.mjs +171 -59
- package/dist/motion/View.mjs.map +1 -1
- package/dist/{types-DeZZzE_e.d.mts → types-DAhX3fC2.d.mts} +40 -16
- package/dist/{types-DeZZzE_e.d.ts → types-DAhX3fC2.d.ts} +40 -16
- package/llms.txt +25 -2
- package/package.json +1 -1
- package/src/motion/createMotionComponent.tsx +258 -97
- package/src/motion/installCheck.ts +69 -0
- package/src/types.ts +44 -16
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
useState,
|
|
8
8
|
} from 'react'
|
|
9
9
|
import Animated, {
|
|
10
|
+
interpolateColor,
|
|
10
11
|
runOnJS,
|
|
11
12
|
useAnimatedStyle,
|
|
12
13
|
useSharedValue,
|
|
@@ -15,11 +16,13 @@ import Animated, {
|
|
|
15
16
|
import { useShouldReduceMotion } from '../config'
|
|
16
17
|
import { isFocusVisible } from '../gestures'
|
|
17
18
|
import { usePresence } from '../presence'
|
|
18
|
-
import { resolveAnimatableValue } from '../transitions'
|
|
19
|
+
import { resolveAnimatableValue, resolveTransition } from '../transitions'
|
|
20
|
+
import { ensureReanimatedInstalled } from './installCheck'
|
|
19
21
|
import {
|
|
20
22
|
type AnimatableValue,
|
|
21
23
|
type AnimateStyle,
|
|
22
24
|
type AnimationCallbackInfo,
|
|
25
|
+
type GestureLayerTransitions,
|
|
23
26
|
type GestureSubStates,
|
|
24
27
|
type MotionComponent,
|
|
25
28
|
type MotionProps,
|
|
@@ -84,6 +87,16 @@ type AnimatableKey = (typeof ALL_KEYS)[number]
|
|
|
84
87
|
type TransformKey = (typeof TRANSFORM_KEYS)[number]
|
|
85
88
|
|
|
86
89
|
const TRANSFORM_KEY_SET = new Set<AnimatableKey>(TRANSFORM_KEYS)
|
|
90
|
+
const COLOR_KEY_SET = new Set<AnimatableKey>(COLOR_KEYS)
|
|
91
|
+
|
|
92
|
+
const GESTURE_LAYER_NAMES = [
|
|
93
|
+
'hovered',
|
|
94
|
+
'focused',
|
|
95
|
+
'focusVisible',
|
|
96
|
+
'pressed',
|
|
97
|
+
] as const
|
|
98
|
+
type GestureLayerName = (typeof GESTURE_LAYER_NAMES)[number]
|
|
99
|
+
const GESTURE_LAYER_NAME_SET = new Set<string>(GESTURE_LAYER_NAMES)
|
|
87
100
|
|
|
88
101
|
// Stable style object applied while a Motion primitive is mid-exit so taps
|
|
89
102
|
// fall through. Hoisted so every render shares the same reference and
|
|
@@ -140,9 +153,24 @@ function transitionFor<S>(
|
|
|
140
153
|
): TransitionConfig | undefined {
|
|
141
154
|
if (!transition) return undefined
|
|
142
155
|
if (isTopLevelTransition(transition)) return transition
|
|
156
|
+
// Gesture-layer keys (`pressed`, `hovered`, …) live on the same map as
|
|
157
|
+
// per-property keys; skip them when looking up a property transition so a
|
|
158
|
+
// user who wires `transition.pressed` doesn't accidentally apply that to a
|
|
159
|
+
// style key named `pressed` (none currently exist, but keep the lookup
|
|
160
|
+
// honest).
|
|
161
|
+
if (GESTURE_LAYER_NAME_SET.has(prop as string)) return undefined
|
|
143
162
|
return (transition as PerPropertyTransition<S>)[prop]
|
|
144
163
|
}
|
|
145
164
|
|
|
165
|
+
function gestureLayerTransitionFor<S>(
|
|
166
|
+
layer: GestureLayerName,
|
|
167
|
+
transition: Transition<S> | undefined,
|
|
168
|
+
): TransitionConfig | undefined {
|
|
169
|
+
if (!transition) return undefined
|
|
170
|
+
if (isTopLevelTransition(transition)) return transition
|
|
171
|
+
return (transition as GestureLayerTransitions)[layer]
|
|
172
|
+
}
|
|
173
|
+
|
|
146
174
|
/**
|
|
147
175
|
* Factory that wraps a React Native primitive as a `Motion.*` component.
|
|
148
176
|
*
|
|
@@ -154,10 +182,17 @@ function transitionFor<S>(
|
|
|
154
182
|
* borderRadius) and color properties (backgroundColor, borderColor, color,
|
|
155
183
|
* tintColor) applied via Reanimated shared values + `useAnimatedStyle`.
|
|
156
184
|
*/
|
|
185
|
+
// `ComponentType<any>` is React's canonical "accept any component" idiom.
|
|
186
|
+
// `unknown` doesn't work — props need to widen to whatever `C` actually accepts
|
|
187
|
+
// at the call site. The two `any` uses below are deliberate.
|
|
188
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
157
189
|
export function createMotionComponent<C extends ComponentType<any>>(
|
|
158
190
|
Component: C,
|
|
159
191
|
): MotionComponent<C> {
|
|
192
|
+
ensureReanimatedInstalled()
|
|
193
|
+
|
|
160
194
|
const AnimatedComponent = Animated.createAnimatedComponent(
|
|
195
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
161
196
|
Component as ComponentType<any>,
|
|
162
197
|
)
|
|
163
198
|
|
|
@@ -220,10 +255,10 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
220
255
|
>)
|
|
221
256
|
: undefined
|
|
222
257
|
|
|
223
|
-
// Gesture sub-state activation tracked as JS state
|
|
224
|
-
// the
|
|
225
|
-
//
|
|
226
|
-
//
|
|
258
|
+
// Gesture sub-state activation tracked as JS state. Activation flips drive
|
|
259
|
+
// the per-layer progress shared values (0↔1); they intentionally do NOT
|
|
260
|
+
// re-run the value-driving effect — gesture sub-state targets live on the
|
|
261
|
+
// worklet's composition chain, not on the base `animate` SV.
|
|
227
262
|
const [pressed, setPressed] = useState(false)
|
|
228
263
|
const [focused, setFocused] = useState(false)
|
|
229
264
|
const [focusVisible, setFocusVisible] = useState(false)
|
|
@@ -233,7 +268,8 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
233
268
|
// variants in play the union across all variants is what matters — a key
|
|
234
269
|
// touched by any variant must be active so the worklet picks it up when
|
|
235
270
|
// the controller transitions. Gesture sub-states join the same union so
|
|
236
|
-
// pressed/focused/hovered targets can drive any key they
|
|
271
|
+
// pressed/focused/focusVisible/hovered targets can drive any key they
|
|
272
|
+
// declare even when the base `animate` doesn't touch it.
|
|
237
273
|
const activeKeysRef = useRef<readonly AnimatableKey[] | null>(null)
|
|
238
274
|
if (activeKeysRef.current === null) {
|
|
239
275
|
const touched = new Set<AnimatableKey>()
|
|
@@ -285,24 +321,46 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
285
321
|
)
|
|
286
322
|
})
|
|
287
323
|
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
324
|
+
// One progress SV per gesture layer, allocated unconditionally for hook
|
|
325
|
+
// stability. Each layer's progress animates 0↔1 with its own transition
|
|
326
|
+
// when its activation flips; the worklet reads them when compositing.
|
|
327
|
+
// Initial value is 0 — even if a sub-state is somehow active on mount,
|
|
328
|
+
// the activation effect below will animate it to 1 on the next tick.
|
|
329
|
+
const pressedProgress = useSharedValue(0)
|
|
330
|
+
const focusedProgress = useSharedValue(0)
|
|
331
|
+
const focusVisibleProgress = useSharedValue(0)
|
|
332
|
+
const hoveredProgress = useSharedValue(0)
|
|
333
|
+
|
|
334
|
+
// Mirror gesture targets into a UI-runtime-resident shared value so the
|
|
335
|
+
// animated-style worklet can read the latest layer values without having
|
|
336
|
+
// to capture `gesture` directly (which would re-register the worklet on
|
|
337
|
+
// every render where the consumer passes a fresh literal). The signature
|
|
338
|
+
// dependency means we only push to the SV when targets actually change —
|
|
339
|
+
// the SV ref itself is stable across renders.
|
|
292
340
|
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
341
|
+
// The resolved value is a layer-keyed map of primitive endpoints (numbers
|
|
342
|
+
// or color strings); sequence/`{ to }` step shapes on a sub-state collapse
|
|
343
|
+
// to their final endpoint via `targetEndValue` because a gesture layer
|
|
344
|
+
// describes a steady target, not a keyframe sequence.
|
|
345
|
+
const gestureSV = useSharedValue<ResolvedGestureLayers | null>(
|
|
346
|
+
resolveGestureLayers(gesture),
|
|
347
|
+
)
|
|
348
|
+
const gestureTargetsSig = stableSig(gesture)
|
|
349
|
+
useEffect(() => {
|
|
350
|
+
gestureSV.value = resolveGestureLayers(gesture)
|
|
351
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
352
|
+
}, [gestureTargetsSig])
|
|
353
|
+
|
|
354
|
+
// The base record drives the per-key shared values. Gesture sub-state
|
|
355
|
+
// targets are intentionally NOT merged here — they layer on top in the
|
|
356
|
+
// worklet. Exit values still take precedence over `animate` while exiting
|
|
357
|
+
// because the base SV is what <Presence> waits on to settle.
|
|
358
|
+
const baseRecord =
|
|
296
359
|
isExiting && exitRecord
|
|
297
360
|
? { ...animateRecord, ...exitRecord }
|
|
298
|
-
:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
focusVisible,
|
|
302
|
-
hovered,
|
|
303
|
-
})
|
|
304
|
-
const mergedSig =
|
|
305
|
-
stableSig(mergedRecord) +
|
|
361
|
+
: animateRecord
|
|
362
|
+
const baseSig =
|
|
363
|
+
stableSig(baseRecord) +
|
|
306
364
|
(isExiting ? '|exit' : '') +
|
|
307
365
|
(shouldReduceMotion ? '|rm' : '')
|
|
308
366
|
const transitionSig = stableSig(transition)
|
|
@@ -337,7 +395,7 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
337
395
|
// the factory skip the coalescing branch entirely.
|
|
338
396
|
let transformPending = 0
|
|
339
397
|
for (const k of ALL_KEYS) {
|
|
340
|
-
if (TRANSFORM_KEY_SET.has(k) &&
|
|
398
|
+
if (TRANSFORM_KEY_SET.has(k) && baseRecord[k] !== undefined) {
|
|
341
399
|
transformPending++
|
|
342
400
|
}
|
|
343
401
|
}
|
|
@@ -345,7 +403,7 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
345
403
|
transformPending > 0 ? { remaining: transformPending } : undefined
|
|
346
404
|
|
|
347
405
|
for (const key of ALL_KEYS) {
|
|
348
|
-
const target =
|
|
406
|
+
const target = baseRecord[key]
|
|
349
407
|
if (target === undefined) continue
|
|
350
408
|
// Reduced-motion overrides every per-key transition (and any nested
|
|
351
409
|
// sequence-step transition) with `no-animation`, which the resolver
|
|
@@ -382,15 +440,117 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
382
440
|
safeToRemoveRef.current?.()
|
|
383
441
|
}
|
|
384
442
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
385
|
-
}, [
|
|
443
|
+
}, [baseSig, transitionSig])
|
|
444
|
+
|
|
445
|
+
// Per-layer progress: when a sub-state activation flips, animate its
|
|
446
|
+
// progress SV 0↔1 with the layer's own transition (or the parent
|
|
447
|
+
// transition / library default, in priority order). On exit we snap every
|
|
448
|
+
// layer to 0 instantly so the unmount-bound base SV isn't fighting a
|
|
449
|
+
// stale layer contribution mid-fade.
|
|
450
|
+
//
|
|
451
|
+
// The `declared` flag short-circuits the effect when the consumer hasn't
|
|
452
|
+
// wired the corresponding sub-state — so a Motion primitive without a
|
|
453
|
+
// `gesture` prop (or with only some sub-states declared) makes zero extra
|
|
454
|
+
// `withSpring` / `withTiming` calls on mount.
|
|
455
|
+
useGestureLayerProgress(
|
|
456
|
+
pressedProgress,
|
|
457
|
+
pressed,
|
|
458
|
+
gesture?.pressed != null,
|
|
459
|
+
'pressed',
|
|
460
|
+
transition,
|
|
461
|
+
isExiting,
|
|
462
|
+
shouldReduceMotion,
|
|
463
|
+
)
|
|
464
|
+
useGestureLayerProgress(
|
|
465
|
+
focusedProgress,
|
|
466
|
+
focused,
|
|
467
|
+
gesture?.focused != null,
|
|
468
|
+
'focused',
|
|
469
|
+
transition,
|
|
470
|
+
isExiting,
|
|
471
|
+
shouldReduceMotion,
|
|
472
|
+
)
|
|
473
|
+
useGestureLayerProgress(
|
|
474
|
+
focusVisibleProgress,
|
|
475
|
+
focusVisible,
|
|
476
|
+
gesture?.focusVisible != null,
|
|
477
|
+
'focusVisible',
|
|
478
|
+
transition,
|
|
479
|
+
isExiting,
|
|
480
|
+
shouldReduceMotion,
|
|
481
|
+
)
|
|
482
|
+
useGestureLayerProgress(
|
|
483
|
+
hoveredProgress,
|
|
484
|
+
hovered,
|
|
485
|
+
gesture?.hovered != null,
|
|
486
|
+
'hovered',
|
|
487
|
+
transition,
|
|
488
|
+
isExiting,
|
|
489
|
+
shouldReduceMotion,
|
|
490
|
+
)
|
|
386
491
|
|
|
387
492
|
const animatedStyle = useAnimatedStyle(() => {
|
|
388
493
|
const activeKeys = activeKeysRef.current!
|
|
389
494
|
const hasTransform = hasTransformRef.current
|
|
390
495
|
const out: Record<string, unknown> = {}
|
|
391
496
|
const transform: Array<Record<string, unknown>> = []
|
|
497
|
+
|
|
498
|
+
// Read each progress SV exactly once so the chain below sees a coherent
|
|
499
|
+
// snapshot for this frame. Reading them on the UI thread is cheap.
|
|
500
|
+
const ph = hoveredProgress.value
|
|
501
|
+
const pf = focusedProgress.value
|
|
502
|
+
const pfv = focusVisibleProgress.value
|
|
503
|
+
const pp = pressedProgress.value
|
|
504
|
+
|
|
505
|
+
const layers = gestureSV.value
|
|
506
|
+
// Locals are suffixed `Layer` so they don't shadow the outer `pressed` /
|
|
507
|
+
// `focused` / `focusVisible` / `hovered` JS-state booleans — Reanimated's
|
|
508
|
+
// worklet closure tracker would otherwise pick those up as captured
|
|
509
|
+
// dependencies and re-register the worklet on every activation flip.
|
|
510
|
+
const hoveredLayer = layers ? layers.hovered : null
|
|
511
|
+
const focusedLayer = layers ? layers.focused : null
|
|
512
|
+
const focusVisibleLayer = layers ? layers.focusVisible : null
|
|
513
|
+
const pressedLayer = layers ? layers.pressed : null
|
|
514
|
+
|
|
392
515
|
for (const key of activeKeys) {
|
|
393
|
-
|
|
516
|
+
let v = sharedValues[key].value
|
|
517
|
+
const isColor = COLOR_KEY_SET.has(key)
|
|
518
|
+
|
|
519
|
+
// Composite gesture layers in priority order (lowest first). Each
|
|
520
|
+
// active layer pulls the value toward its pre-resolved primitive
|
|
521
|
+
// endpoint by `progress`; numeric keys lerp, color keys go through
|
|
522
|
+
// Reanimated's RGBA `interpolateColor`. We skip layers with progress
|
|
523
|
+
// 0 to avoid an `interpolateColor(0, ...)` call that would parse the
|
|
524
|
+
// target color string for no visible effect.
|
|
525
|
+
if (hoveredLayer && ph > 0 && hoveredLayer[key] !== undefined) {
|
|
526
|
+
const t = hoveredLayer[key]
|
|
527
|
+
v = isColor
|
|
528
|
+
? interpolateColor(ph, [0, 1], [v as string, t as string])
|
|
529
|
+
: (v as number) + ((t as number) - (v as number)) * ph
|
|
530
|
+
}
|
|
531
|
+
if (focusedLayer && pf > 0 && focusedLayer[key] !== undefined) {
|
|
532
|
+
const t = focusedLayer[key]
|
|
533
|
+
v = isColor
|
|
534
|
+
? interpolateColor(pf, [0, 1], [v as string, t as string])
|
|
535
|
+
: (v as number) + ((t as number) - (v as number)) * pf
|
|
536
|
+
}
|
|
537
|
+
if (
|
|
538
|
+
focusVisibleLayer &&
|
|
539
|
+
pfv > 0 &&
|
|
540
|
+
focusVisibleLayer[key] !== undefined
|
|
541
|
+
) {
|
|
542
|
+
const t = focusVisibleLayer[key]
|
|
543
|
+
v = isColor
|
|
544
|
+
? interpolateColor(pfv, [0, 1], [v as string, t as string])
|
|
545
|
+
: (v as number) + ((t as number) - (v as number)) * pfv
|
|
546
|
+
}
|
|
547
|
+
if (pressedLayer && pp > 0 && pressedLayer[key] !== undefined) {
|
|
548
|
+
const t = pressedLayer[key]
|
|
549
|
+
v = isColor
|
|
550
|
+
? interpolateColor(pp, [0, 1], [v as string, t as string])
|
|
551
|
+
: (v as number) + ((t as number) - (v as number)) * pp
|
|
552
|
+
}
|
|
553
|
+
|
|
394
554
|
if (TRANSFORM_KEY_SET.has(key)) {
|
|
395
555
|
transform.push(
|
|
396
556
|
key === 'rotate' ? { rotate: `${v}deg` } : { [key]: v },
|
|
@@ -770,78 +930,72 @@ function stableStringify(v: unknown): string {
|
|
|
770
930
|
}
|
|
771
931
|
|
|
772
932
|
/**
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
* priority order (lowest first):
|
|
778
|
-
* `hovered` < `focused` < `focusVisible` < `pressed`.
|
|
933
|
+
* Per-layer resolved targets: each declared gesture sub-state collapses to a
|
|
934
|
+
* map of primitive endpoints (numbers or color strings), already passed
|
|
935
|
+
* through `targetEndValue` so the worklet can use them directly without
|
|
936
|
+
* inspecting `AnimatableValue` shapes on the UI thread.
|
|
779
937
|
*/
|
|
780
|
-
|
|
781
|
-
|
|
938
|
+
type ResolvedGestureLayers = {
|
|
939
|
+
pressed?: Record<string, number | string>
|
|
940
|
+
focused?: Record<string, number | string>
|
|
941
|
+
focusVisible?: Record<string, number | string>
|
|
942
|
+
hovered?: Record<string, number | string>
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function resolveGestureLayers(
|
|
782
946
|
gesture: GestureSubStates<unknown> | undefined,
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
796
|
-
const subStates = [
|
|
797
|
-
gesture.hovered,
|
|
798
|
-
gesture.focused,
|
|
799
|
-
gesture.focusVisible,
|
|
800
|
-
gesture.pressed,
|
|
801
|
-
] as Array<
|
|
802
|
-
Partial<Record<AnimatableKey, AnimatableValue<number | string>>> | undefined
|
|
803
|
-
>
|
|
804
|
-
for (const sub of subStates) {
|
|
805
|
-
if (!sub) continue
|
|
806
|
-
for (const k of ALL_KEYS) {
|
|
807
|
-
if (k in sub && !(k in merged)) {
|
|
808
|
-
merged[k] = DEFAULT_RESTING[k]
|
|
809
|
-
}
|
|
947
|
+
): ResolvedGestureLayers | null {
|
|
948
|
+
if (!gesture) return null
|
|
949
|
+
const out: ResolvedGestureLayers = {}
|
|
950
|
+
for (const layer of GESTURE_LAYER_NAMES) {
|
|
951
|
+
const subState = gesture[layer]
|
|
952
|
+
if (!subState) continue
|
|
953
|
+
const resolved: Record<string, number | string> = {}
|
|
954
|
+
for (const key of ALL_KEYS) {
|
|
955
|
+
const raw = (subState as Record<string, unknown>)[key]
|
|
956
|
+
if (raw === undefined) continue
|
|
957
|
+
const t = targetEndValue(raw as AnimatableValue<number | string>)
|
|
958
|
+
if (t !== undefined) resolved[key] = t
|
|
810
959
|
}
|
|
960
|
+
out[layer] = resolved
|
|
811
961
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
962
|
+
return out
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Drive a single gesture layer's progress shared value 0↔1 with its own
|
|
967
|
+
* transition. Resolution priority for the layer config:
|
|
968
|
+
* `transition.<layerName>` → top-level `transition` → library default spring.
|
|
969
|
+
* On exit, snap to 0 instantly so the unmount-bound base SV finishes its exit
|
|
970
|
+
* animation without a stale layer pulling the value off-target.
|
|
971
|
+
*
|
|
972
|
+
* The hook is invoked unconditionally (one call per layer) so hook order
|
|
973
|
+
* stays stable even when `gesture` adds or removes sub-states across renders.
|
|
974
|
+
*/
|
|
975
|
+
function useGestureLayerProgress<S>(
|
|
976
|
+
progress: SharedValue<number>,
|
|
977
|
+
active: boolean,
|
|
978
|
+
declared: boolean,
|
|
979
|
+
layer: GestureLayerName,
|
|
980
|
+
transition: Transition<S> | undefined,
|
|
981
|
+
isExiting: boolean,
|
|
982
|
+
shouldReduceMotion: boolean,
|
|
983
|
+
): void {
|
|
984
|
+
const layerCfgSig = stableSig(gestureLayerTransitionFor(layer, transition))
|
|
985
|
+
useEffect(() => {
|
|
986
|
+
if (!declared) return
|
|
987
|
+
if (isExiting) {
|
|
988
|
+
progress.value = 0
|
|
989
|
+
return
|
|
990
|
+
}
|
|
991
|
+
const target = active ? 1 : 0
|
|
992
|
+
const cfg = shouldReduceMotion
|
|
993
|
+
? ({ type: 'no-animation' } as const)
|
|
994
|
+
: (gestureLayerTransitionFor(layer, transition) ??
|
|
995
|
+
({ type: 'spring' } as const))
|
|
996
|
+
progress.value = resolveTransition(cfg, target) as never
|
|
997
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
998
|
+
}, [active, declared, isExiting, shouldReduceMotion, layerCfgSig])
|
|
845
999
|
}
|
|
846
1000
|
|
|
847
1001
|
type GestureHandlers = Record<string, (event: unknown) => void>
|
|
@@ -863,6 +1017,13 @@ function useGestureHandlers(
|
|
|
863
1017
|
setFocusVisible: (next: boolean) => void,
|
|
864
1018
|
setHovered: (next: boolean) => void,
|
|
865
1019
|
): GestureHandlers {
|
|
1020
|
+
// Deps key on declared-ness, not object identity — a fresh `gesture={...}`
|
|
1021
|
+
// literal each render must not rebuild handlers if the same sub-states are
|
|
1022
|
+
// declared.
|
|
1023
|
+
const hasPressed = gesture?.pressed ? 1 : 0
|
|
1024
|
+
const hasFocused = gesture?.focused ? 1 : 0
|
|
1025
|
+
const hasFocusVisible = gesture?.focusVisible ? 1 : 0
|
|
1026
|
+
const hasHovered = gesture?.hovered ? 1 : 0
|
|
866
1027
|
return useMemo(() => {
|
|
867
1028
|
if (!gesture) return {}
|
|
868
1029
|
const handlers: GestureHandlers = {}
|
|
@@ -903,10 +1064,10 @@ function useGestureHandlers(
|
|
|
903
1064
|
return handlers
|
|
904
1065
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
905
1066
|
}, [
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1067
|
+
hasPressed,
|
|
1068
|
+
hasFocused,
|
|
1069
|
+
hasFocusVisible,
|
|
1070
|
+
hasHovered,
|
|
910
1071
|
rest.onTouchStart,
|
|
911
1072
|
rest.onTouchEnd,
|
|
912
1073
|
rest.onTouchCancel,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
declare const __DEV__: boolean
|
|
2
|
+
declare const process: { env?: Record<string, string | undefined> }
|
|
3
|
+
declare const require: (path: string) => unknown
|
|
4
|
+
|
|
5
|
+
let alreadyChecked = false
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Surface a clear, actionable error at first `createMotionComponent` call when
|
|
9
|
+
* the consumer's Reanimated install is broken. Production builds, repeat calls,
|
|
10
|
+
* and Jest test runs are all skipped — the check is purely a dev-time
|
|
11
|
+
* paper-cut sander for the two failure modes we can detect from JS:
|
|
12
|
+
*
|
|
13
|
+
* 1. `react-native-reanimated` resolves but is on a v3.x line we don't
|
|
14
|
+
* support (the plugin name and worklet runtime both changed at v4).
|
|
15
|
+
* 2. The worklets babel plugin (`react-native-worklets/plugin` in v4) isn't
|
|
16
|
+
* wired into `babel.config.js`, so `'worklet'` directives are dead strings
|
|
17
|
+
* and the first `withSpring` / `withTiming` call would crash on the UI
|
|
18
|
+
* thread with a generic "non-worklet function called" error.
|
|
19
|
+
*
|
|
20
|
+
* The "Reanimated isn't installed at all" case isn't handled here — Metro
|
|
21
|
+
* fails to resolve the static `import 'react-native-reanimated'` at the top
|
|
22
|
+
* of `createMotionComponent.tsx` long before this check runs.
|
|
23
|
+
*/
|
|
24
|
+
export function ensureReanimatedInstalled(): void {
|
|
25
|
+
if (!__DEV__ || alreadyChecked) return
|
|
26
|
+
// The standard `react-native-reanimated/mock` doesn't run the worklets
|
|
27
|
+
// babel plugin, so the marker probe would false-positive every test run.
|
|
28
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'test') {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
alreadyChecked = true
|
|
32
|
+
|
|
33
|
+
let version: string | undefined
|
|
34
|
+
try {
|
|
35
|
+
const pkg = require('react-native-reanimated/package.json') as {
|
|
36
|
+
version?: string
|
|
37
|
+
}
|
|
38
|
+
version = pkg.version
|
|
39
|
+
} catch {
|
|
40
|
+
// package.json subpath blocked by `exports` field — skip the version
|
|
41
|
+
// probe rather than emit a misleading error.
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (version) {
|
|
45
|
+
const major = parseInt(version.split('.')[0] ?? '0', 10)
|
|
46
|
+
if (major < 4) {
|
|
47
|
+
console.error(
|
|
48
|
+
`[inertia] react-native-reanimated v${version} is installed, but @onlynative/inertia requires v4.0.0 or later. ` +
|
|
49
|
+
`Upgrade with \`pnpm add react-native-reanimated@^4\` (or your package manager's equivalent).`,
|
|
50
|
+
)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The worklets plugin rewrites any function carrying a top-of-body
|
|
56
|
+
// `'worklet'` directive to expose a `__workletHash` property at runtime.
|
|
57
|
+
// Its absence means the plugin didn't run.
|
|
58
|
+
const probe = function probe() {
|
|
59
|
+
'worklet'
|
|
60
|
+
return 0
|
|
61
|
+
} as { __workletHash?: number }
|
|
62
|
+
if (typeof probe.__workletHash !== 'number') {
|
|
63
|
+
console.error(
|
|
64
|
+
`[inertia] The Reanimated worklets babel plugin is not configured. ` +
|
|
65
|
+
`Add \`'react-native-worklets/plugin'\` as the LAST entry in the \`plugins\` array of your \`babel.config.js\`, ` +
|
|
66
|
+
`then restart Metro with a fresh cache: \`npx expo start -c\` or \`npx react-native start --reset-cache\`.`,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -75,7 +75,25 @@ export type PerPropertyTransition<S> = {
|
|
|
75
75
|
[K in keyof S]?: TransitionConfig
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Per-gesture-layer transition map. Each `gesture` sub-state animates a
|
|
80
|
+
* progress value 0↔1 with its own transition; the worklet composites the
|
|
81
|
+
* layers in priority order (`hovered → focused → focusVisible → pressed`).
|
|
82
|
+
*
|
|
83
|
+
* Keys live on the same `transition` object as `PerPropertyTransition` because
|
|
84
|
+
* the only other place they could go (nested inside `gesture` itself) would
|
|
85
|
+
* collide with the primitive's inferred style keys.
|
|
86
|
+
*/
|
|
87
|
+
export interface GestureLayerTransitions {
|
|
88
|
+
pressed?: TransitionConfig
|
|
89
|
+
focused?: TransitionConfig
|
|
90
|
+
focusVisible?: TransitionConfig
|
|
91
|
+
hovered?: TransitionConfig
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type Transition<S> =
|
|
95
|
+
| TransitionConfig
|
|
96
|
+
| (PerPropertyTransition<S> & GestureLayerTransitions)
|
|
79
97
|
|
|
80
98
|
/**
|
|
81
99
|
* The animation state shape inferred from the underlying component's style
|
|
@@ -124,18 +142,26 @@ export type VariantsMap<C> = Record<string, AnimateStyle<C>>
|
|
|
124
142
|
* - `hovered` — web-only. Typed for cross-platform call sites; the runtime is
|
|
125
143
|
* a no-op on native.
|
|
126
144
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
145
|
+
* Sub-states layer additively. Each declared sub-state owns an independent
|
|
146
|
+
* progress value (0↔1) that animates in/out with its own transition; the
|
|
147
|
+
* worklet composites layers in priority order (lowest-to-highest):
|
|
148
|
+
* `hovered → focused → focusVisible → pressed`. Per-property the chain is
|
|
149
|
+
*
|
|
150
|
+
* v = base
|
|
151
|
+
* v = lerp(v, hovered.value, progressHovered) // if declared
|
|
152
|
+
* v = lerp(v, focused.value, progressFocused) // if declared
|
|
153
|
+
* v = lerp(v, focusVisible.value, progressFocusVisible) // if declared
|
|
154
|
+
* v = lerp(v, pressed.value, progressPressed) // if declared
|
|
155
|
+
*
|
|
156
|
+
* (Color-valued keys use `interpolateColor` instead of `lerp`.) When a single
|
|
157
|
+
* sub-state is active, this collapses to "the highest-priority declared layer
|
|
158
|
+
* wins". When multiple are mid-transition (e.g. release-while-still-hovered)
|
|
159
|
+
* each layer fades independently — a press layer fading out at 50ms while a
|
|
160
|
+
* hover layer holds at full opacity matches MD3 state-layer semantics.
|
|
132
161
|
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* transition
|
|
136
|
-
* sub-states (e.g. release-while-still-hovered) follow the standard `transition`
|
|
137
|
-
* for that property — the resolver does not run multiple parallel
|
|
138
|
-
* interpolations the way a hand-rolled chained-`interpolateColor` would.
|
|
162
|
+
* Configure per-layer fade timing via `transition.<stateName>` on the parent
|
|
163
|
+
* primitive (see `GestureLayerTransitions`); without it, layers default to
|
|
164
|
+
* the parent transition or the library default spring.
|
|
139
165
|
*/
|
|
140
166
|
export interface GestureSubStates<C> {
|
|
141
167
|
pressed?: AnimateStyle<C>
|
|
@@ -193,10 +219,11 @@ export interface MotionProps<C> {
|
|
|
193
219
|
*/
|
|
194
220
|
controller?: VariantController
|
|
195
221
|
/**
|
|
196
|
-
* Gesture-driven sub-states (`pressed`, `focused`, `
|
|
197
|
-
* no handlers are mounted on the underlying
|
|
198
|
-
*
|
|
199
|
-
*
|
|
222
|
+
* Gesture-driven sub-states (`pressed`, `focused`, `focusVisible`,
|
|
223
|
+
* `hovered`). When omitted, no handlers are mounted on the underlying
|
|
224
|
+
* component. Each declared sub-state animates as an independent layer
|
|
225
|
+
* fading in/out over the base `animate` target — see `GestureSubStates`
|
|
226
|
+
* for the composition model and per-layer transition wiring.
|
|
200
227
|
*/
|
|
201
228
|
gesture?: GestureSubStates<C>
|
|
202
229
|
/**
|
|
@@ -216,6 +243,7 @@ export interface MotionProps<C> {
|
|
|
216
243
|
* underlying component's props (minus `style`, which we replace with an
|
|
217
244
|
* animated style) with the Motion-specific props above.
|
|
218
245
|
*/
|
|
246
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
219
247
|
export type MotionComponent<C extends ComponentType<any>> = ComponentType<
|
|
220
248
|
Omit<React.ComponentProps<C>, 'style'> &
|
|
221
249
|
MotionProps<React.ComponentProps<C>> & {
|