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 {
|
||||
DEFAULT_MAP_SIZE,
|
||||
clampMapZoom,
|
||||
getObjectKey,
|
||||
} from "../lib/locationMapUtils";
|
||||
import useMobileMapAutoFit from "./useMobileMapAutoFit";
|
||||
import { getObjectKey } from "../lib/locationMapUtils";
|
||||
|
||||
export default function useLocationMapViewControls({
|
||||
editorTool,
|
||||
@ -48,6 +45,14 @@ export default function useLocationMapViewControls({
|
||||
}
|
||||
}, [filters.showZones, objects, selectedObjectKey, setSelectedObjectKey]);
|
||||
|
||||
const fitMapToCanvas = useMobileMapAutoFit({
|
||||
mapSize,
|
||||
mapState,
|
||||
mode,
|
||||
setZoom,
|
||||
svgRef,
|
||||
});
|
||||
|
||||
const handleUndo = useCallback(() => {
|
||||
setHistory((previous) => {
|
||||
if (previous.length === 0) return previous;
|
||||
@ -121,22 +126,9 @@ export default function useLocationMapViewControls({
|
||||
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 {
|
||||
handleEditMode,
|
||||
handleFitMap,
|
||||
handleFitMap: fitMapToCanvas,
|
||||
handlePreviewDraft,
|
||||
handleRedo,
|
||||
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();
|
||||
});
|
||||
|
||||
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 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");
|
||||
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) => {
|
||||
element.scrollLeft = 320;
|
||||
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();
|
||||
|
||||
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);
|
||||
|
||||
const scrollBox = await page.locator(".location-map-scroll").boundingBox();
|
||||
const svgBox = await page.locator(".location-map-svg").boundingBox();
|
||||
scrollBox = await page.locator(".location-map-scroll").boundingBox();
|
||||
svgBox = await page.locator(".location-map-svg").boundingBox();
|
||||
expect(scrollBox).not.toBeNull();
|
||||
expect(svgBox).not.toBeNull();
|
||||
if (!scrollBox || !svgBox) throw new Error("Mobile fitted map was not measurable");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user