mirror of https://github.com/sveltejs/svelte
fix: measure nested transitions before applying their starting styles (#18647)
Fixes #18421 (regression from #16035, which re-broke the #4784 scenario). Since #16035 the dummy delay animation runs with `fill: 'forwards'`, so an intro's `t = 0` styles (for `slide`, `height: 0`) apply the moment the transition is created and stay applied until the real animation starts a frame later. Transition functions of other elements in the same batch run after that and measure a collapsed DOM. With nested intros (the TreeView from the issue), intro effects run deepest-first, so every ancestor's `slide` captures a height where its children are pinned at 0. On the issue's REPL every `<ul>` animates `0px -> 36px` (the height of a single row) and then jumps to its real size when the animation finishes, which is the "children pop out" behavior from #4784. The fix waits one microtask before applying the initial styles and creating the dummy, the same way deferred transitions (`crossfade`) already work in `animate()`. Everything in the batch measures the DOM first; the microtask still runs before the next paint, so elements never render unstyled and the #14732 behavior is preserved. Verified in a real browser that a delayed intro (`delay: 300`) stays held at `height: 0` for the whole delay, including the first painted frame. Most of the diff is the existing dummy/onfinish block moving into `queue_micro_task`; hiding whitespace shows the actual change.pull/18721/head
parent
c7d8233a5c
commit
955b701df2
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: measure nested transitions before applying their starting styles
|
||||
@ -0,0 +1,14 @@
|
||||
<script>
|
||||
import Nested from './Nested.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
let { depth } = $props();
|
||||
</script>
|
||||
|
||||
<div class="level-{depth}" in:slide|global={{ duration: 100 }}>
|
||||
{#if depth > 0}
|
||||
<Nested depth={depth - 1} />
|
||||
{:else}
|
||||
<div style="height: 100px">leaf</div>
|
||||
{/if}
|
||||
</div>
|
||||
@ -0,0 +1,36 @@
|
||||
import { test } from '../../assert';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
const button = target.querySelector('button');
|
||||
button?.click();
|
||||
|
||||
// wait for the transition's keyframes to be created
|
||||
const animation = await new Promise((resolve, reject) => {
|
||||
const start = performance.now();
|
||||
|
||||
function check() {
|
||||
const outer = target.querySelector('.level-2');
|
||||
const animation = outer
|
||||
?.getAnimations()
|
||||
.find((a) => a.effect?.getTiming().duration === 100);
|
||||
|
||||
if (animation) {
|
||||
resolve(animation);
|
||||
} else if (performance.now() - start > 2000) {
|
||||
reject(new Error('timed out waiting for the transition to start'));
|
||||
} else {
|
||||
requestAnimationFrame(check);
|
||||
}
|
||||
}
|
||||
|
||||
check();
|
||||
});
|
||||
|
||||
// the outermost `slide` must have measured the element with its
|
||||
// descendants at their natural size, not collapsed to zero by their
|
||||
// own starting styles (#18421)
|
||||
const keyframes = animation.effect?.getKeyframes() ?? [];
|
||||
assert.equal(keyframes[keyframes.length - 1].height, '100px');
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
<script>
|
||||
import Nested from './Nested.svelte';
|
||||
|
||||
let visible = $state(false);
|
||||
</script>
|
||||
|
||||
<button onclick={() => (visible = !visible)}>toggle</button>
|
||||
|
||||
{#if visible}
|
||||
<Nested depth={2} />
|
||||
{/if}
|
||||
Loading…
Reference in new issue