@defra/interactive-map 0.0.43-fmp-experimental → 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 (275) hide show
  1. package/assets/templates/add-polygons.njk +5 -1
  2. package/assets/templates/add-symbols.njk +5 -1
  3. package/dist/css/index.css +1 -1
  4. package/dist/esm/im-core.js +1 -1
  5. package/dist/esm/im-shell.js +1 -1
  6. package/dist/umd/im-core.js +1 -1
  7. package/dist/umd/index.js +1 -1
  8. package/docs/api/context.md +24 -0
  9. package/docs/api/keyboard-shortcut-definition.md +102 -0
  10. package/docs/api.md +18 -8
  11. package/docs/building-a-plugin.md +1 -0
  12. package/docs/plugins/draw.md +2 -2
  13. package/docs/plugins/plugin-manifest.md +9 -0
  14. package/package.json +4 -2
  15. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  16. package/plugins/beta/draw-es/src/api/newPolygon.js +1 -0
  17. package/plugins/beta/draw-es/src/events.js +17 -8
  18. package/plugins/beta/draw-es/src/reducer.js +8 -1
  19. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  20. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  21. package/plugins/beta/draw-ol/dist/css/index.css +1 -13
  22. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  23. package/plugins/beta/frame/dist/css/index.css +1 -11
  24. package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
  25. package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
  26. package/plugins/beta/frame/src/Frame.jsx +28 -2
  27. package/plugins/beta/map-styles/dist/css/index.css +1 -1
  28. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  29. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  30. package/plugins/beta/scale-bar/dist/css/index.css +1 -8
  31. package/plugins/datasets/dist/css/5648.index.css +1 -5
  32. package/plugins/datasets/dist/css/index.css +1 -1
  33. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  34. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  35. package/plugins/datasets/src/components/LayersMenu/Layers.module.scss +1 -0
  36. package/plugins/datasets/src/initialise/DatasetsInit.jsx +2 -2
  37. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +6 -5
  38. package/plugins/datasets/src/registry/dataset.js +8 -12
  39. package/plugins/datasets/src/registry/dataset.test.js +17 -0
  40. package/plugins/datasets/src/registry/datasetRegistry.js +4 -5
  41. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  42. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  43. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  44. package/plugins/draw/dist/esm/index.js +1 -1
  45. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -1
  46. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  47. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  48. package/plugins/draw/dist/umd/index.js +1 -1
  49. package/plugins/draw/src/DrawInit.jsx +41 -26
  50. package/plugins/draw/src/DrawInit.test.jsx +55 -12
  51. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +113 -114
  52. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +82 -9
  53. package/plugins/draw/src/adapters/maplibre/drawEvents.js +3 -1
  54. package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +1 -1
  55. package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +18 -9
  56. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +9 -1
  57. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +11 -1
  58. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +23 -2
  59. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +53 -0
  60. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +29 -19
  61. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +17 -2
  62. package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +1 -1
  63. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +35 -29
  64. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +24 -6
  65. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +22 -20
  66. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +1 -1
  67. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +2 -2
  68. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +3 -3
  69. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +31 -0
  70. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +8 -1
  71. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +38 -30
  72. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +20 -3
  73. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +5 -5
  74. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +2 -2
  75. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +2 -2
  76. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +3 -3
  77. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -7
  78. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +4 -4
  79. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +14 -14
  80. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +12 -32
  81. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +10 -10
  82. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +48 -22
  83. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +74 -6
  84. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +2 -2
  85. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +6 -0
  86. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +9 -0
  87. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +15 -2
  88. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +27 -1
  89. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +1 -1
  90. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +40 -17
  91. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +55 -1
  92. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +13 -7
  93. package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +7 -15
  94. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +36 -4
  95. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +41 -1
  96. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +35 -25
  97. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +40 -2
  98. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +2 -2
  99. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +2 -2
  100. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +40 -54
  101. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +32 -13
  102. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +1 -1
  103. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +17 -1
  104. package/plugins/draw/src/events.js +4 -4
  105. package/plugins/draw/src/events.test.js +1 -1
  106. package/plugins/draw/src/hooks/useSpatialList.js +180 -0
  107. package/plugins/draw/src/hooks/useSpatialList.test.js +339 -0
  108. package/plugins/draw/src/manifest.js +4 -1
  109. package/plugins/draw/src/manifest.test.js +19 -7
  110. package/plugins/draw/src/utils/spatial.js +1 -31
  111. package/plugins/draw/src/utils/spatial.test.js +1 -24
  112. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  113. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  114. package/plugins/interact/dist/umd/index.js +1 -1
  115. package/plugins/interact/src/InteractInit.jsx +2 -2
  116. package/plugins/interact/src/InteractInit.test.js +3 -3
  117. package/plugins/interact/src/events.js +11 -2
  118. package/plugins/interact/src/events.test.js +32 -0
  119. package/plugins/interact/src/hooks/useAttachEvents.js +20 -0
  120. package/plugins/interact/src/hooks/useAttachEvents.test.js +44 -1
  121. package/plugins/interact/src/hooks/useCrossHairVisibility.js +9 -4
  122. package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +7 -7
  123. package/plugins/interact/src/hooks/useSpatialList.js +251 -0
  124. package/plugins/interact/src/hooks/{useMapItemList.test.js → useSpatialList.test.js} +216 -70
  125. package/plugins/interact/src/manifest.js +12 -1
  126. package/plugins/interact/src/manifest.test.js +20 -0
  127. package/plugins/interact/src/utils/spatial.js +24 -1
  128. package/plugins/interact/src/utils/spatial.test.js +26 -1
  129. package/plugins/map-key/dist/css/index.css +1 -106
  130. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -1
  131. package/plugins/map-key/dist/umd/im-map-key-plugin.js +1 -1
  132. package/plugins/map-key/dist/umd/index.js +1 -1
  133. package/plugins/map-key/src/components/Key/Key.jsx +3 -4
  134. package/plugins/map-key/src/components/Key/Key.module.scss +10 -8
  135. package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +2 -2
  136. package/plugins/map-key/src/components/Key/KeyItem.jsx +2 -2
  137. package/plugins/map-key/src/components/Key/KeySvg.jsx +9 -7
  138. package/plugins/map-key/src/components/Key/KeySvg.test.jsx +4 -4
  139. package/plugins/map-key/src/components/Key/MapKey.jsx +5 -3
  140. package/plugins/map-key/src/manifest.js +1 -1
  141. package/plugins/map-key/src/utils/groupStyles.js +7 -0
  142. package/plugins/map-key/src/utils/mergeKeyGroupItems.js +12 -0
  143. package/plugins/map-key/src/utils/mergeKeyGroupItems.test.js +53 -0
  144. package/plugins/menu/dist/css/index.css +1 -80
  145. package/plugins/menu/dist/esm/im-menu-plugin.js +1 -1
  146. package/plugins/menu/dist/umd/im-menu-plugin.js +1 -1
  147. package/plugins/menu/dist/umd/index.js +1 -1
  148. package/plugins/menu/src/initialise/MenuInit.jsx +15 -3
  149. package/plugins/menu/src/initialise/MenuInit.test.jsx +25 -0
  150. package/plugins/menu/src/initialise/createMenu.js +3 -1
  151. package/plugins/menu/src/initialise/createMenu.test.js +20 -5
  152. package/plugins/menu/src/initialise/updateUrl.js +44 -0
  153. package/plugins/menu/src/initialise/updateUrl.test.js +91 -0
  154. package/plugins/menu/src/reducers/menuStateReducer.js +19 -1
  155. package/plugins/menu/src/reducers/menuStateReducer.test.js +53 -20
  156. package/plugins/menu/src/reducers/pluginState.js +1 -6
  157. package/plugins/menu/src/reducers/pluginState.test.js +0 -25
  158. package/plugins/search/dist/css/index.css +1 -1
  159. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  160. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  161. package/plugins/search/src/manifest.js +5 -9
  162. package/plugins/search/src/manifest.test.js +5 -0
  163. package/providers/beta/esri/dist/css/index.css +1 -34
  164. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  165. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  166. package/providers/beta/openlayers/src/openlayersProvider.js +3 -3
  167. package/providers/beta/openlayers/src/openlayersProvider.test.js +6 -3
  168. package/providers/beta/openlayers/src/utils/queryFeatures.js +72 -0
  169. package/providers/beta/openlayers/src/utils/queryFeatures.test.js +122 -1
  170. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  171. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  172. package/providers/maplibre/dist/umd/index.js +1 -1
  173. package/providers/maplibre/src/defaults.test.js +26 -0
  174. package/providers/maplibre/src/utils/labels.js +2 -2
  175. package/providers/maplibre/src/utils/labels.test.js +5 -5
  176. package/providers/maplibre/src/utils/spatial.js +0 -57
  177. package/providers/maplibre/src/utils/spatial.test.js +0 -27
  178. package/rollup.esm.mjs +2 -1
  179. package/scripts/publish-package.sh +36 -0
  180. package/src/App/App.jsx +1 -1
  181. package/src/App/App.test.jsx +6 -1
  182. package/src/App/components/CrossHair/CrossHair.jsx +25 -11
  183. package/src/App/components/CrossHair/CrossHair.module.scss +37 -12
  184. package/src/App/components/CrossHair/CrossHair.test.jsx +40 -11
  185. package/src/App/components/Hints/Hints.jsx +3 -3
  186. package/src/App/components/Hints/Hints.test.jsx +3 -3
  187. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +4 -1
  188. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +17 -0
  189. package/src/App/components/MapButton/MapButton.jsx +19 -7
  190. package/src/App/components/MapButton/MapButton.module.scss +1 -2
  191. package/src/App/components/MapButton/MapButton.test.jsx +42 -4
  192. package/src/App/components/{MoveControls/MoveControls.jsx → MapControls/MapControls.jsx} +10 -10
  193. package/src/App/components/{MoveControls/MoveControls.module.scss → MapControls/MapControls.module.scss} +25 -25
  194. package/src/App/components/{MoveControls/MoveControls.test.jsx → MapControls/MapControls.test.jsx} +48 -48
  195. package/src/App/components/Markers/Markers.jsx +2 -2
  196. package/src/App/components/Markers/Markers.test.jsx +3 -3
  197. package/src/App/components/Markers/SymbolMarker.jsx +2 -3
  198. package/src/App/components/Markers/SymbolMarker.test.jsx +8 -9
  199. package/src/App/components/Panel/Panel.jsx +19 -20
  200. package/src/App/components/Panel/Panel.module.scss +5 -0
  201. package/src/App/components/Panel/Panel.test.jsx +69 -4
  202. package/src/App/components/PopupMenu/PopupMenu.jsx +7 -3
  203. package/src/App/components/PopupMenu/PopupMenu.test.jsx +71 -0
  204. package/src/App/components/PopupMenu/usePopupMenu.js +10 -9
  205. package/src/App/components/Viewport/MapStatus.jsx +1 -1
  206. package/src/App/components/Viewport/SpatialList.jsx +47 -0
  207. package/src/App/components/Viewport/{Features.module.scss → SpatialList.module.scss} +6 -6
  208. package/src/App/components/Viewport/SpatialList.test.jsx +285 -0
  209. package/src/App/components/Viewport/Viewport.jsx +21 -11
  210. package/src/App/components/Viewport/Viewport.test.jsx +18 -2
  211. package/src/App/controls/keyboardMappings.js +7 -2
  212. package/src/App/controls/keyboardMappings.test.js +21 -0
  213. package/src/App/controls/keyboardShortcuts.js +16 -2
  214. package/src/App/controls/keyboardShortcuts.test.js +21 -0
  215. package/src/App/hooks/useCrossHairAPI.js +29 -1
  216. package/src/App/hooks/useCrossHairAPI.test.js +17 -1
  217. package/src/App/hooks/useHintsAPI.js +5 -5
  218. package/src/App/hooks/useInterfaceAPI.js +2 -1
  219. package/src/App/hooks/useInterfaceAPI.test.js +9 -0
  220. package/src/App/hooks/useKeyboardShortcuts.js +66 -35
  221. package/src/App/hooks/useKeyboardShortcuts.test.js +52 -0
  222. package/src/App/hooks/useModalPanelBehaviour.js +48 -34
  223. package/src/App/hooks/useModalPanelBehaviour.test.js +59 -0
  224. package/src/App/hooks/useSpatialListFocus.js +296 -0
  225. package/src/App/hooks/useSpatialListFocus.test.js +957 -0
  226. package/src/App/hooks/useSpatialListItems.js +51 -0
  227. package/src/App/hooks/useSpatialListItems.test.js +217 -0
  228. package/src/App/initialiseApp.js +6 -3
  229. package/src/App/initialiseApp.test.js +11 -0
  230. package/src/App/layout/Layout.jsx +4 -2
  231. package/src/App/layout/Layout.test.jsx +21 -0
  232. package/src/App/layout/layout.module.scss +7 -1
  233. package/src/App/registry/keyboardShortcutRegistry.js +2 -2
  234. package/src/App/registry/keyboardShortcutRegistry.test.js +16 -0
  235. package/src/App/registry/pluginRegistry.js +13 -0
  236. package/src/App/registry/pluginRegistry.test.js +15 -0
  237. package/src/App/registry/spatialListRegistry.js +104 -0
  238. package/src/App/registry/spatialListRegistry.test.js +218 -0
  239. package/src/App/renderer/HtmlElementHost.jsx +12 -32
  240. package/src/App/renderer/mapButtons.js +4 -0
  241. package/src/App/renderer/mapButtons.test.js +20 -0
  242. package/src/App/renderer/mapPanels.js +33 -53
  243. package/src/App/renderer/mapPanels.test.js +29 -4
  244. package/src/App/renderer/slotHelpers.js +33 -0
  245. package/src/App/renderer/slotHelpers.test.js +79 -2
  246. package/src/App/store/AppProvider.jsx +3 -2
  247. package/src/App/store/ServiceProvider.jsx +5 -3
  248. package/src/App/store/ServiceProvider.test.jsx +20 -1
  249. package/src/InteractiveMap/InteractiveMap.js +13 -2
  250. package/src/InteractiveMap/InteractiveMap.test.js +33 -0
  251. package/src/config/appConfig.js +16 -16
  252. package/src/config/appConfig.test.js +19 -19
  253. package/src/config/defaults.js +2 -2
  254. package/src/config/events.js +8 -8
  255. package/src/scss/main.scss +2 -2
  256. package/src/test-utils.js +8 -0
  257. package/src/types.js +51 -9
  258. package/src/utils/findNearestItemInDirection.js +29 -0
  259. package/src/utils/findNearestItemInDirection.test.js +67 -0
  260. package/src/utils/getPanelElementId.js +4 -0
  261. package/src/utils/getPanelElementId.test.js +11 -0
  262. package/src/utils/getPanelRole.js +22 -0
  263. package/src/utils/getPanelRole.test.js +56 -0
  264. package/src/utils/globalAltShortcuts.js +12 -0
  265. package/src/utils/globalAltShortcuts.test.js +27 -0
  266. package/src/utils/spatialNavigate.js +49 -0
  267. package/src/utils/spatialNavigate.test.js +42 -0
  268. package/webpack.umd.mjs +6 -1
  269. package/plugins/interact/src/hooks/useMapItemList.js +0 -187
  270. package/src/App/components/Viewport/Features.jsx +0 -35
  271. package/src/App/components/Viewport/Features.test.jsx +0 -124
  272. package/src/App/hooks/useFeatureFocus.js +0 -199
  273. package/src/App/hooks/useFeatureFocus.test.js +0 -635
  274. package/src/App/hooks/useFeatureItems.js +0 -39
  275. package/src/App/hooks/useFeatureItems.test.js +0 -155
