fix(ui): stop iOS Safari zooming in on 14px form fields (#1114)
Closes #1105. iOS Safari zooms the whole page in when a focused form control computes to under 16px, and it does not zoom back out. Unlocking a gallery is a client-side transition rather than a document navigation, so the zoom the password field triggers carries straight into the gallery: the layout pans horizontally and the header actions sit off-screen until the visitor pinch-zooms out by hand. A real page load would have reset it. `.input` and `.input-themed` are `text-sm`, so the field is 14px on every phone, and GalleryPage inverts the breakpoint on top of that (`text-sm sm:text-base` — 14px below 640px, where iOS zooms, and 16px above it, where it never does). Keyed to the POINTER, not a width. The zoom depends on the computed font size and a touch device, never on how wide the viewport is — and a phone in landscape is 667-956 CSS px, above any width you could call "phone". Measured on the admin login page, which has no `sm:` override: main portrait 390x844 14px zooms main landscape 844x390 14px zooms main iPad 820x1180 14px zooms fixed all three 16px fixed desktop (mouse) 14px unchanged, no zoom off touch One media query rather than flipping each call site. `.input` alone backs 334 `<Input>` usages, but there are also ~440 raw inputs, selects and textareas carrying their own `text-sm`, and Tailwind utilities sit in a later layer than @layer components — so a fix at the component definition misses most controls and any new `text-sm` silently reintroduces the bug. The `:not()` on each selector is load-bearing, not decoration: it buys the specificity to beat a utility class. Measured in a browser — input.text-sm 16px (0,2,1 beats .text-sm) select.text-sm 14px (0,0,1 loses) textarea.text-sm 14px (0,0,1 loses) 24 selects and textareas in the tree carry `text-sm`, so the bare form would have left them zooming. Checkbox and radio stay excluded so font-size never sizes their box. max(16px, 1em, 1rem) is a FLOOR, not a size. A flat 16px would make controls that are already bigger smaller: Typography -> Large sets --font-size-base to 18px on body, so anything inheriting it would be clamped down and the setting quietly ignored. Each term covers a case the others miss: normal (body 16) 16px Large theme (body 18) 18px Small theme (body 14) 16px browser default 20px 20px The viewport meta is deliberately left alone: `maximum-scale=1` would suppress the zoom by disabling pinch-to-zoom for everyone.
This commit is contained in:
@@ -716,3 +716,56 @@
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* iOS Safari zooms the whole page in when a focused form control computes to
|
||||||
|
* less than 16px, and it does not zoom back out (#1105). Unlocking a gallery
|
||||||
|
* is a client-side transition rather than a document navigation, so the zoom
|
||||||
|
* the password field triggered carries straight into the gallery: the layout
|
||||||
|
* pans horizontally and the header actions sit off-screen until the visitor
|
||||||
|
* pinch-zooms out by hand.
|
||||||
|
*
|
||||||
|
* The lever is the font size, not the viewport meta — adding maximum-scale=1
|
||||||
|
* would suppress the zoom by disabling pinch-to-zoom for everyone, which is an
|
||||||
|
* accessibility regression, so index.html deliberately omits it.
|
||||||
|
*
|
||||||
|
* Keyed to the POINTER, not a width. The zoom depends on the computed font
|
||||||
|
* size and a touch device, never on how wide the viewport is — and a phone in
|
||||||
|
* landscape is 667–956 CSS px, above any width you could call "phone". A
|
||||||
|
* max-width query fixes portrait and leaves every landscape phone (and iPad)
|
||||||
|
* still zooming. `pointer: coarse` is the population that actually has the
|
||||||
|
* behaviour; a mouse-driven desktop reports `fine` and keeps its 14px density.
|
||||||
|
*
|
||||||
|
* Deliberately NOT inside @layer, and deliberately more specific than a single
|
||||||
|
* utility class: `.input` is 14px and ~440 raw controls carry their own
|
||||||
|
* `text-sm`, so a rule that loses to a utility fixes almost nothing. The
|
||||||
|
* `:not()` on each selector is what buys that specificity — without it,
|
||||||
|
* `select`/`textarea` (0,0,1) lose to `.text-sm` (0,1,0) and keep zooming,
|
||||||
|
* while `input` alone happens to win. Excluding checkbox and radio keeps
|
||||||
|
* font-size off controls that size their box from it.
|
||||||
|
*
|
||||||
|
* max(16px, 1em, 1rem) is a FLOOR, not a size. Writing a flat 16px would make
|
||||||
|
* controls that are already larger smaller: Typography -> Large sets
|
||||||
|
* --font-size-base to 18px on body, so anything inheriting it would be clamped
|
||||||
|
* down and the setting quietly ignored. Each term covers a case the others
|
||||||
|
* miss - 1em follows the theme's body size, 1rem follows a browser default the
|
||||||
|
* visitor raised themselves, 16px catches Small themes and .text-sm controls:
|
||||||
|
*
|
||||||
|
* normal (body 16) 16px Large theme (body 18) 18px
|
||||||
|
* Small theme (body 14) 16px browser default 20px 20px
|
||||||
|
*
|
||||||
|
* The specificity that beats a utility class also beats a gallery's custom CSS
|
||||||
|
* (Theme -> Custom CSS), so `.input-themed { font-size: 20px }` lands at 16px
|
||||||
|
* on touch. That is unavoidable here rather than an oversight: nothing in CSS
|
||||||
|
* distinguishes a class that sets 14px from one that sets 20px, so a rule that
|
||||||
|
* loses to the second also loses to the first and fixes nothing. Overriding
|
||||||
|
* DOWNWARD is the point; upward is the cost. `font-size: 20px !important`
|
||||||
|
* still wins for anyone who wants it.
|
||||||
|
*/
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
input:not([type="checkbox"]):not([type="radio"]),
|
||||||
|
select:not([hidden]),
|
||||||
|
textarea:not([hidden]) {
|
||||||
|
font-size: max(16px, 1em, 1rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user