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