A focused <input type="number"> is a spinbutton, and a browser lets the mouse
wheel drive it. Measured on the running page at 1280x900 with the sidebar
open: focus the small blind and scroll, $5 becomes $5.50. Focus a seat's stack
and scroll, $500 becomes $501. Both go straight through to localStorage,
because these fields save on `input` like everything else here.
Nothing is announced and nothing looks wrong. The tool has invented a chip, on
a table whose whole purpose is that the chip count is right, and the gesture
that did it was a dealer reaching for another panel.
The rule is narrow on purpose. Every other way of driving a spinbutton is
deliberate -- the arrow keys, the stepper arrows, typing -- so scrolling is
the only one taken away. Verified after: the value holds, one ArrowUp still
moves it, typing still works, and the page still scrolls.
Blur rather than preventDefault. Cancelling the event stops the value moving
and stops the page moving with it, which trades a silent bug for a sidebar
that mysteriously will not scroll -- a problem the dealer can see and cannot
explain, instead of one they cannot see at all.
The decision lives in Money as a predicate beside selectsOnFocus, so the rule
is testable without a browser and main.js only does the blurring.
Two things checked rather than assumed. The tagName guard in the predicate is
decoration: deleting it passes every test, because nothing but an <input>
carries type="number". That is now said in the comment instead of being left
to look load-bearing. And the page's one <select> -- which way the blinds move
-- is not vulnerable: focused, hovered and scrolled 300px in Chromium it stays
where it was, so it is left out of the rule rather than defended against.
916 tests pass, 6 of them new. Four fault injections into the predicate are
caught; a fifth was applied, not caught, and turned out not to be a fault.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb