From 828d2a51cd332bcc23ede9917ad84b0ee39b8889 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 17 Apr 2024 10:04:04 -0400 Subject: [PATCH 1/2] fix: take outroing elements out of the flow when animating siblings --- .../src/internal/client/dom/blocks/each.js | 15 +++-- .../client/dom/elements/transitions.js | 56 +++++++++++++++++-- .../src/internal/client/reactivity/effects.js | 7 ++- .../svelte/src/internal/client/types.d.ts | 2 +- 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index db529a38b560..c1e0dc82f398 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -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} */ + 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)) { diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index 7ee9e33b5629..31d705b27987 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -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: () => { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index f7cdbc13f908..5f4c600dcdee 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -302,7 +302,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(); }); @@ -310,14 +310,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(); diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index cd900b0eb12c..7b1c4333c8cc 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -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; } From 92db5b817e1bafa92847887447616e3ee8ddd238 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 17 Apr 2024 10:04:19 -0400 Subject: [PATCH 2/2] changeset --- .changeset/proud-pets-hang.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/proud-pets-hang.md diff --git a/.changeset/proud-pets-hang.md b/.changeset/proud-pets-hang.md new file mode 100644 index 000000000000..dd67868e7cda --- /dev/null +++ b/.changeset/proud-pets-hang.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: take outroing elements out of the flow when animating siblings