From 5b964eef1fbbdbb5cf6196f56ba94753dad8d718 Mon Sep 17 00:00:00 2001 From: Nico Date: Wed, 3 Jun 2026 11:35:49 -0700 Subject: [PATCH] fix: wrap long map labels --- .../src/components/maps/LocationMapCanvas.jsx | 43 ++++++++++++++++--- frontend/tests/location-map-manager.spec.ts | 40 +++++++++++++++++ 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/maps/LocationMapCanvas.jsx b/frontend/src/components/maps/LocationMapCanvas.jsx index 05a969c..b509164 100644 --- a/frontend/src/components/maps/LocationMapCanvas.jsx +++ b/frontend/src/components/maps/LocationMapCanvas.jsx @@ -138,11 +138,34 @@ export default function LocationMapCanvas({ event.currentTarget.releasePointerCapture?.(event.pointerId); }; - const fittedLabel = (object, fallback) => { - const rawLabel = object.label || object.zone_name || fallback; - const maxCharacters = Math.max(8, Math.floor((Number(object.width) - 28) / 14)); - if (rawLabel.length <= maxCharacters) return rawLabel; - return `${rawLabel.slice(0, Math.max(5, maxCharacters - 3))}...`; + const truncateLabel = (value, maxCharacters) => { + if (value.length <= maxCharacters) return value; + return `${value.slice(0, Math.max(5, maxCharacters - 3)).trimEnd()}...`; + }; + + const fittedLabelLines = (object, fallback) => { + const rawLabel = String(object.label || object.zone_name || fallback).trim(); + const maxCharacters = Math.max(8, Math.floor((Number(object.width) - 28) / 13)); + const canUseTwoLines = Number(object.height) >= 92 && rawLabel.length > maxCharacters; + if (!canUseTwoLines) return [truncateLabel(rawLabel, maxCharacters)]; + + const words = rawLabel.split(/\s+/); + let firstLine = ""; + let splitIndex = 0; + for (let index = 0; index < words.length; index += 1) { + const candidate = firstLine ? `${firstLine} ${words[index]}` : words[index]; + if (candidate.length > maxCharacters && firstLine) break; + firstLine = candidate; + splitIndex = index + 1; + if (candidate.length >= maxCharacters) break; + } + + const secondLine = words.slice(splitIndex).join(" "); + if (!secondLine) return [truncateLabel(firstLine, maxCharacters)]; + return [ + truncateLabel(firstLine, maxCharacters), + truncateLabel(secondLine, maxCharacters), + ]; }; const renderMapObject = (object, index) => { @@ -152,6 +175,8 @@ export default function LocationMapCanvas({ const zoneItems = itemsForZone(mapItems, object.zone_id, filters, username); const count = zoneItems.length; const countLabel = `${count} item${count === 1 ? "" : "s"}`; + const labelLines = fittedLabelLines(object, "Area"); + const countY = labelLines.length > 1 ? object.y + 76 : object.y + 52; const pinDots = filters.showPins ? zoneItems.slice(0, 12).map((item, itemIndex) => { const column = itemIndex % 4; @@ -192,11 +217,15 @@ export default function LocationMapCanvas({ /> {filters.showLabels ? ( - {fittedLabel(object, "Area")} + {labelLines.map((line, lineIndex) => ( + + {line} + + ))} ) : null} {filters.showCounts ? ( - + {countLabel} ) : null} diff --git a/frontend/tests/location-map-manager.spec.ts b/frontend/tests/location-map-manager.spec.ts index e91dccb..050e0a7 100644 --- a/frontend/tests/location-map-manager.spec.ts +++ b/frontend/tests/location-map-manager.spec.ts @@ -423,6 +423,46 @@ test("admin status follows the visible map when a saved draft exists", async ({ await expect(page.locator(".location-map-object", { hasText: "Live Bakery" })).toBeVisible(); }); +test("viewer wraps long zone labels and keeps item counts visible", async ({ page }) => { + await mockMapShell(page); + + const mapState = publishedMapState([ + { + id: 1501, + location_map_id: 901, + zone_id: 501, + zone_name: "Produce & Fresh Vegetables", + type: "zone", + label: "Produce & Fresh Vegetables", + x: 40, + y: 40, + width: 260, + height: 120, + 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"); + + const produceArea = page.locator('[data-object-key="1501"]'); + await expect(produceArea.locator(".location-map-label tspan")).toHaveCount(2); + await expect(produceArea.locator(".location-map-label tspan").nth(0)).toHaveText("Produce & Fresh"); + await expect(produceArea.locator(".location-map-label tspan").nth(1)).toHaveText("Vegetables"); + await expect(produceArea.locator(".location-map-count")).toHaveText("2 items"); + await expect(page.locator(".location-map-pin")).toHaveCount(0); +}); + test("members can view a published map but cannot edit it", async ({ page }) => { await mockMapShell(page, memberHousehold);