@tamagui/animations-css 2.7.7 → 3.0.0-beta.1097.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/animated-number.cjs +194 -0
- package/dist/cjs/animated-number.native.cjs +291 -0
- package/dist/cjs/animated-number.native.js +293 -0
- package/dist/cjs/animated-number.native.js.map +1 -0
- package/dist/cjs/createAnimations.cjs +321 -418
- package/dist/cjs/createAnimations.native.cjs +430 -0
- package/dist/cjs/createAnimations.native.js +405 -511
- package/dist/cjs/createAnimations.native.js.map +1 -1
- package/dist/cjs/extras.cjs +36 -0
- package/dist/cjs/extras.native.cjs +38 -0
- package/dist/cjs/extras.native.js +40 -0
- package/dist/cjs/extras.native.js.map +1 -0
- package/dist/cjs/index.cjs +9 -11
- package/dist/cjs/index.native.cjs +20 -0
- package/dist/cjs/index.native.js +9 -10
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/animated-number.mjs +164 -0
- package/dist/esm/animated-number.mjs.map +1 -0
- package/dist/esm/animated-number.native.js +259 -0
- package/dist/esm/animated-number.native.js.map +1 -0
- package/dist/esm/createAnimations.mjs +307 -393
- package/dist/esm/createAnimations.mjs.map +1 -1
- package/dist/esm/createAnimations.native.js +390 -486
- package/dist/esm/createAnimations.native.js.map +1 -1
- package/dist/esm/extras.mjs +17 -0
- package/dist/esm/extras.mjs.map +1 -0
- package/dist/esm/extras.native.js +17 -0
- package/dist/esm/extras.native.js.map +1 -0
- package/dist/esm/index.js +1 -2
- package/dist/esm/index.mjs +1 -2
- package/dist/esm/index.native.js +1 -2
- package/package.json +27 -9
- package/src/animated-number.tsx +262 -0
- package/src/createAnimations.tsx +294 -555
- package/src/extras.ts +24 -0
- package/types/animated-number.d.ts +19 -0
- package/types/animated-number.d.ts.map +11 -0
- package/types/createAnimations.d.ts +2 -1
- package/types/createAnimations.d.ts.map +2 -2
- package/types/extras.d.ts +6 -0
- package/types/extras.d.ts.map +11 -0
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/index.mjs.map +0 -1
- package/dist/esm/index.native.js.map +0 -1
package/src/createAnimations.tsx
CHANGED
|
@@ -1,162 +1,128 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
forAnimationState,
|
|
3
|
+
hasTransition,
|
|
4
|
+
resolveTransition,
|
|
5
|
+
toCSSTransition,
|
|
6
|
+
type AnimationsConfig,
|
|
7
7
|
} from '@tamagui/animation-helpers'
|
|
8
8
|
import { useIsomorphicLayoutEffect } from '@tamagui/constants'
|
|
9
9
|
import { ResetPresence, usePresence } from '@tamagui/use-presence'
|
|
10
|
-
import type { AnimationDriver
|
|
11
|
-
import { transformsToString } from '@tamagui/web'
|
|
12
|
-
import React
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// Try to match seconds and convert to milliseconds
|
|
30
|
-
const sMatch = animation.match(EXTRACT_S_REGEX)
|
|
31
|
-
if (sMatch) {
|
|
32
|
-
return Math.round(Number.parseFloat(sMatch[1]) * 1000)
|
|
10
|
+
import type { AnimationDriver } from '@tamagui/web'
|
|
11
|
+
import { transformsToString } from '@tamagui/web/internal-runtime'
|
|
12
|
+
import React from 'react'
|
|
13
|
+
|
|
14
|
+
// rAF-driven animated number is browser-only. read (don't call) at module scope
|
|
15
|
+
// so ssr never touches requestAnimationFrame.
|
|
16
|
+
const hasRAF = typeof requestAnimationFrame !== 'undefined'
|
|
17
|
+
|
|
18
|
+
// resolve once all WAAPI animations on `node` finish. mirrors base-ui's
|
|
19
|
+
// useAnimationsFinished: resolves immediately when the browser exposes no
|
|
20
|
+
// animations (zero-animation elements), and re-checks after an aborted
|
|
21
|
+
// animation in case a property it depended on changed mid-flight and started a
|
|
22
|
+
// new one. falls back to immediate resolve when getAnimations is unavailable
|
|
23
|
+
// (ssr / older webviews). resolves `false` when animations were canceled with
|
|
24
|
+
// nothing left running (interruption).
|
|
25
|
+
function waitForAnimations(node: HTMLElement): Promise<boolean> {
|
|
26
|
+
if (typeof node.getAnimations !== 'function') {
|
|
27
|
+
return Promise.resolve(true)
|
|
33
28
|
}
|
|
29
|
+
return new Promise<boolean>((resolve) => {
|
|
30
|
+
const check = () => {
|
|
31
|
+
const animations = node.getAnimations()
|
|
32
|
+
if (animations.length === 0) {
|
|
33
|
+
resolve(true)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
Promise.all(animations.map((a) => a.finished))
|
|
37
|
+
.then(() => resolve(true))
|
|
38
|
+
.catch(() => {
|
|
39
|
+
const remaining = node.getAnimations()
|
|
40
|
+
if (remaining.some((a) => a.playState === 'running' || a.pending)) {
|
|
41
|
+
check()
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
resolve(false)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
// css transitions register as pending until the next style recalc, so give
|
|
48
|
+
// the browser one frame to start them before we read getAnimations.
|
|
49
|
+
if (hasRAF) {
|
|
50
|
+
requestAnimationFrame(check)
|
|
51
|
+
} else {
|
|
52
|
+
check()
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
}
|
|
34
56
|
|
|
35
|
-
|
|
36
|
-
|
|
57
|
+
const CSS_TRANSFORM_PROPERTIES: Record<string, string[]> = {
|
|
58
|
+
transform: ['translate', 'scale', 'rotate', 'transform'],
|
|
59
|
+
x: ['translate'],
|
|
60
|
+
y: ['translate'],
|
|
61
|
+
scale: ['scale'],
|
|
62
|
+
scaleX: ['scale'],
|
|
63
|
+
scaleY: ['scale'],
|
|
64
|
+
rotate: ['rotate'],
|
|
65
|
+
rotateX: ['transform'],
|
|
66
|
+
rotateY: ['transform'],
|
|
67
|
+
rotateZ: ['transform'],
|
|
68
|
+
skewX: ['transform'],
|
|
69
|
+
skewY: ['transform'],
|
|
37
70
|
}
|
|
38
71
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Apply duration override to a CSS animation string
|
|
44
|
-
* Replaces the existing duration with the override value
|
|
45
|
-
*/
|
|
46
|
-
function applyDurationOverride(animation: string, durationMs: number): string {
|
|
47
|
-
// Replace ms duration
|
|
48
|
-
const msReplaced = animation.replace(MS_DURATION_REGEX, `${durationMs}ms`)
|
|
49
|
-
if (msReplaced !== animation) {
|
|
50
|
-
return msReplaced
|
|
51
|
-
}
|
|
72
|
+
const getCSSProperties = (key: string) => {
|
|
73
|
+
return CSS_TRANSFORM_PROPERTIES[key] || [key]
|
|
74
|
+
}
|
|
52
75
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (sReplaced !== animation) {
|
|
56
|
-
return sReplaced
|
|
57
|
-
}
|
|
76
|
+
const hyphenatedPropertyCache: Record<string, string> = {}
|
|
77
|
+
const emptyProperties: string[] = []
|
|
58
78
|
|
|
59
|
-
|
|
60
|
-
return
|
|
79
|
+
function hyphenateProperty(property: string): string {
|
|
80
|
+
if (property.startsWith('--')) return property
|
|
81
|
+
return (hyphenatedPropertyCache[property] ||= property.replace(
|
|
82
|
+
/[A-Z]/g,
|
|
83
|
+
(letter) => `-${letter.toLowerCase()}`
|
|
84
|
+
))
|
|
61
85
|
}
|
|
62
86
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
'rotate',
|
|
71
|
-
'rotateX',
|
|
72
|
-
'rotateY',
|
|
73
|
-
'rotateZ',
|
|
74
|
-
'skewX',
|
|
75
|
-
'skewY',
|
|
76
|
-
] as const
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Build a CSS transform string from a style object containing transform properties
|
|
80
|
-
*/
|
|
81
|
-
function buildTransformString(style: Record<string, unknown> | undefined): string {
|
|
82
|
-
if (!style) return ''
|
|
83
|
-
|
|
84
|
-
const parts: string[] = []
|
|
85
|
-
|
|
86
|
-
if (style.x !== undefined || style.y !== undefined) {
|
|
87
|
-
const x = style.x ?? 0
|
|
88
|
-
const y = style.y ?? 0
|
|
89
|
-
parts.push(`translate(${x}px, ${y}px)`)
|
|
90
|
-
}
|
|
91
|
-
if (style.scale !== undefined) {
|
|
92
|
-
parts.push(`scale(${style.scale})`)
|
|
93
|
-
}
|
|
94
|
-
if (style.scaleX !== undefined) {
|
|
95
|
-
parts.push(`scaleX(${style.scaleX})`)
|
|
96
|
-
}
|
|
97
|
-
if (style.scaleY !== undefined) {
|
|
98
|
-
parts.push(`scaleY(${style.scaleY})`)
|
|
99
|
-
}
|
|
100
|
-
if (style.rotate !== undefined) {
|
|
101
|
-
const val = style.rotate
|
|
102
|
-
const unit = typeof val === 'string' && val.includes('deg') ? '' : 'deg'
|
|
103
|
-
parts.push(`rotate(${val}${unit})`)
|
|
104
|
-
}
|
|
105
|
-
if (style.rotateX !== undefined) {
|
|
106
|
-
parts.push(`rotateX(${style.rotateX}deg)`)
|
|
107
|
-
}
|
|
108
|
-
if (style.rotateY !== undefined) {
|
|
109
|
-
parts.push(`rotateY(${style.rotateY}deg)`)
|
|
110
|
-
}
|
|
111
|
-
if (style.rotateZ !== undefined) {
|
|
112
|
-
parts.push(`rotateZ(${style.rotateZ}deg)`)
|
|
113
|
-
}
|
|
114
|
-
if (style.skewX !== undefined) {
|
|
115
|
-
parts.push(`skewX(${style.skewX}deg)`)
|
|
116
|
-
}
|
|
117
|
-
if (style.skewY !== undefined) {
|
|
118
|
-
parts.push(`skewY(${style.skewY}deg)`)
|
|
87
|
+
function getLifecycleCSSProperties(keys: Set<string> | undefined): string[] {
|
|
88
|
+
if (!keys?.size) return emptyProperties
|
|
89
|
+
const properties = new Set<string>()
|
|
90
|
+
for (const key of keys) {
|
|
91
|
+
for (const property of getCSSProperties(key)) {
|
|
92
|
+
properties.add(hyphenateProperty(property))
|
|
93
|
+
}
|
|
119
94
|
}
|
|
120
|
-
|
|
121
|
-
return parts.join(' ')
|
|
95
|
+
return [...properties].sort()
|
|
122
96
|
}
|
|
123
97
|
|
|
124
|
-
|
|
125
|
-
* Apply a style object to a DOM node, handling transform keys specially
|
|
126
|
-
*/
|
|
127
|
-
function applyStylesToNode(
|
|
98
|
+
function readComputedProperties(
|
|
128
99
|
node: HTMLElement,
|
|
129
|
-
|
|
130
|
-
):
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
node.style.transform = transformStr
|
|
100
|
+
properties: readonly string[]
|
|
101
|
+
): Record<string, string> {
|
|
102
|
+
const computed = getComputedStyle(node)
|
|
103
|
+
const values: Record<string, string> = {}
|
|
104
|
+
for (const property of properties) {
|
|
105
|
+
const value = computed.getPropertyValue(property)
|
|
106
|
+
if (value) values[property] = value
|
|
137
107
|
}
|
|
108
|
+
return values
|
|
109
|
+
}
|
|
138
110
|
|
|
139
|
-
|
|
140
|
-
for (const
|
|
141
|
-
|
|
142
|
-
if (value === undefined) continue
|
|
143
|
-
|
|
144
|
-
if (key === 'opacity') {
|
|
145
|
-
node.style.opacity = String(value)
|
|
146
|
-
} else if (key === 'backgroundColor') {
|
|
147
|
-
node.style.backgroundColor = String(value)
|
|
148
|
-
} else if (key === 'color') {
|
|
149
|
-
node.style.color = String(value)
|
|
150
|
-
} else {
|
|
151
|
-
// generic fallback
|
|
152
|
-
node.style[key as any] = typeof value === 'number' ? `${value}px` : String(value)
|
|
153
|
-
}
|
|
111
|
+
function applyCSSProperties(node: HTMLElement, values: Record<string, string>): void {
|
|
112
|
+
for (const property in values) {
|
|
113
|
+
node.style.setProperty(property, values[property])
|
|
154
114
|
}
|
|
155
115
|
}
|
|
156
116
|
|
|
157
|
-
|
|
158
|
-
const
|
|
117
|
+
function clearCSSProperties(node: HTMLElement, properties: readonly string[]): void {
|
|
118
|
+
for (const property of properties) {
|
|
119
|
+
node.style.removeProperty(property)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
159
122
|
|
|
123
|
+
export function createAnimations<A extends AnimationsConfig>(
|
|
124
|
+
animations: A
|
|
125
|
+
): AnimationDriver<A> {
|
|
160
126
|
return {
|
|
161
127
|
animations,
|
|
162
128
|
usePresence,
|
|
@@ -164,80 +130,6 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
164
130
|
inputStyle: 'css',
|
|
165
131
|
outputStyle: 'css',
|
|
166
132
|
|
|
167
|
-
useAnimatedNumber(initial): UniversalAnimatedNumber<Function> {
|
|
168
|
-
const [val, setVal] = React.useState(initial)
|
|
169
|
-
const finishTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
getInstance() {
|
|
173
|
-
return setVal
|
|
174
|
-
},
|
|
175
|
-
getValue() {
|
|
176
|
-
return val
|
|
177
|
-
},
|
|
178
|
-
setValue(next, config, onFinish) {
|
|
179
|
-
setVal(next)
|
|
180
|
-
|
|
181
|
-
// clear any pending finish callback from a previous setValue
|
|
182
|
-
if (finishTimerRef.current) {
|
|
183
|
-
clearTimeout(finishTimerRef.current)
|
|
184
|
-
finishTimerRef.current = null
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (onFinish) {
|
|
188
|
-
if (
|
|
189
|
-
!config ||
|
|
190
|
-
config.type === 'direct' ||
|
|
191
|
-
(config.type === 'timing' && config.duration === 0)
|
|
192
|
-
) {
|
|
193
|
-
onFinish()
|
|
194
|
-
} else {
|
|
195
|
-
// estimate duration: use explicit duration, or fall back to
|
|
196
|
-
// default CSS transition duration for spring-type configs
|
|
197
|
-
const duration = config.type === 'timing' ? config.duration : 300
|
|
198
|
-
finishTimerRef.current = setTimeout(onFinish, duration)
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// call reaction listeners with the new value
|
|
203
|
-
const listeners = reactionListeners.get(setVal)
|
|
204
|
-
if (listeners) {
|
|
205
|
-
listeners.forEach((listener) => listener(next))
|
|
206
|
-
}
|
|
207
|
-
},
|
|
208
|
-
stop() {
|
|
209
|
-
if (finishTimerRef.current) {
|
|
210
|
-
clearTimeout(finishTimerRef.current)
|
|
211
|
-
finishTimerRef.current = null
|
|
212
|
-
}
|
|
213
|
-
},
|
|
214
|
-
}
|
|
215
|
-
},
|
|
216
|
-
|
|
217
|
-
useAnimatedNumberReaction({ value }, onValue) {
|
|
218
|
-
React.useEffect(() => {
|
|
219
|
-
const instance = value.getInstance()
|
|
220
|
-
let queue = reactionListeners.get(instance)
|
|
221
|
-
if (!queue) {
|
|
222
|
-
const next = new Set<Function>()
|
|
223
|
-
reactionListeners.set(instance, next)
|
|
224
|
-
queue = next!
|
|
225
|
-
}
|
|
226
|
-
queue.add(onValue)
|
|
227
|
-
return () => {
|
|
228
|
-
queue?.delete(onValue)
|
|
229
|
-
}
|
|
230
|
-
}, [])
|
|
231
|
-
},
|
|
232
|
-
|
|
233
|
-
useAnimatedNumberStyle(val, getStyle) {
|
|
234
|
-
return getStyle(val.getValue())
|
|
235
|
-
},
|
|
236
|
-
|
|
237
|
-
useAnimatedNumbersStyle(vals, getStyle) {
|
|
238
|
-
return getStyle(...vals.map((v) => v.getValue()))
|
|
239
|
-
},
|
|
240
|
-
|
|
241
133
|
// @ts-ignore - styleState is added by createComponent
|
|
242
134
|
useAnimations: ({
|
|
243
135
|
props,
|
|
@@ -246,11 +138,23 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
246
138
|
componentState,
|
|
247
139
|
stateRef,
|
|
248
140
|
styleState,
|
|
141
|
+
onTransition,
|
|
249
142
|
}: any) => {
|
|
250
143
|
const isHydrating = componentState.unmounted === true
|
|
251
144
|
const isEntering = !!componentState.unmounted
|
|
252
145
|
const isExiting = presence?.[0] === false
|
|
253
146
|
const sendExitComplete = presence?.[1]
|
|
147
|
+
const onTransitionRef = React.useRef(onTransition)
|
|
148
|
+
onTransitionRef.current = onTransition
|
|
149
|
+
const emit = (
|
|
150
|
+
phase: 'start' | 'end',
|
|
151
|
+
cause: 'enter' | 'exit' | 'update',
|
|
152
|
+
finished?: boolean
|
|
153
|
+
) => {
|
|
154
|
+
onTransitionRef.current?.(
|
|
155
|
+
phase === 'end' ? { phase, cause, finished } : { phase, cause }
|
|
156
|
+
)
|
|
157
|
+
}
|
|
254
158
|
|
|
255
159
|
// Track if we just finished entering (transition from entering to not entering)
|
|
256
160
|
// This is needed because the CSS transition happens on the render AFTER t_unmounted is removed
|
|
@@ -264,11 +168,23 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
264
168
|
const exitCycleIdRef = React.useRef(0)
|
|
265
169
|
const exitCompletedRef = React.useRef(false)
|
|
266
170
|
const wasExitingRef = React.useRef(false)
|
|
267
|
-
const exitInterruptedRef = React.useRef(false)
|
|
268
171
|
const sendExitCompleteRef = React.useRef(sendExitComplete)
|
|
269
|
-
const
|
|
172
|
+
const lastMountedStyleRef = React.useRef<Record<string, string>>({})
|
|
270
173
|
sendExitCompleteRef.current = sendExitComplete
|
|
271
174
|
|
|
175
|
+
const exitCSSProperties = getLifecycleCSSProperties(
|
|
176
|
+
styleState?.programLifecycleStyleKeys?.exit
|
|
177
|
+
)
|
|
178
|
+
const exitCSSPropertiesSignature = exitCSSProperties.join('\0')
|
|
179
|
+
|
|
180
|
+
// onTransition lifecycle bookkeeping (independent from presence completion)
|
|
181
|
+
const enterCycleIdRef = React.useRef(0)
|
|
182
|
+
const enterStartedRef = React.useRef(false)
|
|
183
|
+
const updateCycleIdRef = React.useRef(0)
|
|
184
|
+
const updateInFlightRef = React.useRef(false)
|
|
185
|
+
const prevUpdateSigRef = React.useRef<string | null>(null)
|
|
186
|
+
const exitStartedRef = React.useRef(false)
|
|
187
|
+
|
|
272
188
|
// detect transition into/out of exiting state
|
|
273
189
|
const justStartedExiting = isExiting && !wasExitingRef.current
|
|
274
190
|
const justStoppedExiting = !isExiting && wasExitingRef.current
|
|
@@ -278,10 +194,8 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
278
194
|
exitCycleIdRef.current++
|
|
279
195
|
exitCompletedRef.current = false
|
|
280
196
|
}
|
|
281
|
-
// track interruptions so we know to force-restart transitions
|
|
282
197
|
if (justStoppedExiting) {
|
|
283
198
|
exitCycleIdRef.current++
|
|
284
|
-
exitInterruptedRef.current = true
|
|
285
199
|
}
|
|
286
200
|
|
|
287
201
|
// track previous exiting state
|
|
@@ -289,58 +203,59 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
289
203
|
wasExitingRef.current = isExiting
|
|
290
204
|
})
|
|
291
205
|
|
|
206
|
+
// Snapshot the actual mounted CSS values for every property with an exit
|
|
207
|
+
// clause. Most program values live in generated classes rather than the
|
|
208
|
+
// inline `style` object, so computed style is the only complete source.
|
|
209
|
+
// The snapshot becomes the reset point for normal and interrupted exits.
|
|
292
210
|
useIsomorphicLayoutEffect(() => {
|
|
211
|
+
if (isExiting) return
|
|
293
212
|
const host = stateRef.current.host
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
opacity: computedStyle.opacity,
|
|
213
|
+
if (!host || !exitCSSProperties.length) {
|
|
214
|
+
lastMountedStyleRef.current = {}
|
|
215
|
+
return
|
|
298
216
|
}
|
|
299
|
-
|
|
217
|
+
const node = host as HTMLElement
|
|
218
|
+
const capture = () => {
|
|
219
|
+
if (stateRef.current.host !== node || wasExitingRef.current) return
|
|
220
|
+
lastMountedStyleRef.current = readComputedProperties(node, exitCSSProperties)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// The first mounted layout effect can run while an enter clause is still
|
|
224
|
+
// active. Capture again after that concrete transition settles so a later
|
|
225
|
+
// exit restarts from the mounted value, not the enter value.
|
|
226
|
+
if (justFinishedEntering) {
|
|
227
|
+
void waitForAnimations(node).then(capture)
|
|
228
|
+
} else {
|
|
229
|
+
capture()
|
|
230
|
+
}
|
|
231
|
+
}, [isExiting, justFinishedEntering, exitCSSPropertiesSignature])
|
|
300
232
|
|
|
301
233
|
// use effectiveTransition computed by createComponent (single source of truth)
|
|
302
234
|
const effectiveTransition = styleState?.effectiveTransition ?? props.transition
|
|
303
235
|
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// Determine animation state and get effective animation
|
|
308
|
-
// Use 'enter' if we're entering OR if we just finished entering (transition is happening)
|
|
236
|
+
// one parser, one preset lookup, one spring solver, shared with the other
|
|
237
|
+
// three drivers. this driver decides nothing about what a transition means.
|
|
309
238
|
const animationState = isExiting
|
|
310
239
|
? 'exit'
|
|
311
240
|
: isEntering || justFinishedEntering
|
|
312
241
|
? 'enter'
|
|
313
242
|
: 'default'
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
//
|
|
322
|
-
// -
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
let keys: string[]
|
|
331
|
-
if (props.animateOnly) {
|
|
332
|
-
// animateOnly is explicit filter
|
|
333
|
-
keys = props.animateOnly
|
|
334
|
-
} else if (hasPerPropertyConfigs && !hasDefault) {
|
|
335
|
-
// object format without default: { opacity: '200ms' } = only animate opacity
|
|
336
|
-
keys = animatedProperties
|
|
337
|
-
} else if (hasPerPropertyConfigs && hasDefault) {
|
|
338
|
-
// array format or object with default: 'all' first, then per-property overrides
|
|
339
|
-
// CSS transition specificity: later declarations override earlier ones for the same property
|
|
340
|
-
keys = ['all', ...animatedProperties]
|
|
341
|
-
} else {
|
|
342
|
-
// simple string format: 'quick' = animate all
|
|
343
|
-
keys = ['all']
|
|
243
|
+
const resolved = forAnimationState(
|
|
244
|
+
resolveTransition(effectiveTransition, {
|
|
245
|
+
animations: animations as Record<string, unknown>,
|
|
246
|
+
}),
|
|
247
|
+
animationState
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
// the entries stand as authored; the browser resolves them under css
|
|
251
|
+
// last-wins, and narrowing to a property list is `transitionProperty`
|
|
252
|
+
const hasKeys = hasTransition(resolved)
|
|
253
|
+
|
|
254
|
+
let transition: string | undefined
|
|
255
|
+
const getTransition = () => {
|
|
256
|
+
if (transition !== undefined) return transition
|
|
257
|
+
transition = toCSSTransition(resolved) ?? ''
|
|
258
|
+
return transition
|
|
344
259
|
}
|
|
345
260
|
|
|
346
261
|
useIsomorphicLayoutEffect(() => {
|
|
@@ -351,235 +266,68 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
351
266
|
// capture current cycle id for this effect
|
|
352
267
|
const cycleId = exitCycleIdRef.current
|
|
353
268
|
|
|
354
|
-
//
|
|
355
|
-
|
|
269
|
+
// emit exit start once per cycle
|
|
270
|
+
if (!exitStartedRef.current) {
|
|
271
|
+
exitStartedRef.current = true
|
|
272
|
+
emit('start', 'exit')
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// helper to complete exit with guards. the exit 'end' event fires
|
|
276
|
+
// immediately before presence safeToRemove so users can observe exit
|
|
277
|
+
// completion without reaching into presence internals.
|
|
278
|
+
const completeExit = (finished = true) => {
|
|
356
279
|
if (cycleId !== exitCycleIdRef.current) return
|
|
357
280
|
if (exitCompletedRef.current) return
|
|
358
281
|
exitCompletedRef.current = true
|
|
282
|
+
if (exitStartedRef.current) {
|
|
283
|
+
exitStartedRef.current = false
|
|
284
|
+
emit('end', 'exit', finished)
|
|
285
|
+
}
|
|
359
286
|
sendExitCompleteRef.current?.()
|
|
360
287
|
}
|
|
361
288
|
|
|
362
|
-
// if no properties
|
|
363
|
-
if (
|
|
289
|
+
// if no properties animate (`transition="none"`), complete immediately
|
|
290
|
+
if (!hasKeys) {
|
|
364
291
|
completeExit()
|
|
365
292
|
return
|
|
366
293
|
}
|
|
367
294
|
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
// 1. Reset to non-exit state
|
|
372
|
-
// 2. Force reflow
|
|
373
|
-
// 3. Re-apply exit state to trigger transition
|
|
295
|
+
// React can apply the exit class in the same render batch as the
|
|
296
|
+
// transition. Restart from the last mounted computed values so normal
|
|
297
|
+
// and interrupted exits both produce a concrete browser transition.
|
|
374
298
|
let rafId: number | undefined
|
|
375
|
-
|
|
376
|
-
// flag to ignore transitioncancel during reset (we intentionally cancel the old transition)
|
|
377
|
-
let ignoreCancelEvents = wasInterrupted
|
|
378
|
-
// get enter/exit styles for potential restart
|
|
379
|
-
const enterStyle = props.enterStyle as Record<string, unknown> | undefined
|
|
380
|
-
const exitStyle = props.exitStyle as Record<string, unknown> | undefined
|
|
381
|
-
|
|
382
|
-
// Build the exit transition string - needed for both normal and interrupted exits
|
|
383
|
-
const delayStr = normalized.delay ? ` ${normalized.delay}ms` : ''
|
|
384
|
-
const durationOverride = normalized.config?.duration
|
|
385
|
-
const exitTransitionString = keys
|
|
386
|
-
.map((key) => {
|
|
387
|
-
const propAnimation = normalized.properties[key]
|
|
388
|
-
let animationValue: string | null = null
|
|
389
|
-
if (typeof propAnimation === 'string') {
|
|
390
|
-
animationValue = animations[propAnimation]
|
|
391
|
-
} else if (
|
|
392
|
-
propAnimation &&
|
|
393
|
-
typeof propAnimation === 'object' &&
|
|
394
|
-
propAnimation.type
|
|
395
|
-
) {
|
|
396
|
-
animationValue = animations[propAnimation.type]
|
|
397
|
-
} else if (defaultAnimation) {
|
|
398
|
-
animationValue = defaultAnimation
|
|
399
|
-
}
|
|
400
|
-
if (animationValue && durationOverride) {
|
|
401
|
-
animationValue = applyDurationOverride(animationValue, durationOverride)
|
|
402
|
-
}
|
|
403
|
-
return animationValue ? `${key} ${animationValue}${delayStr}` : null
|
|
404
|
-
})
|
|
405
|
-
.filter(Boolean)
|
|
406
|
-
.join(', ')
|
|
407
|
-
|
|
408
|
-
const getResetValue = (key: string) => {
|
|
409
|
-
if (key === 'opacity') {
|
|
410
|
-
return (
|
|
411
|
-
style?.opacity ??
|
|
412
|
-
props.opacity ??
|
|
413
|
-
lastNonExitingStyleRef.current.opacity ??
|
|
414
|
-
1
|
|
415
|
-
)
|
|
416
|
-
}
|
|
417
|
-
if (TRANSFORM_KEYS.includes(key as any)) {
|
|
418
|
-
return key === 'scale' || key === 'scaleX' || key === 'scaleY' ? 1 : 0
|
|
419
|
-
}
|
|
420
|
-
return enterStyle?.[key]
|
|
421
|
-
}
|
|
299
|
+
let disposed = false
|
|
422
300
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
301
|
+
const mountedStyle = lastMountedStyleRef.current
|
|
302
|
+
const canRestart =
|
|
303
|
+
exitCSSProperties.length > 0 && Object.keys(mountedStyle).length > 0
|
|
304
|
+
let exitTarget: Record<string, string> | undefined
|
|
305
|
+
if (canRestart) {
|
|
426
306
|
node.style.transition = 'none'
|
|
427
|
-
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const resetStyle: Record<string, unknown> = {}
|
|
432
|
-
for (const key of Object.keys(exitStyle)) {
|
|
433
|
-
const resetValue = getResetValue(key)
|
|
434
|
-
if (resetValue !== undefined) {
|
|
435
|
-
resetStyle[key] = resetValue
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
applyStylesToNode(node, resetStyle)
|
|
439
|
-
} else {
|
|
440
|
-
// fallback if no exitStyle defined
|
|
441
|
-
node.style.opacity = '1'
|
|
442
|
-
node.style.transform = 'none'
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// force reflow
|
|
307
|
+
// With transitions disabled, the exit classes expose their final
|
|
308
|
+
// targets immediately. Capture those, then restore the mounted values.
|
|
309
|
+
exitTarget = readComputedProperties(node, exitCSSProperties)
|
|
310
|
+
applyCSSProperties(node, mountedStyle)
|
|
446
311
|
void node.offsetHeight
|
|
447
|
-
} else if (exitStyle) {
|
|
448
|
-
// For normal (non-interrupted) exits, we need to ensure the CSS transition is
|
|
449
|
-
// processed by the browser BEFORE the exitStyle takes effect. The issue is that
|
|
450
|
-
// React may have already applied exitStyle in the same render batch. To fix this:
|
|
451
|
-
// 1. Disable transition and reset to non-exit state
|
|
452
|
-
// 2. Force reflow so browser processes the reset
|
|
453
|
-
// 3. Use RAF to ensure we're in a new frame
|
|
454
|
-
// 4. Re-enable transition and apply exitStyle
|
|
455
|
-
// This mirrors the interrupted exit handling approach (which also uses RAF).
|
|
456
|
-
ignoreCancelEvents = true
|
|
457
|
-
node.style.transition = 'none'
|
|
458
|
-
|
|
459
|
-
// Reset to the active/open state (not enterStyle, which may equal exitStyle).
|
|
460
|
-
// enterStyle is the "unmounted" initial state and can share values with exitStyle
|
|
461
|
-
// (e.g., both have opacity: 0). resetting to enterStyle would mean no value change
|
|
462
|
-
// when exitStyle is applied, so the CSS transition wouldn't fire.
|
|
463
|
-
const resetStyle: Record<string, unknown> = {}
|
|
464
|
-
for (const key of Object.keys(exitStyle)) {
|
|
465
|
-
const resetValue = getResetValue(key)
|
|
466
|
-
if (resetValue !== undefined) {
|
|
467
|
-
resetStyle[key] = resetValue
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
applyStylesToNode(node, resetStyle)
|
|
471
|
-
|
|
472
|
-
// Force reflow
|
|
473
|
-
void node.offsetHeight
|
|
474
|
-
|
|
475
|
-
// Use RAF to ensure transition is applied in a new frame
|
|
476
312
|
rafId = requestAnimationFrame(() => {
|
|
477
313
|
if (cycleId !== exitCycleIdRef.current) return
|
|
478
|
-
|
|
479
|
-
node.style.transition = exitTransitionString
|
|
480
|
-
// Force reflow to ensure transition is active
|
|
314
|
+
node.style.transition = getTransition()
|
|
481
315
|
void node.offsetHeight
|
|
482
|
-
|
|
483
|
-
applyStylesToNode(node, exitStyle)
|
|
484
|
-
// Re-enable cancel event handling
|
|
485
|
-
ignoreCancelEvents = false
|
|
316
|
+
applyCSSProperties(node, exitTarget!)
|
|
486
317
|
})
|
|
487
318
|
}
|
|
488
319
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
* 2. Event handlers to stop working
|
|
496
|
-
*
|
|
497
|
-
* Fix: Calculate the MAXIMUM duration across all animated properties, not just
|
|
498
|
-
* the default. With animateOnly and per-property configs, different properties
|
|
499
|
-
* can have different durations, and we need to wait for the LONGEST one.
|
|
500
|
-
*/
|
|
501
|
-
|
|
502
|
-
// calculate max duration across all animated properties
|
|
503
|
-
let maxDuration = defaultAnimation ? extractDuration(defaultAnimation) : 200
|
|
504
|
-
|
|
505
|
-
// check per-property animation durations using shared helper
|
|
506
|
-
const animationConfigs = getAnimationConfigsForKeys(
|
|
507
|
-
normalized,
|
|
508
|
-
animations as Record<string, string>,
|
|
509
|
-
keys,
|
|
510
|
-
defaultAnimation
|
|
511
|
-
)
|
|
512
|
-
for (const animationValue of animationConfigs.values()) {
|
|
513
|
-
if (animationValue) {
|
|
514
|
-
const duration = extractDuration(animationValue)
|
|
515
|
-
if (duration > maxDuration) {
|
|
516
|
-
maxDuration = duration
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const delay = normalized.delay ?? 0
|
|
522
|
-
const fallbackTimeout = maxDuration + delay
|
|
523
|
-
|
|
524
|
-
const timeoutId = setTimeout(() => {
|
|
525
|
-
completeExit()
|
|
526
|
-
}, fallbackTimeout)
|
|
527
|
-
|
|
528
|
-
// track number of transitioning properties to wait for all to finish
|
|
529
|
-
// (each property fires its own transitionend event)
|
|
530
|
-
const transitioningProps = new Set(keys)
|
|
531
|
-
let completedCount = 0
|
|
532
|
-
|
|
533
|
-
const onFinishAnimation = (event: TransitionEvent) => {
|
|
534
|
-
// only count transitions on THIS element, not bubbled from children
|
|
535
|
-
if (event.target !== node) return
|
|
536
|
-
|
|
537
|
-
// map CSS property names to our key names
|
|
538
|
-
// e.g., transitionend fires with propertyName 'transform' for scale/x/y
|
|
539
|
-
const eventProp = event.propertyName
|
|
540
|
-
if (transitioningProps.has(eventProp) || eventProp === 'all') {
|
|
541
|
-
completedCount++
|
|
542
|
-
// wait for all properties to finish
|
|
543
|
-
if (completedCount >= transitioningProps.size) {
|
|
544
|
-
clearTimeout(timeoutId)
|
|
545
|
-
completeExit()
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// on cancel, still complete (element is exiting and animation was interrupted)
|
|
551
|
-
// the guards prevent duplicate completion if this is a stale cycle
|
|
552
|
-
const onCancelAnimation = () => {
|
|
553
|
-
// ignore cancel events during reset phase (we intentionally cancel the old transition)
|
|
554
|
-
if (ignoreCancelEvents) return
|
|
555
|
-
clearTimeout(timeoutId)
|
|
556
|
-
completeExit()
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
node.addEventListener('transitionend', onFinishAnimation)
|
|
560
|
-
node.addEventListener('transitioncancel', onCancelAnimation)
|
|
561
|
-
|
|
562
|
-
// For interrupted exits, re-enable transition and re-apply exit styles
|
|
563
|
-
// This must happen AFTER listeners are set up so we catch the transitionend
|
|
564
|
-
if (wasInterrupted) {
|
|
565
|
-
rafId = requestAnimationFrame(() => {
|
|
566
|
-
if (cycleId !== exitCycleIdRef.current) return
|
|
567
|
-
// re-enable transition using the pre-built string
|
|
568
|
-
node.style.transition = exitTransitionString
|
|
569
|
-
// force reflow again
|
|
570
|
-
void node.offsetHeight
|
|
571
|
-
// now apply exit styles - this triggers the transition
|
|
572
|
-
applyStylesToNode(node, exitStyle)
|
|
573
|
-
// re-enable cancel event handling now that reset is complete
|
|
574
|
-
ignoreCancelEvents = false
|
|
575
|
-
})
|
|
576
|
-
}
|
|
320
|
+
// wait for the browser's concrete animations. this covers `all`,
|
|
321
|
+
// transform aliases, delays, and concurrent WAAPI animations without
|
|
322
|
+
// guessing property names or maintaining a duration timer.
|
|
323
|
+
void waitForAnimations(node).then((finished) => {
|
|
324
|
+
if (!disposed) completeExit(finished)
|
|
325
|
+
})
|
|
577
326
|
|
|
578
327
|
return () => {
|
|
579
|
-
|
|
328
|
+
disposed = true
|
|
580
329
|
if (rafId !== undefined) cancelAnimationFrame(rafId)
|
|
581
|
-
node
|
|
582
|
-
node.removeEventListener('transitioncancel', onCancelAnimation)
|
|
330
|
+
clearCSSProperties(node, exitCSSProperties)
|
|
583
331
|
// restore transition: the exit handling sets node.style.transition='none'
|
|
584
332
|
// directly on the DOM (bypassing React). if exit is interrupted (e.g. same-key
|
|
585
333
|
// re-entry in AnimatePresence), React won't re-apply its managed transition
|
|
@@ -587,7 +335,88 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
587
335
|
// override lets React's value take effect again.
|
|
588
336
|
node.style.transition = ''
|
|
589
337
|
}
|
|
590
|
-
}, [isExiting])
|
|
338
|
+
}, [isExiting, exitCSSPropertiesSignature])
|
|
339
|
+
|
|
340
|
+
// signature of the animatable style, so the update effect can detect
|
|
341
|
+
// in-place style changes. the css driver applies most style values as
|
|
342
|
+
// atomic classNames (not inline style), so the signature must include the
|
|
343
|
+
// className map. only computed when a listener is attached.
|
|
344
|
+
const styleSignature = onTransition
|
|
345
|
+
? (() => {
|
|
346
|
+
const { transition: _t, ...rest } = style
|
|
347
|
+
return `${JSON.stringify(styleState?.classNames ?? null)}|${JSON.stringify(rest)}`
|
|
348
|
+
})()
|
|
349
|
+
: ''
|
|
350
|
+
|
|
351
|
+
// enter lifecycle: emit start when the enter transition kicks off, end
|
|
352
|
+
// once every animation on the node finishes (getAnimations-based, resolves
|
|
353
|
+
// immediately for zero-animation elements). the promise outlives benign
|
|
354
|
+
// re-renders because it keys off the cycle id, not the effect lifetime.
|
|
355
|
+
useIsomorphicLayoutEffect(() => {
|
|
356
|
+
const host = stateRef.current.host
|
|
357
|
+
if (!onTransitionRef.current || isExiting || !justFinishedEntering || !host) {
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
const node = host as HTMLElement
|
|
361
|
+
const cycleId = ++enterCycleIdRef.current
|
|
362
|
+
enterStartedRef.current = true
|
|
363
|
+
emit('start', 'enter')
|
|
364
|
+
void waitForAnimations(node).then((finished) => {
|
|
365
|
+
if (cycleId !== enterCycleIdRef.current || !enterStartedRef.current) return
|
|
366
|
+
enterStartedRef.current = false
|
|
367
|
+
emit('end', 'enter', finished)
|
|
368
|
+
})
|
|
369
|
+
}, [justFinishedEntering, isExiting])
|
|
370
|
+
|
|
371
|
+
// update lifecycle: a style change while mounted (not entering or exiting).
|
|
372
|
+
// a new update that supersedes an in-flight one emits end(finished:false).
|
|
373
|
+
useIsomorphicLayoutEffect(() => {
|
|
374
|
+
const host = stateRef.current.host
|
|
375
|
+
if (
|
|
376
|
+
!onTransitionRef.current ||
|
|
377
|
+
isEntering ||
|
|
378
|
+
justFinishedEntering ||
|
|
379
|
+
isExiting ||
|
|
380
|
+
!host
|
|
381
|
+
) {
|
|
382
|
+
// keep the signature current so leaving enter/exit isn't seen as an update
|
|
383
|
+
prevUpdateSigRef.current = styleSignature
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
if (prevUpdateSigRef.current === null) {
|
|
387
|
+
prevUpdateSigRef.current = styleSignature
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
if (styleSignature === prevUpdateSigRef.current) return
|
|
391
|
+
prevUpdateSigRef.current = styleSignature
|
|
392
|
+
|
|
393
|
+
const node = host as HTMLElement
|
|
394
|
+
if (updateInFlightRef.current) {
|
|
395
|
+
emit('end', 'update', false)
|
|
396
|
+
}
|
|
397
|
+
updateInFlightRef.current = true
|
|
398
|
+
const cycleId = ++updateCycleIdRef.current
|
|
399
|
+
emit('start', 'update')
|
|
400
|
+
void waitForAnimations(node).then((finished) => {
|
|
401
|
+
if (cycleId !== updateCycleIdRef.current) return
|
|
402
|
+
updateInFlightRef.current = false
|
|
403
|
+
emit('end', 'update', finished)
|
|
404
|
+
})
|
|
405
|
+
}, [styleSignature, isEntering, justFinishedEntering, isExiting])
|
|
406
|
+
|
|
407
|
+
// interruption: emit a finished:false end for an enter canceled by an exit,
|
|
408
|
+
// or an exit canceled by a re-enter (before its own completion fired).
|
|
409
|
+
useIsomorphicLayoutEffect(() => {
|
|
410
|
+
if (justStartedExiting && enterStartedRef.current) {
|
|
411
|
+
enterCycleIdRef.current++
|
|
412
|
+
enterStartedRef.current = false
|
|
413
|
+
emit('end', 'enter', false)
|
|
414
|
+
}
|
|
415
|
+
if (justStoppedExiting && exitStartedRef.current && !exitCompletedRef.current) {
|
|
416
|
+
exitStartedRef.current = false
|
|
417
|
+
emit('end', 'exit', false)
|
|
418
|
+
}
|
|
419
|
+
}, [justStartedExiting, justStoppedExiting])
|
|
591
420
|
|
|
592
421
|
// tamagui doesnt even use animation output during hydration
|
|
593
422
|
if (isHydrating) {
|
|
@@ -595,7 +424,7 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
595
424
|
}
|
|
596
425
|
|
|
597
426
|
// Check if we have any animation to apply
|
|
598
|
-
if (!
|
|
427
|
+
if (!hasTransition(resolved)) {
|
|
599
428
|
return null
|
|
600
429
|
}
|
|
601
430
|
|
|
@@ -603,45 +432,13 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
603
432
|
style.transform = transformsToString(style.transform)
|
|
604
433
|
}
|
|
605
434
|
|
|
606
|
-
|
|
607
|
-
// TODO: we disabled the transform transition, because it will create issue for inverse function and animate function
|
|
608
|
-
// for non layout transform properties either use animate function or find a workaround to do it with css
|
|
609
|
-
const delayStr = normalized.delay ? ` ${normalized.delay}ms` : ''
|
|
610
|
-
const durationOverride = normalized.config?.duration
|
|
611
|
-
style.transition = keys
|
|
612
|
-
.map((key) => {
|
|
613
|
-
// Check for property-specific animation, fall back to default
|
|
614
|
-
const propAnimation = normalized.properties[key]
|
|
615
|
-
let animationValue: string | null = null
|
|
616
|
-
|
|
617
|
-
if (typeof propAnimation === 'string') {
|
|
618
|
-
animationValue = animations[propAnimation]
|
|
619
|
-
} else if (
|
|
620
|
-
propAnimation &&
|
|
621
|
-
typeof propAnimation === 'object' &&
|
|
622
|
-
propAnimation.type
|
|
623
|
-
) {
|
|
624
|
-
animationValue = animations[propAnimation.type]
|
|
625
|
-
} else if (defaultAnimation) {
|
|
626
|
-
animationValue = defaultAnimation
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// Apply global duration override if specified
|
|
630
|
-
if (animationValue && durationOverride) {
|
|
631
|
-
animationValue = applyDurationOverride(animationValue, durationOverride)
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
return animationValue ? `${key} ${animationValue}${delayStr}` : null
|
|
635
|
-
})
|
|
636
|
-
.filter(Boolean)
|
|
637
|
-
.join(', ')
|
|
435
|
+
style.transition = getTransition()
|
|
638
436
|
|
|
639
437
|
if (process.env.NODE_ENV === 'development' && props['debug'] === 'verbose') {
|
|
640
438
|
console.info('CSS animation', {
|
|
641
439
|
props,
|
|
642
440
|
animations,
|
|
643
|
-
|
|
644
|
-
defaultAnimation,
|
|
441
|
+
resolved,
|
|
645
442
|
style,
|
|
646
443
|
isEntering,
|
|
647
444
|
isExiting,
|
|
@@ -652,61 +449,3 @@ export function createAnimations<A extends object>(animations: A): AnimationDriv
|
|
|
652
449
|
},
|
|
653
450
|
}
|
|
654
451
|
}
|
|
655
|
-
|
|
656
|
-
// layout animations
|
|
657
|
-
// useIsomorphicLayoutEffect(() => {
|
|
658
|
-
// if (!host || !props.layout) {
|
|
659
|
-
// return
|
|
660
|
-
// }
|
|
661
|
-
// // @ts-ignore
|
|
662
|
-
// const boundingBox = host?.getBoundingClientRect()
|
|
663
|
-
// if (isChanged(initialPositionRef.current, boundingBox)) {
|
|
664
|
-
// const transform = invert(
|
|
665
|
-
// host,
|
|
666
|
-
// boundingBox,
|
|
667
|
-
// initialPositionRef.current
|
|
668
|
-
// )
|
|
669
|
-
|
|
670
|
-
// animate({
|
|
671
|
-
// from: transform,
|
|
672
|
-
// to: { x: 0, y: 0, scaleX: 1, scaleY: 1 },
|
|
673
|
-
// duration: 1000,
|
|
674
|
-
// onUpdate: ({ x, y, scaleX, scaleY }) => {
|
|
675
|
-
// // @ts-ignore
|
|
676
|
-
// host.style.transform = `translate(${x}px, ${y}px) scaleX(${scaleX}) scaleY(${scaleY})`
|
|
677
|
-
// // TODO: handle childRef inverse scale
|
|
678
|
-
// // childRef.current.style.transform = `scaleX(${1 / scaleX}) scaleY(${
|
|
679
|
-
// // 1 / scaleY
|
|
680
|
-
// // })`
|
|
681
|
-
// },
|
|
682
|
-
// // TODO: extract ease-in from string and convert/map it to a cubicBezier array
|
|
683
|
-
// cubicBezier: [0, 1.38, 1, -0.41],
|
|
684
|
-
// })
|
|
685
|
-
// }
|
|
686
|
-
// initialPositionRef.current = boundingBox
|
|
687
|
-
// })
|
|
688
|
-
|
|
689
|
-
// style.transition = `${keys} ${animation}${
|
|
690
|
-
// props.layout ? ',width 0s, height 0s, margin 0s, padding 0s, transform' : ''
|
|
691
|
-
// }`
|
|
692
|
-
|
|
693
|
-
// const isChanged = (initialBox: any, finalBox: any) => {
|
|
694
|
-
// // we just mounted, so we don't have complete data yet
|
|
695
|
-
// if (!initialBox || !finalBox) return false
|
|
696
|
-
|
|
697
|
-
// // deep compare the two boxes
|
|
698
|
-
// return JSON.stringify(initialBox) !== JSON.stringify(finalBox)
|
|
699
|
-
// }
|
|
700
|
-
|
|
701
|
-
// const invert = (el, from, to) => {
|
|
702
|
-
// const { x: fromX, y: fromY, width: fromWidth, height: fromHeight } = from
|
|
703
|
-
// const { x, y, width, height } = to
|
|
704
|
-
|
|
705
|
-
// const transform = {
|
|
706
|
-
// x: x - fromX - (fromWidth - width) / 2,
|
|
707
|
-
// y: y - fromY - (fromHeight - height) / 2,
|
|
708
|
-
// scaleX: width / fromWidth,
|
|
709
|
-
// scaleY: height / fromHeight,
|
|
710
|
-
// }
|
|
711
|
-
|
|
712
|
-
// el.style.transform = `
|