Keep dashboard mobile/tablet viewport transitions from blanking or leaving stale mobile chrome. - update viewport-mode detection to resync on resize, orientation changes, and visualViewport resize events - stabilize board scroll handling across mobile viewport changes without forcing tablet layouts back to the mobile scroll position - expand dashboard regression coverage for mobile/tablet/desktop auto-merge toggles, app-shell reflow, and viewport listener wiring Files changed: packages/dashboard/app/components/Board.tsx | 33 ++++- .../app/components/__tests__/ChatView.test.tsx | 4 +- ...-merge-toggle-blank.mobile-integration.test.tsx | 145 ++++++++++++++++++--- .../app/hooks/__tests__/useViewportMode.test.ts | 126 +++++++++++++++++- packages/dashboard/app/hooks/useViewportMode.ts | 21 ++- 5 files changed, 295 insertions(+), 34 deletions(-) Fusion-Task-Id: FN-6047 Fusion-Task-Lineage: 5d5dcedf-6265-4672-b63e-414f3339e417
74 lines
2.6 KiB
TypeScript
74 lines
2.6 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
|
|
export type ViewportMode = "mobile" | "tablet" | "desktop";
|
|
|
|
// `(max-height: 480px)` catches phones held in landscape, which can exceed
|
|
// 768 CSS px wide but stay short. Without it, landscape phones fall out of
|
|
// mobile mode and lose the bottom nav bar + get the desktop horizontally-
|
|
// scrollable board.
|
|
export const MOBILE_MEDIA_QUERY = "(max-width: 768px), (max-height: 480px)";
|
|
|
|
export function getViewportMode(): ViewportMode {
|
|
if (typeof window === "undefined") return "desktop";
|
|
if (window.matchMedia(MOBILE_MEDIA_QUERY).matches) return "mobile";
|
|
if (window.matchMedia("(min-width: 769px) and (max-width: 1024px)").matches) return "tablet";
|
|
return "desktop";
|
|
}
|
|
|
|
export function useViewportMode(): ViewportMode {
|
|
const [mode, setMode] = useState<ViewportMode>(getViewportMode);
|
|
|
|
useEffect(() => {
|
|
if (typeof window === "undefined") return;
|
|
|
|
const mobileQuery = window.matchMedia(MOBILE_MEDIA_QUERY);
|
|
const tabletQuery = window.matchMedia("(min-width: 769px) and (max-width: 1024px)");
|
|
|
|
const updateMode = () => {
|
|
setMode(getViewportMode());
|
|
};
|
|
|
|
const addChangeListener = (query: MediaQueryList, listener: () => void) => {
|
|
if (typeof query.addEventListener === "function") {
|
|
query.addEventListener("change", listener);
|
|
return;
|
|
}
|
|
if (typeof query.addListener === "function") {
|
|
query.addListener(listener);
|
|
}
|
|
};
|
|
|
|
const removeChangeListener = (query: MediaQueryList, listener: () => void) => {
|
|
if (typeof query.removeEventListener === "function") {
|
|
query.removeEventListener("change", listener);
|
|
return;
|
|
}
|
|
if (typeof query.removeListener === "function") {
|
|
query.removeListener(listener);
|
|
}
|
|
};
|
|
|
|
addChangeListener(mobileQuery, updateMode);
|
|
addChangeListener(tabletQuery, updateMode);
|
|
window.addEventListener("resize", updateMode);
|
|
window.addEventListener("orientationchange", updateMode);
|
|
const visualViewport = window.visualViewport;
|
|
if (typeof visualViewport?.addEventListener === "function") {
|
|
visualViewport.addEventListener("resize", updateMode);
|
|
}
|
|
updateMode();
|
|
|
|
return () => {
|
|
removeChangeListener(mobileQuery, updateMode);
|
|
removeChangeListener(tabletQuery, updateMode);
|
|
window.removeEventListener("resize", updateMode);
|
|
window.removeEventListener("orientationchange", updateMode);
|
|
if (typeof visualViewport?.removeEventListener === "function") {
|
|
visualViewport.removeEventListener("resize", updateMode);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
return mode;
|
|
}
|