fix: do not dispatch transition event with animation (#18122)

closes #18056
related: #17567 and #14009

# Changes

move `dispatch_event()` calls in `transitions.js` out of `animate()`
function using an additional `on_begin()` callback parameter. Doing so
makes it possible to dispatch the `introstart` and `outrostart` events
only from `transition()`.

# Testing

add a test checking that svelte dispatches no event when it runs an
animation
pull/18125/head
dabund24 5 months ago committed by GitHub
parent 48dc9b40c0
commit 51736e576d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: do not dispatch introstart event with animation of animate directive

@ -115,10 +115,17 @@ export function animation(element, get_fn, get_params) {
) { ) {
const options = get_fn()(this.element, { from, to }, get_params?.()); const options = get_fn()(this.element, { from, to }, get_params?.());
animation = animate(this.element, options, undefined, 1, () => { animation = animate(
animation?.abort(); this.element,
animation = undefined; options,
}); undefined,
1,
() => {},
() => {
animation?.abort();
animation = undefined;
}
);
} }
}, },
fix() { fix() {
@ -239,15 +246,24 @@ export function transition(flags, element, get_fn, get_params) {
intro?.abort(); intro?.abort();
} }
intro = animate(element, get_options(), outro, 1, () => { intro = animate(
dispatch_event(element, 'introend'); element,
get_options(),
// Ensure we cancel the animation to prevent leaking outro,
intro?.abort(); 1,
intro = current_options = undefined; () => {
dispatch_event(element, 'introstart');
element.style.overflow = overflow; },
}); () => {
dispatch_event(element, 'introend');
// Ensure we cancel the animation to prevent leaking
intro?.abort();
intro = current_options = undefined;
element.style.overflow = overflow;
}
);
}, },
out(fn) { out(fn) {
if (!is_outro) { if (!is_outro) {
@ -258,10 +274,19 @@ export function transition(flags, element, get_fn, get_params) {
element.inert = true; element.inert = true;
outro = animate(element, get_options(), intro, 0, () => { outro = animate(
dispatch_event(element, 'outroend'); element,
fn?.(); get_options(),
}); intro,
0,
() => {
dispatch_event(element, 'outrostart');
},
() => {
dispatch_event(element, 'outroend');
fn?.();
}
);
}, },
stop: () => { stop: () => {
intro?.abort(); intro?.abort();
@ -306,10 +331,11 @@ export function transition(flags, element, get_fn, get_params) {
* @param {AnimationConfig | ((opts: { direction: 'in' | 'out' }) => AnimationConfig)} options * @param {AnimationConfig | ((opts: { direction: 'in' | 'out' }) => AnimationConfig)} options
* @param {Animation | undefined} counterpart The corresponding intro/outro to this outro/intro * @param {Animation | undefined} counterpart The corresponding intro/outro to this outro/intro
* @param {number} t2 The target `t` value `1` for intro, `0` for outro * @param {number} t2 The target `t` value `1` for intro, `0` for outro
* @param {(() => void)} on_begin Called just before beginning the animation
* @param {(() => void)} on_finish Called after successfully completing the animation * @param {(() => void)} on_finish Called after successfully completing the animation
* @returns {Animation} * @returns {Animation}
*/ */
function animate(element, options, counterpart, t2, on_finish) { function animate(element, options, counterpart, t2, on_begin, on_finish) {
var is_intro = t2 === 1; var is_intro = t2 === 1;
if (is_function(options)) { if (is_function(options)) {
@ -323,7 +349,7 @@ function animate(element, options, counterpart, t2, on_finish) {
queue_micro_task(() => { queue_micro_task(() => {
if (aborted) return; if (aborted) return;
var o = options({ direction: is_intro ? 'in' : 'out' }); var o = options({ direction: is_intro ? 'in' : 'out' });
a = animate(element, o, counterpart, t2, on_finish); a = animate(element, o, counterpart, t2, on_begin, on_finish);
}); });
// ...but we want to do so without using `async`/`await` everywhere, so // ...but we want to do so without using `async`/`await` everywhere, so
@ -342,7 +368,7 @@ function animate(element, options, counterpart, t2, on_finish) {
counterpart?.deactivate(); counterpart?.deactivate();
if (!options?.duration && !options?.delay) { if (!options?.duration && !options?.delay) {
dispatch_event(element, is_intro ? 'introstart' : 'outrostart'); on_begin();
on_finish(); on_finish();
return { return {
@ -382,7 +408,7 @@ function animate(element, options, counterpart, t2, on_finish) {
// remove dummy animation from the stack to prevent conflict with main animation // remove dummy animation from the stack to prevent conflict with main animation
animation.cancel(); animation.cancel();
dispatch_event(element, is_intro ? 'introstart' : 'outrostart'); on_begin();
// for bidirectional transitions, we start from the current position, // for bidirectional transitions, we start from the current position,
// rather than doing a full intro/outro // rather than doing a full intro/outro

@ -0,0 +1,31 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, raf, target, logs }) {
let divs = target.querySelectorAll('div');
divs.forEach((div) => {
// @ts-expect-error
div.getBoundingClientRect = function () {
// @ts-expect-error
const index = [...this.parentNode.children].indexOf(this);
const top = index * 30;
return {
left: 0,
right: 100,
top,
bottom: top + 20
};
};
});
const [btn] = target.querySelectorAll('button');
flushSync(() => btn.click());
raf.tick(1);
assert.deepEqual(logs, []);
raf.tick(100);
assert.deepEqual(logs, []);
}
});

@ -0,0 +1,19 @@
<script>
import { flip } from "svelte/animate";
let numbers = $state([0,1]);
</script>
<button onclick={() => numbers.reverse()}>reverse</button>
{#each numbers as num (num)}
<div
onintrostart={() => console.log("intro start")}
onoutrostart={() => console.log("outro start")}
onintroend={() => console.log("intro end")}
onoutroend={() => console.log("outro end")}
animate:flip={{ duration: 100 }}
>
{num}
</div>
{/each}
Loading…
Cancel
Save