Skip to content

fix(client): block Safari page-level pinch-zoom#3901

Merged
evanpelle merged 1 commit into
openfrontio:mainfrom
vansszh:fix/2330-safari-pinch-zoom
May 12, 2026
Merged

fix(client): block Safari page-level pinch-zoom#3901
evanpelle merged 1 commit into
openfrontio:mainfrom
vansszh:fix/2330-safari-pinch-zoom

Conversation

@vansszh
Copy link
Copy Markdown
Contributor

@vansszh vansszh commented May 11, 2026

iOS Safari has ignored the user-scalable=no viewport hint since iOS

10, so two-finger pinch still zooms the whole page and can softlock the

in-game HUD. Intercept WebKit's non-standard gesturestart,

gesturechange and gestureend events at document and call

preventDefault() so the page stays put. The game's own pinch-to-zoom

on the map canvas is driven by pointer events (InputHandler) and is

unaffected; browsers that do not fire GestureEvent treat the listeners

as a no-op.

Resolves #2330

If this PR fixes an issue, link it below. If not, delete these two lines.
Resolves #(issue number)

Description:

Describe the PR.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

DISCORD_USERNAME

iOS Safari has ignored the `user-scalable=no` viewport hint since iOS

10, so two-finger pinch still zooms the whole page and can softlock the

in-game HUD. Intercept WebKit's non-standard `gesturestart`,

`gesturechange` and `gestureend` events at `document` and call

`preventDefault()` so the page stays put. The game's own pinch-to-zoom

on the map canvas is driven by pointer events (InputHandler) and is

unaffected; browsers that do not fire GestureEvent treat the listeners

as a no-op.

Resolves openfrontio#2330
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

Review Change Stack

Walkthrough

This PR introduces a Safari-specific pinch-zoom blocker to prevent iOS Safari's native gesture zooming, which can interfere with the HUD. A new utility function registers preventDefault() handlers for Safari's non-standard gesturestart, gesturechange, and gestureend events, is called during app bootstrap, and is validated with targeted tests covering default prevention, target scoping, listener cleanup, and event filtering.

Changes

Safari Pinch-Zoom Prevention

Layer / File(s) Summary
Gesture Event Blocking Implementation
src/client/utilities/DisableSafariPinchZoom.ts
New utility exports installSafariPinchZoomBlocker(target) which registers preventDefault() listeners for Safari gesture events (gesturestart, gesturechange, gestureend) on a provided target (defaulting to document), and returns a cleanup function to remove those listeners.
Bootstrap Integration
src/client/Main.ts
Imports and calls installSafariPinchZoomBlocker() during the bootstrap() sequence before layout and navigation initialization, with a comment explaining its purpose.
Test Suite
tests/client/DisableSafariPinchZoom.test.ts
Adds six test cases covering: basic gesture event prevention on document, blocker behavior on explicit targets, listener disposal via returned disposer, scope isolation between targets, and confirmation that unrelated event types like touchstart remain unaffected. Includes a helper function to dispatch cancelable gesture events under jsdom.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

Safari's pinch-zoom threat meets its match,
Gesture listeners block with a catch,
HUD stays safe from zooming spree,
iOS browsers finally agree! 📱✋

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: blocking Safari's page-level pinch-zoom behavior on the client side.
Description check ✅ Passed The description explains the problem (iOS Safari ignoring viewport hints), the solution (intercepting GestureEvent), and confirms no disruption to game functionality.
Linked Issues check ✅ Passed The PR fully addresses issue #2330 by implementing Safari gesture event listeners to prevent page-level pinch-zoom while preserving in-game pinch functionality.
Out of Scope Changes check ✅ Passed All changes (new utility, integration in Main.ts, and comprehensive tests) are directly scoped to resolving the Safari pinch-zoom issue with no extraneous modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/client/utilities/DisableSafariPinchZoom.ts (1)

23-40: Clean implementation that correctly blocks Safari gesture zoom.

The function properly:

  • Captures the block handler reference for both registration and cleanup
  • Returns a disposer that removes the exact same listeners
  • Defaults to document scope (where Safari decides page zoom) while allowing custom targets

The code will work correctly as-is. If you want to be explicit about the listener options (and avoid potential future console warnings), you could add { passive: false } when registering:

Optional: Make non-passive intent explicit
 const events = ["gesturestart", "gesturechange", "gestureend"] as const;
 for (const type of events) {
-  target.addEventListener(type, block);
+  target.addEventListener(type, block, { passive: false });
 }

 return () => {
   for (const type of events) {
-    target.removeEventListener(type, block);
+    target.removeEventListener(type, block, { passive: false });
   }
 };

This is not required—GestureEvent defaults to non-passive—but makes the intent clearer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/utilities/DisableSafariPinchZoom.ts` around lines 23 - 40, Add
explicit listener options to make non-passive intent clear: in
installSafariPinchZoomBlocker capture an options object like const opts = {
passive: false } and pass opts to target.addEventListener(type, block, opts) for
each event, and use the same opts when calling target.removeEventListener(type,
block, opts) in the returned disposer; this ensures the non-passive intent is
explicit and removal matches the registered listeners (referencing
installSafariPinchZoomBlocker, block, and events).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/client/utilities/DisableSafariPinchZoom.ts`:
- Around line 23-40: Add explicit listener options to make non-passive intent
clear: in installSafariPinchZoomBlocker capture an options object like const
opts = { passive: false } and pass opts to target.addEventListener(type, block,
opts) for each event, and use the same opts when calling
target.removeEventListener(type, block, opts) in the returned disposer; this
ensures the non-passive intent is explicit and removal matches the registered
listeners (referencing installSafariPinchZoomBlocker, block, and events).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e1c1328a-1bbb-4994-bbe9-ef1f759a3921

📥 Commits

Reviewing files that changed from the base of the PR and between 2b04c56 and d9ba457.

📒 Files selected for processing (3)
  • src/client/Main.ts
  • src/client/utilities/DisableSafariPinchZoom.ts
  • tests/client/DisableSafariPinchZoom.test.ts

Comment on lines +35 to +39
return () => {
for (const type of events) {
target.removeEventListener(type, block);
}
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like this is never called? any reason to return it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contribution

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

@highheat4
Copy link
Copy Markdown

@vansszh I have a fix ready, looks like you missed my comment on the original issue. The fix looks solid at first glance so feel free to continue

@evanpelle evanpelle added this to the v32 milestone May 12, 2026
Comment on lines +35 to +39
return () => {
for (const type of events) {
target.removeEventListener(type, block);
}
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

@github-project-automation github-project-automation Bot moved this from Triage to Final Review in OpenFront Release Management May 12, 2026
@evanpelle evanpelle merged commit 9e39a7f into openfrontio:main May 12, 2026
10 of 13 checks passed
@github-project-automation github-project-automation Bot moved this from Final Review to Complete in OpenFront Release Management May 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

Safari allows zooming

4 participants