@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,457 @@
1
+ import { DrawPointMode } from './drawPointMode.js'
2
+ import { createUndoStack } from '../../../utils/undoStack.js'
3
+ import PointFeature from '../../../../../../node_modules/@mapbox/mapbox-gl-draw/src/feature_types/point.js'
4
+
5
+ const CENTER = { lng: 5, lat: 5 }
6
+
7
+ const createMap = () => {
8
+ const canvas = document.createElement('canvas')
9
+ const listeners = {}
10
+ return {
11
+ _undoInProgress: false,
12
+ _undoStack: null,
13
+ _snapInstance: null,
14
+ _drawGeometryValidator: undefined,
15
+ getCanvas: () => canvas,
16
+ getCenter: () => ({ ...CENTER }),
17
+ project: jest.fn(() => ({ x: 50, y: 50 })),
18
+ unproject: jest.fn(() => ({ ...CENTER })),
19
+ fire: jest.fn(function (type, e) { (listeners[type] ?? []).forEach((h) => h(e)) }),
20
+ on: jest.fn((type, h) => { (listeners[type] ??= []).push(h) }),
21
+ off: jest.fn((type, h) => { listeners[type] = (listeners[type] ?? []).filter((x) => x !== h) })
22
+ }
23
+ }
24
+
25
+ const contexts = []
26
+ const removeListeners = (ctx) => ctx._listeners?.forEach(([t, e, h]) =>
27
+ t.removeEventListener ? t.removeEventListener(e, h) : t.off(e, h))
28
+
29
+ const createModeContext = () => {
30
+ const map = createMap()
31
+ const features = new Map()
32
+ const store = {
33
+ add: jest.fn((f) => features.set(f.id, f)),
34
+ delete: jest.fn((ids) => ids.forEach((id) => features.delete(id))),
35
+ render: jest.fn(),
36
+ featureChanged: jest.fn(),
37
+ getInitialConfigValue: jest.fn(() => true)
38
+ }
39
+ const api = { changeMode: jest.fn(), add: jest.fn(), delete: jest.fn() }
40
+ const ctx = {
41
+ map,
42
+ _ctx: { store, api, options: { userProperties: true } },
43
+ addFeature: (f) => store.add(f),
44
+ getFeature: (id) => features.get(id),
45
+ deleteFeature: jest.fn((ids) => ids.forEach((id) => features.delete(id))),
46
+ clearSelectedFeatures: jest.fn(),
47
+ updateUIClasses: jest.fn(),
48
+ activateUIButton: jest.fn(),
49
+ setActionableState: jest.fn(),
50
+ changeMode: jest.fn()
51
+ }
52
+ ctx.newFeature = (geojson) => new PointFeature(ctx._ctx, geojson)
53
+ Object.assign(ctx, DrawPointMode)
54
+ contexts.push(ctx)
55
+ return ctx
56
+ }
57
+
58
+ const createContainer = () => {
59
+ const container = document.createElement('div')
60
+ container.tabIndex = 0
61
+ const button = document.createElement('button')
62
+ button.id = 'add-point'
63
+ container.appendChild(button)
64
+ document.body.appendChild(container)
65
+ return { container, button }
66
+ }
67
+
68
+ const createCrossHair = () => ({ show: jest.fn(), hide: jest.fn() })
69
+
70
+ const setup = (options = {}) => {
71
+ const ctx = createModeContext()
72
+ const dom = createContainer()
73
+ ctx.map._undoStack = createUndoStack(() => {})
74
+ const crossHair = createCrossHair()
75
+ const state = ctx.onSetup({
76
+ container: dom.container,
77
+ addVertexButtonId: 'add-point',
78
+ interfaceType: 'mouse',
79
+ crossHair,
80
+ featureId: 'point-1',
81
+ properties: { label: 'a point' },
82
+ ...options
83
+ })
84
+ return { ctx, state, crossHair, ...dom }
85
+ }
86
+
87
+ const clickEvent = (ctx, lng, lat, overrides = {}) => ({
88
+ lngLat: { lng, lat },
89
+ point: { x: lng, y: lat },
90
+ originalEvent: { button: 0, target: ctx.map.getCanvas(), ...overrides }
91
+ })
92
+
93
+ const firedWith = (map, type) => map.fire.mock.calls.filter(([t]) => t === type).map(([, e]) => e)
94
+
95
+ afterEach(() => {
96
+ contexts.splice(0).forEach(removeListeners)
97
+ document.body.innerHTML = ''
98
+ })
99
+
100
+ describe('DrawPointMode', () => {
101
+ describe('onSetup', () => {
102
+ it('creates an empty in-progress point feature and applies caller properties', () => {
103
+ const { state } = setup({ properties: { label: 'field' } })
104
+ expect(state.point.type).toBe('Point')
105
+ expect(state.point.coordinates).toEqual([])
106
+ expect(state.point.properties).toEqual({ label: 'field' })
107
+ })
108
+
109
+ it('shows the crosshair for touch/keyboard interface types', () => {
110
+ const { crossHair } = setup({ interfaceType: 'touch' })
111
+ expect(crossHair.show).toHaveBeenCalled()
112
+ })
113
+
114
+ it('hides the crosshair for mouse', () => {
115
+ const { crossHair } = setup({ interfaceType: 'mouse' })
116
+ expect(crossHair.hide).toHaveBeenCalled()
117
+ })
118
+
119
+ it('falls back to a DOM vertexMarker element when no crossHair is supplied', () => {
120
+ const ctx = createModeContext()
121
+ const dom = createContainer()
122
+ const marker = document.createElement('div')
123
+ marker.id = 'vertex-marker'
124
+ dom.container.appendChild(marker)
125
+ ctx.map._undoStack = createUndoStack(() => {})
126
+
127
+ const state = ctx.onSetup({
128
+ container: dom.container,
129
+ vertexMarkerId: 'vertex-marker',
130
+ interfaceType: 'touch', // shown on setup
131
+ featureId: 'point-1',
132
+ properties: {}
133
+ })
134
+ expect(marker.style.display).toBe('block')
135
+
136
+ ctx.onPointermove(state, { pointerType: 'mouse' }) // hidden
137
+ expect(marker.style.display).toBe('none')
138
+ })
139
+ })
140
+
141
+ describe('onClick (mouse)', () => {
142
+ it('commits the coordinate, re-ids the feature to the caller featureId, and fires draw.create', () => {
143
+ const { ctx, state } = setup()
144
+ ctx.onClick(state, clickEvent(ctx, 1, 2))
145
+ expect(state.point.coordinates).toEqual([1, 2])
146
+ const created = firedWith(ctx.map, 'draw.create')
147
+ expect(created).toHaveLength(1)
148
+ expect(created[0].features[0].geometry.coordinates).toEqual([1, 2])
149
+ // Re-id happens via the internal draw.create listener registered in onSetup
150
+ expect(ctx._ctx.api.delete).toHaveBeenCalled()
151
+ expect(ctx._ctx.api.add).toHaveBeenCalledWith(
152
+ expect.objectContaining({ id: 'point-1' }),
153
+ { userProperties: true }
154
+ )
155
+ })
156
+
157
+ it('calls the adapter-injected resolvePointSymbol hook with the final featureId, after re-id', () => {
158
+ const resolvePointSymbol = jest.fn()
159
+ const { ctx, state } = setup({ resolvePointSymbol, properties: { symbol: 'pin' } })
160
+ ctx.onClick(state, clickEvent(ctx, 1, 2))
161
+ expect(resolvePointSymbol).toHaveBeenCalledWith('point-1', { symbol: 'pin' })
162
+ })
163
+
164
+ it('does not throw when no resolvePointSymbol hook was supplied', () => {
165
+ const { ctx, state } = setup()
166
+ expect(() => ctx.onClick(state, clickEvent(ctx, 1, 2))).not.toThrow()
167
+ })
168
+
169
+ it('ignores non-primary-button clicks', () => {
170
+ const { ctx, state } = setup()
171
+ ctx.onClick(state, clickEvent(ctx, 1, 2, { button: 1 }))
172
+ expect(state.point.coordinates).toEqual([])
173
+ expect(firedWith(ctx.map, 'draw.create')).toHaveLength(0)
174
+ })
175
+
176
+ it('ignores clicks whose target is not the map canvas', () => {
177
+ const { ctx, state } = setup()
178
+ ctx.onClick(state, clickEvent(ctx, 1, 2, { target: document.createElement('div') }))
179
+ expect(state.point.coordinates).toEqual([])
180
+ })
181
+
182
+ it('vetoes placement via onGeometryChange and never commits', () => {
183
+ const { ctx, state } = setup()
184
+ ctx.map._drawGeometryValidator = () => ({ valid: false, reason: 'not allowed here' })
185
+ ctx.onClick(state, clickEvent(ctx, 1, 2))
186
+ expect(state.point.coordinates).toEqual([])
187
+ const blocked = firedWith(ctx.map, 'draw.placementblocked')
188
+ expect(blocked).toHaveLength(1)
189
+ expect(blocked[0]).toMatchObject({ reason: 'not allowed here', phase: 'place', mode: 'draw_point' })
190
+ expect(firedWith(ctx.map, 'draw.create')).toHaveLength(0)
191
+ })
192
+
193
+ it('defaults the blocked reason to null when the veto gives none', () => {
194
+ const { ctx, state } = setup()
195
+ ctx.map._drawGeometryValidator = () => false // no {reason} shape
196
+ ctx.onClick(state, clickEvent(ctx, 1, 2))
197
+ const blocked = firedWith(ctx.map, 'draw.placementblocked')
198
+ expect(blocked[0].reason).toBeNull()
199
+ })
200
+ })
201
+
202
+ describe('onTap', () => {
203
+ it('behaves the same as onClick', () => {
204
+ const { ctx, state } = setup()
205
+ ctx.onTap(state, clickEvent(ctx, 3, 4))
206
+ expect(state.point.coordinates).toEqual([3, 4])
207
+ })
208
+ })
209
+
210
+ describe('crosshair / keyboard placement', () => {
211
+ it('Enter commits at the map centre once keyboard interaction has started', () => {
212
+ const { ctx, state, container } = setup({ interfaceType: 'keyboard' })
213
+ container.focus()
214
+ ctx.onKeydown(state, { key: 'Enter' })
215
+ ctx.onKeyup(state, { key: 'Enter' })
216
+ expect(state.point.coordinates).toEqual([CENTER.lng, CENTER.lat])
217
+ expect(firedWith(ctx.map, 'draw.create')).toHaveLength(1)
218
+ })
219
+
220
+ it('does not commit on Enter before onKeydown has marked the session active', () => {
221
+ const { ctx, state, container } = setup({ interfaceType: 'keyboard' })
222
+ container.focus()
223
+ ctx.onKeyup(state, { key: 'Enter' })
224
+ expect(state.point.coordinates).toEqual([])
225
+ })
226
+
227
+ it('onKeydown ignores keys while the container is not focused', () => {
228
+ const { ctx, state } = setup({ interfaceType: 'mouse' })
229
+ expect(() => ctx.onKeydown(state, { key: 'Enter' })).not.toThrow()
230
+ expect(state.isActive).toBeFalsy()
231
+ expect(state.interfaceType).toBe('mouse') // never switched to keyboard
232
+ })
233
+
234
+ it('onKeydown Escape is a no-op there (handled window-level in onKeyup instead)', () => {
235
+ const { ctx, state, container } = setup({ interfaceType: 'mouse' })
236
+ container.focus()
237
+ const preventDefault = jest.fn()
238
+ ctx.onKeydown(state, { key: 'Escape', preventDefault })
239
+ expect(preventDefault).toHaveBeenCalled()
240
+ expect(state.interfaceType).toBe('mouse')
241
+ })
242
+
243
+ it('onKeydown ignores non-interface keys', () => {
244
+ const { ctx, state, container } = setup({ interfaceType: 'mouse' })
245
+ container.focus()
246
+ ctx.onKeydown(state, { key: 'a' })
247
+ expect(state.interfaceType).toBe('mouse')
248
+ })
249
+
250
+ it('onKeyup ignores non-Escape keys while the container is not focused', () => {
251
+ const { ctx, state } = setup({ interfaceType: 'mouse' })
252
+ expect(() => ctx.onKeyup(state, { key: 'Enter' })).not.toThrow()
253
+ expect(state.interfaceType).toBe('mouse')
254
+ })
255
+
256
+ it('onKeyup ignores non-interface keys', () => {
257
+ const { ctx, state, container } = setup({ interfaceType: 'mouse' })
258
+ container.focus()
259
+ ctx.onKeyup(state, { key: 'a' })
260
+ expect(state.interfaceType).toBe('mouse')
261
+ })
262
+
263
+ it('the "Add point" button commits at the map centre', () => {
264
+ const { ctx, state, button } = setup()
265
+ ctx.onVertexButtonClick(state, { target: button })
266
+ expect(state.point.coordinates).toEqual([CENTER.lng, CENTER.lat])
267
+ expect(firedWith(ctx.map, 'draw.create')).toHaveLength(1)
268
+ })
269
+
270
+ it('a click on an unrelated element does not commit', () => {
271
+ const { ctx, state } = setup()
272
+ ctx.onVertexButtonClick(state, { target: document.createElement('div') })
273
+ expect(state.point.coordinates).toEqual([])
274
+ })
275
+ })
276
+
277
+ describe('Escape', () => {
278
+ it('fires draw.cancel regardless of interface type', () => {
279
+ const { ctx, state } = setup({ interfaceType: 'mouse' })
280
+ ctx.onKeyup(state, { key: 'Escape' })
281
+ expect(firedWith(ctx.map, 'draw.cancel')).toHaveLength(1)
282
+ })
283
+
284
+ it('does not throw via the neutralised built-in capital onKeyUp', () => {
285
+ const { ctx, state } = setup()
286
+ expect(() => ctx.onKeyUp(state, { key: 'Enter' })).not.toThrow()
287
+ expect(state.point.coordinates).toEqual([])
288
+ })
289
+ })
290
+
291
+ describe('no-op handlers required by createLifecycle\'s bind contract', () => {
292
+ it('onUndo and onPointerup are harmless no-ops — nothing to undo/report before a single-click commit', () => {
293
+ const { ctx, state } = setup()
294
+ expect(() => ctx.onUndo(state)).not.toThrow()
295
+ expect(() => ctx.onPointerup(state)).not.toThrow()
296
+ })
297
+ })
298
+
299
+ describe('onStop', () => {
300
+ it('deletes an uncommitted point', () => {
301
+ const { ctx, state } = setup()
302
+ ctx.onStop(state)
303
+ expect(ctx.deleteFeature).toHaveBeenCalledWith([state.point.id], { silent: true })
304
+ })
305
+
306
+ it('keeps a committed point', () => {
307
+ const { ctx, state } = setup()
308
+ ctx.onClick(state, clickEvent(ctx, 1, 2))
309
+ ctx.deleteFeature.mockClear()
310
+ ctx.onStop(state)
311
+ expect(ctx.deleteFeature).not.toHaveBeenCalled()
312
+ })
313
+ })
314
+
315
+ describe('interface type tracking', () => {
316
+ it('touch start shows the crosshair', () => {
317
+ const { ctx, state, crossHair } = setup({ interfaceType: 'mouse' })
318
+ crossHair.show.mockClear()
319
+ ctx.onTouchStart(state)
320
+ expect(state.interfaceType).toBe('touch')
321
+ expect(crossHair.show).toHaveBeenCalled()
322
+ })
323
+
324
+ it('a non-touch pointerdown switches interface type to mouse without forcing the crosshair visible', () => {
325
+ const { ctx, state, crossHair } = setup({ interfaceType: 'touch' })
326
+ crossHair.show.mockClear()
327
+ ctx.onPointerdown(state, { pointerType: 'mouse' })
328
+ expect(state.interfaceType).toBe('mouse')
329
+ expect(crossHair.show).not.toHaveBeenCalled()
330
+ })
331
+
332
+ it('a touch pointerdown leaves the interface type alone', () => {
333
+ const { ctx, state } = setup({ interfaceType: 'touch' })
334
+ ctx.onPointerdown(state, { pointerType: 'touch' })
335
+ expect(state.interfaceType).toBe('touch')
336
+ })
337
+
338
+ it('a non-touch pointermove hides the crosshair', () => {
339
+ const { ctx, state, crossHair } = setup({ interfaceType: 'touch' })
340
+ ctx.onPointermove(state, { pointerType: 'mouse' })
341
+ expect(crossHair.hide).toHaveBeenCalled()
342
+ })
343
+
344
+ it('a touch pointermove does not hide the crosshair', () => {
345
+ const { ctx, state, crossHair } = setup({ interfaceType: 'touch' })
346
+ ctx.onPointermove(state, { pointerType: 'touch' })
347
+ expect(crossHair.hide).not.toHaveBeenCalled()
348
+ })
349
+
350
+ it('touch end shows the crosshair', () => {
351
+ const { ctx, state, crossHair } = setup({ interfaceType: 'mouse' })
352
+ crossHair.show.mockClear()
353
+ ctx.onTouchEnd(state)
354
+ expect(state.interfaceType).toBe('touch')
355
+ expect(crossHair.show).toHaveBeenCalled()
356
+ })
357
+
358
+ it('onInterfaceTypeChange only shows the crosshair for touch/keyboard', () => {
359
+ const { ctx, state, crossHair } = setup({ interfaceType: 'touch' })
360
+ crossHair.show.mockClear()
361
+ ctx.onInterfaceTypeChange(state, { interfaceType: 'mouse' })
362
+ expect(state.interfaceType).toBe('mouse')
363
+ expect(crossHair.show).not.toHaveBeenCalled()
364
+
365
+ ctx.onInterfaceTypeChange(state, { interfaceType: 'keyboard' })
366
+ expect(state.interfaceType).toBe('keyboard')
367
+ expect(crossHair.show).toHaveBeenCalled()
368
+ })
369
+
370
+ it('blur outside the container hides the crosshair', () => {
371
+ const { ctx, state, crossHair } = setup({ interfaceType: 'touch' })
372
+ ctx.onBlur(state, { target: document.createElement('div') })
373
+ expect(crossHair.hide).toHaveBeenCalled()
374
+ })
375
+
376
+ it('blur landing on the container itself does not hide the crosshair', () => {
377
+ const { ctx, state, crossHair, container } = setup({ interfaceType: 'touch' })
378
+ crossHair.hide.mockClear()
379
+ ctx.onBlur(state, { target: container })
380
+ expect(crossHair.hide).not.toHaveBeenCalled()
381
+ })
382
+ })
383
+
384
+ describe('snap', () => {
385
+ it('onMouseMove triggers a snap check at the pointer when snapping is enabled', () => {
386
+ const { ctx, state } = setup()
387
+ const snap = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn() }
388
+ ctx.map._snapInstance = snap
389
+ state.getSnapEnabled = () => true
390
+ ctx.onMouseMove(state, { point: { x: 7, y: 8 } })
391
+ expect(snap.snapToClosestPoint).toHaveBeenCalledWith({ point: { x: 7, y: 8 }, lngLat: CENTER, _explicit: true })
392
+ })
393
+
394
+ it('onMouseMove does nothing while snapping is disabled', () => {
395
+ const { ctx, state } = setup()
396
+ const snap = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn() }
397
+ ctx.map._snapInstance = snap
398
+ state.getSnapEnabled = () => false
399
+ ctx.onMouseMove(state, { point: { x: 7, y: 8 } })
400
+ expect(snap.snapToClosestPoint).not.toHaveBeenCalled()
401
+ })
402
+
403
+ it('a real click commits the snapped coordinate when a snap candidate is active', () => {
404
+ const { ctx, state } = setup()
405
+ ctx.map._snapInstance = { status: true, snapStatus: true, snapCoords: [9, 9], snapToClosestPoint: jest.fn() }
406
+ state.getSnapEnabled = () => true
407
+ ctx.onClick(state, clickEvent(ctx, 1, 2))
408
+ expect(state.point.coordinates).toEqual([9, 9])
409
+ })
410
+
411
+ it('crosshair placement snaps to a candidate found at the map centre', () => {
412
+ const { ctx, state, button } = setup()
413
+ const snap = { status: true, snapStatus: true, snapCoords: [9, 9], snapToClosestPoint: jest.fn() }
414
+ ctx.map._snapInstance = snap
415
+ state.getSnapEnabled = () => true
416
+ ctx.onVertexButtonClick(state, { target: button })
417
+ expect(snap.snapToClosestPoint).toHaveBeenCalled()
418
+ expect(state.point.coordinates).toEqual([9, 9])
419
+ })
420
+
421
+ // Regression coverage: onMove used to be a no-op, so touch/keyboard users never saw a
422
+ // live snap-target indicator while panning — it only ever checked at the moment of
423
+ // commit, with nothing shown beforehand.
424
+ it('onMove keeps the snap indicator live at the crosshair while panning, touch/keyboard only', () => {
425
+ const { ctx, state } = setup({ interfaceType: 'touch' })
426
+ const snap = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn() }
427
+ ctx.map._snapInstance = snap
428
+ state.getSnapEnabled = () => true
429
+
430
+ ctx.onMove(state)
431
+ expect(snap.snapToClosestPoint).toHaveBeenCalledWith({ point: expect.any(Object), lngLat: CENTER, _explicit: true })
432
+
433
+ snap.snapToClosestPoint.mockClear()
434
+ state.interfaceType = 'mouse'
435
+ ctx.onMove(state)
436
+ expect(snap.snapToClosestPoint).not.toHaveBeenCalled()
437
+ })
438
+
439
+ it('switching to touch or keyboard shows the snap indicator immediately, not just on the first pan', () => {
440
+ const snap = { status: true, snapStatus: false, snapCoords: null, snapToClosestPoint: jest.fn() }
441
+
442
+ const touch = setup({ interfaceType: 'mouse' })
443
+ touch.ctx.map._snapInstance = snap
444
+ touch.state.getSnapEnabled = () => true
445
+ touch.ctx.onTouchStart(touch.state)
446
+ expect(snap.snapToClosestPoint).toHaveBeenCalled()
447
+
448
+ snap.snapToClosestPoint.mockClear()
449
+ const keyboard = setup({ interfaceType: 'mouse' })
450
+ keyboard.ctx.map._snapInstance = snap
451
+ keyboard.state.getSnapEnabled = () => true
452
+ keyboard.container.focus()
453
+ keyboard.ctx.onKeydown(keyboard.state, { key: 'ArrowUp' })
454
+ expect(snap.snapToClosestPoint).toHaveBeenCalled()
455
+ })
456
+ })
457
+ })
@@ -0,0 +1,102 @@
1
+ import { EditPointMode } from '../../editPointMode.js'
2
+ import { createUndoStack } from '../../../../../utils/undoStack.js'
3
+ import PointFeature from '../../../../../../../../node_modules/@mapbox/mapbox-gl-draw/src/feature_types/point.js'
4
+
5
+ /**
6
+ * Shared test harness for the point-edit mode and its handler modules. Builds the real
7
+ * EditPointMode (with the real DirectSelect parent methods it spreads in and a real
8
+ * mapbox-gl-draw Point feature class) over test doubles for the map, store, api and DOM.
9
+ * Mirrors editVertexMode/__helpers__/harness.js. Excluded from coverage.
10
+ */
11
+
12
+ // Deterministic projection: lngLat <-> pixel is a x10 scale.
13
+ const project = (p) => {
14
+ const [lng, lat] = Array.isArray(p) ? p : [p.lng, p.lat]
15
+ return { x: lng * 10, y: lat * 10 }
16
+ }
17
+ const unproject = ({ x, y }) => ({ lng: x / 10, lat: y / 10 })
18
+
19
+ export const POINT = () => ({ type: 'Point', coordinates: [5, 5] })
20
+ export const dragEvt = (lng, lat) => ({ originalEvent: { stopPropagation: jest.fn() }, lngLat: { lng, lat }, point: { x: lng * 10, y: lat * 10 } })
21
+ export const svgTarget = (state) => ({ parentNode: state.touchPointTarget })
22
+
23
+ const contexts = []
24
+
25
+ export const createHarness = (geometry = POINT(), setupOptions = {}) => {
26
+ const features = new Map()
27
+ const store = {
28
+ featureChanged: jest.fn(),
29
+ render: jest.fn(),
30
+ get: (id) => features.get(id)
31
+ }
32
+ const api = {
33
+ add: jest.fn((geojson) => {
34
+ features.set(geojson.id, new PointFeature(_ctx, { ...geojson, id: geojson.id }))
35
+ return [geojson.id]
36
+ }),
37
+ changeMode: jest.fn(),
38
+ delete: jest.fn(),
39
+ trash: jest.fn()
40
+ }
41
+ const _ctx = { store, api, options: {} }
42
+
43
+ const feature = new PointFeature(_ctx, { type: 'Feature', id: 'feat-1', properties: {}, geometry })
44
+ features.set('feat-1', feature)
45
+
46
+ const listeners = {}
47
+ const map = {
48
+ _undoStack: createUndoStack(() => {}),
49
+ _snapInstance: null,
50
+ _lastEditFeatureId: null,
51
+ _drawEditContainer: null,
52
+ _editingFeatureId: null,
53
+ _drawCurrentMapStyle: { mapColorScheme: 'light' },
54
+ dragPan: { enable: jest.fn(), disable: jest.fn(), isEnabled: () => true },
55
+ doubleClickZoom: { enable: jest.fn(), disable: jest.fn() },
56
+ getSource: jest.fn(() => ({ setData: jest.fn() })),
57
+ getLayer: jest.fn(() => null),
58
+ setLayoutProperty: jest.fn(),
59
+ getCenter: () => ({ lng: 5, lat: 5 }),
60
+ project: jest.fn(project),
61
+ unproject: jest.fn(unproject),
62
+ fire: jest.fn(function (type, e) { (listeners[type] ?? []).forEach((h) => h(e)) }),
63
+ on: jest.fn((type, h) => { (listeners[type] ??= []).push(h) }),
64
+ off: jest.fn((type, h) => { listeners[type] = (listeners[type] ?? []).filter((x) => x !== h) })
65
+ }
66
+
67
+ const container = document.createElement('div')
68
+ container.tabIndex = 0
69
+ document.body.appendChild(container)
70
+
71
+ const ctx = {
72
+ map,
73
+ _ctx,
74
+ getFeature: (id) => features.get(id),
75
+ getSelected: jest.fn(() => [feature]),
76
+ setSelected: jest.fn(),
77
+ setSelectedCoordinates: jest.fn(),
78
+ clearSelectedCoordinates: jest.fn(),
79
+ setActionableState: jest.fn(),
80
+ updateUIClasses: jest.fn(),
81
+ fire: jest.fn()
82
+ }
83
+ Object.assign(ctx, EditPointMode)
84
+
85
+ const options = {
86
+ container,
87
+ featureId: 'feat-1',
88
+ interfaceType: 'mouse',
89
+ undoButtonId: 'undo-vertex',
90
+ isPanEnabled: true,
91
+ ...setupOptions
92
+ }
93
+ const state = ctx.onSetup(options)
94
+ contexts.push({ ctx, state })
95
+ return { ctx, state, feature, features, map, store, api, container, options }
96
+ }
97
+
98
+ afterEach(() => {
99
+ contexts.splice(0).forEach(({ ctx, state }) => { try { ctx.onStop(state) } catch { /* already stopped */ } })
100
+ document.body.innerHTML = ''
101
+ jest.useRealTimers()
102
+ })
@@ -0,0 +1,82 @@
1
+ import { ARROW_KEYS, ARROW_OFFSETS, isInteractiveElementFocused, isUndoShortcut, sharedKeyboardHandlers } from '../../utils/keyboardShortcuts.js'
2
+
3
+ /**
4
+ * Keyboard interaction for the point-edit mode: arrow-key/Shift+arrow nudge and Cmd/Ctrl+Z
5
+ * undo. Mixed into EditPointMode. Unlike editVertexMode: the point is always selected (no
6
+ * Space-to-select step, no Alt+arrow navigation — nothing to navigate to with one
7
+ * coordinate), and Escape is deliberately left unbound here — deselecting would strand the
8
+ * user with no way back except the Cancel button.
9
+ */
10
+ export const keyboardHandlers = {
11
+ ...sharedKeyboardHandlers,
12
+
13
+ onKeydown (state, e) {
14
+ if (isInteractiveElementFocused(state)) {
15
+ return
16
+ }
17
+
18
+ state.interfaceType = 'keyboard'
19
+ this.hideTouchPointIndicator(state)
20
+
21
+ if (e.key === ' ') {
22
+ // Prevent the page from scrolling; there's nothing to select, the point already is.
23
+ e.preventDefault()
24
+ return
25
+ }
26
+ if (ARROW_KEYS.has(e.key)) {
27
+ this.handleArrowKey(state, e)
28
+ return
29
+ }
30
+ if (isUndoShortcut(e)) {
31
+ this.handleUndoShortcut(state, e)
32
+ }
33
+ },
34
+
35
+ // A plain or Shift+arrow nudges the point (Shift's finer-step-vs-coarse-step distinction
36
+ // lives in pointOperations.js's getOffset). Alt+arrow is left unhandled — nothing to
37
+ // navigate to with a single coordinate.
38
+ handleArrowKey (state, e) {
39
+ e.preventDefault()
40
+ e.stopPropagation()
41
+ this.movePointByKey(state, e)
42
+ },
43
+
44
+ movePointByKey (state, e) {
45
+ const currentCoord = this.getPointCoord(state)
46
+ if (!currentCoord) {
47
+ return
48
+ }
49
+
50
+ // Save starting position for undo (only on first move of a held-key sequence)
51
+ if (!state._keyboardMoveStartPosition) {
52
+ state._keyboardMoveStartPosition = [...currentCoord]
53
+ }
54
+
55
+ this.movePoint(state, this._keyboardMoveTarget(state, e, currentCoord))
56
+ },
57
+
58
+ // Resolve the destination coordinate for a keyboard nudge, applying or breaking snap —
59
+ // delegates to the shared resolver (utils/snapMovement.js) also used by MoveControls'
60
+ // nudgePointByDelta, so both snap identically.
61
+ _keyboardMoveTarget (state, e, currentCoord) {
62
+ const [dx, dy] = ARROW_OFFSETS[e.key]
63
+ return this.resolveSnapTarget(state, dx, dy, currentCoord, () => this.getNewCoord(state, e))
64
+ },
65
+
66
+ onKeyup (state, e) {
67
+ if (isInteractiveElementFocused(state)) {
68
+ return
69
+ }
70
+
71
+ state.interfaceType = 'keyboard'
72
+ if (ARROW_KEYS.has(e.key)) {
73
+ e.stopPropagation()
74
+
75
+ // Push undo for the whole held-key move sequence as one step
76
+ if (state._keyboardMoveStartPosition) {
77
+ this.pushUndo({ type: 'move_point', featureId: state.featureId, vertexIndex: 0, previousPosition: state._keyboardMoveStartPosition })
78
+ state._keyboardMoveStartPosition = null
79
+ }
80
+ }
81
+ }
82
+ }