fix: fit compact store maps

This commit is contained in:
Nico 2026-06-04 16:41:28 -07:00
parent bf60bd6a3f
commit 0778c1a71c
3 changed files with 76 additions and 29 deletions

View File

@ -1,6 +1,8 @@
import { useCallback, useEffect, useMemo, useRef } from "react";
import { DEFAULT_MAP_SIZE, clampMapZoom } from "../lib/locationMapUtils";
const COMPACT_MAP_LAYOUT_QUERY = "(max-width: 839px)";
function getAutoFitKey(mapState, mapSize) {
return [
mapState?.location?.id || "",
@ -14,8 +16,12 @@ function getAutoFitKey(mapState, mapSize) {
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);
const isCompactLayout = typeof window !== "undefined"
&& window.matchMedia(COMPACT_MAP_LAYOUT_QUERY).matches;
const minimumWidth = isCompactLayout ? 160 : 280;
const minimumHeight = isCompactLayout ? 120 : 220;
const availableWidth = Math.max(minimumWidth, (scrollElement?.clientWidth || fallbackWidth) - 24);
const availableHeight = Math.max(minimumHeight, (scrollElement?.clientHeight || fallbackHeight) - 24);
return Number(clampMapZoom(
Math.min(availableWidth / mapSize.width, availableHeight / mapSize.height)
@ -44,7 +50,7 @@ export default function useMobileMapAutoFit({
useEffect(() => {
if (typeof window === "undefined") return undefined;
if (!mapState || mode === "setup") return undefined;
if (!window.matchMedia("(max-width: 520px)").matches) return undefined;
if (!window.matchMedia(COMPACT_MAP_LAYOUT_QUERY).matches) return undefined;
if (lastAutoFitKeyRef.current === autoFitKey) return undefined;
const frameId = window.requestAnimationFrame(() => {

View File

@ -958,6 +958,17 @@
}
}
@media (max-width: 839px) {
.location-map-bottom-sheet {
max-height: 34dvh;
}
.location-map-svg {
min-width: 0;
min-height: 0;
}
}
@media (max-width: 520px) {
.location-map-canvas-shell {
padding: 0.5rem;

View File

@ -187,6 +187,25 @@ async function mockMapShell(
});
}
async function expectMapFitsCanvas(page: Page, maxZoomPercent: number, context: string) {
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(maxZoomPercent);
const scrollBox = await 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(`${context} fitted map was not measurable`);
expect(svgBox.width).toBeLessThanOrEqual(scrollBox.width + 1);
expect(svgBox.height).toBeLessThanOrEqual(scrollBox.height + 1);
return { scroll };
}
test("admin creates a map from zones, saves a draft, and publishes it", async ({ page }) => {
await mockMapShell(page);
@ -3645,20 +3664,7 @@ test("mobile starts fitted and fit returns panned maps into the canvas", async (
});
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 { scroll } = await expectMapFitsCanvas(page, 45, "Initial mobile");
const zoomIn = page.getByRole("button", { name: "Zoom in" });
await zoomIn.click();
@ -3677,24 +3683,48 @@ test("mobile starts fitted and fit returns panned maps into the canvas", async (
await page.getByRole("button", { name: "Fit" }).click();
await expect.poll(async () => {
const value = await zoomText.textContent();
return Number(value?.replace("%", "") || 0);
}).toBeLessThan(45);
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");
expect(svgBox.width).toBeLessThanOrEqual(scrollBox.width + 1);
expect(svgBox.height).toBeLessThanOrEqual(scrollBox.height + 1);
await expectMapFitsCanvas(page, 45, "Mobile");
await expect.poll(async () => scroll.evaluate((element) => ({
left: element.scrollLeft,
top: element.scrollTop,
}))).toEqual({ left: 0, top: 0 });
});
test("compact tablet map starts fitted on first load", async ({ page }) => {
await page.setViewportSize({ width: 768, height: 720 });
await mockMapShell(page);
const mapState = publishedMapState([
{
id: 1586,
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 expectMapFitsCanvas(page, 75, "Compact tablet");
});
test("zoom controls disable at map zoom limits", async ({ page }) => {
await mockMapShell(page);