|
1 | 1 | import { useRef, useState, type MutableRefObject } from 'react' |
2 | 2 | import { disposables } from '../utils/disposables' |
3 | | -import { once } from '../utils/once' |
4 | 3 | import { useDisposables } from './use-disposables' |
5 | 4 | import { useFlags } from './use-flags' |
6 | 5 | import { useIsoMorphicEffect } from './use-iso-morphic-effect' |
@@ -211,84 +210,43 @@ function transition( |
211 | 210 | // This means that no transition happens at all. To fix this, we delay the |
212 | 211 | // actual transition by one frame. |
213 | 212 | d.nextFrame(() => { |
214 | | - // Wait for the transition, once the transition is complete we can cleanup. |
215 | | - // This is registered first to prevent race conditions, otherwise it could |
216 | | - // happen that the transition is already done before we start waiting for |
217 | | - // the actual event. |
218 | | - d.add(waitForTransition(node, done)) |
219 | | - |
220 | 213 | // Initiate the transition by applying the new classes. |
221 | 214 | run() |
| 215 | + |
| 216 | + // Wait for the transition, once the transition is complete we can cleanup. |
| 217 | + // We wait for a frame such that the browser has time to flush the changes |
| 218 | + // to the DOM. |
| 219 | + d.requestAnimationFrame(() => { |
| 220 | + d.add(waitForTransition(node, done)) |
| 221 | + }) |
222 | 222 | }) |
223 | 223 |
|
224 | 224 | return d.dispose |
225 | 225 | } |
226 | 226 |
|
227 | | -function waitForTransition(node: HTMLElement, _done: () => void) { |
228 | | - let done = once(_done) |
| 227 | +function waitForTransition(node: HTMLElement | null, done: () => void) { |
229 | 228 | let d = disposables() |
230 | | - |
231 | 229 | if (!node) return d.dispose |
232 | 230 |
|
233 | | - // Safari returns a comma separated list of values, so let's sort them and take the highest value. |
234 | | - let { transitionDuration, transitionDelay } = getComputedStyle(node) |
235 | | - |
236 | | - let [durationMs, delayMs] = [transitionDuration, transitionDelay].map((value) => { |
237 | | - let [resolvedValue = 0] = value |
238 | | - .split(',') |
239 | | - // Remove falsy we can't work with |
240 | | - .filter(Boolean) |
241 | | - // Values are returned as `0.3s` or `75ms` |
242 | | - .map((v) => (v.includes('ms') ? parseFloat(v) : parseFloat(v) * 1000)) |
243 | | - .sort((a, z) => z - a) |
244 | | - |
245 | | - return resolvedValue |
| 231 | + let cancelled = false |
| 232 | + d.add(() => { |
| 233 | + cancelled = true |
246 | 234 | }) |
247 | 235 |
|
248 | | - let totalDuration = durationMs + delayMs |
249 | | - |
250 | | - if (totalDuration !== 0) { |
251 | | - if (process.env.NODE_ENV === 'test') { |
252 | | - let dispose = d.setTimeout(() => { |
253 | | - done() |
254 | | - dispose() |
255 | | - }, totalDuration) |
256 | | - } else { |
257 | | - let disposeGroup = d.group((d) => { |
258 | | - // Mark the transition as done when the timeout is reached. This is a fallback in case the |
259 | | - // transitionrun event is not fired. |
260 | | - let cancelTimeout = d.setTimeout(() => { |
261 | | - done() |
262 | | - d.dispose() |
263 | | - }, totalDuration) |
264 | | - |
265 | | - // The moment the transitionrun event fires, we should cleanup the timeout fallback, because |
266 | | - // then we know that we can use the native transition events because something is |
267 | | - // transitioning. |
268 | | - d.addEventListener(node, 'transitionrun', (event) => { |
269 | | - if (event.target !== event.currentTarget) return |
270 | | - cancelTimeout() |
271 | | - |
272 | | - d.addEventListener(node, 'transitioncancel', (event) => { |
273 | | - if (event.target !== event.currentTarget) return |
274 | | - done() |
275 | | - disposeGroup() |
276 | | - }) |
277 | | - }) |
278 | | - }) |
279 | | - |
280 | | - d.addEventListener(node, 'transitionend', (event) => { |
281 | | - if (event.target !== event.currentTarget) return |
282 | | - done() |
283 | | - d.dispose() |
284 | | - }) |
285 | | - } |
286 | | - } else { |
287 | | - // No transition is happening, so we should cleanup already. Otherwise we have to wait until we |
288 | | - // get disposed. |
| 236 | + let transitions = node.getAnimations().filter((animation) => animation instanceof CSSTransition) |
| 237 | + // If there are no transitions, we can stop early. |
| 238 | + if (transitions.length === 0) { |
289 | 239 | done() |
| 240 | + return d.dispose |
290 | 241 | } |
291 | 242 |
|
| 243 | + // Wait for all the transitions to complete. |
| 244 | + Promise.allSettled(transitions.map((transition) => transition.finished)).then(() => { |
| 245 | + if (!cancelled) { |
| 246 | + done() |
| 247 | + } |
| 248 | + }) |
| 249 | + |
292 | 250 | return d.dispose |
293 | 251 | } |
294 | 252 |
|
|
0 commit comments