Stop the page growing wider than the screen
A phone had started allowing zoom-out, which is how a document wider than the viewport announces itself. The suspect was the "Ate" modal, but the dialogs are not it: all seven open with showModal(), so their containing block is the viewport and width: calc(100% - 32px) cannot exceed it. It was the month grid, added three commits ago: left: 50%; transform: translateX(-50%); width: 268px; max-width bounded the panel's width and nothing bounded its position. Centred on the date button — which sits near the right edge of the bar — a 268px panel hangs off the side of a phone, and being absolutely positioned it drags the document's scrollable width out with it. Anchoring to the button cannot be made safe: pin it right and it overflows the left on a narrow screen, pin it left and it overflows the right. So it is a child of the day bar now, pinned to that bar's inner edge and capped at the bar's own width. The bar spans the content width exactly, so the panel is on screen at every size by construction. A long unbroken word was a second way in, and a pre-existing one. A history row's note is a flex item with neither min-width: 0 nor a break rule, so a URL or something copied off a food bag sets its content-based minimum and widens the row. The Notes log directly below already guarded against precisely this, so the history row had simply been missed; exercise names and their instructions had the same gap. The checks gained the general form of both, since this class of bug is invisible until a phone starts zooming out: every element that renders text the user typed must be able to break a long word, the grid must stay edge-anchored inside the bar, and no fixed width may exceed the content box of a 320px phone. Each was confirmed to fail with its fix reverted.
This commit is contained in:
@@ -149,4 +149,48 @@ suite("both day-bar timers fit on a row of their own");
|
||||
}
|
||||
}
|
||||
|
||||
// Anything wider than the screen makes the whole document wider than the
|
||||
// viewport, and a phone responds by letting you zoom out — which is how this
|
||||
// class of bug is usually noticed, long after it was introduced.
|
||||
suite("nothing can push the page wider than the screen");
|
||||
{
|
||||
// Free text the user types has no width limit of its own. A long unbroken
|
||||
// token — a URL in a note, a chemical name off a food bag — sets a flex
|
||||
// item's content-based minimum, or simply spills out of its box, and either
|
||||
// way it widens the document. Every element that renders user input needs a
|
||||
// break rule; this is the list, and it is easier to extend than to remember.
|
||||
const USER_TEXT = [
|
||||
"\\.event \\.note", // a note on a history row
|
||||
"\\.event \\.note-text", // the Notes log
|
||||
"\\.ex-name", // exercise names
|
||||
"\\.ex-note", // exercise instructions
|
||||
"\\.guest-item-label", // the label on a guest link
|
||||
];
|
||||
for (const sel of USER_TEXT) {
|
||||
const block = rule(new RegExp(`\\n${sel}[^{]*\\{([^}]*)\\}`));
|
||||
ok(/overflow-wrap:\s*(anywhere|break-word)/.test(block),
|
||||
`${sel.replace(/\\/g, "")} can break a long unbroken word`);
|
||||
}
|
||||
|
||||
// The month grid is the one panel positioned against something narrower than
|
||||
// the page. Centred on the date button it hung off the right of a phone; it
|
||||
// is anchored to the day bar instead, which spans the content width.
|
||||
const dayCal = rule(/\n\.day-cal \{([^}]*)\}/);
|
||||
ok(/right:/.test(dayCal) && !/left:\s*50%/.test(dayCal),
|
||||
"the month grid is edge-anchored, not centred on the date button");
|
||||
ok(!/max-width:[^;]*vw/.test(dayCal),
|
||||
"…and bounded by its container rather than by the viewport");
|
||||
const main = html.slice(html.indexOf('<section class="day-bar">'),
|
||||
html.indexOf("</section>", html.indexOf('<section class="day-bar">')));
|
||||
ok(/id="day-cal"/.test(main), "…and sits inside the day bar, which is what it is measured against");
|
||||
|
||||
// A fixed width wider than the narrowest content box cannot fit by
|
||||
// definition. 320px phone, less the body's two 16px gutters.
|
||||
const NARROWEST = 320 - 2 * BODY_GUTTER;
|
||||
const tooWide = [...css.matchAll(/(?:^|[;{\s])(width|min-width):\s*(\d{3,})px/g)]
|
||||
.filter(m => Number(m[2]) > NARROWEST)
|
||||
.map(m => `${m[1]}: ${m[2]}px`);
|
||||
eq(tooWide, [], `no fixed width exceeds a ${NARROWEST}px content box`);
|
||||
}
|
||||
|
||||
export default report("layout");
|
||||
|
||||
Reference in New Issue
Block a user