Compare commits

..
Author SHA1 Message Date
jheapsandClaude Opus 5 f199148046 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 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-01 14:40:04 -06:00
jheapsandClaude Opus 5 38536a5c0c Ask about the thumb, not the window width
There is a careful touch-target pass in this stylesheet -- 44px fields, 44px delete
buttons, a padded hit area for the rulings link. Every rule in it sits behind
`max-width: 700px`.

A phone held sideways is 844px wide.

So none of that work applied to the device it was written for, half the time. Measured
on an emulated touch device with the sidebar open -- twelve controls under 44px at
844x390 and again at 915x412:

  the rulings link            91x14
  the dedicated-dealer box    13x13
  chip-colour delete          28px wide
  blinds-direction select     32px tall
  five panel headers          38px

The rules are restated against `pointer: coarse`. The width-scoped block keeps its
layout changes -- one column, stacked dialogs -- because those really are about how
much room there is. This is only about what a thumb can hit, so it asks about the
thumb.

  844x390 touch   12 controls under 44px -> 1
  915x412 touch   12 -> 1
  390x844 touch    6 -> 1
  360x640 touch    6 -> 1

The one left is the bare 24x24 checkbox, whose row is 44px tall -- the row is the
target, which is the existing design and the only thing that works for a checkbox.

Two things were too small on every device, not just touch, and are fixed outright.
The panel headers -- the sidebar's entire navigation -- were 38px everywhere, phones
included; the pass below never reached them. And the rulings link was 91x14, which
misses the 24px minimum with a mouse as well as with a thumb. Under 24px anywhere is
now zero, at every size measured.

Two traps worth recording. A <summary> only draws its disclosure triangle while it is
display: list-item, so the headers get there on padding rather than a flex box. And a
<summary> is content-box, so `min-height: 44px` was *added* to the padding and gave
71px -- the same box-model trap as the header buttons one PR ago, in a different tag.
Both are stated in the CSS where the next person will hit them.

214 tests still passing; layout audit and contrast clean at six sizes across touch and
pointer contexts; no console errors; panels still toggle and the triangle is still
there.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-01 14:24:07 -06:00
jheapsandClaude Opus 5 286a924fe4 Give buttons and links the same box
The header on a portrait phone was 227px -- a quarter of the screen -- before the
table started, and at 360x640 the bottom of the table sat 16px behind the action bar
with the gesture hint off screen entirely.

Chasing where the pixels went turned up something better than a tight header.

.holdemBtn is worn by three <button>s and two <a>s. The class never stated a box
model, and the browser does not give those two tags the same one: Chromium makes a
button border-box and leaves a link content-box. So `min-height: 46px` on the header
row came out as 46px on the buttons and 46 + 16 padding + 2 border = 64px on the
links -- and a flex row is as tall as its tallest item, so all five became 64px.

A 46px touch target was costing 64px of screen, twice over, because the header wraps
to two rows on a phone.

  390x844   header 227 -> 191   buttons 64 -> 46
  412x915   header 227 -> 191
  430x932   header 227 -> 191
  360x640   header 173 -> 137   table clearance -16px -> +20px, so the felt now
                                clears the action bar instead of running under it

The other half is the one I did not go looking for. The 46px floor only exists under
the 700px breakpoint, and the accidental stretch was the only reason anything above
it was big enough to press. Once the boxes agree, a landscape phone -- wider than the
breakpoint, still a thumb -- would have been left with 42px buttons. So the 44px
floor is stated on the class itself. Measured across the page:

  844x390   13 .holdemBtn under 44px -> 0
  915x412   13 -> 0
  1280x900  13 -> 0

That is every button on the page, not just the header: the panel buttons, the pot
wizard, the confirm dialogs.

