Skip to content

Commit c44234d

Browse files
authored
fix: take outroing elements out of the flow when animating siblings (#11208)
* fix: take outroing elements out of the flow when animating siblings * changeset
1 parent 27d48c6 commit c44234d

File tree

5 files changed

+69
-16
lines changed

5 files changed

+69
-16
lines changed

.changeset/proud-pets-hang.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: take outroing elements out of the flow when animating siblings

packages/svelte/src/internal/client/dom/blocks/each.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
} from '../../reactivity/effects.js';
2525
import { source, mutable_source, set } from '../../reactivity/sources.js';
2626
import { is_array, is_frozen } from '../../utils.js';
27-
import { STATE_SYMBOL } from '../../constants.js';
27+
import { INERT, STATE_SYMBOL } from '../../constants.js';
2828

2929
/**
3030
* 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) {
7070
parent_node.append(controlled_anchor);
7171
}
7272

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

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

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

268268
if (item !== undefined) {
269269
item.a?.measure();
270-
to_animate.push(item);
270+
to_animate.add(item);
271271
}
272272
}
273273
}
@@ -302,7 +302,10 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
302302
update_item(item, value, i, flags);
303303
}
304304

305-
resume_effect(item.e);
305+
if ((item.e.f & INERT) !== 0) {
306+
resume_effect(item.e);
307+
to_animate.delete(item);
308+
}
306309

307310
if (item !== current) {
308311
if (seen.has(item)) {

packages/svelte/src/internal/client/dom/elements/transitions.js

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export function animation(element, get_fn, get_params) {
106106
) {
107107
const options = get_fn()(this.element, { from, to }, get_params?.());
108108

109-
animation = animate(this.element, options, undefined, 1, () => {
109+
animation = animate(this.element, options, false, undefined, 1, () => {
110110
animation?.abort();
111111
animation = undefined;
112112
});
@@ -169,7 +169,7 @@ export function transition(flags, element, get_fn, get_params) {
169169

170170
if (is_intro) {
171171
dispatch_event(element, 'introstart');
172-
intro = animate(element, get_options(), outro, 1, () => {
172+
intro = animate(element, get_options(), false, outro, 1, () => {
173173
dispatch_event(element, 'introend');
174174
intro = current_options = undefined;
175175
});
@@ -178,12 +178,12 @@ export function transition(flags, element, get_fn, get_params) {
178178
reset?.();
179179
}
180180
},
181-
out(fn) {
181+
out(fn, position_absolute = false) {
182182
if (is_outro) {
183183
element.inert = true;
184184

185185
dispatch_event(element, 'outrostart');
186-
outro = animate(element, get_options(), intro, 0, () => {
186+
outro = animate(element, get_options(), position_absolute, intro, 0, () => {
187187
dispatch_event(element, 'outroend');
188188
outro = current_options = undefined;
189189
fn?.();
@@ -229,12 +229,13 @@ export function transition(flags, element, get_fn, get_params) {
229229
* Animates an element, according to the provided configuration
230230
* @param {Element} element
231231
* @param {import('#client').AnimationConfig | ((opts: { direction: 'in' | 'out' }) => import('#client').AnimationConfig)} options
232+
* @param {boolean} position_absolute
232233
* @param {import('#client').Animation | undefined} counterpart The corresponding intro/outro to this outro/intro
233234
* @param {number} t2 The target `t` value — `1` for intro, `0` for outro
234235
* @param {(() => void) | undefined} callback
235236
* @returns {import('#client').Animation}
236237
*/
237-
function animate(element, options, counterpart, t2, callback) {
238+
function animate(element, options, position_absolute, counterpart, t2, callback) {
238239
if (is_function(options)) {
239240
// In the case of a deferred transition (such as `crossfade`), `option` will be
240241
// a function rather than an `AnimationConfig`. We need to call this function
@@ -244,7 +245,7 @@ function animate(element, options, counterpart, t2, callback) {
244245

245246
effect(() => {
246247
var o = untrack(() => options({ direction: t2 === 1 ? 'in' : 'out' }));
247-
a = animate(element, o, counterpart, t2, callback);
248+
a = animate(element, o, position_absolute, counterpart, t2, callback);
248249
});
249250

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

288+
/** @type {null | { position: string, width: string, height: string }} */
289+
var original_styles = null;
290+
287291
if (css) {
288292
// WAAPI
289293
var keyframes = [];
@@ -295,6 +299,37 @@ function animate(element, options, counterpart, t2, callback) {
295299
keyframes.push(css_to_keyframe(styles));
296300
}
297301

302+
if (position_absolute) {
303+
// we take the element out of the flow, so that sibling elements with an `animate:`
304+
// directive can transform to the correct position
305+
var computed_style = getComputedStyle(element);
306+
307+
if (computed_style.position !== 'absolute' && computed_style.position !== 'fixed') {
308+
var style = /** @type {HTMLElement | SVGElement} */ (element).style;
309+
310+
original_styles = {
311+
position: style.position,
312+
width: style.width,
313+
height: style.height
314+
};
315+
316+
var rect_a = element.getBoundingClientRect();
317+
style.position = 'absolute';
318+
style.width = computed_style.width;
319+
style.height = computed_style.height;
320+
var rect_b = element.getBoundingClientRect();
321+
322+
if (rect_a.left !== rect_b.left || rect_a.top !== rect_b.top) {
323+
var transform = `translate(${rect_a.left - rect_b.left}px, ${rect_a.top - rect_b.top}px)`;
324+
for (var keyframe of keyframes) {
325+
keyframe.transform = keyframe.transform
326+
? `${keyframe.transform} ${transform}`
327+
: transform;
328+
}
329+
}
330+
}
331+
}
332+
298333
animation = element.animate(keyframes, {
299334
delay,
300335
duration,
@@ -345,6 +380,15 @@ function animate(element, options, counterpart, t2, callback) {
345380
task?.abort();
346381
},
347382
deactivate: () => {
383+
if (original_styles) {
384+
// revert `animate:` position fixing
385+
var style = /** @type {HTMLElement | SVGElement} */ (element).style;
386+
387+
style.position = original_styles.position;
388+
style.width = original_styles.width;
389+
style.height = original_styles.height;
390+
}
391+
348392
callback = undefined;
349393
},
350394
reset: () => {

packages/svelte/src/internal/client/reactivity/effects.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,22 +334,23 @@ export function pause_effect(effect, callback) {
334334

335335
pause_children(effect, transitions, true);
336336

337-
run_out_transitions(transitions, () => {
337+
run_out_transitions(transitions, false, () => {
338338
destroy_effect(effect);
339339
if (callback) callback();
340340
});
341341
}
342342

343343
/**
344344
* @param {import('#client').TransitionManager[]} transitions
345+
* @param {boolean} position_absolute
345346
* @param {() => void} fn
346347
*/
347-
export function run_out_transitions(transitions, fn) {
348+
export function run_out_transitions(transitions, position_absolute, fn) {
348349
var remaining = transitions.length;
349350
if (remaining > 0) {
350351
var check = () => --remaining || fn();
351352
for (var transition of transitions) {
352-
transition.out(check);
353+
transition.out(check, position_absolute);
353354
}
354355
} else {
355356
fn();

packages/svelte/src/internal/client/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export interface TransitionManager {
7777
/** Called inside `resume_effect` */
7878
in: () => void;
7979
/** Called inside `pause_effect` */
80-
out: (callback?: () => void) => void;
80+
out: (callback?: () => void, position_absolute?: boolean) => void;
8181
/** Called inside `destroy_effect` */
8282
stop: () => void;
8383
}

0 commit comments

Comments
 (0)