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

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
This commit is contained in:
Vansh
2026-05-13 05:01:17 +05:30
committed by GitHub
parent 990eba6134
commit 9e39a7f5a1
3 changed files with 128 additions and 0 deletions
@@ -0,0 +1,83 @@
import { installSafariPinchZoomBlocker } from "../../src/client/utilities/DisableSafariPinchZoom";
const GESTURE_EVENTS = ["gesturestart", "gesturechange", "gestureend"] as const;
function dispatchCancelableGestureEvent(
target: EventTarget,
type: string,
): Event {
// Safari's GestureEvent is not available in jsdom. Dispatch a plain
// cancelable Event of the same name so preventDefault() is observable via
// defaultPrevented.
const event = new Event(type, { bubbles: true, cancelable: true });
target.dispatchEvent(event);
return event;
}
describe("installSafariPinchZoomBlocker", () => {
it("prevents the default action of each Safari gesture event on document", () => {
const uninstall = installSafariPinchZoomBlocker();
try {
for (const type of GESTURE_EVENTS) {
const event = dispatchCancelableGestureEvent(document, type);
expect(event.defaultPrevented).toBe(true);
}
} finally {
uninstall();
}
});
it("attaches listeners to the provided target", () => {
const target = document.createElement("div");
const uninstall = installSafariPinchZoomBlocker(target);
try {
for (const type of GESTURE_EVENTS) {
const event = dispatchCancelableGestureEvent(target, type);
expect(event.defaultPrevented).toBe(true);
}
} finally {
uninstall();
}
});
it("removes the listeners when the returned disposer is called", () => {
const target = document.createElement("div");
const uninstall = installSafariPinchZoomBlocker(target);
uninstall();
for (const type of GESTURE_EVENTS) {
const event = dispatchCancelableGestureEvent(target, type);
expect(event.defaultPrevented).toBe(false);
}
});
it("does not affect events on unrelated targets", () => {
const scope = document.createElement("div");
const other = document.createElement("div");
const uninstall = installSafariPinchZoomBlocker(scope);
try {
const event = dispatchCancelableGestureEvent(other, "gesturestart");
expect(event.defaultPrevented).toBe(false);
} finally {
uninstall();
}
});
it("leaves unrelated event types alone", () => {
const uninstall = installSafariPinchZoomBlocker();
try {
const event = new Event("touchstart", {
bubbles: true,
cancelable: true,
});
document.dispatchEvent(event);
expect(event.defaultPrevented).toBe(false);
} finally {
uninstall();
}
});
});