diff --git a/.changeset/lemon-trees-act.md b/.changeset/lemon-trees-act.md new file mode 100644 index 0000000000..4e1df373a9 --- /dev/null +++ b/.changeset/lemon-trees-act.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: measure elements before taking siblings out of the flow diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 663cee91db..bb9f11a67c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -71,7 +71,7 @@ function pause_effects(items, controlled_anchor, callback) { parent_node.append(controlled_anchor); } - run_out_transitions(transitions, true, () => { + run_out_transitions(transitions, () => { for (var i = 0; i < length; i++) { destroy_effect(items[i].e); } @@ -312,7 +312,10 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { if ((item.e.f & INERT) !== 0) { resume_effect(item.e); - to_animate.delete(item); + if (is_animated) { + item.a?.unfix(); + to_animate.delete(item); + } } if (item !== current) { @@ -391,6 +394,16 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && length === 0 ? anchor : null; + if (is_animated) { + for (i = 0; i < to_destroy.length; i += 1) { + to_destroy[i].a?.measure(); + } + + for (i = 0; i < to_destroy.length; i += 1) { + to_destroy[i].a?.fix(); + } + } + pause_effects(to_destroy, controlled_anchor, () => { for (var i = 0; i < to_destroy.length; i += 1) { var item = to_destroy[i]; diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index 31d705b279..cc98aa3be4 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -88,6 +88,9 @@ export function animation(element, get_fn, get_params) { /** @type {import('#client').Animation | undefined} */ var animation; + /** @type {null | { position: string, width: string, height: string }} */ + var original_styles = null; + item.a ??= { element, measure() { @@ -106,11 +109,43 @@ export function animation(element, get_fn, get_params) { ) { const options = get_fn()(this.element, { from, to }, get_params?.()); - animation = animate(this.element, options, false, undefined, 1, () => { + animation = animate(this.element, options, undefined, 1, () => { animation?.abort(); animation = undefined; }); } + }, + fix() { + 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 + }; + + style.position = 'absolute'; + style.width = computed_style.width; + style.height = computed_style.height; + var to = element.getBoundingClientRect(); + + if (from.left !== to.left || from.top !== to.top) { + var transform = `translate(${from.left - to.left}px, ${from.top - to.top}px)`; + style.transform = style.transform ? `${style.transform} ${transform}` : transform; + } + } + }, + unfix() { + if (original_styles) { + var style = /** @type {HTMLElement | SVGElement} */ (element).style; + + style.position = original_styles.position; + style.width = original_styles.width; + style.height = original_styles.height; + } } }; @@ -169,7 +204,7 @@ export function transition(flags, element, get_fn, get_params) { if (is_intro) { dispatch_event(element, 'introstart'); - intro = animate(element, get_options(), false, outro, 1, () => { + intro = animate(element, get_options(), outro, 1, () => { dispatch_event(element, 'introend'); intro = current_options = undefined; }); @@ -178,12 +213,12 @@ export function transition(flags, element, get_fn, get_params) { reset?.(); } }, - out(fn, position_absolute = false) { + out(fn) { if (is_outro) { element.inert = true; dispatch_event(element, 'outrostart'); - outro = animate(element, get_options(), position_absolute, intro, 0, () => { + outro = animate(element, get_options(), intro, 0, () => { dispatch_event(element, 'outroend'); outro = current_options = undefined; fn?.(); @@ -229,13 +264,12 @@ 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, position_absolute, counterpart, t2, callback) { +function animate(element, options, 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 @@ -245,7 +279,7 @@ function animate(element, options, position_absolute, counterpart, t2, callback) effect(() => { var o = untrack(() => options({ direction: t2 === 1 ? 'in' : 'out' })); - a = animate(element, o, position_absolute, counterpart, t2, callback); + a = animate(element, o, counterpart, t2, callback); }); // ...but we want to do so without using `async`/`await` everywhere, so @@ -285,9 +319,6 @@ function animate(element, options, position_absolute, 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 = []; @@ -299,37 +330,6 @@ function animate(element, options, position_absolute, 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, @@ -340,6 +340,10 @@ function animate(element, options, position_absolute, counterpart, t2, callback) animation.finished .then(() => { callback?.(); + + if (t2 === 1) { + animation.cancel(); + } }) .catch((e) => { // Error for DOMException: The user aborted a request. This results in two things: @@ -380,15 +384,6 @@ function animate(element, options, position_absolute, 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 b11921536a..8e7bd12b04 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -334,7 +334,7 @@ export function pause_effect(effect, callback) { pause_children(effect, transitions, true); - run_out_transitions(transitions, false, () => { + run_out_transitions(transitions, () => { destroy_effect(effect); if (callback) callback(); }); @@ -342,15 +342,14 @@ export function pause_effect(effect, callback) { /** * @param {import('#client').TransitionManager[]} transitions - * @param {boolean} position_absolute * @param {() => void} fn */ -export function run_out_transitions(transitions, position_absolute, fn) { +export function run_out_transitions(transitions, fn) { var remaining = transitions.length; if (remaining > 0) { var check = () => --remaining || fn(); for (var transition of transitions) { - transition.out(check, position_absolute); + transition.out(check); } } else { fn(); diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 7530b07f32..e1568494a1 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -79,7 +79,7 @@ export interface TransitionManager { /** Called inside `resume_effect` */ in: () => void; /** Called inside `pause_effect` */ - out: (callback?: () => void, position_absolute?: boolean) => void; + out: (callback?: () => void) => void; /** Called inside `destroy_effect` */ stop: () => void; } @@ -91,6 +91,10 @@ export interface AnimationManager { measure: () => void; /** Called during keyed each block reconciliation, after updates — this triggers the animation */ apply: () => void; + /** Fix the element position, so that siblings can move to the correct destination */ + fix: () => void; + /** Unfix the element position if the outro is aborted */ + unfix: () => void; } export interface Animation {