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,
|
setZoom,
|
||||||
svgRef,
|
svgRef,
|
||||||
syncMapDraftFromState,
|
syncMapDraftFromState,
|
||||||
|
zoom,
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedObjectKey) return;
|
if (!selectedObjectKey) return;
|
||||||
@ -38,6 +39,7 @@ export default function useLocationMapViewControls({
|
|||||||
mode,
|
mode,
|
||||||
setZoom,
|
setZoom,
|
||||||
svgRef,
|
svgRef,
|
||||||
|
zoom,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleUndo = useCallback(() => {
|
const handleUndo = useCallback(() => {
|
||||||
|
|||||||
@ -34,16 +34,25 @@ export default function useMobileMapAutoFit({
|
|||||||
mode,
|
mode,
|
||||||
setZoom,
|
setZoom,
|
||||||
svgRef,
|
svgRef,
|
||||||
|
zoom,
|
||||||
}) {
|
}) {
|
||||||
const lastAutoFitKeyRef = useRef(null);
|
const lastAutoFitKeyRef = useRef(null);
|
||||||
|
const lastFittedZoomRef = useRef(null);
|
||||||
|
const latestZoomRef = useRef(zoom);
|
||||||
const autoFitKey = useMemo(
|
const autoFitKey = useMemo(
|
||||||
() => getAutoFitKey(mapState, mapSize),
|
() => getAutoFitKey(mapState, mapSize),
|
||||||
[mapSize, mapState]
|
[mapSize, mapState]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
latestZoomRef.current = zoom;
|
||||||
|
}, [zoom]);
|
||||||
|
|
||||||
const fitMapToCanvas = useCallback(() => {
|
const fitMapToCanvas = useCallback(() => {
|
||||||
const scrollElement = svgRef.current?.closest(".location-map-scroll");
|
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 });
|
scrollElement?.scrollTo({ left: 0, top: 0 });
|
||||||
}, [mapSize, setZoom, svgRef]);
|
}, [mapSize, setZoom, svgRef]);
|
||||||
|
|
||||||
@ -62,5 +71,41 @@ export default function useMobileMapAutoFit({
|
|||||||
return () => window.cancelAnimationFrame(frameId);
|
return () => window.cancelAnimationFrame(frameId);
|
||||||
}, [autoFitKey, fitMapToCanvas, mapState, mode, svgRef]);
|
}, [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;
|
return fitMapToCanvas;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -203,6 +203,7 @@ export default function LocationMapManager() {
|
|||||||
setZoom,
|
setZoom,
|
||||||
svgRef,
|
svgRef,
|
||||||
syncMapDraftFromState,
|
syncMapDraftFromState,
|
||||||
|
zoom,
|
||||||
});
|
});
|
||||||
|
|
||||||
const performBackNavigation = useCallback(() => {
|
const performBackNavigation = useCallback(() => {
|
||||||
|
|||||||
@ -206,6 +206,11 @@ async function expectMapFitsCanvas(page: Page, maxZoomPercent: number, context:
|
|||||||
return { scroll };
|
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 }) => {
|
test("admin creates a map from zones, saves a draft, and publishes it", async ({ page }) => {
|
||||||
await mockMapShell(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);
|
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 }) => {
|
test("narrow mobile toolbar keeps map controls tappable without overflow", async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 360, height: 844 });
|
await page.setViewportSize({ width: 360, height: 844 });
|
||||||
await mockMapShell(page);
|
await mockMapShell(page);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user