Follow the browser's text size instead of sliding out from under it
Every length in this stylesheet that isn't a structural clearance is in `rem`. That is the right way round — it means the page follows the browser's font size, which is what a phone's text-size slider changes — and it means there was a whole axis of behaviour that 320 audits had never once touched, because every one of them ran at the default 16px. The sweep gets a third pass: the same surfaces on the eight phone-shaped viewports at 20px of root text, which is 125% on an Android slider. Everything it found on its first run was caught by a check that had been sitting there for months with nothing to say. The page slid sideways. Three header buttons that will not break and will not shrink — `white-space: nowrap` and a `min-width: auto` floor apiece — were told `flex-wrap: nowrap`, so when they stopped fitting they had nowhere to go: "Resolve Pot" ended 23px past the right edge of a 360px window and put a horizontal scrollbar under the whole document; at 24px, 60px past the edge of a 390px one. They take a second row now on the day they need one, and still fit on the first and still grow into it at any ordinary size. Figures painted over the seat next door. Which of the two type steps an amount got was decided by counting characters — eight for one, ten for the other — which is a guess at how wide a figure is that happens to hold at one text size and one puck width. At 20px on a 44px puck, "$1,200" is six characters, took no step at all, and wanted 38px of the 34 it had. Render.fitAmounts measures now, in the batched passes fitSeatNames already uses, and takes the separators out before it takes a step of type: "$1200" at full size beats "$1,200" at three-quarters of it. And the pucks grew, which the ring cannot answer. The row heights are px tokens so an amount stepping its type down shortens its digits and not its row — but a token is a floor, not a ceiling. At 125% the puck went from 55px tall to 65 and twelve of them came down on each other, because at the 44px floor a thumb needs there is nowhere narrower to search. The two sizes a puck is written at are capped at exactly the sizes they have always been drawn at, so nothing changes for anybody at an ordinary text size — and the felt is a diagram, so the rule that already governed a puck's width now governs its height. Roomy pucks are exempt; they have the room. The same figure is in Standings in full, at whatever size its owner asked for. Two smaller things on the way. The landscape rail opened at its top, so its overflow came off the bottom — where Cancel and Raise to are; it opens at the end now, which is the same reason it is anchored low at all. And the check that called those buttons unreachable was wrong: the rail scrolls on purpose, and a sweep that cries wolf is a sweep nobody reads. 418 audits, clean. One shape it is not held to, with the arithmetic in runUiAudit.js: twelve players all-in on a 360x640 phone leaves the felt 290x163 once the bar has taken 283px of the 640, and twelve 44x62 pucks do not fit around an ellipse 163px deep. Reported as a note every run rather than a failure every run. 1026 unit tests. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
@@ -1055,10 +1055,35 @@ button,
|
|||||||
that height spends room that was already spent. What pays for the third row is the
|
that height spends room that was already spent. What pays for the third row is the
|
||||||
second line of name going away above: uniform-with-two-lines was 88px, which an earlier
|
second line of name going away above: uniform-with-two-lines was 88px, which an earlier
|
||||||
round measured and rightly refused. Uniform-with-one is 71. */
|
round measured and rightly refused. Uniform-with-one is 71. */
|
||||||
|
/* The two sizes a puck is written at, named so that the steps below them are steps *of*
|
||||||
|
them rather than separate sizes to be kept in agreement — and capped, which is the one
|
||||||
|
place in this stylesheet where type does not follow the browser's font size.
|
||||||
|
|
||||||
|
Why it has to be. Every row above is a `px` token so that an amount stepping its type
|
||||||
|
down shortens its digits and not its row — but a token is a floor, not a ceiling, and
|
||||||
|
text bigger than its row simply makes the row taller. Turn a phone's text up to the
|
||||||
|
125% its slider offers and --fs-2xs goes from 10px to 12.5, the two money rows go from
|
||||||
|
14px each to 17.5, and the puck grows with them. At 150% the figures were being cut
|
||||||
|
off by the puck's own edge; on the tightest ring, where twelve pucks are already at
|
||||||
|
the 44px a thumb needs and there is nowhere narrower to go, they came down on each
|
||||||
|
other instead — the sweep measured seat 3's "$1,495" five-sevenths under seat 4's BB
|
||||||
|
badge at every phone in the list.
|
||||||
|
|
||||||
|
`min(…, Npx)` is exactly the size each of these has always been drawn at, so nothing
|
||||||
|
changes for anybody at an ordinary text size: the cap only ever engages where the
|
||||||
|
alternative is a figure clipped in half or two seats printed on top of each other.
|
||||||
|
And what it holds is the one thing on the page that is also somewhere that does
|
||||||
|
scale — the Standings panel lists every stack in full, at whatever size its owner
|
||||||
|
asked for. The felt is a diagram; its cells are sized by the table, and their writing
|
||||||
|
has to fit inside them. That rule already governed a puck's width. This is the same
|
||||||
|
rule for its height.
|
||||||
|
|
||||||
|
.seatsRoomy is the exception, below: a puck past the ordinary ceiling has the room. */
|
||||||
#seatsLayer {
|
#seatsLayer {
|
||||||
--seatRowName: 15px; /* 12px of name at 1.2 */
|
--seatRowName: 15px; /* 12px of name at 1.2 */
|
||||||
--seatRowMoney: 14px; /* 10px of figures at 1.4 */
|
--seatRowMoney: 14px; /* 10px of figures at 1.4 */
|
||||||
--seatNameSize: var(--fs-sm);
|
--seatNameSize: min(var(--fs-sm), 12px);
|
||||||
|
--seatAmountSize: min(var(--fs-2xs), 10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* One line, and the ellipsis is not what saves it.
|
/* One line, and the ellipsis is not what saves it.
|
||||||
@@ -1155,7 +1180,7 @@ button,
|
|||||||
.seatPuckTotal {
|
.seatPuckTotal {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: var(--sp-0);
|
margin-top: var(--sp-0);
|
||||||
font-size: var(--fs-2xs);
|
font-size: var(--seatAmountSize);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #a9f5b8;
|
color: #a9f5b8;
|
||||||
background: rgba(9, 255, 0, 0.1);
|
background: rgba(9, 255, 0, 0.1);
|
||||||
@@ -1498,10 +1523,12 @@ button,
|
|||||||
#seatsLayer.seatsTight {
|
#seatsLayer.seatsTight {
|
||||||
--seatRowName: 12px; /* 10px of name at 1.2 */
|
--seatRowName: 12px; /* 10px of name at 1.2 */
|
||||||
--seatRowMoney: 12px; /* and 10px of figures at 1.2, not 1.4 */
|
--seatRowMoney: 12px; /* and 10px of figures at 1.2, not 1.4 */
|
||||||
--seatNameSize: var(--fs-2xs);
|
--seatNameSize: min(var(--fs-2xs), 10px);
|
||||||
|
--seatAmountSize: min(var(--fs-2xs), 10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.seatsTight .seatPuckTotal {
|
.seatsTight .seatPuckTotal,
|
||||||
|
.seatsTight .seatPuckStack {
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1510,10 +1537,6 @@ button,
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.seatsTight .seatPuckStack {
|
|
||||||
font-size: var(--fs-2xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Not wrapping is only half an answer: a line that cannot wrap and still does not fit
|
/* Not wrapping is only half an answer: a line that cannot wrap and still does not fit
|
||||||
paints over the seat next door instead. So on the tightest pucks the mark gives up
|
paints over the seat next door instead. So on the tightest pucks the mark gives up
|
||||||
most of its letter-spacing, which is 3.6px of the 36 it wanted.
|
most of its letter-spacing, which is 3.6px of the 36 it wanted.
|
||||||
@@ -1543,11 +1566,7 @@ button,
|
|||||||
--seatRowName: 18px; /* 15px of name at 1.2 */
|
--seatRowName: 18px; /* 15px of name at 1.2 */
|
||||||
--seatRowMoney: 17px; /* 12px of figures at 1.4 */
|
--seatRowMoney: 17px; /* 12px of figures at 1.4 */
|
||||||
--seatNameSize: var(--fs-md);
|
--seatNameSize: var(--fs-md);
|
||||||
}
|
--seatAmountSize: var(--fs-sm);
|
||||||
|
|
||||||
.seatsRoomy .seatPuckStack,
|
|
||||||
.seatsRoomy .seatPuckTotal {
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.seatsRoomy .seatChairNumber {
|
.seatsRoomy .seatChairNumber {
|
||||||
@@ -3841,7 +3860,7 @@ body.hasActionBar #holdemApp {
|
|||||||
600 — so the hierarchy is intact; it is just carried by size and weight instead of by
|
600 — so the hierarchy is intact; it is just carried by size and weight instead of by
|
||||||
fading the number underneath it. */
|
fading the number underneath it. */
|
||||||
.seatPuckStack {
|
.seatPuckStack {
|
||||||
font-size: var(--fs-2xs);
|
font-size: var(--seatAmountSize);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
margin-top: var(--sp-0);
|
margin-top: var(--sp-0);
|
||||||
@@ -3868,21 +3887,30 @@ body.hasActionBar #holdemApp {
|
|||||||
border-color: rgba(255, 207, 107, 0.45);
|
border-color: rgba(255, 207, 107, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tournament-sized amounts. A number has no spaces to break on, so an amount too
|
/* The two steps an amount comes down when it is too wide for its puck. A number has no
|
||||||
wide for the 64px puck neither wraps nor ellipsises — it draws straight out over
|
spaces to break on, so an amount too wide neither wraps nor ellipsises — it draws
|
||||||
the seat next to it. These step the size down so every digit stays on its own
|
straight out over the seat next to it. Shrinking rather than abbreviating is
|
||||||
puck. Shrinking rather than abbreviating is deliberate: "$1.25M" would fit more
|
deliberate: "$1.25M" would fit more comfortably, but a dealer reading a rounded stack
|
||||||
comfortably, but a dealer reading a rounded stack off the felt is a worse failure
|
off the felt is a worse failure than a small one, and the exact figure is what the
|
||||||
than a small one, and the exact figure is what the tool is for. */
|
tool is for.
|
||||||
|
|
||||||
|
Both are steps *of* --seatAmountSize rather than sizes of their own, so they follow
|
||||||
|
the tight ring's cap down instead of stepping back over it — and so the ladder is one
|
||||||
|
number with two fractions under it rather than three sizes to keep in agreement.
|
||||||
|
|
||||||
|
Which step a figure gets is measured, not counted. See Render.fitAmounts: the old
|
||||||
|
rule was eight characters for the first and ten for the second, which is a guess at
|
||||||
|
how wide a figure is, and it stopped being true the moment anybody turned their
|
||||||
|
phone's text up. */
|
||||||
.seatPuckStack.puckAmountLong,
|
.seatPuckStack.puckAmountLong,
|
||||||
.seatPuckTotal.puckAmountLong {
|
.seatPuckTotal.puckAmountLong {
|
||||||
font-size: var(--fs-2xs);
|
font-size: calc(var(--seatAmountSize) * 0.85);
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.seatPuckStack.puckAmountHuge,
|
.seatPuckStack.puckAmountHuge,
|
||||||
.seatPuckTotal.puckAmountHuge {
|
.seatPuckTotal.puckAmountHuge {
|
||||||
font-size: 0.44rem;
|
font-size: calc(var(--seatAmountSize) * 0.7);
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4656,10 +4684,20 @@ body.hasActionBar #toastHost {
|
|||||||
/* Takes the row rather than shrink-wrapping to the right of it. Sized to their
|
/* Takes the row rather than shrink-wrapping to the right of it. Sized to their
|
||||||
labels the three buttons left 78px of nothing where the title used to be, and
|
labels the three buttons left 78px of nothing where the title used to be, and
|
||||||
that gap buys nothing — spent on the buttons instead it is about 26px of extra
|
that gap buys nothing — spent on the buttons instead it is about 26px of extra
|
||||||
target each, on the controls a dealer presses once a hand with a thumb. */
|
target each, on the controls a dealer presses once a hand with a thumb.
|
||||||
|
|
||||||
|
Wrapping, though, and this used to say `nowrap`. Three labels that will not break
|
||||||
|
and will not shrink — `white-space: nowrap` and a `min-width: auto` floor apiece —
|
||||||
|
have to go somewhere when they stop fitting, and with nowrap that somewhere was
|
||||||
|
off the side of the screen. Measured with the text turned up, which every phone
|
||||||
|
lets you do: at 20px of root text (an Android slider at 125%) "Resolve Pot" ended
|
||||||
|
23px past the right edge of a 360px window and put a horizontal scrollbar under
|
||||||
|
the whole page; at 24px it was 60px past the edge of a 390px one. Nothing is lost
|
||||||
|
by letting them take a second row on the day they need one — at any ordinary text
|
||||||
|
size they still fit on the first and still grow into it. */
|
||||||
#holdemHeaderActions {
|
#holdemHeaderActions {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: wrap;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
gap: var(--sp-1);
|
gap: var(--sp-1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,9 +247,27 @@ const ActionBar = {
|
|||||||
// After the tank timer, because whether the stopwatch and the clock button are
|
// After the tank timer, because whether the stopwatch and the clock button are
|
||||||
// on the acting line is what decides how much room the name has left.
|
// on the acting line is what decides how much room the name has left.
|
||||||
this.fitActingLine();
|
this.fitActingLine();
|
||||||
|
this.showRailEnd(host);
|
||||||
this.announce();
|
this.announce();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// A phone held sideways puts this bar down the right-hand edge as a rail, anchored
|
||||||
|
// low: the buttons sit at the bottom, where a thumb rests rather than where a hand
|
||||||
|
// has to reach up. The rail scrolls when what it holds outgrows it, which the raise
|
||||||
|
// pad does on a 360px-tall screen the moment the dealer has their text turned up —
|
||||||
|
// measured at 640x360 with 20px of root text, 407px of pad in a 358px rail.
|
||||||
|
//
|
||||||
|
// And a scroll container starts at its top. So the overflow came off the bottom,
|
||||||
|
// which is exactly where the two buttons that end a raise are: Cancel and Raise to
|
||||||
|
// were half under the edge of the screen with the hand number and the pot — the two
|
||||||
|
// things a dealer had already read — holding the room above them. Anchored low, the
|
||||||
|
// rail should open at the end for the same reason it is anchored low at all.
|
||||||
|
showRailEnd(host) {
|
||||||
|
if (!this.isRail(host)) return;
|
||||||
|
if (host.scrollHeight <= host.clientHeight) return;
|
||||||
|
host.scrollTop = host.scrollHeight;
|
||||||
|
},
|
||||||
|
|
||||||
// The empty table. One sentence and one button, in the place every other prompt in
|
// The empty table. One sentence and one button, in the place every other prompt in
|
||||||
// this tool appears, so a dealer who has never opened it before is looking at the
|
// this tool appears, so a dealer who has never opened it before is looking at the
|
||||||
// next step rather than at ten chairs labelled EMPTY.
|
// next step rather than at ten chairs labelled EMPTY.
|
||||||
|
|||||||
@@ -1495,6 +1495,7 @@ const Render = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.fitSeatNames(layer);
|
this.fitSeatNames(layer);
|
||||||
|
this.fitAmounts(this.seatAmountCells(layer));
|
||||||
|
|
||||||
// The pucks are only now what they will be drawn as, and the ring was sized
|
// The pucks are only now what they will be drawn as, and the ring was sized
|
||||||
// before any of it went in. Usually those agree — a puck's height is a stack of
|
// before any of it went in. Usually those agree — a puck's height is a stack of
|
||||||
@@ -1518,6 +1519,8 @@ const Render = {
|
|||||||
this.placeSeat(layer.children[index], index, aspect, radii);
|
this.placeSeat(layer.children[index], index, aspect, radii);
|
||||||
}
|
}
|
||||||
this.fitSeatNames(layer);
|
this.fitSeatNames(layer);
|
||||||
|
// The puck is a different width than the one these were fitted to.
|
||||||
|
this.fitAmounts(this.seatAmountCells(layer));
|
||||||
}
|
}
|
||||||
|
|
||||||
const settled = this.settleSeatsOnTable(layer, aspect, radii);
|
const settled = this.settleSeatsOnTable(layer, aspect, radii);
|
||||||
@@ -2028,7 +2031,11 @@ const Render = {
|
|||||||
: HoldemState.computeContribution(player, this.state.chipLegend, this.state.basePotAmount);
|
: HoldemState.computeContribution(player, this.state.chipLegend, this.state.basePotAmount);
|
||||||
const totalEl = node.querySelector(".seatPuckTotal");
|
const totalEl = node.querySelector(".seatPuckTotal");
|
||||||
totalEl.textContent = total > 0 ? Money.format(total) : "";
|
totalEl.textContent = total > 0 ? Money.format(total) : "";
|
||||||
this.fitAmount(totalEl);
|
// The figure as it is meant to be read, kept beside the one that gets drawn.
|
||||||
|
// fitAmounts may take the separators out of what is on screen, and the pass
|
||||||
|
// after it has to start from the grouped figure again or the puck can only ever
|
||||||
|
// get further from it.
|
||||||
|
totalEl.dataset.figure = totalEl.textContent;
|
||||||
|
|
||||||
// A player with nothing behind used to leave this line blank, so the felt drew
|
// A player with nothing behind used to leave this line blank, so the felt drew
|
||||||
// an all-in exactly like a seat whose stack nobody had entered. On a table with
|
// an all-in exactly like a seat whose stack nobody had entered. On a table with
|
||||||
@@ -2039,7 +2046,7 @@ const Render = {
|
|||||||
const allIn = Betting.isAllIn(this.state, player);
|
const allIn = Betting.isAllIn(this.state, player);
|
||||||
stackEl.textContent = empty ? "" : allIn ? "ALL IN" : stack > 0 ? Money.format(stack) : "";
|
stackEl.textContent = empty ? "" : allIn ? "ALL IN" : stack > 0 ? Money.format(stack) : "";
|
||||||
stackEl.classList.toggle("seatPuckAllIn", allIn);
|
stackEl.classList.toggle("seatPuckAllIn", allIn);
|
||||||
this.fitAmount(stackEl);
|
stackEl.dataset.figure = stackEl.textContent;
|
||||||
|
|
||||||
node.classList.toggle("seatAllIn", allIn);
|
node.classList.toggle("seatAllIn", allIn);
|
||||||
node.classList.toggle("seatActing", index === this.state.actingSeatIndex);
|
node.classList.toggle("seatActing", index === this.state.actingSeatIndex);
|
||||||
@@ -2049,35 +2056,90 @@ const Render = {
|
|||||||
node.classList.toggle("seatLive", player.inHand && !player.folded);
|
node.classList.toggle("seatLive", player.inHand && !player.folded);
|
||||||
},
|
},
|
||||||
|
|
||||||
// A seat puck is 64px of usable width. A tournament stack doesn't fit in that and,
|
// The two steps of type an amount can come down, in order, once taking the
|
||||||
// because the number has no spaces to break on, it doesn't wrap or ellipsise
|
// separators out has not been enough.
|
||||||
// either — it just draws straight out over the neighbouring seat. Stepping the
|
AMOUNT_STEPS: ["puckAmountLong", "puckAmountHuge"],
|
||||||
// font down keeps every digit, which matters: a dealer misreading a stack is a
|
|
||||||
// worse outcome than a small one, so this shrinks rather than abbreviating.
|
|
||||||
fitAmount(el) {
|
|
||||||
const formatted = el.textContent || "";
|
|
||||||
// The separators go before the type size does. "$1,234,567" is ten characters,
|
|
||||||
// which used to drop the puck to 7px — and 7px is not a size anybody reads a
|
|
||||||
// stack off a felt at, so the shrink-rather-than-abbreviate rule above was
|
|
||||||
// buying precision the dealer then couldn't see. Without its commas the same
|
|
||||||
// number is eight characters, and eight fits at 10px with every digit intact.
|
|
||||||
// Grouping is a reading aid for a column of figures; this is one number on a
|
|
||||||
// 64px puck, and the aid costs more than it gives.
|
|
||||||
const text = formatted.length >= 10 ? formatted.replace(/,/g, "") : formatted;
|
|
||||||
if (text !== formatted) el.textContent = text;
|
|
||||||
|
|
||||||
const length = text.length;
|
// Puts one amount back to the figure and the size it is meant to be read at, and
|
||||||
el.classList.toggle("puckAmountLong", length >= 8 && length < 10);
|
// hands back the grouped figure. Everything below measures from here, so a puck that
|
||||||
// Only a stack of $100,000,000 or more still lands here. It is an overflow
|
// once went small can come back — left as it was, this would be a ratchet.
|
||||||
// guard rather than a size anybody is expected to read.
|
resetAmount(el) {
|
||||||
el.classList.toggle("puckAmountHuge", length >= 10);
|
const figure = el.dataset.figure === undefined ? el.textContent : el.dataset.figure;
|
||||||
|
if (el.textContent !== figure) el.textContent = figure;
|
||||||
|
this.AMOUNT_STEPS.forEach(step => el.classList.remove(step));
|
||||||
|
return figure;
|
||||||
|
},
|
||||||
|
|
||||||
|
// A seat puck is 64px of usable width, and 34 on the tightest ring in the sweep. A
|
||||||
|
// tournament stack doesn't fit in that and, because a number has no spaces to break
|
||||||
|
// on, it doesn't wrap or ellipsise either — it draws straight out over the seat next
|
||||||
|
// to it. Stepping the type down keeps every digit, which matters: a dealer misreading
|
||||||
|
// a stack is a worse outcome than a small one, so this shrinks rather than
|
||||||
|
// abbreviating.
|
||||||
|
//
|
||||||
|
// Measured, and it used to be counted. The old rule was eight characters for one
|
||||||
|
// step and ten for the other, which is a guess at how wide a figure is that happens
|
||||||
|
// to hold at one text size and one puck width. Turn a phone's text up — 20px of root
|
||||||
|
// text is what an Android slider gives you at 125%, and every size in this
|
||||||
|
// stylesheet is in `rem` on purpose — and it stops holding: measured at 360x640
|
||||||
|
// twelve-handed, "$1,200" is six characters, took no step at all, and wanted 38px of
|
||||||
|
// a 34px puck. The sweep caught it as a stack figure painted over the seat next door
|
||||||
|
// at every phone in the list. What the puck can hold is a question about the puck,
|
||||||
|
// so it is asked of the puck.
|
||||||
|
//
|
||||||
|
// Written as passes over every amount on the ring rather than a loop per seat: each
|
||||||
|
// pass writes to all of them and then reads back from all of them, so the browser
|
||||||
|
// lays the felt out four times whatever the table's size. Interleaved, twelve seats
|
||||||
|
// would have cost twenty-four layouts, and this runs on every render.
|
||||||
|
fitAmounts(cells) {
|
||||||
|
const live = cells.filter(el => el && (el.dataset.figure || el.textContent));
|
||||||
|
if (!live.length) return;
|
||||||
|
const figures = new Map(live.map(el => [el, this.resetAmount(el)]));
|
||||||
|
|
||||||
|
// The separators go before the type size does. "$1,234,567" is ten characters,
|
||||||
|
// which used to drop the puck's figures to 7px — and 7px is not a size anybody
|
||||||
|
// reads a stack off a felt at, so the shrink-rather-than-abbreviate rule above
|
||||||
|
// was buying a precision the dealer then couldn't see. Without its commas the
|
||||||
|
// same number is eight characters and fits two steps higher up this ladder.
|
||||||
|
// Grouping is a reading aid for a column of figures; this is one number on a
|
||||||
|
// 44px puck, and there the aid costs more than it gives.
|
||||||
|
const ungroup = live.filter(el => figures.get(el).includes(",") && this.overflowing(el));
|
||||||
|
ungroup.forEach(el => { el.textContent = figures.get(el).replace(/,/g, ""); });
|
||||||
|
|
||||||
|
let carried = null;
|
||||||
|
this.AMOUNT_STEPS.forEach(step => {
|
||||||
|
const over = live.filter(el => this.overflowing(el));
|
||||||
|
const previous = carried;
|
||||||
|
over.forEach(el => {
|
||||||
|
if (previous) el.classList.remove(previous);
|
||||||
|
el.classList.add(step);
|
||||||
|
});
|
||||||
|
carried = step;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// One puck's worth — for refreshPuck, which redraws a single seat.
|
||||||
|
fitAmount(el) {
|
||||||
|
this.fitAmounts([el]);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Every amount on the ring, so fitAmounts can do the whole table in four layouts.
|
||||||
|
seatAmountCells(layer) {
|
||||||
|
const cells = [];
|
||||||
|
for (let index = 0; index < layer.childElementCount; index++) {
|
||||||
|
const node = layer.children[index];
|
||||||
|
cells.push(node.querySelector(".seatPuckTotal"), node.querySelector(".seatPuckStack"));
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Cheaper than a full renderSeats() — used after edits so the seat editor's inputs
|
// Cheaper than a full renderSeats() — used after edits so the seat editor's inputs
|
||||||
// (which live in a separate panel) never lose focus.
|
// (which live in a separate panel) never lose focus.
|
||||||
refreshPuck(index) {
|
refreshPuck(index) {
|
||||||
const node = document.querySelector(`.seat[data-seat-index="${index}"]`);
|
const node = document.querySelector(`.seat[data-seat-index="${index}"]`);
|
||||||
if (node) this.updatePuckContent(node, this.state.seats[index], index);
|
if (!node) return;
|
||||||
|
this.updatePuckContent(node, this.state.seats[index], index);
|
||||||
|
this.fitAmounts([node.querySelector(".seatPuckTotal"), node.querySelector(".seatPuckStack")]);
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---- Seat gestures ----
|
// ---- Seat gestures ----
|
||||||
|
|||||||
@@ -1017,6 +1017,53 @@ lose. Chromium and Safari answer silently on their own engagement heuristics; Fi
|
|||||||
asks. A no is not an error worth reporting at a table, so the only thing that changes is
|
asks. A no is not an error worth reporting at a table, so the only thing that changes is
|
||||||
how confident Save & reset is allowed to sound, and whether it offers the ask as a button.
|
how confident Save & reset is allowed to sound, and whether it offers the ask as a button.
|
||||||
|
|
||||||
|
## The sweep had never once been run at a size anybody sets
|
||||||
|
|
||||||
|
Every length in `holdem.css` that isn't a structural clearance is in `rem`. That is the
|
||||||
|
right way round: it means the whole page follows the browser's font size, which is what
|
||||||
|
a phone's text-size slider changes. It also means the page had a whole axis of behaviour
|
||||||
|
that 320 audits had never touched, because every one of them ran at the default 16px.
|
||||||
|
|
||||||
|
A third pass now runs the same surfaces on the eight phone-shaped viewports with the
|
||||||
|
root at **20px** — the 125% an Android accessibility slider gives you. It found four
|
||||||
|
things on its first run, and every check that caught them was already written:
|
||||||
|
|
||||||
|
- **The page slid sideways.** `#holdemHeaderActions` was `flex-wrap: nowrap` with three
|
||||||
|
`white-space: nowrap` labels inside it, so when they stopped fitting they had nowhere
|
||||||
|
to go: "Resolve Pot" ended 23px past the right edge of a 360px window and put a
|
||||||
|
horizontal scrollbar under the whole document. At 24px it was 60px past the edge of a
|
||||||
|
390px one. `page never scrolls sideways` had been in the audit for months and had never
|
||||||
|
had anything to say.
|
||||||
|
- **Figures painted over the seat next door.** Which of the two type steps an amount got
|
||||||
|
was decided by *counting characters* — eight for one, ten for the other — which is a
|
||||||
|
guess at how wide a figure is that happens to hold at one text size and one puck width.
|
||||||
|
At 20px, "$1,200" is six characters, took no step at all, and wanted 38px of a 34px
|
||||||
|
puck. `Render.fitAmounts` measures now, in the same batched passes `fitSeatNames` uses,
|
||||||
|
and takes the separators out before it takes a step of type.
|
||||||
|
- **Pucks grew and the ring could not answer.** The row heights are `px` tokens so that
|
||||||
|
an amount stepping its type down shortens its digits and not its row — but a token is
|
||||||
|
a floor, not a ceiling, and text bigger than its row makes the row taller. The puck
|
||||||
|
went from 55px to 65 and twelve of them came down on each other, because at the 44px
|
||||||
|
floor there is nowhere narrower for the search to go. The two sizes a puck is written
|
||||||
|
at are capped at exactly the sizes they have always been drawn at, so nothing changes
|
||||||
|
at an ordinary text size; the felt is a diagram, and the rule that already governed a
|
||||||
|
puck's width now governs its height. `.seatsRoomy` is exempt — those pucks have room.
|
||||||
|
- **A check that was crying wolf.** `no control is parked where scrolling can't reach it`
|
||||||
|
flagged the raise pad's Cancel and Raise-to in the landscape rail. The rail is
|
||||||
|
`overflow-y: auto` on purpose and they were one flick away, so the message was untrue;
|
||||||
|
it now asks whether a scrollable ancestor can bring the control back. What was true is
|
||||||
|
that the rail opened at its *top*, so the overflow came off the bottom — where the two
|
||||||
|
buttons that end a raise are. It opens at the end now, which is the same reason it is
|
||||||
|
anchored low in the first place.
|
||||||
|
|
||||||
|
One shape it is not held to, with the arithmetic in `runUiAudit.js`: twelve players
|
||||||
|
all-in on a 360x640 phone. The action bar is at its tallest there — 283px of the 640 at
|
||||||
|
20px — and with the header that leaves the felt 290x163 where it had 296x262. Twelve
|
||||||
|
pucks 44px wide (the floor a thumb needs) and 62px tall do not fit around an ellipse
|
||||||
|
163px deep. That is a geometry with no answer rather than a fault with a fix, so it is
|
||||||
|
reported as a note every run instead of a failure every run — a sweep that is always red
|
||||||
|
stops being read.
|
||||||
|
|
||||||
## Why the hand evaluator is tested the way it is
|
## Why the hand evaluator is tested the way it is
|
||||||
|
|
||||||
`handRank.js` is the only module here that can pay the wrong person without anybody
|
`handRank.js` is the only module here that can pay the wrong person without anybody
|
||||||
|
|||||||
@@ -79,15 +79,26 @@ describe("the spacing scale", () => {
|
|||||||
describe("the type scale", () => {
|
describe("the type scale", () => {
|
||||||
// 16px — the size below which iOS zooms the page when a field is focused, so
|
// 16px — the size below which iOS zooms the page when a field is focused, so
|
||||||
// every text input states it. Not a typographic choice.
|
// every text input states it. Not a typographic choice.
|
||||||
// 1.05em, 0.44rem — relative bumps inside a component, and the shrink-to-fit for
|
// 1.05em — a relative bump inside a component.
|
||||||
// a six-figure stack on a 58px seat puck.
|
// 9px — the floor under the seat puck's shrink-to-fit applied to a name. It is a
|
||||||
// 9px — the floor under the same shrink-to-fit applied to a name. It is a limit
|
// limit rather than a size: past it the word stops being legible from
|
||||||
// rather than a size: past it the word stops being legible from across a
|
// across a table and an honest ellipsis is the better answer.
|
||||||
// table and an honest ellipsis is the better answer.
|
// 10px, 12px — the ceilings on a seat puck's two sizes, which are the one place in
|
||||||
const ALLOWED = new Set(["16px", "1.05em", "0.44rem", "9px"]);
|
// this file where type does not follow the browser's font size. Each is
|
||||||
|
// exactly the size that row has always been drawn at, so a cap only ever
|
||||||
|
// engages when a phone's text is turned up; see the #seatsLayer token block
|
||||||
|
// for why a ring whose pucks are already at the 44px floor cannot answer a
|
||||||
|
// taller row any other way. Limits rather than sizes.
|
||||||
|
const ALLOWED = new Set(["16px", "1.05em", "9px", "10px", "12px"]);
|
||||||
|
|
||||||
|
// The seat puck's two sizes are custom properties that feed `font-size` through
|
||||||
|
// `var()`, so the sweep above walks straight past them — which is exactly where a
|
||||||
|
// raw number would end up hiding. They are held to the same scale.
|
||||||
|
const TYPE_TOKENS = ["--seatNameSize", "--seatAmountSize"];
|
||||||
|
|
||||||
test("every font-size comes off the scale", () => {
|
test("every font-size comes off the scale", () => {
|
||||||
const offenders = valuesIn(["font-size"]).filter(v => !ALLOWED.has(v.value));
|
const offenders = valuesIn(["font-size", ...TYPE_TOKENS])
|
||||||
|
.filter(v => !ALLOWED.has(v.value));
|
||||||
assert.deepEqual(offenders, [],
|
assert.deepEqual(offenders, [],
|
||||||
"use var(--fs-2xs … --fs-2xl), or add the value to ALLOWED with a reason");
|
"use var(--fs-2xs … --fs-2xl), or add the value to ALLOWED with a reason");
|
||||||
});
|
});
|
||||||
|
|||||||
+64
-18
@@ -110,23 +110,72 @@ async function main() {
|
|||||||
const warnings = [];
|
const warnings = [];
|
||||||
let checks = 0;
|
let checks = 0;
|
||||||
|
|
||||||
// Twice over: once with a mouse, once with a thumb. The 44px rule stands itself
|
// Three passes over the same surfaces.
|
||||||
// down where `hover: hover` says there's a pointer, and a headless desktop browser
|
//
|
||||||
// is a pointer — so a single pass here has never once measured a touch target,
|
// A mouse and a thumb, because the 44px rule stands itself down where `hover: hover`
|
||||||
// however many viewports it visited. The phone-sized ones are exactly the pass that
|
// says there's a pointer, and a headless desktop browser is a pointer — so a single
|
||||||
// needs it.
|
// pass has never once measured a touch target, however many viewports it visited.
|
||||||
for (const touch of [false, true]) {
|
//
|
||||||
|
// And a thumb on a phone whose owner has turned the text up. Every length in this
|
||||||
|
// stylesheet that isn't a structural clearance is in `rem`, which is the right way
|
||||||
|
// round and means the whole page follows the browser's font size — including out of
|
||||||
|
// the side of the window, which is what it was doing: at 20px, which is the 125%
|
||||||
|
// an Android text-size slider gives you, "Resolve Pot" ran 23px off the right of a
|
||||||
|
// 360px phone and took the document's scroll width with it. Every check in the
|
||||||
|
// audit already knew how to catch that; not one of them had ever been run at a size
|
||||||
|
// anybody actually sets.
|
||||||
|
//
|
||||||
|
// One shape it is not held to, and the arithmetic for why. Twelve players all-in on
|
||||||
|
// a 360x640 phone is the tallest the action bar ever gets — the status block and the
|
||||||
|
// pot breakdown both grow with the text — and at 20px it stands 283px of the 640,
|
||||||
|
// which with the header leaves the felt 290x163 where it had 296x262. Twelve pucks
|
||||||
|
// 44px wide (the floor a thumb needs; there is nothing narrower to go to) and 62px
|
||||||
|
// tall do not fit around an ellipse 163px deep: two per side is all the height there
|
||||||
|
// is, and the sweep reports it as seats 9, 10 and 11 written across each other.
|
||||||
|
//
|
||||||
|
// That is a geometry with no answer rather than a fault with a fix, so it is
|
||||||
|
// reported as a note every run instead of a failure every run. A sweep that is
|
||||||
|
// always red stops being read — the same reasoning as the 30% furniture warning.
|
||||||
|
const CROWDED_BEYOND_HELP = "all in";
|
||||||
|
const LARGE_TEXT_PX = 20;
|
||||||
|
const PHONES = VIEWPORTS.filter(([w, h]) => Math.min(w, h) <= 430);
|
||||||
|
const PASSES = [
|
||||||
|
{ hand: "mouse", touch: false, viewports: VIEWPORTS },
|
||||||
|
{ hand: "touch", touch: true, viewports: VIEWPORTS },
|
||||||
|
{ hand: "large text", touch: true, viewports: PHONES, rootPx: LARGE_TEXT_PX, soft: CROWDED_BEYOND_HELP },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { hand, touch, viewports, rootPx, soft } of PASSES) {
|
||||||
const context = await browser.newContext(touch
|
const context = await browser.newContext(touch
|
||||||
? { hasTouch: true, isMobile: true, deviceScaleFactor: 3 }
|
? { hasTouch: true, isMobile: true, deviceScaleFactor: 3 }
|
||||||
: {});
|
: {});
|
||||||
|
// Before any of the page's own scripts, so its listener is registered first and
|
||||||
|
// the size is in place before main.js measures anything. A stylesheet rather
|
||||||
|
// than an inline style because that is what a browser font-size setting is: the
|
||||||
|
// root's used size, from first paint, not something applied afterwards.
|
||||||
|
if (rootPx) {
|
||||||
|
await context.addInitScript((px) => {
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.textContent = `html { font-size: ${px}px }`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}, { once: true });
|
||||||
|
}, rootPx);
|
||||||
|
}
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
const hand = touch ? "touch" : "mouse";
|
|
||||||
|
|
||||||
for (const [width, height, rawLabel] of VIEWPORTS) {
|
for (const [width, height, rawLabel] of viewports) {
|
||||||
await page.setViewportSize({ width, height });
|
await page.setViewportSize({ width, height });
|
||||||
|
|
||||||
for (const [shape, seats, players, board, shoves] of TABLES) {
|
for (const [shape, seats, players, board, shoves] of TABLES) {
|
||||||
const label = `${rawLabel} · ${shape} · ${hand}`;
|
const label = `${rawLabel} · ${shape} · ${hand}`;
|
||||||
|
// Measured either way; where the shape has no answer at this text size,
|
||||||
|
// said in the notes rather than counted against the run.
|
||||||
|
const record = (where, offenders) => {
|
||||||
|
if (!offenders.length) return;
|
||||||
|
if (shape === soft) warnings.push(`${where}: ${offenders.join("; ")}`);
|
||||||
|
else failures.push({ where, offenders });
|
||||||
|
};
|
||||||
await page.goto(`${BASE}/Casino/TexasHoldem`);
|
await page.goto(`${BASE}/Casino/TexasHoldem`);
|
||||||
await page.evaluate(seedTable, [seats, players, board]);
|
await page.evaluate(seedTable, [seats, players, board]);
|
||||||
await page.goto(`${BASE}/Casino/TexasHoldem`);
|
await page.goto(`${BASE}/Casino/TexasHoldem`);
|
||||||
@@ -201,7 +250,7 @@ async function main() {
|
|||||||
|
|
||||||
const table = await page.evaluate(() => LayoutAudit.runAll());
|
const table = await page.evaluate(() => LayoutAudit.runAll());
|
||||||
checks++;
|
checks++;
|
||||||
if (!table.clean) failures.push({ where: `table · ${label} ${width}x${height}`, offenders: table.failed.flatMap(f => f.offenders) });
|
record(`table · ${label} ${width}x${height}`, table.failed.flatMap(f => f.offenders));
|
||||||
table.warnings.forEach(w => warnings.push(`${label}: ${w}`));
|
table.warnings.forEach(w => warnings.push(`${label}: ${w}`));
|
||||||
|
|
||||||
// The raise pad, which is part of the bar rather than an overlay and so
|
// The raise pad, which is part of the bar rather than an overlay and so
|
||||||
@@ -224,12 +273,8 @@ async function main() {
|
|||||||
const pad = await page.evaluate(() => LayoutAudit.runAll({
|
const pad = await page.evaluate(() => LayoutAudit.runAll({
|
||||||
occlusionSelector: "#actionBar button, #actionBar input",
|
occlusionSelector: "#actionBar button, #actionBar input",
|
||||||
}));
|
}));
|
||||||
if (!pad.clean) {
|
record(`raise pad · ${label} ${width}x${height}`,
|
||||||
failures.push({
|
pad.failed.flatMap(f => f.offenders));
|
||||||
where: `raise pad · ${label} ${width}x${height}`,
|
|
||||||
offenders: pad.failed.flatMap(f => f.offenders),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const cancel = [...document.querySelectorAll("#actionBar button")]
|
const cancel = [...document.querySelectorAll("#actionBar button")]
|
||||||
.find(b => /^Cancel/.test(b.textContent.trim()));
|
.find(b => /^Cancel/.test(b.textContent.trim()));
|
||||||
@@ -253,12 +298,12 @@ async function main() {
|
|||||||
return LayoutAudit.occlusion(`#${overlay.id} button, #${overlay.id} a, #${overlay.id} input`);
|
return LayoutAudit.occlusion(`#${overlay.id} button, #${overlay.id} a, #${overlay.id} input`);
|
||||||
});
|
});
|
||||||
await page.evaluate(() => document.querySelectorAll("[id$='Overlay']").forEach(o => { o.hidden = true; }));
|
await page.evaluate(() => document.querySelectorAll("[id$='Overlay']").forEach(o => { o.hidden = true; }));
|
||||||
if (result.offenders.length) failures.push({ where: `${name} · ${label} ${width}x${height}`, offenders: result.offenders });
|
record(`${name} · ${label} ${width}x${height}`, result.offenders);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [width, height, rawLabel] of VIEWPORTS.filter((_, i) => i % 4 === 0)) {
|
for (const [width, height, rawLabel] of viewports.filter((_, i) => i % 4 === 0)) {
|
||||||
const label = `${rawLabel} · ${hand}`;
|
const label = `${rawLabel} · ${hand}`;
|
||||||
await page.setViewportSize({ width, height });
|
await page.setViewportSize({ width, height });
|
||||||
await page.goto(`${BASE}/Casino/TexasHoldem/QuickSplit`);
|
await page.goto(`${BASE}/Casino/TexasHoldem/QuickSplit`);
|
||||||
@@ -278,7 +323,8 @@ async function main() {
|
|||||||
|
|
||||||
await browser.close();
|
await browser.close();
|
||||||
|
|
||||||
console.log(`${checks} audits across ${VIEWPORTS.length} viewports, with a mouse and with a thumb`);
|
console.log(`${checks} audits across ${VIEWPORTS.length} viewports — with a mouse, with a thumb, ` +
|
||||||
|
`and with a thumb at ${LARGE_TEXT_PX}px of root text on ${PHONES.length} phones`);
|
||||||
warnings.forEach(w => console.log(` note: ${w}`));
|
warnings.forEach(w => console.log(` note: ${w}`));
|
||||||
if (failures.length === 0) {
|
if (failures.length === 0) {
|
||||||
console.log("clean");
|
console.log("clean");
|
||||||
|
|||||||
@@ -240,3 +240,118 @@ describe("and the ring checks its own measurement afterwards", () => {
|
|||||||
assert.equal(withBox(null, () => Render.seatBoxMoved({ width: 72, height: 56 })), false);
|
assert.equal(withBox(null, () => Render.seatBoxMoved({ width: 72, height: 56 })), false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("how much room a figure has on the puck it is written on", () => {
|
||||||
|
// The two steps of type an amount comes down used to be chosen by counting
|
||||||
|
// characters: eight for the first, ten for the second. That is a guess at how wide a
|
||||||
|
// figure is, and it happens to hold at one text size and one puck width.
|
||||||
|
//
|
||||||
|
// Turn a phone's text up — 20px of root text is what an Android slider gives you at
|
||||||
|
// 125%, and every size in the stylesheet is in `rem` on purpose — and it stops
|
||||||
|
// holding. Measured at 360x640 twelve-handed: the puck is at its 44px floor, which
|
||||||
|
// is 34px of usable width, and "$1,200" is six characters, took no step at all, and
|
||||||
|
// wanted 38. The sweep caught it as a stack figure painted over the seat next door
|
||||||
|
// at every phone in the list, and as "ALL IN" doing the same on the all-in table.
|
||||||
|
//
|
||||||
|
// So it is measured. A cell is a stand-in for the real one: what it wants against
|
||||||
|
// what it has, and a class list to write the answer into.
|
||||||
|
const cell = (figure, wants, has) => ({
|
||||||
|
dataset: { figure },
|
||||||
|
textContent: figure,
|
||||||
|
classes: new Set(),
|
||||||
|
classList: {
|
||||||
|
add(name) { this.owner.classes.add(name); },
|
||||||
|
remove(name) { this.owner.classes.delete(name); },
|
||||||
|
},
|
||||||
|
// What the text wants is proportional to how many characters are in it and how
|
||||||
|
// far down the ladder it has come — which is what a browser would tell us.
|
||||||
|
get scrollWidth() {
|
||||||
|
const step = this.classes.has("puckAmountHuge") ? 0.7
|
||||||
|
: this.classes.has("puckAmountLong") ? 0.85 : 1;
|
||||||
|
return Math.round((wants * this.textContent.length / figure.length) * step);
|
||||||
|
},
|
||||||
|
clientWidth: has,
|
||||||
|
});
|
||||||
|
|
||||||
|
const make = (...args) => {
|
||||||
|
const c = cell(...args);
|
||||||
|
c.classList.owner = c;
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fit = cells => {
|
||||||
|
const before = Render.overflowing;
|
||||||
|
Render.overflowing = el => el.scrollWidth > el.clientWidth + 1;
|
||||||
|
try { Render.fitAmounts(cells); } finally { Render.overflowing = before; }
|
||||||
|
};
|
||||||
|
|
||||||
|
test("a figure that fits is left at the size it is meant to be read at", () => {
|
||||||
|
const c = make("$600", 30, 34);
|
||||||
|
fit([c]);
|
||||||
|
assert.equal(c.textContent, "$600");
|
||||||
|
assert.deepEqual([...c.classes], []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("one that doesn't gives up its separators first", () => {
|
||||||
|
// Grouping is a reading aid for a column of figures. This is one number on a
|
||||||
|
// 44px puck, and a step of type costs more than the commas do.
|
||||||
|
const c = make("$1,200", 38, 34);
|
||||||
|
fit([c]);
|
||||||
|
assert.equal(c.textContent, "$1200");
|
||||||
|
assert.deepEqual([...c.classes], [], "no type step needed once the commas are out");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("then a step of type, and then another", () => {
|
||||||
|
const c = make("$1,234,567", 90, 34);
|
||||||
|
fit([c]);
|
||||||
|
assert.equal(c.textContent, "$1234567");
|
||||||
|
assert.deepEqual([...c.classes], ["puckAmountHuge"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("and only ever one step at a time is worn", () => {
|
||||||
|
// The two rules have the same specificity, so a puck wearing both would be sized
|
||||||
|
// by whichever the stylesheet happens to mention last.
|
||||||
|
const c = make("$1,234,567", 45, 34);
|
||||||
|
fit([c]);
|
||||||
|
assert.equal(c.classes.has("puckAmountHuge") && c.classes.has("puckAmountLong"), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a figure with no separators to give goes straight to the type", () => {
|
||||||
|
// "ALL IN" is the one thing on this row that isn't a number, and it is the widest
|
||||||
|
// — six characters and a space, at 700 weight with its letters spread apart.
|
||||||
|
const c = make("ALL IN", 39, 34);
|
||||||
|
fit([c]);
|
||||||
|
assert.equal(c.textContent, "ALL IN");
|
||||||
|
assert.deepEqual([...c.classes], ["puckAmountLong"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a puck that went small can come back", () => {
|
||||||
|
// Everything is measured from the figure and the size it is meant to be read at,
|
||||||
|
// not from whatever the last render left behind. Otherwise this is a ratchet: one
|
||||||
|
// crowded frame and the seat is small for the rest of the night.
|
||||||
|
const c = make("$600", 30, 34);
|
||||||
|
c.classes.add("puckAmountHuge");
|
||||||
|
c.textContent = "$600";
|
||||||
|
fit([c]);
|
||||||
|
assert.deepEqual([...c.classes], []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("and the grouped figure is what it comes back to", () => {
|
||||||
|
const c = make("$1,200", 38, 34);
|
||||||
|
fit([c]);
|
||||||
|
assert.equal(c.textContent, "$1200");
|
||||||
|
c.clientWidth = 96;
|
||||||
|
fit([c]);
|
||||||
|
assert.equal(c.textContent, "$1,200", "a wider puck should get its separators back");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an empty row is not an amount", () => {
|
||||||
|
// The pill keeps its place on a seat with nothing in the pot — that is what makes
|
||||||
|
// every puck on the ring the same puck — and there is nothing there to fit.
|
||||||
|
const c = make("", 0, 34);
|
||||||
|
assert.doesNotThrow(() => fit([c]));
|
||||||
|
assert.deepEqual([...c.classes], []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -213,6 +213,26 @@ const LayoutAudit = {
|
|||||||
// accessibility tree, and because a fixed element does not move when the page does,
|
// accessibility tree, and because a fixed element does not move when the page does,
|
||||||
// no amount of scrolling could bring it back. visibility:hidden puts it away; a
|
// no amount of scrolling could bring it back. visibility:hidden puts it away; a
|
||||||
// transform only moves it.
|
// transform only moves it.
|
||||||
|
// A fixed panel that scrolls is a different thing from a fixed panel that doesn't,
|
||||||
|
// and this check could not tell them apart. The landscape rail is `overflow-y: auto`
|
||||||
|
// on purpose — the raise pad outgrows a 360px-tall screen the moment the text is
|
||||||
|
// turned up, and the stylesheet chose overflow over drawing controls on top of one
|
||||||
|
// another — so a button below its fold is one flick away, not lost. Reported as
|
||||||
|
// unreachable it is simply untrue, and a sweep that cries wolf is a sweep nobody
|
||||||
|
// reads.
|
||||||
|
//
|
||||||
|
// The ancestor has to be on screen itself for this to hold, which the rail is: it
|
||||||
|
// is `top: 0; bottom: 0`. If a scrolling panel is ever parked off the side, the
|
||||||
|
// thing to report is the panel, not the button inside it.
|
||||||
|
scrollableAncestor(el) {
|
||||||
|
for (let node = el.parentElement; node; node = node.parentElement) {
|
||||||
|
const overflow = getComputedStyle(node).overflowY;
|
||||||
|
if ((overflow === "auto" || overflow === "scroll") &&
|
||||||
|
node.scrollHeight > node.clientHeight + 1) return node;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
focusablesReachable() {
|
focusablesReachable() {
|
||||||
const offenders = this.visibleControls()
|
const offenders = this.visibleControls()
|
||||||
.filter(el => {
|
.filter(el => {
|
||||||
@@ -225,6 +245,7 @@ const LayoutAudit = {
|
|||||||
const b = el.getBoundingClientRect();
|
const b = el.getBoundingClientRect();
|
||||||
return b.top >= innerHeight || b.bottom <= 0 || b.left >= innerWidth || b.right <= 0;
|
return b.top >= innerHeight || b.bottom <= 0 || b.left >= innerWidth || b.right <= 0;
|
||||||
})
|
})
|
||||||
|
.filter(el => !this.scrollableAncestor(el))
|
||||||
.map(el => `${this.describe(el)} :: fixed and off-screen, so nothing can scroll to it`);
|
.map(el => `${this.describe(el)} :: fixed and off-screen, so nothing can scroll to it`);
|
||||||
|
|
||||||
return { name: "no control is parked where scrolling can't reach it", offenders };
|
return { name: "no control is parked where scrolling can't reach it", offenders };
|
||||||
|
|||||||
Reference in New Issue
Block a user