From e113f4c7136326bc00ea77c9cbb2fdf9a3b8d765 Mon Sep 17 00:00:00 2001 From: Ayush7614 Date: Fri, 14 Aug 2026 18:56:34 +0530 Subject: [PATCH 1/2] fix: use-mobile hook crashes on older Safari without MediaQueryList.addEventListener useIsMobile() called mql.addEventListener('change', onChange) directly on a MediaQueryList. Older Safari (Catalina and before) only implements the deprecated addListener/removeListener pair, causing a runtime crash. The sibling hook useMediaQuery.ts already has an attachMediaListener helper with a try/catch fallback for this exact case. Use the same pattern here for consistency and cross-browser compatibility. Used by Sidebar and Dropdrawer components. --- .../src/hooks/__tests__/use-mobile.test.ts | 76 +++++++++++++++++++ web-app/src/hooks/use-mobile.ts | 29 ++++++- 2 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 web-app/src/hooks/__tests__/use-mobile.test.ts diff --git a/web-app/src/hooks/__tests__/use-mobile.test.ts b/web-app/src/hooks/__tests__/use-mobile.test.ts new file mode 100644 index 000000000..9b3199a44 --- /dev/null +++ b/web-app/src/hooks/__tests__/use-mobile.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect, vi, afterEach } from 'vitest' +import { renderHook } from '@testing-library/react' +import { useIsMobile } from '../use-mobile' + +afterEach(() => { + vi.restoreAllMocks() + vi.useRealTimers() +}) + +describe('useIsMobile', () => { + it('returns false on a wide screen', () => { + Object.defineProperty(window, 'innerWidth', { value: 1024, writable: true }) + const mql = { + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + } + vi.spyOn(window, 'matchMedia').mockReturnValue(mql as any) + + const { result } = renderHook(() => useIsMobile()) + expect(result.current).toBe(false) + }) + + it('returns true on a narrow screen', () => { + Object.defineProperty(window, 'innerWidth', { value: 400, writable: true }) + const mql = { + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + } + vi.spyOn(window, 'matchMedia').mockReturnValue(mql as any) + + const { result } = renderHook(() => useIsMobile()) + expect(result.current).toBe(true) + }) + + it('falls back to deprecated addListener on older browsers that lack addEventListener on MediaQueryList', () => { + const consoleWarnSpy = vi + .spyOn(console, 'warn') + .mockImplementation(() => {}) + + const mql = { + addEventListener: vi.fn(() => { + throw new Error('addEventListener not supported on MediaQueryList') + }), + removeEventListener: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + } + vi.spyOn(window, 'matchMedia').mockReturnValue(mql as any) + Object.defineProperty(window, 'innerWidth', { value: 1024, writable: true }) + + renderHook(() => useIsMobile()) + + expect(mql.addListener).toHaveBeenCalledWith(expect.any(Function)) + consoleWarnSpy.mockRestore() + }) + + it('removes the deprecated listener on unmount in older browsers', () => { + vi.spyOn(console, 'warn').mockImplementation(() => {}) + + const mql = { + addEventListener: vi.fn(() => { + throw new Error('addEventListener not supported') + }), + removeEventListener: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + } + vi.spyOn(window, 'matchMedia').mockReturnValue(mql as any) + Object.defineProperty(window, 'innerWidth', { value: 1024, writable: true }) + + const { unmount } = renderHook(() => useIsMobile()) + unmount() + + expect(mql.removeListener).toHaveBeenCalledWith(expect.any(Function)) + }) +}) diff --git a/web-app/src/hooks/use-mobile.ts b/web-app/src/hooks/use-mobile.ts index 2b0fe1dfe..0d033895b 100644 --- a/web-app/src/hooks/use-mobile.ts +++ b/web-app/src/hooks/use-mobile.ts @@ -2,6 +2,31 @@ import * as React from "react" const MOBILE_BREAKPOINT = 768 +type MediaQueryCallback = (event: { matches: boolean; media: string }) => void + +/** + * Older versions of Safari (shipped with Catalina and before) do not support + * addEventListener on MediaQueryList — they only implement the deprecated + * addListener/removeListener pair. Use the same try/catch fallback pattern as + * useMediaQuery.ts so the hook doesn't crash on those platforms. + */ +function attachMediaListener( + query: MediaQueryList, + callback: MediaQueryCallback +) { + try { + query.addEventListener("change", callback) + return () => query.removeEventListener("change", callback) + } catch (e) { + console.warn(e) + // @ts-expect-error — addListener is deprecated but still present on older browsers + query.addListener(callback) + return () => + // @ts-expect-error + query.removeListener(callback) + } +} + export function useIsMobile() { const [isMobile, setIsMobile] = React.useState(undefined) @@ -10,9 +35,9 @@ export function useIsMobile() { const onChange = () => { setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) } - mql.addEventListener("change", onChange) + const cleanup = attachMediaListener(mql, onChange) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) - return () => mql.removeEventListener("change", onChange) + return cleanup }, []) return !!isMobile From c9bf47351188b25ca4fe17a91ddac5f868a37ff2 Mon Sep 17 00:00:00 2001 From: Ayush7614 Date: Wed, 19 Aug 2026 14:52:20 +0530 Subject: [PATCH 2/2] fix: reuse attachMediaListener from useMediaQuery and drop unused ts-expect-error - lib.dom declares addListener/removeListener (deprecated but present), so the @ts-expect-error directives were unused and failed tsc (TS2578). - Export attachMediaListener + MediaQueryCallback from useMediaQuery.ts and import them here, removing the duplicated helper. --- web-app/src/hooks/use-mobile.ts | 26 +------------------------- web-app/src/hooks/useMediaQuery.ts | 4 ++-- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/web-app/src/hooks/use-mobile.ts b/web-app/src/hooks/use-mobile.ts index 0d033895b..ab309ee5f 100644 --- a/web-app/src/hooks/use-mobile.ts +++ b/web-app/src/hooks/use-mobile.ts @@ -1,32 +1,8 @@ import * as React from "react" +import { attachMediaListener } from "./useMediaQuery" const MOBILE_BREAKPOINT = 768 -type MediaQueryCallback = (event: { matches: boolean; media: string }) => void - -/** - * Older versions of Safari (shipped with Catalina and before) do not support - * addEventListener on MediaQueryList — they only implement the deprecated - * addListener/removeListener pair. Use the same try/catch fallback pattern as - * useMediaQuery.ts so the hook doesn't crash on those platforms. - */ -function attachMediaListener( - query: MediaQueryList, - callback: MediaQueryCallback -) { - try { - query.addEventListener("change", callback) - return () => query.removeEventListener("change", callback) - } catch (e) { - console.warn(e) - // @ts-expect-error — addListener is deprecated but still present on older browsers - query.addListener(callback) - return () => - // @ts-expect-error - query.removeListener(callback) - } -} - export function useIsMobile() { const [isMobile, setIsMobile] = React.useState(undefined) diff --git a/web-app/src/hooks/useMediaQuery.ts b/web-app/src/hooks/useMediaQuery.ts index dd41e1417..bfbb4f789 100644 --- a/web-app/src/hooks/useMediaQuery.ts +++ b/web-app/src/hooks/useMediaQuery.ts @@ -5,7 +5,7 @@ export interface UseMediaQueryOptions { getInitialValueInEffect: boolean } -type MediaQueryCallback = (event: { matches: boolean; media: string }) => void +export type MediaQueryCallback = (event: { matches: boolean; media: string }) => void // Zustand store for small screen state type SmallScreenState = { @@ -22,7 +22,7 @@ export const useSmallScreenStore = create((set) => ({ * Older versions of Safari (shipped withCatalina and before) do not support addEventListener on matchMedia * https://stackoverflow.com/questions/56466261/matchmedia-addlistener-marked-as-deprecated-addeventlistener-equivalent * */ -function attachMediaListener( +export function attachMediaListener( query: MediaQueryList, callback: MediaQueryCallback ) {