fix: fit maps on mobile viewports

This commit is contained in:
Nico 2026-06-03 18:02:55 -07:00
parent bff830dd4b
commit 381a21015c
4 changed files with 69 additions and 8 deletions

View File

@ -1,3 +1,5 @@
import { clampMapZoom } from "../../lib/locationMapUtils";
export default function LocationMapToolbar({
mode,
editorTool,
@ -66,7 +68,7 @@ export default function LocationMapToolbar({
<div className="location-map-zoom-controls" aria-label="Zoom controls">
<button
type="button"
onClick={() => setZoom((value) => Math.max(0.45, value - 0.15))}
onClick={() => setZoom((value) => Number(clampMapZoom(value - 0.15).toFixed(2)))}
disabled={!hasAnyMap}
aria-label="Zoom out"
>
@ -75,7 +77,7 @@ export default function LocationMapToolbar({
<span>{Math.round(zoom * 100)}%</span>
<button
type="button"
onClick={() => setZoom((value) => Math.min(1.6, value + 0.15))}
onClick={() => setZoom((value) => Number(clampMapZoom(value + 0.15).toFixed(2)))}
disabled={!hasAnyMap}
aria-label="Zoom in"
>

View File

@ -5,6 +5,9 @@ export const DEFAULT_MAP_SIZE = {
height: 700,
};
export const MIN_MAP_ZOOM = 0.3;
export const MAX_MAP_ZOOM = 1.6;
export const DEFAULT_MAP_FILTERS = {
showZones: true,
showLabels: true,
@ -178,6 +181,10 @@ export function clientPointToMap(event, svgElement, mapSize) {
return { x, y };
}
export function clampMapZoom(value) {
return Math.max(MIN_MAP_ZOOM, Math.min(MAX_MAP_ZOOM, value));
}
export function clampObjectToMap(object, mapSize) {
const width = Math.max(40, Math.min(object.width, mapSize.width));
const height = Math.max(40, Math.min(object.height, mapSize.height));

View File

@ -21,6 +21,7 @@ import getApiErrorMessage from "../lib/getApiErrorMessage";
import {
DEFAULT_MAP_FILTERS,
DEFAULT_MAP_SIZE,
clampMapZoom,
clampObjectToMap,
createClientObject,
getMapStatus,
@ -427,12 +428,14 @@ export default function LocationMapManager() {
};
const handleFitMap = () => {
const availableWidth = typeof window === "undefined"
? DEFAULT_MAP_SIZE.width
: window.innerWidth >= 840
? window.innerWidth - 420
: window.innerWidth - 24;
const nextZoom = Math.max(0.45, Math.min(1.6, (availableWidth - 24) / mapSize.width));
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)));
};

View File

@ -1082,6 +1082,55 @@ test("viewer handles empty and many-item zone panels without default pins", asyn
await expect(page.locator(".location-map-pin")).toHaveCount(0);
});
test("mobile fit zoom fits the map into the visible canvas", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await mockMapShell(page);
const mapState = publishedMapState([
{
id: 1581,
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 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();
expect(scrollBox).not.toBeNull();
expect(svgBox).not.toBeNull();
if (!scrollBox || !svgBox) throw new Error("Mobile fitted map was not measurable");
expect(svgBox.width).toBeLessThanOrEqual(scrollBox.width + 1);
expect(svgBox.height).toBeLessThanOrEqual(scrollBox.height + 1);
});
test("mobile editor uses bottom sheet controls instead of desktop object toolbar", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await mockMapShell(page);