fix: avoid NaN keyframe values in slide transition (#18430)

Fixes #14205

When a `slide` transition runs on an element without a layout box, for example inside a `display: none` parent, `getComputedStyle` returns values like `auto` for the animated dimensions. These were passed through `parseFloat` unguarded, so the generated css contained `height: NaNpx` and the browser rejected the keyframe property with an "Invalid keyframe value" warning on every transition.

Fix it by omitting properties if they have unparseable numbers - it's the same outcome but you don't see a warning.

---------

Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/17306/merge
Nic Polumeyv 3 days ago committed by GitHub
parent 3405b5e735
commit 3b559bbf96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: avoid `NaN` keyframe values in `slide` transition for elements without a layout box

@ -28,6 +28,17 @@ function split_css_unit(value) {
return split ? [parseFloat(split[1]), split[2] || 'px'] : [/** @type {number} */ (value), 'px'];
}
/**
* Omits unresolved dimensions rather than passing invalid values to Web Animations.
*
* @param {string} property
* @param {number} value
* @param {number} t
*/
function css_dimension(property, value, t) {
return Number.isNaN(value) ? '' : `${property}: ${t * value}px;`;
}
/**
* Animates a `blur` filter alongside an element's opacity.
*
@ -138,13 +149,13 @@ export function slide(node, { delay = 0, duration = 400, easing = cubic_out, axi
css: (t) =>
'overflow: hidden;' +
`opacity: ${Math.min(t * 20, 1) * opacity};` +
`${primary_property}: ${t * primary_property_value}px;` +
`padding-${secondary_properties[0]}: ${t * padding_start_value}px;` +
`padding-${secondary_properties[1]}: ${t * padding_end_value}px;` +
`margin-${secondary_properties[0]}: ${t * margin_start_value}px;` +
`margin-${secondary_properties[1]}: ${t * margin_end_value}px;` +
`border-${secondary_properties[0]}-width: ${t * border_width_start_value}px;` +
`border-${secondary_properties[1]}-width: ${t * border_width_end_value}px;` +
css_dimension(primary_property, primary_property_value, t) +
css_dimension(`padding-${secondary_properties[0]}`, padding_start_value, t) +
css_dimension(`padding-${secondary_properties[1]}`, padding_end_value, t) +
css_dimension(`margin-${secondary_properties[0]}`, margin_start_value, t) +
css_dimension(`margin-${secondary_properties[1]}`, margin_end_value, t) +
css_dimension(`border-${secondary_properties[0]}-width`, border_width_start_value, t) +
css_dimension(`border-${secondary_properties[1]}-width`, border_width_end_value, t) +
`min-${primary_property}: 0`
};
}

@ -0,0 +1,27 @@
import { ok, test } from '../../assert';
export default test({
async test({ assert, window }) {
window.document.querySelector('button')?.click();
await new Promise((r) => setTimeout(r, 100));
const p = window.document.querySelector('p');
const animations = /** @type {HTMLElement} */ (p).getAnimations();
assert.equal(animations.length, 1);
// when the element has no layout box, computed dimensions resolve to 'auto',
// which must not end up as NaN values that the browser rejects (#14205)
const effect = /** @type {KeyframeEffect} */ (animations[0].effect);
const keyframes = effect.getKeyframes();
ok(keyframes.length > 0);
assert.equal(effect.getTiming().duration, 400);
for (const keyframe of keyframes) {
ok(!('height' in keyframe), 'unresolved height should be omitted');
for (const value of Object.values(keyframe)) {
ok(!String(value).includes('NaN'), `unexpected NaN in keyframe: ${value}`);
}
}
}
});

@ -0,0 +1,13 @@
<script>
import { slide } from 'svelte/transition';
let visible = $state(false);
</script>
<button onclick={() => (visible = !visible)}>toggle</button>
<div style="display: none">
{#if visible}
<p transition:slide>hello</p>
{/if}
</div>
Loading…
Cancel
Save