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