diff --git a/app/globals.css b/app/globals.css index 4edd215..84e94ac 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2334,6 +2334,19 @@ li code { .nodec { width: 40px; height: 40px; font-size: 12.5px; } } +/* iOS Safari zooms the whole page when it focuses a text field whose text + is smaller than 16px. Phones get 16px in every text field; !important + because chapter styles size their lab inputs with more specific selectors. */ +@media (max-width: 560px) { + input:not([type]), + input[type="text"], + input[type="search"], + textarea, + select { + font-size: 16px !important; + } +} + @media (prefers-reduced-motion: reduce) { *, *::before, diff --git a/test/e2e/narrow.spec.ts b/test/e2e/narrow.spec.ts index c3ea61c..9ef81a8 100644 --- a/test/e2e/narrow.spec.ts +++ b/test/e2e/narrow.spec.ts @@ -1,5 +1,6 @@ -// On a phone nothing is cut off at the right edge of any page, and printing -// shows every section, without the shell chrome, in the light palette. +// On a phone nothing is cut off at the right edge of any page and text fields +// are large enough that iOS does not zoom in; printing shows every section, +// without the shell chrome, in the light palette. import { expect, test } from "@playwright/test"; import { elementsPastViewport } from "./overflow"; @@ -36,6 +37,35 @@ for (const width of [360, 390]) { }); } +test.describe("text fields on a phone", () => { + test.use({ viewport: { width: 390, height: 844 } }); + + test("are at least 16px, so iOS Safari does not zoom in on focus", async ({ page }) => { + // The labs' inputs are on the page from the start + for (const route of ["/string", "/hash", "/bst", "/heap", "/trie", "/advanced"]) { + await page.goto(route); + const sizes = await page + .locator('input:not([type]), input[type="text"]') + .evaluateAll((els) => els.map((e) => parseFloat(getComputedStyle(e).fontSize))); + expect(sizes.length, route).toBeGreaterThan(0); + for (const size of sizes) expect(size, route).toBeGreaterThanOrEqual(16); + } + // A quiz fill-in appears only on its question, and the palette only when + // open, so measure a field with each of their classes instead + const shared = await page.evaluate(() => + ["q-input", "cmdk-input"].map((cls) => { + const field = document.createElement("input"); + field.className = cls; + document.body.append(field); + const size = parseFloat(getComputedStyle(field).fontSize); + field.remove(); + return size; + }), + ); + for (const size of shared) expect(size).toBeGreaterThanOrEqual(16); + }); +}); + test("printing shows every section, without the shell chrome, in the light palette", async ({ page, }) => {