Give the rulings the window a sideways phone was already holding

Twelve rulings in a 460px sheet in the middle of an 844px window: 754px of
content in a 334px scrollport, 111 of it spent on a title and a caveat that
stand still, 384px of window width unused either side. Four of the twelve
names on screen, and the dealer is looking one up with an argument going on
at the table.

Material's rule for a bottom sheet in landscape is to stop being one, and
this tool already does exactly that under this query for the action bar. So
the sheet takes the window and the width pays for the height: the twelve go
into two columns of six, all of them on screen at 844x390 with nothing to
scroll at all, and the title, the caveat and Done share one line instead of
standing on three.

Two wrappers in the markup rather than one grid of twelve, because a grid
pairs its children into shared rows: opening the third ruling on the left
would push the ninth down the right-hand side to keep the row square. Two
flows means a column only ever moves itself — measured, opening one leaves
the other six names exactly where they were. Everywhere but landscape the
wrappers are `display: contents` and the twelve are the single column they
have always been.

And the same sheet upright: the header is stuck to the top of its own
scrollport now, the way the seat menu's has been. Measured before, at the
bottom of that scroll Done was 221px above the sheet's top edge on a 360x640
phone, and 262 on a 390x844 one with a ruling open — which is the only
reason anybody is in here. A dealer who has read the answer had to scroll
back past it to leave.

The layout sweep's scroll-into-view guard learns the new box, or it finds a
sheet whose scrollHeight equals its clientHeight and decides nothing is out
of view.

