Skip to content

fix: properly delay intro transitions #12389

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

Merged
merged 4 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all 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: 5 additions & 0 deletions .changeset/tricky-ears-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: properly delay intro transitions
18 changes: 15 additions & 3 deletions packages/svelte/src/internal/client/dom/elements/transitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ export function transition(flags, element, get_fn, get_params) {
* @returns {import('#client').Animation}
*/
function animate(element, options, counterpart, t2, callback) {
var is_intro = t2 === 1;

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 @@ -274,7 +276,7 @@ function animate(element, options, counterpart, t2, callback) {
var a;

queue_micro_task(() => {
var o = options({ direction: t2 === 1 ? 'in' : 'out' });
var o = options({ direction: is_intro ? 'in' : 'out' });
a = animate(element, o, counterpart, t2, callback);
});

Expand Down Expand Up @@ -320,15 +322,25 @@ function animate(element, options, counterpart, t2, callback) {
var keyframes = [];
var n = Math.ceil(duration / (1000 / 60)); // `n` must be an integer, or we risk missing the `t2` value

// In case of a delayed intro, apply the initial style for the duration of the delay;
// else in case of a fade-in for example the element would be visible until the animation starts
if (is_intro && delay > 0) {
let m = Math.ceil(delay / (1000 / 60));
let keyframe = css_to_keyframe(css(0, 1));
for (let i = 0; i < m; i += 1) {
keyframes.push(keyframe);
}
}

for (var i = 0; i <= n; i += 1) {
var t = t1 + delta * easing(i / n);
var styles = css(t, 1 - t);
keyframes.push(css_to_keyframe(styles));
}

animation = element.animate(keyframes, {
delay,
duration,
delay: is_intro ? 0 : delay,
duration: duration + (is_intro ? delay : 0),
easing: 'linear',
fill: 'forwards'
});
Expand Down
12 changes: 8 additions & 4 deletions packages/svelte/tests/animation-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Animation {
#target;
#keyframes;
#duration;
#delay;

#offset = raf.time;

Expand All @@ -47,12 +48,13 @@ class Animation {
/**
* @param {HTMLElement} target
* @param {Keyframe[]} keyframes
* @param {{ duration: number }} options // TODO add delay
* @param {{ duration: number, delay: number }} options
*/
constructor(target, keyframes, { duration }) {
constructor(target, keyframes, { duration, delay }) {
this.#target = target;
this.#keyframes = keyframes;
this.#duration = duration;
this.#delay = delay ?? 0;

// Promise-like semantics, but call callbacks immediately on raf.tick
this.finished = {
Expand All @@ -73,7 +75,9 @@ class Animation {
}

_update() {
this.currentTime = raf.time - this.#offset;
this.currentTime = raf.time - this.#offset - this.#delay;
if (this.currentTime < 0) return;

const target_frame = this.currentTime / this.#duration;
this.#apply_keyframe(target_frame);

Expand Down Expand Up @@ -168,7 +172,7 @@ function interpolate(a, b, p) {

/**
* @param {Keyframe[]} keyframes
* @param {{duration: number}} options
* @param {{duration: number, delay: number}} options
* @returns {globalThis.Animation}
*/
HTMLElement.prototype.animate = function (keyframes, options) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { flushSync } from '../../../../src/index-client.js';
import { test } from '../../test';

export default test({
test({ assert, raf, target }) {
const btn = target.querySelector('button');

// in
btn?.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
'<button>toggle</button><p style="opacity: 0;">delayed fade</p>'
);
raf.tick(1);
assert.htmlEqual(
target.innerHTML,
'<button>toggle</button><p style="opacity: 0;">delayed fade</p>'
);

raf.tick(99);
assert.htmlEqual(
target.innerHTML,
'<button>toggle</button><p style="opacity: 0;">delayed fade</p>'
);

raf.tick(150);
assert.htmlEqual(
target.innerHTML,
'<button>toggle</button><p style="opacity: 0.5;">delayed fade</p>'
);

raf.tick(200);
assert.htmlEqual(target.innerHTML, '<button>toggle</button><p style="">delayed fade</p>');

// out
btn?.click();
flushSync();
raf.tick(275);
assert.htmlEqual(target.innerHTML, '<button>toggle</button><p style="">delayed fade</p>');

raf.tick(350);
assert.htmlEqual(
target.innerHTML,
'<button>toggle</button><p style="opacity: 0.5;">delayed fade</p>'
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script>
function fade(_) {
return {
delay: 100,
duration: 100,
css: (t) => `opacity: ${t}`
};
}

let visible = $state(false);
</script>

<button onclick={() => (visible = !visible)}>toggle</button>

{#if visible}
<p transition:fade>delayed fade</p>
{/if}
Loading