@defra/interactive-map 0.0.40-alpha → 0.0.42-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 (374) 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/button-definition.md +7 -8
  7. package/docs/api/control-definition.md +17 -1
  8. package/docs/api/panel-definition.md +5 -0
  9. package/docs/api/slots.md +50 -6
  10. package/docs/assets/images/slot-map.svg +61 -262
  11. package/docs/examples/add-polygons.mdx +8 -2
  12. package/docs/examples/add-symbols.mdx +8 -2
  13. package/docs/examples/draw-tools.mdx +3 -3
  14. package/docs/plugins/datasets.md +23 -15
  15. package/docs/plugins/draw.md +79 -16
  16. package/docs/plugins/map-key.md +147 -0
  17. package/docs/plugins/search.md +1 -1
  18. package/docs/plugins.md +5 -1
  19. package/govuk-prototype-kit.config.json +2 -0
  20. package/jest.setup.js +5 -0
  21. package/package.json +9 -1
  22. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  23. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  24. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  25. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  26. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  27. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  28. package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
  29. package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
  30. package/plugins/beta/use-location/src/manifest.js +1 -1
  31. package/plugins/datasets/dist/css/index.css +1 -1
  32. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  33. package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
  34. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  35. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  36. package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
  37. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  38. package/plugins/datasets/dist/umd/index.js +1 -1
  39. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
  40. package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
  41. package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
  42. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +14 -0
  43. package/plugins/datasets/src/api/setOpacity.test.js +9 -0
  44. package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
  45. package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
  46. package/plugins/datasets/src/datasets.scss +0 -1
  47. package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
  48. package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
  49. package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
  50. package/plugins/datasets/src/index.test.js +36 -0
  51. package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
  52. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
  53. package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -4
  54. package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
  55. package/plugins/datasets/src/manifest.js +0 -38
  56. package/plugins/datasets/src/manifest.test.js +49 -0
  57. package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
  58. package/plugins/datasets/src/reducers/pluginState.js +3 -16
  59. package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
  60. package/plugins/datasets/src/registry/dataset.js +13 -1
  61. package/plugins/datasets/src/registry/dataset.test.js +61 -0
  62. package/plugins/datasets/src/registry/datasetRegistry.js +12 -6
  63. package/plugins/datasets/src/registry/datasetRegistry.test.js +16 -4
  64. package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
  65. package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
  66. package/plugins/datasets/src/utils/bbox.test.js +125 -0
  67. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  68. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  69. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  70. package/plugins/draw/dist/esm/index.js +1 -1
  71. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +2 -1
  72. package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
  73. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  74. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  75. package/plugins/draw/dist/umd/index.js +1 -1
  76. package/plugins/draw/src/DrawInit.jsx +10 -2
  77. package/plugins/draw/src/DrawInit.test.jsx +51 -10
  78. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +91 -11
  79. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
  80. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
  81. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
  82. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
  84. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
  85. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
  90. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
  91. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
  92. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
  93. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
  94. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
  95. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
  96. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
  97. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
  98. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
  99. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
  100. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +4 -27
  101. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
  102. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
  103. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
  104. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
  105. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
  106. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
  107. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +3 -53
  108. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
  109. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
  110. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +10 -3
  111. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
  112. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
  113. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
  114. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
  115. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
  116. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
  117. package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
  118. package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
  119. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
  120. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
  121. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
  122. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
  123. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
  124. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
  125. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
  126. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
  127. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
  128. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
  129. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
  130. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
  131. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
  132. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
  133. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
  134. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
  135. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
  136. package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
  137. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
  138. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +12 -0
  139. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +8 -2
  140. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +31 -0
  141. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
  142. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +33 -0
  143. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
  144. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
  145. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +10 -3
  146. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +24 -1
  147. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
  148. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
  149. package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
  150. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
  151. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
  152. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
  153. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
  154. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
  155. package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
  156. package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
  157. package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
  158. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
  159. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
  160. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
  161. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
  162. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
  163. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
  164. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
  165. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
  166. package/plugins/draw/src/api/addFeature.js +16 -1
  167. package/plugins/draw/src/api/addFeature.test.js +62 -5
  168. package/plugins/draw/src/api/createNewShape.js +41 -0
  169. package/plugins/draw/src/api/createNewShape.test.js +63 -0
  170. package/plugins/draw/src/api/editFeature.js +7 -4
  171. package/plugins/draw/src/api/editFeature.test.js +16 -0
  172. package/plugins/draw/src/api/newLine.js +2 -40
  173. package/plugins/draw/src/api/newPoint.js +42 -0
  174. package/plugins/draw/src/api/newPoint.test.js +117 -0
  175. package/plugins/draw/src/api/newPolygon.js +2 -40
  176. package/plugins/draw/src/api/setStyle.js +43 -0
  177. package/plugins/draw/src/api/setStyle.test.js +75 -0
  178. package/plugins/draw/src/events.js +17 -10
  179. package/plugins/draw/src/events.test.js +14 -0
  180. package/plugins/draw/src/manifest.js +18 -6
  181. package/plugins/draw/src/manifest.test.js +32 -1
  182. package/plugins/draw/src/reducer.test.js +7 -0
  183. package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
  184. package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
  185. package/plugins/draw/src/utils/symbolKeys.js +5 -0
  186. package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
  187. package/plugins/draw/src/validation/liveStroke.test.js +64 -1
  188. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  189. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  190. package/plugins/interact/dist/umd/index.js +1 -1
  191. package/plugins/interact/src/hooks/useHighlightSync.js +33 -12
  192. package/plugins/interact/src/hooks/useHighlightSync.test.js +96 -25
  193. package/plugins/interact/src/manifest.js +1 -1
  194. package/plugins/interact/src/utils/featureQueries.js +18 -0
  195. package/plugins/interact/src/utils/featureQueries.test.js +15 -0
  196. package/plugins/interact/src/utils/spatial.js +9 -3
  197. package/plugins/interact/src/utils/spatial.test.js +12 -1
  198. package/plugins/map-key/dist/css/index.css +68 -0
  199. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
  200. package/plugins/map-key/dist/esm/index.js +1 -0
  201. package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
  202. package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
  203. package/plugins/map-key/dist/umd/index.js +2 -0
  204. package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
  205. package/plugins/map-key/package.json +28 -0
  206. package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
  207. package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
  208. package/plugins/map-key/src/components/Key/Key.jsx +43 -0
  209. package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +21 -14
  210. package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
  211. package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
  212. package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
  213. package/plugins/map-key/src/components/Key/KeyItem.jsx +26 -0
  214. package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
  215. package/plugins/map-key/src/components/Key/KeySvg.jsx +41 -0
  216. package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
  217. package/plugins/map-key/src/components/Key/KeySvgLine.jsx +22 -0
  218. package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
  219. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.jsx +7 -2
  220. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +8 -2
  221. package/plugins/map-key/src/components/Key/KeySvgRect.jsx +26 -0
  222. package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
  223. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.jsx +8 -3
  224. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +17 -6
  225. package/plugins/map-key/src/components/Key/MapKey.jsx +53 -0
  226. package/plugins/map-key/src/components/Key/MapKey.test.jsx +147 -0
  227. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
  228. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
  229. package/plugins/map-key/src/index.js +13 -0
  230. package/plugins/map-key/src/index.test.js +31 -0
  231. package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
  232. package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
  233. package/plugins/map-key/src/manifest.js +30 -0
  234. package/plugins/map-key/src/manifest.test.js +63 -0
  235. package/plugins/map-key/src/mapKey.scss +4 -0
  236. package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
  237. package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
  238. package/plugins/map-key/src/registry/index.js +3 -0
  239. package/plugins/map-key/src/registry/index.test.js +15 -0
  240. package/plugins/menu/dist/css/index.css +80 -0
  241. package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
  242. package/plugins/menu/dist/esm/index.js +1 -0
  243. package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
  244. package/plugins/menu/dist/umd/index.js +2 -0
  245. package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
  246. package/plugins/menu/package.json +28 -0
  247. package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
  248. package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
  249. package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
  250. package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
  251. package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
  252. package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
  253. package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
  254. package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
  255. package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
  256. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
  257. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
  258. package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
  259. package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
  260. package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
  261. package/plugins/menu/src/index.js +13 -0
  262. package/plugins/menu/src/index.test.js +31 -0
  263. package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
  264. package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
  265. package/plugins/menu/src/initialise/createMenu.js +10 -0
  266. package/plugins/menu/src/initialise/createMenu.test.js +37 -0
  267. package/plugins/menu/src/manifest.js +35 -0
  268. package/plugins/menu/src/manifest.test.js +63 -0
  269. package/plugins/menu/src/menu.scss +4 -0
  270. package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
  271. package/plugins/menu/src/reducers/pluginState.js +35 -0
  272. package/plugins/menu/src/reducers/pluginState.test.js +57 -0
  273. package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
  274. package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
  275. package/plugins/menu/src/registry/index.js +3 -0
  276. package/plugins/menu/src/registry/index.test.js +15 -0
  277. package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
  278. package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
  279. package/plugins/search/dist/css/index.css +1 -1
  280. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  281. package/plugins/search/dist/esm/index.js +1 -1
  282. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  283. package/plugins/search/dist/umd/index.js +1 -1
  284. package/plugins/search/src/Search.jsx +2 -3
  285. package/plugins/search/src/components/Form/Form.module.scss +9 -20
  286. package/plugins/search/src/index.js +0 -1
  287. package/plugins/search/src/index.test.js +2 -4
  288. package/plugins/search/src/manifest.js +1 -1
  289. package/plugins/search/src/search.scss +17 -16
  290. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  291. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  292. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  293. package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
  294. package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
  295. package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
  296. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
  297. package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
  298. package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
  299. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  300. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  301. package/providers/maplibre/dist/umd/index.js +1 -1
  302. package/providers/maplibre/src/utils/drawLayerBuckets.js +14 -0
  303. package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
  304. package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
  305. package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
  306. package/providers/maplibre/src/utils/hoverCursor.js +30 -14
  307. package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
  308. package/providers/maplibre/src/utils/patternImages.js +1 -1
  309. package/providers/maplibre/src/utils/symbolImages.js +41 -3
  310. package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
  311. package/rollup.esm.mjs +11 -1
  312. package/src/App/components/Actions/Actions.test.jsx +47 -0
  313. package/src/App/components/Hints/Hints.test.jsx +13 -0
  314. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +25 -23
  315. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +22 -5
  316. package/src/App/components/MoveControls/MoveControls.module.scss +41 -10
  317. package/src/App/components/Panel/Panel.jsx +65 -8
  318. package/src/App/components/Panel/Panel.module.scss +49 -15
  319. package/src/App/components/Panel/Panel.test.jsx +114 -1
  320. package/src/App/components/Tabs/Tabs.jsx +43 -17
  321. package/src/App/components/Tabs/Tabs.module.scss +32 -5
  322. package/src/App/components/Tabs/Tabs.test.jsx +40 -10
  323. package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
  324. package/src/App/hooks/useFeatureItems.js +16 -7
  325. package/src/App/hooks/useFeatureItems.test.js +57 -0
  326. package/src/App/hooks/useLayoutMeasurements.js +95 -84
  327. package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
  328. package/src/App/layout/Layout.jsx +9 -10
  329. package/src/App/layout/Layout.test.jsx +2 -0
  330. package/src/App/layout/layout.module.scss +31 -17
  331. package/src/App/registry/pluginRegistry.js +1 -1
  332. package/src/App/registry/pluginRegistry.test.js +12 -0
  333. package/src/App/renderer/HtmlElementHost.jsx +18 -5
  334. package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
  335. package/src/App/renderer/groupByKey.js +28 -0
  336. package/src/App/renderer/groupByKey.test.js +39 -0
  337. package/src/App/renderer/groupIntoTabs.js +42 -0
  338. package/src/App/renderer/groupIntoTabs.test.js +95 -0
  339. package/src/App/renderer/mapButtons.js +97 -129
  340. package/src/App/renderer/mapButtons.test.js +67 -83
  341. package/src/App/renderer/mapControls.js +4 -1
  342. package/src/App/renderer/mapControls.test.js +32 -0
  343. package/src/App/renderer/mapPanels.js +53 -11
  344. package/src/App/renderer/mapPanels.test.js +95 -2
  345. package/src/App/renderer/orderItems.js +28 -0
  346. package/src/App/renderer/orderItems.test.js +50 -0
  347. package/src/App/renderer/slotAggregator.js +2 -18
  348. package/src/App/renderer/slotHelpers.js +3 -1
  349. package/src/App/renderer/slotHelpers.test.js +5 -0
  350. package/src/App/renderer/slots.js +3 -0
  351. package/src/App/store/AppProvider.jsx +1 -0
  352. package/src/App/store/appDispatchMiddleware.js +1 -1
  353. package/src/App/store/appDispatchMiddleware.test.js +8 -0
  354. package/src/config/appConfig.js +2 -2
  355. package/src/config/events.js +2 -0
  356. package/src/scss/settings/_colors.scss +1 -0
  357. package/src/scss/settings/_dimensions.scss +2 -3
  358. package/src/scss/settings/_transition.scss +1 -1
  359. package/src/services/eventBus.js +44 -0
  360. package/src/services/eventBus.test.js +83 -0
  361. package/src/services/symbolRegistry.js +1 -1
  362. package/src/types.js +31 -3
  363. package/webpack.umd.mjs +2 -0
  364. package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
  365. package/plugins/datasets/src/components/Key/Key.jsx +0 -50
  366. package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
  367. package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
  368. package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
  369. package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
  370. package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
  371. package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
  372. /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
  373. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
  374. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.test.js +0 -0
