Skip to content

Commit 203c228

Browse files
fix: don't cancel transition of already outroing elements (#17186)
* fix: don't cancel transition of already outroing elements #16977 forgot one detail: While an element is outroing, the block of e.g. an if block can be triggered again, resolving to the same condition. In that case we have an in-between state where the element is still onscreen but already outroing. We have to detect this to not eagerly destroy the corresponding effect when we arrive at the outro/destroy logic. Fixes #16982 * fix --------- Co-authored-by: Rich Harris <rich.harris@vercel.com>
1 parent ebb97a6 commit 203c228

File tree

4 files changed

+87
-3
lines changed

4 files changed

+87
-3
lines changed

.changeset/shiny-otters-learn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: don't cancel transition of already outroing elements

packages/svelte/src/internal/client/dom/blocks/branches.js

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,35 @@ export class BranchManager {
2424
/** @type {Map<Batch, Key>} */
2525
#batches = new Map();
2626

27-
/** @type {Map<Key, Effect>} */
27+
/**
28+
* Map of keys to effects that are currently rendered in the DOM.
29+
* These effects are visible and actively part of the document tree.
30+
* Example:
31+
* ```
32+
* {#if condition}
33+
* foo
34+
* {:else}
35+
* bar
36+
* {/if}
37+
* ```
38+
* Can result in the entries `true->Effect` and `false->Effect`
39+
* @type {Map<Key, Effect>}
40+
*/
2841
#onscreen = new Map();
2942

30-
/** @type {Map<Key, Branch>} */
43+
/**
44+
* Similar to #onscreen with respect to the keys, but contains branches that are not yet
45+
* in the DOM, because their insertion is deferred.
46+
* @type {Map<Key, Branch>}
47+
*/
3148
#offscreen = new Map();
3249

50+
/**
51+
* Keys of effects that are currently outroing
52+
* @type {Set<Key>}
53+
*/
54+
#outroing = new Set();
55+
3356
/**
3457
* Whether to pause (i.e. outro) on change, or destroy immediately.
3558
* This is necessary for `<svelte:element>`
@@ -58,6 +81,7 @@ export class BranchManager {
5881
if (onscreen) {
5982
// effect is already in the DOM — abort any current outro
6083
resume_effect(onscreen);
84+
this.#outroing.delete(key);
6185
} else {
6286
// effect is currently offscreen. put it in the DOM
6387
var offscreen = this.#offscreen.get(key);
@@ -96,7 +120,8 @@ export class BranchManager {
96120
// outro/destroy all onscreen effects...
97121
for (const [k, effect] of this.#onscreen) {
98122
// ...except the one that was just committed
99-
if (k === key) continue;
123+
// or those that are already outroing (else the transition is aborted and the effect destroyed right away)
124+
if (k === key || this.#outroing.has(k)) continue;
100125

101126
const on_destroy = () => {
102127
const keys = Array.from(this.#batches.values());
@@ -113,10 +138,12 @@ export class BranchManager {
113138
destroy_effect(effect);
114139
}
115140

141+
this.#outroing.delete(k);
116142
this.#onscreen.delete(k);
117143
};
118144

119145
if (this.#transition || !onscreen) {
146+
this.#outroing.add(k);
120147
pause_effect(effect, on_destroy, false);
121148
} else {
122149
on_destroy();
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { flushSync } from '../../../../src/index-client.js';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
test({ assert, raf, target }) {
6+
const [x, y] = target.querySelectorAll('button');
7+
8+
// Set second part of condition to false first...
9+
y.click();
10+
flushSync();
11+
raf.tick(50);
12+
assert.htmlEqual(
13+
target.innerHTML,
14+
'<button>toggle x</button> <button>toggle y</button> <p foo="0.5">hello</p>'
15+
);
16+
17+
// ...so that when both are toggled the block condition runs again
18+
x.click();
19+
flushSync();
20+
assert.htmlEqual(
21+
target.innerHTML,
22+
'<button>toggle x</button> <button>toggle y</button> <p foo="0.5">hello</p>'
23+
);
24+
25+
raf.tick(100);
26+
assert.htmlEqual(target.innerHTML, '<button>toggle x</button> <button>toggle y</button>');
27+
}
28+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script>
2+
function foo(node) {
3+
return {
4+
duration: 100,
5+
tick: (t, u) => {
6+
node.setAttribute('foo', t)
7+
}
8+
};
9+
}
10+
11+
let x = $state(true);
12+
let y = $state(true);
13+
</script>
14+
15+
<button onclick={() => {
16+
x = !x;
17+
}}>toggle x</button>
18+
<button onclick={() => {
19+
y = !y;
20+
}}>toggle y</button>
21+
22+
{#if x && y}
23+
<p transition:foo>hello</p>
24+
{/if}

0 commit comments

Comments
 (0)