iOS focus-zoom prevention — input font-size floor @ 16px ; JS fallback strengthened
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:
@@ -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 {
|
||||||
|
|||||||
@@ -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)");
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user