@@ -0,0 +1,101 @@
1
+ import { createHarness, svgTarget } from './__helpers__/harness.js'
2
+
3
+ describe('touchHandlers', () => {
4
+ test('addTouchPointTarget resolves colours from map._drawPluginConfig', () => {
5
+ const { ctx, state, map } = createHarness()
6
+ map._drawPluginConfig = { editVertex: '#custom' }
7
+ ctx.addTouchPointTarget(state)
8
+ expect(state.touchPointTarget.style.getPropertyValue('--draw-primary')).toBe('#custom')
9
+ })
10
+
11
+ test('updateTouchPointTarget shows the target for a touch selection and hides it otherwise', () => {
12
+ const { ctx, state } = createHarness()
13
+ state.interfaceType = 'touch'
14
+ ctx.updateTouchPointTarget(state, { x: 12, y: 34 })
15
+ expect(state.touchPointTarget.style.display).toBe('block')
16
+ ctx.updateTouchPointTarget(state, null)
17
+ expect(state.touchPointTarget.style.display).toBe('none')
18
+ })
19
+
20
+ test('hideTouchPointIndicator hides the target', () => {
21
+ const { ctx, state } = createHarness()
22
+ state.touchPointTarget.style.display = 'block'
23
+ ctx.hideTouchPointIndicator(state)
24
+ expect(state.touchPointTarget.style.display).toBe('none')
25
+ })
26
+
27
+ test('onPointerevent only tracks the interface type — the point is always selected, so there is nothing to deselect', () => {
28
+ const { ctx, state, api } = createHarness()
29
+ ctx.onPointerevent(state, { pointerType: 'mouse' })
30
+ expect(state.interfaceType).toBe('mouse')
31
+ expect(api.changeMode).not.toHaveBeenCalled()
32
+
33
+ ctx.onPointerevent(state, { pointerType: 'touch' })
34
+ expect(state.interfaceType).toBe('touch')
35
+ expect(api.changeMode).not.toHaveBeenCalled()
36
+ })
37
+
38
+ test('onTap is a deliberate no-op — a stray tap never relocates the point', () => {
39
+ const { ctx, state, map } = createHarness()
40
+ const before = [...state.feature.coordinates]
41
+ expect(() => ctx.onTap(state, { featureTarget: undefined })).not.toThrow()
42
+ expect(state.feature.coordinates).toEqual(before)
43
+ expect(map.fire).not.toHaveBeenCalledWith('draw.geometrychange', expect.anything())
44
+ })
45
+
46
+ test('onTouchend records a move undo only when a touch move occurred', () => {
47
+ const { ctx, state, map } = createHarness()
48
+ ctx.onTouchend({}) // no state → no throw
49
+ ctx.onTouchend({ ...state, _touchMoved: false }) // no move → no undo
50
+ expect(map._undoStack).toHaveLength(0)
51
+ state._touchMoved = true
52
+ state._moveStartPosition = [0, 0]
53
+ ctx.onTouchend(state)
54
+ expect(map._undoStack.pop()).toMatchObject({ type: 'move_point', vertexIndex: 0, previousPosition: [0, 0] })
55
+ expect(state._moveStartPosition).toBeNull()
56
+ })
57
+
58
+ // Regression: mid-drag the target tracks the raw finger 1:1 from its touchstart-time offset
59
+ // (see onTouchmove), so a mid-drag snap — which moves the point without moving the target by
60
+ // the same amount — leaves the two out of alignment. Re-syncing on touchend to the point's
61
+ // actual final coordinate stops that drift from compounding on the next drag's delta math.
62
+ test('onTouchend re-syncs the target to the point\'s actual final coordinate', () => {
63
+ const { ctx, state } = createHarness()
64
+ state.interfaceType = 'touch'
65
+ state.feature.coordinates = [9, 9]
66
+ ctx.onTouchend(state)
67
+ expect(state.touchPointTarget.style.display).toBe('block')
68
+ expect(state.touchPointTarget.style.left).toBe(`${9 * 10}px`) // harness project(): coord * 10
69
+ expect(state.touchPointTarget.style.top).toBe(`${9 * 10}px`)
70
+ })
71
+
72
+ test('onTouchstart captures move start state, ignoring taps off the point/target', () => {
73
+ const { ctx, state } = createHarness()
74
+ ctx.onTouchstart(state, { target: { parentNode: document.createElement('div') }, touches: [{ clientX: 1, clientY: 1 }] })
75
+ expect(state._moveStartPosition).toBeUndefined()
76
+
77
+ ctx.onTouchstart(state, { target: svgTarget(state), touches: [{ clientX: 20, clientY: 20 }] })
78
+ expect(state._moveStartPosition).toEqual([5, 5])
79
+ expect(state._touchMoved).toBe(false)
80
+ })
81
+
82
+ // resolveTouchDragCoord's own snap/fallback branches are covered by
83
+ // utils/touchDragMath.test.js — this checks onTouchmove ignores off-target moves and wires
84
+ // an active snap through to the point's coordinate.
85
+ test('onTouchmove moves the point, honouring snap, and ignores non-target moves', () => {
86
+ const { ctx, state, map } = createHarness()
87
+ ctx.onTouchstart(state, { target: svgTarget(state), touches: [{ clientX: 50, clientY: 50 }] })
88
+
89
+ ctx.onTouchmove(state, { target: { parentNode: document.createElement('div') }, touches: [{ clientX: 5, clientY: 5 }] })
90
+ expect(state.feature.coordinates).toEqual([5, 5]) // off-target move ignored
91
+
92
+ ctx.onTouchmove(state, { target: svgTarget(state), touches: [{ clientX: 60, clientY: 70 }] })
93
+ expect(state.feature.coordinates).not.toEqual([5, 5])
94
+ expect(state._touchMoved).toBe(true)
95
+
96
+ state.getSnapEnabled = () => true
97
+ map._snapInstance = { status: true, snapStatus: true, snapCoords: [7, 8], snapToClosestPoint: jest.fn() }
98
+ ctx.onTouchmove(state, { target: svgTarget(state), touches: [{ clientX: 80, clientY: 90 }] })
99
+ expect(state.feature.coordinates).toEqual([7, 8])
100
+ })
101
+ })
@@ -0,0 +1,46 @@
1
+ import { scalePoint } from '../editVertexMode/helpers.js'
2
+ import { sharedUndoHandlers } from '../../utils/geometryValidation.js'
3
+
4
+ // Map an undo-stack op type onto the geometry-change `phase` consumed by validation. Read off
5
+ // `this` by the shared pushUndo mixin (utils/geometryValidation.js) — see that file's comment.
6
+ const UNDO_OP_PHASE = { move_point: 'commit-move' }
7
+
8
+ // Undoing a move commits the inverse move, so it re-validates with the same phase.
9
+ const UNDO_INVERSE_PHASE = { move_point: 'commit-move' }
10
+
11
+ // Same dispatch-by-op-type pattern as editVertexMode/undoHandlers.js, but with a single op
12
+ // type — there's no insert/delete concept for a Point's one coordinate — so undoMovePoint
13
+ // writes the coordinate directly, with no getRingSegments/getSegmentForIndex/
14
+ // getModifiableCoords lookup needed at all.
15
+ export const undoHandlers = {
16
+ ...sharedUndoHandlers,
17
+ UNDO_OP_PHASE,
18
+ UNDO_INVERSE_PHASE,
19
+
20
+ handleUndo (state) {
21
+ const undoStack = this.map._undoStack
22
+ if (!undoStack || undoStack.length === 0) {
23
+ return
24
+ }
25
+
26
+ const op = undoStack.pop()
27
+
28
+ if (op.type === 'move_point') {
29
+ this.undoMovePoint(state, op)
30
+ }
31
+ // An undo commits the inverse change, so it must re-validate like any other
32
+ // commit — otherwise the invalid stroke and the Done gate go stale.
33
+ this.emitGeometryValidation(UNDO_INVERSE_PHASE[op.type], op.vertexIndex, op.featureId)
34
+ },
35
+
36
+ undoMovePoint (state, op) {
37
+ const { previousPosition, featureId } = op
38
+ const feature = this.getFeature(featureId)
39
+ if (!feature) { return }
40
+
41
+ feature.updateCoordinate('', previousPosition[0], previousPosition[1])
42
+ this._ctx.store.render()
43
+ this.fireGeometryChange(state)
44
+ this.updateTouchPointTarget(state, scalePoint(this.map.project(previousPosition), state.scale))
45
+ }
46
+ }
@@ -0,0 +1,42 @@
1
+ import { createHarness } from './__helpers__/harness.js'
2
+
3
+ // fireGeometryChange/emitGeometryValidation/pushUndo are the shared mixin from
4
+ // utils/geometryValidation.js — their own behaviour is covered by
5
+ // utils/geometryValidation.test.js. This file only tests what's specific to editPointMode:
6
+ // pushUndo's UNDO_OP_PHASE wiring for 'move_point', handleUndo's single-op-type dispatch,
7
+ // and undoMovePoint itself.
8
+ describe('undoHandlers', () => {
9
+ test('pushUndo maps move_point to the commit-move phase', () => {
10
+ jest.useFakeTimers()
11
+ const { ctx, map } = createHarness()
12
+ map.fire.mockClear()
13
+ ctx.pushUndo({ type: 'move_point', featureId: 'feat-1', vertexIndex: 0 })
14
+ jest.runAllTimers()
15
+ expect(map.fire).toHaveBeenCalledWith('draw.geometrychange', expect.objectContaining({ phase: 'commit-move' }))
16
+ jest.useRealTimers()
17
+ })
18
+
19
+ test('handleUndo ignores an empty stack and dispatches the single move_point op type', () => {
20
+ const { ctx, state, map } = createHarness()
21
+ ctx.handleUndo(state) // empty → no throw
22
+ const spy = jest.spyOn(ctx, 'undoMovePoint').mockImplementation(() => {})
23
+ map._undoStack.push({ type: 'move_point', featureId: 'feat-1', vertexIndex: 0, previousPosition: [0, 0] })
24
+ ctx.handleUndo(state)
25
+ expect(spy).toHaveBeenCalledTimes(1)
26
+
27
+ map._undoStack.push({ type: 'unknown' })
28
+ expect(() => ctx.handleUndo(state)).not.toThrow()
29
+ })
30
+
31
+ // handleUndo's own call to emitGeometryValidation with the inverse phase isn't
32
+ // re-asserted here — for a Point, UNDO_OP_PHASE and UNDO_INVERSE_PHASE are the same
33
+ // single-entry map, so it would just re-confirm the 'commit-move' phase already checked
34
+ // above via pushUndo. (editVertexMode's equivalent op/inverse maps genuinely differ —
35
+ // see its own undoHandlers.test.js.)
36
+ test('undoMovePoint restores the previous position directly, guarding a missing feature', () => {
37
+ const { ctx, state } = createHarness()
38
+ ctx.undoMovePoint({ ...state, featureId: 'missing' }, { previousPosition: [0, 0], featureId: 'missing' })
39
+ ctx.undoMovePoint(state, { previousPosition: [3, 4], featureId: 'feat-1' })
40
+ expect(ctx.getFeature('feat-1').coordinates).toEqual([3, 4])
41
+ })
42
+ })
@@ -0,0 +1,151 @@
1
+ import MapboxDraw from '@mapbox/mapbox-gl-draw'
2
+ import doubleClickZoom from '../../../../../../node_modules/@mapbox/mapbox-gl-draw/src/lib/double_click_zoom.js' // NOSONAR
3
+ import { CUSTOM_DRAW_EVENTS } from '../drawEvents.js'
4
+ import { scalePoint } from './editVertexMode/helpers.js'
5
+ import { bindEditModeListeners, unbindEditModeListeners, clearActiveSnapIndicator, buildEditModeHandlers } from '../utils/editModeEvents.js'
6
+ import { undoHandlers } from './editPointMode/undoHandlers.js'
7
+ import { touchHandlers } from './editPointMode/touchHandlers.js'
8
+ import { pointOperations } from './editPointMode/pointOperations.js'
9
+ import { keyboardHandlers } from './editPointMode/keyboardHandlers.js'
10
+ import { pointerHandlers } from './editPointMode/pointerHandlers.js'
11
+
12
+ /**
13
+ * Relocating an already-placed single-coordinate feature. Composed the same way
14
+ * editVertexMode.js is, but cannot inherit DirectSelect.onSetup (throws a TypeError on a
15
+ * Point — see this file's own onSetup) or its ring-shaped toDisplayFeatures/changeMode —
16
+ * the point has one coordinate, always selected, with no vertex list to navigate.
17
+ */
18
+ export const EditPointMode = {
19
+ ...MapboxDraw.modes.direct_select,
20
+ ...undoHandlers,
21
+ ...touchHandlers,
22
+ ...pointOperations,
23
+ ...keyboardHandlers,
24
+ ...pointerHandlers,
25
+
26
+ onSetup (options) {
27
+ const featureId = options.featureId
28
+ const feature = this.getFeature(featureId)
29
+
30
+ if (!feature) {
31
+ throw new Error('You must provide a featureId to enter edit_point mode')
32
+ }
33
+
34
+ const state = {
35
+ featureId,
36
+ feature,
37
+ dragMoveLocation: null,
38
+ dragMoving: false,
39
+ canDragMove: false,
40
+ // Empty, not [''] — this is what lets DirectSelect's own onFeature/onDrag/dragFeature
41
+ // machinery move a Point via its stock whole-feature-delta path unmodified.
42
+ selectedCoordPaths: []
43
+ }
44
+
45
+ this.setSelectedCoordinates([])
46
+ this.setSelected(featureId)
47
+ doubleClickZoom.disable(this)
48
+ this.setActionableState({ trash: false })
49
+
50
+ Object.assign(state, {
51
+ container: options.container,
52
+ interfaceType: options.interfaceType,
53
+ undoButtonId: options.undoButtonId,
54
+ isPanEnabled: options.isPanEnabled,
55
+ getSnapEnabled: options.getSnapEnabled,
56
+ scale: options.scale ?? 1
57
+ })
58
+
59
+ // Clear undo stack only on initial entry to edit mode for a feature (same convention
60
+ // as edit_vertex, so switching Undo between features never applies a stale op).
61
+ if (this.map._lastEditFeatureId !== state.featureId) {
62
+ this.map._undoStack?.clear()
63
+ this.map._lastEditFeatureId = state.featureId
64
+ }
65
+
66
+ this.setupEventListeners(state)
67
+ this.map._drawEditContainer = options.container
68
+ this.addTouchPointTarget(state)
69
+
70
+ // edit_vertex's first interaction is always a click/tap that selects a vertex — which,
71
+ // as a side effect, moves focus onto the map viewport, satisfying keyboardHandlers.js's
72
+ // isInteractiveElementFocused guard (it only blocks shortcuts when focus is OUTSIDE
73
+ // state.container). A point is already selected from the moment this mode starts, so a
74
+ // keyboard-only session can go straight to arrow keys with no click in between — meaning
75
+ // focus may still be sitting on whatever button opened this mode. Claim it explicitly so
76
+ // that guard reads correctly from the first keypress, not just after an incidental click.
77
+ state.container?.focus({ preventScroll: true })
78
+
79
+ clearActiveSnapIndicator(this.map)
80
+
81
+ // Show the touch target if entering on a touch interface.
82
+ if (state.interfaceType === 'touch') {
83
+ setTimeout(() => {
84
+ this.updateTouchPointTarget(state, scalePoint(this.map.project(state.feature.coordinates), state.scale))
85
+ }, 0)
86
+ }
87
+
88
+ // Claim the D-pad once for the whole session. Unlike edit_vertex (which re-fires this on
89
+ // every vertex selection change), a point never emits draw.vertexchange — that event
90
+ // unconditionally releases mapProvider.activeMoveTarget, so it must never fire here.
91
+ // attachEvents' resetState() (events.js) is the release point, on Done/Cancel/edit-finish.
92
+ // numVertecies (misspelled) — MaplibreDrawAdapter.js's vertexselection handler only reads
93
+ // that exact key when normalising to the adapter contract's numVertices; firing the
94
+ // correctly-spelled key here would get silently clobbered to undefined instead.
95
+ this.map.fire(CUSTOM_DRAW_EVENTS.VERTEX_SELECTION, { index: 0, numVertecies: 1 })
96
+
97
+ return state
98
+ },
99
+
100
+ setupEventListeners (state) {
101
+ const handlers = this.handlers = buildEditModeHandlers(this, state, { nudge: this.onNudgePoint })
102
+ bindEditModeListeners(state, this.map, handlers)
103
+ },
104
+
105
+ // active isn't cosmetic here — mapbox-gl-draw's own isActiveFeature()/isInactiveFeature()
106
+ // selectors (used by DirectSelect.onMouseDown to route mousedown into onFeature/
107
+ // startDragging, and by this mode's own onClick to tell "click the point" apart from
108
+ // "click elsewhere") key off it, and Feature.prototype.internal() defaults it to 'false'.
109
+ // Without setting it here, mousedown never starts a drag and every click — including on
110
+ // the point itself — falls through to onClick's relocate branch. Deliberately does not
111
+ // call createSupplementaryPoints, which would draw a plain vertex disc over the icon.
112
+ toDisplayFeatures (state, geojson, push) {
113
+ geojson.properties.active = state.featureId === geojson.properties.id ? 'true' : 'false'
114
+ push(geojson)
115
+ },
116
+
117
+ onScaleChange (state, event) {
118
+ state.scale = event.scale
119
+ },
120
+
121
+ onInterfaceTypeChange (state, event) {
122
+ state.interfaceType = event.interfaceType
123
+ this.updateTouchPointTarget(state, scalePoint(this.map.project(state.feature.coordinates), state.scale))
124
+ },
125
+
126
+ onMove (state) {
127
+ this.updateTouchPointTarget(state, scalePoint(this.map.project(state.feature.coordinates), state.scale))
128
+ },
129
+
130
+ // Inbound signal from MaplibreDrawAdapter.nudgeSelectedVertex — the D-pad entry point for
131
+ // all three interface types (see mapProvider.activeMoveTarget in events.js). Repositions
132
+ // the touch target the same way onMove/onInterfaceTypeChange do, since nudgePointByDelta
133
+ // has no equivalent hook of its own.
134
+ onNudgePoint (state, event) {
135
+ this.nudgePointByDelta(state, event.dx, event.dy, event.isLargeStep)
136
+ this.updateTouchPointTarget(state, scalePoint(this.map.project(state.feature.coordinates), state.scale))
137
+ },
138
+
139
+ onButtonClick (state, event) {
140
+ if (event.target.closest(`#${state.undoButtonId}`)) {
141
+ this.handleUndo(state)
142
+ }
143
+ },
144
+
145
+ onStop (state) {
146
+ this.map._drawEditContainer = null
147
+ this.map._editingFeatureId = null
148
+ unbindEditModeListeners(state, this.map, this.handlers)
149
+ this.hideTouchPointIndicator(state)
150
+ }
151
+ }
@@ -0,0 +1,156 @@
1
+ import { createHarness } from './editPointMode/__helpers__/harness.js'
2
+ import { isActiveFeature } from '../../../../../../node_modules/@mapbox/mapbox-gl-draw/src/lib/common_selectors.js' // NOSONAR
3
+
4
+ /**
5
+ * Tests for EditPointMode's own methods: setup/teardown lifecycle, scale/move/interface-type
6
+ * events, and toDisplayFeatures/button routing. The keyboard, pointer, touch, undo and
7
+ * point-operation handlers are covered in their own colocated test files under editPointMode/.
8
+ */
9
+
10
+ describe('onSetup / onStop lifecycle', () => {
11
+ test('builds state without throwing on a Point (DirectSelect.onSetup itself throws a TypeError here)', () => {
12
+ const { state, map } = createHarness()
13
+ expect(state.featureId).toBe('feat-1')
14
+ expect(state.selectedCoordPaths).toEqual([])
15
+ expect(state.feature.coordinates).toEqual([5, 5])
16
+ expect(map._lastEditFeatureId).toBe('feat-1')
17
+ expect(map._drawEditContainer).toBe(state.container)
18
+ })
19
+
20
+ // Regression: a keyboard-only session has no click/tap to move focus onto the map first
21
+ // (the point is already selected from the moment this mode starts) — without claiming
22
+ // focus explicitly, keyboardHandlers.js's isInteractiveElementFocused guard would block
23
+ // every arrow key until an unrelated click happened to land on the viewport.
24
+ test('claims focus on the container so a keyboard-only session works without a prior click', () => {
25
+ const { state } = createHarness()
26
+ expect(document.activeElement).toBe(state.container)
27
+ })
28
+
29
+ test('throws when no feature is found for the given featureId', () => {
30
+ const { ctx, options } = createHarness()
31
+ expect(() => ctx.onSetup({ ...options, featureId: 'missing' })).toThrow()
32
+ })
33
+
34
+ test('claims the D-pad exactly once at setup, never followed by draw.vertexchange', () => {
35
+ const { map } = createHarness()
36
+ // numVertecies (misspelled) — MaplibreDrawAdapter.js's vertexselection handler only reads
37
+ // that exact key when normalising to the adapter contract's numVertices.
38
+ expect(map.fire).toHaveBeenCalledWith('draw.vertexselection', { index: 0, numVertecies: 1 })
39
+ expect(map.fire).not.toHaveBeenCalledWith('draw.vertexchange', expect.anything())
40
+ })
41
+
42
+ // Shared listener wiring (scalechange, nudge, pointer/touch/keyboard) is covered by
43
+ // editModeEvents.test.js — this only asserts what's specific to edit_point: no ring-only
44
+ // vertex/update events.
45
+ test("does not register the ring-only vertex/update events — those are edit_vertex's alone", () => {
46
+ const { map } = createHarness()
47
+ expect(map.on).not.toHaveBeenCalledWith('draw.selectionchange', expect.any(Function))
48
+ expect(map.on).not.toHaveBeenCalledWith('draw.update', expect.any(Function))
49
+ })
50
+
51
+ test('does not clear the undo stack when re-entering the same feature', () => {
52
+ const { ctx, map, options } = createHarness()
53
+ map._undoStack.push({ type: 'move_point' })
54
+ // map._lastEditFeatureId is already 'feat-1' from the first setup
55
+ ctx.onSetup(options)
56
+ expect(map._undoStack).toHaveLength(1)
57
+ })
58
+
59
+ // Listener teardown itself is covered by editModeEvents.test.js.
60
+ test('onStop clears the editing container; DirectSelect touch stubs are inert', () => {
61
+ const { ctx, state, map } = createHarness()
62
+ expect(() => { ctx.onTouchStart(); ctx.onTouchMove(); ctx.onTouchEnd() }).not.toThrow()
63
+ ctx.onStop(state)
64
+ expect(map._drawEditContainer).toBeNull()
65
+ })
66
+
67
+ test('clears an active snap indicator and positions the touch target when entering on touch', () => {
68
+ jest.useFakeTimers()
69
+ const { ctx, map, options } = createHarness()
70
+ map._snapInstance = { status: true, snapStatus: false, snapCoords: null }
71
+ map.getLayer.mockClear()
72
+
73
+ const state = ctx.onSetup({ ...options, interfaceType: 'touch' })
74
+ expect(map.getLayer).toHaveBeenCalledWith('snap-helper-circle')
75
+ jest.runAllTimers()
76
+ expect(state.touchPointTarget.style.display).toBe('block')
77
+ jest.useRealTimers()
78
+ })
79
+ })
80
+
81
+ describe('toDisplayFeatures', () => {
82
+ test('flags the edited feature active (isActiveFeature() depends on it — see the mode\'s own comment) and pushes every feature, without supplementary points', () => {
83
+ const { ctx, state } = createHarness()
84
+ const pushed = []
85
+ const push = (f) => pushed.push(f)
86
+
87
+ ctx.toDisplayFeatures(state, { properties: { id: 'feat-1' } }, push)
88
+ ctx.toDisplayFeatures(state, { properties: { id: 'other' } }, push)
89
+
90
+ expect(pushed).toEqual([
91
+ { properties: { id: 'feat-1', active: 'true' } },
92
+ { properties: { id: 'other', active: 'false' } }
93
+ ])
94
+ })
95
+
96
+ // Regression: mapbox-gl-draw's own isActiveFeature() selector (which DirectSelect.onMouseDown
97
+ // uses to route a mousedown into onFeature/startDragging, and which this mode's own onClick
98
+ // uses to tell "click the point" apart from "click elsewhere") requires active === 'true' on
99
+ // the rendered feature — asserting against the real selector, not a hand-built fixture,
100
+ // catches a regression here that unit tests using a hand-built featureTarget would miss.
101
+ test('the pushed feature is recognised by the real isActiveFeature() selector', () => {
102
+ const { ctx, state } = createHarness()
103
+ const pushed = []
104
+ // meta: 'feature' mirrors what mapbox-gl-draw's own Feature.prototype.internal() sets on
105
+ // every stored feature's rendered GeoJSON — toDisplayFeatures only ever adds `active`.
106
+ ctx.toDisplayFeatures(state, { properties: { id: state.featureId, meta: 'feature' } }, (f) => pushed.push(f))
107
+ expect(isActiveFeature({ featureTarget: pushed[0] })).toBe(true)
108
+ })
109
+ })
110
+
111
+ describe('scale, move and interface-type events', () => {
112
+ test('onScaleChange and onInterfaceTypeChange update state and reposition the touch target', () => {
113
+ const { ctx, state } = createHarness()
114
+ ctx.onScaleChange(state, { scale: 2 })
115
+ expect(state.scale).toBe(2)
116
+ ctx.onInterfaceTypeChange(state, { interfaceType: 'touch' })
117
+ expect(state.interfaceType).toBe('touch')
118
+ })
119
+
120
+ test('onMove keeps the touch target aligned with the point', () => {
121
+ const { ctx, state } = createHarness()
122
+ state.interfaceType = 'touch'
123
+ ctx.onMove(state)
124
+ expect(state.touchPointTarget.style.display).toBe('block')
125
+ })
126
+
127
+ test('draw.nudgevertex moves the point and repositions the touch target — the inbound bridge for MoveControls.mapProvider.activeMoveTarget', () => {
128
+ jest.useFakeTimers()
129
+ const { state, map } = createHarness(undefined, { interfaceType: 'touch' })
130
+ jest.runAllTimers() // flush onSetup's deferred initial touch-target positioning
131
+ const before = [...state.feature.coordinates]
132
+ const targetBefore = { top: state.touchPointTarget.style.top, left: state.touchPointTarget.style.left }
133
+ map.fire('draw.nudgevertex', { dx: 1, dy: 0, isLargeStep: true })
134
+ expect(state.feature.coordinates).not.toEqual(before)
135
+ expect({ top: state.touchPointTarget.style.top, left: state.touchPointTarget.style.left }).not.toEqual(targetBefore)
136
+ jest.useRealTimers()
137
+ })
138
+ })
139
+
140
+ describe('button routing', () => {
141
+ test('onButtonClick only wires the undo control — there is no delete-vertex button in this mode', () => {
142
+ const { ctx, state, container } = createHarness()
143
+ const undo = document.createElement('button')
144
+ undo.id = 'undo-vertex'
145
+ container.append(undo)
146
+ const undoSpy = jest.spyOn(ctx, 'handleUndo').mockImplementation(() => {})
147
+
148
+ ctx.onButtonClick(state, { target: undo })
149
+ expect(undoSpy).toHaveBeenCalled()
150
+
151
+ const other = document.createElement('button')
152
+ container.append(other)
153
+ ctx.onButtonClick(state, { target: other })
154
+ expect(undoSpy).toHaveBeenCalledTimes(1)
155
+ })
156
+ })
@@ -1,26 +1,14 @@
1
1
  import { getSnapInstance, clearSnapIndicator } from '../../utils/snapHelpers.js'
