fix: wrap long map labels

This commit is contained in:
Nico 2026-06-03 11:35:49 -07:00
parent 7bb76a8eb4
commit 5b964eef1f
2 changed files with 76 additions and 7 deletions

View File

@ -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 ? (
<text x={object.x + 16} y={object.y + 28} className="location-map-label">
{fittedLabel(object, "Area")}
{labelLines.map((line, lineIndex) => (
<tspan key={`${line}-${lineIndex}`} x={object.x + 16} dy={lineIndex === 0 ? 0 : 24}>
{line}
</tspan>
))}
</text>
) : null}
{filters.showCounts ? (
<text x={object.x + 16} y={object.y + 52} className="location-map-count">
<text x={object.x + 16} y={countY} className="location-map-count">
{countLabel}
</text>
) : null}

View File

@ -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);