@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.
Files changed (159) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/esm/im-shell.js +1 -1
  4. package/dist/umd/im-core.js +1 -1
  5. package/dist/umd/index.js +1 -1
  6. package/docs/api.md +16 -8
  7. package/docs/plugins/draw.md +1 -1
  8. package/package.json +1 -1
  9. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  10. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  11. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  12. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  13. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  14. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  15. package/plugins/datasets/dist/css/index.css +1 -1
  16. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  17. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  18. package/plugins/datasets/src/components/LayersMenu/Layers.module.scss +1 -0
  19. package/plugins/datasets/src/initialise/DatasetsInit.jsx +2 -2
  20. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +6 -5
  21. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  22. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  23. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  24. package/plugins/draw/dist/esm/index.js +1 -1
  25. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -1
  26. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  27. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  28. package/plugins/draw/dist/umd/index.js +1 -1
  29. package/plugins/draw/src/DrawInit.jsx +31 -26
  30. package/plugins/draw/src/DrawInit.test.jsx +55 -12
  31. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +113 -114
  32. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +82 -9
  33. package/plugins/draw/src/adapters/maplibre/drawEvents.js +3 -1
  34. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +1 -1
  35. package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +18 -9
  36. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +9 -1
  37. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +11 -1
  38. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +8 -2
  39. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +35 -0
  40. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +29 -19
  41. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +17 -2
  42. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +1 -1
  43. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +35 -29
  44. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +24 -6
  45. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +22 -20
  46. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +1 -1
  47. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +2 -2
  48. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +3 -3
  49. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +31 -0
  50. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +8 -1
  51. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +38 -30
  52. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +20 -3
  53. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +5 -5
  54. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +2 -2
  55. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +2 -2
  56. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +3 -3
  57. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -7
  58. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +4 -4
  59. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +14 -14
  60. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +12 -32
  61. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +10 -10
  62. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +48 -22
  63. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +74 -6
  64. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +2 -2
  65. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +6 -0
  66. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +9 -0
  67. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +15 -2
  68. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +27 -1
  69. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +1 -1
  70. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +31 -17
  71. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +55 -1
  72. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +13 -7
  73. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +7 -15
  74. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +36 -4
  75. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +41 -1
  76. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +35 -25
  77. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +40 -2
  78. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +2 -2
  79. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +2 -2
  80. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +32 -57
  81. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +14 -13
  82. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +1 -1
  83. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +17 -1
  84. package/plugins/draw/src/events.js +4 -4
  85. package/plugins/draw/src/events.test.js +1 -1
  86. package/plugins/draw/src/hooks/useSpatialList.js +180 -0
  87. package/plugins/draw/src/hooks/useSpatialList.test.js +339 -0
  88. package/plugins/draw/src/utils/spatial.js +1 -31
  89. package/plugins/draw/src/utils/spatial.test.js +1 -24
  90. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  91. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  92. package/plugins/interact/dist/umd/index.js +1 -1
  93. package/plugins/interact/src/InteractInit.jsx +2 -2
  94. package/plugins/interact/src/InteractInit.test.js +3 -3
  95. package/plugins/interact/src/events.js +5 -0
  96. package/plugins/interact/src/hooks/useAttachEvents.js +20 -0
  97. package/plugins/interact/src/hooks/useAttachEvents.test.js +44 -1
  98. package/plugins/interact/src/hooks/useCrossHairVisibility.js +4 -4
  99. package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +7 -7
  100. package/plugins/interact/src/hooks/{useMapItemList.js → useSpatialList.js} +87 -52
  101. package/plugins/interact/src/hooks/{useMapItemList.test.js → useSpatialList.test.js} +152 -71
  102. package/plugins/interact/src/manifest.js +12 -1
  103. package/plugins/interact/src/manifest.test.js +20 -0
  104. package/plugins/interact/src/utils/spatial.js +1 -1
  105. package/providers/beta/openlayers/src/utils/queryFeatures.js +1 -1
  106. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  107. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  108. package/providers/maplibre/dist/umd/index.js +1 -1
  109. package/providers/maplibre/src/utils/labels.js +2 -2
  110. package/providers/maplibre/src/utils/labels.test.js +5 -5
  111. package/providers/maplibre/src/utils/spatial.js +0 -57
  112. package/providers/maplibre/src/utils/spatial.test.js +0 -27
  113. package/src/App/components/CrossHair/CrossHair.jsx +10 -4
  114. package/src/App/components/CrossHair/CrossHair.module.scss +16 -12
  115. package/src/App/components/CrossHair/CrossHair.test.jsx +9 -4
  116. package/src/App/components/Hints/Hints.jsx +3 -3
  117. package/src/App/components/Hints/Hints.test.jsx +3 -3
  118. package/src/App/components/MapButton/MapButton.module.scss +1 -2
  119. package/src/App/components/{MoveControls/MoveControls.jsx → MapControls/MapControls.jsx} +10 -10
  120. package/src/App/components/{MoveControls/MoveControls.module.scss → MapControls/MapControls.module.scss} +25 -25
  121. package/src/App/components/{MoveControls/MoveControls.test.jsx → MapControls/MapControls.test.jsx} +48 -48
  122. package/src/App/components/Markers/Markers.jsx +2 -2
  123. package/src/App/components/Markers/Markers.test.jsx +3 -3
  124. package/src/App/components/Markers/SymbolMarker.test.jsx +2 -2
  125. package/src/App/components/Viewport/MapStatus.jsx +1 -1
  126. package/src/App/components/Viewport/SpatialList.jsx +47 -0
  127. package/src/App/components/Viewport/{Features.module.scss → SpatialList.module.scss} +3 -3
  128. package/src/App/components/Viewport/{Features.test.jsx → SpatialList.test.jsx} +60 -43
  129. package/src/App/components/Viewport/Viewport.jsx +20 -10
  130. package/src/App/components/Viewport/Viewport.test.jsx +18 -2
  131. package/src/App/hooks/useCrossHairAPI.js +5 -5
  132. package/src/App/hooks/useCrossHairAPI.test.js +5 -5
  133. package/src/App/hooks/useHintsAPI.js +5 -5
  134. package/src/App/hooks/useSpatialListFocus.js +296 -0
  135. package/src/App/hooks/{useFeatureFocus.test.js → useSpatialListFocus.test.js} +318 -163
  136. package/src/App/hooks/useSpatialListItems.js +51 -0
  137. package/src/App/hooks/useSpatialListItems.test.js +217 -0
  138. package/src/App/initialiseApp.js +6 -3
  139. package/src/App/initialiseApp.test.js +11 -0
  140. package/src/App/registry/spatialListRegistry.js +104 -0
  141. package/src/App/registry/spatialListRegistry.test.js +218 -0
  142. package/src/App/store/AppProvider.jsx +3 -2
  143. package/src/config/appConfig.js +14 -14
  144. package/src/config/appConfig.test.js +19 -19
  145. package/src/config/defaults.js +2 -2
  146. package/src/config/events.js +8 -8
  147. package/src/scss/main.scss +2 -2
  148. package/src/test-utils.js +8 -0
  149. package/src/types.js +11 -9
  150. package/src/utils/findNearestItemInDirection.js +29 -0
  151. package/src/utils/findNearestItemInDirection.test.js +67 -0
  152. package/src/utils/globalAltShortcuts.js +12 -0
  153. package/src/utils/globalAltShortcuts.test.js +27 -0
  154. package/src/utils/spatialNavigate.js +49 -0
  155. package/src/utils/spatialNavigate.test.js +42 -0
  156. package/src/App/components/Viewport/Features.jsx +0 -49
  157. package/src/App/hooks/useFeatureFocus.js +0 -257
  158. package/src/App/hooks/useFeatureItems.js +0 -39
  159. package/src/App/hooks/useFeatureItems.test.js +0 -155
