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..ab309ee5f 100644 --- a/web-app/src/hooks/use-mobile.ts +++ b/web-app/src/hooks/use-mobile.ts @@ -1,4 +1,5 @@ import * as React from "react" +import { attachMediaListener } from "./useMediaQuery" const MOBILE_BREAKPOINT = 768 @@ -10,9 +11,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 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 ) {