@@ -56,6 +56,41 @@ test('arrow keys switch to the keyboard interface without placing', () => {
56
56
  expect(placement.placeVertex).not.toHaveBeenCalled()
57
57
  })
58
58
 
59
+ describe('global Alt+<key> shortcut shadowing', () => {
60
+ test('Alt+Enter keyup stops propagation while drawing — never reaches the app-wide "highlight label at center" shortcut', () => {
61
+ setup()
62
+ const event = new KeyboardEvent('keyup', { key: 'Enter', altKey: true, cancelable: true, bubbles: true })
63
+ const stopSpy = jest.spyOn(event, 'stopPropagation')
64
+ window.dispatchEvent(event)
65
+ expect(stopSpy).toHaveBeenCalled()
66
+ })
67
+
68
+ test('Alt+Arrow keyup stops propagation too — never reaches the app-wide "highlight next label" shortcut', () => {
69
+ setup()
70
+ const event = new KeyboardEvent('keyup', { key: 'ArrowRight', altKey: true, cancelable: true, bubbles: true })
71
+ const stopSpy = jest.spyOn(event, 'stopPropagation')
72
+ window.dispatchEvent(event)
73
+ expect(stopSpy).toHaveBeenCalled()
74
+ })
75
+
76
+ test('a plain Enter keyup (no Alt) is left alone', () => {
77
+ setup()
78
+ const event = new KeyboardEvent('keyup', { key: 'Enter', altKey: false, cancelable: true, bubbles: true })
79
+ const stopSpy = jest.spyOn(event, 'stopPropagation')
80
+ window.dispatchEvent(event)
81
+ expect(stopSpy).not.toHaveBeenCalled()
82
+ })
83
+
84
+ test('the shadow listener is removed on destroy — Alt+Enter bubbles normally again', () => {
85
+ const { input } = setup()
86
+ input.destroy()
87
+ const event = new KeyboardEvent('keyup', { key: 'Enter', altKey: true, cancelable: true, bubbles: true })
88
+ const stopSpy = jest.spyOn(event, 'stopPropagation')
89
+ window.dispatchEvent(event)
90
+ expect(stopSpy).not.toHaveBeenCalled()
91
+ })
92
+ })
93
+
59
94
  test('ctrl/cmd+z triggers undo', () => {
60
95
  const { container, onUndo } = setup()
61
96
  container.focus()
@@ -111,7 +146,26 @@ test('pointer moves and map pans update the rubber band except for the mouse int
111
146
  expect(mouse.placement.updateRubberbanding).not.toHaveBeenCalled()
112
147
  })
113
148
 
114
- test('setInterfaceType updates the interface and refreshes the rubber band immediately for non-mouse types, e.g. switching to touch and panning via MoveControls mid-session', () => {
149
+ // Voice Control's simulated clicks report pointerType 'mouse' (interfaceType never leaves
150
+ // 'mouse') and produce no real pointermove, so panning via MapControls needs its own
151
+ // fallback: getAnimating() is true for MapControls' own panBy/zoomIn/zoomOut (both animate
152
+ // the view) but not for a live mouse drag (which sets the center directly), so it's a safe
153
+ // signal to also track even while interfaceType is 'mouse'.
154
+ test('a programmatic move (e.g. MapControls panBy) still updates the rubber band while interfaceType is mouse', () => {
155
+ const { view, placement } = setup('mouse')
156
+ view.getAnimating.mockReturnValue(true)
157
+ view.emit('change:center')
158
+ expect(placement.updateRubberbanding).toHaveBeenCalledTimes(1)
159
+ })
160
+
161
+ test('postrender tracks an animated move for the mouse interface too, same as keyboard', () => {
162
+ const { map, view, placement } = setup('mouse')
163
+ view.getAnimating.mockReturnValue(true)
164
+ map.emit('postrender')
165
+ expect(placement.updateRubberbanding).toHaveBeenCalledTimes(1)
166
+ })
167
+
168
+ test('setInterfaceType updates the interface and refreshes the rubber band immediately for non-mouse types, e.g. switching to touch and panning via MapControls mid-session', () => {
115
169
  const { input, placement } = setup('mouse')
116
170
  input.setInterfaceType('touch')
117
171
  expect(input.getInterfaceType()).toBe('touch')
@@ -84,7 +84,14 @@ const tryClose = ({ drawInteraction, canFinish, geom, sketchCoords, coord, lastP
84
84
  *
85
85
  * @returns {{ placeVertex, updateRubberbanding, clearLastCoord }}
86
86
  */
87
- export const createVertexPlacement = ({ drawInteraction, mapProvider, snap, canFinish, canPlace, getInterfaceType }) => {
87
+ // Both snappedCenter and updateRubberbanding are only ever reached via the crosshair path
88
+ // (Enter, the add-vertex button, or the crosshair's own click) — never a real mouse click,
89
+ // which goes through OL's own Draw interaction and its separate snap interaction instead.
90
+ // So there's no interfaceType to gate on here: whenever these run, the target is by
91
+ // definition the crosshair, and snap (when available) should always apply. Callers decide
92
+ // *when* to call updateRubberbanding at all (see draw/drawInput.js's onCenterChange etc.),
93
+ // not this file.
94
+ export const createVertexPlacement = ({ drawInteraction, mapProvider, snap, canFinish, canPlace }) => {
88
95
  let sketchFeature = null
89
96
  let lastPlacedCoord = null
90
97
 
@@ -92,21 +99,20 @@ export const createVertexPlacement = ({ drawInteraction, mapProvider, snap, canF
92
99
  sketchFeature = feature
93
100
  lastPlacedCoord = null
94
101
  }
95
- drawInteraction.on('drawstart', (e) => resetSketch(e.feature))
102
+ drawInteraction.on('drawstart', (event) => resetSketch(event.feature))
96
103
  drawInteraction.on('drawend', () => resetSketch())
97
104
  drawInteraction.on('drawabort', () => resetSketch())
98
105
 
99
106
  const snappedCenter = () => {
100
107
  const raw = mapProvider.getCenter()
101
- return (getInterfaceType() !== 'mouse' && snap) ? snap.apply(raw) : raw
108
+ return snap ? snap.apply(raw) : raw
102
109
  }
103
110
 
104
111
  const updateRubberbanding = () => {
105
112
  if (!sketchFeature) {
106
- // No sketch yet — update snap indicator at crosshair position so targets are
107
- // visible before the first vertex is placed (touch/keyboard only; mouse uses
108
- // the OL snap interaction's pointermove handler instead).
109
- if (getInterfaceType() !== 'mouse' && snap) {
113
+ // No sketch yet — update the snap indicator at the crosshair so targets are visible
114
+ // before the first vertex is placed.
115
+ if (snap) {
110
116
  snap.apply(mapProvider.getCenter())
111
117
  }
112
118
  return
@@ -6,7 +6,7 @@ import { createFakeMap, createEmitter, polygonFeature, lineFeature } from '../__
6
6
 
7
7
  const CENTER = [5, 5]
8
8
 
9
- const setup = ({ interfaceType = 'touch', snap = null, canFinish = () => true, canPlace } = {}) => {
9
+ const setup = ({ snap = null, canFinish = () => true, canPlace } = {}) => {
10
10
  const map = createFakeMap()
11
11
  const bus = createEmitter()
12
12
  const drawInteraction = {
@@ -21,8 +21,7 @@ const setup = ({ interfaceType = 'touch', snap = null, canFinish = () => true, c
21
21
  mapProvider: { getCenter: () => CENTER },
22
22
  snap,
23
23
  canFinish,
24
- canPlace,
25
- getInterfaceType: () => interfaceType
24
+ canPlace
26
25
  })
27
26
  const startSketch = (feature) => { bus.emit('drawstart', { feature }); return feature }
28
27
  return { drawInteraction, placement, startSketch, bus }
@@ -59,13 +58,6 @@ describe('placing vertices', () => {
59
58
  expect(drawInteraction.finishDrawing).toHaveBeenCalledTimes(1)
60
59
  })
61
60
 
62
- test('the mouse interface ignores snapping (the OL snap interaction covers it)', () => {
63
- const snap = { apply: jest.fn(() => [9, 9]), hideIndicator: jest.fn() }
64
- const { placement, drawInteraction } = setup({ interfaceType: 'mouse', snap })
65
- placement.placeVertex()
66
- expect(drawInteraction.appendCoordinates).toHaveBeenCalledWith([CENTER])
67
- })
68
-
69
61
  test('a second tap at the same spot finishes the shape instead of duplicating — when finishable', () => {
70
62
  const { placement, drawInteraction, startSketch } = setup()
71
63
  startSketch(lineFeature([[0, 0], [50, 50]]))
@@ -152,16 +144,16 @@ describe('rubber-banding', () => {
152
144
  expect(feature.getGeometry().getCoordinates()[1]).toEqual(hole) // inner rings untouched
153
145
  })
154
146
 
155
- test('without a sketch it only refreshes the snap indicator at the crosshair (non-mouse)', () => {
147
+ test('without a sketch it refreshes the snap indicator at the crosshair — the caller decides when it\'s worth calling this at all', () => {
156
148
  const snap = { apply: jest.fn((c) => c), hideIndicator: jest.fn() }
157
149
  const { placement } = setup({ snap })
158
150
  placement.updateRubberbanding()
159
151
  expect(snap.apply).toHaveBeenCalledWith(CENTER)
152
+ })
160
153
 
161
- const mouse = setup({ interfaceType: 'mouse', snap })
162
- snap.apply.mockClear()
163
- mouse.placement.updateRubberbanding()
164
- expect(snap.apply).not.toHaveBeenCalled()
154
+ test('without a sketch and no snap manager, updateRubberbanding is a harmless no-op', () => {
155
+ const { placement } = setup()
156
+ expect(() => placement.updateRubberbanding()).not.toThrow()
165
157
  })
166
158
 
167
159
  test('an empty sketch geometry is left alone', () => {
@@ -258,8 +258,8 @@ const wireMapSync = ({ map, manager, layers, selection, touchHandler, live }) =>
258
258
  }
259
259
 
260
260
  // The mode interface consumed by OLDrawManager
261
- const buildModeApi = ({ manager, store, olFeature, originalFeatureStyle, selection, actions, parts }) => {
262
- const { state } = selection
261
+ const buildModeApi = ({ manager, store, olFeature, originalFeatureStyle, selection, actions, parts, undoStack }) => {
262
+ const { state, setState, syncGeom, emitGeometryValidation } = selection
263
263
  const { touchHandler } = parts
264
264
 
265
265
  return {
@@ -291,10 +291,41 @@ const buildModeApi = ({ manager, store, olFeature, originalFeatureStyle, selecti
291
291
 
292
292
  undo: actions.doUndo,
293
293
  deleteVertex: actions.doDeleteVertex,
294
- // MoveControls' D-pad, routed here via mapProvider.activeMoveTarget (see
294
+ // MapControls' D-pad, routed here via mapProvider.activeMoveTarget (see
295
295
  // events.js) once a vertex is selected.
296
296
  nudgeSelectedVertex: parts.keyboardHandler.nudgeByDelta,
297
297
 
298
+ // Read-only vertex/midpoint coordinates for the shared spatial listbox
299
+ // (plugins/draw/src/hooks/useSpatialList.js) — already kept live and correctly indexed
300
+ // by syncGeom()/updateLayersFromGeom() (selectionState.js), so nothing to recompute here.
301
+ getVertexItems: () => ({ vertices: state.vertices, midpoints: state.midpoints }),
302
+
303
+ // Moves the live cursor to a vertex or midpoint by flat index — preview only, no geometry
304
+ // change — for the spatial listbox's roving-tabindex move. Same shape as the local
305
+ // selectVertex closure in wireTouchHandler above, promoted here since buildModeApi needs
306
+ // its own entry point too.
307
+ selectVertex (index) {
308
+ setState({ selectedVertexIndex: index, selectedVertexType: index < state.vertices.length ? VERTEX_TYPE : 'midpoint' })
309
+ touchHandler.updateTargetPosition()
310
+ },
311
+
312
+ // Commits a new vertex exactly at midpoint `index` (no directional offset) — the
313
+ // destructive counterpart to selectVertex's preview-only move, for the spatial listbox's
314
+ // Enter/Space confirm on a midpoint item. Mirrors onTap's own midpoint-insert handling
315
+ // above exactly (insertAtMidpoint already inserts with no offset — that's how tap-to-insert
316
+ // already works, no keyboard-style directional nudge to strip).
317
+ insertVertexAtMidpoint (index) {
318
+ const result = insertAtMidpoint(olFeature, state.midpoints, index, state.vertices.length)
319
+ if (!result) {
320
+ return
321
+ }
322
+ undoStack.push({ type: 'insert_vertex', vertexIndex: result.insertedIndex })
323
+ syncGeom()
324
+ emitGeometryValidation(INSERT_VERTEX, result.insertedIndex)
325
+ setState({ selectedVertexIndex: result.insertedIndex, selectedVertexType: VERTEX_TYPE })
326
+ touchHandler.updateTargetPosition()
327
+ },
328
+
298
329
  destroy () {
299
330
  parts.live.destroy()
300
331
  olFeature.setStyle(originalFeatureStyle)
@@ -312,7 +343,7 @@ const buildModeApi = ({ manager, store, olFeature, originalFeatureStyle, selecti
312
343
  }
313
344
 
314
345
  /**
315
- * @returns {{ setInterfaceType, done, cancel, undo, deleteVertex, nudgeSelectedVertex, destroy } | null}
346
+ * @returns {{ setInterfaceType, done, cancel, undo, deleteVertex, nudgeSelectedVertex, getVertexItems, selectVertex, insertVertexAtMidpoint, destroy } | null}
316
347
  */
317
348
  export const createEditMode = ({ map, manager, options }) => {
318
349
  const { featureId, container, interfaceType, deleteVertexButtonId, snap } = options
@@ -380,6 +411,7 @@ export const createEditMode = ({ map, manager, options }) => {
380
411
  originalFeatureStyle,
381
412
  selection,
382
413
  actions,
414
+ undoStack,
383
415
  parts: { touchHandler, keyboardHandler, pointerHandlers, modify, mapSync, layers, live }
384
416
  })
385
417
  }
@@ -154,7 +154,7 @@ test('select via pointer, delete the vertex, then undo restores it', () => {
154
154
  mode.undo() // empty stack — no-op
155
155
  })
156
156
 
157
- test('nudgeSelectedVertex (MoveControls D-pad) moves the selected vertex and is undoable', () => {
157
+ test('nudgeSelectedVertex (MapControls D-pad) moves the selected vertex and is undoable', () => {
158
158
  const { container, manager, mode, ring } = setup()
159
159
  container.dispatchEvent(domEvent('pointerdown', { pointerType: 'mouse', clientX: 100, clientY: 0 }))
160
160
  mode.nudgeSelectedVertex(1, 0, true)
@@ -165,6 +165,46 @@ test('nudgeSelectedVertex (MoveControls D-pad) moves the selected vertex and is
165
165
  expect(ring()[1]).toEqual([100, 0])
166
166
  })
167
167
 
168
+ // getVertexItems/selectVertex/insertVertexAtMidpoint feed the shared spatial listbox
169
+ // (plugins/draw/src/hooks/useSpatialList.js).
170
+ describe('getVertexItems / selectVertex / insertVertexAtMidpoint (spatial listbox bridge)', () => {
171
+ test('getVertexItems reads the live vertices/midpoints kept in sync by selectionState', () => {
172
+ const { mode } = setup()
173
+ const { vertices, midpoints } = mode.getVertexItems()
174
+ expect(vertices).toEqual([[0, 0], [100, 0], [100, 100], [0, 100]])
175
+ expect(midpoints).toHaveLength(4)
176
+ })
177
+
178
+ test('selectVertex moves the cursor to a real vertex, without changing the geometry', () => {
179
+ const { manager, mode, ring } = setup()
180
+ mode.selectVertex(1)
181
+ expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_SELECTION, expect.objectContaining({ index: 1 }))
182
+ expect(ring()).toHaveLength(5) // unchanged
183
+ })
184
+
185
+ test('selectVertex moves the cursor to a midpoint (flat index past the real vertices) — reports index: -1', () => {
186
+ const { manager, mode } = setup()
187
+ mode.selectVertex(4) // first midpoint, between vertex 0 and 1
188
+ expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_SELECTION, expect.objectContaining({ index: -1 }))
189
+ })
190
+
191
+ test('insertVertexAtMidpoint commits a new vertex exactly at the midpoint, selects it, and is undoable', () => {
192
+ const { manager, mode, ring } = setup()
193
+ mode.insertVertexAtMidpoint(4) // first midpoint, between vertex 0 and 1
194
+ expect(ring()).toHaveLength(6)
195
+ expect(ring()[1]).toEqual([50, 0]) // exactly the midpoint — no directional offset
196
+ expect(manager.undoStack.pop()).toEqual({ type: 'insert_vertex', vertexIndex: 1 })
197
+ expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_SELECTION, expect.objectContaining({ index: 1 }))
198
+ })
199
+
200
+ test('insertVertexAtMidpoint is a no-op for an out-of-range index', () => {
201
+ const { manager, mode, ring } = setup()
202
+ mode.insertVertexAtMidpoint(999)
203
+ expect(ring()).toHaveLength(5)
204
+ expect(manager.undoStack.length).toBe(0)
205
+ })
206
+ })
207
+
168
208
  test('undo re-validates with the inverse change phase (undo of a delete re-inserts)', () => {
169
209
  jest.useFakeTimers()
170
210
  const { container, manager, mode } = setup()
@@ -1,5 +1,6 @@
1
1
  import { coordToPixel } from '../utils/olCoords.js'
2
- import { spatialNavigate } from '../../../utils/spatial.js'
2
+ import { spatialNavigate } from '../../../../../../src/utils/spatialNavigate.js'
3
+ import { stopIfGlobalAltKey } from '../../../../../../src/utils/globalAltShortcuts.js'
3
4
  import { wireNudge } from './nudge.js'
4
5
 
5
6
  const ARROW_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'])
@@ -54,34 +55,34 @@ const isInteractiveElementFocused = (appViewport) => {
54
55
  }
55
56
 
56
57
  const buildKeydownHandler = ({ map, getState, setState, nudge, keyMove, onUndo, onKeyboardActive, isFocused }) => {
57
- const handleArrowKey = (e) => {
58
- if (e.altKey) {
59
- e.preventDefault()
60
- e.stopPropagation()
61
- navigateTo(e.key, map, getState, setState)
58
+ const handleArrowKey = (event) => {
59
+ if (event.altKey) {
60
+ event.preventDefault()
61
+ event.stopPropagation()
62
+ navigateTo(event.key, map, getState, setState)
62
63
  } else if (getState().selectedVertexIndex >= 0) {
63
- e.preventDefault()
64
- e.stopPropagation()
65
- nudge(e)
64
+ event.preventDefault()
65
+ event.stopPropagation()
66
+ nudge(event)
66
67
  } else {
67
68
  // No action: arrow with no selection and no alt modifier
68
69
  }
69
70
  }
70
71
 
71
- const handleKey = (e) => {
72
+ const handleKey = (event) => {
72
73
  onKeyboardActive?.()
73
- if (e.key === ' ') {
74
- e.preventDefault()
74
+ if (event.key === ' ') {
75
+ event.preventDefault()
75
76
  if (getState().selectedVertexIndex < 0) {
76
77
  selectNearest(map, getState, setState)
77
78
  }
78
- } else if (ARROW_KEYS.has(e.key)) {
79
- handleArrowKey(e)
80
- } else if (e.key === 'z' && (e.metaKey || e.ctrlKey)) {
79
+ } else if (ARROW_KEYS.has(event.key)) {
80
+ handleArrowKey(event)
81
+ } else if (event.key === 'z' && (event.metaKey || event.ctrlKey)) {
81
82
  const tag = document.activeElement?.tagName
82
83
  if (!INTERACTIVE_TAGS.has(tag)) {
83
- e.preventDefault()
84
- e.stopPropagation()
84
+ event.preventDefault()
85
+ event.stopPropagation()
85
86
  onUndo()
86
87
  }
87
88
  } else {
@@ -89,33 +90,42 @@ const buildKeydownHandler = ({ map, getState, setState, nudge, keyMove, onUndo,
89
90
  }
90
91
  }
91
92
 
92
- return (e) => {
93
+ return (event) => {
93
94
  if (isFocused()) {
94
95
  return
95
96
  }
96
- if (e.key === 'Escape' && getState().selectedVertexIndex >= 0) {
97
- e.preventDefault()
97
+ if (event.key === 'Escape' && getState().selectedVertexIndex >= 0) {
98
+ event.preventDefault()
98
99
  keyMove.start = null
99
100
  keyMove.index = null
100
101
  setState({ selectedVertexIndex: -1, selectedVertexType: null })
101
102
  } else {
102
- handleKey(e)
103
+ handleKey(event)
103
104
  }
104
105
  }
105
106
  }
106
107
 
107
- const buildKeyupHandler = ({ keyMove, onVertexMoved, onDeleted, isFocused }) => (e) => {
108
+ const buildKeyupHandler = ({ getState, keyMove, onVertexMoved, onDeleted, isFocused }) => (event) => {
108
109
  if (isFocused()) {
109
110
  return
110
111
  }
111
- if (ARROW_KEYS.has(e.key) && keyMove.start && keyMove.index != null) {
112
+ // Registered with capture:true below, so this runs first — shadows global Alt+<key>
113
+ // shortcuts (src/utils/globalAltShortcuts.js) only while something is actually selected —
114
+ // the same condition navigateTo/nudge already require locally. edit_point's state always
115
+ // reports selectedVertexIndex: 0 (a point is "always selected" — see pointSelectionState.js),
116
+ // so this naturally stays unconditional for that mode while genuinely gating on selection
117
+ // for edit_vertex, without needing to special-case either.
118
+ if (getState().selectedVertexIndex >= 0) {
119
+ stopIfGlobalAltKey(event)
120
+ }
121
+ if (ARROW_KEYS.has(event.key) && keyMove.start && keyMove.index != null) {
112
122
  // Not hiding the snap indicator here — nudge.js's own snap.apply() already left it showing
113
123
  // correctly, and it should stay that way after the key is released.
114
124
  onVertexMoved({ vertexIndex: keyMove.index, previousCoord: keyMove.start })
115
125
  keyMove.start = null
116
126
  keyMove.index = null
117
127
  }
118
- if (e.key === 'Delete') {
128
+ if (event.key === 'Delete') {
119
129
  onDeleted()
120
130
  }
121
131
  }
@@ -146,7 +156,7 @@ export const createKeyboardHandler = (options) => {
146
156
  const isFocused = () => isInteractiveElementFocused(appViewport)
147
157
 
148
158
  const onKeydown = buildKeydownHandler({ map, getState, setState, nudge, keyMove, onUndo, onKeyboardActive, isFocused })
149
- const onKeyup = buildKeyupHandler({ keyMove, onVertexMoved, onDeleted, isFocused })
159
+ const onKeyup = buildKeyupHandler({ getState, keyMove, onVertexMoved, onDeleted, isFocused })
150
160
 
151
161
  globalThis.addEventListener('keydown', onKeydown, { capture: true })
152
162
  globalThis.addEventListener('keyup', onKeyup, { capture: true })
@@ -27,13 +27,13 @@ const setup = () => {
27
27
 
28
28
  const liveHandlers = []
29
29
  afterEach(() => {
30
- liveHandlers.splice(0).forEach((h) => h.destroy())
30
+ liveHandlers.splice(0).forEach((handler) => handler.destroy())
31
31
  document.body.innerHTML = ''
32
32
  })
33
33
 
34
34
  const key = (type, props) => window.dispatchEvent(new KeyboardEvent(type, { cancelable: true, ...props }))
35
35
 
36
- test('nudgeByDelta (exposed for MoveControls) moves the selected vertex and reports it via onVertexMoved', () => {
36
+ test('nudgeByDelta (exposed for MapControls) moves the selected vertex and reports it via onVertexMoved', () => {
37
37
  const { state, handler, onVertexMoved } = setup()
38
38
  Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
39
39
  handler.nudgeByDelta(1, 0, true)
@@ -178,6 +178,44 @@ test('Delete deletes, ctrl/cmd+z undoes — but not while typing in an input ins
178
178
  expect(onUndo).toHaveBeenCalledTimes(1)
179
179
  })
180
180
 
181
+ test('Alt+Enter keyup stops propagation while a vertex is selected — never reaches the app-wide "highlight label at center" shortcut', () => {
182
+ const { state } = setup()
183
+ state.selectedVertexIndex = 1
184
+ const event = new KeyboardEvent('keyup', { key: 'Enter', altKey: true, cancelable: true, bubbles: true })
185
+ const stopSpy = jest.spyOn(event, 'stopPropagation')
186
+ window.dispatchEvent(event)
187
+ expect(stopSpy).toHaveBeenCalled()
188
+ })
189
+
190
+ test('Alt+Arrow keyup stops propagation too, while a vertex is selected — never reaches the app-wide "highlight next label" shortcut', () => {
191
+ const { state } = setup()
192
+ state.selectedVertexIndex = 1
193
+ const event = new KeyboardEvent('keyup', { key: 'ArrowRight', altKey: true, cancelable: true, bubbles: true })
194
+ const stopSpy = jest.spyOn(event, 'stopPropagation')
195
+ window.dispatchEvent(event)
196
+ expect(stopSpy).toHaveBeenCalled()
197
+ })
198
+
199
+ test('leaves Alt+Enter/Alt+Arrow alone with nothing selected — no local meaning to protect, so map-label selection still works', () => {
200
+ setup() // default state.selectedVertexIndex is -1 — nothing selected
201
+ const enterEvent = new KeyboardEvent('keyup', { key: 'Enter', altKey: true, cancelable: true, bubbles: true })
202
+ const arrowEvent = new KeyboardEvent('keyup', { key: 'ArrowRight', altKey: true, cancelable: true, bubbles: true })
203
+ const enterSpy = jest.spyOn(enterEvent, 'stopPropagation')
204
+ const arrowSpy = jest.spyOn(arrowEvent, 'stopPropagation')
205
+ window.dispatchEvent(enterEvent)
206
+ window.dispatchEvent(arrowEvent)
207
+ expect(enterSpy).not.toHaveBeenCalled()
208
+ expect(arrowSpy).not.toHaveBeenCalled()
209
+ })
210
+
211
+ test('a plain Enter keyup (no Alt) is left alone', () => {
212
+ setup()
213
+ const event = new KeyboardEvent('keyup', { key: 'Enter', altKey: false, cancelable: true, bubbles: true })
214
+ const stopSpy = jest.spyOn(event, 'stopPropagation')
215
+ window.dispatchEvent(event)
216
+ expect(stopSpy).not.toHaveBeenCalled()
217
+ })
218
+
181
219
  test('keys are ignored while an interactive element outside the viewport has focus', () => {
182
220
  const { onDeleted, onKeyboardActive } = setup()
183
221
  const button = document.createElement('button')
@@ -106,12 +106,12 @@ export const wireNudge = ({ map, snap, getState, setState, onInserted, onVertexM
106
106
  moveSelectedVertexBy(dx, dy)
107
107
  }
108
108
 
109
- // Explicit-delta counterpart to nudge(e) — the entry point for MoveControls'
109
+ // Explicit-delta counterpart to nudge(e) — the entry point for MapControls'
110
110
  // D-pad (see mapProvider.activeMoveTarget in events.js). Unlike nudge(e)'s
111
111
  // held-key sequencing (undo pushed on keyup via keyMove, so repeated keydowns
112
112
  // while held batch into one undo step), each call here is one complete,
113
113
  // undoable action — a button click has no "held" state to batch. Midpoints
114
- // aren't handled here: MoveControls only claims the D-pad once a real vertex is
114
+ // aren't handled here: MapControls only claims the D-pad once a real vertex is
115
115
  // selected (see buildVertexMoveTarget in events.js).
116
116
  const nudgeByDelta = (dx, dy, isLargeStep) => {
117
117
  const step = isLargeStep ? KEYBOARD.stepAmount : KEYBOARD.nudgeAmount
@@ -53,7 +53,7 @@ describe('nudging a vertex', () => {
53
53
  })
54
54
  })
55
55
 
56
- describe('nudgeByDelta (MoveControls D-pad)', () => {
56
+ describe('nudgeByDelta (MapControls D-pad)', () => {
57
57
  test('moves the selected vertex by an explicit direction, scaled by isLargeStep, and reports the move for undo', () => {
58
58
  const { state, nudgeByDelta, olFeature, onVertexMoved } = setup()
59
59
  Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
@@ -79,7 +79,7 @@ describe('nudgeByDelta (MoveControls D-pad)', () => {
79
79
  const { state, nudgeByDelta, onVertexMoved } = setup()
80
80
  nudgeByDelta(1, 0, true) // nothing selected
81
81
  Object.assign(state, { selectedVertexIndex: 4, selectedVertexType: 'midpoint' })
82
- nudgeByDelta(1, 0, true) // midpoint selected — MoveControls only claims a real vertex
82
+ nudgeByDelta(1, 0, true) // midpoint selected — MapControls only claims a real vertex
83
83
  state.olFeature = null
84
84
  Object.assign(state, { selectedVertexIndex: 1, selectedVertexType: 'vertex' })
85
85
  nudgeByDelta(1, 0, true)