@onlynative/inertia 0.0.1-alpha.0
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 +33 -0
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/index.d.mts +185 -0
- package/dist/index.d.ts +185 -0
- package/dist/index.js +817 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +796 -0
- package/dist/index.mjs.map +1 -0
- package/dist/motion/Image.d.mts +12 -0
- package/dist/motion/Image.d.ts +12 -0
- package/dist/motion/Image.js +656 -0
- package/dist/motion/Image.js.map +1 -0
- package/dist/motion/Image.mjs +650 -0
- package/dist/motion/Image.mjs.map +1 -0
- package/dist/motion/Pressable.d.mts +15 -0
- package/dist/motion/Pressable.d.ts +15 -0
- package/dist/motion/Pressable.js +656 -0
- package/dist/motion/Pressable.js.map +1 -0
- package/dist/motion/Pressable.mjs +650 -0
- package/dist/motion/Pressable.mjs.map +1 -0
- package/dist/motion/ScrollView.d.mts +12 -0
- package/dist/motion/ScrollView.d.ts +12 -0
- package/dist/motion/ScrollView.js +656 -0
- package/dist/motion/ScrollView.js.map +1 -0
- package/dist/motion/ScrollView.mjs +650 -0
- package/dist/motion/ScrollView.mjs.map +1 -0
- package/dist/motion/Text.d.mts +11 -0
- package/dist/motion/Text.d.ts +11 -0
- package/dist/motion/Text.js +656 -0
- package/dist/motion/Text.js.map +1 -0
- package/dist/motion/Text.mjs +650 -0
- package/dist/motion/Text.mjs.map +1 -0
- package/dist/motion/View.d.mts +11 -0
- package/dist/motion/View.d.ts +11 -0
- package/dist/motion/View.js +656 -0
- package/dist/motion/View.js.map +1 -0
- package/dist/motion/View.mjs +650 -0
- package/dist/motion/View.mjs.map +1 -0
- package/dist/types-CmbXx-G3.d.mts +185 -0
- package/dist/types-CmbXx-G3.d.ts +185 -0
- package/llms.txt +78 -0
- package/package.json +120 -0
- package/src/config/MotionConfig.tsx +30 -0
- package/src/config/MotionConfigContext.ts +53 -0
- package/src/config/index.ts +9 -0
- package/src/index.ts +49 -0
- package/src/motion/Image.tsx +9 -0
- package/src/motion/Pressable.tsx +12 -0
- package/src/motion/ScrollView.tsx +9 -0
- package/src/motion/Text.tsx +8 -0
- package/src/motion/View.tsx +8 -0
- package/src/motion/createMotionComponent.tsx +850 -0
- package/src/motion/index.ts +26 -0
- package/src/presence/Presence.tsx +165 -0
- package/src/presence/PresenceContext.ts +28 -0
- package/src/presence/index.ts +6 -0
- package/src/transitions/easing.ts +29 -0
- package/src/transitions/index.ts +2 -0
- package/src/transitions/resolve.ts +265 -0
- package/src/types.ts +207 -0
- package/src/values/index.ts +1 -0
- package/src/values/useVariants.ts +60 -0
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComponentType,
|
|
3
|
+
forwardRef,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react'
|
|
9
|
+
import Animated, {
|
|
10
|
+
runOnJS,
|
|
11
|
+
useAnimatedStyle,
|
|
12
|
+
useSharedValue,
|
|
13
|
+
type SharedValue,
|
|
14
|
+
} from 'react-native-reanimated'
|
|
15
|
+
import { useShouldReduceMotion } from '../config'
|
|
16
|
+
import { usePresence } from '../presence'
|
|
17
|
+
import { resolveAnimatableValue } from '../transitions'
|
|
18
|
+
import {
|
|
19
|
+
type AnimatableValue,
|
|
20
|
+
type AnimateStyle,
|
|
21
|
+
type AnimationCallbackInfo,
|
|
22
|
+
type GestureSubStates,
|
|
23
|
+
type MotionComponent,
|
|
24
|
+
type MotionProps,
|
|
25
|
+
type PerPropertyTransition,
|
|
26
|
+
type Transition,
|
|
27
|
+
type TransitionConfig,
|
|
28
|
+
type VariantController,
|
|
29
|
+
type VariantsMap,
|
|
30
|
+
} from '../types'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Animatable properties supported in the alpha. Expanding this set is a
|
|
34
|
+
* mechanical change — add the key here, decide whether it lives inside
|
|
35
|
+
* `transform`, and wire it through `buildAnimatedStyle` below.
|
|
36
|
+
*/
|
|
37
|
+
const TRANSFORM_KEYS = [
|
|
38
|
+
'translateX',
|
|
39
|
+
'translateY',
|
|
40
|
+
'scale',
|
|
41
|
+
'scaleX',
|
|
42
|
+
'scaleY',
|
|
43
|
+
'rotate',
|
|
44
|
+
] as const
|
|
45
|
+
|
|
46
|
+
const TOP_LEVEL_KEYS = ['opacity', 'width', 'height', 'borderRadius'] as const
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Per-effect transform-group coordinator. Counts how many transform-axis
|
|
50
|
+
* terminal callbacks are still pending; when the last one fires, the
|
|
51
|
+
* factory emits a single coalesced `onAnimationEnd({ key: 'transform' })`
|
|
52
|
+
* instead of N per-axis callbacks. Mutated by the dispatch closure.
|
|
53
|
+
*/
|
|
54
|
+
type TransformGroup = { remaining: number }
|
|
55
|
+
|
|
56
|
+
const ALL_KEYS = [...TRANSFORM_KEYS, ...TOP_LEVEL_KEYS] as const
|
|
57
|
+
type AnimatableKey = (typeof ALL_KEYS)[number]
|
|
58
|
+
type TransformKey = (typeof TRANSFORM_KEYS)[number]
|
|
59
|
+
|
|
60
|
+
const TRANSFORM_KEY_SET = new Set<AnimatableKey>(TRANSFORM_KEYS)
|
|
61
|
+
|
|
62
|
+
// Stable style object applied while a Motion primitive is mid-exit so taps
|
|
63
|
+
// fall through. Hoisted so every render shares the same reference and
|
|
64
|
+
// Reanimated's style merging treats it as a no-op when present.
|
|
65
|
+
const EXITING_POINTER_EVENTS_STYLE = { pointerEvents: 'none' } as const
|
|
66
|
+
|
|
67
|
+
const DEFAULT_RESTING: Record<AnimatableKey, number> = {
|
|
68
|
+
translateX: 0,
|
|
69
|
+
translateY: 0,
|
|
70
|
+
scale: 1,
|
|
71
|
+
scaleX: 1,
|
|
72
|
+
scaleY: 1,
|
|
73
|
+
rotate: 0,
|
|
74
|
+
opacity: 1,
|
|
75
|
+
width: 0,
|
|
76
|
+
height: 0,
|
|
77
|
+
borderRadius: 0,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const TRANSITION_KEYS = new Set([
|
|
81
|
+
'type',
|
|
82
|
+
'tension',
|
|
83
|
+
'friction',
|
|
84
|
+
'mass',
|
|
85
|
+
'velocity',
|
|
86
|
+
'restSpeedThreshold',
|
|
87
|
+
'restDisplacementThreshold',
|
|
88
|
+
'duration',
|
|
89
|
+
'easing',
|
|
90
|
+
'delay',
|
|
91
|
+
'repeat',
|
|
92
|
+
'deceleration',
|
|
93
|
+
'clamp',
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
function isTopLevelTransition(t: unknown): t is TransitionConfig {
|
|
97
|
+
if (t === null || typeof t !== 'object') return false
|
|
98
|
+
const keys = Object.keys(t as object)
|
|
99
|
+
if (keys.length === 0) return false
|
|
100
|
+
return keys.every((k) => TRANSITION_KEYS.has(k))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function transitionFor<S>(
|
|
104
|
+
prop: keyof S,
|
|
105
|
+
transition: Transition<S> | undefined,
|
|
106
|
+
): TransitionConfig | undefined {
|
|
107
|
+
if (!transition) return undefined
|
|
108
|
+
if (isTopLevelTransition(transition)) return transition
|
|
109
|
+
return (transition as PerPropertyTransition<S>)[prop]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Factory that wraps a React Native primitive as a `Motion.*` component.
|
|
114
|
+
*
|
|
115
|
+
* The generic `C` flows through `MotionProps`, so `animate` / `initial` /
|
|
116
|
+
* `exit` / `transition` all infer from `C`'s `style` prop. There is no
|
|
117
|
+
* shared `ViewStyle & TextStyle & ImageStyle` fallback.
|
|
118
|
+
*
|
|
119
|
+
* Alpha scope: numeric properties listed in `ALL_KEYS`, applied via
|
|
120
|
+
* Reanimated shared values + `useAnimatedStyle`. Sequences, variants,
|
|
121
|
+
* gestures, color animation, and the cross-render memoization optimization
|
|
122
|
+
* land in later phases.
|
|
123
|
+
*/
|
|
124
|
+
export function createMotionComponent<C extends ComponentType<any>>(
|
|
125
|
+
Component: C,
|
|
126
|
+
): MotionComponent<C> {
|
|
127
|
+
const AnimatedComponent = Animated.createAnimatedComponent(
|
|
128
|
+
Component as ComponentType<any>,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
type Props = React.ComponentProps<C> & MotionProps<React.ComponentProps<C>>
|
|
132
|
+
|
|
133
|
+
const Motion = forwardRef<unknown, Props>(function Motion(props, ref) {
|
|
134
|
+
const {
|
|
135
|
+
initial,
|
|
136
|
+
animate,
|
|
137
|
+
exit,
|
|
138
|
+
transition,
|
|
139
|
+
variants,
|
|
140
|
+
controller,
|
|
141
|
+
gesture,
|
|
142
|
+
onAnimationEnd,
|
|
143
|
+
style,
|
|
144
|
+
...rest
|
|
145
|
+
} = props as Props & { style?: unknown }
|
|
146
|
+
|
|
147
|
+
// <Presence> contract: when an ancestor flips `isPresent` to false the
|
|
148
|
+
// child stays rendered until `safeToRemove` is called, giving the exit
|
|
149
|
+
// animation time to play. `null` when there is no <Presence> ancestor.
|
|
150
|
+
const presence = usePresence()
|
|
151
|
+
const isExiting = presence !== null && presence.isPresent === false
|
|
152
|
+
|
|
153
|
+
// Resolved reduced-motion preference for this subtree. When true, every
|
|
154
|
+
// per-key transition is replaced with `no-animation` below, so values
|
|
155
|
+
// snap to target without interpolation. The hook also subscribes to OS
|
|
156
|
+
// changes (via Reanimated's `useReducedMotion`), so toggling the
|
|
157
|
+
// accessibility setting at runtime re-renders this component.
|
|
158
|
+
const shouldReduceMotion = useShouldReduceMotion()
|
|
159
|
+
|
|
160
|
+
// Pin the latest `onAnimationEnd` in a ref so the worklet callback always
|
|
161
|
+
// dispatches against the current closure without re-resolving the
|
|
162
|
+
// animation graph. Worklets can read refs via `runOnJS`.
|
|
163
|
+
const onAnimationEndRef = useRef(onAnimationEnd)
|
|
164
|
+
onAnimationEndRef.current = onAnimationEnd
|
|
165
|
+
|
|
166
|
+
// Resolve `animate` against `variants` / `controller`. The controller's
|
|
167
|
+
// `current` wins when both are set (typed contract: don't mix
|
|
168
|
+
// `controller` and `animate` — controller drives the animation in that
|
|
169
|
+
// mode). When `animate` is a string and `variants` exist, look it up.
|
|
170
|
+
const variantKey = useControllerKey(controller)
|
|
171
|
+
const resolvedAnimate = resolveAnimateInput(
|
|
172
|
+
animate as AnimateStyle<unknown> | string | undefined,
|
|
173
|
+
variants as VariantsMap<unknown> | undefined,
|
|
174
|
+
variantKey,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
const animateRecord = (resolvedAnimate ?? {}) as Partial<
|
|
178
|
+
Record<AnimatableKey, AnimatableValue<number>>
|
|
179
|
+
>
|
|
180
|
+
const initialRecord =
|
|
181
|
+
initial && initial !== false
|
|
182
|
+
? (initial as Partial<Record<AnimatableKey, number>>)
|
|
183
|
+
: undefined
|
|
184
|
+
const exitRecord = exit
|
|
185
|
+
? (exit as Partial<Record<AnimatableKey, AnimatableValue<number>>>)
|
|
186
|
+
: undefined
|
|
187
|
+
|
|
188
|
+
// Gesture sub-state activation tracked as JS state so changes invalidate
|
|
189
|
+
// the merged-target signature and re-run the animation effect. The cost
|
|
190
|
+
// is three useState slots regardless of whether `gesture` is set; that's
|
|
191
|
+
// tiny and lets us stay rules-of-hooks-clean.
|
|
192
|
+
const [pressed, setPressed] = useState(false)
|
|
193
|
+
const [focused, setFocused] = useState(false)
|
|
194
|
+
const [hovered, setHovered] = useState(false)
|
|
195
|
+
|
|
196
|
+
// The set of keys this instance animates is locked at first render. With
|
|
197
|
+
// variants in play the union across all variants is what matters — a key
|
|
198
|
+
// touched by any variant must be active so the worklet picks it up when
|
|
199
|
+
// the controller transitions. Gesture sub-states join the same union so
|
|
200
|
+
// pressed/focused/hovered targets can drive any key they declare.
|
|
201
|
+
const activeKeysRef = useRef<readonly AnimatableKey[] | null>(null)
|
|
202
|
+
if (activeKeysRef.current === null) {
|
|
203
|
+
const touched = new Set<AnimatableKey>()
|
|
204
|
+
for (const k of ALL_KEYS) {
|
|
205
|
+
if (k in animateRecord) touched.add(k)
|
|
206
|
+
if (initialRecord && k in initialRecord) touched.add(k)
|
|
207
|
+
}
|
|
208
|
+
if (variants) {
|
|
209
|
+
for (const variant of Object.values(variants) as object[]) {
|
|
210
|
+
if (!variant) continue
|
|
211
|
+
for (const k of ALL_KEYS) {
|
|
212
|
+
if (k in variant) touched.add(k)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (gesture) {
|
|
217
|
+
for (const subState of [
|
|
218
|
+
gesture.pressed,
|
|
219
|
+
gesture.focused,
|
|
220
|
+
gesture.hovered,
|
|
221
|
+
] as Array<object | undefined>) {
|
|
222
|
+
if (!subState) continue
|
|
223
|
+
for (const k of ALL_KEYS) {
|
|
224
|
+
if (k in subState) touched.add(k)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (exitRecord) {
|
|
229
|
+
for (const k of ALL_KEYS) {
|
|
230
|
+
if (k in exitRecord) touched.add(k)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
activeKeysRef.current = ALL_KEYS.filter((k) => touched.has(k))
|
|
234
|
+
}
|
|
235
|
+
const hasTransformRef = useRef<boolean>(
|
|
236
|
+
activeKeysRef.current.some((k) => TRANSFORM_KEY_SET.has(k)),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
const sharedValues = useAnimatableSharedValues((key) => {
|
|
240
|
+
if (initial === false) {
|
|
241
|
+
const a = animateRecord[key]
|
|
242
|
+
return restValue(a) ?? DEFAULT_RESTING[key]
|
|
243
|
+
}
|
|
244
|
+
return (
|
|
245
|
+
initialRecord?.[key] ??
|
|
246
|
+
restValue(animateRecord[key]) ??
|
|
247
|
+
DEFAULT_RESTING[key]
|
|
248
|
+
)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// Merge gesture sub-state targets over the base `animate` record. Keys
|
|
252
|
+
// touched by any sub-state always appear in the merged record (falling
|
|
253
|
+
// back to `animateRecord` or `DEFAULT_RESTING`) so releasing a gesture
|
|
254
|
+
// animates back to a defined value rather than getting skipped.
|
|
255
|
+
//
|
|
256
|
+
// While exiting, exit values override everything — gesture / animate
|
|
257
|
+
// targets are inert because the component is on its way out.
|
|
258
|
+
const mergedRecord =
|
|
259
|
+
isExiting && exitRecord
|
|
260
|
+
? { ...animateRecord, ...exitRecord }
|
|
261
|
+
: mergeGestureTargets(animateRecord, gesture, {
|
|
262
|
+
pressed,
|
|
263
|
+
focused,
|
|
264
|
+
hovered,
|
|
265
|
+
})
|
|
266
|
+
const mergedSig =
|
|
267
|
+
stableSig(mergedRecord) +
|
|
268
|
+
(isExiting ? '|exit' : '') +
|
|
269
|
+
(shouldReduceMotion ? '|rm' : '')
|
|
270
|
+
const transitionSig = stableSig(transition)
|
|
271
|
+
|
|
272
|
+
// Stable ref to the live `safeToRemove` so the effect's settle-counter
|
|
273
|
+
// closure can reach the latest <Presence> binding without retriggering.
|
|
274
|
+
const safeToRemoveRef = useRef<(() => void) | undefined>(undefined)
|
|
275
|
+
safeToRemoveRef.current = presence?.safeToRemove
|
|
276
|
+
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
// Exit fast-path: nothing to animate (or no exit prop), tell <Presence>
|
|
279
|
+
// immediately so the unmount isn't gated on a phantom animation.
|
|
280
|
+
if (isExiting && (!exitRecord || Object.keys(exitRecord).length === 0)) {
|
|
281
|
+
safeToRemoveRef.current?.()
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let pending = 0
|
|
286
|
+
let done = false
|
|
287
|
+
const onSettle = () => {
|
|
288
|
+
if (done) return
|
|
289
|
+
pending--
|
|
290
|
+
if (pending <= 0) {
|
|
291
|
+
done = true
|
|
292
|
+
if (isExiting) safeToRemoveRef.current?.()
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Count transform axes participating in this effect run so the factory
|
|
297
|
+
// can coalesce their terminal callbacks into a single transform-group
|
|
298
|
+
// event. `undefined` when no transform axis is animating, which lets
|
|
299
|
+
// the factory skip the coalescing branch entirely.
|
|
300
|
+
let transformPending = 0
|
|
301
|
+
for (const k of ALL_KEYS) {
|
|
302
|
+
if (TRANSFORM_KEY_SET.has(k) && mergedRecord[k] !== undefined) {
|
|
303
|
+
transformPending++
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const transformGroup: TransformGroup | undefined =
|
|
307
|
+
transformPending > 0 ? { remaining: transformPending } : undefined
|
|
308
|
+
|
|
309
|
+
for (const key of ALL_KEYS) {
|
|
310
|
+
const target = mergedRecord[key]
|
|
311
|
+
if (target === undefined) continue
|
|
312
|
+
// Reduced-motion overrides every per-key transition (and any nested
|
|
313
|
+
// sequence-step transition) with `no-animation`, which the resolver
|
|
314
|
+
// turns into a direct value assignment. Sequences still iterate but
|
|
315
|
+
// each step settles instantly, which matches the "snap to final
|
|
316
|
+
// state" expectation.
|
|
317
|
+
const cfg = shouldReduceMotion
|
|
318
|
+
? ({ type: 'no-animation' } as const)
|
|
319
|
+
: transitionFor(key, transition)
|
|
320
|
+
if (isExiting) pending++
|
|
321
|
+
const factory = makeKeyCallbackFactory(
|
|
322
|
+
key,
|
|
323
|
+
sharedValues[key],
|
|
324
|
+
targetEndValue(target),
|
|
325
|
+
onAnimationEndRef,
|
|
326
|
+
{
|
|
327
|
+
stepCount: stepCountOf(target),
|
|
328
|
+
totalIterations: totalIterationsOf(cfg),
|
|
329
|
+
},
|
|
330
|
+
isExiting ? onSettle : undefined,
|
|
331
|
+
TRANSFORM_KEY_SET.has(key) ? transformGroup : undefined,
|
|
332
|
+
)
|
|
333
|
+
sharedValues[key].value = resolveAnimatableValue(
|
|
334
|
+
target,
|
|
335
|
+
cfg,
|
|
336
|
+
factory,
|
|
337
|
+
) as never
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// No exit-targeted keys (only `animate` keys present, no `exit`)
|
|
341
|
+
// → release immediately rather than wait for animations that aren't
|
|
342
|
+
// headed toward an exit value.
|
|
343
|
+
if (isExiting && pending === 0) {
|
|
344
|
+
safeToRemoveRef.current?.()
|
|
345
|
+
}
|
|
346
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
347
|
+
}, [mergedSig, transitionSig])
|
|
348
|
+
|
|
349
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
350
|
+
const activeKeys = activeKeysRef.current!
|
|
351
|
+
const hasTransform = hasTransformRef.current
|
|
352
|
+
const out: Record<string, unknown> = {}
|
|
353
|
+
const transform: Array<Record<string, unknown>> = []
|
|
354
|
+
for (const key of activeKeys) {
|
|
355
|
+
const v = sharedValues[key].value
|
|
356
|
+
if (TRANSFORM_KEY_SET.has(key)) {
|
|
357
|
+
transform.push(
|
|
358
|
+
key === 'rotate' ? { rotate: `${v}deg` } : { [key]: v },
|
|
359
|
+
)
|
|
360
|
+
} else {
|
|
361
|
+
out[key] = v
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (hasTransform) out.transform = transform
|
|
365
|
+
return out
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
// Exiting children are tap-deaf: the next press should fall through to
|
|
369
|
+
// whatever is underneath, not re-trigger a soon-to-unmount node. This is
|
|
370
|
+
// the moti #297 fix and a v0.1 acceptance criterion. RN 0.71+ deprecates
|
|
371
|
+
// `pointerEvents` as a prop in favor of the style key, so we merge it
|
|
372
|
+
// alongside the animated style instead of spreading as a prop.
|
|
373
|
+
const mergedStyle = useMemo(
|
|
374
|
+
() =>
|
|
375
|
+
(isExiting
|
|
376
|
+
? [style, animatedStyle, EXITING_POINTER_EVENTS_STYLE]
|
|
377
|
+
: [style, animatedStyle]) as unknown,
|
|
378
|
+
[style, animatedStyle, isExiting],
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
const gestureHandlers = useGestureHandlers(
|
|
382
|
+
gesture,
|
|
383
|
+
rest as Record<string, unknown>,
|
|
384
|
+
setPressed,
|
|
385
|
+
setFocused,
|
|
386
|
+
setHovered,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<AnimatedComponent
|
|
391
|
+
ref={ref as never}
|
|
392
|
+
{...(rest as object)}
|
|
393
|
+
{...gestureHandlers}
|
|
394
|
+
style={mergedStyle}
|
|
395
|
+
/>
|
|
396
|
+
)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
Motion.displayName = `Motion(${Component.displayName ?? Component.name ?? 'Component'})`
|
|
400
|
+
|
|
401
|
+
return Motion as unknown as MotionComponent<C>
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
type SharedValueMap = Record<AnimatableKey, SharedValue<number>>
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Allocate one shared value per animatable key in `ALL_KEYS` and return a
|
|
408
|
+
* **stable** map — same object reference across every render.
|
|
409
|
+
*
|
|
410
|
+
* Stability matters: `useAnimatedStyle` derives its dep array from
|
|
411
|
+
* `Object.values(updater.__closure)`. Our worklet captures `sharedValues`,
|
|
412
|
+
* so a fresh object literal each render would change that dep, fire
|
|
413
|
+
* Reanimated's effect, and re-bind the worklet on the UI thread on every
|
|
414
|
+
* render — the exact cost design principle 8 calls out. The shared values themselves
|
|
415
|
+
* are stable across renders (Reanimated's `useSharedValue` is a `useRef`
|
|
416
|
+
* under the hood), so snapshotting the wrapping object once is safe.
|
|
417
|
+
*
|
|
418
|
+
* Hooks are called in a stable, lexical order — fine for rules-of-hooks.
|
|
419
|
+
* Unused shared values are cheap; the worklet skips them via
|
|
420
|
+
* `activeKeysRef`.
|
|
421
|
+
*/
|
|
422
|
+
function useAnimatableSharedValues(
|
|
423
|
+
init: (key: AnimatableKey) => number,
|
|
424
|
+
): SharedValueMap {
|
|
425
|
+
const translateX = useSharedValue(init('translateX'))
|
|
426
|
+
const translateY = useSharedValue(init('translateY'))
|
|
427
|
+
const scale = useSharedValue(init('scale'))
|
|
428
|
+
const scaleX = useSharedValue(init('scaleX'))
|
|
429
|
+
const scaleY = useSharedValue(init('scaleY'))
|
|
430
|
+
const rotate = useSharedValue(init('rotate'))
|
|
431
|
+
const opacity = useSharedValue(init('opacity'))
|
|
432
|
+
const width = useSharedValue(init('width'))
|
|
433
|
+
const height = useSharedValue(init('height'))
|
|
434
|
+
const borderRadius = useSharedValue(init('borderRadius'))
|
|
435
|
+
|
|
436
|
+
const ref = useRef<SharedValueMap | null>(null)
|
|
437
|
+
if (ref.current === null) {
|
|
438
|
+
ref.current = {
|
|
439
|
+
translateX,
|
|
440
|
+
translateY,
|
|
441
|
+
scale,
|
|
442
|
+
scaleX,
|
|
443
|
+
scaleY,
|
|
444
|
+
rotate,
|
|
445
|
+
opacity,
|
|
446
|
+
width,
|
|
447
|
+
height,
|
|
448
|
+
borderRadius,
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return ref.current
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Build a per-key `CallbackFactory` for the resolver. Each step in a sequence
|
|
456
|
+
* (or the single animation, when `value` isn't an array) gets its own
|
|
457
|
+
* Reanimated callback; when it settles on the UI thread, the callback bridges
|
|
458
|
+
* to JS via `runOnJS` and invokes the user's `onAnimationEnd` with a fully
|
|
459
|
+
* populated `AnimationCallbackInfo`.
|
|
460
|
+
*
|
|
461
|
+
* Phase resolution lives here, on the JS thread. The resolver hands us a
|
|
462
|
+
* coarse rawPhase (`'step'` for any sequence step, `'animation'` for a
|
|
463
|
+
* single-shot terminal); we map that onto the public phase set
|
|
464
|
+
* (`'step' | 'sequence' | 'repeat' | 'animation'`) using `meta` and the
|
|
465
|
+
* iteration counter. `iteration` resets per effect run because the factory
|
|
466
|
+
* is constructed fresh inside the effect.
|
|
467
|
+
*/
|
|
468
|
+
function makeKeyCallbackFactory(
|
|
469
|
+
key: string,
|
|
470
|
+
sharedValue: SharedValue<number>,
|
|
471
|
+
target: number | string | undefined,
|
|
472
|
+
onAnimationEndRef: {
|
|
473
|
+
current: ((info: AnimationCallbackInfo<unknown>) => void) | undefined
|
|
474
|
+
},
|
|
475
|
+
meta: { stepCount: number; totalIterations: number },
|
|
476
|
+
onSettle?: () => void,
|
|
477
|
+
transformGroup?: TransformGroup,
|
|
478
|
+
) {
|
|
479
|
+
if (!onAnimationEndRef.current && !onSettle) return undefined
|
|
480
|
+
|
|
481
|
+
// Shared across this animation graph's callbacks (one per sequence step,
|
|
482
|
+
// or one for a single-shot). Mutated when a full pass completes.
|
|
483
|
+
const state = { iteration: 0 }
|
|
484
|
+
|
|
485
|
+
const isTransformKey = TRANSFORM_KEY_SET.has(key as AnimatableKey)
|
|
486
|
+
|
|
487
|
+
const dispatch = (
|
|
488
|
+
rawPhase: 'step' | 'animation',
|
|
489
|
+
step: number | undefined,
|
|
490
|
+
finished: boolean,
|
|
491
|
+
value: number | string | undefined,
|
|
492
|
+
) => {
|
|
493
|
+
const isLastIteration = state.iteration >= meta.totalIterations - 1
|
|
494
|
+
let phase: 'step' | 'sequence' | 'repeat' | 'animation'
|
|
495
|
+
let isTerminal = false
|
|
496
|
+
|
|
497
|
+
if (rawPhase === 'step') {
|
|
498
|
+
const isLastInPass = step !== undefined && step === meta.stepCount - 1
|
|
499
|
+
if (!isLastInPass) {
|
|
500
|
+
phase = 'step'
|
|
501
|
+
} else if (isLastIteration) {
|
|
502
|
+
phase = 'animation'
|
|
503
|
+
isTerminal = true
|
|
504
|
+
} else {
|
|
505
|
+
phase = 'sequence'
|
|
506
|
+
}
|
|
507
|
+
} else if (isLastIteration) {
|
|
508
|
+
phase = 'animation'
|
|
509
|
+
isTerminal = true
|
|
510
|
+
} else {
|
|
511
|
+
phase = 'repeat'
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const reportedIteration = state.iteration
|
|
515
|
+
if (phase === 'sequence' || phase === 'repeat') state.iteration++
|
|
516
|
+
|
|
517
|
+
const fn = onAnimationEndRef.current
|
|
518
|
+
if (fn) {
|
|
519
|
+
// Transform-group coalescing: a multi-axis translate / scale /
|
|
520
|
+
// rotate animation should fire onAnimationEnd ONCE for the logical
|
|
521
|
+
// transform, not once per axis. We only coalesce the terminal
|
|
522
|
+
// `'animation'` phase — `step`/`sequence`/`repeat` events fire
|
|
523
|
+
// per-axis since each is its own logical event. Released per-axis
|
|
524
|
+
// for a single-axis case too, with `key: 'transform'` for
|
|
525
|
+
// consistency.
|
|
526
|
+
if (isTransformKey && transformGroup && phase === 'animation') {
|
|
527
|
+
transformGroup.remaining--
|
|
528
|
+
if (transformGroup.remaining <= 0) {
|
|
529
|
+
fn({
|
|
530
|
+
key: 'transform' as never,
|
|
531
|
+
finished,
|
|
532
|
+
value,
|
|
533
|
+
target,
|
|
534
|
+
phase,
|
|
535
|
+
step,
|
|
536
|
+
iteration: reportedIteration,
|
|
537
|
+
})
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
fn({
|
|
541
|
+
key: key as never,
|
|
542
|
+
finished,
|
|
543
|
+
value,
|
|
544
|
+
target,
|
|
545
|
+
phase,
|
|
546
|
+
step,
|
|
547
|
+
iteration: reportedIteration,
|
|
548
|
+
})
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// Settle hooks fire per-axis on the terminal phase — <Presence> waits
|
|
552
|
+
// for *every* exiting property to settle before unmounting, so we
|
|
553
|
+
// intentionally do not coalesce these (the transform-group coalesce
|
|
554
|
+
// is purely a user-callback ergonomic).
|
|
555
|
+
if (onSettle && isTerminal) onSettle()
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return (rawPhase: 'step' | 'animation', step: number | undefined) => {
|
|
559
|
+
// Reanimated invokes the callback with only `finished` (see
|
|
560
|
+
// valueSetter.js:24,40,51 in 4.x) — `current` is never passed. Read the
|
|
561
|
+
// shared value inside the worklet; by the time the callback fires the
|
|
562
|
+
// final/clamped value has already been written to it.
|
|
563
|
+
const cb = (finished?: boolean) => {
|
|
564
|
+
'worklet'
|
|
565
|
+
runOnJS(dispatch)(rawPhase, step, !!finished, sharedValue.value)
|
|
566
|
+
}
|
|
567
|
+
return cb
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Number of sequence steps in an animatable value. `1` for plain values and
|
|
573
|
+
* single-step `{ to }` objects; the array length for keyframe arrays.
|
|
574
|
+
*/
|
|
575
|
+
function stepCountOf(v: AnimatableValue<number> | undefined): number {
|
|
576
|
+
if (Array.isArray(v)) return v.length
|
|
577
|
+
return 1
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Total number of iterations the animation will run, including the initial
|
|
582
|
+
* pass. `1` when there is no `repeat`; `Number.POSITIVE_INFINITY` for
|
|
583
|
+
* `'infinite'`. Decay and `no-animation` configs cannot repeat — both return
|
|
584
|
+
* `1` so the iteration counter stays at 0.
|
|
585
|
+
*/
|
|
586
|
+
function totalIterationsOf(cfg: TransitionConfig | undefined): number {
|
|
587
|
+
if (!cfg || cfg.type === 'no-animation' || cfg.type === 'decay') return 1
|
|
588
|
+
const r = cfg.repeat
|
|
589
|
+
if (r === undefined) return 1
|
|
590
|
+
if (r === 'infinite') return Number.POSITIVE_INFINITY
|
|
591
|
+
if (typeof r === 'number') return r
|
|
592
|
+
if (r.count === 'infinite') return Number.POSITIVE_INFINITY
|
|
593
|
+
return r.count
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Pull a single end-value out of an `AnimatableValue` for the
|
|
598
|
+
* `AnimationCallbackInfo.target` field. Plain numbers/strings come through;
|
|
599
|
+
* the last sequence step's `to`/value is used for arrays; `{ to }` step
|
|
600
|
+
* objects use `to`. Returns `undefined` for unrecognized shapes.
|
|
601
|
+
*/
|
|
602
|
+
function targetEndValue(
|
|
603
|
+
v: AnimatableValue<number> | undefined,
|
|
604
|
+
): number | string | undefined {
|
|
605
|
+
if (v === undefined) return undefined
|
|
606
|
+
if (typeof v === 'number' || typeof v === 'string') return v
|
|
607
|
+
if (Array.isArray(v)) {
|
|
608
|
+
return v.length > 0
|
|
609
|
+
? targetEndValue(v[v.length - 1] as AnimatableValue<number>)
|
|
610
|
+
: undefined
|
|
611
|
+
}
|
|
612
|
+
if (typeof v === 'object' && v !== null && 'to' in v) {
|
|
613
|
+
const to = (v as { to: unknown }).to
|
|
614
|
+
return typeof to === 'number' || typeof to === 'string' ? to : undefined
|
|
615
|
+
}
|
|
616
|
+
return undefined
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Subscribe to a `VariantController` and return its `current` key. Returns
|
|
621
|
+
* `undefined` when no controller is provided so callers can fall back to a
|
|
622
|
+
* literal `animate` value.
|
|
623
|
+
*/
|
|
624
|
+
function useControllerKey(
|
|
625
|
+
controller: VariantController | undefined,
|
|
626
|
+
): string | undefined {
|
|
627
|
+
const [, setTick] = useState(0)
|
|
628
|
+
useEffect(() => {
|
|
629
|
+
if (!controller) return
|
|
630
|
+
const unsub = controller.subscribe(() => setTick((n) => n + 1))
|
|
631
|
+
return unsub
|
|
632
|
+
}, [controller])
|
|
633
|
+
return controller?.current
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Resolve the effective `animate` target from the public-prop tuple.
|
|
638
|
+
*
|
|
639
|
+
* Precedence: `controller.current` (when controller is set) > string-keyed
|
|
640
|
+
* `animate` looked up in `variants` > literal `animate` object > `undefined`.
|
|
641
|
+
*/
|
|
642
|
+
function resolveAnimateInput(
|
|
643
|
+
animate: AnimateStyle<unknown> | string | undefined,
|
|
644
|
+
variants: VariantsMap<unknown> | undefined,
|
|
645
|
+
controllerKey: string | undefined,
|
|
646
|
+
): AnimateStyle<unknown> | undefined {
|
|
647
|
+
if (controllerKey !== undefined && variants && controllerKey in variants) {
|
|
648
|
+
return variants[controllerKey]
|
|
649
|
+
}
|
|
650
|
+
if (typeof animate === 'string') {
|
|
651
|
+
if (variants && animate in variants) return variants[animate]
|
|
652
|
+
if (__DEV__) {
|
|
653
|
+
console.warn(
|
|
654
|
+
`[inertia] animate="${animate}" but no matching variant. Did you forget to pass \`variants\`?`,
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
return undefined
|
|
658
|
+
}
|
|
659
|
+
return animate as AnimateStyle<unknown> | undefined
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
declare const __DEV__: boolean
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Pick the resting/initial-frame number out of an `AnimatableValue`. Plain
|
|
666
|
+
* numbers come through unchanged; sequence arrays use their first element;
|
|
667
|
+
* `{ to }` step objects use `to`. Non-numeric or unresolvable shapes return
|
|
668
|
+
* `undefined` so the caller can fall back to `DEFAULT_RESTING`.
|
|
669
|
+
*/
|
|
670
|
+
function restValue(v: AnimatableValue<number> | undefined): number | undefined {
|
|
671
|
+
if (v === undefined) return undefined
|
|
672
|
+
if (typeof v === 'number') return v
|
|
673
|
+
if (Array.isArray(v)) {
|
|
674
|
+
return v.length > 0 ? restValue(v[0] as AnimatableValue<number>) : undefined
|
|
675
|
+
}
|
|
676
|
+
if (typeof v === 'object' && v !== null && 'to' in v) {
|
|
677
|
+
const to = (v as { to: unknown }).to
|
|
678
|
+
return typeof to === 'number' ? to : undefined
|
|
679
|
+
}
|
|
680
|
+
return undefined
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function stableSig(value: unknown): string {
|
|
684
|
+
if (value === undefined) return ''
|
|
685
|
+
try {
|
|
686
|
+
return stableStringify(value)
|
|
687
|
+
} catch {
|
|
688
|
+
return String(value)
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* JSON.stringify with keys sorted at every level — gives a stable signature
|
|
694
|
+
* regardless of property declaration order. Functions serialize as `null` so a
|
|
695
|
+
* change in easing-fn reference is invisible here; that's fine for v0.1
|
|
696
|
+
* (easing swaps are rare and the worklet wrapper handles correctness).
|
|
697
|
+
*/
|
|
698
|
+
function stableStringify(v: unknown): string {
|
|
699
|
+
if (v === null || typeof v !== 'object') {
|
|
700
|
+
if (typeof v === 'function' || v === undefined) return 'null'
|
|
701
|
+
return JSON.stringify(v)
|
|
702
|
+
}
|
|
703
|
+
if (Array.isArray(v)) {
|
|
704
|
+
return '[' + v.map(stableStringify).join(',') + ']'
|
|
705
|
+
}
|
|
706
|
+
const obj = v as Record<string, unknown>
|
|
707
|
+
const keys = Object.keys(obj).sort()
|
|
708
|
+
return (
|
|
709
|
+
'{' +
|
|
710
|
+
keys
|
|
711
|
+
.map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k]))
|
|
712
|
+
.join(',') +
|
|
713
|
+
'}'
|
|
714
|
+
)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Merge gesture sub-state targets over the base `animate` record. Keys touched
|
|
719
|
+
* by any declared sub-state are always present in the result so releasing a
|
|
720
|
+
* gesture animates the property back to a defined value (the base `animate`
|
|
721
|
+
* value when present, otherwise `DEFAULT_RESTING`). Sub-states layer in
|
|
722
|
+
* priority order: `hovered` < `focused` < `pressed`.
|
|
723
|
+
*/
|
|
724
|
+
function mergeGestureTargets(
|
|
725
|
+
base: Partial<Record<AnimatableKey, AnimatableValue<number>>>,
|
|
726
|
+
gesture: GestureSubStates<unknown> | undefined,
|
|
727
|
+
active: { pressed: boolean; focused: boolean; hovered: boolean },
|
|
728
|
+
): Partial<Record<AnimatableKey, AnimatableValue<number>>> {
|
|
729
|
+
if (!gesture) return base
|
|
730
|
+
const merged: Partial<Record<AnimatableKey, AnimatableValue<number>>> = {
|
|
731
|
+
...base,
|
|
732
|
+
}
|
|
733
|
+
const subStates = [
|
|
734
|
+
gesture.hovered,
|
|
735
|
+
gesture.focused,
|
|
736
|
+
gesture.pressed,
|
|
737
|
+
] as Array<
|
|
738
|
+
Partial<Record<AnimatableKey, AnimatableValue<number>>> | undefined
|
|
739
|
+
>
|
|
740
|
+
for (const sub of subStates) {
|
|
741
|
+
if (!sub) continue
|
|
742
|
+
for (const k of ALL_KEYS) {
|
|
743
|
+
if (k in sub && !(k in merged)) {
|
|
744
|
+
merged[k] = DEFAULT_RESTING[k]
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (active.hovered && gesture.hovered) {
|
|
749
|
+
Object.assign(
|
|
750
|
+
merged,
|
|
751
|
+
gesture.hovered as Partial<
|
|
752
|
+
Record<AnimatableKey, AnimatableValue<number>>
|
|
753
|
+
>,
|
|
754
|
+
)
|
|
755
|
+
}
|
|
756
|
+
if (active.focused && gesture.focused) {
|
|
757
|
+
Object.assign(
|
|
758
|
+
merged,
|
|
759
|
+
gesture.focused as Partial<
|
|
760
|
+
Record<AnimatableKey, AnimatableValue<number>>
|
|
761
|
+
>,
|
|
762
|
+
)
|
|
763
|
+
}
|
|
764
|
+
if (active.pressed && gesture.pressed) {
|
|
765
|
+
Object.assign(
|
|
766
|
+
merged,
|
|
767
|
+
gesture.pressed as Partial<
|
|
768
|
+
Record<AnimatableKey, AnimatableValue<number>>
|
|
769
|
+
>,
|
|
770
|
+
)
|
|
771
|
+
}
|
|
772
|
+
return merged
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
type GestureHandlers = Record<string, (event: unknown) => void>
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Build the touch / focus / hover handler props for a gesture-enabled Motion
|
|
779
|
+
* primitive. Returns an empty object when `gesture` is undefined so the
|
|
780
|
+
* component renders identically to the gesture-less path (zero overhead).
|
|
781
|
+
*
|
|
782
|
+
* Existing user-supplied handlers on the same events are composed: the user's
|
|
783
|
+
* handler runs first, then the internal state setter. We pull user handlers
|
|
784
|
+
* out of `rest` rather than overwriting them.
|
|
785
|
+
*/
|
|
786
|
+
function useGestureHandlers(
|
|
787
|
+
gesture: GestureSubStates<unknown> | undefined,
|
|
788
|
+
rest: Record<string, unknown>,
|
|
789
|
+
setPressed: (next: boolean) => void,
|
|
790
|
+
setFocused: (next: boolean) => void,
|
|
791
|
+
setHovered: (next: boolean) => void,
|
|
792
|
+
): GestureHandlers {
|
|
793
|
+
return useMemo(() => {
|
|
794
|
+
if (!gesture) return {}
|
|
795
|
+
const handlers: GestureHandlers = {}
|
|
796
|
+
if (gesture.pressed) {
|
|
797
|
+
handlers.onTouchStart = compose(rest.onTouchStart, () => setPressed(true))
|
|
798
|
+
handlers.onTouchEnd = compose(rest.onTouchEnd, () => setPressed(false))
|
|
799
|
+
handlers.onTouchCancel = compose(rest.onTouchCancel, () =>
|
|
800
|
+
setPressed(false),
|
|
801
|
+
)
|
|
802
|
+
// Pressable / TouchableOpacity expose press hooks above the touch layer;
|
|
803
|
+
// forward to those when present so wrapping consumers stay consistent.
|
|
804
|
+
handlers.onPressIn = compose(rest.onPressIn, () => setPressed(true))
|
|
805
|
+
handlers.onPressOut = compose(rest.onPressOut, () => setPressed(false))
|
|
806
|
+
}
|
|
807
|
+
if (gesture.focused) {
|
|
808
|
+
handlers.onFocus = compose(rest.onFocus, () => setFocused(true))
|
|
809
|
+
handlers.onBlur = compose(rest.onBlur, () => setFocused(false))
|
|
810
|
+
}
|
|
811
|
+
if (gesture.hovered) {
|
|
812
|
+
// Web-only events. RN-Web 0.72+ accepts these on View; native ignores
|
|
813
|
+
// them so the cost is zero on iOS / Android.
|
|
814
|
+
handlers.onMouseEnter = compose(rest.onMouseEnter, () => setHovered(true))
|
|
815
|
+
handlers.onMouseLeave = compose(rest.onMouseLeave, () =>
|
|
816
|
+
setHovered(false),
|
|
817
|
+
)
|
|
818
|
+
}
|
|
819
|
+
return handlers
|
|
820
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
821
|
+
}, [
|
|
822
|
+
gesture?.pressed ? 1 : 0,
|
|
823
|
+
gesture?.focused ? 1 : 0,
|
|
824
|
+
gesture?.hovered ? 1 : 0,
|
|
825
|
+
rest.onTouchStart,
|
|
826
|
+
rest.onTouchEnd,
|
|
827
|
+
rest.onTouchCancel,
|
|
828
|
+
rest.onPressIn,
|
|
829
|
+
rest.onPressOut,
|
|
830
|
+
rest.onFocus,
|
|
831
|
+
rest.onBlur,
|
|
832
|
+
rest.onMouseEnter,
|
|
833
|
+
rest.onMouseLeave,
|
|
834
|
+
])
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function compose(
|
|
838
|
+
user: unknown,
|
|
839
|
+
ours: (event: unknown) => void,
|
|
840
|
+
): (event: unknown) => void {
|
|
841
|
+
if (typeof user !== 'function') return ours
|
|
842
|
+
return (event: unknown) => {
|
|
843
|
+
;(user as (event: unknown) => void)(event)
|
|
844
|
+
ours(event)
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Suppress the implicit any-return of the rotate ternary's union shape.
|
|
849
|
+
// `TransformKey` is exported only to keep the type readable in d.ts.
|
|
850
|
+
export type { TransformKey }
|