From c8a29d20a8d64d8afd2c7180e205b466404a4fd0 Mon Sep 17 00:00:00 2001 From: Nico Date: Wed, 3 Jun 2026 19:02:24 -0700 Subject: [PATCH] fix: nudge map areas with arrow keys --- .../src/components/maps/LocationMapCanvas.jsx | 27 +++++++++- frontend/tests/location-map-manager.spec.ts | 54 +++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/maps/LocationMapCanvas.jsx b/frontend/src/components/maps/LocationMapCanvas.jsx index 673ca0a..2d06fbe 100644 --- a/frontend/src/components/maps/LocationMapCanvas.jsx +++ b/frontend/src/components/maps/LocationMapCanvas.jsx @@ -6,6 +6,7 @@ import { } from "../../lib/locationMapUtils"; const DRAG_CHANGE_THRESHOLD = 2; +const NUDGE_KEYS = new Set(["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft"]); export default function LocationMapCanvas({ mode, @@ -118,12 +119,34 @@ export default function LocationMapCanvas({ }; const handleObjectKeyDown = (event, object) => { - if (!["Enter", " ", "Spacebar"].includes(event.key)) { + if (["Enter", " ", "Spacebar"].includes(event.key)) { + event.stopPropagation(); + event.preventDefault(); + selectObject(object); return; } + + if (!canEditObjects || object.locked || !NUDGE_KEYS.has(event.key)) return; + event.stopPropagation(); event.preventDefault(); - selectObject(object); + const nudgeAmount = event.shiftKey ? 25 : 10; + const deltaX = event.key === "ArrowLeft" ? -nudgeAmount : event.key === "ArrowRight" ? nudgeAmount : 0; + const deltaY = event.key === "ArrowUp" ? -nudgeAmount : event.key === "ArrowDown" ? nudgeAmount : 0; + + remember(); + setSelectedObjectKey(getObjectKey(object)); + updateObjects((currentObjects) => + currentObjects.map((candidate) => + getObjectKey(candidate) === getObjectKey(object) + ? { + ...candidate, + x: candidate.x + deltaX, + y: candidate.y + deltaY, + } + : candidate + ) + ); }; const handleShowZones = (event) => { diff --git a/frontend/tests/location-map-manager.spec.ts b/frontend/tests/location-map-manager.spec.ts index 0cff626..a4d6879 100644 --- a/frontend/tests/location-map-manager.spec.ts +++ b/frontend/tests/location-map-manager.spec.ts @@ -596,6 +596,60 @@ test("admin selecting an object does not mark a draft dirty until it changes", a await expect(page.getByRole("button", { name: "Undo" })).toBeEnabled(); }); +test("admin nudges selected map areas with arrow keys", async ({ page }) => { + await mockMapShell(page); + + const mapState = draftMapState([ + { + id: 1352, + location_map_id: 900, + 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: "Continue Editing" }).click(); + await page.getByRole("button", { name: "Edit Objects" }).click(); + const bakeryArea = page.getByRole("button", { name: "Map area Bakery" }); + await bakeryArea.focus(); + await expect(bakeryArea).toBeFocused(); + await page.keyboard.press("Enter"); + await expect(page.getByRole("spinbutton", { name: "X" })).toHaveValue("40"); + await expect(page.getByRole("spinbutton", { name: "Y" })).toHaveValue("40"); + await expect(page.locator(".location-map-status")).toHaveText("Draft"); + + await page.keyboard.press("ArrowRight"); + await expect(page.getByRole("spinbutton", { name: "X" })).toHaveValue("50"); + await expect(page.getByRole("spinbutton", { name: "Y" })).toHaveValue("40"); + await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft"); + await expect(page.getByRole("button", { name: "Save Draft" })).toBeEnabled(); + await expect(page.getByRole("button", { name: "Undo" })).toBeEnabled(); + + await page.keyboard.press("Shift+ArrowDown"); + await expect(page.getByRole("spinbutton", { name: "X" })).toHaveValue("50"); + await expect(page.getByRole("spinbutton", { name: "Y" })).toHaveValue("65"); +}); + test("admin object numeric fields expose map bounds and clamp edits", async ({ page }) => { await mockMapShell(page);