Skip to content

fix: take outroing elements out of the flow when animating siblings #11208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/proud-pets-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: take outroing elements out of the flow when animating siblings
15 changes: 9 additions & 6 deletions packages/svelte/src/internal/client/dom/blocks/each.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from '../../reactivity/effects.js';
import { source, mutable_source, set } from '../../reactivity/sources.js';
import { is_array, is_frozen } from '../../utils.js';
import { STATE_SYMBOL } from '../../constants.js';
import { INERT, STATE_SYMBOL } from '../../constants.js';

/**
* The row of a keyed each block that is currently updating. We track this
Expand Down Expand Up @@ -70,7 +70,7 @@ function pause_effects(items, controlled_anchor, callback) {
parent_node.append(controlled_anchor);
}

run_out_transitions(transitions, () => {
run_out_transitions(transitions, true, () => {
for (var i = 0; i < length; i++) {
destroy_effect(items[i].e);
}
Expand Down Expand Up @@ -238,8 +238,8 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
/** @type {import('#client').EachState | import('#client').EachItem} */
var prev = state;

/** @type {import('#client').EachItem[]} */
var to_animate = [];
/** @type {Set<import('#client').EachItem>} */
var to_animate = new Set();

/** @type {import('#client').EachItem[]} */
var matched = [];
Expand Down Expand Up @@ -267,7 +267,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {

if (item !== undefined) {
item.a?.measure();
to_animate.push(item);
to_animate.add(item);
}
}
}
Expand Down Expand Up @@ -302,7 +302,10 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
update_item(item, value, i, flags);
}

resume_effect(item.e);
if ((item.e.f & INERT) !== 0) {
resume_effect(item.e);
to_animate.delete(item);
}

if (item !== current) {
if (seen.has(item)) {
Expand Down
56 changes: 50 additions & 6 deletions packages/svelte/src/internal/client/dom/elements/transitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ 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 = animate(this.element, options, false, undefined, 1, () => {
animation?.abort();
animation = undefined;
});
Expand Down Expand Up @@ -169,7 +169,7 @@ export function transition(flags, element, get_fn, get_params) {

if (is_intro) {
dispatch_event(element, 'introstart');
intro = animate(element, get_options(), outro, 1, () => {
intro = animate(element, get_options(), false, outro, 1, () => {
dispatch_event(element, 'introend');
intro = current_options = undefined;
});
Expand All @@ -178,12 +178,12 @@ export function transition(flags, element, get_fn, get_params) {
reset?.();
}
},
out(fn) {
out(fn, position_absolute = false) {
if (is_outro) {
element.inert = true;

dispatch_event(element, 'outrostart');
outro = animate(element, get_options(), intro, 0, () => {
outro = animate(element, get_options(), position_absolute, intro, 0, () => {
dispatch_event(element, 'outroend');
outro = current_options = undefined;
fn?.();
Expand Down Expand Up @@ -229,12 +229,13 @@ export function transition(flags, element, get_fn, get_params) {
* Animates an element, according to the provided configuration
* @param {Element} element
* @param {import('#client').AnimationConfig | ((opts: { direction: 'in' | 'out' }) => import('#client').AnimationConfig)} options
* @param {boolean} position_absolute
* @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
* @returns {import('#client').Animation}
*/
function animate(element, options, counterpart, t2, callback) {
function animate(element, options, position_absolute, counterpart, t2, callback) {
if (is_function(options)) {
// In the case of a deferred transition (such as `crossfade`), `option` will be
// a function rather than an `AnimationConfig`. We need to call this function
Expand All @@ -244,7 +245,7 @@ function animate(element, options, counterpart, t2, callback) {

effect(() => {
var o = untrack(() => options({ direction: t2 === 1 ? 'in' : 'out' }));
a = animate(element, o, counterpart, t2, callback);
a = animate(element, o, position_absolute, counterpart, t2, callback);
});

// ...but we want to do so without using `async`/`await` everywhere, so
Expand Down Expand Up @@ -284,6 +285,9 @@ function animate(element, options, counterpart, t2, callback) {
/** @type {import('#client').Task} */
var task;

/** @type {null | { position: string, width: string, height: string }} */
var original_styles = null;

if (css) {
// WAAPI
var keyframes = [];
Expand All @@ -295,6 +299,37 @@ function animate(element, options, counterpart, t2, callback) {
keyframes.push(css_to_keyframe(styles));
}

if (position_absolute) {
// we take the element out of the flow, so that sibling elements with an `animate:`
// directive can transform to the correct position
var computed_style = getComputedStyle(element);

if (computed_style.position !== 'absolute' && computed_style.position !== 'fixed') {
var style = /** @type {HTMLElement | SVGElement} */ (element).style;

original_styles = {
position: style.position,
width: style.width,
height: style.height
};

var rect_a = element.getBoundingClientRect();
style.position = 'absolute';
style.width = computed_style.width;
style.height = computed_style.height;
var rect_b = element.getBoundingClientRect();

if (rect_a.left !== rect_b.left || rect_a.top !== rect_b.top) {
var transform = `translate(${rect_a.left - rect_b.left}px, ${rect_a.top - rect_b.top}px)`;
for (var keyframe of keyframes) {
keyframe.transform = keyframe.transform
? `${keyframe.transform} ${transform}`
: transform;
}
}
}
}

animation = element.animate(keyframes, {
delay,
duration,
Expand Down Expand Up @@ -345,6 +380,15 @@ function animate(element, options, counterpart, t2, callback) {
task?.abort();
},
deactivate: () => {
if (original_styles) {
// revert `animate:` position fixing
var style = /** @type {HTMLElement | SVGElement} */ (element).style;

style.position = original_styles.position;
style.width = original_styles.width;
style.height = original_styles.height;
}

callback = undefined;
},
reset: () => {
Expand Down
7 changes: 4 additions & 3 deletions packages/svelte/src/internal/client/reactivity/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,22 +302,23 @@ export function pause_effect(effect, callback) {

pause_children(effect, transitions, true);

run_out_transitions(transitions, () => {
run_out_transitions(transitions, false, () => {
destroy_effect(effect);
if (callback) callback();
});
}

/**
* @param {import('#client').TransitionManager[]} transitions
* @param {boolean} position_absolute
* @param {() => void} fn
*/
export function run_out_transitions(transitions, fn) {
export function run_out_transitions(transitions, position_absolute, fn) {
var remaining = transitions.length;
if (remaining > 0) {
var check = () => --remaining || fn();
for (var transition of transitions) {
transition.out(check);
transition.out(check, position_absolute);
}
} else {
fn();
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export interface TransitionManager {
/** Called inside `resume_effect` */
in: () => void;
/** Called inside `pause_effect` */
out: (callback?: () => void) => void;
out: (callback?: () => void, position_absolute?: boolean) => void;
/** Called inside `destroy_effect` */
stop: () => void;
}
Expand Down
Loading