fix: auto-fit mobile store maps
This commit is contained in:
parent
faf96ef929
commit
bf60bd6a3f
@ -1,9 +1,6 @@
|
|||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import {
|
import useMobileMapAutoFit from "./useMobileMapAutoFit";
|
||||||
DEFAULT_MAP_SIZE,
|
import { getObjectKey } from "../lib/locationMapUtils";
|
||||||
clampMapZoom,
|
|
||||||
getObjectKey,
|
|
||||||
} from "../lib/locationMapUtils";
|
|
||||||
|
|
||||||
export default function useLocationMapViewControls({
|
export default function useLocationMapViewControls({
|
||||||
editorTool,
|
editorTool,
|
||||||
@ -48,6 +45,14 @@ export default function useLocationMapViewControls({
|
|||||||
}
|
}
|
||||||
}, [filters.showZones, objects, selectedObjectKey, setSelectedObjectKey]);
|
}, [filters.showZones, objects, selectedObjectKey, setSelectedObjectKey]);
|
||||||
|
|
||||||
|
const fitMapToCanvas = useMobileMapAutoFit({
|
||||||
|
mapSize,
|
||||||
|
mapState,
|
||||||
|
mode,
|
||||||
|
setZoom,
|
||||||
|
svgRef,
|
||||||
|
});
|
||||||
|
|
||||||
const handleUndo = useCallback(() => {
|
const handleUndo = useCallback(() => {
|
||||||
setHistory((previous) => {
|
setHistory((previous) => {
|
||||||
if (previous.length === 0) return previous;
|
if (previous.length === 0) return previous;
|
||||||
@ -121,22 +126,9 @@ export default function useLocationMapViewControls({
|
|||||||
syncMapDraftFromState,
|
syncMapDraftFromState,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleFitMap = useCallback(() => {
|
|
||||||
const scrollElement = svgRef.current?.closest(".location-map-scroll");
|
|
||||||
const fallbackWidth = typeof window === "undefined" ? DEFAULT_MAP_SIZE.width : window.innerWidth;
|
|
||||||
const fallbackHeight = typeof window === "undefined" ? DEFAULT_MAP_SIZE.height : window.innerHeight;
|
|
||||||
const availableWidth = Math.max(280, (scrollElement?.clientWidth || fallbackWidth) - 24);
|
|
||||||
const availableHeight = Math.max(220, (scrollElement?.clientHeight || fallbackHeight) - 24);
|
|
||||||
const nextZoom = clampMapZoom(
|
|
||||||
Math.min(availableWidth / mapSize.width, availableHeight / mapSize.height)
|
|
||||||
);
|
|
||||||
setZoom(Number(nextZoom.toFixed(2)));
|
|
||||||
scrollElement?.scrollTo({ left: 0, top: 0 });
|
|
||||||
}, [mapSize.height, mapSize.width, setZoom, svgRef]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleEditMode,
|
handleEditMode,
|
||||||
handleFitMap,
|
handleFitMap: fitMapToCanvas,
|
||||||
handlePreviewDraft,
|
handlePreviewDraft,
|
||||||
handleRedo,
|
handleRedo,
|
||||||
handleUndo,
|
handleUndo,
|
||||||
|
|||||||
60
frontend/src/hooks/useMobileMapAutoFit.js
Normal file
60
frontend/src/hooks/useMobileMapAutoFit.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
|
import { DEFAULT_MAP_SIZE, clampMapZoom } from "../lib/locationMapUtils";
|
||||||
|
|
||||||
|
function getAutoFitKey(mapState, mapSize) {
|
||||||
|
return [
|
||||||
|
mapState?.location?.id || "",
|
||||||
|
mapState?.draft_map?.id || "",
|
||||||
|
mapState?.published_map?.id || "",
|
||||||
|
mapSize.width,
|
||||||
|
mapSize.height,
|
||||||
|
].join(":");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFitZoom(mapSize, scrollElement) {
|
||||||
|
const fallbackWidth = typeof window === "undefined" ? DEFAULT_MAP_SIZE.width : window.innerWidth;
|
||||||
|
const fallbackHeight = typeof window === "undefined" ? DEFAULT_MAP_SIZE.height : window.innerHeight;
|
||||||
|
const availableWidth = Math.max(280, (scrollElement?.clientWidth || fallbackWidth) - 24);
|
||||||
|
const availableHeight = Math.max(220, (scrollElement?.clientHeight || fallbackHeight) - 24);
|
||||||
|
|
||||||
|
return Number(clampMapZoom(
|
||||||
|
Math.min(availableWidth / mapSize.width, availableHeight / mapSize.height)
|
||||||
|
).toFixed(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useMobileMapAutoFit({
|
||||||
|
mapSize,
|
||||||
|
mapState,
|
||||||
|
mode,
|
||||||
|
setZoom,
|
||||||
|
svgRef,
|
||||||
|
}) {
|
||||||
|
const lastAutoFitKeyRef = useRef(null);
|
||||||
|
const autoFitKey = useMemo(
|
||||||
|
() => getAutoFitKey(mapState, mapSize),
|
||||||
|
[mapSize, mapState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fitMapToCanvas = useCallback(() => {
|
||||||
|
const scrollElement = svgRef.current?.closest(".location-map-scroll");
|
||||||
|
setZoom(getFitZoom(mapSize, scrollElement));
|
||||||
|
scrollElement?.scrollTo({ left: 0, top: 0 });
|
||||||
|
}, [mapSize, setZoom, svgRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined") return undefined;
|
||||||
|
if (!mapState || mode === "setup") return undefined;
|
||||||
|
if (!window.matchMedia("(max-width: 520px)").matches) return undefined;
|
||||||
|
if (lastAutoFitKeyRef.current === autoFitKey) return undefined;
|
||||||
|
|
||||||
|
const frameId = window.requestAnimationFrame(() => {
|
||||||
|
if (!svgRef.current) return;
|
||||||
|
lastAutoFitKeyRef.current = autoFitKey;
|
||||||
|
fitMapToCanvas();
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => window.cancelAnimationFrame(frameId);
|
||||||
|
}, [autoFitKey, fitMapToCanvas, mapState, mode, svgRef]);
|
||||||
|
|
||||||
|
return fitMapToCanvas;
|
||||||
|
}
|
||||||
@ -3613,7 +3613,7 @@ test("viewer map areas are keyboard selectable", async ({ page }) => {
|
|||||||
await expect(page.getByLabel("Map summary")).toBeVisible();
|
await expect(page.getByLabel("Map summary")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("mobile fit zoom fits the map into the visible canvas", async ({ page }) => {
|
test("mobile starts fitted and fit returns panned maps into the canvas", async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 390, height: 844 });
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
await mockMapShell(page);
|
await mockMapShell(page);
|
||||||
|
|
||||||
@ -3646,6 +3646,24 @@ test("mobile fit zoom fits the map into the visible canvas", async ({ page }) =>
|
|||||||
|
|
||||||
await page.goto("/stores/100/locations/10/map");
|
await page.goto("/stores/100/locations/10/map");
|
||||||
const scroll = page.locator(".location-map-scroll");
|
const scroll = page.locator(".location-map-scroll");
|
||||||
|
const zoomText = page.locator(".location-map-zoom-controls span");
|
||||||
|
await expect.poll(async () => {
|
||||||
|
const value = await zoomText.textContent();
|
||||||
|
return Number(value?.replace("%", "") || 0);
|
||||||
|
}).toBeLessThan(45);
|
||||||
|
|
||||||
|
let scrollBox = await page.locator(".location-map-scroll").boundingBox();
|
||||||
|
let svgBox = await page.locator(".location-map-svg").boundingBox();
|
||||||
|
expect(scrollBox).not.toBeNull();
|
||||||
|
expect(svgBox).not.toBeNull();
|
||||||
|
if (!scrollBox || !svgBox) throw new Error("Initial mobile fitted map was not measurable");
|
||||||
|
expect(svgBox.width).toBeLessThanOrEqual(scrollBox.width + 1);
|
||||||
|
expect(svgBox.height).toBeLessThanOrEqual(scrollBox.height + 1);
|
||||||
|
|
||||||
|
const zoomIn = page.getByRole("button", { name: "Zoom in" });
|
||||||
|
await zoomIn.click();
|
||||||
|
await zoomIn.click();
|
||||||
|
await zoomIn.click();
|
||||||
await scroll.evaluate((element) => {
|
await scroll.evaluate((element) => {
|
||||||
element.scrollLeft = 320;
|
element.scrollLeft = 320;
|
||||||
element.scrollTop = 240;
|
element.scrollTop = 240;
|
||||||
@ -3659,14 +3677,13 @@ test("mobile fit zoom fits the map into the visible canvas", async ({ page }) =>
|
|||||||
|
|
||||||
await page.getByRole("button", { name: "Fit" }).click();
|
await page.getByRole("button", { name: "Fit" }).click();
|
||||||
|
|
||||||
const zoomText = page.locator(".location-map-zoom-controls span");
|
|
||||||
await expect.poll(async () => {
|
await expect.poll(async () => {
|
||||||
const value = await zoomText.textContent();
|
const value = await zoomText.textContent();
|
||||||
return Number(value?.replace("%", "") || 0);
|
return Number(value?.replace("%", "") || 0);
|
||||||
}).toBeLessThan(45);
|
}).toBeLessThan(45);
|
||||||
|
|
||||||
const scrollBox = await page.locator(".location-map-scroll").boundingBox();
|
scrollBox = await page.locator(".location-map-scroll").boundingBox();
|
||||||
const svgBox = await page.locator(".location-map-svg").boundingBox();
|
svgBox = await page.locator(".location-map-svg").boundingBox();
|
||||||
expect(scrollBox).not.toBeNull();
|
expect(scrollBox).not.toBeNull();
|
||||||
expect(svgBox).not.toBeNull();
|
expect(svgBox).not.toBeNull();
|
||||||
if (!scrollBox || !svgBox) throw new Error("Mobile fitted map was not measurable");
|
if (!scrollBox || !svgBox) throw new Error("Mobile fitted map was not measurable");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user