@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
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import MapLibreGL, { type MarkerOptions, type PopupOptions } from "maplibre-gl";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
use,
|
|
7
|
+
useEffect,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
type ReactNode,
|
|
13
|
+
} from "react";
|
|
5
14
|
import { createPortal } from "react-dom";
|
|
6
15
|
import { X } from "lucide-react";
|
|
7
16
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
8
17
|
import { useLocale } from "@elabs-ai/components-ui";
|
|
9
18
|
|
|
10
19
|
import { useMap } from "../map-canvas/map-context";
|
|
20
|
+
import {
|
|
21
|
+
type MapResponsive,
|
|
22
|
+
useMapBreakpoint,
|
|
23
|
+
resolveMapResponsive,
|
|
24
|
+
} from "../lib/use-map-breakpoint";
|
|
25
|
+
import {
|
|
26
|
+
type MapLabelAnchor,
|
|
27
|
+
clampShift,
|
|
28
|
+
mapAnchorGeometry,
|
|
29
|
+
mapAnchorTransform,
|
|
30
|
+
} from "../map-annotation/anchor";
|
|
31
|
+
import { resolveMapPosition, toPlanPoint, type MapPosition } from "../lib/map-position";
|
|
32
|
+
import type { PlanPoint } from "../lib/plan-crs";
|
|
11
33
|
|
|
12
34
|
type MarkerContextValue = {
|
|
13
35
|
marker: MapLibreGL.Marker;
|
|
@@ -24,35 +46,68 @@ function useMarkerContext() {
|
|
|
24
46
|
return context;
|
|
25
47
|
}
|
|
26
48
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
|
|
49
|
+
/**
|
|
50
|
+
* A marker's own text label — the locator-map label: one of eight positions,
|
|
51
|
+
* optionally on a box, optionally pushed out on a callout line.
|
|
52
|
+
*/
|
|
53
|
+
export interface MapMarkerLabelSpec {
|
|
54
|
+
/** The label text. */
|
|
55
|
+
text: string;
|
|
56
|
+
/** Where the label sits relative to the point (default `"top"`). */
|
|
57
|
+
position?: MapLabelAnchor;
|
|
58
|
+
/** Set the label on a background box so it reads over a busy basemap. */
|
|
59
|
+
box?: boolean;
|
|
60
|
+
/** Push the label further out and draw a callout line back to the point. */
|
|
61
|
+
callout?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type MapMarkerProps = MapPosition & {
|
|
65
|
+
/**
|
|
66
|
+
* Marker sub-components (MapMarkerContent, MapMarkerPopup, MapMarkerTooltip,
|
|
67
|
+
* MapMarkerLabel). Optional when `label` alone marks the place — an inline
|
|
68
|
+
* area label, for example.
|
|
69
|
+
*/
|
|
70
|
+
children?: ReactNode;
|
|
71
|
+
/**
|
|
72
|
+
* Whether the marker shows at a tier (default `true` everywhere):
|
|
73
|
+
* `{ base: true, narrow: false }` drops it from a phone-width map. The
|
|
74
|
+
* The editorial advice applies — duplicate a marker with a shorter label and
|
|
75
|
+
* the opposite `showAt` rather than cramming one label into both.
|
|
76
|
+
*/
|
|
77
|
+
showAt?: MapResponsive<boolean>;
|
|
78
|
+
/** A text label drawn with the marker (see {@link MapMarkerLabelSpec}). */
|
|
79
|
+
label?: MapMarkerLabelSpec;
|
|
34
80
|
/** Callback when the marker is clicked. */
|
|
35
81
|
onClick?: (e: MouseEvent) => void;
|
|
36
82
|
/** Callback when the mouse enters the marker. */
|
|
37
83
|
onMouseEnter?: (e: MouseEvent) => void;
|
|
38
84
|
/** Callback when the mouse leaves the marker. */
|
|
39
85
|
onMouseLeave?: (e: MouseEvent) => void;
|
|
40
|
-
/** Callback when a drag starts (requires `draggable`). */
|
|
41
|
-
onDragStart?: (lngLat: { lng: number; lat: number }) => void;
|
|
42
|
-
/** Callback during a drag (requires `draggable`). */
|
|
43
|
-
onDrag?: (lngLat: { lng: number; lat: number }) => void;
|
|
44
|
-
/** Callback when a drag ends (requires `draggable`). */
|
|
45
|
-
onDragEnd?: (lngLat: { lng: number; lat: number }) => void;
|
|
86
|
+
/** Callback when a drag starts (requires `draggable`). The plan point is `null` off a plan. */
|
|
87
|
+
onDragStart?: (lngLat: { lng: number; lat: number }, plan: PlanPoint | null) => void;
|
|
88
|
+
/** Callback during a drag (requires `draggable`). The plan point is `null` off a plan. */
|
|
89
|
+
onDrag?: (lngLat: { lng: number; lat: number }, plan: PlanPoint | null) => void;
|
|
90
|
+
/** Callback when a drag ends (requires `draggable`). The plan point is `null` off a plan. */
|
|
91
|
+
onDragEnd?: (lngLat: { lng: number; lat: number }, plan: PlanPoint | null) => void;
|
|
92
|
+
/**
|
|
93
|
+
* The accessible name of a `draggable` marker — say what it moves ("Depot
|
|
94
|
+
* location"). Defaults to the locale seam's generic name. Ignored when the
|
|
95
|
+
* marker is not draggable.
|
|
96
|
+
*/
|
|
97
|
+
dragLabel?: string;
|
|
46
98
|
} & Omit<MarkerOptions, "element">;
|
|
47
99
|
|
|
48
100
|
/**
|
|
49
|
-
* A marker anchored at a lng/lat
|
|
50
|
-
* `MapMarkerContent` (the visual),
|
|
51
|
-
* on click) and `MapMarkerTooltip`
|
|
101
|
+
* A marker anchored at a lng/lat — or, on a canvas with a `plan` extent, at a
|
|
102
|
+
* plan `x`/`y`. Compose the pieces you need: `MapMarkerContent` (the visual),
|
|
103
|
+
* `MapMarkerLabel`, `MapMarkerPopup` (opens on click) and `MapMarkerTooltip`
|
|
104
|
+
* (shows on hover).
|
|
52
105
|
*/
|
|
53
106
|
export function MapMarker({
|
|
54
|
-
longitude,
|
|
55
|
-
latitude,
|
|
107
|
+
longitude: longitudeProp,
|
|
108
|
+
latitude: latitudeProp,
|
|
109
|
+
x,
|
|
110
|
+
y,
|
|
56
111
|
children,
|
|
57
112
|
onClick,
|
|
58
113
|
onMouseEnter,
|
|
@@ -61,11 +116,26 @@ export function MapMarker({
|
|
|
61
116
|
onDrag,
|
|
62
117
|
onDragEnd,
|
|
63
118
|
draggable = false,
|
|
119
|
+
dragLabel,
|
|
120
|
+
showAt = true,
|
|
121
|
+
label,
|
|
64
122
|
...markerOptions
|
|
65
123
|
}: MapMarkerProps) {
|
|
66
|
-
const { map } = useMap();
|
|
124
|
+
const { map, plan } = useMap();
|
|
125
|
+
const { t } = useLocale();
|
|
126
|
+
const visible = resolveMapResponsive(showAt, useMapBreakpoint());
|
|
67
127
|
const [marker, setMarker] = useState<MapLibreGL.Marker | null>(null);
|
|
68
128
|
|
|
129
|
+
// One position prop, two coordinate systems, two scalars out — so every sync
|
|
130
|
+
// effect below keeps primitive dependencies.
|
|
131
|
+
const [longitude, latitude] = resolveMapPosition(
|
|
132
|
+
{ longitude: longitudeProp, latitude: latitudeProp, x, y },
|
|
133
|
+
plan,
|
|
134
|
+
"MapMarker",
|
|
135
|
+
);
|
|
136
|
+
const planRef = useRef(plan);
|
|
137
|
+
planRef.current = plan;
|
|
138
|
+
|
|
69
139
|
const callbacksRef = useRef({
|
|
70
140
|
onClick,
|
|
71
141
|
onMouseEnter,
|
|
@@ -111,6 +181,12 @@ export function MapMarker({
|
|
|
111
181
|
draggable: initialRef.current.draggable,
|
|
112
182
|
}).setLngLat([initialRef.current.longitude, initialRef.current.latitude]);
|
|
113
183
|
|
|
184
|
+
// c-10: MapLibre gives a marker a tab stop of its own (a popup, a
|
|
185
|
+
// keyboard drag), and a raw element shows the BROWSER's focus ring, which
|
|
186
|
+
// does not follow the theme. The house indicator is a class, so the
|
|
187
|
+
// element wears the same `focus-ring` every other control does.
|
|
188
|
+
markerInstance.getElement()?.classList.add("focus-ring");
|
|
189
|
+
|
|
114
190
|
const handleClick = (e: MouseEvent) => callbacksRef.current.onClick?.(e);
|
|
115
191
|
const handleMouseEnter = (e: MouseEvent) => callbacksRef.current.onMouseEnter?.(e);
|
|
116
192
|
const handleMouseLeave = (e: MouseEvent) => callbacksRef.current.onMouseLeave?.(e);
|
|
@@ -121,15 +197,24 @@ export function MapMarker({
|
|
|
121
197
|
|
|
122
198
|
const handleDragStart = () => {
|
|
123
199
|
const lngLat = markerInstance.getLngLat();
|
|
124
|
-
callbacksRef.current.onDragStart?.(
|
|
200
|
+
callbacksRef.current.onDragStart?.(
|
|
201
|
+
{ lng: lngLat.lng, lat: lngLat.lat },
|
|
202
|
+
toPlanPoint(planRef.current, lngLat),
|
|
203
|
+
);
|
|
125
204
|
};
|
|
126
205
|
const handleDrag = () => {
|
|
127
206
|
const lngLat = markerInstance.getLngLat();
|
|
128
|
-
callbacksRef.current.onDrag?.(
|
|
207
|
+
callbacksRef.current.onDrag?.(
|
|
208
|
+
{ lng: lngLat.lng, lat: lngLat.lat },
|
|
209
|
+
toPlanPoint(planRef.current, lngLat),
|
|
210
|
+
);
|
|
129
211
|
};
|
|
130
212
|
const handleDragEnd = () => {
|
|
131
213
|
const lngLat = markerInstance.getLngLat();
|
|
132
|
-
callbacksRef.current.onDragEnd?.(
|
|
214
|
+
callbacksRef.current.onDragEnd?.(
|
|
215
|
+
{ lng: lngLat.lng, lat: lngLat.lat },
|
|
216
|
+
toPlanPoint(planRef.current, lngLat),
|
|
217
|
+
);
|
|
133
218
|
};
|
|
134
219
|
|
|
135
220
|
markerInstance.on("dragstart", handleDragStart);
|
|
@@ -142,15 +227,16 @@ export function MapMarker({
|
|
|
142
227
|
// Attaches/detaches the CURRENT marker instance to the map — its own effect
|
|
143
228
|
// so both a `map` change and an `anchor`/`className`-driven rebuild above
|
|
144
229
|
// are handled the same way, without a double `remove()`.
|
|
230
|
+
// A marker hidden at this tier (`showAt`) is simply not on the map.
|
|
145
231
|
useEffect(() => {
|
|
146
|
-
if (!map || !marker) return;
|
|
232
|
+
if (!map || !marker || !visible) return;
|
|
147
233
|
|
|
148
234
|
marker.addTo(map);
|
|
149
235
|
|
|
150
236
|
return () => {
|
|
151
237
|
marker.remove();
|
|
152
238
|
};
|
|
153
|
-
}, [map, marker]);
|
|
239
|
+
}, [map, marker, visible]);
|
|
154
240
|
|
|
155
241
|
useEffect(() => {
|
|
156
242
|
if (!marker) return;
|
|
@@ -184,6 +270,88 @@ export function MapMarker({
|
|
|
184
270
|
}
|
|
185
271
|
}, [marker, longitude, latitude, draggable, offset, rotation, rotationAlignment, pitchAlignment]);
|
|
186
272
|
|
|
273
|
+
// c-4 (WCAG 2.1.1): `marker.setDraggable(true)` buys a MOUSE affordance and
|
|
274
|
+
// nothing else — MapLibre gives a marker element a tab stop and a key
|
|
275
|
+
// handler only from inside `setPopup`, so a popup-less draggable marker is
|
|
276
|
+
// not focusable and answers no key. The drag gets its keyboard equivalent
|
|
277
|
+
// here: a real tab stop, a name that says what moves, and arrow keys that
|
|
278
|
+
// step the marker in PIXELS (Shift = a coarser step) through the same
|
|
279
|
+
// project/unproject round trip the pointer drag uses, firing the identical
|
|
280
|
+
// dragstart/drag/dragend payloads.
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
const element = marker?.getElement();
|
|
283
|
+
if (!(draggable && marker && map && element)) return;
|
|
284
|
+
|
|
285
|
+
const hadTabIndex = element.getAttribute("tabindex");
|
|
286
|
+
const hadRole = element.getAttribute("role");
|
|
287
|
+
const hadLabel = element.getAttribute("aria-label");
|
|
288
|
+
if (hadTabIndex === null) element.setAttribute("tabindex", "0");
|
|
289
|
+
if (hadRole === null) element.setAttribute("role", "button");
|
|
290
|
+
// MapLibre's own name is the generic "Map marker"; it says neither what
|
|
291
|
+
// this one moves nor that it can be moved at all. A draggable marker is
|
|
292
|
+
// named for its drag, and the original name goes back on cleanup.
|
|
293
|
+
element.setAttribute("aria-label", dragLabel ?? t("maps.marker.drag"));
|
|
294
|
+
|
|
295
|
+
let dragging = false;
|
|
296
|
+
const position = () => {
|
|
297
|
+
const lngLat = marker.getLngLat();
|
|
298
|
+
return { lng: lngLat.lng, lat: lngLat.lat };
|
|
299
|
+
};
|
|
300
|
+
// The keyboard path reports the plan point exactly as the pointer path
|
|
301
|
+
// does, so a plan consumer cannot tell the two apart.
|
|
302
|
+
const planPoint = () => toPlanPoint(planRef.current, marker.getLngLat());
|
|
303
|
+
const settle = () => {
|
|
304
|
+
if (!dragging) return;
|
|
305
|
+
dragging = false;
|
|
306
|
+
callbacksRef.current.onDragEnd?.(position(), planPoint());
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
310
|
+
const step = event.shiftKey ? KEYBOARD_DRAG_STEP_COARSE_PX : KEYBOARD_DRAG_STEP_PX;
|
|
311
|
+
const delta = KEYBOARD_DRAG_DELTAS[event.key];
|
|
312
|
+
if (!delta) return;
|
|
313
|
+
event.preventDefault();
|
|
314
|
+
// MapLibre appends the marker element straight into
|
|
315
|
+
// `map.getCanvasContainer()` — the SAME element its own
|
|
316
|
+
// `HandlerManager` listens on for `keydown` (bubble phase, no
|
|
317
|
+
// `defaultPrevented` check). Without this, every arrow key we handle
|
|
318
|
+
// here also reaches MapLibre's built-in keyboard pan handler, which
|
|
319
|
+
// fires an `easeTo` that moves the CAMERA by 100px the opposite way
|
|
320
|
+
// (its `offset` is negated) and — since that `easeTo` isn't marked
|
|
321
|
+
// `essential` — collapses to an instant jump under reduced motion but
|
|
322
|
+
// animates over 300ms otherwise, so repeated presses compose
|
|
323
|
+
// differently per motion setting. Stopping propagation keeps the key
|
|
324
|
+
// entirely inside this handler: only `marker.setLngLat` moves, the
|
|
325
|
+
// camera never does.
|
|
326
|
+
event.stopPropagation();
|
|
327
|
+
if (!dragging) {
|
|
328
|
+
dragging = true;
|
|
329
|
+
callbacksRef.current.onDragStart?.(position(), planPoint());
|
|
330
|
+
}
|
|
331
|
+
const point = map.project(marker.getLngLat());
|
|
332
|
+
const next = map.unproject([point.x + delta[0] * step, point.y + delta[1] * step]);
|
|
333
|
+
marker.setLngLat(next);
|
|
334
|
+
callbacksRef.current.onDrag?.(position(), planPoint());
|
|
335
|
+
};
|
|
336
|
+
const handleKeyUp = (event: KeyboardEvent) => {
|
|
337
|
+
if (KEYBOARD_DRAG_DELTAS[event.key]) settle();
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
element.addEventListener("keydown", handleKeyDown);
|
|
341
|
+
element.addEventListener("keyup", handleKeyUp);
|
|
342
|
+
element.addEventListener("blur", settle);
|
|
343
|
+
|
|
344
|
+
return () => {
|
|
345
|
+
element.removeEventListener("keydown", handleKeyDown);
|
|
346
|
+
element.removeEventListener("keyup", handleKeyUp);
|
|
347
|
+
element.removeEventListener("blur", settle);
|
|
348
|
+
if (hadTabIndex === null) element.removeAttribute("tabindex");
|
|
349
|
+
if (hadRole === null) element.removeAttribute("role");
|
|
350
|
+
if (hadLabel === null) element.removeAttribute("aria-label");
|
|
351
|
+
else element.setAttribute("aria-label", hadLabel);
|
|
352
|
+
};
|
|
353
|
+
}, [draggable, dragLabel, map, marker, t]);
|
|
354
|
+
|
|
187
355
|
// No marker yet (first client tick after mount, or the moment an
|
|
188
356
|
// `anchor`/`className` rebuild is in flight) — render nothing rather than
|
|
189
357
|
// a sub-component (`MapMarkerContent` et al.) reaching into a null marker,
|
|
@@ -192,7 +360,148 @@ export function MapMarker({
|
|
|
192
360
|
return null;
|
|
193
361
|
}
|
|
194
362
|
|
|
195
|
-
return
|
|
363
|
+
return (
|
|
364
|
+
<MarkerContext.Provider value={{ marker, map }}>
|
|
365
|
+
{children}
|
|
366
|
+
{label && <MarkerOwnLabel label={label} />}
|
|
367
|
+
</MarkerContext.Provider>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** One arrow-key press of a keyboard drag, in screen px (c-4). */
|
|
372
|
+
const KEYBOARD_DRAG_STEP_PX = 8;
|
|
373
|
+
/** The same press with Shift held — a coarser step across the map. */
|
|
374
|
+
const KEYBOARD_DRAG_STEP_COARSE_PX = 40;
|
|
375
|
+
/** Arrow key → screen-space direction. `y` grows downward, as `map.project` does. */
|
|
376
|
+
const KEYBOARD_DRAG_DELTAS: Record<string, [number, number] | undefined> = {
|
|
377
|
+
ArrowLeft: [-1, 0],
|
|
378
|
+
ArrowRight: [1, 0],
|
|
379
|
+
ArrowUp: [0, -1],
|
|
380
|
+
ArrowDown: [0, 1],
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const LABEL_GAP = 10;
|
|
384
|
+
const CALLOUT_DISTANCE = 28;
|
|
385
|
+
/** How close to the map's edge a clamped label may sit, in px. */
|
|
386
|
+
const LABEL_EDGE_PAD = 4;
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Keep a marker's label inside the map box (c-7 / c-8).
|
|
390
|
+
*
|
|
391
|
+
* A label is a pure offset from its marker, so at the edge of a static map it
|
|
392
|
+
* was painted half outside the box and clipped — "bottom-righ", "…urg Gate" —
|
|
393
|
+
* with no way to pan it back into view. This is the pass `MapAnnotation`
|
|
394
|
+
* already makes: measure the painted box, clamp it into the map's rectangle
|
|
395
|
+
* with `clampShift`, and write the correction to the CSS `translate` property,
|
|
396
|
+
* which composes with the label's own `transform` instead of replacing it.
|
|
397
|
+
*
|
|
398
|
+
* A label whose own point has left the box is hidden rather than pinned to the
|
|
399
|
+
* edge: a name parked at the border beside no marker claims a place the reader
|
|
400
|
+
* cannot see. Skipped while the map box measures 0 (jsdom, first paint).
|
|
401
|
+
*/
|
|
402
|
+
function useLabelInsideMap(
|
|
403
|
+
element: HTMLElement | null,
|
|
404
|
+
map: MapLibreGL.Map | null,
|
|
405
|
+
marker: MapLibreGL.Marker,
|
|
406
|
+
onShift?: (sx: number, sy: number) => void,
|
|
407
|
+
) {
|
|
408
|
+
const onShiftRef = useRef(onShift);
|
|
409
|
+
onShiftRef.current = onShift;
|
|
410
|
+
|
|
411
|
+
useLayoutEffect(() => {
|
|
412
|
+
if (!element || !map) return undefined;
|
|
413
|
+
const container = map.getContainer();
|
|
414
|
+
|
|
415
|
+
const update = () => {
|
|
416
|
+
const frame = container.getBoundingClientRect();
|
|
417
|
+
if (frame.width === 0 || frame.height === 0) return;
|
|
418
|
+
|
|
419
|
+
const point = map.project(marker.getLngLat());
|
|
420
|
+
const inside =
|
|
421
|
+
point.x >= 0 && point.y >= 0 && point.x <= frame.width && point.y <= frame.height;
|
|
422
|
+
element.style.visibility = inside ? "" : "hidden";
|
|
423
|
+
if (!inside) return;
|
|
424
|
+
|
|
425
|
+
// Measure the label where it would sit unshifted, then re-apply.
|
|
426
|
+
element.style.translate = "";
|
|
427
|
+
const box = element.getBoundingClientRect();
|
|
428
|
+
const { sx, sy } = clampShift(
|
|
429
|
+
box.left - frame.left,
|
|
430
|
+
box.top - frame.top,
|
|
431
|
+
box.width,
|
|
432
|
+
box.height,
|
|
433
|
+
frame.width,
|
|
434
|
+
frame.height,
|
|
435
|
+
LABEL_EDGE_PAD,
|
|
436
|
+
);
|
|
437
|
+
element.style.translate = sx === 0 && sy === 0 ? "" : `${sx}px ${sy}px`;
|
|
438
|
+
onShiftRef.current?.(sx, sy);
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
update();
|
|
442
|
+
map.on("move", update);
|
|
443
|
+
map.on("resize", update);
|
|
444
|
+
const observer =
|
|
445
|
+
typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => update());
|
|
446
|
+
observer?.observe(element);
|
|
447
|
+
return () => {
|
|
448
|
+
map.off("move", update);
|
|
449
|
+
map.off("resize", update);
|
|
450
|
+
observer?.disconnect();
|
|
451
|
+
};
|
|
452
|
+
}, [element, map, marker]);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** The `label` prop, portaled into the marker element and centred on the point. */
|
|
456
|
+
function MarkerOwnLabel({ label }: { label: MapMarkerLabelSpec }) {
|
|
457
|
+
const { marker, map } = useMarkerContext();
|
|
458
|
+
const { text, position = "top", box = false, callout = false } = label;
|
|
459
|
+
const distance = callout ? CALLOUT_DISTANCE : LABEL_GAP;
|
|
460
|
+
const { dx, dy } = mapAnchorGeometry(position, distance);
|
|
461
|
+
const [labelEl, setLabelEl] = useState<HTMLDivElement | null>(null);
|
|
462
|
+
const lineRef = useRef<SVGLineElement | null>(null);
|
|
463
|
+
|
|
464
|
+
// A clamped label pulls its callout line's end along with it, so the line
|
|
465
|
+
// still meets the box it points at.
|
|
466
|
+
useLabelInsideMap(labelEl, map, marker, (sx, sy) => {
|
|
467
|
+
const line = lineRef.current;
|
|
468
|
+
if (!line) return;
|
|
469
|
+
line.setAttribute("x2", String(dx + sx));
|
|
470
|
+
line.setAttribute("y2", String(dy + sy));
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
return createPortal(
|
|
474
|
+
<>
|
|
475
|
+
{callout && (
|
|
476
|
+
<svg
|
|
477
|
+
aria-hidden="true"
|
|
478
|
+
data-slot="map-marker-callout"
|
|
479
|
+
className="pointer-events-none absolute overflow-visible text-foreground"
|
|
480
|
+
// The marker element is centred on the point; draw from its centre.
|
|
481
|
+
style={{ left: "50%", top: "50%" }}
|
|
482
|
+
width={1}
|
|
483
|
+
height={1}
|
|
484
|
+
>
|
|
485
|
+
<line ref={lineRef} x1={0} y1={0} x2={dx} y2={dy} stroke="currentColor" strokeWidth={1} />
|
|
486
|
+
</svg>
|
|
487
|
+
)}
|
|
488
|
+
<div
|
|
489
|
+
ref={setLabelEl}
|
|
490
|
+
data-slot="map-marker-label"
|
|
491
|
+
data-position={position}
|
|
492
|
+
className={cn(
|
|
493
|
+
"pointer-events-none absolute whitespace-nowrap text-meta font-medium text-foreground",
|
|
494
|
+
box && "rounded-sm bg-background/90 px-1 py-px",
|
|
495
|
+
)}
|
|
496
|
+
// Geographic placement: physical offsets from the point in every
|
|
497
|
+
// writing direction (east stays east), like MapLibre's own anchors.
|
|
498
|
+
style={{ left: "50%", top: "50%", transform: mapAnchorTransform(position, distance) }}
|
|
499
|
+
>
|
|
500
|
+
{text}
|
|
501
|
+
</div>
|
|
502
|
+
</>,
|
|
503
|
+
marker.getElement(),
|
|
504
|
+
);
|
|
196
505
|
}
|
|
197
506
|
|
|
198
507
|
export interface MapMarkerContentProps {
|
|
@@ -330,17 +639,36 @@ export function MapMarkerTooltip({ children, className, ...popupOptions }: MapMa
|
|
|
330
639
|
|
|
331
640
|
tooltip.setDOMContent(container);
|
|
332
641
|
|
|
333
|
-
const
|
|
642
|
+
const show = () => {
|
|
334
643
|
tooltip.setLngLat(marker.getLngLat()).addTo(map);
|
|
335
644
|
};
|
|
336
|
-
const
|
|
645
|
+
const hide = () => tooltip.remove();
|
|
646
|
+
// WCAG 1.4.13: a tooltip that only answers a pointer is unreachable, and
|
|
647
|
+
// one that cannot be dismissed without moving the pointer traps the view.
|
|
648
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
649
|
+
if (event.key === "Escape") hide();
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const element = marker.getElement();
|
|
653
|
+
// MapLibre only makes a marker focusable inside `setPopup`, so a
|
|
654
|
+
// tooltip-only marker has to ask for the tab stop itself. The original
|
|
655
|
+
// value is put back on cleanup, so a marker that had one keeps it.
|
|
656
|
+
const hadTabIndex = element?.getAttribute("tabindex") ?? null;
|
|
657
|
+
if (element && hadTabIndex === null) element.setAttribute("tabindex", "0");
|
|
337
658
|
|
|
338
|
-
|
|
339
|
-
|
|
659
|
+
element?.addEventListener("mouseenter", show);
|
|
660
|
+
element?.addEventListener("mouseleave", hide);
|
|
661
|
+
element?.addEventListener("focus", show);
|
|
662
|
+
element?.addEventListener("blur", hide);
|
|
663
|
+
element?.addEventListener("keydown", handleKeyDown);
|
|
340
664
|
|
|
341
665
|
return () => {
|
|
342
|
-
|
|
343
|
-
|
|
666
|
+
element?.removeEventListener("mouseenter", show);
|
|
667
|
+
element?.removeEventListener("mouseleave", hide);
|
|
668
|
+
element?.removeEventListener("focus", show);
|
|
669
|
+
element?.removeEventListener("blur", hide);
|
|
670
|
+
element?.removeEventListener("keydown", handleKeyDown);
|
|
671
|
+
if (element && hadTabIndex === null) element.removeAttribute("tabindex");
|
|
344
672
|
tooltip.remove();
|
|
345
673
|
};
|
|
346
674
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- tooltip/marker/container are stable
|
|
@@ -384,14 +712,23 @@ export interface MapMarkerLabelProps {
|
|
|
384
712
|
* sibling composition rendering the label against the map container instead).
|
|
385
713
|
*/
|
|
386
714
|
export function MapMarkerLabel({ children, className, position = "top" }: MapMarkerLabelProps) {
|
|
387
|
-
const { marker } = useMarkerContext();
|
|
715
|
+
const { marker, map } = useMarkerContext();
|
|
716
|
+
const [labelEl, setLabelEl] = useState<HTMLDivElement | null>(null);
|
|
388
717
|
const positionClasses = {
|
|
389
718
|
top: "bottom-full mb-1",
|
|
390
719
|
bottom: "top-full mt-1",
|
|
391
720
|
};
|
|
392
721
|
|
|
722
|
+
// Same edge clamp as the `label` prop's own label (c-7 / c-8): a composed
|
|
723
|
+
// label is a plain offset from the marker too, so at the map's edge it was
|
|
724
|
+
// painted outside the box and clipped.
|
|
725
|
+
useLabelInsideMap(labelEl, map, marker);
|
|
726
|
+
|
|
393
727
|
return createPortal(
|
|
394
728
|
<div
|
|
729
|
+
ref={setLabelEl}
|
|
730
|
+
data-slot="map-marker-label"
|
|
731
|
+
data-position={position}
|
|
395
732
|
className={cn(
|
|
396
733
|
"absolute left-1/2 -translate-x-1/2 whitespace-nowrap",
|
|
397
734
|
"text-meta font-medium text-foreground",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MapNorthArrow, type MapNorthArrowProps } from "./map-north-arrow";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, waitFor } from "storybook/test";
|
|
3
|
+
|
|
4
|
+
import { MapCanvas } from "../map-canvas";
|
|
5
|
+
import { MapNorthArrow } from "./map-north-arrow";
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "Maps/MapNorthArrow",
|
|
9
|
+
component: MapNorthArrow,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
parameters: { layout: "fullscreen" },
|
|
12
|
+
} satisfies Meta<typeof MapNorthArrow>;
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof meta>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A rotated map (bearing 30°) shows the arrow, turned to point at true north.
|
|
18
|
+
* On a north-up map the arrow stays hidden — it would only state the default.
|
|
19
|
+
*/
|
|
20
|
+
export const Default: Story = {
|
|
21
|
+
render: (args) => (
|
|
22
|
+
<div className="w-full p-4">
|
|
23
|
+
<MapCanvas interactive={false} center={[-79.38, 43.65]} zoom={9} bearing={30}>
|
|
24
|
+
<MapNorthArrow {...args} />
|
|
25
|
+
</MapCanvas>
|
|
26
|
+
</div>
|
|
27
|
+
),
|
|
28
|
+
play: async ({ canvasElement }) => {
|
|
29
|
+
if (!canvasElement.querySelector(".maplibregl-canvas")) return;
|
|
30
|
+
await waitFor(() => {
|
|
31
|
+
const arrow = canvasElement.querySelector<HTMLElement>('[data-slot="map-north-arrow"]');
|
|
32
|
+
expect(arrow).not.toBeNull();
|
|
33
|
+
expect(arrow!.hidden).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** North-up: nothing to show. */
|
|
39
|
+
export const NorthUp: Story = {
|
|
40
|
+
render: () => (
|
|
41
|
+
<div className="w-full p-4">
|
|
42
|
+
<MapCanvas interactive={false} center={[-79.38, 43.65]} zoom={9}>
|
|
43
|
+
<MapNorthArrow />
|
|
44
|
+
</MapCanvas>
|
|
45
|
+
</div>
|
|
46
|
+
),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** In another corner, with a translated name. */
|
|
50
|
+
export const Positioned: Story = {
|
|
51
|
+
args: { position: "bottom-right", label: "Norden" },
|
|
52
|
+
render: Default.render,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** The remaining corners. */
|
|
56
|
+
export const OtherCorners: Story = {
|
|
57
|
+
render: () => (
|
|
58
|
+
<div className="w-full p-4">
|
|
59
|
+
<MapCanvas interactive={false} center={[-79.38, 43.65]} zoom={9} bearing={-45}>
|
|
60
|
+
<MapNorthArrow position="top-right" />
|
|
61
|
+
<MapNorthArrow position="bottom-left" />
|
|
62
|
+
</MapCanvas>
|
|
63
|
+
</div>
|
|
64
|
+
),
|
|
65
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { act, cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
vi.mock("maplibre-gl", async () => {
|
|
5
|
+
const { createMaplibreMock } = await import("../test-utils/maplibre-mock");
|
|
6
|
+
return createMaplibreMock();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
import { MockMap, resetMaplibreMock } from "../test-utils/maplibre-mock";
|
|
10
|
+
import { MapCanvas } from "../map-canvas";
|
|
11
|
+
import { MapNorthArrow } from "./map-north-arrow";
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
cleanup();
|
|
15
|
+
resetMaplibreMock();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("MapNorthArrow", () => {
|
|
19
|
+
it("stays away on a north-up map and appears once the map rotates", async () => {
|
|
20
|
+
render(
|
|
21
|
+
<MapCanvas>
|
|
22
|
+
<MapNorthArrow />
|
|
23
|
+
</MapCanvas>,
|
|
24
|
+
);
|
|
25
|
+
const arrow = await waitFor(() => {
|
|
26
|
+
const el = document.querySelector('[data-slot="map-north-arrow"]');
|
|
27
|
+
expect(el).not.toBeNull();
|
|
28
|
+
return el!;
|
|
29
|
+
});
|
|
30
|
+
expect(arrow).toHaveAttribute("hidden");
|
|
31
|
+
act(() => {
|
|
32
|
+
MockMap.instances[0]!.bearing = 30;
|
|
33
|
+
MockMap.instances[0]!.emit("rotate");
|
|
34
|
+
});
|
|
35
|
+
expect(arrow).not.toHaveAttribute("hidden");
|
|
36
|
+
expect(screen.getByRole("img", { name: "North" })).toBeVisible();
|
|
37
|
+
expect(arrow.querySelector("svg")!.style.transform).toBe("rotate(-30deg)");
|
|
38
|
+
});
|
|
39
|
+
});
|