fix: fit maps on mobile viewports
This commit is contained in:
parent
bff830dd4b
commit
381a21015c
@ -1,3 +1,5 @@
|
|||||||
|
import { clampMapZoom } from "../../lib/locationMapUtils";
|
||||||
|
|
||||||
export default function LocationMapToolbar({
|
export default function LocationMapToolbar({
|
||||||
mode,
|
mode,
|
||||||
editorTool,
|
editorTool,
|
||||||
@ -66,7 +68,7 @@ export default function LocationMapToolbar({
|
|||||||
<div className="location-map-zoom-controls" aria-label="Zoom controls">
|
<div className="location-map-zoom-controls" aria-label="Zoom controls">
|
||||||
<button
|
<button
|
||||||
type="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}
|
disabled={!hasAnyMap}
|
||||||
aria-label="Zoom out"
|
aria-label="Zoom out"
|
||||||
>
|
>
|
||||||
@ -75,7 +77,7 @@ export default function LocationMapToolbar({
|
|||||||
<span>{Math.round(zoom * 100)}%</span>
|
<span>{Math.round(zoom * 100)}%</span>
|
||||||
<button
|
<button
|
||||||
type="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}
|
disabled={!hasAnyMap}
|
||||||
aria-label="Zoom in"
|
aria-label="Zoom in"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -5,6 +5,9 @@ export const DEFAULT_MAP_SIZE = {
|
|||||||
height: 700,
|
height: 700,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MIN_MAP_ZOOM = 0.3;
|
||||||
|
export const MAX_MAP_ZOOM = 1.6;
|
||||||
|
|
||||||
export const DEFAULT_MAP_FILTERS = {
|
export const DEFAULT_MAP_FILTERS = {
|
||||||
showZones: true,
|
showZones: true,
|
||||||
showLabels: true,
|
showLabels: true,
|
||||||
@ -178,6 +181,10 @@ export function clientPointToMap(event, svgElement, mapSize) {
|
|||||||
return { x, y };
|
return { x, y };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clampMapZoom(value) {
|
||||||
|
return Math.max(MIN_MAP_ZOOM, Math.min(MAX_MAP_ZOOM, value));
|
||||||
|
}
|
||||||
|
|
||||||
export function clampObjectToMap(object, mapSize) {
|
export function clampObjectToMap(object, mapSize) {
|
||||||
const width = Math.max(40, Math.min(object.width, mapSize.width));
|
const width = Math.max(40, Math.min(object.width, mapSize.width));
|
||||||
const height = Math.max(40, Math.min(object.height, mapSize.height));
|
const height = Math.max(40, Math.min(object.height, mapSize.height));
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import getApiErrorMessage from "../lib/getApiErrorMessage";
|
|||||||
import {
|
import {
|
||||||
DEFAULT_MAP_FILTERS,
|
DEFAULT_MAP_FILTERS,
|
||||||
DEFAULT_MAP_SIZE,
|
DEFAULT_MAP_SIZE,
|
||||||
|
clampMapZoom,
|
||||||
clampObjectToMap,
|
clampObjectToMap,
|
||||||
createClientObject,
|
createClientObject,
|
||||||
getMapStatus,
|
getMapStatus,
|
||||||
@ -427,12 +428,14 @@ export default function LocationMapManager() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFitMap = () => {
|
const handleFitMap = () => {
|
||||||
const availableWidth = typeof window === "undefined"
|
const scrollElement = svgRef.current?.closest(".location-map-scroll");
|
||||||
? DEFAULT_MAP_SIZE.width
|
const fallbackWidth = typeof window === "undefined" ? DEFAULT_MAP_SIZE.width : window.innerWidth;
|
||||||
: window.innerWidth >= 840
|
const fallbackHeight = typeof window === "undefined" ? DEFAULT_MAP_SIZE.height : window.innerHeight;
|
||||||
? window.innerWidth - 420
|
const availableWidth = Math.max(280, (scrollElement?.clientWidth || fallbackWidth) - 24);
|
||||||
: window.innerWidth - 24;
|
const availableHeight = Math.max(220, (scrollElement?.clientHeight || fallbackHeight) - 24);
|
||||||
const nextZoom = Math.max(0.45, Math.min(1.6, (availableWidth - 24) / mapSize.width));
|
const nextZoom = clampMapZoom(
|
||||||
|
Math.min(availableWidth / mapSize.width, availableHeight / mapSize.height)
|
||||||
|
);
|
||||||
setZoom(Number(nextZoom.toFixed(2)));
|
setZoom(Number(nextZoom.toFixed(2)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
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 }) => {
|
test("mobile editor uses bottom sheet controls instead of desktop object toolbar", async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 390, height: 844 });
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
await mockMapShell(page);
|
await mockMapShell(page);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user