Add useIsMobileViewport hook + Online-Board mobile time defaults per TZ 4.1.1-R4/R5
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { useIsMobileViewport } from "./useIsMobileViewport.js";
|
||||
|
||||
describe("useIsMobileViewport", () => {
|
||||
let matchMediaMock: ReturnType<typeof vi.fn>;
|
||||
let listeners: ((e: { matches: boolean }) => void)[];
|
||||
let currentMatches = false;
|
||||
|
||||
beforeEach(() => {
|
||||
listeners = [];
|
||||
currentMatches = false;
|
||||
matchMediaMock = vi.fn().mockImplementation((query: string) => ({
|
||||
matches: currentMatches,
|
||||
media: query,
|
||||
addEventListener: (_type: string, listener: (e: { matches: boolean }) => void) => {
|
||||
listeners.push(listener);
|
||||
},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => false,
|
||||
}));
|
||||
Object.defineProperty(window, "matchMedia", { value: matchMediaMock, configurable: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("returns false when viewport is wider than mobile breakpoint", () => {
|
||||
currentMatches = false;
|
||||
const { result } = renderHook(() => useIsMobileViewport());
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when viewport is narrower than mobile breakpoint", () => {
|
||||
currentMatches = true;
|
||||
const { result } = renderHook(() => useIsMobileViewport());
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it("updates when media-query match changes", () => {
|
||||
currentMatches = false;
|
||||
const { result } = renderHook(() => useIsMobileViewport());
|
||||
expect(result.current).toBe(false);
|
||||
act(() => {
|
||||
for (const l of listeners) l({ matches: true });
|
||||
});
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/** Mobile breakpoint — matches the project's `$media-breakpoint-mobile` token (640px). */
|
||||
const MOBILE_BREAKPOINT_PX = 640;
|
||||
|
||||
/**
|
||||
* Returns `true` when the viewport is ≤ the mobile breakpoint. Used to
|
||||
* branch TZ-prescribed mobile-only behaviors (e.g. Online-Board's
|
||||
* `Время рейса` default = -1h/+3h per §4.1.1-R5).
|
||||
*
|
||||
* SSR: returns `false` initially, then updates on first client render.
|
||||
*/
|
||||
export function useIsMobileViewport(): boolean {
|
||||
const [isMobile, setIsMobile] = useState<boolean>(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
return window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT_PX}px)`).matches;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT_PX}px)`);
|
||||
const listener = (e: MediaQueryListEvent | { matches: boolean }) => setIsMobile(e.matches);
|
||||
setIsMobile(mql.matches);
|
||||
mql.addEventListener("change", listener as (e: MediaQueryListEvent) => void);
|
||||
return () => mql.removeEventListener("change", listener as (e: MediaQueryListEvent) => void);
|
||||
}, []);
|
||||
|
||||
return isMobile;
|
||||
}
|
||||
Reference in New Issue
Block a user