Hold'em: every seat on the ring is the same seat, and the ring is measured before it is believed #122
@@ -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
|
||||
|
||||
@@ -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 ----------
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user