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:
@@ -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