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

@@ -159,7 +159,11 @@ body {
&.form-control-lg { &.form-control-lg {
--_pad-v: 0.75rem; --_pad-v: 0.75rem;
padding: var(--_pad-v) 1rem; padding: var(--_pad-v) 1rem;
font-size: 1.125rem; // 1.125rem at rem=14 (small portrait clamp floor) is 15.75px
// — just under iOS Safari's 16px auto-zoom threshold. Floor
// at 16px to prevent the focus-zoom; native CSS max() handles
// the unit mix Sass can't reconcile at compile time.
font-size: unquote("max(16px, 1.125rem)");
} }
&.is-invalid ~ .invalid-feedback { &.is-invalid ~ .invalid-feedback {

View File

@@ -23,4 +23,11 @@ select,
[contenteditable] { [contenteditable] {
user-select: text; user-select: text;
touch-action: auto; touch-action: auto;
// iOS Safari auto-zooms when focusing a form field whose computed font-size
// is < 16px. At rem=14 (small portrait viewports w. clamp(14,2.4vmin,22))
// a 1rem input is 14px → triggers zoom. `max(16px, 1em)` enforces the 16px
// floor while still inheriting larger sizes from parent contexts when set.
// unquote() keeps Sass from trying to evaluate the px/em compare at compile
// time (it can't reconcile units); CSS max() handles it natively at runtime.
font-size: unquote("max(16px, 1em)");
} }

View File

@@ -101,17 +101,31 @@
// iOS Safari auto-zooms when focusing an <input>/<textarea>/<select> // iOS Safari auto-zooms when focusing an <input>/<textarea>/<select>
// whose font-size is < 16px, and does NOT auto-zoom back out on blur. // 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 // Belt-and-suspenders: primary prevention is the global `font-size:
// reset to 1:1; reverting after 100ms restores the default content so // max(16px, 1em)` rule in core.scss; this JS is a fallback for inputs
// users can still pinch-zoom manually elsewhere on the page. // 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 () { (function () {
var vp = document.querySelector('meta[name="viewport"]'); var origMeta = document.querySelector('meta[name="viewport"]');
if (!vp) return; if (!origMeta) return;
var base = vp.getAttribute('content'); var baseContent = origMeta.getAttribute('content');
document.addEventListener('focusout', function (e) { document.addEventListener('focusout', function (e) {
if (!e.target.matches || !e.target.matches('input, textarea, select')) return; if (!e.target.matches || !e.target.matches('input, textarea, select')) return;
vp.setAttribute('content', base + ', maximum-scale=1.0'); var oldMeta = document.querySelector('meta[name="viewport"]');
setTimeout(function () { vp.setAttribute('content', base); }, 100); 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> </script>