Skip to content

Commit 127b91c

Browse files
committed
Add hook to prevent ios zoom
1 parent 933c6c3 commit 127b91c

5 files changed

Lines changed: 78 additions & 50 deletions

File tree

packages/plugin-annotation/src/shared/components/annotations/free-text.tsx

Lines changed: 9 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
1-
import {
2-
MouseEvent,
3-
useEffect,
4-
useLayoutEffect,
5-
useRef,
6-
useState,
7-
suppressContentEditableWarningProps,
8-
} from '@framework';
1+
import { MouseEvent, useEffect, useRef, suppressContentEditableWarningProps } from '@framework';
92
import {
103
PdfFreeTextAnnoObject,
114
PdfVerticalAlignment,
125
standardFontCssProperties,
136
textAlignmentToCss,
147
} from '@embedpdf/models';
15-
import { useAnnotationCapability } from '../..';
8+
import { useAnnotationCapability, useIOSZoomPrevention } from '../..';
169
import { TrackedAnnotation } from '@embedpdf/plugin-annotation';
1710

1811
interface FreeTextProps {
@@ -42,7 +35,10 @@ export function FreeText({
4235
const editingRef = useRef(false);
4336
const { provides: annotationCapability } = useAnnotationCapability();
4437
const annotationProvides = annotationCapability?.forDocument(documentId) ?? null;
45-
const [isIOS, setIsIOS] = useState(false);
38+
const { adjustedFontPx, wrapperStyle } = useIOSZoomPrevention(
39+
annotation.object.fontSize * scale,
40+
isEditing,
41+
);
4642

4743
useEffect(() => {
4844
if (isEditing && editorRef.current) {
@@ -67,18 +63,6 @@ export function FreeText({
6763
}
6864
}, [isEditing]);
6965

70-
useLayoutEffect(() => {
71-
try {
72-
const nav = navigator as any;
73-
const ios =
74-
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
75-
(navigator.platform === 'MacIntel' && nav?.maxTouchPoints > 1);
76-
setIsIOS(ios);
77-
} catch {
78-
setIsIOS(false);
79-
}
80-
}, []);
81-
8266
const handleBlur = () => {
8367
if (!editingRef.current) return;
8468
editingRef.current = false;
@@ -89,15 +73,6 @@ export function FreeText({
8973
});
9074
};
9175

92-
// iOS zoom prevention: keep focused font-size >= 16px, visually scale down if needed.
93-
const computedFontPx = annotation.object.fontSize * scale;
94-
const MIN_IOS_FOCUS_FONT_PX = 16;
95-
const needsComp =
96-
isIOS && isEditing && computedFontPx > 0 && computedFontPx < MIN_IOS_FOCUS_FONT_PX;
97-
const adjustedFontPx = needsComp ? MIN_IOS_FOCUS_FONT_PX : computedFontPx;
98-
const scaleComp = needsComp ? computedFontPx / MIN_IOS_FOCUS_FONT_PX : 1;
99-
const invScalePercent = needsComp ? 100 / scaleComp : 100;
100-
10176
return (
10277
<div
10378
style={{
@@ -130,14 +105,13 @@ export function FreeText({
130105
display: 'flex',
131106
backgroundColor: annotation.object.color ?? annotation.object.backgroundColor,
132107
opacity: annotation.object.opacity,
133-
width: needsComp ? `${invScalePercent}%` : '100%',
134-
height: needsComp ? `${invScalePercent}%` : '100%',
108+
width: '100%',
109+
height: '100%',
135110
lineHeight: '1.18',
136111
overflow: 'hidden',
137112
cursor: isEditing ? 'text' : onClick ? 'pointer' : 'default',
138113
outline: 'none',
139-
transform: needsComp ? `scale(${scaleComp})` : undefined,
140-
transformOrigin: 'top left',
114+
...wrapperStyle,
141115
}}
142116
contentEditable={isEditing}
143117
{...suppressContentEditableWarningProps}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './use-annotation';
2+
export * from './use-ios-zoom-prevention';
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { useMemo, CSSProperties } from '@framework';
2+
3+
const MIN_IOS_FOCUS_FONT_PX = 16;
4+
5+
function detectIOS(): boolean {
6+
try {
7+
const nav = navigator as any;
8+
return (
9+
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
10+
(navigator.platform === 'MacIntel' && nav?.maxTouchPoints > 1)
11+
);
12+
} catch {
13+
return false;
14+
}
15+
}
16+
17+
let _isIOS: boolean | undefined;
18+
function getIsIOS(): boolean {
19+
if (_isIOS === undefined) {
20+
_isIOS = detectIOS();
21+
}
22+
return _isIOS;
23+
}
24+
25+
/**
26+
* Prevents iOS Safari from auto-zooming when a focused input's computed
27+
* font-size is below 16px. Returns an adjusted font size and optional
28+
* wrapper style that uses `transform: scale()` to visually match the
29+
* intended size while keeping the real font at >= 16px.
30+
*
31+
* @param computedFontPx - The intended on-screen font size (fontSize * scale)
32+
* @param active - Whether compensation should apply (e.g. isEditing, or always true for form inputs)
33+
*/
34+
export function useIOSZoomPrevention(computedFontPx: number, active: boolean) {
35+
const isIOS = getIsIOS();
36+
37+
return useMemo(() => {
38+
const needsComp =
39+
isIOS && active && computedFontPx > 0 && computedFontPx < MIN_IOS_FOCUS_FONT_PX;
40+
const adjustedFontPx = needsComp ? MIN_IOS_FOCUS_FONT_PX : computedFontPx;
41+
const scaleComp = needsComp ? computedFontPx / MIN_IOS_FOCUS_FONT_PX : 1;
42+
43+
const wrapperStyle: CSSProperties | undefined = needsComp
44+
? {
45+
width: `${100 / scaleComp}%`,
46+
height: `${100 / scaleComp}%`,
47+
transform: `scale(${scaleComp})`,
48+
transformOrigin: 'top left',
49+
}
50+
: undefined;
51+
52+
return { needsComp, adjustedFontPx, scaleComp, wrapperStyle };
53+
}, [isIOS, active, computedFontPx]);
54+
}

packages/plugin-form/src/shared/components/fields/text.tsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { PDF_FORM_FIELD_FLAG, standardFontCssProperties } from '@embedpdf/models';
22
import { CSSProperties, FormEvent, useCallback, useEffect, useMemo, useState } from '@framework';
3+
import { useIOSZoomPrevention } from '@embedpdf/plugin-annotation/@framework';
34

45
import { TextFieldProps } from '../types';
56
import { inputStyle, textareaStyle } from './style';
@@ -151,6 +152,7 @@ export function TextField(props: TextFieldProps) {
151152

152153
const bw = (annotation.strokeWidth ?? 0) * scale;
153154
const fontCss = standardFontCssProperties(annotation.fontFamily);
155+
const { adjustedFontPx, wrapperStyle } = useIOSZoomPrevention(annotation.fontSize * scale, true);
154156

155157
const visualStyle: CSSProperties = useMemo(
156158
() => ({
@@ -160,18 +162,10 @@ export function TextField(props: TextFieldProps) {
160162
borderWidth: bw,
161163
color: annotation.fontColor,
162164
...fontCss,
163-
fontSize: annotation.fontSize * scale,
165+
fontSize: adjustedFontPx,
164166
padding: `${bw}px ${bw}px`,
165167
}),
166-
[
167-
annotation.color,
168-
annotation.strokeColor,
169-
annotation.fontColor,
170-
annotation.fontSize,
171-
bw,
172-
fontCss,
173-
scale,
174-
],
168+
[annotation.color, annotation.strokeColor, annotation.fontColor, adjustedFontPx, bw, fontCss],
175169
);
176170

177171
const isDisabled = !isEditable || !!(flag & PDF_FORM_FIELD_FLAG.READONLY);
@@ -197,10 +191,10 @@ export function TextField(props: TextFieldProps) {
197191
const cellFont: CSSProperties = {
198192
color: annotation.fontColor,
199193
...fontCss,
200-
fontSize: annotation.fontSize * scale,
194+
fontSize: adjustedFontPx,
201195
};
202196

203-
return (
197+
const combContent = (
204198
<CombField
205199
inputRef={inputRef as (el: HTMLInputElement | null) => void}
206200
required={isRequired}
@@ -218,9 +212,11 @@ export function TextField(props: TextFieldProps) {
218212
onBlur={onBlur}
219213
/>
220214
);
215+
216+
return wrapperStyle ? <div style={wrapperStyle}>{combContent}</div> : combContent;
221217
}
222218

223-
return isMultipleLine ? (
219+
const inputContent = isMultipleLine ? (
224220
<textarea
225221
ref={inputRef as (el: HTMLTextAreaElement | null) => void}
226222
required={isRequired}
@@ -248,4 +244,6 @@ export function TextField(props: TextFieldProps) {
248244
style={{ ...inputStyle, ...visualStyle }}
249245
/>
250246
);
247+
248+
return wrapperStyle ? <div style={wrapperStyle}>{inputContent}</div> : inputContent;
251249
}

viewers/snippet/src/components/mode-select-button.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ export function ModeSelectButton({ documentId, className }: ModeSelectButtonProp
1414
const commandAnnotate = useCommand('mode:annotate', documentId);
1515
const commandShapes = useCommand('mode:shapes', documentId);
1616
const commandRedact = useCommand('mode:redact', documentId);
17+
const commandForm = useCommand('mode:form', documentId);
1718
const commandOverflow = useCommand('tabs:overflow-menu', documentId);
1819

1920
// Find the currently active mode
2021
const activeCommand = useMemo(() => {
21-
const commands = [commandView, commandAnnotate, commandShapes, commandRedact];
22+
const commands = [commandView, commandAnnotate, commandShapes, commandForm, commandRedact];
2223
return commands.find((cmd) => cmd?.active) || commandView;
23-
}, [commandView, commandAnnotate, commandShapes, commandRedact]);
24+
}, [commandView, commandAnnotate, commandShapes, commandForm, commandRedact]);
2425

2526
const handleClick = useCallback(
2627
(e: MouseEvent) => {

0 commit comments

Comments
 (0)