Hold'em: every seat on the ring is the same seat, and the ring is measured before it is believed #122

Open
jheaps wants to merge 27 commits from polish/holdem-seats-one-size into master
4 changed files with 446 additions and 150 deletions
Showing only changes of commit 023228efc0 - Show all commits
+160 -144
View File
@@ -549,8 +549,12 @@
<div id="rulingsOverlay" hidden> <div id="rulingsOverlay" hidden>
<div id="rulingsSheet" role="dialog" aria-labelledby="rulingsTitle"> <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="rulingsHeader">
<div class="seatSheetGrip" aria-hidden="true"></div>
<h2 id="rulingsTitle" tabindex="-1" data-modal-focus>Common rulings</h2> <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> <button id="closeRulingsBtn" data-modal-close class="holdemBtn holdemBtnSmall holdemBtnGhost" type="button">Done</button>
</div> </div>
@@ -579,160 +583,172 @@
Not exclusive (no `name` attribute): closing one because another opened moves Not exclusive (no `name` attribute): closing one because another opened moves
everything under the dealer's finger, which is the disorientation the everything under the dealer's finger, which is the disorientation the
accordion is here to avoid. *@ 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"> @* Six and six, down one column and then the next. Two boxes rather than one
<summary><h3>A board card turned too early</h3></summary> list so that landscape can lay them side by side without the columns
<p> sharing rows: opening a ruling in one of them moves what is under it in
If the fourth board card gets turned before the betting round is finished, that column and nothing else. Everywhere but landscape the wrappers are
that card is out of play for the round. Finish the betting, then burn and `display: contents` and the twelve are one column, exactly as before. *@
turn what would have been the fifth card in the fourth card's place. <div class="rulingsList">
</p> <div class="rulingsColumn">
</details> <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"> <details class="ruling">
<summary><h3>Acting out of turn</h3></summary> <summary><h3>A board card turned too early</h3></summary>
<p> <p>
A one-off is usually just backed up if no one has acted behind them. If the fourth board card gets turned before the betting round is finished,
Doing it repeatedly is what earns a penalty. that card is out of play for the round. Finish the betting, then burn and
</p> turn what would have been the fifth card in the fourth card's place.
</details> </p>
</details>
<details class="ruling"> <details class="ruling">
<summary><h3>Calling the clock on a player</h3></summary> <summary><h3>Acting out of turn</h3></summary>
<p> <p>
Any player may ask for a clock once a decision is taking unreasonably A one-off is usually just backed up if no one has acted behind them.
long; it is the dealer, not the table, who grants it. The player then Doing it repeatedly is what earns a penalty.
gets a set time and a countdown out loud — thirty seconds and ten is the </p>
usual form. </details>
</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"> <details class="ruling">
<summary><h3>A straddle is a blind raise</h3></summary> <summary><h3>Calling the clock on a player</h3></summary>
<p> <p>
The player left of the big blind may put out twice it before the cards, Any player may ask for a clock once a decision is taking unreasonably
without looking. It is a blind bet, not a raise made in turn, so it does long; it is the dealer, not the table, who grants it. The player then
three things at once: everyone else acts first, the minimum raise becomes gets a set time and a countdown out loud — thirty seconds and ten is the
twice the straddle, and the straddler still has the option when the usual form.
action comes back — they may raise even if nobody raised into them. </p>
</p> <p>
<p> If the count reaches zero with no action, the hand is <em>dead</em> if
Re-straddles carry on to the left, each doubling again, and stop before they were facing a bet, and <em>checked</em> if they were not: nobody is
the small blind. Tap <em>Straddle</em> on the deal prompt and the tool folded for slowness when checking was free. The action bar runs the
posts it, puts the right player on the clock and prices the raise; count once you tap <em>Call the clock</em>, and offers the ruling as one
the seat wears an <em>STR</em> badge for the hand. button when it ends.
</p> </p>
</details> </details>
<details class="ruling"> <details class="ruling">
<summary><h3>A short all-in doesn't reopen the betting</h3></summary> <summary><h3>A straddle is a blind raise</h3></summary>
<p> <p>
Somebody shoving in less than a full raise — say $3 more when the last The player left of the big blind may put out twice it before the cards,
raise was $8 — does not give the players behind them another go. Anyone without looking. It is a blind bet, not a raise made in turn, so it does
who has already acted owes the difference or folds; they don't get to three things at once: everyone else acts first, the minimum raise becomes
raise again because a short stack happened to land between them. twice the straddle, and the straddler still has the option when the
</p> action comes back — they may raise even if nobody raised into them.
<p> </p>
A player who hadn't acted yet keeps full rights and may still raise, and <p>
the minimum raise stays measured off the last <em>full</em> raise, not Re-straddles carry on to the left, each doubling again, and stop before
off the short one. The action bar follows this for you. the small blind. Tap <em>Straddle</em> on the deal prompt and the tool
</p> posts it, puts the right player on the clock and prices the raise;
</details> the seat wears an <em>STR</em> badge for the hand.
</p>
</details>
<details class="ruling"> <details class="ruling">
<summary><h3>Chips nobody called come back</h3></summary> <summary><h3>A short all-in doesn't reopen the betting</h3></summary>
<p> <p>
Bet more than anyone was able to call and the excess is returned to you Somebody shoving in less than a full raise — say $3 more when the last
before the showdown — it was never contested, so it isn't part of the raise was $8 — does not give the players behind them another go. Anyone
pot. The pot breakdown shows it as "uncalled" rather than pretending it's who has already acted owes the difference or folds; they don't get to
a side pot you could win. raise again because a short stack happened to land between them.
</p> </p>
</details> <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"> <div class="rulingsColumn">
<summary><h3>One big chip, nothing said</h3></summary> <details class="ruling">
<p> <summary><h3>Chips nobody called come back</h3></summary>
The most common argument at a home table. A player facing a bet who <p>
pushes out a single oversized chip without saying anything has Bet more than anyone was able to call and the excess is returned to you
<em>called</em> — not raised, however big the chip is. To raise with one before the showdown — it was never contested, so it isn't part of the
chip you have to say "raise" before it lands; say it without an amount pot. The pot breakdown shows it as "uncalled" rather than pretending it's
and the raise is the most that chip allows. a side pot you could win.
</p> </p>
<p> </details>
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"> <details class="ruling">
<summary><h3>A raise has to be a full raise</h3></summary> <summary><h3>One big chip, nothing said</h3></summary>
<p> <p>
At least as much again as the last raise, or the big blind if there The most common argument at a home table. A player facing a bet who
hasn't been one. Anything between a call and that minimum isn't a raise pushes out a single oversized chip without saying anything has
a player can make — it's a call, unless they're putting their last chips <em>called</em> — not raised, however big the chip is. To raise with one
in. The raise pad says so if you type one, and won't stop you: your chip you have to say "raise" before it lands; say it without an amount
ruling beats the arithmetic. and the raise is the most that chip allows.
</p> </p>
</details> <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"> <details class="ruling">
<summary><h3>Who turns their hand over first</h3></summary> <summary><h3>A raise has to be a full raise</h3></summary>
<p> <p>
If there was a bet on the river, the last player to bet or raise shows At least as much again as the last raise, or the big blind if there
first. If the river was checked all the way round, it's the first live hasn't been one. Anything between a call and that minimum isn't a raise
player left of the button. Nobody has to be asked twice — a player may a player can make — it's a call, unless they're putting their last chips
always turn their hand over early if they want to. in. The raise pad says so if you type one, and won't stop you: your
</p> ruling beats the arithmetic.
<p> </p>
When everyone is all-in before the river there was no final round to </details>
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"> <details class="ruling">
<summary><h3>The odd chip in a split pot</h3></summary> <summary><h3>Who turns their hand over first</h3></summary>
<p> <p>
When a chopped pot won't divide evenly, the odd chip goes to the first If there was a bet on the river, the last player to bet or raise shows
player left of the button, not to whoever the dealer happened to tick first. If the river was checked all the way round, it's the first live
first. Pot payouts here order the winners that way, so the leftover lands player left of the button. Nobody has to be asked twice — a player may
on the right seat every time. always turn their hand over early if they want to.
</p> </p>
</details> <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"> <details class="ruling">
<summary><h3>Betting in one motion — the string bet</h3></summary> <summary><h3>The odd chip in a split pot</h3></summary>
<p> <p>
A bet is whatever goes in on the first trip to the stack, unless the When a chopped pot won't divide evenly, the odd chip goes to the first
amount was said out loud first. Reaching back for more chips without player left of the button, not to whoever the dealer happened to tick
having said "raise" is a string bet, and only the first motion stands. first. Pot payouts here order the winners that way, so the leftover lands
Saying the number is always enough on its own: "raise to sixty" binds on the right seat every time.
whatever order the chips arrive in. </p>
</p> </details>
<p>
It's the rule home games invoke most and enforce least, and both are <details class="ruling">
fine — the point is that a player can't watch the table react and then <summary><h3>Betting in one motion — the string bet</h3></summary>
decide how much to bet. Say it, or push it all at once. <p>
</p> A bet is whatever goes in on the first trip to the stack, unless the
</details> 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"> <p class="rulingsSource">
Follows the Tournament Directors Association rules, which most rooms and 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-width: 460px;
max-height: 86vh; max-height: 86vh;
overflow-y: auto; overflow-y: auto;
overscroll-behavior: contain;
background: linear-gradient(180deg, var(--surface-2), var(--surface-1)); background: linear-gradient(180deg, var(--surface-2), var(--surface-1));
border: 1px solid var(--line); border: 1px solid var(--line);
border-bottom: none; border-bottom: none;
border-radius: var(--r-lg) var(--r-lg) 0 0; 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); 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 { .rulingsHeader {
display: flex; position: sticky;
top: 0;
z-index: 1;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center; align-items: center;
gap: var(--sp-2); column-gap: var(--sp-2);
margin-bottom: 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 { .rulingsHeader h2 {
flex: 1;
margin: 0; margin: 0;
font-size: var(--fs-lg); font-size: var(--fs-lg);
font-weight: 500; font-weight: 500;
color: var(--text-bright); 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 { .rulingsIntro {
margin: 0 0 var(--sp-4); margin: 0 0 var(--sp-4);
color: var(--text-muted); color: var(--text-muted);
@@ -5853,6 +5884,116 @@ body.hasActionBar #toastHost {
inset: max(24px, 12%) 44px min(26px, 12%); 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 ---------- /* ---------- Short phones, upright ----------
+132
View File
@@ -40,6 +40,35 @@ const ruleFor = selector => {
return CSS.slice(at, CSS.indexOf("}", at)); 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. // Each ruling as its own slice of markup.
const rulings = () => { const rulings = () => {
const parts = sheet().split('<details class="ruling"'); const parts = sheet().split('<details class="ruling"');
@@ -140,3 +169,106 @@ describe("reading one of them", () => {
assert.match(MAIN, /getElementById\("openRulingsBtn"\)\.addEventListener\("click", openRulings\)/); 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)", // 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. // the button that pays the pot out, went unmeasured for 36 rounds.
// Bring it into view before probing. // 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) { if (scroller && scroller.scrollHeight > scroller.clientHeight) {
const box = scroller.getBoundingClientRect(); const box = scroller.getBoundingClientRect();
const own = el.getBoundingClientRect(); const own = el.getBoundingClientRect();