Skip to content

Commit

Permalink
fix: ensure previous transitions are properly aborted
Browse files Browse the repository at this point in the history
- nullify options when the transition is aborted. This ensures a change in options is reflected the next time, else it would stick around indefinetly
- abort previous intro (if exists) when new intro plays (same for outro)

fixes #11372
  • Loading branch information
dummdidumm committed Jul 16, 2024
1 parent b1cf2ec commit 02b9723
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/wicked-carrots-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: ensure previous transitions are properly aborted
81 changes: 60 additions & 21 deletions packages/svelte/src/internal/client/dom/elements/transitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,17 @@ export function animation(element, get_fn, get_params) {
) {
const options = get_fn()(this.element, { from, to }, get_params?.());

animation = animate(this.element, options, undefined, 1, () => {
animation?.abort();
animation = undefined;
});
animation = animate(
this.element,
options,
undefined,
1,
() => {
animation?.abort();
animation = undefined;
},
undefined
);
}
},
fix() {
Expand Down Expand Up @@ -157,10 +164,11 @@ export function animation(element, get_fn, get_params) {
export function transition(flags, element, get_fn, get_params) {
var is_intro = (flags & TRANSITION_IN) !== 0;
var is_outro = (flags & TRANSITION_OUT) !== 0;
var is_both = is_intro && is_outro;
var is_global = (flags & TRANSITION_GLOBAL) !== 0;

/** @type {'in' | 'out' | 'both'} */
var direction = is_intro && is_outro ? 'both' : is_intro ? 'in' : 'out';
var direction = is_both ? 'both' : is_intro ? 'in' : 'out';

/** @type {import('#client').AnimationConfig | ((opts: { direction: 'in' | 'out' }) => import('#client').AnimationConfig) | undefined} */
var current_options;
Expand Down Expand Up @@ -191,27 +199,54 @@ export function transition(flags, element, get_fn, get_params) {

// abort the outro to prevent overlap with the intro
outro?.abort();
// abort previous intro (can happen if an element is intro'd, then outro'd, then intro'd again)
intro?.abort();

if (is_intro) {
dispatch_event(element, 'introstart');
intro = animate(element, get_options(), outro, 1, () => {
dispatch_event(element, 'introend');
intro = current_options = undefined;
});
intro = animate(
element,
get_options(),
outro,
1,
() => {
dispatch_event(element, 'introend');
intro = current_options = undefined;
},
is_both
? undefined
: () => {
intro = current_options = undefined;
}
);
} else {
reset?.();
}
},
out(fn) {
// abort previous outro (can happen if an element is outro'd, then intro'd, then outro'd again)
outro?.abort();

if (is_outro) {
element.inert = true;

dispatch_event(element, 'outrostart');
outro = animate(element, get_options(), intro, 0, () => {
dispatch_event(element, 'outroend');
outro = current_options = undefined;
fn?.();
});
outro = animate(
element,
get_options(),
intro,
0,
() => {
dispatch_event(element, 'outroend');
outro = current_options = undefined;
fn?.();
},
is_both
? undefined
: () => {
outro = current_options = undefined;
}
);

// TODO arguably the outro should never null itself out until _all_ outros for this effect have completed...
// in that case we wouldn't need to store `reset` separately
Expand Down Expand Up @@ -263,10 +298,11 @@ export function transition(flags, element, get_fn, get_params) {
* @param {import('#client').AnimationConfig | ((opts: { direction: 'in' | 'out' }) => import('#client').AnimationConfig)} options
* @param {import('#client').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 {(() => void) | undefined} callback
* @param {(() => void) | undefined} on_finish Called after successfully completing the animation
* @param {(() => void) | undefined} on_abort Called if the animation is aborted
* @returns {import('#client').Animation}
*/
function animate(element, options, counterpart, t2, callback) {
function animate(element, options, counterpart, t2, on_finish, on_abort) {
var is_intro = t2 === 1;

if (is_function(options)) {
Expand All @@ -278,7 +314,7 @@ function animate(element, options, counterpart, t2, callback) {

queue_micro_task(() => {
var o = options({ direction: is_intro ? 'in' : 'out' });
a = animate(element, o, counterpart, t2, callback);
a = animate(element, o, counterpart, t2, on_finish, on_abort);
});

// ...but we want to do so without using `async`/`await` everywhere, so
Expand All @@ -294,7 +330,7 @@ function animate(element, options, counterpart, t2, callback) {
counterpart?.deactivate();

if (!options?.duration) {
callback?.();
on_finish?.();
return {
abort: noop,
deactivate: noop,
Expand All @@ -319,6 +355,7 @@ function animate(element, options, counterpart, t2, callback) {
var task;

if (css) {
// run after a micro task so that all transitions that are lining up and are about to run can correctly measure the DOM
queue_micro_task(() => {
// WAAPI
var keyframes = [];
Expand Down Expand Up @@ -349,7 +386,7 @@ function animate(element, options, counterpart, t2, callback) {

animation.finished
.then(() => {
callback?.();
on_finish?.();

if (t2 === 1) {
animation.cancel();
Expand All @@ -376,7 +413,7 @@ function animate(element, options, counterpart, t2, callback) {
task = loop((now) => {
if (now >= end) {
tick?.(t2, 1 - t2);
callback?.();
on_finish?.();
return false;
}

Expand All @@ -393,9 +430,11 @@ function animate(element, options, counterpart, t2, callback) {
abort: () => {
animation?.cancel();
task?.abort();
on_abort?.();
},
deactivate: () => {
callback = undefined;
on_finish = undefined;
on_abort = undefined;
},
reset: () => {
if (t2 === 0) {
Expand Down

0 comments on commit 02b9723

Please sign in to comment.