1324 tests pass. The sweep is clean across 428 audits; contrast unchanged.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
2026-09-15 04:36:36 -06:00
co-authored by Claude Opus 5
parent f86e50268e
commit 023228efc0
4 changed files with 446 additions and 150 deletions
+160 -144
View File
@@ -549,8 +549,12 @@
<div id="rulingsOverlay" hidden>
<div id="rulingsSheet" role="dialog" aria-labelledby="rulingsTitle">
<div class="seatSheetGrip" aria-hidden="true"></div>
@* The grip belongs inside the header, not above it, because the header is stuck
to the top of the scrollport — anything above a sticky thing is something the
list scrolls out through. The seat menu's header is built the same way, and
for the same reason. *@
<div class="rulingsHeader">
<div class="seatSheetGrip" aria-hidden="true"></div>
<h2 id="rulingsTitle" tabindex="-1" data-modal-focus>Common rulings</h2>
<button id="closeRulingsBtn" data-modal-close class="holdemBtn holdemBtnSmall holdemBtnGhost" type="button">Done</button>
</div>
@@ -579,160 +583,172 @@
Not exclusive (no `name` attribute): closing one because another opened moves
everything under the dealer's finger, which is the disorientation the
accordion is here to avoid. *@
<details class="ruling">
<summary><h3>Redeal it — misdeal</h3></summary>
<ul>
<li>The first card goes to the wrong seat.</li>
<li>A seat entitled to a hand gets dealt out, or a seat that isn't entitled gets dealt in.</li>
<li>Two or more boxed cards turn up on the initial deal.</li>
<li>Either of the first two cards is exposed by dealer error.</li>
</ul>
</details>
<details class="ruling">
<summary><h3>A board card turned too early</h3></summary>
<p>
If the fourth board card gets turned before the betting round is finished,
that card is out of play for the round. Finish the betting, then burn and
turn what would have been the fifth card in the fourth card's place.
</p>
</details>
@* Six and six, down one column and then the next. Two boxes rather than one
list so that landscape can lay them side by side without the columns
sharing rows: opening a ruling in one of them moves what is under it in
that column and nothing else. Everywhere but landscape the wrappers are
`display: contents` and the twelve are one column, exactly as before. *@
<div class="rulingsList">
<div class="rulingsColumn">
<details class="ruling">
<summary><h3>Redeal it — misdeal</h3></summary>
<ul>
<li>The first card goes to the wrong seat.</li>
<li>A seat entitled to a hand gets dealt out, or a seat that isn't entitled gets dealt in.</li>
<li>Two or more boxed cards turn up on the initial deal.</li>
<li>Either of the first two cards is exposed by dealer error.</li>
</ul>
</details>
<details class="ruling">
<summary><h3>Acting out of turn</h3></summary>
<p>
A one-off is usually just backed up if no one has acted behind them.
Doing it repeatedly is what earns a penalty.
</p>
</details>
<details class="ruling">
<summary><h3>A board card turned too early</h3></summary>
<p>
If the fourth board card gets turned before the betting round is finished,
that card is out of play for the round. Finish the betting, then burn and
turn what would have been the fifth card in the fourth card's place.
</p>
</details>
<details class="ruling">
<summary><h3>Calling the clock on a player</h3></summary>
<p>
Any player may ask for a clock once a decision is taking unreasonably
long; it is the dealer, not the table, who grants it. The player then
gets a set time and a countdown out loud — thirty seconds and ten is the
usual form.
</p>
<p>
If the count reaches zero with no action, the hand is <em>dead</em> if
they were facing a bet, and <em>checked</em> if they were not: nobody is
folded for slowness when checking was free. The action bar runs the
count once you tap <em>Call the clock</em>, and offers the ruling as one
button when it ends.
</p>
</details>
<details class="ruling">
<summary><h3>Acting out of turn</h3></summary>
<p>
A one-off is usually just backed up if no one has acted behind them.
Doing it repeatedly is what earns a penalty.
</p>
</details>
<details class="ruling">
<summary><h3>A straddle is a blind raise</h3></summary>
<p>
The player left of the big blind may put out twice it before the cards,
without looking. It is a blind bet, not a raise made in turn, so it does
three things at once: everyone else acts first, the minimum raise becomes
twice the straddle, and the straddler still has the option when the
action comes back — they may raise even if nobody raised into them.
</p>
<p>
Re-straddles carry on to the left, each doubling again, and stop before
the small blind. Tap <em>Straddle</em> on the deal prompt and the tool
posts it, puts the right player on the clock and prices the raise;
the seat wears an <em>STR</em> badge for the hand.
</p>
</details>
<details class="ruling">
<summary><h3>Calling the clock on a player</h3></summary>
<p>
Any player may ask for a clock once a decision is taking unreasonably
long; it is the dealer, not the table, who grants it. The player then
gets a set time and a countdown out loud — thirty seconds and ten is the
usual form.
</p>
<p>
If the count reaches zero with no action, the hand is <em>dead</em> if
they were facing a bet, and <em>checked</em> if they were not: nobody is
folded for slowness when checking was free. The action bar runs the
count once you tap <em>Call the clock</em>, and offers the ruling as one
button when it ends.
</p>
</details>
<details class="ruling">
<summary><h3>A short all-in doesn't reopen the betting</h3></summary>
<p>
Somebody shoving in less than a full raise — say $3 more when the last
raise was $8 — does not give the players behind them another go. Anyone
who has already acted owes the difference or folds; they don't get to
raise again because a short stack happened to land between them.
</p>
<p>
A player who hadn't acted yet keeps full rights and may still raise, and
the minimum raise stays measured off the last <em>full</em> raise, not
off the short one. The action bar follows this for you.
</p>
</details>
<details class="ruling">
<summary><h3>A straddle is a blind raise</h3></summary>
<p>
The player left of the big blind may put out twice it before the cards,
without looking. It is a blind bet, not a raise made in turn, so it does
three things at once: everyone else acts first, the minimum raise becomes
twice the straddle, and the straddler still has the option when the
action comes back — they may raise even if nobody raised into them.
</p>
<p>
Re-straddles carry on to the left, each doubling again, and stop before
the small blind. Tap <em>Straddle</em> on the deal prompt and the tool
posts it, puts the right player on the clock and prices the raise;
the seat wears an <em>STR</em> badge for the hand.
</p>
</details>
<details class="ruling">
<summary><h3>Chips nobody called come back</h3></summary>
<p>
Bet more than anyone was able to call and the excess is returned to you
before the showdown — it was never contested, so it isn't part of the
pot. The pot breakdown shows it as "uncalled" rather than pretending it's
a side pot you could win.
</p>
</details>
<details class="ruling">
<summary><h3>A short all-in doesn't reopen the betting</h3></summary>
<p>
Somebody shoving in less than a full raise — say $3 more when the last
raise was $8 — does not give the players behind them another go. Anyone
who has already acted owes the difference or folds; they don't get to
raise again because a short stack happened to land between them.
</p>
<p>
A player who hadn't acted yet keeps full rights and may still raise, and
the minimum raise stays measured off the last <em>full</em> raise, not
off the short one. The action bar follows this for you.
</p>
</details>
</div>
<details class="ruling">
<summary><h3>One big chip, nothing said</h3></summary>
<p>
The most common argument at a home table. A player facing a bet who
pushes out a single oversized chip without saying anything has
<em>called</em> — not raised, however big the chip is. To raise with one
chip you have to say "raise" before it lands; say it without an amount
and the raise is the most that chip allows.
</p>
<p>
Facing no bet, the same silent chip is a bet of the whole chip. And a
clear verbal declaration in turn is binding either way — if words and
chips disagree, whichever came first is the action.
</p>
</details>
<div class="rulingsColumn">
<details class="ruling">
<summary><h3>Chips nobody called come back</h3></summary>
<p>
Bet more than anyone was able to call and the excess is returned to you
before the showdown — it was never contested, so it isn't part of the
pot. The pot breakdown shows it as "uncalled" rather than pretending it's
a side pot you could win.
</p>
</details>
<details class="ruling">
<summary><h3>A raise has to be a full raise</h3></summary>
<p>
At least as much again as the last raise, or the big blind if there
hasn't been one. Anything between a call and that minimum isn't a raise
a player can make — it's a call, unless they're putting their last chips
in. The raise pad says so if you type one, and won't stop you: your
ruling beats the arithmetic.
</p>
</details>
<details class="ruling">
<summary><h3>One big chip, nothing said</h3></summary>
<p>
The most common argument at a home table. A player facing a bet who
pushes out a single oversized chip without saying anything has
<em>called</em> — not raised, however big the chip is. To raise with one
chip you have to say "raise" before it lands; say it without an amount
and the raise is the most that chip allows.
</p>
<p>
Facing no bet, the same silent chip is a bet of the whole chip. And a
clear verbal declaration in turn is binding either way — if words and
chips disagree, whichever came first is the action.
</p>
</details>
<details class="ruling">
<summary><h3>Who turns their hand over first</h3></summary>
<p>
If there was a bet on the river, the last player to bet or raise shows
first. If the river was checked all the way round, it's the first live
player left of the button. Nobody has to be asked twice — a player may
always turn their hand over early if they want to.
</p>
<p>
When everyone is all-in before the river there was no final round to
lead, so it falls to whoever last put money in, on whichever street that
was. The action bar names the seat and the reason at every showdown, so
the call is already made before anyone asks.
</p>
</details>
<details class="ruling">
<summary><h3>A raise has to be a full raise</h3></summary>
<p>
At least as much again as the last raise, or the big blind if there
hasn't been one. Anything between a call and that minimum isn't a raise
a player can make — it's a call, unless they're putting their last chips
in. The raise pad says so if you type one, and won't stop you: your
ruling beats the arithmetic.
</p>
</details>
<details class="ruling">
<summary><h3>The odd chip in a split pot</h3></summary>
<p>
When a chopped pot won't divide evenly, the odd chip goes to the first
player left of the button, not to whoever the dealer happened to tick
first. Pot payouts here order the winners that way, so the leftover lands
on the right seat every time.
</p>
</details>
<details class="ruling">
<summary><h3>Who turns their hand over first</h3></summary>
<p>
If there was a bet on the river, the last player to bet or raise shows
first. If the river was checked all the way round, it's the first live
player left of the button. Nobody has to be asked twice — a player may
always turn their hand over early if they want to.
</p>
<p>
When everyone is all-in before the river there was no final round to
lead, so it falls to whoever last put money in, on whichever street that
was. The action bar names the seat and the reason at every showdown, so
the call is already made before anyone asks.
</p>
</details>
<details class="ruling">
<summary><h3>Betting in one motion — the string bet</h3></summary>
<p>
A bet is whatever goes in on the first trip to the stack, unless the
amount was said out loud first. Reaching back for more chips without
having said "raise" is a string bet, and only the first motion stands.
Saying the number is always enough on its own: "raise to sixty" binds
whatever order the chips arrive in.
</p>
<p>
It's the rule home games invoke most and enforce least, and both are
fine — the point is that a player can't watch the table react and then
decide how much to bet. Say it, or push it all at once.
</p>
</details>
<details class="ruling">
<summary><h3>The odd chip in a split pot</h3></summary>
<p>
When a chopped pot won't divide evenly, the odd chip goes to the first
player left of the button, not to whoever the dealer happened to tick
first. Pot payouts here order the winners that way, so the leftover lands
on the right seat every time.
</p>
</details>
<details class="ruling">
<summary><h3>Betting in one motion — the string bet</h3></summary>
<p>
A bet is whatever goes in on the first trip to the stack, unless the
amount was said out loud first. Reaching back for more chips without
having said "raise" is a string bet, and only the first motion stands.
Saying the number is always enough on its own: "raise to sixty" binds
whatever order the chips arrive in.
</p>
<p>
It's the rule home games invoke most and enforce least, and both are
fine — the point is that a player can't watch the table react and then
decide how much to bet. Say it, or push it all at once.
</p>
</details>
</div>
</div>
<p class="rulingsSource">
Follows the Tournament Directors Association rules, which most rooms and
+146 -5
View File
@@ -2179,29 +2179,60 @@ input[type="checkbox"] {
max-width: 460px;
max-height: 86vh;
overflow-y: auto;
overscroll-behavior: contain;
background: linear-gradient(180deg, var(--surface-2), var(--surface-1));
border: 1px solid var(--line);
border-bottom: none;
border-radius: var(--r-lg) var(--r-lg) 0 0;
padding: var(--sp-1) var(--sp-4) calc(var(--sp-5) + env(safe-area-inset-bottom, 0px));
/* No padding at the top: the header owns it, because the header is sticky and a
gap above a sticky thing is a gap the list scrolls through. The seat menu's
sheet is built the same way, one section down. */
padding: 0 var(--sp-4) calc(var(--sp-5) + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
}
/* Stuck to the top of the sheet's own scrollport, for the reason the seat menu's header
is: the way out should not scroll away. This sheet is the longer of the two and never
had it. Measured before: on a 360x640 phone the twelve shut rulings are 791px of a
549px sheet, so at the bottom of that scroll Done sits 221px above the top edge — and
on a 390x844 phone with one ruling opened, which is the only reason anybody is in
here, 262px. A dealer who has read the answer has to scroll back past it to leave. */
.rulingsHeader {
display: flex;
position: sticky;
top: 0;
z-index: 1;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: var(--sp-2);
margin-bottom: var(--sp-2);
column-gap: var(--sp-2);
/* Out to the sheet's edges and back again, so nothing shows through either side of
a header sitting over a scrolling list. */
margin: 0 calc(var(--sp-4) * -1) var(--sp-2);
padding: var(--sp-1) var(--sp-4) var(--sp-2);
background: var(--surface-2);
border-bottom: 1px solid var(--line);
}
/* The grip is inside the header now rather than above it — above a sticky element is
the one place on this sheet that scrolls out of sight. */
.rulingsHeader .seatSheetGrip {
grid-column: 1 / -1;
}
.rulingsHeader h2 {
flex: 1;
margin: 0;
font-size: var(--fs-lg);
font-weight: 500;
color: var(--text-bright);
}
/* Two boxes of six that are not boxes at all: `display: contents` hands the twelve
rulings straight to the list, so every layout but landscape sees the single column it
always had. Landscape turns them back into boxes and stands them side by side. */
.rulingsColumn {
display: contents;
}
.rulingsIntro {
margin: 0 0 var(--sp-4);
color: var(--text-muted);
@@ -5853,6 +5884,116 @@ body.hasActionBar #toastHost {
inset: max(24px, 12%) 44px min(26px, 12%);
}
/* ---------- Common rulings: the sheet becomes the screen ----------
Everywhere else this is a bottom sheet a dealer scrolls, and that works because
upright there is height to scroll through. Sideways there is not. Measured at
844x390 before this: a 460px-wide sheet standing in the middle of an 844px window
with 384px of that window unused either side of it, 754px of rulings in 334px of
scrollport, 111px of it spent on a title and a caveat that stand still — four of
the twelve names on screen, and the dealer is looking one up with an argument
going on at the table.
So the sheet takes the window, and the width pays for the height. Material's rule
for a bottom sheet on a landscape screen is to stop being one — a side sheet on a
wide screen, a full-screen dialog when the content needs real scrolling — and this
is the tool's own move as well: the action bar becomes a right-hand rail under
exactly this query, for exactly this reason.
The twelve go into two columns of six. Measured after: all twelve names on screen
at 844x390 with nothing to scroll at all, and the columns are 400px wide, which is
a better measure for the prose inside an open ruling than the 460 it had.
Capped rather than unbounded, because this query also matches a short desktop
window, and two columns of a 1400px window would be 680px lines of body text. */
#rulingsOverlay {
align-items: stretch;
}
#rulingsSheet {
max-width: 62rem;
max-height: none;
height: 100%;
border: none;
border-radius: 0;
/* The list inside does the scrolling now, so the sheet itself does none. */
overflow: hidden;
padding:
var(--sp-2)
calc(var(--sp-4) + env(safe-area-inset-right, 0px))
calc(var(--sp-2) + env(safe-area-inset-bottom, 0px))
calc(var(--sp-4) + env(safe-area-inset-left, 0px));
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-rows: auto minmax(0, 1fr) auto;
grid-template-areas:
"title intro done"
"list list list"
"src src src";
column-gap: var(--sp-3);
row-gap: var(--sp-2);
}
/* The title, the caveat and the way out share one line, which is the whole of the
111px this screen was spending on them. `display: contents` because the three sit
in two different boxes in the markup and the sheet's own grid is where they need
to be. The sticky positioning goes with the box it is dropping, and is not wanted
here: nothing above the list scrolls, so nothing has to stick. */
.rulingsHeader {
display: contents;
}
/* A grab handle for a sheet that is not a sheet any more. */
.rulingsHeader .seatSheetGrip {
display: none;
}
.rulingsHeader h2 {
grid-area: title;
align-self: center;
}
.rulingsIntro {
grid-area: intro;
align-self: center;
margin: 0;
font-size: var(--fs-sm);
}
#closeRulingsBtn {
grid-area: done;
align-self: center;
}
/* Two tracks holding two independent flows, not one flow wrapped into two. That is
what the pair of wrappers in the markup buys: a grid with twelve children of its
own would pair them into six shared rows, and opening the third ruling on the left
would push the ninth down the right-hand side to keep the row square. Here a
column only ever moves itself.
`scrollbar-gutter` because the gutter arrives the moment a ruling is opened, and
without the reservation it takes 8px off the tracks and slides the right-hand
column sideways while the dealer is reading it. */
.rulingsList {
grid-area: list;
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: var(--sp-5);
align-items: start;
}
.rulingsColumn {
display: block;
}
.rulingsSource {
grid-area: src;
padding-top: var(--sp-1);
}
}
/* ---------- Short phones, upright ----------
+132
View File
@@ -40,6 +40,35 @@ const ruleFor = selector => {
return CSS.slice(at, CSS.indexOf("}", at));
};
// Every @media block written at exactly this condition, concatenated — the same helper
// theHeader.test.js uses, and for the same reason: this condition is opened more than
// once in the file, and a helper that stops at the first finds a block with none of
// these rules in it and reports them missing.
const mediaBlock = condition => {
const found = [];
const needle = "@media ";
for (let at = CSS.indexOf(needle); at >= 0; at = CSS.indexOf(needle, at + 1)) {
const open = CSS.indexOf("{", at);
if (CSS.slice(at + needle.length, open).trim() !== condition) continue;
let depth = 0;
for (let i = open; i < CSS.length; i++) {
if (CSS[i] === "{") depth++;
else if (CSS[i] === "}" && --depth === 0) { found.push(CSS.slice(open + 1, i)); break; }
}
}
assert.ok(found.length > 0, `no @media block written as "${condition}"`);
return found.join("\n");
};
const ruleIn = (block, selector) => {
const re = new RegExp(`(^|[},])\\s*${selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\{([^}]*)\\}`, "m");
const found = block.match(re);
assert.ok(found, `there is no \`${selector}\` rule in that block`);
return found[2];
};
const SIDEWAYS = () => mediaBlock("(orientation: landscape) and (max-height: 560px)");
// Each ruling as its own slice of markup.
const rulings = () => {
const parts = sheet().split('<details class="ruling"');
@@ -140,3 +169,106 @@ describe("reading one of them", () => {
assert.match(MAIN, /getElementById\("openRulingsBtn"\)\.addEventListener\("click", openRulings\)/);
});
});
// The dealer who has read the answer has to be able to leave without scrolling back
// past it. Measured before this: on a 360x640 phone the twelve shut rulings are 791px of
// a 549px sheet, so at the bottom of that scroll Done is 221px above the sheet's top
// edge; on a 390x844 phone with one ruling opened — the only reason anybody is in here —
// it is 262px. The seat menu's sheet had already been given the fix and this one, the
// longer of the two, had not.
describe("the way out does not scroll away", () => {
test("the header is stuck to the top of the sheet's own scrollport", () => {
const rule = ruleFor(".rulingsHeader");
assert.match(rule, /position:\s*sticky/);
assert.match(rule, /top:\s*0/);
// Out to the sheet's edges and back, or the list shows through either side of it.
assert.match(rule, /margin:\s*0 calc\(var\(--sp-4\) \* -1\)/);
assert.match(rule, /background:\s*var\(--surface-2\)/);
});
test("and nothing is left above it for the list to scroll out through", () => {
assert.match(ruleFor("#rulingsSheet"), /padding:\s*0 var\(--sp-4\)/,
"the sheet keeps a top padding, which is a gap above a sticky header");
// The grip was the one thing up there. It belongs inside the header now.
const header = sheet().match(/<div class="rulingsHeader">[\s\S]*?<\/div>\s*<\/div>/);
assert.ok(header, "the header is not in the markup where it was");
assert.match(header[0], /seatSheetGrip/,
"the grip is still outside the header, so it scrolls away above it");
assert.ok(ruleFor(".rulingsHeader .seatSheetGrip"),
"the grip has no rule placing it inside a two-column header");
});
test("the scroll inside the sheet stays inside it", () => {
assert.match(ruleFor("#rulingsSheet"), /overscroll-behavior:\s*contain/);
});
});
// A phone held sideways is 844x390: no height and swimming in width. Measured before
// this, the bottom sheet was 460px wide in the middle of that window — 384px of it
// unused either side — holding 754px of rulings in a 334px scrollport, 111px of which
// went on a title and a caveat that stand still. Four of the twelve names on screen.
//
// Material's rule for a bottom sheet in landscape is to stop being one, and this tool
// already does exactly that under this query for the action bar. So the sheet takes the
// window and the width pays for the height: two columns of six, all twelve names on
// screen at 844x390 with nothing to scroll at all.
describe("a phone on its side gets the whole window", () => {
test("the twelve are six and six in the markup", () => {
const columns = sheet().split('<div class="rulingsColumn">').slice(1);
assert.equal(columns.length, 2, "there are not exactly two columns to stand side by side");
columns.forEach((c, i) => {
const count = (c.slice(0, c.indexOf("\n </div>")).match(/<details class="ruling"/g) || []).length;
assert.equal(count, 6, `column ${i + 1} holds ${count} rulings rather than six`);
});
});
test("but everywhere else they are still one column", () => {
// `display: contents` hands the rulings straight to the list, so every layout
// that isn't landscape sees exactly the markup it saw before the wrappers.
assert.match(ruleFor(".rulingsColumn"), /display:\s*contents/);
assert.match(ruleIn(SIDEWAYS(), ".rulingsColumn"), /display:\s*block/);
});
test("sideways the sheet stops being a sheet and becomes the screen", () => {
const block = SIDEWAYS();
assert.match(ruleIn(block, "#rulingsOverlay"), /align-items:\s*stretch/,
"the sheet is still anchored to the bottom edge, so it is still short");
const sheetRule = ruleIn(block, "#rulingsSheet");
assert.match(sheetRule, /height:\s*100%/);
assert.match(sheetRule, /max-height:\s*none/);
// Capped rather than unbounded: this query also matches a short desktop window,
// and two columns of a 1400px one would be 680px lines of body text.
assert.match(sheetRule, /max-width:\s*62rem/);
assert.match(sheetRule, /overflow:\s*hidden/,
"the sheet scrolls as well as the list, which is two scrollbars for one list");
});
test("the title, the caveat and the way out share one line", () => {
// That line is the whole of the 111px this screen was spending on them, and the
// sticky positioning goes with the box being dropped: nothing above the list
// scrolls here, so nothing has to stick.
const block = SIDEWAYS();
assert.match(ruleIn(block, ".rulingsHeader"), /display:\s*contents/);
assert.match(ruleIn(block, ".rulingsHeader .seatSheetGrip"), /display:\s*none/,
"a grab handle on a sheet that is not a sheet any more");
assert.match(ruleIn(block, "#rulingsSheet"), /grid-template-areas:\s*\n?\s*"title intro done"/);
});
test("the two columns are two flows, not one flow wrapped", () => {
// A grid with twelve children of its own pairs them into six shared rows, and
// opening the third ruling on the left pushes the ninth down the right-hand side
// to keep the row square. Two wrappers means a column only ever moves itself.
const rule = ruleIn(SIDEWAYS(), ".rulingsList");
assert.match(rule, /grid-template-columns:\s*1fr 1fr/);
assert.match(rule, /overflow-y:\s*auto/);
assert.match(rule, /min-height:\s*0/,
"a grid item's floor is its content, so the list would push the sheet open");
});
test("and the gutter is reserved before it is needed", () => {
// It arrives the moment a ruling is opened. Without the reservation it takes 8px
// off the tracks and slides the right-hand column sideways under the eye that is
// reading it.
assert.match(ruleIn(SIDEWAYS(), ".rulingsList"), /scrollbar-gutter:\s*stable/);
});
});
+8 -1
View File
@@ -957,7 +957,14 @@ const LayoutAudit = {
// check used to pass it in silence — which is how "Confirm Winner(s)",
// the button that pays the pot out, went unmeasured for 36 rounds.
// Bring it into view before probing.
const scroller = el.closest("[style*='overflow'], .modalBody, #potWizardBody, #rulingsSheet, #seatEditorPanel");
// `.rulingsList` before `#rulingsSheet`, and `closest` takes the nearer
// of the two: on a phone held sideways the sheet fills the window and
// stops scrolling, and the twelve rulings scroll inside the list instead.
// Naming only the sheet there finds a box whose scrollHeight equals its
// clientHeight, decides nothing is out of view, and puts the check back
// in the silence this guard exists to break.
const scroller = el.closest("[style*='overflow'], .modalBody, #potWizardBody, " +
".rulingsList, #rulingsSheet, #seatEditorPanel");
if (scroller && scroller.scrollHeight > scroller.clientHeight) {
const box = scroller.getBoundingClientRect();
const own = el.getBoundingClientRect();