2
+ import { ARROW_KEYS, ARROW_OFFSETS, isInteractiveElementFocused, isUndoShortcut, sharedKeyboardHandlers } from '../../utils/keyboardShortcuts.js'
2
3
  import { getCoords } from './geometryHelpers.js'
3
4
 
4
- const ARROW_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'])
5
- const ARROW_OFFSETS = { ArrowUp: [0, -1], ArrowDown: [0, 1], ArrowLeft: [-1, 0], ArrowRight: [1, 0] }
6
- const INTERACTIVE_TAGS = new Set(['INPUT', 'TEXTAREA', 'BUTTON', 'SELECT', 'A'])
7
-
8
- // Keyboard shortcuts are ignored while a form control outside the map viewport has
9
- // focus, but still work for elements inside the viewport (e.g. draw toolbar buttons).
10
- const isInteractiveElementFocused = (state) => {
11
- const el = document.activeElement
12
- if (!el || el === document.body) { return false }
13
- if (state.container?.contains(el)) { return false }
14
- return INTERACTIVE_TAGS.has(el.tagName) || el.isContentEditable || el.hasAttribute('tabindex')
15
- }
16
-
17
- const isUndoShortcut = (e) => e.key === 'z' && (e.metaKey || e.ctrlKey) && !e.shiftKey
18
-
19
5
  /**
20
6
  * Keyboard interaction for the vertex-edit mode: arrow-key vertex movement/insertion,
21
7
  * space-to-select, Escape, and Cmd/Ctrl+Z undo. Mixed into EditVertexMode.
22
8
  */
