Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

Co-authored-by: Claude <noreply@anthropic.com>

authored by

Samuel Newman
Claude
and committed by
GitHub
7b490d8b 6fc31eeb

+76 -2
+1 -1
bskyweb/templates/base.html
··· 2 2 <html> 3 3 <head> 4 4 <meta charset="UTF-8"> 5 - <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, viewport-fit=cover"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover"> 6 6 <meta name="referrer" content="origin-when-cross-origin"> 7 7 <!-- 8 8 Preconnect to essential domains
+71
src/lib/hooks/useViewportZoomLock.ts
··· 1 + import {useEffect} from 'react' 2 + 3 + import {IS_WEB_MOBILE_IOS} from '#/env' 4 + 5 + const ZOOM_LOCKED_VIEWPORT = 6 + 'width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, viewport-fit=cover' 7 + 8 + /** 9 + * iOS Safari zooms in when a text input with `font-size < 16px` gains focus. 10 + * To avoid that while still allowing users to pinch-zoom for accessibility, we 11 + * only pin `maximum-scale=1` while a text input is focused and restore the 12 + * original viewport on blur. 13 + * 14 + * Listeners run in the capture phase so we update the viewport before iOS 15 + * commits to auto-zooming the input. 16 + */ 17 + export function useViewportZoomLock({enabled} = {enabled: true}) { 18 + useEffect(() => { 19 + if (!IS_WEB_MOBILE_IOS) return 20 + if (!enabled) return 21 + 22 + const meta = document.querySelector('meta[name="viewport"]') 23 + if (!(meta instanceof HTMLMetaElement)) return 24 + 25 + const originalContent = meta.content 26 + 27 + const onFocus = (e: FocusEvent) => { 28 + if (isTextInput(e.target)) { 29 + meta.content = ZOOM_LOCKED_VIEWPORT 30 + } 31 + } 32 + 33 + const onBlur = (e: FocusEvent) => { 34 + if (isTextInput(e.target)) { 35 + meta.content = originalContent 36 + } 37 + } 38 + 39 + document.addEventListener('focus', onFocus, true) 40 + document.addEventListener('blur', onBlur, true) 41 + 42 + return () => { 43 + document.removeEventListener('focus', onFocus, true) 44 + document.removeEventListener('blur', onBlur, true) 45 + meta.content = originalContent 46 + } 47 + }, [enabled]) 48 + } 49 + 50 + const NON_TEXT_INPUT_TYPES = new Set([ 51 + 'button', 52 + 'checkbox', 53 + 'color', 54 + 'file', 55 + 'hidden', 56 + 'image', 57 + 'radio', 58 + 'range', 59 + 'reset', 60 + 'submit', 61 + ]) 62 + 63 + function isTextInput(target: EventTarget | null): boolean { 64 + if (!(target instanceof HTMLElement)) return false 65 + if (target.isContentEditable) return true 66 + if (target instanceof HTMLTextAreaElement) return true 67 + if (target instanceof HTMLInputElement) { 68 + return !NON_TEXT_INPUT_TYPES.has(target.type) 69 + } 70 + return false 71 + }
+3
src/screens/Messages/Conversation.tsx
··· 18 18 import {RemoveScrollBar} from 'react-remove-scroll-bar' 19 19 20 20 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 21 + import {useViewportZoomLock} from '#/lib/hooks/useViewportZoomLock' 21 22 import { 22 23 type CommonNavigatorParams, 23 24 type NavigationProp, ··· 105 106 const isFocused = useIsFocused() 106 107 const {top: topInset} = useSafeAreaInsets() 107 108 const {data: convoData} = useConvoQuery({convoId}) 109 + 110 + useViewportZoomLock({enabled: isFocused}) 108 111 109 112 const convo = convoData 110 113 ? parseConvoView(convoData, currentAccount?.did)
+1 -1
web/index.html
··· 9 9 --> 10 10 <meta 11 11 name="viewport" 12 - content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, viewport-fit=cover" 12 + content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover" 13 13 /> 14 14 <!-- 15 15 Preconnect to essential domains