From 38098a931142dfef6ca7330d33264d1908b698fa Mon Sep 17 00:00:00 2001 From: Mariana Castro <121824373+maricastroc@users.noreply.github.com> Date: Wed, 23 Sep 2026 13:58:28 -0300 Subject: [PATCH] fix: don't overwrite an unchanged spread `value` (#18864) Fixes #18862 When a `value` is applied through a spread (``), `set_attributes` always writes `element.value`, even if the element already has that value. Writing `value` on a number input clears any incomplete text: typing `250.` makes `input.value` `''` (the text is still visible, but `validity.badInput` is `true`). If the spread `value` then becomes `''` too, writing `''` erases what the user typed. This is what happens with SvelteKit remote forms: `fields.quantity.as('number')` coerces `''` to `undefined`, the spread `value` becomes `''`, and Svelte writes it back to the input. When `.` is typed: - Chromium clears the input in every locale, because `250.` is always incomplete input there. - WebKit/Safari clears it in locales where `.` is not the decimal separator (e.g. `fr-CH`), which is where the reporter saw it. A plain `` never clears. Neither does the same `value` in Svelte without a spread (`value={...}`), because `set_value` already skips writing when `element.value === value`. This PR adds the same check to the spread path. Svelte 3 had this check too (#3426, #3495); it was lost in Svelte 5. The first write still always happens. Writing `value` sets the input's dirty value flag, and without that flag a `defaultValue` applied afterwards would change the current value (covered by `form-default-value-spread`). This also likely affects sveltejs/kit#16270 (same symptom, plus a cursor jump caused by redundant writes). That issue is handled on the kit side in sveltejs/kit#16320; this PR is independent of it. The test runs in a real browser (`runtime-browser`). It types with `document.execCommand('insertText')`, which goes through Chromium's own editing, so `250.` is real incomplete input. The locale is not mocked. The test fails without this change and passes with it. ### Before submitting the PR, please make sure you do the following - [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [x] Ideally, include a test that fails without this PR but passes with it. - [x] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` --------- Co-authored-by: Paolo Ricciuti --- .changeset/quiet-inputs-keep.md | 5 ++++ .../client/dom/elements/attributes.js | 16 ++++++++++- .../_config.js | 28 +++++++++++++++++++ .../main.svelte | 5 ++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 .changeset/quiet-inputs-keep.md create mode 100644 packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/_config.js create mode 100644 packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/main.svelte 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)} />