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

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

* changeset
pull/11217/head
Rich Harris 1 year ago committed by GitHub
parent 27d48c601f
commit c44234dc2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: take outroing elements out of the flow when animating siblings

@ -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
@ -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);
}
@ -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 = [];
@ -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);
}
}
}
@ -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)) {

@ -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;
});
@ -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;
});
@ -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?.();
@ -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
@ -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
@ -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 = [];
@ -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,
@ -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: () => {

@ -334,7 +334,7 @@ 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();
});
@ -342,14 +342,15 @@ export function pause_effect(effect, 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();

@ -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;
}

Loading…
Cancel
Save