Only lock viewport zoom while text inputs are focused (#10340)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-04-23 20:42:05 +03:00
committed by GitHub
parent 6fc31eeb48
commit 7b490d8bcd
4 changed files with 76 additions and 2 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, viewport-fit=cover">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover">
<meta name="referrer" content="origin-when-cross-origin">
<!--
Preconnect to essential domains
+71
View File
@@ -0,0 +1,71 @@
import {useEffect} from 'react'
import {IS_WEB_MOBILE_IOS} from '#/env'
const ZOOM_LOCKED_VIEWPORT =
'width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, viewport-fit=cover'
/**
* iOS Safari zooms in when a text input with `font-size < 16px` gains focus.
* To avoid that while still allowing users to pinch-zoom for accessibility, we
* only pin `maximum-scale=1` while a text input is focused and restore the
* original viewport on blur.
*
* Listeners run in the capture phase so we update the viewport before iOS
* commits to auto-zooming the input.
*/
export function useViewportZoomLock({enabled} = {enabled: true}) {
useEffect(() => {
if (!IS_WEB_MOBILE_IOS) return
if (!enabled) return
const meta = document.querySelector('meta[name="viewport"]')
if (!(meta instanceof HTMLMetaElement)) return
const originalContent = meta.content
const onFocus = (e: FocusEvent) => {
if (isTextInput(e.target)) {
meta.content = ZOOM_LOCKED_VIEWPORT
}
}
const onBlur = (e: FocusEvent) => {
if (isTextInput(e.target)) {
meta.content = originalContent
}
}
document.addEventListener('focus', onFocus, true)
document.addEventListener('blur', onBlur, true)
return () => {
document.removeEventListener('focus', onFocus, true)
document.removeEventListener('blur', onBlur, true)
meta.content = originalContent
}
}, [enabled])
}
const NON_TEXT_INPUT_TYPES = new Set([
'button',
'checkbox',
'color',
'file',
'hidden',
'image',
'radio',
'range',
'reset',
'submit',
])
function isTextInput(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return false
if (target.isContentEditable) return true
if (target instanceof HTMLTextAreaElement) return true
if (target instanceof HTMLInputElement) {
return !NON_TEXT_INPUT_TYPES.has(target.type)
}
return false
}
+3
View File
@@ -18,6 +18,7 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {RemoveScrollBar} from 'react-remove-scroll-bar'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useViewportZoomLock} from '#/lib/hooks/useViewportZoomLock'
import {
type CommonNavigatorParams,
type NavigationProp,
@@ -106,6 +107,8 @@ function Inner({convoId}: {convoId: string}) {
const {top: topInset} = useSafeAreaInsets()
const {data: convoData} = useConvoQuery({convoId})
useViewportZoomLock({enabled: isFocused})
const convo = convoData
? parseConvoView(convoData, currentAccount?.did)
: null
+1 -1
View File
@@ -9,7 +9,7 @@
-->
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, viewport-fit=cover"
content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover"
/>
<!--
Preconnect to essential domains