@rootnative/inertia 0.0.8 → 0.0.10
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/CHANGELOG.md +81 -1
- package/README.md +7 -7
- package/dist/applyBounds-BYGiHrYO.d.mts +18 -0
- package/dist/applyBounds-BYGiHrYO.d.ts +18 -0
- package/dist/{chunk-E3ALFSH2.js → chunk-2UGWAB6N.js} +78 -70
- package/dist/{chunk-ZBHQPVWE.js → chunk-3ZNHPT5Z.js} +2 -2
- package/dist/{chunk-IHVMNOS4.js → chunk-46766TBM.js} +2 -2
- package/dist/{chunk-MUT6BTZS.js → chunk-4EZ5DBL3.js} +10 -7
- package/dist/{chunk-MFAFB4K7.mjs → chunk-6FCATUMY.mjs} +8 -10
- package/dist/{chunk-6YIUETBF.mjs → chunk-7BH3CAO5.mjs} +1 -1
- package/dist/{chunk-JBK5ZLMH.mjs → chunk-CUHOCONB.mjs} +1 -1
- package/dist/{chunk-M4766VUV.mjs → chunk-DRA2K53O.mjs} +1 -1
- package/dist/{chunk-L4JFECXU.mjs → chunk-F4HMCXAX.mjs} +6 -3
- package/dist/{chunk-W7NTRSPD.mjs → chunk-HEAWMZEM.mjs} +105 -66
- package/dist/{chunk-IX6SEOSK.mjs → chunk-JENYDTXH.mjs} +52 -44
- package/dist/chunk-KFHZMFAX.js +23 -0
- package/dist/{chunk-6AN5X3YE.js → chunk-KXU6VH2Y.js} +10 -11
- package/dist/{chunk-WNVHPMBI.js → chunk-N33GSDNQ.js} +106 -65
- package/dist/chunk-OH46TGEA.mjs +20 -0
- package/dist/{chunk-QNLSCOR5.mjs → chunk-OLOSU2BD.mjs} +1 -1
- package/dist/chunk-PLKXCJIB.js +8 -0
- package/dist/{chunk-GSUC4HWP.mjs → chunk-QXKPIENP.mjs} +1 -1
- package/dist/chunk-UBHQJFUW.js +8 -0
- package/dist/{chunk-6NOCY764.js → chunk-VKXSL3ZL.js} +2 -2
- package/dist/chunk-XKNEQDAT.js +8 -0
- package/dist/{chunk-EN4PN7X3.mjs → chunk-Z23AMUSW.mjs} +1 -1
- package/dist/gestureLayer/index.d.mts +3 -3
- package/dist/gestureLayer/index.d.ts +3 -3
- package/dist/gestureLayer/index.js +12 -18
- package/dist/gestureLayer/index.mjs +5 -11
- package/dist/index.d.mts +105 -47
- package/dist/index.d.ts +105 -47
- package/dist/index.js +119 -92
- package/dist/index.mjs +71 -60
- package/dist/motion/FlatList.d.mts +1 -1
- package/dist/motion/FlatList.d.ts +1 -1
- package/dist/motion/FlatList.js +5 -5
- package/dist/motion/FlatList.mjs +4 -4
- package/dist/motion/Image.d.mts +1 -1
- package/dist/motion/Image.d.ts +1 -1
- package/dist/motion/Image.js +5 -5
- package/dist/motion/Image.mjs +4 -4
- package/dist/motion/Pressable.d.mts +1 -1
- package/dist/motion/Pressable.d.ts +1 -1
- package/dist/motion/Pressable.js +5 -5
- package/dist/motion/Pressable.mjs +4 -4
- package/dist/motion/ScrollView.d.mts +1 -1
- package/dist/motion/ScrollView.d.ts +1 -1
- package/dist/motion/ScrollView.js +5 -5
- package/dist/motion/ScrollView.mjs +4 -4
- package/dist/motion/Text.d.mts +1 -1
- package/dist/motion/Text.d.ts +1 -1
- package/dist/motion/Text.js +5 -5
- package/dist/motion/Text.mjs +4 -4
- package/dist/motion/View.d.mts +1 -1
- package/dist/motion/View.d.ts +1 -1
- package/dist/motion/View.js +5 -5
- package/dist/motion/View.mjs +4 -4
- package/dist/touch/index.d.mts +2 -1
- package/dist/touch/index.d.ts +2 -1
- package/dist/touch/index.js +16 -31
- package/dist/touch/index.mjs +11 -29
- package/dist/{types-DyJpG64F.d.mts → types-08mG8fNk.d.mts} +10 -3
- package/dist/{types-DyJpG64F.d.ts → types-08mG8fNk.d.ts} +10 -3
- package/dist/{useGesture-DbH46EBp.d.ts → useGesture-D1lHeQQ6.d.ts} +1 -1
- package/dist/{useGesture-CnZQnYHH.d.mts → useGesture-DjszaHWz.d.mts} +1 -1
- package/llms.txt +1 -1
- package/package.json +4 -2
- package/src/config/namedTransitions.ts +7 -9
- package/src/gestureLayer/index.ts +3 -0
- package/src/gestureLayer/useGestureLayer.ts +6 -9
- package/src/gestures/focusVisibility.ts +19 -17
- package/src/gestures/index.ts +1 -1
- package/src/index.ts +10 -0
- package/src/layout/resolveLayout.ts +4 -1
- package/src/layout/useSharedLayout.ts +2 -1
- package/src/motion/createMotionComponent.tsx +92 -44
- package/src/presence/Presence.tsx +5 -7
- package/src/touch/applyBounds.ts +30 -0
- package/src/touch/index.ts +1 -0
- package/src/touch/useTouchDrag.ts +15 -45
- package/src/transitions/constants.ts +14 -0
- package/src/transitions/easing.ts +23 -11
- package/src/transitions/index.ts +5 -0
- package/src/transitions/keys.ts +0 -2
- package/src/transitions/resolve.ts +86 -11
- package/src/transitions/resolveSequence.ts +3 -1
- package/src/transitions/runtime.ts +4 -7
- package/src/transitions/spring.ts +7 -2
- package/src/types.ts +9 -2
- package/src/values/extrapolation.ts +21 -0
- package/src/values/index.ts +2 -0
- package/src/values/useGesture.ts +9 -2
- package/src/values/useInterpolatedStyle.ts +42 -19
- package/src/values/useShadow.ts +18 -7
- package/src/values/useSpring.ts +34 -25
- package/src/values/useTransform.ts +24 -20
- package/src/values/useTranslateStyle.ts +31 -0
- package/dist/chunk-24NUL23E.js +0 -8
- package/dist/chunk-4XBX7KOD.js +0 -8
- package/dist/chunk-Z5USXWTE.js +0 -8
|
@@ -20,3 +20,6 @@ export type {
|
|
|
20
20
|
UseGestureLayerOptions,
|
|
21
21
|
UseGestureLayerResult,
|
|
22
22
|
} from './useGestureLayer'
|
|
23
|
+
// The per-layer form of `UseGestureLayerOptions.transition`. Re-exported here
|
|
24
|
+
// so a consumer of this subpath can name it without also importing the root.
|
|
25
|
+
export type { GestureLayerTransitions } from '../types'
|
|
@@ -210,7 +210,10 @@ export function useGestureLayer(
|
|
|
210
210
|
// JS-thread precompute: union of keys across all layers, per-key type
|
|
211
211
|
// (number vs color), and a rest-fallback table. The worklet body reads
|
|
212
212
|
// from `meta` instead of probing each layer per frame — the type check
|
|
213
|
-
// only runs when
|
|
213
|
+
// only runs when the layers change structurally. Keyed on a signature, not
|
|
214
|
+
// on object identity: `states` is usually an inline literal, and a fresh
|
|
215
|
+
// `meta` each render would rebuild the animated style's worklet.
|
|
216
|
+
const statesSig = stableSig(states)
|
|
214
217
|
const meta = useMemo(() => {
|
|
215
218
|
const layers = {
|
|
216
219
|
rest: states.rest,
|
|
@@ -251,14 +254,8 @@ export function useGestureLayer(
|
|
|
251
254
|
restRaw !== undefined ? restRaw : isColor ? 'transparent' : 0
|
|
252
255
|
}
|
|
253
256
|
return { layers, keys, types, restValues }
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
states.hovered,
|
|
257
|
-
states.focused,
|
|
258
|
-
states.focusVisible,
|
|
259
|
-
states.pressed,
|
|
260
|
-
states.disabled,
|
|
261
|
-
])
|
|
257
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
258
|
+
}, [statesSig])
|
|
262
259
|
|
|
263
260
|
const style = useAnimatedStyle(() => {
|
|
264
261
|
const { layers, keys, types, restValues } = meta
|
|
@@ -12,15 +12,16 @@ import { Platform } from 'react-native'
|
|
|
12
12
|
* arrives via D-pad, screen reader, or hardware keyboard, all of which are
|
|
13
13
|
* keyboard-equivalent — so `isFocusVisible()` is unconditionally `true`.
|
|
14
14
|
*
|
|
15
|
-
* The web listeners attach
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
15
|
+
* The web listeners attach when the first component that tracks
|
|
16
|
+
* `focusVisible` mounts (`installFocusVisibility`, called from a mount
|
|
17
|
+
* effect), in the capture phase so they run before the focus event reaches
|
|
18
|
+
* the focused element, and they stay installed for the lifetime of the
|
|
19
|
+
* document. Mount-time installation is early enough: the click that focuses
|
|
20
|
+
* a gesture-wired element can only land on an element that is already
|
|
21
|
+
* mounted, so its `mousedown` is observed. The listeners are not attached at
|
|
22
|
+
* import time — `@rootnative/inertia` declares `sideEffects: false`, and an
|
|
23
|
+
* import-time listener would make that declaration false. They are passive
|
|
24
|
+
* and idle-cheap; the cost is one boolean read per `onFocus` dispatch.
|
|
24
25
|
*/
|
|
25
26
|
|
|
26
27
|
type InputModality = 'keyboard' | 'pointer'
|
|
@@ -38,7 +39,14 @@ function setPointer() {
|
|
|
38
39
|
modality = 'pointer'
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Attach the document listeners that track input modality on web. Idempotent
|
|
44
|
+
* and a no-op on native or without a `document`. Call it from a mount effect
|
|
45
|
+
* of any component that reads `isFocusVisible()`, so the pointer event that
|
|
46
|
+
* precedes the first focus is observed (see module doc above).
|
|
47
|
+
* `isFocusVisible` also calls it as a safety net.
|
|
48
|
+
*/
|
|
49
|
+
export function installFocusVisibility(): void {
|
|
42
50
|
if (installed) return
|
|
43
51
|
if (Platform.OS !== 'web') return
|
|
44
52
|
if (typeof document === 'undefined') return
|
|
@@ -49,12 +57,6 @@ function ensureInstalled(): void {
|
|
|
49
57
|
installed = true
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
// Install at import time so the pointer event that precedes the first focus
|
|
53
|
-
// is observed (see module doc above). `ensureInstalled` stays in
|
|
54
|
-
// `isFocusVisible` as a safety net for environments where `document` appears
|
|
55
|
-
// after import.
|
|
56
|
-
ensureInstalled()
|
|
57
|
-
|
|
58
60
|
/**
|
|
59
61
|
* `true` if the next `onFocus` should be treated as "focus-visible" (keyboard
|
|
60
62
|
* focus). On native, always `true`. On web, reflects the most recent user
|
|
@@ -62,7 +64,7 @@ ensureInstalled()
|
|
|
62
64
|
*/
|
|
63
65
|
export function isFocusVisible(): boolean {
|
|
64
66
|
if (Platform.OS !== 'web') return true
|
|
65
|
-
|
|
67
|
+
installFocusVisibility()
|
|
66
68
|
return modality === 'keyboard'
|
|
67
69
|
}
|
|
68
70
|
|
package/src/gestures/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { isFocusVisible } from './focusVisibility'
|
|
1
|
+
export { installFocusVisibility, isFocusVisible } from './focusVisibility'
|
package/src/index.ts
CHANGED
|
@@ -42,12 +42,18 @@ export {
|
|
|
42
42
|
resolveTransition,
|
|
43
43
|
resolveAnimatableValue,
|
|
44
44
|
ensureWorkletEasing,
|
|
45
|
+
isTopLevelTransition,
|
|
46
|
+
stableSig,
|
|
45
47
|
type AnimationCallback,
|
|
48
|
+
type CallbackFactory,
|
|
46
49
|
} from './transitions'
|
|
47
50
|
// Seed value for any color shared value a custom animated component drives
|
|
48
51
|
// through `resolveTransition`. Exported because the obvious choice —
|
|
49
52
|
// `'transparent'` — cannot be animated away from; see the symbol's own docs.
|
|
50
53
|
export { TRANSPARENT } from './internal/color'
|
|
54
|
+
// Worklet-safe clamp shared by `useTouchDrag` and the gesture adapters'
|
|
55
|
+
// `useDrag`; exported so a custom drag can apply the same rubber-band rule.
|
|
56
|
+
export { applyBounds } from './touch/applyBounds'
|
|
51
57
|
export {
|
|
52
58
|
useAnimation,
|
|
53
59
|
useAnimator,
|
|
@@ -61,6 +67,7 @@ export {
|
|
|
61
67
|
useShadow,
|
|
62
68
|
useSpring,
|
|
63
69
|
useTransform,
|
|
70
|
+
useTranslateStyle,
|
|
64
71
|
useVariants,
|
|
65
72
|
} from './values'
|
|
66
73
|
// The value-layer hooks above all return Reanimated `SharedValue`s — export
|
|
@@ -74,6 +81,7 @@ export type {
|
|
|
74
81
|
ColorCascadeLayer,
|
|
75
82
|
ColorStyleKey,
|
|
76
83
|
ExtrapolationMode,
|
|
84
|
+
InterpolatedStyle,
|
|
77
85
|
InterpolatedStyleMap,
|
|
78
86
|
NumericStyleKey,
|
|
79
87
|
ShadowConfig,
|
|
@@ -96,8 +104,10 @@ export type {
|
|
|
96
104
|
EasingFunction,
|
|
97
105
|
EasingFunctionFactory,
|
|
98
106
|
EasingInput,
|
|
107
|
+
GestureLayerTransitions,
|
|
99
108
|
GestureSubStates,
|
|
100
109
|
MotionComponent,
|
|
110
|
+
MotionComponentProps,
|
|
101
111
|
MotionProps,
|
|
102
112
|
NamedTransitions,
|
|
103
113
|
NoAnimationTransition,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LinearTransition } from 'react-native-reanimated'
|
|
2
|
+
import { DEFAULT_LAYOUT_DURATION } from '../transitions/constants'
|
|
2
3
|
import { ensureWorkletEasing } from '../transitions/easing'
|
|
3
4
|
import { DEFAULT_SPRING, springToReanimated } from '../transitions/spring'
|
|
4
5
|
import { type TransitionConfig } from '../types'
|
|
@@ -34,7 +35,9 @@ export function resolveLayoutTransition(
|
|
|
34
35
|
if (cfg.type === 'no-animation') return undefined
|
|
35
36
|
|
|
36
37
|
if (cfg.type === 'timing') {
|
|
37
|
-
let builder = LinearTransition.duration(
|
|
38
|
+
let builder = LinearTransition.duration(
|
|
39
|
+
cfg.duration ?? DEFAULT_LAYOUT_DURATION,
|
|
40
|
+
)
|
|
38
41
|
const easing = ensureWorkletEasing(cfg.easing)
|
|
39
42
|
if (easing) builder = builder.easing(easing)
|
|
40
43
|
if (cfg.delay) builder = builder.delay(cfg.delay)
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
withSpring,
|
|
16
16
|
withTiming,
|
|
17
17
|
} from 'react-native-reanimated'
|
|
18
|
+
import { DEFAULT_LAYOUT_DURATION } from '../transitions/constants'
|
|
18
19
|
import { DEFAULT_SPRING, springToReanimated } from '../transitions/spring'
|
|
19
20
|
import { type SpringTransition, type TransitionConfig } from '../types'
|
|
20
21
|
import { measureWindowRect } from './measureWindow'
|
|
@@ -334,7 +335,7 @@ function legBuilder(
|
|
|
334
335
|
transition: TransitionConfig | undefined,
|
|
335
336
|
): (from: number, to: number) => number {
|
|
336
337
|
if (transition?.type === 'timing') {
|
|
337
|
-
const duration = transition.duration ??
|
|
338
|
+
const duration = transition.duration ?? DEFAULT_LAYOUT_DURATION
|
|
338
339
|
return (from, to) =>
|
|
339
340
|
withSequence(
|
|
340
341
|
withTiming(from, { duration: 0 }),
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
useNamedTransitions,
|
|
22
22
|
useShouldReduceMotion,
|
|
23
23
|
} from '../config'
|
|
24
|
-
import { isFocusVisible } from '../gestures'
|
|
24
|
+
import { installFocusVisibility, isFocusVisible } from '../gestures'
|
|
25
25
|
import {
|
|
26
26
|
layersToPayload,
|
|
27
27
|
normalizeBoxShadow,
|
|
@@ -47,6 +47,7 @@ import { useStaggerDelay } from '../stagger'
|
|
|
47
47
|
import {
|
|
48
48
|
applyDelay,
|
|
49
49
|
isTopLevelTransition,
|
|
50
|
+
repeatIterationsOf,
|
|
50
51
|
resolveAnimatableValue,
|
|
51
52
|
resolveTransition,
|
|
52
53
|
stableSig,
|
|
@@ -271,6 +272,18 @@ const SHARED_STYLE_KEY_SET = new Set<AnimatableKey>(SHARED_STYLE_KEYS)
|
|
|
271
272
|
*/
|
|
272
273
|
type AnimatableSlotValue = number | string | BoxShadowPayload
|
|
273
274
|
|
|
275
|
+
/**
|
|
276
|
+
* The keys one instance animates plus the three flags the worklet branches
|
|
277
|
+
* on. Grown monotonically on the JS thread and mirrored to the UI thread
|
|
278
|
+
* through a shared value — see `activeSetSV` in the factory.
|
|
279
|
+
*/
|
|
280
|
+
type ActiveKeySet = {
|
|
281
|
+
keys: readonly AnimatableKey[]
|
|
282
|
+
hasTransform: boolean
|
|
283
|
+
hasShadowOffset: boolean
|
|
284
|
+
hasBoxShadow: boolean
|
|
285
|
+
}
|
|
286
|
+
|
|
274
287
|
/**
|
|
275
288
|
* Resting `boxShadow` — no shadow at all. Frozen and hoisted so every
|
|
276
289
|
* primitive shares one reference and an untouched slot never allocates.
|
|
@@ -603,10 +616,10 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
603
616
|
// render silently dropped the new key (its SV updated, but the
|
|
604
617
|
// worklet — which iterates this set — never read it).
|
|
605
618
|
//
|
|
606
|
-
// Growing-only keeps the worklet stable: the
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
619
|
+
// Growing-only keeps the worklet stable: the set object only changes on
|
|
620
|
+
// the renders that actually add a key. The worklet reads it through a
|
|
621
|
+
// shared value (`activeSetSV`, below), so an expansion crosses to the UI
|
|
622
|
+
// thread once per growth and nothing churns frame-to-frame.
|
|
610
623
|
const hasLayoutId = layoutId !== undefined
|
|
611
624
|
|
|
612
625
|
// The static `style`, flattened at most once per render and only when
|
|
@@ -668,14 +681,11 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
668
681
|
}
|
|
669
682
|
}
|
|
670
683
|
|
|
671
|
-
const
|
|
672
|
-
const hasTransformRef = useRef<boolean>(false)
|
|
673
|
-
const hasShadowOffsetRef = useRef<boolean>(false)
|
|
674
|
-
const hasBoxShadowRef = useRef<boolean>(false)
|
|
684
|
+
const activeSetRef = useRef<ActiveKeySet | null>(null)
|
|
675
685
|
// Expand the active set only when this render touched a key we haven't
|
|
676
|
-
// recorded yet. When nothing new appears we keep the existing
|
|
677
|
-
// identity so the
|
|
678
|
-
const prevActive =
|
|
686
|
+
// recorded yet. When nothing new appears we keep the existing object
|
|
687
|
+
// identity so the JS-side readers below see a stable value.
|
|
688
|
+
const prevActive = activeSetRef.current?.keys ?? null
|
|
679
689
|
let grew = prevActive === null
|
|
680
690
|
if (!grew && prevActive) {
|
|
681
691
|
for (const k of touched) {
|
|
@@ -688,14 +698,13 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
688
698
|
if (grew) {
|
|
689
699
|
const merged = new Set<AnimatableKey>(prevActive ?? [])
|
|
690
700
|
for (const k of touched) merged.add(k)
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
hasBoxShadowRef.current = activeKeysRef.current.includes('boxShadow')
|
|
701
|
+
const keys = ALL_KEYS.filter((k) => merged.has(k))
|
|
702
|
+
activeSetRef.current = {
|
|
703
|
+
keys,
|
|
704
|
+
hasTransform: keys.some((k) => TRANSFORM_KEY_SET.has(k)),
|
|
705
|
+
hasShadowOffset: keys.some((k) => SHADOW_OFFSET_KEY_SET.has(k)),
|
|
706
|
+
hasBoxShadow: keys.includes('boxShadow'),
|
|
707
|
+
}
|
|
699
708
|
// Animating `boxShadow` alongside the native `shadow*` keys puts two
|
|
700
709
|
// shadow systems on one element; whichever the underlying view resolves
|
|
701
710
|
// last wins, and the result reads as a bug rather than a choice.
|
|
@@ -703,10 +712,8 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
703
712
|
// decides it, and this block only runs when that set grows.
|
|
704
713
|
if (
|
|
705
714
|
__DEV__ &&
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
(k) => k !== 'boxShadow' && k.startsWith('shadow'),
|
|
709
|
-
)
|
|
715
|
+
activeSetRef.current.hasBoxShadow &&
|
|
716
|
+
keys.some((k) => k !== 'boxShadow' && k.startsWith('shadow'))
|
|
710
717
|
) {
|
|
711
718
|
warnOnce(
|
|
712
719
|
'boxShadow-with-native-shadow',
|
|
@@ -717,6 +724,25 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
717
724
|
)
|
|
718
725
|
}
|
|
719
726
|
}
|
|
727
|
+
const activeKeys = activeSetRef.current!.keys
|
|
728
|
+
|
|
729
|
+
// The worklet's copy of the active set. It must be a shared value, not
|
|
730
|
+
// the ref above: react-native-worklets clones a captured plain object
|
|
731
|
+
// once, when the worklet is first serialised, and in dev it freezes the
|
|
732
|
+
// original so later writes to `.current` are dropped with a warning. A
|
|
733
|
+
// ref read inside the worklet therefore only ever sees the mount-time
|
|
734
|
+
// set, and a key added after mount drives its shared value but never
|
|
735
|
+
// reaches the screen (Worklet rule 2). The Jest mock runs the worklet on
|
|
736
|
+
// the JS thread, so only a device shows the difference. The write lives
|
|
737
|
+
// in an effect because Reanimated's strict mode warns on a shared-value
|
|
738
|
+
// write during render; it is declared before the value-driving effects
|
|
739
|
+
// below, so the grown set reaches the UI thread in the same commit as
|
|
740
|
+
// the first animation of the new key.
|
|
741
|
+
const activeSetSV = useSharedValue<ActiveKeySet>(activeSetRef.current!)
|
|
742
|
+
const activeSet = activeSetRef.current!
|
|
743
|
+
useEffect(() => {
|
|
744
|
+
if (activeSetSV.value !== activeSet) activeSetSV.value = activeSet
|
|
745
|
+
}, [activeSetSV, activeSet])
|
|
720
746
|
|
|
721
747
|
// Which keys a record actually drives *right now*. Everything else in
|
|
722
748
|
// the active set is there only because a gesture sub-state, an `exit`
|
|
@@ -741,7 +767,6 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
741
767
|
// Resting values pulled off the static `style`, for active keys nothing
|
|
742
768
|
// has driven. Gated so the common `animate`-only instance never flattens
|
|
743
769
|
// a style it has no use for.
|
|
744
|
-
const activeKeys = activeKeysRef.current!
|
|
745
770
|
const everDriven = everDrivenRef.current
|
|
746
771
|
const styleResting: Partial<Record<AnimatableKey, AnimatableSlotValue>> =
|
|
747
772
|
{}
|
|
@@ -857,7 +882,7 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
857
882
|
|
|
858
883
|
const styleRestingSig = stableSig(styleResting)
|
|
859
884
|
useEffect(() => {
|
|
860
|
-
for (const key of
|
|
885
|
+
for (const key of activeKeys) {
|
|
861
886
|
if (everDrivenRef.current.has(key)) continue
|
|
862
887
|
sharedValues[key].value = (styleResting[key] ??
|
|
863
888
|
DEFAULT_RESTING[key]) as never
|
|
@@ -926,6 +951,11 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
926
951
|
(isExiting ? '|exit' : '') +
|
|
927
952
|
(shouldReduceMotion ? '|rm' : '')
|
|
928
953
|
const transitionSig = stableSig(transition)
|
|
954
|
+
// The callback factory skips callback installation when there is no
|
|
955
|
+
// `onAnimationEnd` and nothing to settle, so a handler that appears
|
|
956
|
+
// after mount needs the effect to run again. Keyed on presence, not
|
|
957
|
+
// identity: a changing handler is read through `onAnimationEndRef`.
|
|
958
|
+
const hasAnimationEnd = onAnimationEnd !== undefined
|
|
929
959
|
|
|
930
960
|
// Stable ref to the live `safeToRemove` so the effect's settle-counter
|
|
931
961
|
// closure can reach the latest <Presence> binding without retriggering.
|
|
@@ -943,6 +973,10 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
943
973
|
return
|
|
944
974
|
}
|
|
945
975
|
|
|
976
|
+
// `done` doubles as the run token. When this effect re-runs, the
|
|
977
|
+
// cleanup below sets it, so the callbacks of the superseded animations
|
|
978
|
+
// — which Reanimated fires with `finished: false` as they are replaced
|
|
979
|
+
// — cannot drain this run's counter and release <Presence> early.
|
|
946
980
|
let pending = 0
|
|
947
981
|
let done = false
|
|
948
982
|
const onSettle = () => {
|
|
@@ -1095,8 +1129,11 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
1095
1129
|
if (isExiting && pending === 0) {
|
|
1096
1130
|
safeToRemoveRef.current?.()
|
|
1097
1131
|
}
|
|
1132
|
+
return () => {
|
|
1133
|
+
done = true
|
|
1134
|
+
}
|
|
1098
1135
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1099
|
-
}, [baseSig, transitionSig])
|
|
1136
|
+
}, [baseSig, transitionSig, hasAnimationEnd])
|
|
1100
1137
|
|
|
1101
1138
|
// Per-layer progress: when a sub-state activation flips, animate its
|
|
1102
1139
|
// progress SV 0↔1 with the layer's own transition (or the parent
|
|
@@ -1162,7 +1199,7 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
1162
1199
|
if (!hasLayoutId) return undefined
|
|
1163
1200
|
const out: SharedStyleSnapshot = {}
|
|
1164
1201
|
let any = false
|
|
1165
|
-
for (const key of
|
|
1202
|
+
for (const key of activeKeys) {
|
|
1166
1203
|
if (!SHARED_STYLE_KEY_SET.has(key)) continue
|
|
1167
1204
|
const v = sharedValues[key].value
|
|
1168
1205
|
// Structured slots can't be carried, and a key nothing ever gave a
|
|
@@ -1186,10 +1223,11 @@ export function createMotionComponent<C extends ComponentType<any>>(
|
|
|
1186
1223
|
const carry = sharedLayout.carry
|
|
1187
1224
|
|
|
1188
1225
|
const animatedStyle = useAnimatedStyle(() => {
|
|
1189
|
-
const
|
|
1190
|
-
const
|
|
1191
|
-
const
|
|
1192
|
-
const
|
|
1226
|
+
const activeSet = activeSetSV.value
|
|
1227
|
+
const activeKeys = activeSet.keys
|
|
1228
|
+
const hasTransform = activeSet.hasTransform
|
|
1229
|
+
const hasShadowOffset = activeSet.hasShadowOffset
|
|
1230
|
+
const hasBoxShadow = activeSet.hasBoxShadow
|
|
1193
1231
|
const out: Record<string, unknown> = {}
|
|
1194
1232
|
const transform: Array<Record<string, unknown>> = []
|
|
1195
1233
|
// shadow-offset reassembly buffers. The two synthetic axis SVs feed in
|
|
@@ -1448,7 +1486,7 @@ type SharedValueMap = Record<AnimatableKey, SharedValue<AnimatableSlotValue>>
|
|
|
1448
1486
|
* The ESLint exhaustive-deps rule can't see through the loop, hence the
|
|
1449
1487
|
* disable — the invariant it would check for us is asserted above instead.
|
|
1450
1488
|
*
|
|
1451
|
-
* Unused shared values are cheap; the worklet skips them via `
|
|
1489
|
+
* Unused shared values are cheap; the worklet skips them via `activeSetSV`.
|
|
1452
1490
|
* Color keys are seeded with the initial color string so Reanimated's value
|
|
1453
1491
|
* setter recognizes the slot as a color from the first `withSpring` /
|
|
1454
1492
|
* `withTiming` call.
|
|
@@ -1805,12 +1843,9 @@ function stepCountOf(v: AnimatableValue<number | string> | undefined): number {
|
|
|
1805
1843
|
*/
|
|
1806
1844
|
function totalIterationsOf(cfg: TransitionConfig | undefined): number {
|
|
1807
1845
|
if (!cfg || cfg.type === 'no-animation' || cfg.type === 'decay') return 1
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
if (typeof r === 'number') return r
|
|
1812
|
-
if (r.count === 'infinite') return Number.POSITIVE_INFINITY
|
|
1813
|
-
return r.count
|
|
1846
|
+
// Shares the resolver's count rule, so a `repeat: 0` that the resolver
|
|
1847
|
+
// treats as "run once" is also counted as one iteration here.
|
|
1848
|
+
return repeatIterationsOf(cfg.repeat)
|
|
1814
1849
|
}
|
|
1815
1850
|
|
|
1816
1851
|
/**
|
|
@@ -1880,16 +1915,23 @@ function resolveAnimateInput(
|
|
|
1880
1915
|
variants: VariantsMap<unknown> | undefined,
|
|
1881
1916
|
controllerKey: string | undefined,
|
|
1882
1917
|
): AnimateStyle<unknown> | undefined {
|
|
1918
|
+
if (controllerKey !== undefined && animate !== undefined) {
|
|
1919
|
+
warnOnce(
|
|
1920
|
+
'controller-and-animate',
|
|
1921
|
+
'[inertia] Both `controller` and `animate` are set on the same ' +
|
|
1922
|
+
'primitive. The controller drives the animation and `animate` is ' +
|
|
1923
|
+
'ignored — remove one of the two.',
|
|
1924
|
+
)
|
|
1925
|
+
}
|
|
1883
1926
|
if (controllerKey !== undefined && variants && controllerKey in variants) {
|
|
1884
1927
|
return variants[controllerKey]
|
|
1885
1928
|
}
|
|
1886
1929
|
if (typeof animate === 'string') {
|
|
1887
1930
|
if (variants && animate in variants) return variants[animate]
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
}
|
|
1931
|
+
warnOnce(
|
|
1932
|
+
`missing-variant:${animate}`,
|
|
1933
|
+
`[inertia] animate="${animate}" but no matching variant. Did you forget to pass \`variants\`?`,
|
|
1934
|
+
)
|
|
1893
1935
|
return undefined
|
|
1894
1936
|
}
|
|
1895
1937
|
return animate as AnimateStyle<unknown> | undefined
|
|
@@ -2047,6 +2089,12 @@ function useGestureHandlers(
|
|
|
2047
2089
|
const hasFocused = gesture?.focused ? 1 : 0
|
|
2048
2090
|
const hasFocusVisible = gesture?.focusVisible ? 1 : 0
|
|
2049
2091
|
const hasHovered = gesture?.hovered ? 1 : 0
|
|
2092
|
+
// The web modality listeners attach on the first mount that tracks
|
|
2093
|
+
// `focusVisible` — see `focusVisibility.ts` for why mount time is early
|
|
2094
|
+
// enough and why import time is not an option.
|
|
2095
|
+
useEffect(() => {
|
|
2096
|
+
if (hasFocusVisible) installFocusVisibility()
|
|
2097
|
+
}, [hasFocusVisible])
|
|
2050
2098
|
return useMemo(() => {
|
|
2051
2099
|
if (!gesture) return {}
|
|
2052
2100
|
const handlers: GestureHandlers = {}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
useRef,
|
|
10
10
|
useState,
|
|
11
11
|
} from 'react'
|
|
12
|
+
import { warnOnce } from '../internal/warnOnce'
|
|
12
13
|
import { PresenceContext, type PresenceContextValue } from './PresenceContext'
|
|
13
14
|
|
|
14
15
|
interface RenderEntry {
|
|
@@ -38,11 +39,10 @@ export function Presence({ children }: { children: ReactNode }) {
|
|
|
38
39
|
Children.forEach(children, (child) => {
|
|
39
40
|
if (!isValidElement(child)) return
|
|
40
41
|
if (child.key === null) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
42
|
+
warnOnce(
|
|
43
|
+
'presence-keyless-child',
|
|
44
|
+
'[inertia] <Presence> children must have a `key`. Skipping a keyless child.',
|
|
45
|
+
)
|
|
46
46
|
return
|
|
47
47
|
}
|
|
48
48
|
out.push(child)
|
|
@@ -224,5 +224,3 @@ function PresenceItem({
|
|
|
224
224
|
</PresenceContext.Provider>
|
|
225
225
|
)
|
|
226
226
|
}
|
|
227
|
-
|
|
228
|
-
declare const __DEV__: boolean
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clamp `value` to `[min, max]`. When `elastic > 0` the overshoot past a
|
|
3
|
+
* bound is scaled by `elastic` instead of hard-clamped, giving a rubber-band
|
|
4
|
+
* feel. `min` / `max` may be `undefined` to leave that side unbounded.
|
|
5
|
+
*
|
|
6
|
+
* Worklet, so it runs both inside a gesture-handler pan handler on the UI
|
|
7
|
+
* thread (`useDrag`) and from a JS-thread `PanResponder` callback
|
|
8
|
+
* (`useTouchDrag`).
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* applyBounds(120, -100, 100, 0) // 100
|
|
12
|
+
* applyBounds(120, -100, 100, 0.5) // 110
|
|
13
|
+
* applyBounds(50, undefined, 100, 0) // 50
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function applyBounds(
|
|
17
|
+
value: number,
|
|
18
|
+
min: number | undefined,
|
|
19
|
+
max: number | undefined,
|
|
20
|
+
elastic: number,
|
|
21
|
+
): number {
|
|
22
|
+
'worklet'
|
|
23
|
+
if (min !== undefined && value < min) {
|
|
24
|
+
return elastic > 0 ? min + (value - min) * elastic : min
|
|
25
|
+
}
|
|
26
|
+
if (max !== undefined && value > max) {
|
|
27
|
+
return elastic > 0 ? max + (value - max) * elastic : max
|
|
28
|
+
}
|
|
29
|
+
return value
|
|
30
|
+
}
|
package/src/touch/index.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { useMemo } from 'react'
|
|
1
|
+
import { useMemo, useRef } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
PanResponder,
|
|
4
4
|
type PanResponderGestureState,
|
|
5
5
|
type PanResponderInstance,
|
|
6
6
|
} from 'react-native'
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
useSharedValue,
|
|
10
|
-
type SharedValue,
|
|
11
|
-
} from 'react-native-reanimated'
|
|
7
|
+
import { useSharedValue, type SharedValue } from 'react-native-reanimated'
|
|
8
|
+
import type { useAnimatedStyle } from 'react-native-reanimated'
|
|
12
9
|
import { buildReleaseAnimation } from '../transitions'
|
|
10
|
+
import { useTranslateStyle } from '../values/useTranslateStyle'
|
|
11
|
+
import { applyBounds } from './applyBounds'
|
|
13
12
|
import type { TransitionConfig } from '../types'
|
|
14
13
|
|
|
15
14
|
/**
|
|
@@ -166,23 +165,17 @@ export function useTouchDrag(
|
|
|
166
165
|
const top = constraints?.top
|
|
167
166
|
const bottom = constraints?.bottom
|
|
168
167
|
const elasticCoef = elastic
|
|
169
|
-
|
|
168
|
+
|
|
169
|
+
// Callbacks are read through a ref at call time, so an inline arrow in
|
|
170
|
+
// `options` does not rebuild the PanResponder every render. PanResponder
|
|
171
|
+
// callbacks run on the JS thread, so a ref read is safe here.
|
|
172
|
+
const callbacksRef = useRef(options)
|
|
173
|
+
callbacksRef.current = options
|
|
170
174
|
|
|
171
175
|
const responder = useMemo(
|
|
172
176
|
() => buildResponder(),
|
|
173
177
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
174
|
-
[
|
|
175
|
-
lockX,
|
|
176
|
-
lockY,
|
|
177
|
-
left,
|
|
178
|
-
right,
|
|
179
|
-
top,
|
|
180
|
-
bottom,
|
|
181
|
-
elasticCoef,
|
|
182
|
-
onDragStart,
|
|
183
|
-
onDragEnd,
|
|
184
|
-
onRelease,
|
|
185
|
-
],
|
|
178
|
+
[lockX, lockY, left, right, top, bottom, elasticCoef],
|
|
186
179
|
)
|
|
187
180
|
|
|
188
181
|
// Hoisted out of the inline `useMemo` factory to keep the dep list readable
|
|
@@ -196,6 +189,7 @@ export function useTouchDrag(
|
|
|
196
189
|
// `@rootnative/inertia-gestures` API (px/sec from gesture-handler).
|
|
197
190
|
const vx = g.vx * 1000
|
|
198
191
|
const vy = g.vy * 1000
|
|
192
|
+
const { onRelease, onDragEnd } = callbacksRef.current
|
|
199
193
|
if (onRelease) {
|
|
200
194
|
const result = onRelease({ x, y, velocity: { x: vx, y: vy } })
|
|
201
195
|
if (result) {
|
|
@@ -228,7 +222,7 @@ export function useTouchDrag(
|
|
|
228
222
|
startX.value = dragX.value
|
|
229
223
|
startY.value = dragY.value
|
|
230
224
|
isDragging.value = true
|
|
231
|
-
|
|
225
|
+
callbacksRef.current.onDragStart?.()
|
|
232
226
|
},
|
|
233
227
|
onPanResponderMove: (_e, g) => {
|
|
234
228
|
if (lockX) {
|
|
@@ -253,9 +247,7 @@ export function useTouchDrag(
|
|
|
253
247
|
})
|
|
254
248
|
}
|
|
255
249
|
|
|
256
|
-
const animatedStyle =
|
|
257
|
-
transform: [{ translateX: dragX.value }, { translateY: dragY.value }],
|
|
258
|
-
}))
|
|
250
|
+
const animatedStyle = useTranslateStyle(dragX, dragY)
|
|
259
251
|
|
|
260
252
|
return {
|
|
261
253
|
panHandlers: responder.panHandlers,
|
|
@@ -265,25 +257,3 @@ export function useTouchDrag(
|
|
|
265
257
|
isDragging,
|
|
266
258
|
}
|
|
267
259
|
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Clamp `value` to `[min, max]`. When `elastic > 0` the overshoot past a
|
|
271
|
-
* bound is scaled by `elastic`, giving a rubber-band feel. `min` / `max`
|
|
272
|
-
* may be `undefined` to leave that side unbounded.
|
|
273
|
-
*
|
|
274
|
-
* JS-thread (PanResponder callbacks are JS, not worklets).
|
|
275
|
-
*/
|
|
276
|
-
function applyBounds(
|
|
277
|
-
value: number,
|
|
278
|
-
min: number | undefined,
|
|
279
|
-
max: number | undefined,
|
|
280
|
-
elastic: number,
|
|
281
|
-
): number {
|
|
282
|
-
if (min !== undefined && value < min) {
|
|
283
|
-
return elastic > 0 ? min + (value - min) * elastic : min
|
|
284
|
-
}
|
|
285
|
-
if (max !== undefined && value > max) {
|
|
286
|
-
return elastic > 0 ? max + (value - max) * elastic : max
|
|
287
|
-
}
|
|
288
|
-
return value
|
|
289
|
-
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default `duration` for a `timing` transition that does not set one, in ms.
|
|
3
|
+
* Shared by the JS-thread resolver (`resolve.ts`) and the UI-thread builder
|
|
4
|
+
* (`runtime.ts`) so the two paths cannot drift apart.
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_TIMING_DURATION = 250
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default `duration` for a `timing` layout transition (`layout` prop and
|
|
10
|
+
* `layoutId` shared-element legs), in ms. Layout moves cover more distance
|
|
11
|
+
* than a property tween, so the default is longer than
|
|
12
|
+
* `DEFAULT_TIMING_DURATION`.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_LAYOUT_DURATION = 300
|