diff --git a/.changeset/tough-pandas-shout.md b/.changeset/tough-pandas-shout.md new file mode 100644 index 0000000000..b0f6ea64fd --- /dev/null +++ b/.changeset/tough-pandas-shout.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid `NaN` keyframe values in `slide` transition for elements without a layout box diff --git a/packages/svelte/src/transition/index.js b/packages/svelte/src/transition/index.js index 898a929eb1..31e8fbde2b 100644 --- a/packages/svelte/src/transition/index.js +++ b/packages/svelte/src/transition/index.js @@ -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` }; } diff --git a/packages/svelte/tests/runtime-browser/samples/transition-slide-hidden-parent/_config.js b/packages/svelte/tests/runtime-browser/samples/transition-slide-hidden-parent/_config.js new file mode 100644 index 0000000000..a8c1e07111 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/transition-slide-hidden-parent/_config.js @@ -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}`); + } + } + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/transition-slide-hidden-parent/main.svelte b/packages/svelte/tests/runtime-browser/samples/transition-slide-hidden-parent/main.svelte new file mode 100644 index 0000000000..e1a7c54bdb --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/transition-slide-hidden-parent/main.svelte @@ -0,0 +1,13 @@ + + + + +