diff --git a/.changeset/shiny-otters-learn.md b/.changeset/shiny-otters-learn.md new file mode 100644 index 000000000000..8bf4a135886b --- /dev/null +++ b/.changeset/shiny-otters-learn.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't cancel transition of already outroing elements diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index f1b9baf6f633..527f0b0a8fc4 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -24,12 +24,35 @@ export class BranchManager { /** @type {Map} */ #batches = new Map(); - /** @type {Map} */ + /** + * Map of keys to effects that are currently rendered in the DOM. + * These effects are visible and actively part of the document tree. + * Example: + * ``` + * {#if condition} + * foo + * {:else} + * bar + * {/if} + * ``` + * Can result in the entries `true->Effect` and `false->Effect` + * @type {Map} + */ #onscreen = new Map(); - /** @type {Map} */ + /** + * Similar to #onscreen with respect to the keys, but contains branches that are not yet + * in the DOM, because their insertion is deferred. + * @type {Map} + */ #offscreen = new Map(); + /** + * Keys of effects that are currently outroing + * @type {Set} + */ + #outroing = new Set(); + /** * Whether to pause (i.e. outro) on change, or destroy immediately. * This is necessary for `` @@ -58,6 +81,7 @@ export class BranchManager { if (onscreen) { // effect is already in the DOM — abort any current outro resume_effect(onscreen); + this.#outroing.delete(key); } else { // effect is currently offscreen. put it in the DOM var offscreen = this.#offscreen.get(key); @@ -96,7 +120,8 @@ export class BranchManager { // outro/destroy all onscreen effects... for (const [k, effect] of this.#onscreen) { // ...except the one that was just committed - if (k === key) continue; + // or those that are already outroing (else the transition is aborted and the effect destroyed right away) + if (k === key || this.#outroing.has(k)) continue; const on_destroy = () => { const keys = Array.from(this.#batches.values()); @@ -113,10 +138,12 @@ export class BranchManager { destroy_effect(effect); } + this.#outroing.delete(k); this.#onscreen.delete(k); }; if (this.#transition || !onscreen) { + this.#outroing.add(k); pause_effect(effect, on_destroy, false); } else { on_destroy(); diff --git a/packages/svelte/tests/runtime-runes/samples/transition-if/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-if/_config.js new file mode 100644 index 000000000000..7cabc36163c0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-if/_config.js @@ -0,0 +1,28 @@ +import { flushSync } from '../../../../src/index-client.js'; +import { test } from '../../test'; + +export default test({ + test({ assert, raf, target }) { + const [x, y] = target.querySelectorAll('button'); + + // Set second part of condition to false first... + y.click(); + flushSync(); + raf.tick(50); + assert.htmlEqual( + target.innerHTML, + '

hello

' + ); + + // ...so that when both are toggled the block condition runs again + x.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + '

hello

' + ); + + raf.tick(100); + assert.htmlEqual(target.innerHTML, ' '); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/transition-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/transition-if/main.svelte new file mode 100644 index 000000000000..b60c6f22eb4c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-if/main.svelte @@ -0,0 +1,24 @@ + + + + + +{#if x && y} +

hello

+{/if}