What it costs: 844x390 loses 2px of table clearance, -5 to -7, because its header
grows with the buttons. That size already has the bottom seat behind the bar (#63),
and 13 unpressable controls was the worse of the two problems.

214 tests still passing; layout audit and contrast clean at 9 sizes; no console
errors; every header button still hit-testable at 46x121 and 46x112.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-01 14:14:04 -06:00
jheapsandClaude Opus 5 3e9e2e899d Stop the big blind's own chip from covering the BB badge
On a phone the badge marking the big blind was not hard to read. It was not there.
The chip showing their $2 sat on top of it.

Both live on the same spoke. Bets sit between a seat and the pot, so on the bottom
half of the ring the chip comes down onto the seat's top edge, which is exactly where
the badge was -- and the chip is a layer above it.

  390x844, six seats:  badge 24x12 = 288px², chip covered 196px² of it
                       badge z-index 2, chip z-index 3
                       the badge's own centre hit-tested as the chip

So seats on the bottom of the ring wear their badge underneath instead, on the edge
the pot is not on. And badges now outrank chips regardless, so a layout this does not
anticipate cannot hide one again: which blind a seat is posting is the one thing on
that puck nothing else on the felt repeats, while a bet chip's amount is on the seat's
own total line as well.

Measured across 20 table sizes -- badge area lost to bet chips:

  430x932, 390x844, 360x640 at 2 and 6 seats    35% -> 0%
  8 seats                                        4-12%, unchanged: the blinds land
                                                 on the top arc there, where the
                                                 badge was already out of the way
  desktop and tablet                             0% either way, more room

Two other placements were measured and rejected. Tucking the badge inside the seat's
bottom edge put 38-42% of it on the seat's own stack line at every size including
desktop -- trading a chip covering the badge for the badge covering the money. Leaving
it on top and only raising its z-index worked everywhere, but leaves the badge sitting
on the chip rather than clear of it; the z-index is kept as a backstop, the move is
what does the work.

What it costs

At 360x640 heads-up the bottom seat's badge moves down into the action bar, where it
was visible before. That seat is already substantially behind the bar at that size --
the narrow layout reserves scroll room rather than capping the felt, which #63
measured and settled on -- but this change is what put the badge there, so I am
saying so rather than leaving it to be found. Every other size and seat count is
clean, including 360x640 at six and nine seats.

An instrument note, because it nearly became a finding. Bet chips are
pointer-events: none above 900px and auto below it, so elementFromPoint reports them
as covering things on a phone and as invisible on a desktop. The 35% figures above
are geometric overlap and a cropped screenshot of the rendered pixels, not hit tests.
Three "chip covered by DIV" results I saw earlier were that quirk and are not real.

214 tests still passing; layout audit and contrast clean at 8 sizes x 3 seat counts;
no console errors.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-01 14:00:34 -06:00
jheapsandClaude Opus 5 96cd7684f0 Buying chips puts you back in the game
Handing over cash and being dealt in are one event at a real table. Here they were
two, and the second one was easy to miss.

Last change deals a busted player around until they rebuy. So the dealer takes their
money, taps Add chips, and:

  Ben:  inHand false -> false     stack 0 -> 100     buyIns 100 -> 200
  toast: "Ben added $100."

He has chips, he is not in the game, and nothing on screen says so. He gets skipped
hand after hand until somebody notices and finds "Deal in" in the seat menu.

Now the buy-in does both, and says which:

  mid-hand:      "Ben added $100 - in from the next hand."   inHand true, folded true
  between hands: "Ben added $100 - dealt in."                inHand true, folded false

Folding him for the hand in progress is the rule "Deal in" already follows -- you
can't buy into a pot you have not been paying for -- so the two doors into the game
behave the same way. It is one undoable step: undo takes back the chips, the cash
record and the seat.

Not for a seat that was sat out while holding chips. That is a dealer who sat someone
out on purpose and is topping them up, and dealing them back in would be the tool
guessing at something it was not told. The test reads the seat's own recorded stack
rather than stackOf(), which substitutes the table's starting stack and would make a
seat with nothing recorded look like it was holding chips.

  busted, sat out          -> back in
  new chair, no stack yet  -> in
  sat out holding $100     -> stays out
  already in the hand      -> nothing to do, including an all-in at zero chips

8 tests (214 total).

Driven through the UI: the rebuy, the toast, the next hand actually dealing him in
(4 players, live 4), and undo putting all three fields back.

Found on the way, not fixed here

Buying in on a seat with no stack of its own credits the table's starting stack *plus*
the buy-in. A new player handed $100 on a table with a $100 default ends up with $200
of chips against $100 of recorded cash.

I am leaving it because the right answer is genuinely unclear and it is not mine to
pick. A blank stack means "no chips recorded" to the bank check, which counts it as
zero, and "the table's starting stack" to the betting code and the felt, which show
it as $100. Both are defensible for their own job, and which one a buy-in should
follow decides whether a casual dealer who never types stacks gets $200 or $100 after
a rebuy. Worth saying out loud rather than quietly picking one.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-01 13:39:56 -06:00
7 changed files with 340 additions and 4 deletions
+119 -1
View File
@@ -212,6 +212,20 @@ button,
}
.holdemBtn {
/* Stated, because this class is worn by both <button> and <a> and the browser
does not give them the same box. Chromium's own stylesheet makes a button
border-box and leaves a link content-box, so `min-height: 46px` on the header
row came out as 46px on the buttons and 64px on the two links — and the row
stretches to its tallest item, so every button became 64px too. A 46px touch
target was quietly costing 64px of a phone screen, twice over, because the
header wraps to two rows. */
box-sizing: border-box;
/* And the touch floor is stated here rather than only under a width breakpoint.
It used to arrive by accident — the content-box links were 64px tall and the
row stretched everything to match — so once the boxes agree, a landscape phone
(wider than the phone breakpoint, still a thumb) would have been left with 42px
buttons. 44px is the floor everywhere; the narrow layout still asks for 46. */
min-height: 44px;
background: linear-gradient(180deg, #2e2e2e, #232323);
color: #eee;
border: 1px solid #3a3a3a;
@@ -695,6 +709,23 @@ button,
display: inline-block;
}
/* Seats on the bottom of the ring wear their badge underneath, because that's the
edge the pot isn't on. Bets sit between a seat and the pot, so on the bottom arc
the chip comes down onto the seat's top edge — which is exactly where this badge
was, and the chip is a layer above it. See render.js for the measurement. */
.seat.seatBelowCentre .blindBadge {
top: auto;
bottom: -9px;
}
/* And the badge outranks the chips regardless, so a layout this doesn't anticipate
can't hide it again. Which blind a seat is posting is the one thing on that puck
nothing else on the felt repeats — a bet chip's amount is on the seat's own total
line as well. Measured across 12 table sizes: no chip loses its amount to this. */
.blindBadge {
z-index: 4;
}
/* Both badges failed WCAG AA against their own gradients — measured at the worst
stop, the small blind was 3.52:1 and the big blind 2.15:1, in 9px bold text a
dealer is meant to read at a glance across a table.
@@ -982,9 +1013,24 @@ button,
text-align: center;
}
/* A bare checkbox ships at 13x13, which misses the 24px minimum with a mouse as well
as with a thumb. The row around it is the real target on touch, but the box itself
should be findable on any device. */
#dedicatedDealerToggle {
width: 24px;
height: 24px;
}
.linkBtn {
background: none;
border: none;
/* An underlined word was a 91x14 target, which misses the 24px minimum with a
mouse as well as with a thumb. The coarse-pointer block takes it to 44; this is
the floor everywhere else, and it changes nothing about how it looks. */
display: inline-flex;
align-items: center;
box-sizing: border-box;
min-height: 24px;
padding: 0;
color: #d8b46a;
font-family: inherit;
@@ -1439,7 +1485,16 @@ button,
.sidebarPanel summary {
cursor: pointer;
padding: 0.7rem 0;
/* These headers are the sidebar's whole navigation and were 38px tall on every
device, phones included — the touch pass further down never reached them.
Padding rather than a flex box with min-height, because a <summary> only draws
its disclosure triangle while it is display: list-item. */
padding: 0.85rem 0;
/* A <summary> is content-box, so min-height would have been *added* to the padding
— measured at 71px before this line went in. Same trap the header buttons were
in, one file over. */
box-sizing: border-box;
min-height: 44px;
font-size: 0.9rem;
color: #eee;
letter-spacing: 0.03em;
@@ -3154,3 +3209,66 @@ textarea:focus-visible {
font-size: 0.72rem;
color: #9a9a9a;
}
/* ---------- Touch targets on a wide touch screen ----------
Every touch-target rule in this file sits behind `max-width: 700px`, and width was
never the question. A phone held sideways is 844px wide, so none of that work
applied to it. Measured on an emulated touch device at 844x390, with the sidebar
panels open: twelve controls under 44px, including the rulings link at 91x14 and
the dedicated-dealer checkbox at 13x13.
Restated here against the pointer instead of the viewport. The width-scoped block
keeps its layout changes — one column, stacked dialogs — because those really are
about how much room there is. This is only about what a thumb can hit, so it asks
about the thumb. */
@media (pointer: coarse) {
.panelBody input:not([type="checkbox"]):not([type="color"]),
.subPanelBody input:not([type="checkbox"]):not([type="color"]),
.panelBody select {
font-size: 16px;
min-height: 44px;
padding: 0.4rem 0.6rem;
}
.panelBody .holdemBtnSmall,
.handHistoryActions .holdemBtn,
.toastAction {
min-height: 44px;
}
/* Destructive and tiny is the worst combination: these delete a chip colour or a
custom field, and are 28px wide. */
.removeChipColorBtn,
.removeFieldBtn {
min-width: 44px;
min-height: 44px;
}
/* flex-shrink would take a stated width straight back off again. */
.chipColorInput {
flex: none;
width: 44px;
height: 44px;
}
/* A bare checkbox can't grow into a target, but the row it sits in can — the
label is what a tap actually lands on. */
.panelBody .fieldRow {
min-height: 44px;
}
#dedicatedDealerToggle {
width: 24px;
height: 24px;
}
/* An underlined word is a 14px-tall target; give it room without making it look
like a button. */
.linkBtn {
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 0 0.4rem;
}
}
@@ -90,6 +90,28 @@ const Betting = {
};
},
// Does handing this seat chips also put them back in the game?
//
// At a real table those are one event, and here they were two: a busted player got
// their chips and stayed sat out, silently skipped hand after hand until somebody
// noticed and found "Deal in" in the seat menu. Nothing said so — the toast just
// read "Ben added $100".
//
// Not for a seat that was sat out while *holding* chips. That's a dealer who sat
// somebody out on purpose and is topping them up, and dealing them back in would be
// the tool guessing at something it wasn't told. Only a seat with no stack of its
// own: busted at zero, or a chair being bought into for the first time.
//
// Deliberately reads player.stack rather than stackOf(), which substitutes the
// table's starting stack for a seat that has none — the question here is what this
// seat was actually recorded as holding.
buyInDealsThemIn(player) {
if (!player || player.inHand) return false;
const own = player.stack;
const recorded = own !== null && own !== undefined && String(own).trim() !== "";
return !recorded || Number(own) <= this.EPSILON_CHIPS;
},
// "bet" or "raise" — what putting more in is actually called right now.
//
// A wager into a pot nobody has wagered into is a bet; everything after that is a
+42 -3
View File
@@ -140,11 +140,27 @@ document.addEventListener("DOMContentLoaded", () => {
editSeat: index => Render.toggleSeatEditor(index),
addChips: index => AddChips.open(state, index, (seatIndex, amount) => {
seatAction(seatIndex, `${nameOf(seatIndex)} added ${Money.format(amount)}.`, player => {
// Buying chips puts you back in the game — see Betting.buyInDealsThemIn.
const dealBackIn = Betting.buyInDealsThemIn(state.seats[seatIndex]);
const handRunning = !Betting.onlyBlindsIn(state) || Betting.potSoFar(state) > 0;
const message = `${nameOf(seatIndex)} added ${Money.format(amount)}` +
(dealBackIn
? handRunning ? " — in from the next hand." : " — dealt in."
: ".");
seatAction(seatIndex, message, p => {
// Chips in front and cash in are different questions — the stack moves
// every hand, what they've handed over only moves when they buy.
player.stack = Money.round(Betting.stackOf(state, player) + amount);
player.buyIns = Money.round((Number(player.buyIns) || 0) + amount);
p.stack = Money.round(Betting.stackOf(state, p) + amount);
p.buyIns = Money.round((Number(p.buyIns) || 0) + amount);
if (dealBackIn) {
p.inHand = true;
// Same rule "Deal in" follows: you can't buy into a pot you haven't
// been paying for, and `folded` is exactly that state for one hand.
p.folded = handRunning;
}
});
}),
};
@@ -378,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);
[
@@ -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) {
@@ -171,6 +171,16 @@ const Render = {
node.style.top = `${pos.top}%`;
node.dataset.seatIndex = String(index);
// Which side of the seat the pot is on, which is the side its bet chip
// arrives from — the chip sits on the same spoke, closer in. For a seat on
// the top of the ring that's from below; on the bottom, from above.
//
// The blind badge has to be on the other side or the chip lands on top of
// it. Measured on a 390px phone: the big blind's own $2 chip covered 196 of
// the badge's 288 square pixels, and it sits a layer higher, so the marker
// telling the dealer who the big blind is was simply gone.
node.classList.toggle("seatBelowCentre", pos.top > 50);
node.querySelector(".seatChairNumber").textContent = index + 1;
const dealerChip = node.querySelector(".dealerChip");
+76
View File
@@ -0,0 +1,76 @@
// Whether handing a seat chips also puts them back in the game.
const test = require("node:test");
const assert = require("node:assert");
const { Betting, makeTable } = require("./load");
test("a busted player who has been sat out comes back in", () => {
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
state.seats[1].stack = 0;
Betting.beginHand(state); // the deal sits them out
assert.equal(state.seats[1].inHand, false);
assert.equal(Betting.buyInDealsThemIn(state.seats[1]), true);
});
test("a chair being bought into for the first time comes in", () => {
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 });
state.seats[4].name = "Eve";
state.seats[4].stack = null;
assert.equal(Betting.buyInDealsThemIn(state.seats[4]), true);
});
test("a seat sat out while holding chips stays out", () => {
// A dealer sat them out on purpose and is topping them up. Dealing them back in
// would be the tool guessing at something it wasn't told.
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
state.seats[2].inHand = false;
assert.equal(Betting.buyInDealsThemIn(state.seats[2]), false);
});
test("somebody already in the hand is left exactly where they are", () => {
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
assert.equal(Betting.buyInDealsThemIn(state.seats[0]), false);
// Including a player who is in but out of chips — they're all-in, not sat out.
state.seats[2].stack = 0;
assert.equal(state.seats[2].inHand, true);
assert.equal(Betting.buyInDealsThemIn(state.seats[2]), false);
});
test("a blank stack field reads as nothing recorded, not as a number", () => {
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 });
state.seats[3].name = "Dee";
state.seats[3].stack = "";
assert.equal(Betting.buyInDealsThemIn(state.seats[3]), true);
});
test("it doesn't ask the table what a missing stack would have been", () => {
// stackOf substitutes the table's starting stack for a seat that has none, which
// would make a seat with nothing recorded look like it was holding chips.
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 });
state.defaultStack = 500;
state.seats[4].name = "Eve";
state.seats[4].stack = null;
assert.equal(Betting.stackOf(state, state.seats[4]), 500);
assert.equal(Betting.buyInDealsThemIn(state.seats[4]), true);
});
test("asking doesn't change anything", () => {
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
state.seats[2].stack = 0;
state.seats[2].inHand = false;
const before = JSON.stringify(state);
Betting.buyInDealsThemIn(state.seats[2]);
assert.equal(JSON.stringify(state), before);
});
test("nothing to decide about a seat that isn't there", () => {
assert.equal(Betting.buyInDealsThemIn(null), false);
assert.equal(Betting.buyInDealsThemIn(undefined), false);
});
+53
View File
@@ -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);
});