fix: refit mobile maps after viewport changes
This commit is contained in:
parent
90404b778a
commit
870253eee5
@ -22,6 +22,7 @@ export default function useLocationMapViewControls({
|
||||
setZoom,
|
||||
svgRef,
|
||||
syncMapDraftFromState,
|
||||
zoom,
|
||||
}) {
|
||||
useEffect(() => {
|
||||
if (!selectedObjectKey) return;
|
||||
@ -38,6 +39,7 @@ export default function useLocationMapViewControls({
|
||||
mode,
|
||||
setZoom,
|
||||
svgRef,
|
||||
zoom,
|
||||
});
|
||||
|
||||
const handleUndo = useCallback(() => {
|
||||
|
||||
@ -34,16 +34,25 @@ export default function useMobileMapAutoFit({
|
||||
mode,
|
||||
setZoom,
|
||||
svgRef,
|
||||
zoom,
|
||||
}) {
|
||||
const lastAutoFitKeyRef = useRef(null);
|
||||
const lastFittedZoomRef = useRef(null);
|
||||
const latestZoomRef = useRef(zoom);
|
||||
const autoFitKey = useMemo(
|
||||
() => getAutoFitKey(mapState, mapSize),
|
||||
[mapSize, mapState]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
latestZoomRef.current = zoom;
|
||||
}, [zoom]);
|
||||
|
||||
const fitMapToCanvas = useCallback(() => {
|
||||
const scrollElement = svgRef.current?.closest(".location-map-scroll");
|
||||
setZoom(getFitZoom(mapSize, scrollElement));
|
||||
const nextZoom = getFitZoom(mapSize, scrollElement);
|
||||
lastFittedZoomRef.current = nextZoom;
|
||||
setZoom(nextZoom);
|
||||
scrollElement?.scrollTo({ left: 0, top: 0 });
|
||||
}, [mapSize, setZoom, svgRef]);
|
||||
|
||||
@ -62,5 +71,41 @@ export default function useMobileMapAutoFit({
|
||||
return () => window.cancelAnimationFrame(frameId);
|
||||
}, [autoFitKey, fitMapToCanvas, mapState, mode, svgRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
if (!mapState || mode === "setup") return undefined;
|
||||
|
||||
let frameId = null;
|
||||
const mediaQuery = window.matchMedia(COMPACT_MAP_LAYOUT_QUERY);
|
||||
const shouldPreserveManualZoom = () => {
|
||||
const lastFittedZoom = lastFittedZoomRef.current;
|
||||
return lastFittedZoom !== null && Math.abs(latestZoomRef.current - lastFittedZoom) > 0.01;
|
||||
};
|
||||
const scheduleFit = () => {
|
||||
if (!mediaQuery.matches || shouldPreserveManualZoom()) return;
|
||||
if (frameId !== null) {
|
||||
window.cancelAnimationFrame(frameId);
|
||||
}
|
||||
frameId = window.requestAnimationFrame(() => {
|
||||
frameId = null;
|
||||
fitMapToCanvas();
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("resize", scheduleFit);
|
||||
window.addEventListener("orientationchange", scheduleFit);
|
||||
window.visualViewport?.addEventListener("resize", scheduleFit);
|
||||
scheduleFit();
|
||||
|
||||
return () => {
|
||||
if (frameId !== null) {
|
||||
window.cancelAnimationFrame(frameId);
|
||||
}
|
||||
window.removeEventListener("resize", scheduleFit);
|
||||
window.removeEventListener("orientationchange", scheduleFit);
|
||||
window.visualViewport?.removeEventListener("resize", scheduleFit);
|
||||
};
|
||||
}, [fitMapToCanvas, mapState, mode]);
|
||||
|
||||
return fitMapToCanvas;
|
||||
}
|
||||
|
||||
@ -203,6 +203,7 @@ export default function LocationMapManager() {
|
||||
setZoom,
|
||||
svgRef,
|
||||
syncMapDraftFromState,
|
||||
zoom,
|
||||
});
|
||||
|
||||
const performBackNavigation = useCallback(() => {
|
||||
|
||||
@ -206,6 +206,11 @@ async function expectMapFitsCanvas(page: Page, maxZoomPercent: number, context:
|
||||
return { scroll };
|
||||
}
|
||||
|
||||
async function readMapZoomPercent(page: Page) {
|
||||
const value = await page.locator(".location-map-zoom-controls span").textContent();
|
||||
return Number(value?.replace("%", "") || 0);
|
||||
}
|
||||
|
||||
test("admin creates a map from zones, saves a draft, and publishes it", async ({ page }) => {
|
||||
await mockMapShell(page);
|
||||
|
||||
@ -1458,6 +1463,56 @@ test("mobile keeps viewing draft status compact in the topbar", async ({ page })
|
||||
expect(editZoomBox.x + editZoomBox.width).toBeLessThanOrEqual(editToolbarBox.x + editToolbarBox.width + 1);
|
||||
});
|
||||
|
||||
test("mobile map refits after compact viewport resize until manually zoomed", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 700, height: 844 });
|
||||
await mockMapShell(page);
|
||||
|
||||
const mapState = publishedMapState([
|
||||
{
|
||||
id: 1258,
|
||||
location_map_id: 901,
|
||||
zone_id: 501,
|
||||
zone_name: "Bakery",
|
||||
type: "zone",
|
||||
label: "Bakery",
|
||||
x: 40,
|
||||
y: 40,
|
||||
width: 260,
|
||||
height: 160,
|
||||
rotation: 0,
|
||||
locked: false,
|
||||
visible: true,
|
||||
sort_order: 1,
|
||||
},
|
||||
], true);
|
||||
|
||||
await page.route("**/households/1/locations/10/map", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(mapState),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/stores/100/locations/10/map");
|
||||
|
||||
await expect.poll(async () => readMapZoomPercent(page)).toBeGreaterThan(0);
|
||||
const initialZoom = await readMapZoomPercent(page);
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await expect.poll(async () => readMapZoomPercent(page)).toBeLessThan(initialZoom - 8);
|
||||
const refitZoom = await readMapZoomPercent(page);
|
||||
|
||||
await page.getByRole("button", { name: "Zoom in" }).click();
|
||||
await page.getByRole("button", { name: "Zoom in" }).click();
|
||||
const manualZoom = await readMapZoomPercent(page);
|
||||
expect(manualZoom).toBeGreaterThan(refitZoom);
|
||||
|
||||
await page.setViewportSize({ width: 700, height: 844 });
|
||||
await page.waitForTimeout(150);
|
||||
await expect.poll(async () => readMapZoomPercent(page)).toBe(manualZoom);
|
||||
});
|
||||
|
||||
test("narrow mobile toolbar keeps map controls tappable without overflow", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 360, height: 844 });
|
||||
await mockMapShell(page);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user