aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-28 03:50:35 -0700
committerAdam Soltys <[email protected]>2026-06-28 03:50:35 -0700
commita6e6083e4c758a9c07dc7a4ea38f880c561cb365 (patch)
treed69b672c2dc58c7adbfab6ec6ea2c6c868de0f81 /src
parent935adfe4a28cedd17f97ea3a6be7ccbafe6c7cec (diff)
downloadcoinosv3-a6e6083e4c758a9c07dc7a4ea38f880c561cb365.tar.gz
coinosv3-a6e6083e4c758a9c07dc7a4ea38f880c561cb365.zip
Stop input/select fields losing focus
Two causes, both fixed: - _scanSpMempool emitted on every WS push even when our pending set was unchanged. The mainnet mempool churns constantly, so this re-rendered the whole app every few seconds, stealing focus mid-type. Now it only emits when our pending SP set actually changes. - render() rebuilds the DOM (replaceChildren), which inherently drops focus. Added focus + caret preservation: capture the focused field's index-path and selection before the rebuild, restore them after. So any render source (poll, payment push, scan) is now invisible to someone typing. Verified via CDP: typing 'bc1q…' in the Send address, caret at 6, then forcing a re-render — focus, value, and caret all retained. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/app.js31
-rw-r--r--src/wallet.js9
2 files changed, 36 insertions, 4 deletions
diff --git a/src/app.js b/src/app.js
index 9c79ac4..2470ac8 100644
--- a/src/app.js
+++ b/src/app.js
@@ -225,7 +225,32 @@ function openHowItWorks() {
// Per-tab session nav (tab + open tx), so a refresh keeps the user's place.
const NAV_KEY = 'btc-wallet-nav';
+// Index path from #app down to a node, and back — used to re-find the focused
+// field after a full rebuild (a background re-render produces the same structure).
+function focusPath(el) {
+ const path = [];
+ for (let n = el; n && n !== root; n = n.parentNode) {
+ const p = n.parentNode;
+ if (!p) return null;
+ path.unshift(Array.prototype.indexOf.call(p.children, n));
+ }
+ return path;
+}
+function nodeAtPath(path) {
+ let n = root;
+ for (const i of path) n = n && n.children[i];
+ return n || null;
+}
+
function render() {
+ // Preserve focus + caret across the rebuild, so a background update (poll, a
+ // payment push, an SP scan) can't kick the user out of a field they're editing.
+ const a = document.activeElement;
+ let fpath = null, selStart = null, selEnd = null;
+ if (a && root.contains(a) && /^(INPUT|SELECT|TEXTAREA)$/.test(a.tagName)) {
+ fpath = focusPath(a);
+ try { selStart = a.selectionStart; selEnd = a.selectionEnd; } catch {}
+ }
const screen =
ui.screen === 'wallet'
? walletScreen()
@@ -241,6 +266,12 @@ function render() {
? howItWorksScreen()
: unlockScreen();
root.replaceChildren(screen, footer());
+ if (fpath) {
+ const el = nodeAtPath(fpath);
+ if (el && el !== a && /^(INPUT|SELECT|TEXTAREA)$/.test(el.tagName)) {
+ try { el.focus({ preventScroll: true }); if (selStart != null && el.setSelectionRange) el.setSelectionRange(selStart, selEnd); } catch {}
+ }
+ }
// Fast-poll the receive address only while the user is actually watching for a
// payment (wallet screen, Receive tab, online). Idempotent — safe each render.
wallet.setWatchReceive(ui.screen === 'wallet' && ui.tab === 'receive' && !wallet.offline);
diff --git a/src/wallet.js b/src/wallet.js
index 006441c..5943d86 100644
--- a/src/wallet.js
+++ b/src/wallet.js
@@ -602,12 +602,13 @@ export class Wallet {
// any pending that just confirmed, so an outpoint never appears twice.
const confNow = this.spUtxos.filter((u) => u.confirmed);
const confNowIds = new Set(confNow.map((u) => `${u.txid}:${u.vout}`));
- const prevPending = this.spUtxos.length - confNow.length;
+ const oldPendingIds = new Set(this.spUtxos.filter((u) => !u.confirmed).map((u) => `${u.txid}:${u.vout}`));
const freshPending = pending.filter((p) => !confNowIds.has(`${p.txid}:${p.vout}`));
+ // Only emit when our pending set actually changed — the mempool churns
+ // constantly, and a no-op emit would re-render and steal input focus.
+ const changed = freshPending.length !== oldPendingIds.size || freshPending.some((p) => !oldPendingIds.has(`${p.txid}:${p.vout}`));
this.spUtxos = [...confNow, ...freshPending];
- if (freshPending.length !== prevPending || freshPending.length) this._recomputeBalanceFromChains();
- this.saveCache();
- this.emit();
+ if (changed) { this._recomputeBalanceFromChains(); this.saveCache(); this.emit(); }
}
} finally {
this._spMempoolBusy = false;