diff --git a/.changeset/spotty-spiders-compare.md b/.changeset/spotty-spiders-compare.md new file mode 100644 index 000000000000..36b7d98f6a90 --- /dev/null +++ b/.changeset/spotty-spiders-compare.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent transition action overfiring diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 2062095235b5..9e54c1b1f907 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -1375,7 +1375,10 @@ function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) { /** @type {null | import('./types.js').TemplateNode | Array} */ let alternate_dom = null; let has_mounted = false; - let has_mounted_branch = false; + /** + * @type {import("./types.js").EffectSignal | null} + */ + let current_branch_effect = null; const if_effect = render_effect( () => { @@ -1432,20 +1435,24 @@ function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) { ); // Managed effect const consequent_effect = render_effect( - () => { - if (consequent_dom !== null) { + ( + /** @type {any} */ _, + /** @type {import("./types.js").EffectSignal | null} */ consequent_effect + ) => { + const result = block.v; + if (!result && consequent_dom !== null) { remove(consequent_dom); consequent_dom = null; } - if (block.v) { + if (result && current_branch_effect !== consequent_effect) { consequent_fn(anchor_node); - if (!has_mounted_branch) { + if (current_branch_effect === null) { // Restore previous fragment so that Svelte continues to operate in hydration mode set_current_hydration_fragment(previous_hydration_fragment); - has_mounted_branch = true; } + current_branch_effect = consequent_effect; + consequent_dom = block.d; } - consequent_dom = block.d; block.d = null; }, block, @@ -1454,22 +1461,26 @@ function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) { block.ce = consequent_effect; // Managed effect const alternate_effect = render_effect( - () => { - if (alternate_dom !== null) { + ( + /** @type {any} */ _, + /** @type {import("./types.js").EffectSignal | null} */ alternate_effect + ) => { + const result = block.v; + if (result && alternate_dom !== null) { remove(alternate_dom); alternate_dom = null; } - if (!block.v) { + if (!result && current_branch_effect !== alternate_effect) { if (alternate_fn !== null) { alternate_fn(anchor_node); } - if (!has_mounted_branch) { + if (current_branch_effect === null) { // Restore previous fragment so that Svelte continues to operate in hydration mode set_current_hydration_fragment(previous_hydration_fragment); - has_mounted_branch = true; } + current_branch_effect = alternate_effect; + alternate_dom = block.d; } - alternate_dom = block.d; block.d = null; }, block, diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 84c24d2f7c01..fdadc96671be 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -342,9 +342,13 @@ function execute_signal_fn(signal) { try { let res; if (is_render_effect) { - res = /** @type {(block: import('./types.js').Block) => V} */ (init)( - /** @type {import('./types.js').Block} */ (signal.b) - ); + res = + /** @type {(block: import('./types.js').Block, signal: import('./types.js').Signal) => V} */ ( + init + )( + /** @type {import('./types.js').Block} */ (signal.b), + /** @type {import('./types.js').Signal} */ (signal) + ); } else { res = /** @type {() => V} */ (init)(); } diff --git a/packages/svelte/src/internal/client/transitions.js b/packages/svelte/src/internal/client/transitions.js index f2a4363a8140..9a368d054bb5 100644 --- a/packages/svelte/src/internal/client/transitions.js +++ b/packages/svelte/src/internal/client/transitions.js @@ -31,7 +31,6 @@ const DELAY_NEXT_TICK = Number.MIN_SAFE_INTEGER; /** @type {undefined | number} */ let active_tick_ref = undefined; -let skip_mount_intro = false; /** * @template T @@ -496,7 +495,7 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo can_show_intro_on_mount = true; } else if (transition_block.t === IF_BLOCK) { transition_block.r = if_block_transition; - if (can_show_intro_on_mount && !skip_mount_intro) { + if (can_show_intro_on_mount) { /** @type {import('./types.js').Block | null} */ let if_block = transition_block; while (if_block.t === IF_BLOCK) { @@ -511,14 +510,13 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo } } if (!can_apply_lazy_transitions && can_show_intro_on_mount) { - can_show_intro_on_mount = !skip_mount_intro && transition_block.e !== null; + can_show_intro_on_mount = transition_block.e !== null; } if (can_show_intro_on_mount || !global) { can_apply_lazy_transitions = true; } } else if (transition_block.t === ROOT_BLOCK && !can_apply_lazy_transitions) { - can_show_intro_on_mount = - !skip_mount_intro && (transition_block.e !== null || transition_block.i); + can_show_intro_on_mount = transition_block.e !== null || transition_block.i; } transition_block = transition_block.p; } @@ -527,14 +525,11 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo let transition; effect(() => { + let already_mounted = false; if (transition !== undefined) { + already_mounted = true; // Destroy any existing transitions first - try { - skip_mount_intro = true; - transition.x(); - } finally { - skip_mount_intro = false; - } + transition.x(); } const transition_fn = get_transition_fn(); /** @param {DOMRect} [from] */ @@ -557,7 +552,7 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo const is_intro = direction === 'in'; const show_intro = can_show_intro_on_mount && (is_intro || direction === 'both'); - if (show_intro) { + if (show_intro && !already_mounted) { transition.p = transition.i(); } @@ -565,7 +560,7 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo destroy_signal(effect); dom.inert = false; - if (show_intro) { + if (show_intro && !already_mounted) { transition.in(); } @@ -662,7 +657,8 @@ function if_block_transition(transition) { const c = /** @type {Set} */ (consequent_transitions); c.delete(transition); if (c.size === 0) { - execute_effect(/** @type {import('./types.js').EffectSignal} */ (block.ce)); + const consequent_effect = block.ce; + execute_effect(/** @type {import('./types.js').EffectSignal} */ (consequent_effect)); } }); } else { @@ -675,7 +671,8 @@ function if_block_transition(transition) { const a = /** @type {Set} */ (alternate_transitions); a.delete(transition); if (a.size === 0) { - execute_effect(/** @type {import('./types.js').EffectSignal} */ (block.ae)); + const alternate_effect = block.ae; + execute_effect(/** @type {import('./types.js').EffectSignal} */ (alternate_effect)); } }); } diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 31dd4f1162d1..2a36d1fc7b31 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -99,7 +99,11 @@ export type ComputationSignal = { /** The types that the signal represent, as a bitwise value */ f: SignalFlags; /** init: The function that we invoke for effects and computeds */ - i: null | (() => V) | (() => void | (() => void)) | ((b: Block) => void | (() => void)); + i: + | null + | (() => V) + | (() => void | (() => void)) + | ((b: Block, s: Signal) => void | (() => void)); /** references: Anything that a signal owns */ r: null | ComputationSignal[]; /** value: The latest value for this signal, doubles as the teardown for effects */ diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-transition/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-transition/_config.js index 6f25556439a8..43c0bd7f864d 100644 --- a/packages/svelte/tests/runtime-runes/samples/dynamic-transition/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-transition/_config.js @@ -20,6 +20,12 @@ export default test({ b2.click(); }); + assert.deepEqual(log, ['transition 2']); + + flushSync(() => { + b1.click(); + }); + assert.deepEqual(log, ['transition 2', 'transition 1']); } });