From f19914804669aa375c762505abfc882d7708a7aa Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Tue, 1 Sep 2026 14:40:04 -0600 Subject: [PATCH] A pre-filled amount is a suggestion, so reaching for the box replaces it Every amount box in this tool opens pre-filled: the raise pad offers the minimum, Add chips offers a buy-in, the seat editor offers the stack a player already has. A dealer taps one because they want a different number -- and a tap puts the caret after the suggestion rather than over it. Driven on an emulated phone: raise pad shows 4 tap, type "60" -> 460 add chips shows 200 tap, type "50" -> 20050 seat stack shows 200 tap, type "175" -> 200175 The raise pad is the most time-critical control in the tool, and 460 is a number the dealer never said. The all-in warning happens to catch that one against a $200 stack; against a deeper stack it is simply accepted, and the pot is wrong from then on. Number boxes now select what is in them when they take focus, so the first digit typed replaces the suggestion. raise pad 4 -> "60" add chips 200 -> "50" seat stack 200 -> "175" a name Cal -> "CalX" That last line is the point of the rule. Only number boxes: type=number, or the type=text/inputmode=decimal pair the raise pad uses to get a keypad without spinner arrows. A name or a note is a thing you edit, and selecting all of it on focus would be the same mistake pointing the other way. Delegated from the document rather than wired per field. The raise pad, the add-chips sheet and the seat editor all rebuild their inputs on every render, and a box added later should not have to remember to ask for this. The select is deferred a tick. Doing it inside the focus handler loses the race with the browser placing its own caret, and the selection collapses again the moment the tap completes. 10 tests (224 total) on the predicate, including the ones that must stay false -- name, note, colour swatch, checkbox, select, and an input with no type stated. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb --- .../wwwroot/js/casino/holdem/main.js | 23 ++++++++ .../wwwroot/js/casino/holdem/money.js | 18 +++++++ tests/holdem/selectsOnFocus.test.js | 53 +++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 tests/holdem/selectsOnFocus.test.js diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js index 0781bc0..c5da796 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js @@ -394,6 +394,29 @@ document.addEventListener("DOMContentLoaded", () => { Toast.show("Table reset.", { duration: 2500 }); }); + // A pre-filled amount is a suggestion, so reaching for the box replaces it. + // + // Delegated from the document rather than wired per field: the raise pad, the + // add-chips sheet and the seat editor all build their inputs fresh every render, + // and a box added later should not have to remember to ask for this. + document.addEventListener("focusin", event => { + const field = event.target; + if (!Money.selectsOnFocus(field)) return; + if (!field.value) return; + // After the browser has finished placing its own caret, or it wins the race + // and the selection collapses again the moment the tap completes. + setTimeout(() => { + if (document.activeElement !== field) return; + try { + field.select(); + } catch { + // A number input refuses setSelectionRange in some browsers; select() + // is the one that works on both, and if neither does the field is + // simply left as it was. + } + }, 0); + }); + document.getElementById("resolvePotBtn").addEventListener("click", openPotWizard); [ diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/money.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/money.js index 79b41fd..70da18d 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/money.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/money.js @@ -48,6 +48,24 @@ const Money = { return (this.toCents(amount) / 100).toFixed(2); }, + // Should focusing this field select what's already in it? + // + // Every amount box in this tool opens pre-filled with a suggestion: the raise pad + // offers the minimum, Add chips offers a buy-in, the seat editor offers the stack + // it already has. A dealer taps the box because they want a *different* number, + // and a tap puts the caret after the suggestion instead of over it. Measured on a + // phone: the raise pad showing 4, tap, type "60" — the field reads 460. Add chips + // showing 200, tap, type "50" — 20050. + // + // Only number boxes. A name or a note is a thing you edit, and selecting all of + // it on focus would be the same mistake pointing the other way. + selectsOnFocus(field) { + if (!field || field.tagName !== "INPUT") return false; + const type = String(field.type || "").toLowerCase(); + if (type === "number") return true; + return type === "text" && String(field.inputMode || "").toLowerCase() === "decimal"; + }, + // "+$40.00" / "-$40.00" rather than "$-40.00" — a standings column is read at a // glance, and the sign belongs out front where the eye lands first. formatSigned(amount) { diff --git a/tests/holdem/selectsOnFocus.test.js b/tests/holdem/selectsOnFocus.test.js new file mode 100644 index 0000000..9cc4390 --- /dev/null +++ b/tests/holdem/selectsOnFocus.test.js @@ -0,0 +1,53 @@ +// Which fields replace their contents when a dealer reaches for them. +const test = require("node:test"); +const assert = require("node:assert"); +const { Money } = require("./load"); + +// Enough of an input to answer the question, without a DOM. +const field = (props = {}) => ({ tagName: "INPUT", type: "text", inputMode: "", ...props }); + +test("a number box selects", () => { + assert.equal(Money.selectsOnFocus(field({ type: "number" })), true); +}); + +test("a decimal keypad box selects", () => { + // The raise pad's amount is type=text with inputmode=decimal, so the phone shows + // a number pad without the browser's spinner arrows. + assert.equal(Money.selectsOnFocus(field({ type: "text", inputMode: "decimal" })), true); +}); + +test("a name does not", () => { + // Selecting all of a name on focus is the same mistake pointing the other way: + // a name is a thing you edit, not a suggestion you replace. + assert.equal(Money.selectsOnFocus(field({ type: "text" })), false); +}); + +test("a colour swatch does not", () => { + assert.equal(Money.selectsOnFocus(field({ type: "color" })), false); +}); + +test("a checkbox does not", () => { + assert.equal(Money.selectsOnFocus(field({ type: "checkbox" })), false); +}); + +test("a textarea does not, whatever it is asked", () => { + assert.equal(Money.selectsOnFocus({ tagName: "TEXTAREA", type: "text", inputMode: "decimal" }), false); +}); + +test("a select does not", () => { + assert.equal(Money.selectsOnFocus({ tagName: "SELECT", type: "select-one" }), false); +}); + +test("case doesn't matter", () => { + assert.equal(Money.selectsOnFocus(field({ type: "NUMBER" })), true); + assert.equal(Money.selectsOnFocus(field({ type: "TEXT", inputMode: "DECIMAL" })), true); +}); + +test("nothing at all is not a field", () => { + assert.equal(Money.selectsOnFocus(null), false); + assert.equal(Money.selectsOnFocus(undefined), false); +}); + +test("a field with no type stated is a text box, so it does not", () => { + assert.equal(Money.selectsOnFocus({ tagName: "INPUT" }), false); +});