iOS focus-zoom prevention — input font-size floor @ 16px ; JS fallback strengthened
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

Belt-and-suspenders for the iOS Safari auto-zoom-on-input quirk: Mobile Safari zooms the viewport when an `<input>`/`<textarea>`/`<select>` is focused & its computed font-size < 16px, and never zooms back out on blur. Two layers ; PRIMARY — SCSS prevention: new `input, textarea, select, [contenteditable] { font-size: unquote("max(16px, 1em)") }` in core.scss (Sass can't reconcile px/em units in compile-time max() so unquote() passes the CSS max() through verbatim — modern browsers handle natively). 1em inherits parent, max() floors at 16. ALSO floored `.form-control-lg` in _base.scss — was `font-size: 1.125rem`, which at rem=14 (small portrait, clamp(14px, 2.4vmin, 22px) hits its floor) computes to 15.75px → **0.25px** under iOS's 16px threshold → the "ever so slightly" zoom on New Game + New Post applets the user reported (both use `.form-control.form-control-lg`, specificity 0,2,0 beats my element-level 0,0,1 rule). Floor: `unquote("max(16px, 1.125rem)")` ; SECONDARY — JS fallback in base.html: rewritten from `setAttribute('content', ...)` toggle to full meta-element remove+re-add, which modern iOS handles more reliably than attribute mutations on the existing meta. Triggers on document-level `focusout` (bubbles natively, no capture-phase needed) for `input/textarea/select`; injects fresh viewport meta w. `maximum-scale=1.0, user-scalable=no` for 300ms (iOS reads as zoom violation → snaps to 1:1), then swaps back to the cached base content so pinch-zoom remains available elsewhere ; user observed horizontal scrollbar appearing when the page zoomed — that's the symptom the user actually cared about (broken layout, not aesthetic zoom). w. SCSS floor in place the zoom shouldn't trigger to begin with; the JS is purely for inputs that slip through (future custom controls, shadow DOM, etc.) ; iOS-specific behavior — Selenium+Firefox doesn't replicate the auto-zoom so no FT layer added. Verified by user manual iPhone test (post-fix retest pending after force-refresh)

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-18 18:32:45 -04:00
parent 8066ac289f
commit 79706e817a
3 changed files with 34 additions and 9 deletions

View File

@@ -101,17 +101,31 @@
// iOS Safari auto-zooms when focusing an <input>/<textarea>/<select>
// whose font-size is < 16px, and does NOT auto-zoom back out on blur.
// Briefly toggling maximum-scale=1.0 on the viewport meta forces iOS to
// reset to 1:1; reverting after 100ms restores the default content so
// users can still pinch-zoom manually elsewhere on the page.
// Belt-and-suspenders: primary prevention is the global `font-size:
// max(16px, 1em)` rule in core.scss; this JS is a fallback for inputs
// that slip through (custom controls, shadow DOM, etc.). Modern iOS
// doesn't reliably react to a simple `setAttribute('content', ...)`
// tweak on the existing meta — removing and re-appending the meta
// element entirely is more dependable.
(function () {
var vp = document.querySelector('meta[name="viewport"]');
if (!vp) return;
var base = vp.getAttribute('content');
var origMeta = document.querySelector('meta[name="viewport"]');
if (!origMeta) return;
var baseContent = origMeta.getAttribute('content');
document.addEventListener('focusout', function (e) {
if (!e.target.matches || !e.target.matches('input, textarea, select')) return;
vp.setAttribute('content', base + ', maximum-scale=1.0');
setTimeout(function () { vp.setAttribute('content', base); }, 100);
var oldMeta = document.querySelector('meta[name="viewport"]');
if (oldMeta) oldMeta.remove();
var snapMeta = document.createElement('meta');
snapMeta.setAttribute('name', 'viewport');
snapMeta.setAttribute('content', baseContent + ', maximum-scale=1.0, user-scalable=no');
document.head.appendChild(snapMeta);
setTimeout(function () {
snapMeta.remove();
var revertMeta = document.createElement('meta');
revertMeta.setAttribute('name', 'viewport');
revertMeta.setAttribute('content', baseContent);
document.head.appendChild(revertMeta);
}, 300);
});
}());
</script>