@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,136 @@
|
|
|
1
|
+
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { colorScaleFor } from "@elabs-ai/components-ui";
|
|
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 { MapLegend } from "./map-legend";
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
cleanup();
|
|
16
|
+
resetMaplibreMock();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const VALUES = [3, 8, 12, 19, 25, 31, 44, 52, 60];
|
|
20
|
+
|
|
21
|
+
describe("MapLegend", () => {
|
|
22
|
+
it("renders marker rows with their swatches, over a corner", async () => {
|
|
23
|
+
render(
|
|
24
|
+
<MapCanvas>
|
|
25
|
+
<MapLegend
|
|
26
|
+
title="Markers"
|
|
27
|
+
position="top-left"
|
|
28
|
+
items={[
|
|
29
|
+
{ label: "Lake", color: "var(--chart-1)", shape: "square" },
|
|
30
|
+
{ label: "City", color: "var(--chart-2)" },
|
|
31
|
+
]}
|
|
32
|
+
/>
|
|
33
|
+
</MapCanvas>,
|
|
34
|
+
);
|
|
35
|
+
expect(await screen.findByText("Lake")).toBeInTheDocument();
|
|
36
|
+
const legend = document.querySelector<HTMLElement>('[data-slot="map-legend"]')!;
|
|
37
|
+
expect(legend).toHaveAttribute("data-position", "top-left");
|
|
38
|
+
// Inside the map box, over the corner.
|
|
39
|
+
expect(document.querySelector('[data-slot="map-canvas"]')!.contains(legend)).toBe(true);
|
|
40
|
+
const swatches = legend.querySelectorAll<HTMLElement>('[data-slot="map-legend-swatch"]');
|
|
41
|
+
expect([...swatches].map((s) => s.dataset.shape)).toEqual(["square", "circle"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("portals into the strip below the map when positioned below", async () => {
|
|
45
|
+
render(
|
|
46
|
+
<MapCanvas>
|
|
47
|
+
<MapLegend position="below" items={[{ label: "Lake", color: "var(--chart-1)" }]} />
|
|
48
|
+
</MapCanvas>,
|
|
49
|
+
);
|
|
50
|
+
await screen.findByText("Lake");
|
|
51
|
+
const legend = document.querySelector('[data-slot="map-legend"]')!;
|
|
52
|
+
expect(document.querySelector('[data-slot="map-canvas-below"]')!.contains(legend)).toBe(true);
|
|
53
|
+
expect(document.querySelector('[data-slot="map-canvas"]')!.contains(legend)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("keys a stepped scale with token-reference swatches only", async () => {
|
|
57
|
+
render(
|
|
58
|
+
<MapCanvas>
|
|
59
|
+
<MapLegend scale={{ values: VALUES, spec: { type: "stepped", steps: 4 } }} />
|
|
60
|
+
</MapCanvas>,
|
|
61
|
+
);
|
|
62
|
+
await waitFor(() =>
|
|
63
|
+
expect(document.querySelectorAll('[data-slot="map-legend-step"]')).toHaveLength(4),
|
|
64
|
+
);
|
|
65
|
+
const steps = document.querySelectorAll<HTMLElement>('[data-slot="map-legend-step"]');
|
|
66
|
+
for (const step of steps) {
|
|
67
|
+
expect(step.getAttribute("style")).toMatch(/^background: var\(--chart-seq-\d\);$/);
|
|
68
|
+
}
|
|
69
|
+
// 4 steps → 5 boundary ticks.
|
|
70
|
+
expect(document.querySelectorAll('[data-slot="map-legend-tick"]')).toHaveLength(5);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("draws a continuous scale as a gradient of token references", async () => {
|
|
74
|
+
const scale = colorScaleFor(VALUES, { type: "continuous" });
|
|
75
|
+
render(
|
|
76
|
+
<MapCanvas>
|
|
77
|
+
<MapLegend scale={scale} formatValue={(v) => `${v}%`} />
|
|
78
|
+
</MapCanvas>,
|
|
79
|
+
);
|
|
80
|
+
const gradient = await waitFor(() => {
|
|
81
|
+
const el = document.querySelector<HTMLElement>('[data-slot="map-legend-gradient"]');
|
|
82
|
+
expect(el).not.toBeNull();
|
|
83
|
+
return el!;
|
|
84
|
+
});
|
|
85
|
+
const style = gradient.getAttribute("style") ?? "";
|
|
86
|
+
expect(style).toContain("linear-gradient(to right, var(--chart-seq-1) 0%");
|
|
87
|
+
expect(style).not.toMatch(/#[0-9a-f]{3,8}\b|rgb\(|oklch\(/i);
|
|
88
|
+
expect(screen.getByText("3%")).toBeInTheDocument();
|
|
89
|
+
expect(screen.getByText("60%")).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("lists a categorical scale as swatch rows", async () => {
|
|
93
|
+
render(
|
|
94
|
+
<MapCanvas>
|
|
95
|
+
<MapLegend
|
|
96
|
+
scale={{
|
|
97
|
+
values: ["Park", "Water", "Park"],
|
|
98
|
+
spec: { type: "stepped", palette: "categorical" },
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
</MapCanvas>,
|
|
102
|
+
);
|
|
103
|
+
expect(await screen.findByText("Park")).toBeInTheDocument();
|
|
104
|
+
expect(screen.getByText("Water")).toBeInTheDocument();
|
|
105
|
+
expect(document.querySelector('[data-slot="map-legend-ramp"]')).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("MapLegend — a grid key spends the whole strip (c-5)", () => {
|
|
110
|
+
it("lays the grid out with auto-fit, so empty tracks collapse", async () => {
|
|
111
|
+
render(
|
|
112
|
+
<MapCanvas>
|
|
113
|
+
<MapLegend
|
|
114
|
+
layout="grid"
|
|
115
|
+
items={[
|
|
116
|
+
{ id: "lake", label: "Lake Ontario", color: "var(--chart-1)", shape: "square" },
|
|
117
|
+
{ id: "gta", label: "Greater Toronto Area", color: "var(--chart-2)", shape: "square" },
|
|
118
|
+
{ id: "city", label: "City", color: "var(--chart-4)", shape: "circle" },
|
|
119
|
+
{ id: "ferry", label: "Ferry route", color: "var(--chart-3)", shape: "line" },
|
|
120
|
+
]}
|
|
121
|
+
/>
|
|
122
|
+
</MapCanvas>,
|
|
123
|
+
);
|
|
124
|
+
const list = await waitFor(() => {
|
|
125
|
+
const found = document.querySelector('[data-slot="map-legend-items"]');
|
|
126
|
+
expect(found).not.toBeNull();
|
|
127
|
+
return found!;
|
|
128
|
+
});
|
|
129
|
+
// `auto-fill` kept 7 tracks of 113.7 px on an 868 px strip for four keys,
|
|
130
|
+
// so "Greater Toronto Area" truncated (scrollWidth 123 vs clientWidth 98)
|
|
131
|
+
// with three empty tracks beside it. jsdom lays out nothing, so the
|
|
132
|
+
// measured proof lives in the story; this locks the layout rule itself.
|
|
133
|
+
expect(list.className).toContain("repeat(auto-fit,minmax(7rem,1fr))");
|
|
134
|
+
expect(list.className).not.toContain("auto-fill");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useMemo, type HTMLAttributes, type ReactNode } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import {
|
|
6
|
+
colorScaleFor,
|
|
7
|
+
useLocale,
|
|
8
|
+
type ColorScale,
|
|
9
|
+
type ColorScaleSpec,
|
|
10
|
+
type ColorScaleValue,
|
|
11
|
+
} from "@elabs-ai/components-ui";
|
|
12
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
13
|
+
|
|
14
|
+
import { type MapResponsive, useMapFrameSlot, useMapResponsive } from "../lib/use-map-breakpoint";
|
|
15
|
+
import { MAP_CORNER_CLASSES, type MapCorner } from "./map-corner";
|
|
16
|
+
|
|
17
|
+
/** The mark a key row shows: a point, an area or a line marker. */
|
|
18
|
+
export type MapLegendShape = "circle" | "square" | "line";
|
|
19
|
+
|
|
20
|
+
/** One row of a marker key. */
|
|
21
|
+
export interface MapLegendItem {
|
|
22
|
+
/** Stable identity; defaults to `label`. */
|
|
23
|
+
id?: string;
|
|
24
|
+
label: string;
|
|
25
|
+
/** Any CSS colour — use a token reference (`var(--chart-1)`), never a literal. */
|
|
26
|
+
color: string;
|
|
27
|
+
/** Default `"circle"`. */
|
|
28
|
+
shape?: MapLegendShape;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A colour scale to key: one `colorScaleFor` already built (share it with the
|
|
33
|
+
* layer it colours), or the values + spec to build one here.
|
|
34
|
+
*/
|
|
35
|
+
export type MapLegendScale =
|
|
36
|
+
| ColorScale
|
|
37
|
+
| { values: readonly ColorScaleValue[]; spec: ColorScaleSpec };
|
|
38
|
+
|
|
39
|
+
/** Outside the map box (`above` / `below`) or over one of its corners. */
|
|
40
|
+
export type MapLegendPosition = "above" | "below" | MapCorner;
|
|
41
|
+
|
|
42
|
+
/** `list`: one row per item. `grid`: items flow into as many columns as fit. */
|
|
43
|
+
export type MapLegendLayout = "list" | "grid";
|
|
44
|
+
|
|
45
|
+
export interface MapLegendProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
46
|
+
/** A heading for the key. */
|
|
47
|
+
title?: ReactNode;
|
|
48
|
+
/** Marker key rows. */
|
|
49
|
+
items?: readonly MapLegendItem[];
|
|
50
|
+
/** A colour ramp (continuous / stepped) or category key, from `colorScaleFor`. */
|
|
51
|
+
scale?: MapLegendScale;
|
|
52
|
+
/** Default `"list"`. */
|
|
53
|
+
layout?: MapLegendLayout;
|
|
54
|
+
/**
|
|
55
|
+
* Where the key sits, optionally per tier — for example
|
|
56
|
+
* `{ base: "top-left", narrow: "below" }` so it stops covering a phone-width
|
|
57
|
+
* map. Default `"above"`.
|
|
58
|
+
*/
|
|
59
|
+
position?: MapResponsive<MapLegendPosition>;
|
|
60
|
+
/** Formats a ramp's tick values (default: the locale's number format). */
|
|
61
|
+
formatValue?: (value: number) => string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const SHAPE_CLASSES: Record<MapLegendShape, string> = {
|
|
65
|
+
circle: "size-2.5 rounded-full",
|
|
66
|
+
square: "size-2.5 rounded-xs",
|
|
67
|
+
line: "h-0.5 w-3.5 rounded-full",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const LAYOUT_CLASSES: Record<MapLegendLayout, string> = {
|
|
71
|
+
list: "flex flex-col gap-1",
|
|
72
|
+
// `auto-fit`, never `auto-fill` (c-5): `auto-fill` keeps the tracks a wide
|
|
73
|
+
// strip could hold even when nothing occupies them, so four keys on an
|
|
74
|
+
// 868 px strip got 113.7 px each and "Greater Toronto Area" truncated with
|
|
75
|
+
// three empty tracks beside it. `auto-fit` collapses the empty tracks and
|
|
76
|
+
// the real keys share the full width; a key with more items than fit wraps
|
|
77
|
+
// exactly as before.
|
|
78
|
+
grid: "grid grid-cols-[repeat(auto-fit,minmax(7rem,1fr))] gap-x-3 gap-y-1",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function isColorScale(scale: MapLegendScale): scale is ColorScale {
|
|
82
|
+
return "colorOf" in scale;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function Swatch({ color, shape = "circle" }: { color: string; shape?: MapLegendShape }) {
|
|
86
|
+
return (
|
|
87
|
+
<span
|
|
88
|
+
aria-hidden="true"
|
|
89
|
+
data-slot="map-legend-swatch"
|
|
90
|
+
data-shape={shape}
|
|
91
|
+
className={cn("inline-block shrink-0", SHAPE_CLASSES[shape])}
|
|
92
|
+
style={{ background: color }}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface Tick {
|
|
98
|
+
key: string;
|
|
99
|
+
at: number;
|
|
100
|
+
label: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A ramp: stepped swatches or a continuous gradient, with value ticks under it. */
|
|
104
|
+
function Ramp({ scale, format }: { scale: ColorScale; format: (value: number) => string }) {
|
|
105
|
+
const ticks: Tick[] = [];
|
|
106
|
+
let strip: ReactNode = null;
|
|
107
|
+
|
|
108
|
+
if (scale.type === "stepped" && scale.steps.length > 0) {
|
|
109
|
+
const n = scale.steps.length;
|
|
110
|
+
strip = (
|
|
111
|
+
<div className="flex h-2.5 w-full overflow-hidden rounded-xs">
|
|
112
|
+
{scale.steps.map((step) => (
|
|
113
|
+
<span
|
|
114
|
+
key={`${step.from}-${step.to}`}
|
|
115
|
+
data-slot="map-legend-step"
|
|
116
|
+
className="min-w-0 flex-1"
|
|
117
|
+
style={{ background: step.color }}
|
|
118
|
+
/>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
const first = scale.steps[0];
|
|
123
|
+
if (first) ticks.push({ key: "start", at: 0, label: format(first.from) });
|
|
124
|
+
scale.steps.forEach((step, index) => {
|
|
125
|
+
ticks.push({ key: `to-${step.to}`, at: (index + 1) / n, label: format(step.to) });
|
|
126
|
+
});
|
|
127
|
+
} else if (scale.stops.length > 0) {
|
|
128
|
+
const last = Math.max(1, scale.stops.length - 1);
|
|
129
|
+
const gradient = scale.stops
|
|
130
|
+
.map((stop, index) => `${stop.color} ${(index / last) * 100}%`)
|
|
131
|
+
.join(", ");
|
|
132
|
+
strip = (
|
|
133
|
+
<div
|
|
134
|
+
data-slot="map-legend-gradient"
|
|
135
|
+
className="h-2.5 w-full rounded-xs"
|
|
136
|
+
style={{ background: `linear-gradient(to right, ${gradient})` }}
|
|
137
|
+
/>
|
|
138
|
+
);
|
|
139
|
+
const marks = [scale.domain?.[0], scale.center ?? undefined, scale.domain?.[1]];
|
|
140
|
+
for (const value of marks) {
|
|
141
|
+
if (value === undefined) continue;
|
|
142
|
+
const at = scale.positionOf(value);
|
|
143
|
+
if (at === null) continue;
|
|
144
|
+
ticks.push({ key: `v-${value}`, at, label: format(value) });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!strip) return null;
|
|
149
|
+
return (
|
|
150
|
+
// A ramp reads low → high left to right in every writing direction.
|
|
151
|
+
<div dir="ltr" data-slot="map-legend-ramp" className="flex w-48 max-w-full flex-col gap-1">
|
|
152
|
+
{strip}
|
|
153
|
+
<div className="relative h-4">
|
|
154
|
+
{ticks.map((tick) => (
|
|
155
|
+
<span
|
|
156
|
+
key={tick.key}
|
|
157
|
+
data-slot="map-legend-tick"
|
|
158
|
+
className={cn(
|
|
159
|
+
"absolute top-0 text-meta whitespace-nowrap text-muted-foreground tabular-nums",
|
|
160
|
+
tick.at <= 0 ? "" : tick.at >= 1 ? "-translate-x-full" : "-translate-x-1/2",
|
|
161
|
+
)}
|
|
162
|
+
style={{ left: `${Math.min(1, Math.max(0, tick.at)) * 100}%` }}
|
|
163
|
+
>
|
|
164
|
+
{tick.label}
|
|
165
|
+
</span>
|
|
166
|
+
))}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* MapLegend — the map's key: marker rows (`items`) and / or a colour ramp
|
|
174
|
+
* (`scale`, built with `colorScaleFor` from `@elabs-ai/components-ui`, so its
|
|
175
|
+
* colours are token references in every theme). It sits above or below the
|
|
176
|
+
* map box, or over a corner, per tier; the house legend look (`text-meta`,
|
|
177
|
+
* dot markers) matches `charts`' `ChartLegend`. Render inside `<MapCanvas>`.
|
|
178
|
+
*/
|
|
179
|
+
export const MapLegend = forwardRef<HTMLDivElement, MapLegendProps>(function MapLegend(
|
|
180
|
+
{
|
|
181
|
+
title,
|
|
182
|
+
items,
|
|
183
|
+
scale: scaleInput,
|
|
184
|
+
layout = "list",
|
|
185
|
+
position: positionProp = "above",
|
|
186
|
+
formatValue,
|
|
187
|
+
className,
|
|
188
|
+
...props
|
|
189
|
+
},
|
|
190
|
+
ref,
|
|
191
|
+
) {
|
|
192
|
+
const { formatNumber } = useLocale();
|
|
193
|
+
const position = useMapResponsive(positionProp);
|
|
194
|
+
const outside = position === "above" || position === "below" ? position : null;
|
|
195
|
+
const slot = useMapFrameSlot(outside);
|
|
196
|
+
|
|
197
|
+
const scale = useMemo<ColorScale | null>(() => {
|
|
198
|
+
if (!scaleInput) return null;
|
|
199
|
+
return isColorScale(scaleInput)
|
|
200
|
+
? scaleInput
|
|
201
|
+
: colorScaleFor(scaleInput.values, scaleInput.spec);
|
|
202
|
+
}, [scaleInput]);
|
|
203
|
+
const format = formatValue ?? ((value: number) => formatNumber(value));
|
|
204
|
+
|
|
205
|
+
const rows: MapLegendItem[] = [
|
|
206
|
+
...(items ?? []),
|
|
207
|
+
...(scale?.palette === "categorical"
|
|
208
|
+
? scale.categories.map((category) => ({
|
|
209
|
+
id: `category-${category.value}`,
|
|
210
|
+
label: String(category.value),
|
|
211
|
+
color: category.color,
|
|
212
|
+
shape: "square" as const,
|
|
213
|
+
}))
|
|
214
|
+
: []),
|
|
215
|
+
];
|
|
216
|
+
const showRamp = scale !== null && scale.palette !== "categorical";
|
|
217
|
+
|
|
218
|
+
const body = (
|
|
219
|
+
<div
|
|
220
|
+
ref={ref}
|
|
221
|
+
data-slot="map-legend"
|
|
222
|
+
data-position={position}
|
|
223
|
+
data-layout={layout}
|
|
224
|
+
className={cn(
|
|
225
|
+
"flex flex-col gap-1.5 text-meta text-foreground",
|
|
226
|
+
outside
|
|
227
|
+
? "w-full"
|
|
228
|
+
: cn(
|
|
229
|
+
"absolute z-10 max-w-[calc(100%-1rem)] rounded-md bg-background/90 px-2 py-1.5 shadow-ring-sm",
|
|
230
|
+
MAP_CORNER_CLASSES[position as MapCorner],
|
|
231
|
+
),
|
|
232
|
+
className,
|
|
233
|
+
)}
|
|
234
|
+
{...props}
|
|
235
|
+
>
|
|
236
|
+
{title && (
|
|
237
|
+
<div data-slot="map-legend-title" className="font-medium">
|
|
238
|
+
{title}
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
{rows.length > 0 && (
|
|
242
|
+
<ul
|
|
243
|
+
data-slot="map-legend-items"
|
|
244
|
+
className={cn("m-0 list-none p-0", LAYOUT_CLASSES[layout])}
|
|
245
|
+
>
|
|
246
|
+
{rows.map((item) => (
|
|
247
|
+
<li
|
|
248
|
+
key={item.id ?? item.label}
|
|
249
|
+
data-slot="map-legend-item"
|
|
250
|
+
className="flex min-w-0 items-center gap-1.5"
|
|
251
|
+
>
|
|
252
|
+
<Swatch color={item.color} shape={item.shape} />
|
|
253
|
+
<span className="min-w-0 truncate">{item.label}</span>
|
|
254
|
+
</li>
|
|
255
|
+
))}
|
|
256
|
+
</ul>
|
|
257
|
+
)}
|
|
258
|
+
{showRamp && scale && <Ramp scale={scale} format={format} />}
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
if (outside) return slot ? createPortal(body, slot) : null;
|
|
263
|
+
return body;
|
|
264
|
+
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
2
|
import { MapPin } from "lucide-react";
|
|
3
|
+
import { expect, waitFor } from "storybook/test";
|
|
3
4
|
|
|
4
5
|
import { MapCanvas } from "../map-canvas";
|
|
6
|
+
import { MAP_LABEL_ANCHORS } from "../map-annotation";
|
|
7
|
+
import { LOCATOR_BOUNDS, LOCATOR_PLACES } from "../test-utils/locator-fixture";
|
|
5
8
|
import {
|
|
6
9
|
MapMarker,
|
|
7
10
|
MapMarkerContent,
|
|
@@ -32,11 +35,24 @@ export const Default: Story = {
|
|
|
32
35
|
),
|
|
33
36
|
};
|
|
34
37
|
|
|
38
|
+
/** Both markers, with room for the wider label under the second one (c-8). */
|
|
39
|
+
const ALEXANDERPLATZ: [number, number] = [13.405, 52.52];
|
|
40
|
+
const BRANDENBURG_GATE: [number, number] = [13.377, 52.516];
|
|
41
|
+
|
|
35
42
|
export const WithPopupAndTooltip: Story = {
|
|
36
43
|
render: () => (
|
|
37
44
|
<div className="h-[480px]">
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
{/*
|
|
46
|
+
c-8: a fixed center/zoom framed both markers at 900 px only — at
|
|
47
|
+
380 px the second marker sat half off the start edge and its label
|
|
48
|
+
read "…urg Gate". Fitting the markers keeps both inside at every
|
|
49
|
+
width; the padding is the room the wider label needs.
|
|
50
|
+
*/}
|
|
51
|
+
<MapCanvas
|
|
52
|
+
bounds={[BRANDENBURG_GATE, ALEXANDERPLATZ]}
|
|
53
|
+
fitBoundsOptions={{ padding: 72, maxZoom: 14 }}
|
|
54
|
+
>
|
|
55
|
+
<MapMarker longitude={ALEXANDERPLATZ[0]} latitude={ALEXANDERPLATZ[1]}>
|
|
40
56
|
<MapMarkerContent />
|
|
41
57
|
<MapMarkerTooltip>Click for details</MapMarkerTooltip>
|
|
42
58
|
<MapMarkerPopup closeButton>
|
|
@@ -46,7 +62,7 @@ export const WithPopupAndTooltip: Story = {
|
|
|
46
62
|
</p>
|
|
47
63
|
</MapMarkerPopup>
|
|
48
64
|
</MapMarker>
|
|
49
|
-
<MapMarker longitude={
|
|
65
|
+
<MapMarker longitude={BRANDENBURG_GATE[0]} latitude={BRANDENBURG_GATE[1]}>
|
|
50
66
|
<MapMarkerContent>
|
|
51
67
|
<MapPin className="size-6 text-primary" aria-hidden="true" />
|
|
52
68
|
</MapMarkerContent>
|
|
@@ -61,11 +77,127 @@ export const Draggable: Story = {
|
|
|
61
77
|
render: () => (
|
|
62
78
|
<div className="h-[480px]">
|
|
63
79
|
<MapCanvas center={[13.405, 52.52]} zoom={11}>
|
|
64
|
-
<MapMarker longitude={13.405} latitude={52.52} draggable>
|
|
80
|
+
<MapMarker longitude={13.405} latitude={52.52} draggable dragLabel="Depot location">
|
|
65
81
|
<MapMarkerContent />
|
|
66
82
|
<MapMarkerLabel>Drag me</MapMarkerLabel>
|
|
67
83
|
</MapMarker>
|
|
68
84
|
</MapCanvas>
|
|
69
85
|
</div>
|
|
70
86
|
),
|
|
87
|
+
play: async ({ canvasElement }) => {
|
|
88
|
+
// c-4 (WCAG 2.1.1): the story's whole subject is a marker you move, so it
|
|
89
|
+
// has to be movable without a mouse. Before the fix the only tab stop in
|
|
90
|
+
// the story was the map canvas and `element.focus()` did not take.
|
|
91
|
+
const marker = await waitFor(() => {
|
|
92
|
+
const element = canvasElement.querySelector<HTMLElement>(".maplibregl-marker");
|
|
93
|
+
expect(element).not.toBeNull();
|
|
94
|
+
return element as HTMLElement;
|
|
95
|
+
});
|
|
96
|
+
await expect(marker).toHaveAttribute("tabindex", "0");
|
|
97
|
+
await expect(marker).toHaveAccessibleName("Depot location");
|
|
98
|
+
|
|
99
|
+
marker.focus();
|
|
100
|
+
await expect(document.activeElement).toBe(marker);
|
|
101
|
+
|
|
102
|
+
const before = marker.getBoundingClientRect().x;
|
|
103
|
+
marker.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }));
|
|
104
|
+
marker.dispatchEvent(new KeyboardEvent("keyup", { key: "ArrowRight", bubbles: true }));
|
|
105
|
+
await waitFor(() => expect(marker.getBoundingClientRect().x).toBeGreaterThan(before));
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// A marker's own label has no de-confliction pass: it goes where its anchor
|
|
110
|
+
// puts it, however close the next marker is. The boxed row's labels offset
|
|
111
|
+
// OUTWARD from their point, so at the narrow tier — where a degree of
|
|
112
|
+
// longitude is ~100px and a boxed label is up to 71px wide — two neighbours
|
|
113
|
+
// print on top of each other. That row is therefore shown from `medium` up
|
|
114
|
+
// (`showAt`), which is the prop the docblock already points at.
|
|
115
|
+
const LABEL_SPOTS = [
|
|
116
|
+
{ longitude: -79.6, latitude: 44.05 },
|
|
117
|
+
{ longitude: -78.6, latitude: 44.05 },
|
|
118
|
+
{ longitude: -77.6, latitude: 44.05 },
|
|
119
|
+
{ longitude: -76.6, latitude: 44.05 },
|
|
120
|
+
{ longitude: -79.6, latitude: 43.45 },
|
|
121
|
+
{ longitude: -78.6, latitude: 43.45 },
|
|
122
|
+
{ longitude: -77.6, latitude: 43.45 },
|
|
123
|
+
{ longitude: -76.6, latitude: 43.45 },
|
|
124
|
+
] as const;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A marker's own label, at each of the eight positions around its point;
|
|
128
|
+
* the lower row adds a box (a plate that keeps the text readable over busy
|
|
129
|
+
* ground) and a callout line.
|
|
130
|
+
*/
|
|
131
|
+
export const LabelPositions: Story = {
|
|
132
|
+
render: () => (
|
|
133
|
+
<div className="w-full p-4">
|
|
134
|
+
<MapCanvas interactive={false} bounds={LOCATOR_BOUNDS} fitBoundsOptions={{ padding: 48 }}>
|
|
135
|
+
{MAP_LABEL_ANCHORS.map((position, index) => (
|
|
136
|
+
<MapMarker
|
|
137
|
+
key={position}
|
|
138
|
+
{...LABEL_SPOTS[index]!}
|
|
139
|
+
label={{ text: position, position, box: index >= 4, callout: index >= 4 }}
|
|
140
|
+
showAt={index >= 4 ? { base: true, narrow: false } : true}
|
|
141
|
+
>
|
|
142
|
+
<MapMarkerContent />
|
|
143
|
+
</MapMarker>
|
|
144
|
+
))}
|
|
145
|
+
</MapCanvas>
|
|
146
|
+
</div>
|
|
147
|
+
),
|
|
148
|
+
play: async ({ canvasElement }) => {
|
|
149
|
+
// Two labels printed through each other read as neither. Measured, per
|
|
150
|
+
// tier: the boxed row is off at narrow, so every tier comes out clean.
|
|
151
|
+
await waitFor(() =>
|
|
152
|
+
expect(
|
|
153
|
+
canvasElement.querySelectorAll('[data-slot="map-marker-label"]').length,
|
|
154
|
+
).toBeGreaterThan(0),
|
|
155
|
+
);
|
|
156
|
+
const boxes = [
|
|
157
|
+
...canvasElement.querySelectorAll<HTMLElement>('[data-slot="map-marker-label"]'),
|
|
158
|
+
].map((el) => el.getBoundingClientRect());
|
|
159
|
+
const overlaps: string[] = [];
|
|
160
|
+
for (const [i, a] of boxes.entries()) {
|
|
161
|
+
for (const b of boxes.slice(i + 1)) {
|
|
162
|
+
const x = Math.min(a.right, b.right) - Math.max(a.left, b.left);
|
|
163
|
+
const y = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
|
|
164
|
+
if (x > 0 && y > 0) overlaps.push(`${Math.round(x * y)}px²`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
await expect(overlaps).toEqual([]);
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A label with no marker glyph — text placed on the map (a lake or region
|
|
173
|
+
* name) — plus a marker that only shows from `medium` up.
|
|
174
|
+
*/
|
|
175
|
+
export const LabelOnlyAndShowAt: Story = {
|
|
176
|
+
render: () => (
|
|
177
|
+
<div className="w-full p-4">
|
|
178
|
+
<MapCanvas interactive={false} bounds={LOCATOR_BOUNDS} fitBoundsOptions={{ padding: 48 }}>
|
|
179
|
+
<MapMarker {...LOCATOR_PLACES.lakeLabel} label={{ text: "Lake Ontario", box: true }} />
|
|
180
|
+
<MapMarker {...LOCATOR_PLACES.toronto} label={{ text: "Toronto", position: "top-left" }}>
|
|
181
|
+
<MapMarkerContent />
|
|
182
|
+
</MapMarker>
|
|
183
|
+
<MapMarker
|
|
184
|
+
{...LOCATOR_PLACES.rochester}
|
|
185
|
+
showAt={{ base: true, narrow: false }}
|
|
186
|
+
label={{ text: "Rochester, NY", position: "right" }}
|
|
187
|
+
>
|
|
188
|
+
<MapMarkerContent />
|
|
189
|
+
</MapMarker>
|
|
190
|
+
</MapCanvas>
|
|
191
|
+
</div>
|
|
192
|
+
),
|
|
193
|
+
play: async ({ canvasElement }) => {
|
|
194
|
+
const map = canvasElement.querySelector<HTMLElement>('[data-slot="map-canvas"]');
|
|
195
|
+
if (!map || !canvasElement.querySelector(".maplibregl-canvas")) return;
|
|
196
|
+
const narrow = map.getAttribute("data-map-breakpoint") === "narrow";
|
|
197
|
+
await waitFor(() =>
|
|
198
|
+
expect(canvasElement.querySelectorAll('[data-slot="map-marker-label"]')).toHaveLength(
|
|
199
|
+
narrow ? 2 : 3,
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
},
|
|
71
203
|
};
|