@@ -1,20 +1,28 @@
1
1
  import { renderHook, act } from '@testing-library/react'
2
- import { useMapItemList } from './useMapItemList.js'
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:setfeatures'
9
- const SET_ACTIVE = 'map:setactivefeature'
10
- const CONFIRM = 'map:selectfeature'
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((e, fn) => { listeners[e] = fn }),
23
+ on: jest.fn((eventName, fn) => { listeners[eventName] = fn }),
16
24
  off: jest.fn(),
17
- emit: jest.fn((e, payload) => listeners[e]?.(payload))
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
- const { result, unmount } = renderHook(() => useMapItemList({
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
- // ─── useMapItemList — lifecycle ──────────────────────────────────────────
87
+ // ─── useSpatialList — lifecycle ──────────────────────────────────────────
63
88
 
64
- describe('useMapItemList — lifecycle', () => {
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
- // ─── useMapItemList — initial population ─────────────────────────────────
143
+ // ─── useSpatialList — initial population ─────────────────────────────────
89
144
 
90
- describe('useMapItemList — initial population', () => {
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
- // ─── useMapItemList — datachange trigger ─────────────────────────────────
167
+ // ─── useSpatialList — datachange trigger ─────────────────────────────────
113
168
 
114
- describe('useMapItemList — datachange trigger', () => {
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
- // ─── useMapItemList — selectMarker mode ───────────────────────────────────
187
+ // ─── useSpatialList — selectMarker mode ───────────────────────────────────
133
188
 
134
- describe('useMapItemList — selectMarker mode', () => {
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
- // ─── useMapItemList — selectFeature mode: label resolution ───────────────
289
+ // ─── useSpatialList — selectFeature mode: label resolution ───────────────
235
290
 
236
- describe('useMapItemList — selectFeature mode: label resolution', () => {
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' }], multiselectable: false
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
- // Line 45: feature.properties is undefined → ?. short-circuits → ?? stringId used
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
- // ─── useMapItemList — selectFeature mode: guards ─────────────────────────
410
+ // ─── useSpatialList — selectFeature mode: guards ─────────────────────────
354
411
 
355
- describe('useMapItemList — selectFeature mode: guards', () => {
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
- // ─── useMapItemList — combined modes ─────────────────────────────────────
443
+ // ─── useSpatialList — combined modes ─────────────────────────────────────
387
444
 
388
- describe('useMapItemList — combined modes', () => {
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' }], multiselectable: false
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
- // ─── useMapItemList — map:setactivefeature listener ──────────────────────
480
+ // ─── useSpatialList — map:setactivefeature listener ──────────────────────
422
481
 
423
- describe('useMapItemList — map:setactivefeature listener', () => {
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 markers = makeMarkers([{ id: 'm1', label: 'Marker', symbol: 'pin', isVisible: true }])
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 feature id is not found in visible features', () => {
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('skips features from unknown layers when searching by id', () => {
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 search features when interactionModes excludes selectFeature', () => {
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 search features when layers is empty', () => {
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
- // ─── useMapItemList — confirm: lifecycle and guards ──────────────────────
613
+ // ─── useSpatialList — confirm: lifecycle and guards ──────────────────────
535
614
 
536
- describe('useMapItemList — confirm: lifecycle and guards', () => {
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 markers = makeMarkers([{ id: 'm1', label: 'Marker', symbol: 'pin', isVisible: true }])
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
- // ─── useMapItemList — confirm: dispatches ────────────────────────────────
641
+ // ─── useSpatialList — confirm: dispatches ────────────────────────────────
562
642
 
563
- describe('useMapItemList — confirm: marker dispatches', () => {
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 markers = makeMarkers([{ id: 'm1', label: 'Marker', symbol: 'pin', isVisible: true }])
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('useMapItemList — confirm: feature dispatches', () => {
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: 'Navigate features',
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
- * useMapItemList.js) so coordinate-based AT overlays (e.g. macOS Voice Control's Show Numbers)
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 useMapItemList.js) to find selectable/labelled
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