From f39f5e730ad7bf05cd8a5c9b6dd094acac1121b7 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:19:20 +0300 Subject: [PATCH] fix: a held spinner interrupted by a redraw still commits on release (Chromium blurs the removed field) Co-Authored-By: Claude Opus 5.5 --- app/ui/composer/elements/input.js | 2 +- .../composer/tests/elements_behaviour.test.js | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/ui/composer/elements/input.js b/app/ui/composer/elements/input.js index b2a5983a..38d6d5a0 100644 --- a/app/ui/composer/elements/input.js +++ b/app/ui/composer/elements/input.js @@ -96,7 +96,7 @@ define("number", "md", ({ value = 0, min, max, step = 1, precision, unit, snap, if (skipped) { skipped = false; commit(); } }; input.addEventListener("change", () => { if (held) skipped = true; else commit(); }); - input.addEventListener("blur", () => { held = skipped = false; commit(); }); // also ends a hold whose release never came + input.addEventListener("blur", () => { if (swapping.depth) return; held = skipped = false; commit(); }); // ends a hold whose release never came; a redraw's blur does not: the release still commits input.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); commit(); } else if (e.repeat && /^(ArrowUp|ArrowDown|PageUp|PageDown)$/.test(e.key)) held = true; diff --git a/app/ui/composer/tests/elements_behaviour.test.js b/app/ui/composer/tests/elements_behaviour.test.js index 0e80f11f..d6966188 100644 --- a/app/ui/composer/tests/elements_behaviour.test.js +++ b/app/ui/composer/tests/elements_behaviour.test.js @@ -755,3 +755,21 @@ test("a field Chromium blurs mid-redraw (still attached) does not commit then; t region.node.remove(); } }); + +test("a held spinner interrupted by a redraw (with Chromium's blur on removal) still commits on release", () => { + let state = 15; + const region = mount(composer.region.stack()); + const redraw = () => { const kids = [composer.number.md({ label: "N", value: state, onChange: (v) => { state = v; redraw(); } })]; region.set(kids); }; + redraw(); + const real = region.node.replaceChildren.bind(region.node); + region.node.replaceChildren = (...kids) => { const f = document.activeElement; if (region.node.contains(f)) { fire(f, "change"); fire(f, "blur"); } real(...kids); }; + const box = () => region.node.querySelector('[aria-label="N"]'); + box().focus(); fire(box(), "pointerdown"); + box().value = "16"; fire(box(), "change"); // a step while held: not committed yet + redraw(); // something else redraws mid-hold + delete region.node.replaceChildren; + window.dispatchEvent(new window.Event("pointerup")); + assert.equal(state, 16); + assert.equal(box().value, "16"); + region.node.remove(); +});