Bluesky app fork with some witchin' additions 💫
1import {useEffect} from 'react'
2
3import {IS_WEB_MOBILE_IOS} from '#/env'
4
5const 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 */
17export 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
50const 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
63function 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}