pull/18431/merge
Nic Polumeyv 3 days ago committed by GitHub
commit 28877338bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't resurrect outroing elements when an ancestor block is paused and resumed

@ -21,6 +21,13 @@ export const BOUNDARY_EFFECT = 1 << 7;
* no dependents, we can disconnect it from the graph, allowing it to either be
* GC'd or reconnected later if an effect comes to depend on it again
*/
/**
* Set on the effect that `pause_effect` was called on, i.e. the root of a paused subtree,
* as opposed to its descendants which are merely `INERT`. This allows `resume_effect` on
* an ancestor to skip subtrees that were paused for their own reasons (such as a block
* whose condition is still false) rather than resurrecting them
*/
export const PAUSED = 1 << 8;
export const CONNECTED = 1 << 9;
export const CLEAN = 1 << 10;
export const DIRTY = 1 << 11;

@ -34,7 +34,8 @@ import {
ASYNC,
CONNECTED,
MANAGED_EFFECT,
DESTROYING
DESTROYING,
PAUSED
} from '#client/constants';
import * as e from '../errors.js';
import { DEV } from 'esm-env';
@ -610,6 +611,7 @@ export function pause_effect(effect, callback, destroy = true) {
/** @type {TransitionManager[]} */
var transitions = [];
effect.f |= PAUSED;
pause_children(effect, transitions, true);
var fn = () => {
@ -677,6 +679,7 @@ function pause_children(effect, transitions, local) {
* @param {Effect} effect
*/
export function resume_effect(effect) {
effect.f &= ~PAUSED;
resume_children(effect, true);
}
@ -685,6 +688,10 @@ export function resume_effect(effect) {
* @param {boolean} local
*/
function resume_children(effect, local) {
// this subtree was paused for its own reasons (e.g. a block whose condition
// is still false) — its controller will resume or destroy it
if ((effect.f & PAUSED) !== 0) return;
if ((effect.f & INERT) === 0) return;
effect.f ^= INERT;

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
// #15604 — a removed each item mid-outro must not be resurrected by an ancestor pause/resume
export default test({
test({ assert, target, raf }) {
const [remove, fetch] = target.querySelectorAll('button');
raf.tick(200);
assert.equal(target.querySelectorAll('.item').length, 3);
flushSync(() => remove.click());
flushSync(() => fetch.click());
raf.tick(30);
flushSync(() => fetch.click());
raf.tick(2000);
const items = [...target.querySelectorAll('.item')].map((el) => el.textContent);
assert.deepEqual(items, ['a', 'c']);
}
});

@ -0,0 +1,19 @@
<script>
import { fade } from 'svelte/transition';
let fetching = $state(false);
let items = $state(['a', 'b', 'c']);
</script>
<button onclick={() => (items = items.filter((i) => i !== 'b'))}>remove</button>
<button onclick={() => (fetching = !fetching)}>fetch</button>
{#if fetching}
<p>loading</p>
{:else}
<div transition:fade={{ duration: 100 }}>
{#each items as item (item)}
<div class="item" transition:fade|global={{ duration: 100 }}>{item}</div>
{/each}
</div>
{/if}

@ -0,0 +1,24 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
// #15604 companion — flipping the condition back while the ancestor is paused must still revive the element
export default test({
test({ assert, target, raf }) {
const [toggle, fetch] = target.querySelectorAll('button');
raf.tick(200);
assert.ok(target.querySelector('.red'));
flushSync(() => toggle.click());
flushSync(() => fetch.click());
raf.tick(30);
flushSync(() => toggle.click());
flushSync(() => fetch.click());
raf.tick(2000);
assert.ok(target.querySelector('.red'));
}
});

@ -0,0 +1,19 @@
<script>
import { fade } from 'svelte/transition';
let fetching = $state(false);
let shown = $state(true);
</script>
<button onclick={() => (shown = !shown)}>toggle</button>
<button onclick={() => (fetching = !fetching)}>fetch</button>
{#if fetching}
<p>loading</p>
{:else}
<div transition:fade={{ duration: 100 }}>
{#if shown}
<div class="red" transition:fade|global={{ duration: 100 }}>red</div>
{/if}
</div>
{/if}

@ -0,0 +1,29 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
// #15604 — an element mid-outro must not be resurrected when an ancestor
// block is paused and resumed while the element's own condition is still false.
// The outer `transition:fade` matters: it keeps the outer branch alive (pending
// its own outro) so that toggling back takes the resume path
export default test({
test({ assert, target, raf }) {
const [hide, fetch] = target.querySelectorAll('button');
// let the mount intro (dom variant) finish
raf.tick(200);
assert.ok(target.querySelector('.red'));
// start the outro of the inner block
flushSync(() => hide.click());
// pause the outer block while the outro is in flight...
flushSync(() => fetch.click());
raf.tick(250);
// ...then resume it before either outro completes
flushSync(() => fetch.click());
raf.tick(2000);
assert.equal(target.querySelector('.red'), null);
}
});

@ -0,0 +1,19 @@
<script>
import { fade } from 'svelte/transition';
let fetching = $state(false);
let shown = $state(true);
</script>
<button onclick={() => (shown = false)}>hide</button>
<button onclick={() => (fetching = !fetching)}>fetch</button>
{#if fetching}
<p>loading</p>
{:else}
<div transition:fade={{ duration: 100 }}>
{#if shown}
<div class="red" transition:fade|global={{ duration: 100 }}>red</div>
{/if}
</div>
{/if}
Loading…
Cancel
Save