@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
@@ -5,8 +5,10 @@ import { scaleFactor } from '../../../../src/config/appConfig.js'
5
5
  import { buildLayerConfigMap } from '../utils/featureQueries.js'
6
6
  import { getGeometryCenter } from '../utils/spatial.js'
7
7
 
8
- const getFeatureId = (feature, config) =>
9
- config ? (feature.properties?.[config.idProperty] ?? feature.id) : null
8
+ // Only called after toFeatureItem's own `config?.labelProperty` guard, so config is always
9
+ // truthy here — no defensive null-config branch needed (that was only for findFeatureById's own
10
+ // direct-lookup path, since removed along with the mapProvider.getVisibleFeatures re-query it did).
11
+ const getFeatureId = (feature, config) => feature.properties?.[config.idProperty] ?? feature.id
10
12
 
11
13
  const isInViewport = (el) => {
12
14
  const container = el.closest('.im-c-viewport__markers')
@@ -40,7 +42,7 @@ const collectVisibleMarkers = (markers, mapProvider, mapSize) => {
40
42
  if (!marker.label) { continue }
41
43
  const el = markers.markerRefs?.get(marker.id)
42
44
  if (!isStandaloneLabel(marker) && el && isInViewport(el)) {
43
- const item = { id: marker.id, label: marker.label }
45
+ const item = { id: marker.id, label: marker.label, isMarker: true }
44
46
  if (marker.coords) {
45
47
  Object.assign(item, projectToScreen(mapProvider, mapSize, marker.coords))
46
48
  }
@@ -55,14 +57,28 @@ const toFeatureItem = (feature, layerConfigMap, seenIds, mapProvider, mapSize) =
55
57
  if (!config?.labelProperty) {
56
58
  return null
57
59
  }
58
- const id = getFeatureId(feature, config)
59
- const stringId = id == null ? null : String(id)
60
+ const rawId = getFeatureId(feature, config)
61
+ const stringId = rawId == null ? null : String(rawId)
60
62
  if (stringId == null || seenIds.has(stringId)) {
61
63
  return null
62
64
  }
63
65
  seenIds.add(stringId)
64
66
  const label = feature.properties?.[config.labelProperty] ?? stringId
65
- const item = { id: stringId, label }
67
+ // Keeps the full resolved shape (raw id, layer config, geometry, properties) alongside the
68
+ // listbox-facing id/label — not just for building the item, but so useActiveItemHandler below
69
+ // can resolve "what is this" from this same object later, without re-querying the map engine.
70
+ // Only id/label/x/y ever reach MAP_SET_SPATIAL_LIST itself (see toPublicItem) — the rest stays
71
+ // internal, in itemsRef.
72
+ const item = {
73
+ id: stringId,
74
+ label,
75
+ isMarker: false,
76
+ featureId: rawId,
77
+ layerId: config.layerId,
78
+ idProperty: config.idProperty,
79
+ geometry: feature.geometry,
80
+ properties: feature.properties
81
+ }
66
82
  // Bbox-centre of the feature's geometry (polygon or line) — see getGeometryCenter's own doc
67
83
  // for why a bbox centre rather than a true centroid. null when the feature carries no usable
68
84
  // geometry (e.g. OpenLayers' getVisibleFeatures, currently a stub returning [] — see PR notes).
@@ -88,23 +104,24 @@ const collectVisibleFeatures = (mapProvider, layers, mapSize) => {
88
104
  return items
89
105
  }
90
106
 
91
- const findFeatureById = (features, layerConfigMap, targetId) => {
92
- for (const feature of features) {
93
- const config = layerConfigMap[feature.layer?.id]
94
- const rawId = getFeatureId(feature, config)
95
- if (rawId != null && String(rawId) === String(targetId)) {
96
- return { feature, config, rawId }
97
- }
98
- }
99
- return null
100
- }
101
-
102
107
  /**
103
- * Rebuilds the keyboard-navigable item list whenever the map moves or data changes.
104
- * Collects visible markers (by DOM visibility) and visible features (by viewport query),
105
- * then emits MAP_SET_FEATURES so the listbox stays in sync with what the user can see.
108
+ * Rebuilds the keyboard-navigable item list whenever the map moves or data changes. Collects
109
+ * visible markers (by DOM visibility) and visible features (by viewport query), keeps the
110
+ * result in itemsRef for useActiveItemHandler to resolve against later, and tells
111
+ * spatialListRegistry its items have changed so the shared listbox stays in sync with what's
112
+ * visible. SpatialList.jsx/useSpatialListItems.js only ever render id/label/x/y from each item — the
113
+ * rest (isMarker, geometry, properties, layer config) rides along unused there, but is what
114
+ * lets useActiveItemHandler resolve "what is this" from the same object later, with nothing to
115
+ * re-derive and no separate lean/rich shape to keep in sync.
116
+ *
117
+ * interact no longer emits MAP_SET_SPATIAL_LIST directly — spatialListRegistry owns that (see its
118
+ * own doc comment for why: a single "whoever emits last wins" event doesn't scale once more
119
+ * than one plugin, e.g. draw mid-edit, needs to contribute to or take over the same list).
106
120
  */
107
- function useItemListSync ({ markers, mapSize, interactionModes, layers, mapProvider, multiSelect, eventBus }) {
121
+ function useItemListSync ({ markers, mapSize, interactionModes, layers, mapProvider, multiSelect, spatialListRegistry, eventBus, itemsRef }) {
122
+ const multiSelectRef = useRef(multiSelect)
123
+ multiSelectRef.current = multiSelect // always-current for the provider's getItems below, same convention as itemsRef
124
+
108
125
  useEffect(() => {
109
126
  const handleMoveEnd = () => {
110
127
  const items = []
@@ -114,7 +131,8 @@ function useItemListSync ({ markers, mapSize, interactionModes, layers, mapProvi
114
131
  if (interactionModes?.includes('selectFeature') && layers.length > 0) {
115
132
  items.push(...collectVisibleFeatures(mapProvider, layers, mapSize))
116
133
  }
117
- eventBus.emit(EVENTS.MAP_SET_FEATURES, { items, multiselectable: multiSelect })
134
+ itemsRef.current = items
135
+ spatialListRegistry.notifyItemsChanged('interact')
118
136
  }
119
137
  handleMoveEnd()
120
138
  eventBus.on(EVENTS.MAP_MOVE_END, handleMoveEnd)
@@ -123,16 +141,37 @@ function useItemListSync ({ markers, mapSize, interactionModes, layers, mapProvi
123
141
  eventBus.off(EVENTS.MAP_MOVE_END, handleMoveEnd)
124
142
  eventBus.off(EVENTS.MAP_DATA_CHANGE, handleMoveEnd)
125
143
  }
126
- }, [markers, mapSize, interactionModes, layers, mapProvider, multiSelect, eventBus])
144
+ }, [markers, mapSize, interactionModes, layers, mapProvider, multiSelect, spatialListRegistry, eventBus, itemsRef])
145
+
146
+ // Registers once, for the lifetime of the hook — getItems reads itemsRef/multiSelectRef
147
+ // fresh on every call, so there's never a need to re-register just because a prop changed;
148
+ // the effect above already pushes fresh data out via notifyItemsChanged. Declared after
149
+ // that effect so this registration's own immediate recompute (inside the registry) already
150
+ // sees whatever the effect above just computed on this same mount, rather than momentarily
151
+ // registering with empty items and correcting a moment later.
152
+ useEffect(() => {
153
+ spatialListRegistry.registerItemProvider('interact', {
154
+ getItems: () => ({ items: itemsRef.current, multiselectable: multiSelectRef.current, label: 'Map features' })
155
+ })
156
+ return () => { spatialListRegistry.unregisterItemProvider('interact') }
157
+ }, [spatialListRegistry, itemsRef])
127
158
  }
128
159
 
129
160
  /**
130
- * Listens for MAP_SET_ACTIVE_FEATURE and resolves the active item to its full feature/marker data,
161
+ * Listens for MAP_SET_ACTIVE_ITEM and resolves the active item to its full feature/marker data,
131
162
  * storing it in both a ref (for synchronous access) and plugin state (for highlight rendering).
132
163
  * Shows the keyboard cursor ring without firing interact:selectionchange — committing the item
133
164
  * to the real selection only happens when the user presses Enter/Space.
165
+ *
166
+ * Resolves purely by looking the id up in itemsRef (the same list useItemListSync just built) —
167
+ * deliberately not a fresh mapProvider.getVisibleFeatures() query. This fires on every roving-
168
+ * tabindex move (every arrow-key press while browsing the list), so re-querying the map engine
169
+ * here — as an earlier version of this hook did — meant every keystroke re-ran the same query
170
+ * useItemListSync had just run moments before, plus a second linear scan through the result, to
171
+ * re-derive data already computed once and discarded. A stale/out-of-range id (e.g. the map
172
+ * panned since the list was built) simply resolves to nothing, same as before.
134
173
  */
135
- function useActiveItemHandler ({ markers, interactionModes, layers, mapProvider, eventBus, dispatch, listboxActiveItemRef }) {
174
+ function useActiveItemHandler ({ itemsRef, eventBus, dispatch, listboxActiveItemRef }) {
136
175
  useEffect(() => {
137
176
  const handle = ({ id }) => {
138
177
  if (id === null) {
@@ -140,36 +179,31 @@ function useActiveItemHandler ({ markers, interactionModes, layers, mapProvider,
140
179
  dispatch({ type: 'SET_LISTBOX_ACTIVE', payload: null })
141
180
  return
142
181
  }
143
- const hasMarkerMatch = markers.items.some(m => m.id === id)
144
- if (hasMarkerMatch) {
182
+ const item = itemsRef.current.find(i => i.id === id)
183
+ if (!item) {
184
+ return
185
+ }
186
+ if (item.isMarker) {
145
187
  listboxActiveItemRef.current = { id, isMarker: true }
146
188
  dispatch({ type: 'SET_LISTBOX_ACTIVE', payload: null })
147
189
  return
148
190
  }
149
- if (interactionModes?.includes('selectFeature') && layers.length > 0) {
150
- const layerIds = layers.map(layer => layer.layerId)
151
- const layerConfigMap = buildLayerConfigMap(layers)
152
- const features = mapProvider.getVisibleFeatures(layerIds)
153
- const match = findFeatureById(features, layerConfigMap, id)
154
- if (match) {
155
- const payload = {
156
- featureId: match.rawId,
157
- layerId: match.config.layerId,
158
- idProperty: match.config.idProperty,
159
- geometry: match.feature.geometry
160
- }
161
- listboxActiveItemRef.current = { id, isMarker: false, ...payload, properties: match.feature.properties }
162
- dispatch({ type: 'SET_LISTBOX_ACTIVE', payload })
163
- }
191
+ const payload = {
192
+ featureId: item.featureId,
193
+ layerId: item.layerId,
194
+ idProperty: item.idProperty,
195
+ geometry: item.geometry
164
196
  }
197
+ listboxActiveItemRef.current = { id, isMarker: false, ...payload, properties: item.properties }
198
+ dispatch({ type: 'SET_LISTBOX_ACTIVE', payload })
165
199
  }
166
- eventBus.on(EVENTS.MAP_SET_ACTIVE_FEATURE, handle)
167
- return () => { eventBus.off(EVENTS.MAP_SET_ACTIVE_FEATURE, handle) }
168
- }, [markers, interactionModes, layers, mapProvider, eventBus, dispatch, listboxActiveItemRef])
200
+ eventBus.on(EVENTS.MAP_SET_ACTIVE_ITEM, handle)
201
+ return () => { eventBus.off(EVENTS.MAP_SET_ACTIVE_ITEM, handle) }
202
+ }, [itemsRef, eventBus, dispatch, listboxActiveItemRef])
169
203
  }
170
204
 
171
205
  /**
172
- * Handles MAP_SELECT_FEATURE (Enter/Space keypress) by promoting the currently active
206
+ * Handles MAP_SELECT_ITEM (Enter/Space keypress) by promoting the currently active
173
207
  * listbox item to a confirmed selection, dispatching TOGGLE_SELECTED_FEATURES or
174
208
  * TOGGLE_SELECTED_MARKERS and triggering interact:selectionchange downstream.
175
209
  */
@@ -190,8 +224,8 @@ function useSelectItemHandler ({ eventBus, dispatch, listboxActiveItemRef, multi
190
224
  })
191
225
  }
192
226
  }
193
- eventBus.on(EVENTS.MAP_SELECT_FEATURE, handleConfirm)
194
- return () => { eventBus.off(EVENTS.MAP_SELECT_FEATURE, handleConfirm) }
227
+ eventBus.on(EVENTS.MAP_SELECT_ITEM, handleConfirm)
228
+ return () => { eventBus.off(EVENTS.MAP_SELECT_ITEM, handleConfirm) }
195
229
  }, [eventBus, dispatch, listboxActiveItemRef, multiSelect])
196
230
  }
197
231
 
@@ -203,14 +237,15 @@ function useSelectItemHandler ({ eventBus, dispatch, listboxActiveItemRef, multi
203
237
  * - Active item resolution — translates a listbox cursor position into full feature/marker data
204
238
  * - Selection confirmation — commits the active item to the selection on Enter/Space
205
239
  *
206
- * @param {{ mapState: object, pluginState: object, services: object, mapProvider: object }} params
240
+ * @param {{ mapState: object, pluginState: object, services: object, mapProvider: object, spatialListRegistry: object }} params
207
241
  */
208
- export function useMapItemList ({ mapState, pluginState, services, mapProvider }) {
242
+ export function useSpatialList ({ mapState, pluginState, services, mapProvider, spatialListRegistry }) {
209
243
  const { markers, mapSize } = mapState
210
244
  const { dispatch, interactionModes, layers, multiSelect } = pluginState
211
245
  const { eventBus } = services
212
246
  const listboxActiveItemRef = useRef(null)
213
- useItemListSync({ markers, mapSize, interactionModes, layers, mapProvider, multiSelect, eventBus })
214
- useActiveItemHandler({ markers, interactionModes, layers, mapProvider, eventBus, dispatch, listboxActiveItemRef })
247
+ const itemsRef = useRef([])
248
+ useItemListSync({ markers, mapSize, interactionModes, layers, mapProvider, multiSelect, spatialListRegistry, eventBus, itemsRef })
249
+ useActiveItemHandler({ itemsRef, eventBus, dispatch, listboxActiveItemRef })
215
250
  useSelectItemHandler({ eventBus, dispatch, listboxActiveItemRef, multiSelect })
216
251
  }