Skip to content
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

fix: measure elements before taking siblings out of the flow #11216

Merged
merged 5 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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: 0 additions & 5 deletions .changeset/proud-pets-hang.md

This file was deleted.

17 changes: 15 additions & 2 deletions packages/svelte/src/internal/client/dom/blocks/each.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,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);
}
Expand Down Expand Up @@ -304,7 +304,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) {
Expand Down Expand Up @@ -383,6 +386,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];
Expand Down
95 changes: 45 additions & 50 deletions packages/svelte/src/internal/client/dom/elements/transitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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;
}
}
};

Expand Down Expand Up @@ -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;
});
Expand All @@ -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?.();
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 = [];
Expand All @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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: () => {
Expand Down
7 changes: 3 additions & 4 deletions packages/svelte/src/internal/client/reactivity/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,23 +334,22 @@ 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();
});
}

/**
* @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();
Expand Down
6 changes: 5 additions & 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, position_absolute?: boolean) => void;
out: (callback?: () => void) => void;
/** Called inside `destroy_effect` */
stop: () => void;
}
Expand All @@ -89,6 +89,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 {
Expand Down
Loading