23
9
  export const keyboardHandlers = {
10
+ ...sharedKeyboardHandlers,
11
+
24
12
  onKeydown (state, e) {
25
13
  if (isInteractiveElementFocused(state)) {
26
14
  return
@@ -105,24 +93,13 @@ export const keyboardHandlers = {
105
93
  },
106
94
 
107
95
  // Resolve the destination coordinate for a keyboard nudge, applying or breaking
108
- // snap — delegates to the shared resolver (vertexOperations.js) also used by
96
+ // snap — delegates to the shared resolver (utils/snapMovement.js) also used by
109
97
  // MoveControls' nudgeVertexByDelta, so both snap identically.
110
98
  _keyboardMoveTarget (state, e, currentCoord) {
111
99
  const [dx, dy] = ARROW_OFFSETS[e.key]
112
100
  return this.resolveSnapTarget(state, dx, dy, currentCoord, () => this.getNewCoord(state, e))
113
101
  },
114
102
 
115
- // Cmd/Ctrl+Z: undo the last edit, unless the user is typing in a text field.
116
- handleUndoShortcut (state, e) {
117
- const tag = document.activeElement?.tagName
118
- if (tag === 'INPUT' || tag === 'TEXTAREA') {
119
- return
120
- }
121
- e.preventDefault()
122
- e.stopPropagation()
123
- this.handleUndo(state)
124
- },
125
-
126
103
  onKeyup (state, e) {
127
104
  if (isInteractiveElementFocused(state)) {
128
105
  return
@@ -4,7 +4,9 @@ describe('keyboardHandlers', () => {
4
4
  const keydown = (ctx, state, key, extra = {}) => ctx.onKeydown(state, { key, preventDefault: jest.fn(), stopPropagation: jest.fn(), ...extra })
5
5
  const keyup = (ctx, state, key) => ctx.onKeyup(state, { key, stopPropagation: jest.fn() })
6
6
 
7
- test('shortcuts are ignored outside the viewport (input, focusable non-input) and when the container is absent', () => {
7
+ // isInteractiveElementFocused's own branches are covered by utils/keyboardShortcuts.test.js
8
+ // — this just checks onKeydown/onKeyup actually consult it.
9
+ test('shortcuts are ignored while a form control outside the viewport has focus', () => {
8
10
  const { ctx, state } = createHarness()
9
11
  const input = document.createElement('input')
10
12
  document.body.appendChild(input)
@@ -12,18 +14,6 @@ describe('keyboardHandlers', () => {
12
14
  keydown(ctx, state, 'ArrowRight')
13
15
  keyup(ctx, state, 'ArrowRight')
14
16
  expect(state.interfaceType).not.toBe('keyboard')
15
-
16
- const focusable = document.createElement('div')
17
- focusable.tabIndex = 0
18
- document.body.appendChild(focusable)
19
- focusable.focus()
20
- keydown(ctx, state, 'ArrowRight')
21
- expect(state.interfaceType).not.toBe('keyboard')
22
-
23
- input.focus()
24
- const s = { ...state, container: undefined, interfaceType: 'mouse' }
25
- keydown(ctx, s, 'ArrowRight')
26
- expect(s.interfaceType).toBe('mouse')
27
17
  })
28
18
 
29
19
  test('space selects the first vertex, or does nothing when a vertex is selected or there are none', () => {
@@ -43,24 +33,21 @@ describe('keyboardHandlers', () => {
43
33
  expect(s.isPanEnabled).toBe(true)
44
34
  })
45
35
 
46
- test('an arrow key moves the selected vertex, snapping when active and using the raw coord otherwise', () => {
47
- const { ctx, state, map } = createHarness()
36
+ // resolveSnapTarget's own snap/break-out-of-snap branches are covered by
37
+ // utils/snapMovement.test.js this just checks onKeydown wires an arrow key through to it,
38
+ // records the starting position/index for undo on the first move, and leaves it fixed for
39
+ // the rest of a held-key sequence.
40
+ test('an arrow key moves the selected vertex and starts a keyboard-move undo sequence', () => {
41
+ const { ctx, state } = createHarness()
48
42
  state.selectedVertexIndex = 1
49
43
  keydown(ctx, state, 'ArrowRight')
50
44
  expect(state._keyboardMoveStartIndex).toBe(1)
45
+ expect(state._keyboardMoveStartPosition).toEqual([10, 0])
46
+ expect(state.vertecies[1]).not.toEqual([10, 0])
51
47
 
52
- state.getSnapEnabled = () => true
53
- map._snapInstance = { status: true, snapStatus: true, snapCoords: [7, 8], snapToClosestPoint: jest.fn() }
54
- keydown(ctx, state, 'ArrowUp')
55
- expect(state._isSnapped).toBe(true)
56
- expect(state.vertecies[1]).toEqual([7, 8])
57
-
58
- keydown(ctx, state, 'ArrowLeft') // snapped already → break out of the snap radius
59
- expect(state._isSnapped).toBe(false)
60
-
61
- map._snapInstance = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn() }
62
- keydown(ctx, state, 'ArrowRight') // snap enabled but inactive → raw new coord
63
- expect(state._isSnapped).toBe(false)
48
+ // Still held → start position isn't reset to the intermediate (already-moved) position
49
+ keydown(ctx, state, 'ArrowRight')
50
+ expect(state._keyboardMoveStartPosition).toEqual([10, 0])
64
51
  })
65
52
 
66
53
  test('an arrow key on a midpoint inserts a vertex; guards a missing feature or out-of-range vertex', () => {
@@ -74,7 +61,10 @@ describe('keyboardHandlers', () => {
74
61
  expect(ctx.map._undoStack.length).toBe(0)
75
62
  })
76
63
 
77
- test('alt+arrow steps the selection, Escape clears it, and Cmd/Ctrl+Z undoes (ignoring shift and text fields)', () => {
64
+ // isUndoShortcut/handleUndoShortcut's own edge cases (shift, wrong key, text-field focus)
65
+ // are covered by utils/keyboardShortcuts.test.js — this just checks onKeydown wires
66
+ // Cmd/Ctrl+Z through to handleUndo, and the alt+arrow/Escape behaviour that's specific here.
67
+ test('alt+arrow steps the selection, Escape clears it, and Cmd/Ctrl+Z undoes', () => {
78
68
  const { ctx, state, api } = createHarness()
79
69
  state.selectedVertexIndex = 0
80
70
  const updateSpy = jest.spyOn(ctx, 'updateVertex').mockImplementation(() => {})
@@ -84,19 +74,14 @@ describe('keyboardHandlers', () => {
84
74
  keydown(ctx, state, 'Escape')
85
75
  expect(api.changeMode).toHaveBeenCalledWith('edit_vertex', expect.objectContaining({ isPanEnabled: true }))
86
76
 
87
- const undoSpy = jest.spyOn(ctx, 'handleUndo').mockImplementation(() => {})
88
- keydown(ctx, state, 'z', { metaKey: true })
89
- keydown(ctx, state, 'z', { ctrlKey: true })
90
- expect(undoSpy).toHaveBeenCalledTimes(2)
91
- keydown(ctx, state, 'z', { metaKey: true, shiftKey: true }) // shift → not undo
92
- expect(undoSpy).toHaveBeenCalledTimes(2)
77
+ // An unhandled key (not space/arrow/Escape/undo) falls all the way through harmlessly —
78
+ // unlike editPointMode, Escape returns early here, so this is the only path that reaches
79
+ // isUndoShortcut with a falsy result.
80
+ expect(() => keydown(ctx, state, 'x')).not.toThrow()
93
81
 
94
- // Ignored while typing in a text field inside the viewport
95
- const input = document.createElement('input')
96
- state.container.appendChild(input)
97
- input.focus()
82
+ const undoSpy = jest.spyOn(ctx, 'handleUndo').mockImplementation(() => {})
98
83
  keydown(ctx, state, 'z', { metaKey: true })
99
- expect(undoSpy).toHaveBeenCalledTimes(2)
84
+ expect(undoSpy).toHaveBeenCalledTimes(1)
100
85
  })
101
86
 
102
87
  test('onKeyup pushes a move undo after a sequence, deletes on Delete, no-ops otherwise, and allows viewport focus', () => {