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..44549f8234 100644 --- a/packages/svelte/src/transition/index.js +++ b/packages/svelte/src/transition/index.js @@ -28,6 +28,18 @@ function split_css_unit(value) { return split ? [parseFloat(split[1]), split[2] || 'px'] : [/** @type {number} */ (value), 'px']; } +/** + * Elements without a layout box (e.g. inside a `display: none` parent) have computed + * dimensions like `auto` that parse to `NaN`, producing keyframe values the browser + * rejects (https://github.com/sveltejs/svelte/issues/14205). Treat them as `0` instead + * @param {string} value + * @returns {number} + */ +function parse_dimension(value) { + const result = parseFloat(value); + return Number.isNaN(result) ? 0 : result; +} + /** * Animates a `blur` filter alongside an element's opacity. * @@ -116,19 +128,21 @@ export function slide(node, { delay = 0, duration = 400, easing = cubic_out, axi const opacity = +style.opacity; const primary_property = axis === 'y' ? 'height' : 'width'; - const primary_property_value = parseFloat(style[primary_property]); + const primary_property_value = parse_dimension(style[primary_property]); const secondary_properties = axis === 'y' ? ['top', 'bottom'] : ['left', 'right']; const capitalized_secondary_properties = secondary_properties.map( (e) => /** @type {'Left' | 'Right' | 'Top' | 'Bottom'} */ (`${e[0].toUpperCase()}${e.slice(1)}`) ); - const padding_start_value = parseFloat(style[`padding${capitalized_secondary_properties[0]}`]); - const padding_end_value = parseFloat(style[`padding${capitalized_secondary_properties[1]}`]); - const margin_start_value = parseFloat(style[`margin${capitalized_secondary_properties[0]}`]); - const margin_end_value = parseFloat(style[`margin${capitalized_secondary_properties[1]}`]); - const border_width_start_value = parseFloat( + const padding_start_value = parse_dimension( + style[`padding${capitalized_secondary_properties[0]}`] + ); + const padding_end_value = parse_dimension(style[`padding${capitalized_secondary_properties[1]}`]); + const margin_start_value = parse_dimension(style[`margin${capitalized_secondary_properties[0]}`]); + const margin_end_value = parse_dimension(style[`margin${capitalized_secondary_properties[1]}`]); + const border_width_start_value = parse_dimension( style[`border${capitalized_secondary_properties[0]}Width`] ); - const border_width_end_value = parseFloat( + const border_width_end_value = parse_dimension( style[`border${capitalized_secondary_properties[1]}Width`] ); return { 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..c200b076f3 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/transition-slide-hidden-parent/_config.js @@ -0,0 +1,25 @@ +import { 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 keyframes = /** @type {KeyframeEffect} */ (animations[0].effect).getKeyframes(); + assert.ok(keyframes.length > 0); + + for (const keyframe of keyframes) { + assert.ok('height' in keyframe, 'height was rejected as an invalid keyframe value'); + + for (const value of Object.values(keyframe)) { + assert.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 @@ + + + + +
+ {#if visible} +

hello

+ {/if} +
diff --git a/packages/svelte/tests/runtime-legacy/samples/class-shortcut-with-transition/_config.js b/packages/svelte/tests/runtime-legacy/samples/class-shortcut-with-transition/_config.js index e58e8c53c5..5019b553ee 100644 --- a/packages/svelte/tests/runtime-legacy/samples/class-shortcut-with-transition/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/class-shortcut-with-transition/_config.js @@ -18,7 +18,7 @@ export default test({ raf.tick(150); assert.htmlEqual( target.innerHTML, - '

foo

bar

' + '

foo

bar

' ); component.open = true; raf.tick(250);