@elabs-ai/components-maps 4.2.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/chunk-2FPB53NG.js +36 -0
- package/dist/chunk-2FPB53NG.js.map +1 -0
- package/dist/chunk-2VVJRT66.js +37 -0
- package/dist/chunk-2VVJRT66.js.map +1 -0
- package/dist/chunk-3HXFONRP.js +2 -0
- package/dist/chunk-3HXFONRP.js.map +1 -0
- package/dist/chunk-3SJN3IYT.js +2 -0
- package/dist/chunk-3SJN3IYT.js.map +1 -0
- package/dist/chunk-4DANBD5B.js +55 -0
- package/dist/chunk-4DANBD5B.js.map +1 -0
- package/dist/chunk-5PDVG53B.js +2 -0
- package/dist/chunk-5PDVG53B.js.map +1 -0
- package/dist/chunk-643R7IJC.js +2 -0
- package/dist/chunk-643R7IJC.js.map +1 -0
- package/dist/chunk-B3ZDBQ6I.js +164 -0
- package/dist/chunk-B3ZDBQ6I.js.map +1 -0
- package/dist/chunk-BGCE3RR5.js +35 -0
- package/dist/chunk-BGCE3RR5.js.map +1 -0
- package/dist/chunk-C4VI37TD.js +485 -0
- package/dist/chunk-C4VI37TD.js.map +1 -0
- package/dist/chunk-CJJT6AEI.js +179 -0
- package/dist/chunk-CJJT6AEI.js.map +1 -0
- package/dist/chunk-D2RCPTRO.js +2 -0
- package/dist/chunk-D2RCPTRO.js.map +1 -0
- package/dist/chunk-DTV7JZVN.js +2 -0
- package/dist/chunk-DTV7JZVN.js.map +1 -0
- package/dist/chunk-E65Z3MSV.js +80 -0
- package/dist/chunk-E65Z3MSV.js.map +1 -0
- package/dist/chunk-E7VPNINZ.js +70 -0
- package/dist/chunk-E7VPNINZ.js.map +1 -0
- package/dist/chunk-FHV5IA6N.js +100 -0
- package/dist/chunk-FHV5IA6N.js.map +1 -0
- package/dist/chunk-FQ6V7BFZ.js +347 -0
- package/dist/chunk-FQ6V7BFZ.js.map +1 -0
- package/dist/chunk-G7JDXDJN.js +19 -0
- package/dist/chunk-G7JDXDJN.js.map +1 -0
- package/dist/chunk-GWULZOMW.js +21 -0
- package/dist/chunk-GWULZOMW.js.map +1 -0
- package/dist/chunk-GYDTTAVD.js +120 -0
- package/dist/chunk-GYDTTAVD.js.map +1 -0
- package/dist/chunk-HCOBWIM4.js +48 -0
- package/dist/chunk-HCOBWIM4.js.map +1 -0
- package/dist/chunk-IAHK5MVT.js +2 -0
- package/dist/chunk-IAHK5MVT.js.map +1 -0
- package/dist/chunk-JGJ6442I.js +235 -0
- package/dist/chunk-JGJ6442I.js.map +1 -0
- package/dist/chunk-JYFERXZT.js +18 -0
- package/dist/chunk-JYFERXZT.js.map +1 -0
- package/dist/chunk-LU2UEKFM.js +53 -0
- package/dist/chunk-LU2UEKFM.js.map +1 -0
- package/dist/chunk-LVYM6C5C.js +2 -0
- package/dist/chunk-LVYM6C5C.js.map +1 -0
- package/dist/chunk-MIMDP3EU.js +104 -0
- package/dist/chunk-MIMDP3EU.js.map +1 -0
- package/dist/chunk-NNBURJHY.js +2 -0
- package/dist/chunk-NNBURJHY.js.map +1 -0
- package/dist/chunk-NPUC3V37.js +2 -0
- package/dist/chunk-NPUC3V37.js.map +1 -0
- package/dist/chunk-OEVVNPN3.js +201 -0
- package/dist/chunk-OEVVNPN3.js.map +1 -0
- package/dist/chunk-PF3R2BXM.js +200 -0
- package/dist/chunk-PF3R2BXM.js.map +1 -0
- package/dist/chunk-Q2I5YODW.js +2 -0
- package/dist/chunk-Q2I5YODW.js.map +1 -0
- package/dist/chunk-QDWWP3N7.js +247 -0
- package/dist/chunk-QDWWP3N7.js.map +1 -0
- package/dist/chunk-QU33KPMP.js +2 -0
- package/dist/chunk-QU33KPMP.js.map +1 -0
- package/dist/chunk-RK4BUMHS.js +2 -0
- package/dist/chunk-RK4BUMHS.js.map +1 -0
- package/dist/chunk-RNSLG64F.js +26 -0
- package/dist/chunk-RNSLG64F.js.map +1 -0
- package/dist/chunk-RV673TB4.js +68 -0
- package/dist/chunk-RV673TB4.js.map +1 -0
- package/dist/chunk-S7N2WMVT.js +180 -0
- package/dist/chunk-S7N2WMVT.js.map +1 -0
- package/dist/chunk-T3Z6TCDD.js +2 -0
- package/dist/chunk-T3Z6TCDD.js.map +1 -0
- package/dist/chunk-T4TJTVVA.js +105 -0
- package/dist/chunk-T4TJTVVA.js.map +1 -0
- package/dist/chunk-UMOYRCDX.js +103 -0
- package/dist/chunk-UMOYRCDX.js.map +1 -0
- package/dist/chunk-V2REM6UT.js +185 -0
- package/dist/chunk-V2REM6UT.js.map +1 -0
- package/dist/chunk-VFBRFGID.js +89 -0
- package/dist/chunk-VFBRFGID.js.map +1 -0
- package/dist/chunk-W76NLFER.js +103 -0
- package/dist/chunk-W76NLFER.js.map +1 -0
- package/dist/chunk-WNU4IOGL.js +116 -0
- package/dist/chunk-WNU4IOGL.js.map +1 -0
- package/dist/chunk-WS4Z7ENS.js +494 -0
- package/dist/chunk-WS4Z7ENS.js.map +1 -0
- package/dist/chunk-XQT7YLJY.js +497 -0
- package/dist/chunk-XQT7YLJY.js.map +1 -0
- package/dist/chunk-Y44MEKJV.js +21 -0
- package/dist/chunk-Y44MEKJV.js.map +1 -0
- package/dist/chunk-Z4EPSOB4.js +53 -0
- package/dist/chunk-Z4EPSOB4.js.map +1 -0
- package/dist/chunk-Z5OPDBDC.js +2 -0
- package/dist/chunk-Z5OPDBDC.js.map +1 -0
- package/dist/index.d.ts +1241 -46
- package/dist/index.js +189 -1471
- package/dist/index.js.map +1 -1
- package/dist/lib/arc-math.js +8 -0
- package/dist/lib/arc-math.js.map +1 -0
- package/dist/lib/map-position.js +11 -0
- package/dist/lib/map-position.js.map +1 -0
- package/dist/lib/merge-hover-paint.js +10 -0
- package/dist/lib/merge-hover-paint.js.map +1 -0
- package/dist/lib/plan-crs.js +14 -0
- package/dist/lib/plan-crs.js.map +1 -0
- package/dist/lib/plan-patterns.js +14 -0
- package/dist/lib/plan-patterns.js.map +1 -0
- package/dist/lib/plan-regions.js +18 -0
- package/dist/lib/plan-regions.js.map +1 -0
- package/dist/lib/plan-status.js +14 -0
- package/dist/lib/plan-status.js.map +1 -0
- package/dist/lib/route-arrow.js +12 -0
- package/dist/lib/route-arrow.js.map +1 -0
- package/dist/lib/use-map-breakpoint.js +33 -0
- package/dist/lib/use-map-breakpoint.js.map +1 -0
- package/dist/lib/use-plan-projection.js +9 -0
- package/dist/lib/use-plan-projection.js.map +1 -0
- package/dist/lib/use-token-color.js +10 -0
- package/dist/lib/use-token-color.js.map +1 -0
- package/dist/lib/warn-once.js +10 -0
- package/dist/lib/warn-once.js.map +1 -0
- package/dist/map-annotation/anchor.js +14 -0
- package/dist/map-annotation/anchor.js.map +1 -0
- package/dist/map-annotation/index.js +21 -0
- package/dist/map-annotation/index.js.map +1 -0
- package/dist/map-annotation/map-annotation.js +18 -0
- package/dist/map-annotation/map-annotation.js.map +1 -0
- package/dist/map-arc/index.js +16 -0
- package/dist/map-arc/index.js.map +1 -0
- package/dist/map-arc/map-arc.js +13 -0
- package/dist/map-arc/map-arc.js.map +1 -0
- package/dist/map-canvas/index.css +11 -0
- package/dist/map-canvas/index.css.map +1 -0
- package/dist/map-canvas/index.js +20 -0
- package/dist/map-canvas/index.js.map +1 -0
- package/dist/map-canvas/map-canvas.css +11 -0
- package/dist/map-canvas/map-canvas.css.map +1 -0
- package/dist/map-canvas/map-canvas.js +17 -0
- package/dist/map-canvas/map-canvas.js.map +1 -0
- package/dist/map-canvas/map-context.js +11 -0
- package/dist/map-canvas/map-context.js.map +1 -0
- package/dist/map-canvas/use-resolved-basemap-theme.js +9 -0
- package/dist/map-canvas/use-resolved-basemap-theme.js.map +1 -0
- package/dist/map-cluster-layer/index.js +12 -0
- package/dist/map-cluster-layer/index.js.map +1 -0
- package/dist/map-cluster-layer/map-cluster-layer.js +12 -0
- package/dist/map-cluster-layer/map-cluster-layer.js.map +1 -0
- package/dist/map-controls/index.js +11 -0
- package/dist/map-controls/index.js.map +1 -0
- package/dist/map-controls/map-controls.js +11 -0
- package/dist/map-controls/map-controls.js.map +1 -0
- package/dist/map-geojson/index.js +13 -0
- package/dist/map-geojson/index.js.map +1 -0
- package/dist/map-geojson/map-geojson.js +13 -0
- package/dist/map-geojson/map-geojson.js.map +1 -0
- package/dist/map-inset/index.css +11 -0
- package/dist/map-inset/index.css.map +1 -0
- package/dist/map-inset/index.js +20 -0
- package/dist/map-inset/index.js.map +1 -0
- package/dist/map-inset/map-inset.css +11 -0
- package/dist/map-inset/map-inset.css.map +1 -0
- package/dist/map-inset/map-inset.js +20 -0
- package/dist/map-inset/map-inset.js.map +1 -0
- package/dist/map-legend/index.js +14 -0
- package/dist/map-legend/index.js.map +1 -0
- package/dist/map-legend/map-corner.js +10 -0
- package/dist/map-legend/map-corner.js.map +1 -0
- package/dist/map-legend/map-legend.js +11 -0
- package/dist/map-legend/map-legend.js.map +1 -0
- package/dist/map-marker/index.js +22 -0
- package/dist/map-marker/index.js.map +1 -0
- package/dist/map-marker/map-marker.js +22 -0
- package/dist/map-marker/map-marker.js.map +1 -0
- package/dist/map-north-arrow/index.js +11 -0
- package/dist/map-north-arrow/index.js.map +1 -0
- package/dist/map-north-arrow/map-north-arrow.js +11 -0
- package/dist/map-north-arrow/map-north-arrow.js.map +1 -0
- package/dist/map-plan/index.js +30 -0
- package/dist/map-plan/index.js.map +1 -0
- package/dist/map-plan/map-plan-legend.js +10 -0
- package/dist/map-plan/map-plan-legend.js.map +1 -0
- package/dist/map-plan/map-plan-overlay.js +13 -0
- package/dist/map-plan/map-plan-overlay.js.map +1 -0
- package/dist/map-plan/map-plan-status.js +9 -0
- package/dist/map-plan/map-plan-status.js.map +1 -0
- package/dist/map-plan/map-plan-table.js +10 -0
- package/dist/map-plan/map-plan-table.js.map +1 -0
- package/dist/map-plan/use-plan-selection.js +9 -0
- package/dist/map-plan/use-plan-selection.js.map +1 -0
- package/dist/map-plan-image/index.js +11 -0
- package/dist/map-plan-image/index.js.map +1 -0
- package/dist/map-plan-image/map-plan-image.js +11 -0
- package/dist/map-plan-image/map-plan-image.js.map +1 -0
- package/dist/map-popup/index.js +12 -0
- package/dist/map-popup/index.js.map +1 -0
- package/dist/map-popup/map-popup.js +12 -0
- package/dist/map-popup/map-popup.js.map +1 -0
- package/dist/map-route/index.js +12 -0
- package/dist/map-route/index.js.map +1 -0
- package/dist/map-route/map-route.js +12 -0
- package/dist/map-route/map-route.js.map +1 -0
- package/dist/map-scale-bar/index.js +18 -0
- package/dist/map-scale-bar/index.js.map +1 -0
- package/dist/map-scale-bar/map-scale-bar.js +18 -0
- package/dist/map-scale-bar/map-scale-bar.js.map +1 -0
- package/package.json +5 -5
- package/src/index.ts +83 -0
- package/src/lib/arc-math.test.ts +19 -0
- package/src/lib/arc-math.ts +6 -1
- package/src/lib/map-position.ts +77 -0
- package/src/lib/merge-hover-paint.ts +44 -13
- package/src/lib/plan-crs.parity.test.ts +39 -0
- package/src/lib/plan-crs.test.ts +248 -0
- package/src/lib/plan-crs.ts +252 -0
- package/src/lib/plan-patterns.test.ts +255 -0
- package/src/lib/plan-patterns.ts +160 -0
- package/src/lib/plan-regions.test.ts +186 -0
- package/src/lib/plan-regions.ts +209 -0
- package/src/lib/plan-status.test.ts +87 -0
- package/src/lib/plan-status.ts +147 -0
- package/src/lib/route-arrow.test.ts +88 -0
- package/src/lib/route-arrow.ts +59 -0
- package/src/lib/use-map-breakpoint.test.ts +83 -0
- package/src/lib/use-map-breakpoint.ts +238 -0
- package/src/lib/use-plan-projection.ts +157 -0
- package/src/lib/warn-once.ts +21 -0
- package/src/map-annotation/anchor.ts +92 -0
- package/src/map-annotation/index.ts +15 -0
- package/src/map-annotation/map-annotation.stories.tsx +113 -0
- package/src/map-annotation/map-annotation.test.tsx +106 -0
- package/src/map-annotation/map-annotation.tsx +356 -0
- package/src/map-arc/map-arc.tsx +26 -5
- package/src/map-canvas/map-canvas.stories.tsx +211 -0
- package/src/map-canvas/map-canvas.test.tsx +389 -0
- package/src/map-canvas/map-canvas.tsx +457 -17
- package/src/map-canvas/map-context.ts +11 -0
- package/src/map-cluster-layer/map-cluster-layer.tsx +14 -1
- package/src/map-controls/map-controls.stories.tsx +16 -0
- package/src/map-controls/map-controls.test.tsx +145 -1
- package/src/map-controls/map-controls.tsx +57 -12
- package/src/map-geojson/map-geojson.stories.tsx +153 -3
- package/src/map-geojson/map-geojson.test.tsx +453 -0
- package/src/map-geojson/map-geojson.tsx +591 -51
- package/src/map-inset/index.ts +1 -0
- package/src/map-inset/map-inset.stories.tsx +61 -0
- package/src/map-inset/map-inset.test.tsx +117 -0
- package/src/map-inset/map-inset.tsx +264 -0
- package/src/map-legend/index.ts +10 -0
- package/src/map-legend/map-corner.ts +23 -0
- package/src/map-legend/map-legend.stories.tsx +171 -0
- package/src/map-legend/map-legend.test.tsx +136 -0
- package/src/map-legend/map-legend.tsx +264 -0
- package/src/map-marker/map-marker.stories.tsx +136 -4
- package/src/map-marker/map-marker.test.tsx +328 -3
- package/src/map-marker/map-marker.tsx +370 -33
- package/src/map-north-arrow/index.ts +1 -0
- package/src/map-north-arrow/map-north-arrow.stories.tsx +65 -0
- package/src/map-north-arrow/map-north-arrow.test.tsx +39 -0
- package/src/map-north-arrow/map-north-arrow.tsx +82 -0
- package/src/map-plan/index.ts +13 -0
- package/src/map-plan/map-plan-legend.test.tsx +61 -0
- package/src/map-plan/map-plan-legend.tsx +83 -0
- package/src/map-plan/map-plan-overlay.stories.tsx +260 -0
- package/src/map-plan/map-plan-overlay.test.tsx +302 -0
- package/src/map-plan/map-plan-overlay.tsx +531 -0
- package/src/map-plan/map-plan-status.test.tsx +71 -0
- package/src/map-plan/map-plan-status.tsx +77 -0
- package/src/map-plan/map-plan-table.test.tsx +86 -0
- package/src/map-plan/map-plan-table.tsx +153 -0
- package/src/map-plan/use-plan-selection.ts +78 -0
- package/src/map-plan-image/index.ts +1 -0
- package/src/map-plan-image/map-plan-image.stories.tsx +301 -0
- package/src/map-plan-image/map-plan-image.test.tsx +176 -0
- package/src/map-plan-image/map-plan-image.tsx +279 -0
- package/src/map-popup/map-popup.tsx +15 -10
- package/src/map-route/index.ts +1 -1
- package/src/map-route/map-route.test.tsx +216 -0
- package/src/map-route/map-route.tsx +105 -6
- package/src/map-scale-bar/index.ts +9 -0
- package/src/map-scale-bar/map-scale-bar.stories.tsx +63 -0
- package/src/map-scale-bar/map-scale-bar.test.tsx +65 -0
- package/src/map-scale-bar/map-scale-bar.tsx +145 -0
- package/src/test-utils/locator-fixture.ts +75 -0
- package/src/test-utils/maplibre-mock.ts +256 -19
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { act, cleanup, render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
vi.mock("maplibre-gl", async () => {
|
|
6
|
+
const { createMaplibreMock } = await import("../test-utils/maplibre-mock");
|
|
7
|
+
return createMaplibreMock();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
import { resetMaplibreMock } from "../test-utils/maplibre-mock";
|
|
11
|
+
import { MapCanvas } from "../map-canvas";
|
|
12
|
+
import type { MapPlanRegion } from "../lib/plan-regions";
|
|
13
|
+
import { resetMapWarnings } from "../lib/warn-once";
|
|
14
|
+
import { MapPlanOverlay } from "./map-plan-overlay";
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
cleanup();
|
|
18
|
+
resetMaplibreMock();
|
|
19
|
+
resetMapWarnings();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const PLAN = { width: 1000, height: 500 };
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* studio ── lab
|
|
26
|
+
* │
|
|
27
|
+
* store
|
|
28
|
+
*/
|
|
29
|
+
const REGIONS: MapPlanRegion[] = [
|
|
30
|
+
{
|
|
31
|
+
id: "studio",
|
|
32
|
+
label: "Studio",
|
|
33
|
+
description: "8 seats",
|
|
34
|
+
bounds: { x: 0, y: 0, width: 200, height: 100 },
|
|
35
|
+
},
|
|
36
|
+
{ id: "lab", label: "Lab", bounds: { x: 400, y: 0, width: 200, height: 100 } },
|
|
37
|
+
{ id: "store", label: "Store", bounds: { x: 0, y: 300, width: 200, height: 100 } },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function renderOverlay(props: Partial<React.ComponentProps<typeof MapPlanOverlay>> = {}) {
|
|
41
|
+
return render(
|
|
42
|
+
<MapCanvas plan={PLAN}>
|
|
43
|
+
<MapPlanOverlay regions={REGIONS} label="Rooms on level 3" {...props} />
|
|
44
|
+
</MapCanvas>,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("MapPlanOverlay", () => {
|
|
49
|
+
it("gives every region a real button whose name starts with its visible label", async () => {
|
|
50
|
+
renderOverlay();
|
|
51
|
+
|
|
52
|
+
const studio = await screen.findByRole("button", { name: "Studio, 8 seats" });
|
|
53
|
+
expect(studio).toHaveAccessibleName("Studio, 8 seats");
|
|
54
|
+
expect(screen.getByRole("button", { name: "Lab" })).toBeInTheDocument();
|
|
55
|
+
expect(screen.getByRole("list", { name: "Rooms on level 3" })).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("offers exactly one tab stop into the set", async () => {
|
|
59
|
+
renderOverlay();
|
|
60
|
+
|
|
61
|
+
const buttons = await screen.findAllByRole("button");
|
|
62
|
+
expect(buttons.filter((button) => button.tabIndex === 0)).toHaveLength(1);
|
|
63
|
+
expect(buttons[0]!.tabIndex).toBe(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("places each region where the plan puts it", async () => {
|
|
67
|
+
renderOverlay();
|
|
68
|
+
|
|
69
|
+
const studio = await screen.findByRole("button", { name: "Studio, 8 seats" });
|
|
70
|
+
const box = studio.parentElement as HTMLElement;
|
|
71
|
+
// The mock projects lng/lat linearly, so a region 200 plan units wide is
|
|
72
|
+
// half the width of one 400 units wide — the assertion that matters is that
|
|
73
|
+
// the box has real geometry, written by the projection pass.
|
|
74
|
+
expect(box.style.transform).toMatch(/^translate\(-?\d+px, -?\d+px\)$/);
|
|
75
|
+
expect(Number.parseFloat(box.style.width)).toBeGreaterThan(0);
|
|
76
|
+
expect(Number.parseFloat(box.style.height)).toBeGreaterThan(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("walks the plan with the arrow keys, spatially", async () => {
|
|
80
|
+
const user = userEvent.setup();
|
|
81
|
+
renderOverlay();
|
|
82
|
+
|
|
83
|
+
const studio = await screen.findByRole("button", { name: "Studio, 8 seats" });
|
|
84
|
+
await act(async () => studio.focus());
|
|
85
|
+
|
|
86
|
+
await user.keyboard("{ArrowRight}");
|
|
87
|
+
expect(screen.getByRole("button", { name: "Lab" })).toHaveFocus();
|
|
88
|
+
|
|
89
|
+
await user.keyboard("{ArrowLeft}");
|
|
90
|
+
expect(studio).toHaveFocus();
|
|
91
|
+
|
|
92
|
+
await user.keyboard("{ArrowDown}");
|
|
93
|
+
expect(screen.getByRole("button", { name: "Store" })).toHaveFocus();
|
|
94
|
+
|
|
95
|
+
await user.keyboard("{Home}");
|
|
96
|
+
expect(studio).toHaveFocus();
|
|
97
|
+
|
|
98
|
+
await user.keyboard("{End}");
|
|
99
|
+
expect(screen.getByRole("button", { name: "Store" })).toHaveFocus();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("selects with Enter and clears with Escape", async () => {
|
|
103
|
+
const user = userEvent.setup();
|
|
104
|
+
const onSelect = vi.fn();
|
|
105
|
+
renderOverlay({ onSelect });
|
|
106
|
+
|
|
107
|
+
const studio = await screen.findByRole("button", { name: "Studio, 8 seats" });
|
|
108
|
+
await act(async () => studio.focus());
|
|
109
|
+
|
|
110
|
+
await user.keyboard("{Enter}");
|
|
111
|
+
expect(onSelect).toHaveBeenLastCalledWith("studio", expect.objectContaining({ id: "studio" }));
|
|
112
|
+
|
|
113
|
+
await user.keyboard("{Escape}");
|
|
114
|
+
expect(onSelect).toHaveBeenLastCalledWith(null, null);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("shows the selection as a pressed state", async () => {
|
|
118
|
+
renderOverlay({ selectedId: ["lab"] });
|
|
119
|
+
|
|
120
|
+
const lab = await screen.findByRole("button", { name: "Lab" });
|
|
121
|
+
expect(lab).toHaveAttribute("aria-pressed", "true");
|
|
122
|
+
expect(screen.getByRole("button", { name: "Studio, 8 seats" })).toHaveAttribute(
|
|
123
|
+
"aria-pressed",
|
|
124
|
+
"false",
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("reports the active region so the shape and the proxy light up together", async () => {
|
|
129
|
+
const user = userEvent.setup();
|
|
130
|
+
const onActiveChange = vi.fn();
|
|
131
|
+
renderOverlay({ onActiveChange });
|
|
132
|
+
|
|
133
|
+
const lab = await screen.findByRole("button", { name: "Lab" });
|
|
134
|
+
await user.click(lab);
|
|
135
|
+
|
|
136
|
+
expect(onActiveChange).toHaveBeenCalledWith("lab", expect.objectContaining({ id: "lab" }));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("keeps the label out of the accessible name", async () => {
|
|
140
|
+
renderOverlay();
|
|
141
|
+
|
|
142
|
+
await screen.findByRole("button", { name: "Studio, 8 seats" });
|
|
143
|
+
const labels = document.querySelectorAll("[data-slot='map-plan-overlay-label']");
|
|
144
|
+
expect(labels).toHaveLength(3);
|
|
145
|
+
for (const label of labels) {
|
|
146
|
+
expect(label).toHaveAttribute("aria-hidden", "true");
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("can hand the pointer back to the canvas and stay keyboard-only", async () => {
|
|
151
|
+
renderOverlay({ capturePointer: false });
|
|
152
|
+
|
|
153
|
+
const studio = await screen.findByRole("button", { name: "Studio, 8 seats" });
|
|
154
|
+
expect(studio.className).toContain("pointer-events-none");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Two coaches of two seats — small enough to assert on, shaped like the train
|
|
160
|
+
* fixture group mode exists for.
|
|
161
|
+
*/
|
|
162
|
+
const GROUPED: MapPlanRegion[] = [
|
|
163
|
+
{
|
|
164
|
+
id: "a-1",
|
|
165
|
+
label: "1",
|
|
166
|
+
description: "window",
|
|
167
|
+
group: "coach-a",
|
|
168
|
+
groupLabel: "Coach A",
|
|
169
|
+
bounds: { x: 0, y: 0, width: 50, height: 50 },
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "a-2",
|
|
173
|
+
label: "2",
|
|
174
|
+
group: "coach-a",
|
|
175
|
+
groupLabel: "Coach A",
|
|
176
|
+
bounds: { x: 60, y: 0, width: 50, height: 50 },
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: "b-1",
|
|
180
|
+
label: "1",
|
|
181
|
+
group: "coach-b",
|
|
182
|
+
groupLabel: "Coach B",
|
|
183
|
+
bounds: { x: 500, y: 0, width: 50, height: 50 },
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: "b-2",
|
|
187
|
+
label: "2",
|
|
188
|
+
group: "coach-b",
|
|
189
|
+
groupLabel: "Coach B",
|
|
190
|
+
bounds: { x: 560, y: 0, width: 50, height: 50 },
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
function renderGrouped(props: Partial<React.ComponentProps<typeof MapPlanOverlay>> = {}) {
|
|
195
|
+
return render(
|
|
196
|
+
<MapCanvas plan={PLAN}>
|
|
197
|
+
<MapPlanOverlay
|
|
198
|
+
regions={GROUPED}
|
|
199
|
+
label="Seats"
|
|
200
|
+
mode="groups"
|
|
201
|
+
describeGroup={(group) => `${group.members.length} seats`}
|
|
202
|
+
{...props}
|
|
203
|
+
/>
|
|
204
|
+
</MapCanvas>,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
describe("MapPlanOverlay in group mode", () => {
|
|
209
|
+
it("starts with one button per group, not one per seat", async () => {
|
|
210
|
+
renderGrouped();
|
|
211
|
+
|
|
212
|
+
expect(await screen.findByRole("button", { name: "Coach A, 2 seats" })).toBeInTheDocument();
|
|
213
|
+
expect(screen.getByRole("button", { name: "Coach B, 2 seats" })).toBeInTheDocument();
|
|
214
|
+
expect(screen.getAllByRole("button")).toHaveLength(2);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("covers the whole group with the group's box", async () => {
|
|
218
|
+
const width = async (name: string) => {
|
|
219
|
+
const button = await screen.findByRole("button", { name });
|
|
220
|
+
return Number.parseInt((button.parentElement as HTMLElement).style.width, 10);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
renderGrouped();
|
|
224
|
+
const groupWidth = await width("Coach A, 2 seats");
|
|
225
|
+
cleanup();
|
|
226
|
+
|
|
227
|
+
// The same two seats, each on its own: the group's box spans both of them.
|
|
228
|
+
render(
|
|
229
|
+
<MapCanvas plan={PLAN}>
|
|
230
|
+
<MapPlanOverlay regions={GROUPED} label="Seats" />
|
|
231
|
+
</MapCanvas>,
|
|
232
|
+
);
|
|
233
|
+
const seatWidth = await width("1, window");
|
|
234
|
+
|
|
235
|
+
expect(groupWidth).toBeGreaterThan(seatWidth);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("descends into a group on Enter and comes back out on Escape", async () => {
|
|
239
|
+
const user = userEvent.setup();
|
|
240
|
+
const onSelect = vi.fn();
|
|
241
|
+
renderGrouped({ onSelect });
|
|
242
|
+
|
|
243
|
+
const coach = await screen.findByRole("button", { name: "Coach A, 2 seats" });
|
|
244
|
+
await act(async () => coach.focus());
|
|
245
|
+
await user.keyboard("{Enter}");
|
|
246
|
+
|
|
247
|
+
// Inside: the two seats, and going in selected nothing.
|
|
248
|
+
expect(await screen.findByRole("button", { name: "1, window" })).toBeInTheDocument();
|
|
249
|
+
expect(screen.getAllByRole("button")).toHaveLength(2);
|
|
250
|
+
expect(screen.queryByRole("button", { name: "Coach B, 2 seats" })).not.toBeInTheDocument();
|
|
251
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
252
|
+
|
|
253
|
+
// Focus lands on the group's first seat in the same commit, so the next key
|
|
254
|
+
// press has somewhere to go.
|
|
255
|
+
expect(document.activeElement).toHaveAccessibleName("1, window");
|
|
256
|
+
|
|
257
|
+
await user.keyboard("{Escape}");
|
|
258
|
+
expect(document.activeElement).toHaveAccessibleName("Coach A, 2 seats");
|
|
259
|
+
|
|
260
|
+
expect(await screen.findByRole("button", { name: "Coach A, 2 seats" })).toBeInTheDocument();
|
|
261
|
+
// Escape left the group instead of clearing the selection.
|
|
262
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("selects a member once inside", async () => {
|
|
266
|
+
const user = userEvent.setup();
|
|
267
|
+
const onSelect = vi.fn();
|
|
268
|
+
renderGrouped({ onSelect });
|
|
269
|
+
|
|
270
|
+
const coach = await screen.findByRole("button", { name: "Coach A, 2 seats" });
|
|
271
|
+
await act(async () => coach.focus());
|
|
272
|
+
await user.keyboard("{Enter}");
|
|
273
|
+
await user.click(await screen.findByRole("button", { name: "1, window" }));
|
|
274
|
+
|
|
275
|
+
expect(onSelect).toHaveBeenCalledWith("a-1", expect.objectContaining({ id: "a-1" }));
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("changes group with PageDown, at either level", async () => {
|
|
279
|
+
const user = userEvent.setup();
|
|
280
|
+
renderGrouped();
|
|
281
|
+
|
|
282
|
+
const coachA = await screen.findByRole("button", { name: "Coach A, 2 seats" });
|
|
283
|
+
await act(async () => coachA.focus());
|
|
284
|
+
await user.keyboard("{PageDown}");
|
|
285
|
+
expect(document.activeElement).toHaveAccessibleName("Coach B, 2 seats");
|
|
286
|
+
|
|
287
|
+
await user.keyboard("{Enter}");
|
|
288
|
+
expect(await screen.findByRole("button", { name: "1" })).toBeInTheDocument();
|
|
289
|
+
await user.keyboard("{PageUp}");
|
|
290
|
+
|
|
291
|
+
// Inside coach A now, on its first seat.
|
|
292
|
+
expect(await screen.findByRole("button", { name: "1, window" })).toBeInTheDocument();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("gives a group's button no pressed state, since it is not a toggle", async () => {
|
|
296
|
+
renderGrouped({ selectedId: "a-1" });
|
|
297
|
+
|
|
298
|
+
const coach = await screen.findByRole("button", { name: "Coach A, 2 seats" });
|
|
299
|
+
expect(coach).not.toHaveAttribute("aria-pressed");
|
|
300
|
+
expect(coach).toHaveAttribute("data-group", "true");
|
|
301
|
+
});
|
|
302
|
+
});
|