@defra/interactive-map 0.0.43-alpha → 0.0.44-alpha
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/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api.md +16 -8
- package/docs/plugins/draw.md +1 -1
- package/package.json +1 -1
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/index.js +1 -1
- package/plugins/datasets/dist/css/index.css +1 -1
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/src/components/LayersMenu/Layers.module.scss +1 -0
- package/plugins/datasets/src/initialise/DatasetsInit.jsx +2 -2
- package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +6 -5
- package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
- package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/esm/index.js +1 -1
- package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/umd/index.js +1 -1
- package/plugins/draw/src/DrawInit.jsx +31 -26
- package/plugins/draw/src/DrawInit.test.jsx +55 -12
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +113 -114
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +82 -9
- package/plugins/draw/src/adapters/maplibre/drawEvents.js +3 -1
- package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +18 -9
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +9 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +11 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +8 -2
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +35 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +29 -19
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +17 -2
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +35 -29
- package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +24 -6
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +22 -20
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +2 -2
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +3 -3
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +31 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +8 -1
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +38 -30
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +20 -3
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +5 -5
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +2 -2
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +2 -2
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +3 -3
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -7
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +4 -4
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +14 -14
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +12 -32
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +10 -10
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +48 -22
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +74 -6
- package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +2 -2
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +6 -0
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +9 -0
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +15 -2
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +27 -1
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +1 -1
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +31 -17
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +55 -1
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +13 -7
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +7 -15
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +36 -4
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +41 -1
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +35 -25
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +40 -2
- package/plugins/draw/src/adapters/openlayers/edit/nudge.js +2 -2
- package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +2 -2
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +32 -57
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +14 -13
- package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +1 -1
- package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +17 -1
- package/plugins/draw/src/events.js +4 -4
- package/plugins/draw/src/events.test.js +1 -1
- package/plugins/draw/src/hooks/useSpatialList.js +180 -0
- package/plugins/draw/src/hooks/useSpatialList.test.js +339 -0
- package/plugins/draw/src/utils/spatial.js +1 -31
- package/plugins/draw/src/utils/spatial.test.js +1 -24
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/index.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +2 -2
- package/plugins/interact/src/InteractInit.test.js +3 -3
- package/plugins/interact/src/events.js +5 -0
- package/plugins/interact/src/hooks/useAttachEvents.js +20 -0
- package/plugins/interact/src/hooks/useAttachEvents.test.js +44 -1
- package/plugins/interact/src/hooks/useCrossHairVisibility.js +4 -4
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +7 -7
- package/plugins/interact/src/hooks/{useMapItemList.js → useSpatialList.js} +87 -52
- package/plugins/interact/src/hooks/{useMapItemList.test.js → useSpatialList.test.js} +152 -71
- package/plugins/interact/src/manifest.js +12 -1
- package/plugins/interact/src/manifest.test.js +20 -0
- package/plugins/interact/src/utils/spatial.js +1 -1
- package/providers/beta/openlayers/src/utils/queryFeatures.js +1 -1
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/utils/labels.js +2 -2
- package/providers/maplibre/src/utils/labels.test.js +5 -5
- package/providers/maplibre/src/utils/spatial.js +0 -57
- package/providers/maplibre/src/utils/spatial.test.js +0 -27
- package/src/App/components/CrossHair/CrossHair.jsx +10 -4
- package/src/App/components/CrossHair/CrossHair.module.scss +16 -12
- package/src/App/components/CrossHair/CrossHair.test.jsx +9 -4
- package/src/App/components/Hints/Hints.jsx +3 -3
- package/src/App/components/Hints/Hints.test.jsx +3 -3
- package/src/App/components/MapButton/MapButton.module.scss +1 -2
- package/src/App/components/{MoveControls/MoveControls.jsx → MapControls/MapControls.jsx} +10 -10
- package/src/App/components/{MoveControls/MoveControls.module.scss → MapControls/MapControls.module.scss} +25 -25
- package/src/App/components/{MoveControls/MoveControls.test.jsx → MapControls/MapControls.test.jsx} +48 -48
- package/src/App/components/Markers/Markers.jsx +2 -2
- package/src/App/components/Markers/Markers.test.jsx +3 -3
- package/src/App/components/Markers/SymbolMarker.test.jsx +2 -2
- package/src/App/components/Viewport/MapStatus.jsx +1 -1
- package/src/App/components/Viewport/SpatialList.jsx +47 -0
- package/src/App/components/Viewport/{Features.module.scss → SpatialList.module.scss} +3 -3
- package/src/App/components/Viewport/{Features.test.jsx → SpatialList.test.jsx} +60 -43
- package/src/App/components/Viewport/Viewport.jsx +20 -10
- package/src/App/components/Viewport/Viewport.test.jsx +18 -2
- package/src/App/hooks/useCrossHairAPI.js +5 -5
- package/src/App/hooks/useCrossHairAPI.test.js +5 -5
- package/src/App/hooks/useHintsAPI.js +5 -5
- package/src/App/hooks/useSpatialListFocus.js +296 -0
- package/src/App/hooks/{useFeatureFocus.test.js → useSpatialListFocus.test.js} +318 -163
- package/src/App/hooks/useSpatialListItems.js +51 -0
- package/src/App/hooks/useSpatialListItems.test.js +217 -0
- package/src/App/initialiseApp.js +6 -3
- package/src/App/initialiseApp.test.js +11 -0
- package/src/App/registry/spatialListRegistry.js +104 -0
- package/src/App/registry/spatialListRegistry.test.js +218 -0
- package/src/App/store/AppProvider.jsx +3 -2
- package/src/config/appConfig.js +14 -14
- package/src/config/appConfig.test.js +19 -19
- package/src/config/defaults.js +2 -2
- package/src/config/events.js +8 -8
- package/src/scss/main.scss +2 -2
- package/src/test-utils.js +8 -0
- package/src/types.js +11 -9
- package/src/utils/findNearestItemInDirection.js +29 -0
- package/src/utils/findNearestItemInDirection.test.js +67 -0
- package/src/utils/globalAltShortcuts.js +12 -0
- package/src/utils/globalAltShortcuts.test.js +27 -0
- package/src/utils/spatialNavigate.js +49 -0
- package/src/utils/spatialNavigate.test.js +42 -0
- package/src/App/components/Viewport/Features.jsx +0 -49
- package/src/App/hooks/useFeatureFocus.js +0 -257
- package/src/App/hooks/useFeatureItems.js +0 -39
- package/src/App/hooks/useFeatureItems.test.js +0 -155
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import { renderHook, act } from '@testing-library/react'
|
|
2
|
-
import {
|
|
2
|
+
import { useSpatialList } from './useSpatialList.js'
|
|
3
|
+
import { createSpatialListRegistry } from '../../../../src/App/registry/spatialListRegistry.js'
|
|
3
4
|
|
|
4
5
|
const MARKER_LABEL = 'Marker One'
|
|
5
6
|
|
|
6
7
|
const MOVE_END = 'map:moveend'
|
|
7
8
|
const DATA_CHANGE = 'map:datachange'
|
|
8
|
-
const SET_FEATURES = 'map:
|
|
9
|
-
const SET_ACTIVE = 'map:
|
|
10
|
-
const CONFIRM = 'map:
|
|
9
|
+
const SET_FEATURES = 'map:setspatiallist'
|
|
10
|
+
const SET_ACTIVE = 'map:setactiveitem'
|
|
11
|
+
const CONFIRM = 'map:selectitem'
|
|
12
|
+
|
|
13
|
+
// Items now carry more than id/label/x/y (isMarker, and for features: featureId/layerId/
|
|
14
|
+
// idProperty/geometry/properties) — see useSpatialList.js's own comment on why. These two
|
|
15
|
+
// helpers assert only the fields each test actually cares about, rather than retyping the full
|
|
16
|
+
// object everywhere a marker/feature item shows up.
|
|
17
|
+
const markerItem = (overrides) => expect.objectContaining({ isMarker: true, ...overrides })
|
|
18
|
+
const featureItem = (overrides) => expect.objectContaining({ isMarker: false, ...overrides })
|
|
11
19
|
|
|
12
20
|
const makeEventBus = () => {
|
|
13
21
|
const listeners = {}
|
|
14
22
|
return {
|
|
15
|
-
on: jest.fn((
|
|
23
|
+
on: jest.fn((eventName, fn) => { listeners[eventName] = fn }),
|
|
16
24
|
off: jest.fn(),
|
|
17
|
-
emit: jest.fn((
|
|
25
|
+
emit: jest.fn((eventName, payload) => listeners[eventName]?.(payload))
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
28
|
|
|
@@ -50,23 +58,70 @@ const setup = ({ interactionModes = [], markers, layers = [], mapProvider, event
|
|
|
50
58
|
const eb = eventBus ?? makeEventBus()
|
|
51
59
|
const mp = mapProvider ?? makeMapProvider()
|
|
52
60
|
const dp = dispatch ?? jest.fn()
|
|
53
|
-
|
|
61
|
+
// A real registry, not a mock — with only interact registered (additive, the default) it
|
|
62
|
+
// behaves exactly like the old direct-emit code did (same items, same multiselectable), so
|
|
63
|
+
// every existing eb.emit(SET_FEATURES, ...) assertion below still holds unchanged. The
|
|
64
|
+
// registry's own aggregation/exclusive-claim logic is covered separately, in
|
|
65
|
+
// spatialListRegistry.test.js.
|
|
66
|
+
const spatialListRegistry = createSpatialListRegistry({ eventBus: eb })
|
|
67
|
+
const { result, unmount } = renderHook(() => useSpatialList({
|
|
54
68
|
mapState: { markers: markers ?? makeMarkers(), mapSize },
|
|
55
69
|
pluginState: { interactionModes, layers, dispatch: dp, multiSelect },
|
|
56
70
|
services: { eventBus: eb },
|
|
57
|
-
mapProvider: mp
|
|
71
|
+
mapProvider: mp,
|
|
72
|
+
spatialListRegistry
|
|
58
73
|
}))
|
|
59
|
-
return { eb, mp, dp, result, unmount }
|
|
74
|
+
return { eb, mp, dp, result, unmount, spatialListRegistry }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A marker "matching" an id now requires it to actually have made it into the built item list
|
|
78
|
+
// (visible, labelled, in-viewport) — not just exist in markers.items — since useActiveItemHandler
|
|
79
|
+
// resolves purely by looking the id up in that same list. This helper builds one that will.
|
|
80
|
+
const setupVisibleMarker = (overrides = {}) => {
|
|
81
|
+
const { el, container } = makeMarkerEl({ inViewport: true })
|
|
82
|
+
const markers = makeMarkers([{ id: 'm1', label: 'Marker', symbol: 'pin', isVisible: true, ...overrides }])
|
|
83
|
+
markers.markerRefs.set('m1', el)
|
|
84
|
+
return { markers, container }
|
|
60
85
|
}
|
|
61
86
|
|
|
62
|
-
// ───
|
|
87
|
+
// ─── useSpatialList — lifecycle ──────────────────────────────────────────
|
|
63
88
|
|
|
64
|
-
describe('
|
|
89
|
+
describe('useSpatialList — lifecycle', () => {
|
|
65
90
|
it('subscribes to map:moveend on mount', () => {
|
|
66
91
|
const { eb } = setup()
|
|
67
92
|
expect(eb.on).toHaveBeenCalledWith(MOVE_END, expect.any(Function))
|
|
68
93
|
})
|
|
69
94
|
|
|
95
|
+
it('registers an "interact" item provider with spatialListRegistry on mount', () => {
|
|
96
|
+
const eb = makeEventBus()
|
|
97
|
+
const spatialListRegistry = createSpatialListRegistry({ eventBus: eb })
|
|
98
|
+
const registerSpy = jest.spyOn(spatialListRegistry, 'registerItemProvider')
|
|
99
|
+
const { unmount } = renderHook(() => useSpatialList({
|
|
100
|
+
mapState: { markers: makeMarkers(), mapSize: 'small' },
|
|
101
|
+
pluginState: { interactionModes: [], layers: [], dispatch: jest.fn(), multiSelect: false },
|
|
102
|
+
services: { eventBus: eb },
|
|
103
|
+
mapProvider: makeMapProvider(),
|
|
104
|
+
spatialListRegistry
|
|
105
|
+
}))
|
|
106
|
+
expect(registerSpy).toHaveBeenCalledWith('interact', { getItems: expect.any(Function) })
|
|
107
|
+
unmount()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('unregisters the "interact" item provider on unmount', () => {
|
|
111
|
+
const { spatialListRegistry, unmount } = setup()
|
|
112
|
+
const unregisterSpy = jest.spyOn(spatialListRegistry, 'unregisterItemProvider')
|
|
113
|
+
unmount()
|
|
114
|
+
expect(unregisterSpy).toHaveBeenCalledWith('interact')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('does not re-register the provider just because the map moves — it registers once for the hook\'s lifetime', () => {
|
|
118
|
+
const { eb, spatialListRegistry } = setup({ interactionModes: ['selectMarker'] })
|
|
119
|
+
const registerSpy = jest.spyOn(spatialListRegistry, 'registerItemProvider')
|
|
120
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
121
|
+
act(() => eb.emit(DATA_CHANGE, {}))
|
|
122
|
+
expect(registerSpy).not.toHaveBeenCalled()
|
|
123
|
+
})
|
|
124
|
+
|
|
70
125
|
it('subscribes to map:datachange on mount', () => {
|
|
71
126
|
const { eb } = setup()
|
|
72
127
|
expect(eb.on).toHaveBeenCalledWith(DATA_CHANGE, expect.any(Function))
|
|
@@ -85,9 +140,9 @@ describe('useMapItemList — lifecycle', () => {
|
|
|
85
140
|
})
|
|
86
141
|
})
|
|
87
142
|
|
|
88
|
-
// ───
|
|
143
|
+
// ─── useSpatialList — initial population ─────────────────────────────────
|
|
89
144
|
|
|
90
|
-
describe('
|
|
145
|
+
describe('useSpatialList — initial population', () => {
|
|
91
146
|
afterEach(() => { document.body.innerHTML = '' })
|
|
92
147
|
|
|
93
148
|
it('emits visible markers immediately on mount without waiting for moveend', () => {
|
|
@@ -98,20 +153,20 @@ describe('useMapItemList — initial population', () => {
|
|
|
98
153
|
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
99
154
|
|
|
100
155
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
101
|
-
items: [{ id: 'm1', label: MARKER_LABEL }], multiselectable: false
|
|
156
|
+
items: [markerItem({ id: 'm1', label: MARKER_LABEL })], multiselectable: false, label: 'Map features'
|
|
102
157
|
})
|
|
103
158
|
container.remove()
|
|
104
159
|
})
|
|
105
160
|
|
|
106
161
|
it('emits empty items immediately when no markers are in viewport', () => {
|
|
107
162
|
const { eb } = setup({ interactionModes: ['selectMarker'] })
|
|
108
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
163
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
109
164
|
})
|
|
110
165
|
})
|
|
111
166
|
|
|
112
|
-
// ───
|
|
167
|
+
// ─── useSpatialList — datachange trigger ─────────────────────────────────
|
|
113
168
|
|
|
114
|
-
describe('
|
|
169
|
+
describe('useSpatialList — datachange trigger', () => {
|
|
115
170
|
afterEach(() => { document.body.innerHTML = '' })
|
|
116
171
|
|
|
117
172
|
it('re-emits items on map:datachange the same as map:moveend', () => {
|
|
@@ -123,15 +178,15 @@ describe('useMapItemList — datachange trigger', () => {
|
|
|
123
178
|
act(() => eb.emit(DATA_CHANGE, {}))
|
|
124
179
|
|
|
125
180
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
126
|
-
items: [{ id: 'm1', label: MARKER_LABEL }], multiselectable: false
|
|
181
|
+
items: [markerItem({ id: 'm1', label: MARKER_LABEL })], multiselectable: false, label: 'Map features'
|
|
127
182
|
})
|
|
128
183
|
container.remove()
|
|
129
184
|
})
|
|
130
185
|
})
|
|
131
186
|
|
|
132
|
-
// ───
|
|
187
|
+
// ─── useSpatialList — selectMarker mode ───────────────────────────────────
|
|
133
188
|
|
|
134
|
-
describe('
|
|
189
|
+
describe('useSpatialList — selectMarker mode', () => {
|
|
135
190
|
afterEach(() => { document.body.innerHTML = '' })
|
|
136
191
|
|
|
137
192
|
it('emits visible markers as items on moveend', () => {
|
|
@@ -143,7 +198,7 @@ describe('useMapItemList — selectMarker mode', () => {
|
|
|
143
198
|
act(() => eb.emit(MOVE_END, {}))
|
|
144
199
|
|
|
145
200
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
146
|
-
items: [{ id: 'm1', label: MARKER_LABEL }], multiselectable: false
|
|
201
|
+
items: [markerItem({ id: 'm1', label: MARKER_LABEL })], multiselectable: false, label: 'Map features'
|
|
147
202
|
})
|
|
148
203
|
container.remove()
|
|
149
204
|
})
|
|
@@ -157,7 +212,7 @@ describe('useMapItemList — selectMarker mode', () => {
|
|
|
157
212
|
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
158
213
|
act(() => eb.emit(MOVE_END, {}))
|
|
159
214
|
|
|
160
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
215
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
161
216
|
el.remove()
|
|
162
217
|
})
|
|
163
218
|
|
|
@@ -169,7 +224,7 @@ describe('useMapItemList — selectMarker mode', () => {
|
|
|
169
224
|
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
170
225
|
act(() => eb.emit(MOVE_END, {}))
|
|
171
226
|
|
|
172
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
227
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
173
228
|
container.remove()
|
|
174
229
|
})
|
|
175
230
|
|
|
@@ -181,7 +236,7 @@ describe('useMapItemList — selectMarker mode', () => {
|
|
|
181
236
|
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
182
237
|
act(() => eb.emit(MOVE_END, {}))
|
|
183
238
|
|
|
184
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
239
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
185
240
|
container.remove()
|
|
186
241
|
})
|
|
187
242
|
|
|
@@ -193,7 +248,7 @@ describe('useMapItemList — selectMarker mode', () => {
|
|
|
193
248
|
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
194
249
|
act(() => eb.emit(MOVE_END, {}))
|
|
195
250
|
|
|
196
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
251
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
197
252
|
container.remove()
|
|
198
253
|
})
|
|
199
254
|
|
|
@@ -209,7 +264,7 @@ describe('useMapItemList — selectMarker mode', () => {
|
|
|
209
264
|
|
|
210
265
|
expect(mp.mapToScreen).toHaveBeenCalledWith([-2.4, 54.5])
|
|
211
266
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
212
|
-
items: [{ id: 'm1', label: MARKER_LABEL, x: 150, y: 300 }], multiselectable: false // scaleFactor.medium = 1.5
|
|
267
|
+
items: [markerItem({ id: 'm1', label: MARKER_LABEL, x: 150, y: 300 })], multiselectable: false, label: 'Map features' // scaleFactor.medium = 1.5
|
|
213
268
|
})
|
|
214
269
|
container.remove()
|
|
215
270
|
})
|
|
@@ -225,15 +280,15 @@ describe('useMapItemList — selectMarker mode', () => {
|
|
|
225
280
|
|
|
226
281
|
expect(mp.mapToScreen).not.toHaveBeenCalled()
|
|
227
282
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
228
|
-
items: [{ id: 'm1', label: MARKER_LABEL }], multiselectable: false
|
|
283
|
+
items: [markerItem({ id: 'm1', label: MARKER_LABEL })], multiselectable: false, label: 'Map features'
|
|
229
284
|
})
|
|
230
285
|
container.remove()
|
|
231
286
|
})
|
|
232
287
|
})
|
|
233
288
|
|
|
234
|
-
// ───
|
|
289
|
+
// ─── useSpatialList — selectFeature mode: label resolution ───────────────
|
|
235
290
|
|
|
236
|
-
describe('
|
|
291
|
+
describe('useSpatialList — selectFeature mode: label resolution', () => {
|
|
237
292
|
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
238
293
|
|
|
239
294
|
it('emits layer features as items on moveend', () => {
|
|
@@ -250,7 +305,9 @@ describe('useMapItemList — selectFeature mode: label resolution', () => {
|
|
|
250
305
|
|
|
251
306
|
expect(mp.getVisibleFeatures).toHaveBeenCalledWith(['roads'])
|
|
252
307
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
253
|
-
items: [{ id: '1', label: 'High Street'
|
|
308
|
+
items: [featureItem({ id: '1', label: 'High Street', featureId: '1', layerId: 'roads', idProperty: 'road_id' })],
|
|
309
|
+
multiselectable: false,
|
|
310
|
+
label: 'Map features'
|
|
254
311
|
})
|
|
255
312
|
})
|
|
256
313
|
|
|
@@ -259,7 +316,7 @@ describe('useMapItemList — selectFeature mode: label resolution', () => {
|
|
|
259
316
|
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: makeMapProvider(features) })
|
|
260
317
|
act(() => eb.emit(MOVE_END, {}))
|
|
261
318
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
262
|
-
items: [{ id: '2', label: '2' }], multiselectable: false
|
|
319
|
+
items: [featureItem({ id: '2', label: '2' })], multiselectable: false, label: 'Map features'
|
|
263
320
|
})
|
|
264
321
|
})
|
|
265
322
|
|
|
@@ -272,7 +329,7 @@ describe('useMapItemList — selectFeature mode: label resolution', () => {
|
|
|
272
329
|
})
|
|
273
330
|
act(() => eb.emit(MOVE_END, {}))
|
|
274
331
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
275
|
-
items: [], multiselectable: false
|
|
332
|
+
items: [], multiselectable: false, label: 'Map features'
|
|
276
333
|
})
|
|
277
334
|
})
|
|
278
335
|
|
|
@@ -281,7 +338,7 @@ describe('useMapItemList — selectFeature mode: label resolution', () => {
|
|
|
281
338
|
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: makeMapProvider(features) })
|
|
282
339
|
act(() => eb.emit(MOVE_END, {}))
|
|
283
340
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
284
|
-
items: [{ id: '42', label: 'Oak Ave' }], multiselectable: false
|
|
341
|
+
items: [featureItem({ id: '42', label: 'Oak Ave' })], multiselectable: false, label: 'Map features'
|
|
285
342
|
})
|
|
286
343
|
})
|
|
287
344
|
|
|
@@ -289,16 +346,16 @@ describe('useMapItemList — selectFeature mode: label resolution', () => {
|
|
|
289
346
|
const features = [{ layer: { id: 'roads' }, properties: { road_name: 'Lost Lane' } }]
|
|
290
347
|
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: makeMapProvider(features) })
|
|
291
348
|
act(() => eb.emit(MOVE_END, {}))
|
|
292
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
349
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
293
350
|
})
|
|
294
351
|
|
|
295
352
|
it('falls back to stringId label when feature has no properties object', () => {
|
|
296
|
-
//
|
|
353
|
+
// feature.properties is undefined → ?. short-circuits → ?? stringId used
|
|
297
354
|
const features = [{ layer: { id: 'roads' }, id: 99 }]
|
|
298
355
|
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: makeMapProvider(features) })
|
|
299
356
|
act(() => eb.emit(MOVE_END, {}))
|
|
300
357
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
301
|
-
items: [{ id: '99', label: '99' }], multiselectable: false
|
|
358
|
+
items: [featureItem({ id: '99', label: '99' })], multiselectable: false, label: 'Map features'
|
|
302
359
|
})
|
|
303
360
|
})
|
|
304
361
|
|
|
@@ -314,7 +371,7 @@ describe('useMapItemList — selectFeature mode: label resolution', () => {
|
|
|
314
371
|
})
|
|
315
372
|
act(() => eb.emit(MOVE_END, {}))
|
|
316
373
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
317
|
-
items: [{ id: '1', label: 'High Street' }], multiselectable: false
|
|
374
|
+
items: [featureItem({ id: '1', label: 'High Street' })], multiselectable: false, label: 'Map features'
|
|
318
375
|
})
|
|
319
376
|
})
|
|
320
377
|
|
|
@@ -332,7 +389,7 @@ describe('useMapItemList — selectFeature mode: label resolution', () => {
|
|
|
332
389
|
|
|
333
390
|
expect(mp.mapToScreen).toHaveBeenCalledWith([2, 1]) // bbox centre of the polygon above
|
|
334
391
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
335
|
-
items: [{ id: '1', label: 'High Street', x: 150, y: 300 }], multiselectable: false // scaleFactor.medium = 1.5
|
|
392
|
+
items: [featureItem({ id: '1', label: 'High Street', x: 150, y: 300 })], multiselectable: false, label: 'Map features' // scaleFactor.medium = 1.5
|
|
336
393
|
})
|
|
337
394
|
})
|
|
338
395
|
|
|
@@ -345,27 +402,27 @@ describe('useMapItemList — selectFeature mode: label resolution', () => {
|
|
|
345
402
|
|
|
346
403
|
expect(mp.mapToScreen).not.toHaveBeenCalled()
|
|
347
404
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
348
|
-
items: [{ id: '1', label: 'High Street' }], multiselectable: false
|
|
405
|
+
items: [featureItem({ id: '1', label: 'High Street' })], multiselectable: false, label: 'Map features'
|
|
349
406
|
})
|
|
350
407
|
})
|
|
351
408
|
})
|
|
352
409
|
|
|
353
|
-
// ───
|
|
410
|
+
// ─── useSpatialList — selectFeature mode: guards ─────────────────────────
|
|
354
411
|
|
|
355
|
-
describe('
|
|
412
|
+
describe('useSpatialList — selectFeature mode: guards', () => {
|
|
356
413
|
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
357
414
|
|
|
358
415
|
it('skips features with no matching layer config', () => {
|
|
359
416
|
const features = [{ layer: { id: 'unknown-layer' }, properties: { road_id: '4' } }]
|
|
360
417
|
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: makeMapProvider(features) })
|
|
361
418
|
act(() => eb.emit(MOVE_END, {}))
|
|
362
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
419
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
363
420
|
})
|
|
364
421
|
|
|
365
422
|
it('emits empty items when layers array is empty', () => {
|
|
366
423
|
const { eb } = setup({ interactionModes: ['selectFeature'], layers: [] })
|
|
367
424
|
act(() => eb.emit(MOVE_END, {}))
|
|
368
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
425
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
369
426
|
})
|
|
370
427
|
|
|
371
428
|
it('includes multiselectable: true in payload when multiSelect is enabled', () => {
|
|
@@ -378,14 +435,14 @@ describe('useMapItemList — selectFeature mode: guards', () => {
|
|
|
378
435
|
})
|
|
379
436
|
act(() => eb.emit(MOVE_END, {}))
|
|
380
437
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
381
|
-
items: [{ id: '1', label: 'High St' }], multiselectable: true
|
|
438
|
+
items: [featureItem({ id: '1', label: 'High St' })], multiselectable: true, label: 'Map features'
|
|
382
439
|
})
|
|
383
440
|
})
|
|
384
441
|
})
|
|
385
442
|
|
|
386
|
-
// ───
|
|
443
|
+
// ─── useSpatialList — combined modes ─────────────────────────────────────
|
|
387
444
|
|
|
388
|
-
describe('
|
|
445
|
+
describe('useSpatialList — combined modes', () => {
|
|
389
446
|
afterEach(() => { document.body.innerHTML = '' })
|
|
390
447
|
|
|
391
448
|
it('emits both markers and features when both modes are active', () => {
|
|
@@ -406,7 +463,9 @@ describe('useMapItemList — combined modes', () => {
|
|
|
406
463
|
act(() => eb.emit(MOVE_END, {}))
|
|
407
464
|
|
|
408
465
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
409
|
-
items: [{ id: 'm1', label: 'A Marker' }, { id: '1', label: 'Main Rd' }],
|
|
466
|
+
items: [markerItem({ id: 'm1', label: 'A Marker' }), featureItem({ id: '1', label: 'Main Rd' })],
|
|
467
|
+
multiselectable: false,
|
|
468
|
+
label: 'Map features'
|
|
410
469
|
})
|
|
411
470
|
container.remove()
|
|
412
471
|
})
|
|
@@ -414,13 +473,13 @@ describe('useMapItemList — combined modes', () => {
|
|
|
414
473
|
it('emits empty items when no interaction modes are active', () => {
|
|
415
474
|
const { eb } = setup({ interactionModes: [] })
|
|
416
475
|
act(() => eb.emit(MOVE_END, {}))
|
|
417
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
476
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
418
477
|
})
|
|
419
478
|
})
|
|
420
479
|
|
|
421
|
-
// ───
|
|
480
|
+
// ─── useSpatialList — map:setactivefeature listener ──────────────────────
|
|
422
481
|
|
|
423
|
-
describe('
|
|
482
|
+
describe('useSpatialList — map:setactivefeature listener', () => {
|
|
424
483
|
afterEach(() => { document.body.innerHTML = '' })
|
|
425
484
|
|
|
426
485
|
it('subscribes to map:setactivefeature on mount and unsubscribes on unmount', () => {
|
|
@@ -437,10 +496,11 @@ describe('useMapItemList — map:setactivefeature listener', () => {
|
|
|
437
496
|
})
|
|
438
497
|
|
|
439
498
|
it('dispatches SET_LISTBOX_ACTIVE null when id matches a marker (ring handled by Markers.jsx)', () => {
|
|
440
|
-
const
|
|
499
|
+
const { markers, container } = setupVisibleMarker()
|
|
441
500
|
const { eb, dp } = setup({ interactionModes: ['selectMarker'], markers })
|
|
442
501
|
act(() => eb.emit(SET_ACTIVE, { id: 'm1' })) // NOSONAR
|
|
443
502
|
expect(dp).toHaveBeenCalledWith({ type: 'SET_LISTBOX_ACTIVE', payload: null })
|
|
503
|
+
container.remove()
|
|
444
504
|
})
|
|
445
505
|
|
|
446
506
|
it('dispatches SET_LISTBOX_ACTIVE with feature payload when id matches a feature', () => {
|
|
@@ -466,9 +526,9 @@ describe('useMapItemList — map:setactivefeature listener', () => {
|
|
|
466
526
|
})
|
|
467
527
|
|
|
468
528
|
it('preserves raw numeric featureId in SET_LISTBOX_ACTIVE payload (MapLibre filter type-strictness)', () => {
|
|
469
|
-
const layers = [{ layerId: 'hedges', idProperty: 'id' }]
|
|
529
|
+
const layers = [{ layerId: 'hedges', idProperty: 'id', labelProperty: 'name' }]
|
|
470
530
|
const features = [
|
|
471
|
-
{ layer: { id: 'hedges' }, properties: { id: 27665979 }, geometry: { type: 'LineString' } }
|
|
531
|
+
{ layer: { id: 'hedges' }, properties: { id: 27665979, name: 'Hedge' }, geometry: { type: 'LineString' } }
|
|
472
532
|
]
|
|
473
533
|
const { eb, dp } = setup({
|
|
474
534
|
interactionModes: ['selectFeature'],
|
|
@@ -487,8 +547,8 @@ describe('useMapItemList — map:setactivefeature listener', () => {
|
|
|
487
547
|
})
|
|
488
548
|
})
|
|
489
549
|
|
|
490
|
-
it('does not dispatch when
|
|
491
|
-
const layers = [{ layerId: 'roads', idProperty: 'road_id' }]
|
|
550
|
+
it('does not dispatch when the id was never part of the built item list', () => {
|
|
551
|
+
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
492
552
|
const { eb, dp } = setup({
|
|
493
553
|
interactionModes: ['selectFeature'],
|
|
494
554
|
layers,
|
|
@@ -498,8 +558,7 @@ describe('useMapItemList — map:setactivefeature listener', () => {
|
|
|
498
558
|
expect(dp).not.toHaveBeenCalled()
|
|
499
559
|
})
|
|
500
560
|
|
|
501
|
-
it('
|
|
502
|
-
// Line 69: getFeatureId returns null for unknown layer → rawId != null is false
|
|
561
|
+
it('does not dispatch for a feature from an unknown layer — it was never built into an item', () => {
|
|
503
562
|
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
504
563
|
const features = [{ layer: { id: 'unknown' }, properties: { some_id: '1' }, geometry: { type: 'Point' } }]
|
|
505
564
|
const { eb, dp } = setup({
|
|
@@ -511,8 +570,7 @@ describe('useMapItemList — map:setactivefeature listener', () => {
|
|
|
511
570
|
expect(dp).not.toHaveBeenCalled()
|
|
512
571
|
})
|
|
513
572
|
|
|
514
|
-
it('does not
|
|
515
|
-
// Line 114: interactionModes.includes('selectFeature') → false → block skipped
|
|
573
|
+
it('does not dispatch for a feature id when interactionModes excludes selectFeature — no feature items were built', () => {
|
|
516
574
|
const features = [{ layer: { id: 'roads' }, properties: { road_id: '1' }, geometry: { type: 'Point' } }]
|
|
517
575
|
const { eb, dp } = setup({
|
|
518
576
|
interactionModes: ['selectMarker'],
|
|
@@ -523,17 +581,38 @@ describe('useMapItemList — map:setactivefeature listener', () => {
|
|
|
523
581
|
expect(dp).not.toHaveBeenCalled()
|
|
524
582
|
})
|
|
525
583
|
|
|
526
|
-
it('does not
|
|
527
|
-
// Line 114: layers.length > 0 → false → block skipped
|
|
584
|
+
it('does not dispatch for a feature id when layers is empty — no feature items were built', () => {
|
|
528
585
|
const { eb, dp } = setup({ interactionModes: ['selectFeature'], layers: [] })
|
|
529
586
|
act(() => eb.emit(SET_ACTIVE, { id: '1' }))
|
|
530
587
|
expect(dp).not.toHaveBeenCalled()
|
|
531
588
|
})
|
|
589
|
+
|
|
590
|
+
it('resolves the active item from the already-built list, without re-querying the map engine', () => {
|
|
591
|
+
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
592
|
+
const features = [
|
|
593
|
+
{ layer: { id: 'roads' }, properties: { road_id: '1', road_name: 'High St' }, geometry: { type: 'Point' } },
|
|
594
|
+
{ layer: { id: 'roads' }, properties: { road_id: '2', road_name: 'Low St' }, geometry: { type: 'Point' } }
|
|
595
|
+
]
|
|
596
|
+
const mp = makeMapProvider(features)
|
|
597
|
+
const { eb, dp } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: mp })
|
|
598
|
+
|
|
599
|
+
expect(mp.getVisibleFeatures).toHaveBeenCalledTimes(1) // once, building the list on mount
|
|
600
|
+
|
|
601
|
+
act(() => eb.emit(SET_ACTIVE, { id: '1' }))
|
|
602
|
+
act(() => eb.emit(SET_ACTIVE, { id: '2' }))
|
|
603
|
+
act(() => eb.emit(SET_ACTIVE, { id: '1' }))
|
|
604
|
+
|
|
605
|
+
expect(mp.getVisibleFeatures).toHaveBeenCalledTimes(1) // still just the once
|
|
606
|
+
expect(dp).toHaveBeenLastCalledWith({
|
|
607
|
+
type: 'SET_LISTBOX_ACTIVE',
|
|
608
|
+
payload: { featureId: '1', layerId: 'roads', idProperty: 'road_id', geometry: { type: 'Point' } }
|
|
609
|
+
})
|
|
610
|
+
})
|
|
532
611
|
})
|
|
533
612
|
|
|
534
|
-
// ───
|
|
613
|
+
// ─── useSpatialList — confirm: lifecycle and guards ──────────────────────
|
|
535
614
|
|
|
536
|
-
describe('
|
|
615
|
+
describe('useSpatialList — confirm: lifecycle and guards', () => {
|
|
537
616
|
it('subscribes to map:confirmfeature on mount and unsubscribes on unmount', () => {
|
|
538
617
|
const { eb, unmount } = setup()
|
|
539
618
|
expect(eb.on).toHaveBeenCalledWith(CONFIRM, expect.any(Function))
|
|
@@ -548,23 +627,24 @@ describe('useMapItemList — confirm: lifecycle and guards', () => {
|
|
|
548
627
|
})
|
|
549
628
|
|
|
550
629
|
it('keeps the active item after confirm so repeated confirms dispatch again', () => {
|
|
551
|
-
const
|
|
630
|
+
const { markers, container } = setupVisibleMarker()
|
|
552
631
|
const { eb, dp } = setup({ interactionModes: ['selectMarker'], markers })
|
|
553
632
|
act(() => eb.emit(SET_ACTIVE, { id: 'm1' }))
|
|
554
633
|
act(() => eb.emit(CONFIRM))
|
|
555
634
|
dp.mockClear()
|
|
556
635
|
act(() => eb.emit(CONFIRM))
|
|
557
636
|
expect(dp).toHaveBeenCalledWith({ type: 'TOGGLE_SELECTED_MARKERS', payload: { markerId: 'm1', multiSelect: false } })
|
|
637
|
+
container.remove()
|
|
558
638
|
})
|
|
559
639
|
})
|
|
560
640
|
|
|
561
|
-
// ───
|
|
641
|
+
// ─── useSpatialList — confirm: dispatches ────────────────────────────────
|
|
562
642
|
|
|
563
|
-
describe('
|
|
643
|
+
describe('useSpatialList — confirm: marker dispatches', () => {
|
|
564
644
|
afterEach(() => { document.body.innerHTML = '' })
|
|
565
645
|
|
|
566
646
|
it('dispatches TOGGLE_SELECTED_MARKERS after activating a marker', () => {
|
|
567
|
-
const
|
|
647
|
+
const { markers, container } = setupVisibleMarker()
|
|
568
648
|
const { eb, dp } = setup({ interactionModes: ['selectMarker'], markers })
|
|
569
649
|
act(() => eb.emit(SET_ACTIVE, { id: 'm1' }))
|
|
570
650
|
act(() => eb.emit(CONFIRM))
|
|
@@ -572,10 +652,11 @@ describe('useMapItemList — confirm: marker dispatches', () => {
|
|
|
572
652
|
type: 'TOGGLE_SELECTED_MARKERS',
|
|
573
653
|
payload: { markerId: 'm1', multiSelect: false }
|
|
574
654
|
})
|
|
655
|
+
container.remove()
|
|
575
656
|
})
|
|
576
657
|
})
|
|
577
658
|
|
|
578
|
-
describe('
|
|
659
|
+
describe('useSpatialList — confirm: feature dispatches', () => {
|
|
579
660
|
it('dispatches TOGGLE_SELECTED_FEATURES after activating a feature', () => {
|
|
580
661
|
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
581
662
|
const features = [
|
|
@@ -622,9 +703,9 @@ describe('useMapItemList — confirm: feature dispatches', () => {
|
|
|
622
703
|
})
|
|
623
704
|
|
|
624
705
|
it('preserves raw numeric featureId in TOGGLE_SELECTED_FEATURES payload', () => {
|
|
625
|
-
const layers = [{ layerId: 'hedges', idProperty: 'id' }]
|
|
706
|
+
const layers = [{ layerId: 'hedges', idProperty: 'id', labelProperty: 'name' }]
|
|
626
707
|
const features = [
|
|
627
|
-
{ layer: { id: 'hedges' }, properties: { id: 27665979 }, geometry: { type: 'LineString' } }
|
|
708
|
+
{ layer: { id: 'hedges' }, properties: { id: 27665979, name: 'Hedge' }, geometry: { type: 'LineString' } }
|
|
628
709
|
]
|
|
629
710
|
const { eb, dp } = setup({
|
|
630
711
|
interactionModes: ['selectFeature'],
|
|
@@ -641,7 +722,7 @@ describe('useMapItemList — confirm: feature dispatches', () => {
|
|
|
641
722
|
replaceAll: true,
|
|
642
723
|
layerId: 'hedges',
|
|
643
724
|
idProperty: 'id',
|
|
644
|
-
properties: { id: 27665979 },
|
|
725
|
+
properties: { id: 27665979, name: 'Hedge' },
|
|
645
726
|
geometry: { type: 'LineString' }
|
|
646
727
|
}
|
|
647
728
|
})
|
|
@@ -9,8 +9,12 @@ import { selectFeature } from './api/selectFeature.js'
|
|
|
9
9
|
import { unselectFeature } from './api/unselectFeature.js'
|
|
10
10
|
import { selectMarker } from './api/selectMarker.js'
|
|
11
11
|
import { unselectMarker } from './api/unselectMarker.js'
|
|
12
|
+
import { isMac } from '../../../src/utils/isMac.js'
|
|
12
13
|
|
|
13
14
|
const SELECT_FEATURES_GROUP = 'Select'
|
|
15
|
+
// Matches the map's own label-navigation shortcut (src/App/controls/keyboardShortcuts.js) —
|
|
16
|
+
// same modifier, same Mac-aware label, different subject depending on what has focus.
|
|
17
|
+
const altKeyHtml = isMac() ? '<kbd>Option</kbd>' : '<kbd>Alt</kbd>'
|
|
14
18
|
|
|
15
19
|
export const manifest = {
|
|
16
20
|
InitComponent: InteractInit,
|
|
@@ -56,9 +60,16 @@ export const manifest = {
|
|
|
56
60
|
id: 'navigateFeatures',
|
|
57
61
|
group: SELECT_FEATURES_GROUP,
|
|
58
62
|
context: 'listbox',
|
|
59
|
-
title: '
|
|
63
|
+
title: 'Highlight next feature',
|
|
60
64
|
command: '<kbd>↑</kbd> or <kbd>↓</kbd>'
|
|
61
65
|
},
|
|
66
|
+
{
|
|
67
|
+
id: 'navigateFeaturesSpatially',
|
|
68
|
+
group: SELECT_FEATURES_GROUP,
|
|
69
|
+
context: 'listbox',
|
|
70
|
+
title: 'Highlight nearby feature',
|
|
71
|
+
command: `${altKeyHtml} + <kbd>↑</kbd>, <kbd>↓</kbd>, <kbd>←</kbd> or <kbd>→</kbd>`
|
|
72
|
+
},
|
|
62
73
|
{
|
|
63
74
|
id: 'selectFeature',
|
|
64
75
|
group: SELECT_FEATURES_GROUP,
|
|
@@ -53,3 +53,23 @@ describe('manifest', () => {
|
|
|
53
53
|
})
|
|
54
54
|
})
|
|
55
55
|
})
|
|
56
|
+
|
|
57
|
+
// altKeyHtml is resolved once at module load (see manifest.js), so exercising both branches
|
|
58
|
+
// needs a fresh require() under each platform value — same approach as
|
|
59
|
+
// src/App/controls/keyboardShortcuts.test.js's own isMac coverage.
|
|
60
|
+
describe('manifest — Alt/Option key label', () => {
|
|
61
|
+
beforeEach(() => jest.resetModules())
|
|
62
|
+
|
|
63
|
+
const loadSpatialShortcut = () =>
|
|
64
|
+
require('./manifest.js').manifest.keyboardShortcuts.find(s => s.id === 'navigateFeaturesSpatially')
|
|
65
|
+
|
|
66
|
+
it('uses Option on Mac', () => {
|
|
67
|
+
Object.defineProperty(navigator, 'platform', { value: 'MacIntel', configurable: true })
|
|
68
|
+
expect(loadSpatialShortcut().command).toContain('<kbd>Option</kbd>')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('uses Alt on non-Mac', () => {
|
|
72
|
+
Object.defineProperty(navigator, 'platform', { value: 'Win32', configurable: true })
|
|
73
|
+
expect(loadSpatialShortcut().command).toContain('<kbd>Alt</kbd>')
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -45,7 +45,7 @@ function isContiguousWithAny (feature, features) {
|
|
|
45
45
|
/**
|
|
46
46
|
* A representative [lng, lat] centre point for a feature's geometry — the centre of its bounding
|
|
47
47
|
* box. Used to position a polygon/line feature's entry in the accessible Features list (see
|
|
48
|
-
*
|
|
48
|
+
* useSpatialList.js) so coordinate-based AT overlays (e.g. macOS Voice Control's Show Numbers)
|
|
49
49
|
* land somewhere on the feature rather than off in a corner of the viewport. Deliberately a
|
|
50
50
|
* bbox centre, not a true centroid/centre-of-mass — works uniformly for polygons and lines with
|
|
51
51
|
* no extra dependency beyond what this file already uses, at the cost of being able to land
|
|
@@ -81,7 +81,7 @@ export const queryFeatures = (map, point, options = {}) => {
|
|
|
81
81
|
* Returns every currently visible feature for the given layer ids, in the same
|
|
82
82
|
* { id, layer: { id }, geometry, properties } shape queryFeatures() above uses — mirrors
|
|
83
83
|
* MapLibre's map.queryRenderedFeatures(undefined, { layers }). Used by the interact plugin's
|
|
84
|
-
* Features list (collectVisibleFeatures in
|
|
84
|
+
* Features list (collectVisibleFeatures in useSpatialList.js) to find selectable/labelled
|
|
85
85
|
* features without requiring a click.
|
|
86
86
|
*
|
|
87
87
|
* VectorTileLayer features come from currently-loaded tiles, the same source
|