diff --git a/.changeset/quiet-inputs-keep.md b/.changeset/quiet-inputs-keep.md new file mode 100644 index 0000000000..0efcfcc058 --- /dev/null +++ b/.changeset/quiet-inputs-keep.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't overwrite an unchanged spread `value`, preserving incomplete number input diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 0fe1b8b2d3..7d2dd62baf 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -422,7 +422,21 @@ function set_attributes( } else if (!is_custom_element && (key === '__value' || (key === 'value' && value != null))) { // @ts-ignore We're not running this for custom elements because __value is actually // how Lit stores the current value on the element, and messing with that would break things. - element.value = element.__value = value; + element.__value = value; + // we don't set the value if it hasn't changed. This supports invalid number inputs like `1e` because + // 1. user types 1e + // 2. the state is updated reading e.target.value which is '' + // 3. the spreaded value is '' + // 4. updating input.value would thus, clear the user value + if ( + prev_value == null || + // @ts-ignore + element.value !== value || + (value === 0 && element.nodeName === PROGRESS_TAG) + ) { + // @ts-ignore + element.value = value; + } } else if (key === 'selected' && is_option_element) { set_selected(/** @type {HTMLOptionElement} */ (element), value); } else { diff --git a/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/_config.js b/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/_config.js new file mode 100644 index 0000000000..cec1e179ac --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/_config.js @@ -0,0 +1,28 @@ +import { flushSync } from 'svelte'; +import { ok, test } from '../../assert'; + +export default test({ + async test({ assert, target }) { + const input = target.querySelector('input'); + ok(input); + + input.focus(); + + // we need to use `document.execCommand('insertText', false, ...)` to simulate user input + // because directly setting an invalid value to `input.value` would simply clear the input + // and dispatching an event would not update the input correctly + document.execCommand('insertText', false, '1'); + flushSync(); + // `1e` is incomplete on every platform, unlike `1.` which Chromium on Linux accepts as `1` + document.execCommand('insertText', false, 'e'); + flushSync(); + + assert.equal(input.value, ''); + assert.equal(input.validity.badInput, true); + + document.execCommand('insertText', false, '5'); + flushSync(); + + assert.equal(input.value, '1e5'); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/main.svelte b/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/main.svelte new file mode 100644 index 0000000000..3e7a5cc335 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/main.svelte @@ -0,0 +1,5 @@ + + + (value = e.currentTarget.value)} />