@octane-xplat/motion 0.9.0 → 0.11.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/README.md +31 -2
- package/UPSTREAM.md +284 -16
- package/dist/native/index.js +980 -112
- package/dist/native/octane-client-build.json +1 -1
- package/dist/web/index.js +1104 -166
- package/dist/web/octane-client-build.json +1 -1
- package/package.json +8 -3
- package/src/clock.ts +13 -1
- package/src/components.mobile.test.tsrx +58 -0
- package/src/components.tsrx +185 -39
- package/src/components.web.test.tsrx +234 -1
- package/src/controller.ts +122 -10
- package/src/delegation.test.ts +269 -0
- package/src/delegation.ts +29 -0
- package/src/delegation.web.test.ts +270 -0
- package/src/drag.mobile.test.ts +81 -0
- package/src/drag.test.ts +167 -0
- package/src/drag.ts +157 -0
- package/src/driver.ts +249 -0
- package/src/driver.web.ts +180 -0
- package/src/engine.test.ts +86 -0
- package/src/engine.ts +137 -15
- package/src/hooks.tsrx +53 -2
- package/src/host-types.ts +44 -1
- package/src/host.ts +39 -2
- package/src/host.web.ts +51 -10
- package/src/index.ts +18 -4
- package/src/index.web.ts +18 -4
- package/src/pan.ts +92 -0
- package/src/pan.web.ts +116 -0
- package/src/presence.tsrx +6 -7
- package/src/reduced-motion.ts +23 -4
- package/src/types.ts +99 -10
- package/src/value.ts +15 -5
- package/src/variants.test.ts +197 -0
- package/src/variants.ts +226 -0
- package/types/engine.d.ts +7 -1
- package/types/index.d.ts +22 -1
- package/types/types.d.ts +93 -10
- package/types/value.d.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octane-xplat/motion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Declarative motion and retained presence for Octane on web, iOS and Android",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"android",
|
|
@@ -43,21 +43,23 @@
|
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@octane-xplat/ui": "0.
|
|
46
|
+
"@octane-xplat/ui": "0.11.0",
|
|
47
47
|
"motion-dom": "12.42.2"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
+
"@nativescript-community/gesturehandler": "2.0.45",
|
|
50
51
|
"@nativescript-community/octane": "0.2.4",
|
|
51
52
|
"@nativescript/core": "9.1.2",
|
|
52
53
|
"@octanejs/vite-plugin": "0.1.61",
|
|
53
54
|
"jsdom": "30.1.1",
|
|
54
55
|
"octane": "0.6.3",
|
|
55
|
-
"tsrx-typegen": "0.2.
|
|
56
|
+
"tsrx-typegen": "0.2.2",
|
|
56
57
|
"typescript": "5.9.3",
|
|
57
58
|
"vite": "8.3.0",
|
|
58
59
|
"vitest": "5.0.1"
|
|
59
60
|
},
|
|
60
61
|
"peerDependencies": {
|
|
62
|
+
"@nativescript-community/gesturehandler": "2.0.45",
|
|
61
63
|
"@nativescript-community/octane": ">=0.2.1 <1",
|
|
62
64
|
"@nativescript/core": ">=9.1.0 <10",
|
|
63
65
|
"octane": ">=0.1.51 <1"
|
|
@@ -68,6 +70,9 @@
|
|
|
68
70
|
},
|
|
69
71
|
"@nativescript/core": {
|
|
70
72
|
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"@nativescript-community/gesturehandler": {
|
|
75
|
+
"optional": true
|
|
71
76
|
}
|
|
72
77
|
},
|
|
73
78
|
"scripts": {
|
package/src/clock.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { requestAnimationFrame, cancelAnimationFrame } from '@nativescript/core/animation-frame'
|
|
2
|
+
import { Application } from '@nativescript/core'
|
|
2
3
|
import type { Clock } from './clock-types'
|
|
4
|
+
|
|
5
|
+
const platform = globalThis as any
|
|
6
|
+
|
|
7
|
+
const now: () => number = Application.ios
|
|
8
|
+
? platform.CACurrentMediaTime
|
|
9
|
+
? () => platform.CACurrentMediaTime() * 1000
|
|
10
|
+
: (platform.__time ?? Date.now)
|
|
11
|
+
: platform.java?.lang?.System
|
|
12
|
+
? () => platform.java.lang.System.nanoTime() / 1e6
|
|
13
|
+
: Date.now
|
|
14
|
+
|
|
3
15
|
export const clock: Clock = {
|
|
4
|
-
now
|
|
16
|
+
now,
|
|
5
17
|
request: requestAnimationFrame,
|
|
6
18
|
cancel: cancelAnimationFrame,
|
|
7
19
|
}
|
|
@@ -95,3 +95,61 @@ it('retargets a retained native host on a signal write', () => {
|
|
|
95
95
|
expect(node.translateX).toBe(75)
|
|
96
96
|
root.unmount()
|
|
97
97
|
})
|
|
98
|
+
|
|
99
|
+
import { handlers, GestureHandlerStateEvent, GestureHandlerTouchEvent, GestureState } from '../test/gesturehandler'
|
|
100
|
+
|
|
101
|
+
function NativeDrag() @{
|
|
102
|
+
<MotionConfig reducedMotion="always">
|
|
103
|
+
<motion.View id="native-drag" drag="x" dragConstraints={{ left: -40, right: 40 }} dragElastic={false} />
|
|
104
|
+
</MotionConfig>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
it('drives the retained native host from its handler and snaps reduced settlement', () => {
|
|
108
|
+
const container = createObjectContainer('nativescript')
|
|
109
|
+
const root = createUniversalRoot(container, createObjectDriver('nativescript'))
|
|
110
|
+
root.render(NativeDrag, {})
|
|
111
|
+
const node: any = container.children[0]
|
|
112
|
+
const handler = handlers.findLast((h) => h.node === node)
|
|
113
|
+
expect(handler).toBeDefined()
|
|
114
|
+
handler.emit(GestureHandlerStateEvent, { state: GestureState.ACTIVE, extraData: { translationX: 10 } })
|
|
115
|
+
handler.emit(GestureHandlerTouchEvent, { state: GestureState.ACTIVE, extraData: { translationX: 20 } })
|
|
116
|
+
expect(node.translateX).toBe(20)
|
|
117
|
+
handler.emit(GestureHandlerStateEvent, { state: GestureState.END, extraData: { translationX: 20, velocityX: 250 } })
|
|
118
|
+
expect(node.translateX).toBe(40)
|
|
119
|
+
root.unmount()
|
|
120
|
+
expect(handler.node).toBeUndefined()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
function NativeVariants(props: any) @{
|
|
124
|
+
<motion.View animate={props.label} variants={{ show: { transition: { duration: 0, when: 'beforeChildren' } }, hide: { transition: { duration: 0 } } }}>
|
|
125
|
+
<motion.View custom={props.custom ?? 70} variants={{ show: (custom: number) => ({ x: custom, transition: { duration: 0 } }), hide: { x: 0, transition: { duration: 0 } } }} />
|
|
126
|
+
</motion.View>
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
it('propagates variant labels to retained native children', async () => {
|
|
130
|
+
const container = createObjectContainer('nativescript')
|
|
131
|
+
const root = createUniversalRoot(container, createObjectDriver('nativescript'))
|
|
132
|
+
root.render(NativeVariants, { label: 'show' })
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
134
|
+
const child: any = container.children[0].children[0]
|
|
135
|
+
expect(child.translateX).toBe(70)
|
|
136
|
+
flushUniversalSync(() => root.render(NativeVariants, { label: 'hide' }))
|
|
137
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
138
|
+
expect(container.children[0].children[0]).toBe(child)
|
|
139
|
+
expect(child.translateX).toBe(0)
|
|
140
|
+
root.unmount()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('retargets native inherited custom resolvers with an unchanged parent label', async () => {
|
|
144
|
+
const container = createObjectContainer('nativescript')
|
|
145
|
+
const root = createUniversalRoot(container, createObjectDriver('nativescript'))
|
|
146
|
+
root.render(NativeVariants, { label: 'show', custom: 30 })
|
|
147
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
148
|
+
const child: any = container.children[0].children[0]
|
|
149
|
+
expect(child.translateX).toBe(30)
|
|
150
|
+
flushUniversalSync(() => root.render(NativeVariants, { label: 'show', custom: 50 }))
|
|
151
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
152
|
+
expect(container.children[0].children[0]).toBe(child)
|
|
153
|
+
expect(child.translateX).toBe(50)
|
|
154
|
+
root.unmount()
|
|
155
|
+
})
|
package/src/components.tsrx
CHANGED
|
@@ -1,42 +1,82 @@
|
|
|
1
|
-
import { useRef, useLayoutEffect, useContext } from 'octane'
|
|
2
|
-
import { View,
|
|
1
|
+
import { createContext, useRef, useLayoutEffect, useContext } from 'octane'
|
|
2
|
+
import { View, HStack, Pressable } from '@octane-xplat/ui'
|
|
3
3
|
import { ConfigContext, useReducedMotion } from './config.tsrx'
|
|
4
4
|
import { PresenceContext } from './presence.tsrx'
|
|
5
5
|
import type { PresenceMember } from './presence-state'
|
|
6
|
+
import { attachDrag, validateDrag } from './drag'
|
|
6
7
|
import { clock } from './clock'
|
|
7
8
|
import { attachHost } from './host'
|
|
8
|
-
import type { HostAdapter } from './host-types'
|
|
9
|
-
import { Controller, keys, defaults
|
|
10
|
-
import {
|
|
9
|
+
import type { GestureKind, HostAdapter } from './host-types'
|
|
10
|
+
import { Controller, keys, defaults } from './controller'
|
|
11
|
+
import { validateTransitionInput } from './engine'
|
|
12
|
+
import { VariantNode, isLabels, resolveVariant, playbackTransition } from './variants'
|
|
11
13
|
import { MotionValue } from './value'
|
|
12
|
-
import type { MotionKey, MotionProps, MotionViewProps, MotionRowProps, MotionPressableProps, Target } from './types'
|
|
14
|
+
import type { MotionKey, MotionProps, MotionViewProps, MotionRowProps, MotionPressableProps, Target, AnimationDefinition } from './types'
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
const VariantContext = createContext<VariantNode | null>(null)
|
|
17
|
+
|
|
18
|
+
/** host node → its motion controller; powers `useAnimate` scope lookup. */
|
|
19
|
+
export const controllers = new WeakMap<object, Controller>()
|
|
20
|
+
|
|
21
|
+
const GESTURE_PROPS: Record<GestureKind, { target: 'whileTap' | 'whileFocus'; transition: 'whileTapTransition' | 'whileFocusTransition' }> = {
|
|
22
|
+
press: { target: 'whileTap', transition: 'whileTapTransition' },
|
|
23
|
+
focus: { target: 'whileFocus', transition: 'whileFocusTransition' },
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function useMotion(props: MotionProps & Pick<MotionViewProps, 'ref'>) {
|
|
27
|
+
for (const name of ['layout', 'layoutId', 'inherit', 'whileHover', 'whileInView', 'whileDrag']) {
|
|
16
28
|
if (name in props) throw new Error(`motion: ${name} is outside the supported subset`)
|
|
17
29
|
}
|
|
18
30
|
|
|
31
|
+
validateDrag(props)
|
|
32
|
+
|
|
33
|
+
const parentVariant = useContext(VariantContext)
|
|
34
|
+
const inherits = props.animate === undefined && parentVariant !== null
|
|
35
|
+
const initialDefinition = props.initial ?? parentVariant?.initial
|
|
36
|
+
const animateDefinition = props.animate ?? parentVariant?.animate
|
|
37
|
+
const resolve = (definition: AnimationDefinition | false | undefined, source: MotionProps = props) => {
|
|
38
|
+
const resolved = resolveVariant(definition, source.variants, source.custom)
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
if (resolved.target[key] !== undefined && source.style?.[key] instanceof MotionValue) throw new Error(`motion: ${key} has both an animate target and a MotionValue writer`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return resolved
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const resolvedAnimate = resolve(animateDefinition)
|
|
47
|
+
const resolvedInitial = resolve(initialDefinition)
|
|
48
|
+
const resolvedExit = resolve(props.exit)
|
|
19
49
|
const presence = useContext(PresenceContext)
|
|
20
50
|
if (props.exit && !presence) throw new Error('motion: exit requires a mounted Presence boundary')
|
|
21
51
|
const config = useContext(ConfigContext)
|
|
22
52
|
const systemReduced = useReducedMotion()
|
|
23
53
|
const reduced = config.reducedMotion === 'always' || (config.reducedMotion === 'user' && systemReduced)
|
|
24
54
|
const transition = props.transition ?? config.transition ?? {}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (props.
|
|
55
|
+
validateTransitionInput(transition)
|
|
56
|
+
resolve(props.whileTap)
|
|
57
|
+
resolve(props.whileFocus)
|
|
58
|
+
if (props.whileTapTransition) validateTransitionInput(props.whileTapTransition)
|
|
59
|
+
if (props.whileFocusTransition) validateTransitionInput(props.whileFocusTransition)
|
|
29
60
|
const latest = useRef(props)
|
|
30
61
|
latest.current = props
|
|
31
|
-
const state = useRef<{
|
|
32
|
-
|
|
62
|
+
const state = useRef<{
|
|
63
|
+
controller: Controller
|
|
64
|
+
host?: any
|
|
65
|
+
adapter?: HostAdapter
|
|
66
|
+
initialized: boolean
|
|
67
|
+
gestureOff?: () => void
|
|
68
|
+
dragOff?: () => void
|
|
69
|
+
preGesture?: Record<GestureKind, Target | undefined>
|
|
70
|
+
} | null>(null)
|
|
71
|
+
|
|
72
|
+
if (!state.current) state.current = { controller: new Controller(clock), initialized: false, preGesture: { press: undefined, focus: undefined } }
|
|
33
73
|
const current = state.current
|
|
34
74
|
const style: Record<string, unknown> = {}
|
|
35
75
|
const bindings: [MotionKey, number | MotionValue][] = []
|
|
36
76
|
for (const [key, value] of Object.entries(props.style ?? {})) {
|
|
37
77
|
if (keys.includes(key as MotionKey)) {
|
|
38
78
|
if (!(value instanceof MotionValue) && (typeof value !== 'number' || !Number.isFinite(value))) throw new Error(`motion: invalid style ${key}`)
|
|
39
|
-
if (value instanceof MotionValue && (
|
|
79
|
+
if (value instanceof MotionValue && (resolvedAnimate.target[key as MotionKey] !== undefined || resolvedExit.target[key as MotionKey] !== undefined || (resolvedInitial.target[key as MotionKey] !== undefined))) throw new Error(`motion: ${key} has both an animate target and a MotionValue writer`)
|
|
40
80
|
bindings.push([key as MotionKey, value as number | MotionValue])
|
|
41
81
|
} else {
|
|
42
82
|
if (value instanceof MotionValue || ['transform', 'translateX', 'translateY', 'animation', 'transition'].includes(key)) throw new Error(`motion: unsupported or conflicting style ${key}`)
|
|
@@ -48,11 +88,40 @@ function useMotion(props: MotionProps & { bind?: (host: any) => void }) {
|
|
|
48
88
|
const boundKeys = useRef<MotionKey[]>([])
|
|
49
89
|
const firstBinding = useRef(true)
|
|
50
90
|
const firstAnimation = useRef(true)
|
|
91
|
+
const inheritedRun = useRef<{ label: string; resolved: string } | null>(null)
|
|
51
92
|
if (previous.current.length !== bindings.length || previous.current.some(([key, value], i) => key !== bindings[i][0] || value !== bindings[i][1])) previous.current = bindings
|
|
52
|
-
|
|
53
|
-
|
|
93
|
+
current.controller.onUpdate = (snapshot) => latest.current.onUpdate?.(snapshot)
|
|
94
|
+
|
|
95
|
+
const gesture = (kind: GestureKind, phase: 'start' | 'end') => {
|
|
96
|
+
const shape = GESTURE_PROPS[kind]
|
|
97
|
+
const definition = latest.current[shape.target]
|
|
98
|
+
if (!definition) return
|
|
99
|
+
const { target, transition: override } = resolve(definition, latest.current)
|
|
100
|
+
const gestureTransition = playbackTransition(override ?? latest.current[shape.transition] ?? playback.current.transition)
|
|
101
|
+
if (phase === 'start') {
|
|
102
|
+
// Snapshot the gesture's keys so release returns to where they were.
|
|
103
|
+
const snapshot = current.controller.snapshot()
|
|
104
|
+
const rest: Target = {}
|
|
105
|
+
for (const key of Object.keys(target) as MotionKey[]) rest[key] = snapshot[key]
|
|
106
|
+
current.preGesture![kind] = rest
|
|
107
|
+
void current.controller.animate(target, gestureTransition, playback.current.reduced)
|
|
108
|
+
} else {
|
|
109
|
+
const snap = current.preGesture![kind] ?? {}
|
|
110
|
+
current.preGesture![kind] = undefined
|
|
111
|
+
// The declarative animate prop wins over the pre-gesture snapshot.
|
|
112
|
+
const rest: Target = {}
|
|
113
|
+
for (const key of Object.keys(target) as MotionKey[]) rest[key] = resolve(latest.current.animate ?? parentVariant?.animate, latest.current).target[key] ?? snap[key] ?? defaults[key]
|
|
114
|
+
void current.controller.animate(rest, gestureTransition, playback.current.reduced)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const ref = useRef<(host: any) => void>(() => {})
|
|
119
|
+
ref.current = (host) => {
|
|
54
120
|
if (!host) return
|
|
55
121
|
if (current.host !== host) {
|
|
122
|
+
current.gestureOff?.()
|
|
123
|
+
current.dragOff?.()
|
|
124
|
+
current.gestureOff = undefined
|
|
56
125
|
current.adapter?.restore()
|
|
57
126
|
current.host = host
|
|
58
127
|
current.adapter = attachHost(host)
|
|
@@ -60,43 +129,73 @@ function useMotion(props: MotionProps & { bind?: (host: any) => void }) {
|
|
|
60
129
|
const baseline = current.adapter.read()
|
|
61
130
|
for (const [key, value] of previous.current) baseline[key] = value instanceof MotionValue ? value.get() : value
|
|
62
131
|
current.controller.seed(baseline)
|
|
63
|
-
current.controller.seed(
|
|
132
|
+
current.controller.seed(initialDefinition === false ? resolvedAnimate.target : resolvedInitial.target)
|
|
64
133
|
current.initialized = true
|
|
65
134
|
}
|
|
66
135
|
|
|
67
|
-
current.controller.attach(current.adapter
|
|
136
|
+
current.controller.attach(current.adapter)
|
|
137
|
+
controllers.set(host, current.controller)
|
|
138
|
+
configureDrag()
|
|
139
|
+
if (current.adapter.gesture) {
|
|
140
|
+
const offs = (['press', 'focus'] as const)
|
|
141
|
+
.map((kind) => current.adapter!.gesture!(kind, { start: () => gesture(kind, 'start'), end: () => gesture(kind, 'end') }))
|
|
142
|
+
.filter((off): off is () => void => typeof off === 'function')
|
|
143
|
+
|
|
144
|
+
current.gestureOff = () => offs.forEach((off) => off())
|
|
145
|
+
}
|
|
68
146
|
}
|
|
69
147
|
|
|
70
148
|
current.controller.flush()
|
|
71
|
-
latest.current.bind?.(host)
|
|
72
149
|
}
|
|
73
150
|
|
|
74
151
|
const playback = useRef({ transition, reduced })
|
|
75
152
|
playback.current = { transition, reduced }
|
|
153
|
+
const nodeRef = useRef<VariantNode | null>(null)
|
|
154
|
+
if (!nodeRef.current) nodeRef.current = new VariantNode()
|
|
155
|
+
const node = nodeRef.current
|
|
156
|
+
node.initial = initialDefinition === false || isLabels(initialDefinition) ? initialDefinition : undefined
|
|
157
|
+
node.animate = isLabels(animateDefinition) ? animateDefinition : undefined
|
|
158
|
+
node.resolve = (definition) => {
|
|
159
|
+
const resolved = resolve(definition, latest.current)
|
|
160
|
+
return { target: resolved.target, transition: resolved.transition ?? playback.current.transition }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
node.stopOwn = () => current.controller.stop()
|
|
164
|
+
node.play = async (definition, target, timing) => {
|
|
165
|
+
latest.current.onAnimationStart?.(definition)
|
|
166
|
+
const result = await current.controller.animate(target, timing, playback.current.reduced)
|
|
167
|
+
if (result === 'finished' && current.host) latest.current.onAnimationComplete?.()
|
|
168
|
+
return result
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
useLayoutEffect(() => inherits ? parentVariant!.register(node) : undefined, [inherits, parentVariant])
|
|
76
172
|
const returning = useRef<Target | null>(null)
|
|
77
173
|
const member = useRef<PresenceMember | null>(null)
|
|
78
174
|
if (!member.current) member.current = {
|
|
79
175
|
exit() {
|
|
176
|
+
node.cancelPending()
|
|
80
177
|
const snapshot = current.controller.snapshot()
|
|
81
178
|
returning.current ??= {}
|
|
82
|
-
|
|
179
|
+
const resolved = resolve(latest.current.exit, latest.current)
|
|
180
|
+
for (const key of Object.keys(resolved.target) as MotionKey[]) {
|
|
83
181
|
if (!(key in returning.current)) returning.current[key] = snapshot[key]
|
|
84
182
|
}
|
|
85
183
|
|
|
86
|
-
return current.controller.animate(
|
|
184
|
+
return current.controller.animate(resolved.target, playbackTransition(resolved.transition ?? playback.current.transition), playback.current.reduced)
|
|
87
185
|
},
|
|
88
186
|
enter() {
|
|
89
|
-
const
|
|
187
|
+
const resolved = resolve(latest.current.animate ?? parentVariant?.animate, latest.current)
|
|
188
|
+
const target = { ...returning.current, ...resolved.target }
|
|
90
189
|
returning.current = null
|
|
91
|
-
void current.controller.animate(target, playback.current.transition, playback.current.reduced).then((result) => {
|
|
190
|
+
void current.controller.animate(target, playbackTransition(resolved.transition ?? playback.current.transition), playback.current.reduced).then((result) => {
|
|
92
191
|
if (result === 'finished' && current.host && presence?.present) latest.current.onAnimationComplete?.()
|
|
93
192
|
})
|
|
94
193
|
},
|
|
95
194
|
}
|
|
96
195
|
|
|
97
196
|
useLayoutEffect(() => presence?.register(member.current!), [presence])
|
|
98
|
-
const
|
|
99
|
-
useLayoutEffect(() => () => { current.controller.destroy(); current.host = undefined; current.adapter = undefined }, [])
|
|
197
|
+
const hostRef = useRef((host: any) => ref.current(host)).current
|
|
198
|
+
useLayoutEffect(() => () => { node.stop(); current.dragOff?.(); current.gestureOff?.(); current.controller.destroy(); current.host = undefined; current.adapter = undefined }, [])
|
|
100
199
|
useLayoutEffect(() => {
|
|
101
200
|
const off: (() => void)[] = []
|
|
102
201
|
for (const key of boundKeys.current) if (!previous.current.some(([next]) => key === next)) current.controller.set(key, defaults[key])
|
|
@@ -110,33 +209,80 @@ function useMotion(props: MotionProps & { bind?: (host: any) => void }) {
|
|
|
110
209
|
return () => off.forEach((unsubscribe) => unsubscribe())
|
|
111
210
|
}, [previous.current])
|
|
112
211
|
|
|
212
|
+
const configureDrag = () => {
|
|
213
|
+
current.dragOff?.()
|
|
214
|
+
current.dragOff = undefined
|
|
215
|
+
if (!current.adapter) return
|
|
216
|
+
current.dragOff = attachDrag(current.adapter, current.controller, latest.current, (axis) => {
|
|
217
|
+
const binding = previous.current.find(([key]) => key === axis)?.[1]
|
|
218
|
+
return binding instanceof MotionValue ? binding : current.controller.value(axis)
|
|
219
|
+
}, () => playback.current.reduced, () => latest.current)
|
|
220
|
+
}
|
|
221
|
+
|
|
113
222
|
useLayoutEffect(() => {
|
|
114
|
-
|
|
115
|
-
|
|
223
|
+
configureDrag()
|
|
224
|
+
}, [props.drag, JSON.stringify(props.dragConstraints), props.dragElastic, props.dragMomentum, previous.current])
|
|
225
|
+
|
|
226
|
+
useLayoutEffect(() => {
|
|
227
|
+
const skip = firstAnimation.current && initialDefinition === false
|
|
116
228
|
firstAnimation.current = false
|
|
117
229
|
if (presence && !presence.present) presence.restart(member.current!)
|
|
118
|
-
else if (
|
|
119
|
-
if (
|
|
120
|
-
|
|
230
|
+
else if (animateDefinition !== undefined && !skip) {
|
|
231
|
+
if (!inherits) void node.run(animateDefinition)
|
|
232
|
+
else {
|
|
233
|
+
const label = JSON.stringify(animateDefinition)
|
|
234
|
+
const resolved = JSON.stringify({ resolvedAnimate, transition, reduced })
|
|
235
|
+
const previousRun = inheritedRun.current
|
|
236
|
+
if (previousRun?.label === label && previousRun.resolved !== resolved) {
|
|
237
|
+
const parentTiming = parentVariant!.resolve(animateDefinition).transition
|
|
238
|
+
void node.run(animateDefinition, (parentTiming.delayChildren ?? 0) + [...parentVariant!.children].indexOf(node) * (parentTiming.staggerChildren ?? 0))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
inheritedRun.current = { label, resolved }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
121
244
|
|
|
122
|
-
return () => {
|
|
123
|
-
}, [JSON.stringify(
|
|
245
|
+
return () => { if (!inherits) node.stop() }
|
|
246
|
+
}, [JSON.stringify(resolvedAnimate), JSON.stringify(resolvedExit), JSON.stringify(animateDefinition), JSON.stringify(transition), reduced, inherits])
|
|
124
247
|
|
|
125
|
-
const { initial, exit, animate, transition: ignoredTransition, onAnimationComplete, style: ignoredStyle,
|
|
126
|
-
return { ...rest, style,
|
|
248
|
+
const { drag, dragConstraints, dragElastic, dragMomentum, onDragStart, onDrag, onDragEnd, variants, custom, initial, exit, animate, transition: ignoredTransition, whileTap, whileTapTransition, whileFocus, whileFocusTransition, onAnimationStart, onAnimationComplete, onUpdate, style: ignoredStyle, ref: ignoredRef, ...rest } = props
|
|
249
|
+
return { hostProps: { ...rest, style, ref: [hostRef, props.ref] }, node }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function MotionLeaf(props: MotionProps & { component?: any; children?: any }) @{
|
|
253
|
+
const Component = props.component
|
|
254
|
+
const { hostProps, node } = useMotion(props)
|
|
255
|
+
|
|
256
|
+
<VariantContext value={node}><Component {...hostProps}>{props.children}</Component></VariantContext>
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Wrap any component that accepts `ref`/`style`/`children` — the portable
|
|
260
|
+
* equivalent of upstream's `motion.<tag>` host factory. Compiled components
|
|
261
|
+
* take (props, slotState, extra) — the factory forwards them into MotionLeaf,
|
|
262
|
+
* whose dynamic <Component> tag renders the wrapped leaf. */
|
|
263
|
+
export function createMotion<P extends Record<string, unknown>>(Component: (props: P) => any) {
|
|
264
|
+
return function MotionCreated(props: P & MotionProps, __s?: any, __extra?: any) {
|
|
265
|
+
return (MotionLeaf as any)({ ...props, component: Component }, __s, __extra)
|
|
266
|
+
}
|
|
127
267
|
}
|
|
128
268
|
|
|
129
269
|
/** Animated View preserving the shared primitive's layout and accessibility. */
|
|
130
270
|
export function MotionView(props: MotionViewProps) @{
|
|
131
|
-
|
|
271
|
+
const { hostProps, node } = useMotion(props)
|
|
272
|
+
|
|
273
|
+
<VariantContext value={node}><View {...hostProps}>{props.children}</View></VariantContext>
|
|
132
274
|
}
|
|
133
275
|
|
|
134
|
-
/** Animated horizontal
|
|
276
|
+
/** Animated horizontal stack. */
|
|
135
277
|
export function MotionRow(props: MotionRowProps) @{
|
|
136
|
-
|
|
278
|
+
const { hostProps, node } = useMotion(props)
|
|
279
|
+
|
|
280
|
+
<VariantContext value={node}><HStack {...hostProps}>{props.children}</HStack></VariantContext>
|
|
137
281
|
}
|
|
138
282
|
|
|
139
283
|
/** Animated Pressable preserving its input callbacks and disabled state. */
|
|
140
284
|
export function MotionPressable(props: MotionPressableProps) @{
|
|
141
|
-
|
|
285
|
+
const { hostProps, node } = useMotion(props)
|
|
286
|
+
|
|
287
|
+
<VariantContext value={node}><Pressable {...hostProps}>{props.children}</Pressable></VariantContext>
|
|
142
288
|
}
|