diff --git a/frontend/src/components/App/App.tsx b/frontend/src/components/App/App.tsx index dadbe8a40..bc89cc405 100644 --- a/frontend/src/components/App/App.tsx +++ b/frontend/src/components/App/App.tsx @@ -18,6 +18,7 @@ import Profile from "../Profile/Profile"; import Reload from "../Reload/Reload"; import StoreProfile from "../StoreProfile/StoreProfile"; import useDisableRightClickOnTouchDevices from "../../hooks/useDisableRightClickOnTouchDevices"; +import useDisableIOSPinchZoomOnTouchDevices from "@/hooks/useDisableIOSPinchZoomOnTouchDevices"; import { InternalRedirect } from "../InternalRedirect/InternalRedirect"; import Helmet from "@/components/Helmet/Helmet"; @@ -31,6 +32,7 @@ const App = () => { const queryParams = window.location.search; useDisableRightClickOnTouchDevices(); + useDisableIOSPinchZoomOnTouchDevices(); useEffect(() => { const urlParams = new URLSearchParams(queryParams); diff --git a/frontend/src/hooks/useDisableIOSPinchZoomOnTouchDevices.test.ts b/frontend/src/hooks/useDisableIOSPinchZoomOnTouchDevices.test.ts new file mode 100644 index 000000000..f94e1fdd2 --- /dev/null +++ b/frontend/src/hooks/useDisableIOSPinchZoomOnTouchDevices.test.ts @@ -0,0 +1,36 @@ +import { vi, describe, test, expect, afterEach } from "vitest"; +import { renderHook } from "@testing-library/react-hooks"; +import { waitFor } from "@testing-library/react"; +import useDisableIOSPinchZoomOnTouchDevices from "./useDisableIOSPinchZoomOnTouchDevices"; + +describe("useDisableIOSPinchZoomOnTouchDevices", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + test("should prevent gesturestart on touch devices", async () => { + vi.stubGlobal("ontouchstart", true); + + const mockEvent = new Event('gesturestart'); + mockEvent.preventDefault = vi.fn(); + + const spy = vi.spyOn(mockEvent, 'preventDefault').mockImplementation(() => { }); + + renderHook(() => useDisableIOSPinchZoomOnTouchDevices()); + + await waitFor(() => document.dispatchEvent(mockEvent)); + await waitFor(() => expect(spy).toHaveBeenCalledTimes(1)); + }); + + test("should not prevent gesturestart on non-touch devices", () => { + const mockEvent = new Event('gesturestart'); + mockEvent.preventDefault = vi.fn(); + + const spy = vi.spyOn(mockEvent, 'preventDefault').mockImplementation(() => { }); + + renderHook(() => useDisableIOSPinchZoomOnTouchDevices()); + + document.dispatchEvent(mockEvent); + expect(spy).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/hooks/useDisableIOSPinchZoomOnTouchDevices.ts b/frontend/src/hooks/useDisableIOSPinchZoomOnTouchDevices.ts new file mode 100644 index 000000000..aa607aa14 --- /dev/null +++ b/frontend/src/hooks/useDisableIOSPinchZoomOnTouchDevices.ts @@ -0,0 +1,20 @@ +import { useEffect } from 'react'; + +const useDisableIOSPinchZoomOnTouchDevices = () => useEffect(() => { + const isTouchDevice = () => !!('ontouchstart' in window || !!(navigator.maxTouchPoints)); + + const handlePinchZoom = (event: Event) => { + if (isTouchDevice()) { + event.preventDefault(); + } + }; + + document.addEventListener('gesturestart', handlePinchZoom); + + return () => { + document.removeEventListener('gesturestart', handlePinchZoom); + }; +}, []); + + +export default useDisableIOSPinchZoomOnTouchDevices; diff --git a/frontend/src/scss/layout.scss b/frontend/src/scss/layout.scss index 728cd0dba..3221346ed 100644 --- a/frontend/src/scss/layout.scss +++ b/frontend/src/scss/layout.scss @@ -6,6 +6,9 @@ body.root { background-position: center center; background-repeat: no-repeat; min-height: 100vh; + + /* disable pinch zoom on iOS */ + touch-action: manipulation; } #root {