@defra/interactive-map 0.0.39-alpha → 0.0.41-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 (387) 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/control-definition.md +9 -1
  7. package/docs/api/slots.md +26 -5
  8. package/docs/api.md +4 -4
  9. package/docs/assets/images/slot-map.svg +61 -262
  10. package/docs/examples/draw-tools.mdx +3 -3
  11. package/docs/plugins/draw.md +80 -17
  12. package/docs/plugins/search.md +1 -1
  13. package/govuk-prototype-kit.config.json +2 -0
  14. package/jest.setup.js +5 -0
  15. package/package.json +9 -1
  16. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  17. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  18. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  19. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  20. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  21. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  22. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  23. package/plugins/datasets/dist/css/index.css +1 -1
  24. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  25. package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
  26. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  27. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  28. package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
  29. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  30. package/plugins/datasets/dist/umd/index.js +1 -1
  31. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +2 -3
  32. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
  33. package/plugins/datasets/src/adapters/esri/registry/esriDataset.js +1 -1
  34. package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
  35. package/plugins/datasets/src/adapters/loadLayerAdapter.js +1 -1
  36. package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
  37. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +1 -3
  38. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +17 -3
  39. package/plugins/datasets/src/api/setOpacity.test.js +9 -0
  40. package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
  41. package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
  42. package/plugins/datasets/src/datasets.scss +0 -1
  43. package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
  44. package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
  45. package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
  46. package/plugins/datasets/src/index.test.js +36 -0
  47. package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
  48. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
  49. package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -5
  50. package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
  51. package/plugins/datasets/src/manifest.js +0 -38
  52. package/plugins/datasets/src/manifest.test.js +49 -0
  53. package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
  54. package/plugins/datasets/src/reducers/pluginState.js +3 -16
  55. package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
  56. package/plugins/datasets/src/registry/dataset.js +15 -4
  57. package/plugins/datasets/src/registry/dataset.test.js +61 -0
  58. package/plugins/datasets/src/registry/datasetRegistry.js +12 -9
  59. package/plugins/datasets/src/registry/datasetRegistry.test.js +17 -5
  60. package/plugins/datasets/src/registry/globalDataset.js +0 -3
  61. package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
  62. package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
  63. package/plugins/datasets/src/utils/bbox.test.js +125 -0
  64. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  65. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  66. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  67. package/plugins/draw/dist/esm/index.js +1 -1
  68. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +2 -1
  69. package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
  70. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  71. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  72. package/plugins/draw/dist/umd/index.js +1 -1
  73. package/plugins/draw/src/DrawInit.jsx +12 -4
  74. package/plugins/draw/src/DrawInit.test.jsx +51 -10
  75. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +92 -12
  76. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
  77. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
  78. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
  79. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
  81. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
  82. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +1 -1
  83. package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +1 -1
  84. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
  90. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
  91. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
  92. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
  93. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
  94. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
  95. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
  96. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
  97. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
  98. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
  99. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +5 -28
  100. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
  101. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
  102. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
  103. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
  104. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
  105. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
  106. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +4 -54
  107. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
  108. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
  109. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +11 -4
  110. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
  111. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
  112. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
  113. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
  114. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
  115. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
  116. package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
  117. package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
  118. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
  119. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
  120. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
  121. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
  122. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
  123. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
  124. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
  125. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
  126. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
  127. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
  128. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
  129. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
  130. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
  131. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
  132. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
  133. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
  134. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
  135. package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
  136. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
  137. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +1 -0
  138. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +27 -1
  139. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +24 -4
  140. package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +47 -3
  141. package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +1 -1
  142. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +32 -1
  143. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
  144. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +34 -1
  145. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
  146. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
  147. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +12 -5
  148. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +26 -3
  149. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
  150. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
  151. package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
  152. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
  153. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
  154. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
  155. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
  156. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
  157. package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
  158. package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
  159. package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
  160. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
  161. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
  162. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
  163. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
  164. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
  165. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
  166. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
  167. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
  168. package/plugins/draw/src/api/addFeature.js +16 -1
  169. package/plugins/draw/src/api/addFeature.test.js +62 -5
  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/newPoint.js +42 -0
  173. package/plugins/draw/src/api/newPoint.test.js +117 -0
  174. package/plugins/draw/src/api/setStyle.js +43 -0
  175. package/plugins/draw/src/api/setStyle.test.js +75 -0
  176. package/plugins/draw/src/events.js +21 -14
  177. package/plugins/draw/src/events.test.js +15 -1
  178. package/plugins/draw/src/manifest.js +18 -6
  179. package/plugins/draw/src/manifest.test.js +32 -1
  180. package/plugins/draw/src/reducer.test.js +7 -0
  181. package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
  182. package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
  183. package/plugins/draw/src/utils/symbolKeys.js +5 -0
  184. package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
  185. package/plugins/draw/src/validation/liveStroke.test.js +64 -1
  186. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  187. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  188. package/plugins/interact/dist/umd/index.js +1 -1
  189. package/plugins/interact/src/hooks/useCrossHairVisibility.js +12 -4
  190. package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +88 -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/utils/featureQueries.js +18 -0
  194. package/plugins/interact/src/utils/featureQueries.test.js +15 -0
  195. package/plugins/interact/src/utils/spatial.js +9 -3
  196. package/plugins/interact/src/utils/spatial.test.js +12 -1
  197. package/plugins/map-key/dist/css/index.css +62 -0
  198. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
  199. package/plugins/map-key/dist/esm/index.js +1 -0
  200. package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
  201. package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
  202. package/plugins/map-key/dist/umd/index.js +2 -0
  203. package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
  204. package/plugins/map-key/package.json +28 -0
  205. package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
  206. package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
  207. package/plugins/map-key/src/components/Key/Key.jsx +41 -0
  208. package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +15 -14
  209. package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
  210. package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
  211. package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
  212. package/plugins/map-key/src/components/Key/KeyItem.jsx +29 -0
  213. package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
  214. package/plugins/map-key/src/components/Key/KeySvg.jsx +42 -0
  215. package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
  216. package/plugins/map-key/src/components/Key/KeySvgLine.jsx +28 -0
  217. package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
  218. package/plugins/map-key/src/components/Key/KeySvgPattern.jsx +23 -0
  219. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +2 -2
  220. package/plugins/map-key/src/components/Key/KeySvgRect.jsx +33 -0
  221. package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
  222. package/plugins/map-key/src/components/Key/KeySvgSymbol.jsx +27 -0
  223. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +5 -6
  224. package/plugins/map-key/src/components/Key/MapKey.jsx +50 -0
  225. package/plugins/map-key/src/components/Key/MapKey.test.jsx +130 -0
  226. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
  227. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
  228. package/plugins/map-key/src/index.js +13 -0
  229. package/plugins/map-key/src/index.test.js +31 -0
  230. package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
  231. package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
  232. package/plugins/map-key/src/manifest.js +30 -0
  233. package/plugins/map-key/src/manifest.test.js +63 -0
  234. package/plugins/map-key/src/mapKey.scss +4 -0
  235. package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
  236. package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
  237. package/plugins/map-key/src/registry/index.js +3 -0
  238. package/plugins/map-key/src/registry/index.test.js +15 -0
  239. package/plugins/menu/dist/css/index.css +80 -0
  240. package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
  241. package/plugins/menu/dist/esm/index.js +1 -0
  242. package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
  243. package/plugins/menu/dist/umd/index.js +2 -0
  244. package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
  245. package/plugins/menu/package.json +28 -0
  246. package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
  247. package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
  248. package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
  249. package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
  250. package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
  251. package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
  252. package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
  253. package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
  254. package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
  255. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
  256. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
  257. package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
  258. package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
  259. package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
  260. package/plugins/menu/src/index.js +13 -0
  261. package/plugins/menu/src/index.test.js +31 -0
  262. package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
  263. package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
  264. package/plugins/menu/src/initialise/createMenu.js +10 -0
  265. package/plugins/menu/src/initialise/createMenu.test.js +37 -0
  266. package/plugins/menu/src/manifest.js +35 -0
  267. package/plugins/menu/src/manifest.test.js +63 -0
  268. package/plugins/menu/src/menu.scss +4 -0
  269. package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
  270. package/plugins/menu/src/reducers/pluginState.js +35 -0
  271. package/plugins/menu/src/reducers/pluginState.test.js +57 -0
  272. package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
  273. package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
  274. package/plugins/menu/src/registry/index.js +3 -0
  275. package/plugins/menu/src/registry/index.test.js +15 -0
  276. package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
  277. package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
  278. package/plugins/search/dist/css/index.css +1 -1
  279. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  280. package/plugins/search/dist/esm/index.js +1 -1
  281. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  282. package/plugins/search/dist/umd/index.js +1 -1
  283. package/plugins/search/src/Search.jsx +7 -5
  284. package/plugins/search/src/components/Form/Form.module.scss +9 -20
  285. package/plugins/search/src/index.js +0 -1
  286. package/plugins/search/src/index.test.js +2 -4
  287. package/plugins/search/src/manifest.js +1 -1
  288. package/plugins/search/src/search.scss +17 -16
  289. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  290. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  291. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  292. package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
  293. package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
  294. package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
  295. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
  296. package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
  297. package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
  298. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  299. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  300. package/providers/maplibre/dist/umd/index.js +1 -1
  301. package/providers/maplibre/src/utils/drawLayerBuckets.js +14 -0
  302. package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
  303. package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
  304. package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
  305. package/providers/maplibre/src/utils/hoverCursor.js +30 -14
  306. package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
  307. package/providers/maplibre/src/utils/patternImages.js +1 -1
  308. package/providers/maplibre/src/utils/symbolImages.js +41 -3
  309. package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
  310. package/rollup.esm.mjs +11 -1
  311. package/sonar-project.properties +7 -1
  312. package/src/App/components/Actions/Actions.test.jsx +47 -0
  313. package/src/App/components/Attributions/Attributions.module.scss +10 -0
  314. package/src/App/components/Hints/Hints.jsx +7 -2
  315. package/src/App/components/Hints/Hints.test.jsx +13 -0
  316. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +2 -3
  317. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +3 -6
  318. package/src/App/components/{MoveControl/MoveControl.jsx → MoveControls/MoveControls.jsx} +10 -10
  319. package/src/App/components/{MoveControl/MoveControl.module.scss → MoveControls/MoveControls.module.scss} +60 -29
  320. package/src/App/components/{MoveControl/MoveControl.test.jsx → MoveControls/MoveControls.test.jsx} +48 -48
  321. package/src/App/components/Panel/Panel.jsx +18 -3
  322. package/src/App/components/Panel/Panel.module.scss +18 -8
  323. package/src/App/components/Panel/Panel.test.jsx +34 -0
  324. package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
  325. package/src/App/components/Viewport/Viewport.jsx +9 -1
  326. package/src/App/components/Viewport/Viewport.test.jsx +11 -0
  327. package/src/App/hooks/useFeatureItems.js +16 -7
  328. package/src/App/hooks/useFeatureItems.test.js +57 -0
  329. package/src/App/hooks/useLayoutMeasurements.js +95 -84
  330. package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
  331. package/src/App/initialiseApp.js +11 -8
  332. package/src/App/initialiseApp.test.js +15 -3
  333. package/src/App/layout/Layout.jsx +12 -11
  334. package/src/App/layout/Layout.test.jsx +2 -0
  335. package/src/App/layout/layout.module.scss +31 -17
  336. package/src/App/registry/keyboardShortcutRegistry.js +25 -13
  337. package/src/App/registry/keyboardShortcutRegistry.test.js +49 -32
  338. package/src/App/registry/pluginRegistry.js +2 -3
  339. package/src/App/registry/pluginRegistry.test.js +16 -3
  340. package/src/App/renderer/HtmlElementHost.jsx +18 -5
  341. package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
  342. package/src/App/renderer/mapButtons.js +28 -12
  343. package/src/App/renderer/mapButtons.test.js +17 -4
  344. package/src/App/renderer/mapControls.js +3 -1
  345. package/src/App/renderer/mapControls.test.js +16 -0
  346. package/src/App/renderer/mapPanels.js +44 -9
  347. package/src/App/renderer/mapPanels.test.js +51 -2
  348. package/src/App/renderer/orderItems.js +28 -0
  349. package/src/App/renderer/orderItems.test.js +50 -0
  350. package/src/App/renderer/slotAggregator.js +2 -18
  351. package/src/App/renderer/slotHelpers.js +3 -1
  352. package/src/App/renderer/slotHelpers.test.js +5 -0
  353. package/src/App/renderer/slots.js +3 -0
  354. package/src/App/store/AppProvider.jsx +4 -2
  355. package/src/App/store/appDispatchMiddleware.js +1 -1
  356. package/src/App/store/appDispatchMiddleware.test.js +8 -0
  357. package/src/InteractiveMap/InteractiveMap.js +8 -2
  358. package/src/InteractiveMap/InteractiveMap.test.js +20 -3
  359. package/src/config/appConfig.js +17 -15
  360. package/src/config/appConfig.test.js +22 -22
  361. package/src/config/defaults.js +3 -1
  362. package/src/config/events.js +2 -0
  363. package/src/config/mergeConfig.js +7 -0
  364. package/src/config/mergeConfig.test.js +20 -0
  365. package/src/scss/main.scss +1 -1
  366. package/src/scss/settings/_colors.scss +1 -0
  367. package/src/scss/settings/_dimensions.scss +2 -3
  368. package/src/scss/settings/_transition.scss +1 -1
  369. package/src/services/eventBus.js +44 -0
  370. package/src/services/eventBus.test.js +83 -0
  371. package/src/services/symbolRegistry.js +1 -1
  372. package/src/test-utils.js +5 -0
  373. package/src/types.js +25 -7
  374. package/webpack.umd.mjs +2 -0
  375. package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
  376. package/plugins/datasets/src/components/Key/Key.jsx +0 -50
  377. package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
  378. package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
  379. package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
  380. package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
  381. package/plugins/datasets/src/components/Key/KeySvgPattern.jsx +0 -13
  382. package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
  383. package/plugins/datasets/src/components/Key/KeySvgSymbol.jsx +0 -16
  384. package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
  385. /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
  386. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
  387. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.test.js +0 -0
@@ -6,12 +6,19 @@ import { setupTouchClickWorkaround } from './utils/touchClickWorkaround.js'
6
6
  import { applyTouchVertexColors } from './modes/editVertexMode/touchHandlers.js'
7
7
  import { resolveColors } from '../../utils/resolveColors.js'
8
8
  import { TOLERANCES, MAP_SIZE_SCALES } from './defaults.js'
9
+ import { refreshAllPointSymbols } from './pointSymbolImages.js'
9
10
  import { createMapboxDraw } from './mapboxDraw.js'
10
11
 
11
12
  jest.mock('@mapbox/mapbox-gl-draw', () => {
12
13
  const MockDraw = jest.fn(function () {
13
14
  this.modes = {}
14
15
  this.changeMode = jest.fn()
16
+ // Mirrors mapbox-gl-draw's own index.js: `api.options = options` — the fully-processed
17
+ // (cold+hot-duplicated) styles array survives on the public instance regardless of any
18
+ // later map.setStyle() wiping the actual map layers built from it.
19
+ this.options = { styles: [{ id: 'fill-inactive.cold' }, { id: 'fill-inactive.hot' }] }
20
+ this.set = jest.fn()
21
+ this.getAll = jest.fn(() => ({ type: 'FeatureCollection', features: [{ type: 'Feature', id: 'f1' }] }))
15
22
  })
16
23
  MockDraw.constants = { classes: {} }
17
24
  MockDraw.modes = { existing_mode: { id: 'existing' } }
@@ -22,6 +29,8 @@ jest.mock('./modes/disabledMode.js', () => ({ DisabledMode: { id: 'disabled' } }
22
29
  jest.mock('./modes/editVertexMode.js', () => ({ EditVertexMode: { id: 'edit_vertex' } }))
23
30
  jest.mock('./modes/drawPolygonMode.js', () => ({ DrawPolygonMode: { id: 'draw_polygon' } }))
24
31
  jest.mock('./modes/drawLineMode.js', () => ({ DrawLineMode: { id: 'draw_line' } }))
32
+ jest.mock('./modes/drawPointMode.js', () => ({ DrawPointMode: { id: 'draw_point' } }))
33
+ jest.mock('./pointSymbolImages.js', () => ({ refreshAllPointSymbols: jest.fn(() => Promise.resolve()) }))
25
34
  jest.mock('./styles.js', () => ({
26
35
  createDrawStyles: jest.fn(() => ['style']),
27
36
  updateDrawStyles: jest.fn()
@@ -38,21 +47,25 @@ jest.mock('./defaults.js', () => ({
38
47
  MAP_SIZE_SCALES: { medium: 1.5 }
39
48
  }))
40
49
 
41
- const EVENTS = { MAP_SET_STYLE: 'map:setstyle', MAP_SET_SIZE: 'map:setsize' }
50
+ const EVENTS = { MAP_SET_STYLE: 'map:setstyle', MAP_STYLE_CHANGE: 'map:stylechange', MAP_SET_SIZE: 'map:setsize', MAP_SET_PIXEL_RATIO: 'map:setpixelratio', MAP_DATA_CHANGE: 'map:datachange' }
42
51
 
43
52
  const handlerFor = (mockFn, eventName) =>
44
53
  mockFn.mock.calls.find(([name]) => name === eventName)?.[1]
45
54
 
46
- const createMap = () => ({
55
+ const createMap = ({ hasDrawSource = true } = {}) => ({
47
56
  addControl: jest.fn(),
48
57
  once: jest.fn(),
49
58
  on: jest.fn(),
50
59
  off: jest.fn(),
51
- fire: jest.fn()
60
+ fire: jest.fn(),
61
+ getSource: jest.fn(() => (hasDrawSource ? {} : undefined)),
62
+ addSource: jest.fn(),
63
+ addLayer: jest.fn(),
64
+ triggerRepaint: jest.fn()
52
65
  })
53
66
 
54
- const setup = ({ existingDraw, existingUndoStack, pluginConfig } = {}) => {
55
- const map = createMap()
67
+ const setup = ({ existingDraw, existingUndoStack, pluginConfig, hasDrawSource } = {}) => {
68
+ const map = createMap({ hasDrawSource })
56
69
  const mapProvider = {
57
70
  map,
58
71
  _mapboxDrawInstance: existingDraw,
@@ -201,36 +214,153 @@ describe('createMapboxDraw – setup side effects', () => {
201
214
  })
202
215
 
203
216
  describe('createMapboxDraw – event handlers', () => {
204
- test('MAP_SET_STYLE updates the current style and restyles on idle', () => {
217
+ // MAP_SET_STYLE fires the instant a style change is *requested* before map.setStyle()
218
+ // has necessarily even been called. Registering the idle/re-add work directly off it risks
219
+ // catching the map still idle from the *previous* style, no-opping, and consuming itself
220
+ // before the real wipe even happens. Only MAP_STYLE_CHANGE (which MapLibre only ever emits
221
+ // off the native 'style.load' event, i.e. strictly after setStyle() was actually called)
222
+ // should trigger the settle/idle work — this is the split under test throughout this block.
223
+ test('MAP_SET_STYLE alone only stashes the style — no idle listener registered yet', () => {
205
224
  const { map, eventBus } = setup()
206
225
 
207
- const styleHandler = handlerFor(eventBus.on, EVENTS.MAP_SET_STYLE)
208
- map._drawEditContainer = { querySelector: jest.fn(() => 'svg-el') }
209
- styleHandler('dark')
226
+ handlerFor(eventBus.on, EVENTS.MAP_SET_STYLE)('dark')
210
227
 
211
228
  expect(map._drawCurrentMapStyle).toBe('dark')
229
+ expect(map.once).not.toHaveBeenCalled()
230
+ })
231
+
232
+ test('MAP_STYLE_CHANGE updates the current style and restyles on idle', () => {
233
+ const { map, eventBus, mapProvider, result } = setup()
234
+
235
+ map._drawEditContainer = { querySelector: jest.fn(() => 'svg-el') }
236
+ handlerFor(eventBus.on, EVENTS.MAP_SET_STYLE)('dark')
237
+ handlerFor(eventBus.on, EVENTS.MAP_STYLE_CHANGE)()
212
238
 
213
239
  const idleCallback = handlerFor(map.once, 'idle')
214
240
  idleCallback()
215
241
  expect(updateDrawStyles).toHaveBeenCalledWith(map, 'dark', {})
216
242
  expect(map._drawEditContainer.querySelector).toHaveBeenCalledWith('[data-im-draw-touch-target]')
217
243
  expect(applyTouchVertexColors).toHaveBeenCalledWith('svg-el', 'dark', {})
244
+ expect(refreshAllPointSymbols).toHaveBeenCalledWith({ draw: result.draw, mapProvider, map })
218
245
  })
219
246
 
220
- test('MAP_SET_STYLE idle handler tolerates a missing edit container', () => {
247
+ // MAP_STYLE_CHANGE genuinely fires twice per style change (mapEvents.js's permanent
248
+ // 'style.load' listener alongside appEvents.js's one-shot listener, both bound to the same
249
+ // native event) — without a dedupe guard the expensive settle work (idle-wait, re-rasterise
250
+ // every point, one full source rewrite) runs twice back to back, and the second cycle's
251
+ // completion can land after useHighlightSync's settle window has already closed.
252
+ test('MAP_STYLE_CHANGE fired twice in a row (both listeners reacting to the same style.load) only settles once', async () => {
221
253
  const { map, eventBus } = setup()
222
254
 
223
255
  handlerFor(eventBus.on, EVENTS.MAP_SET_STYLE)('dark')
256
+ handlerFor(eventBus.on, EVENTS.MAP_STYLE_CHANGE)()
257
+ handlerFor(eventBus.on, EVENTS.MAP_STYLE_CHANGE)()
258
+
259
+ expect(map.once).toHaveBeenCalledTimes(1)
260
+
261
+ handlerFor(map.once, 'idle')()
262
+ await Promise.resolve()
263
+ expect(refreshAllPointSymbols).toHaveBeenCalledTimes(1)
264
+ expect(eventBus.emit).toHaveBeenCalledTimes(1)
265
+
266
+ // Once the in-flight settle has fully completed (onDone fired), a genuinely new style
267
+ // change is free to trigger another one — the guard isn't stuck permanently latched.
268
+ handlerFor(eventBus.on, EVENTS.MAP_STYLE_CHANGE)()
269
+ expect(map.once).toHaveBeenCalledTimes(2)
270
+ })
271
+
272
+ // MapLibre's own setStyle() discards the entire previous Style — including every
273
+ // programmatically-added source/layer, draw's included — and mapbox-gl-draw has no
274
+ // listener of its own to restore them. Without this, every drawn feature (not just a
275
+ // selected point) goes invisible and unqueryable after a style reload.
276
+ //
277
+ // draw.set() is asserted only after refreshAllPointSymbols resolves, not right after the
278
+ // idle callback fires: mapbox-gl-draw's own store.render() is requestAnimationFrame-
279
+ // debounced and bails out silently if the source doesn't exist yet, so pushing draw.getAll()
280
+ // back in *before* every point's image ids have been re-resolved for the new style would
281
+ // schedule a deferred render using the previous (already-wiped) style's ids — racing a
282
+ // still-in-flight worker tile parse of that stale data against the correct one moments
283
+ // later. Exactly one write, with final data, after refreshAllPointSymbols settles.
284
+ test('re-creates the sources and layers, then re-pushes every feature (only once symbols have re-resolved), when the draw source is missing after settling', async () => {
285
+ const { map, eventBus, result } = setup({ hasDrawSource: false })
286
+
287
+ handlerFor(eventBus.on, EVENTS.MAP_SET_STYLE)('dark')
288
+ handlerFor(eventBus.on, EVENTS.MAP_STYLE_CHANGE)()
289
+ handlerFor(map.once, 'idle')()
290
+
291
+ expect(map.addSource).toHaveBeenCalledWith('mapbox-gl-draw-cold', { data: { type: 'FeatureCollection', features: [] }, type: 'geojson' })
292
+ expect(map.addSource).toHaveBeenCalledWith('mapbox-gl-draw-hot', { data: { type: 'FeatureCollection', features: [] }, type: 'geojson' })
293
+ // draw.options.styles (mirrors mapbox-gl-draw's own api.options = options) — the fully
294
+ // processed cold+hot layer pairs from construction time, not re-derived from scratch.
295
+ expect(map.addLayer).toHaveBeenCalledWith({ id: 'fill-inactive.cold' })
296
+ expect(map.addLayer).toHaveBeenCalledWith({ id: 'fill-inactive.hot' })
297
+ // Not yet — refreshAllPointSymbols (mocked as a pending promise below) hasn't resolved.
298
+ expect(result.draw.set).not.toHaveBeenCalled()
299
+
300
+ await Promise.resolve() // flush the refreshAllPointSymbols().then(...) microtask
301
+ // Sources start empty — every feature the draw control still holds (map-independent, in
302
+ // its own JS store) is pushed back through the public API to force a full render, now
303
+ // with every point's image ids already resolved for the new style.
304
+ expect(result.draw.set).toHaveBeenCalledWith(result.draw.getAll())
305
+ })
306
+
307
+ test('does nothing when the draw source survived the style reload', async () => {
308
+ const { map, eventBus, result } = setup({ hasDrawSource: true })
309
+
310
+ handlerFor(eventBus.on, EVENTS.MAP_SET_STYLE)('dark')
311
+ handlerFor(eventBus.on, EVENTS.MAP_STYLE_CHANGE)()
312
+ handlerFor(map.once, 'idle')()
313
+ await Promise.resolve()
314
+
315
+ expect(map.addSource).not.toHaveBeenCalled()
316
+ expect(map.addLayer).not.toHaveBeenCalled()
317
+ expect(result.draw.set).not.toHaveBeenCalled()
318
+ })
319
+
320
+ // A selected point's highlight ring (plugins/interact's useHighlightSync) only re-applies
321
+ // on MAP_DATA_CHANGE — this nudges it explicitly once refreshAllPointSymbols has actually
322
+ // finished, rather than relying on 'styledata'/'sourcedata' firing it as a side effect.
323
+ test('emits MAP_DATA_CHANGE once point symbols have actually finished re-resolving after a style change', async () => {
324
+ const { map, eventBus } = setup()
325
+ handlerFor(eventBus.on, EVENTS.MAP_SET_STYLE)('dark')
326
+ handlerFor(eventBus.on, EVENTS.MAP_STYLE_CHANGE)()
327
+ handlerFor(map.once, 'idle')()
328
+ await Promise.resolve() // flush the refreshAllPointSymbols().then(...) microtask
329
+ expect(eventBus.emit).toHaveBeenCalledWith(EVENTS.MAP_DATA_CHANGE)
330
+ })
331
+
332
+ // The highlight ring is a standalone layer added from outside mapbox-gl-draw's own render
333
+ // cycle (unlike a vertex's active ring, which is just a filter on a layer mapbox-gl-draw
334
+ // itself always keeps painted) — nothing guarantees MapLibre schedules a real paint pass to
335
+ // pick it up right after settling. Confirmed by repro: the ring, and separately this app's
336
+ // own queryRenderedFeatures-based hover cursor (same painted-tile-buffer dependency), stayed
337
+ // stale until an unrelated map drag forced a render.
338
+ test('triggers a repaint once point symbols have finished re-resolving after a style change', async () => {
339
+ const { map, eventBus } = setup()
340
+ handlerFor(eventBus.on, EVENTS.MAP_SET_STYLE)('dark')
341
+ handlerFor(eventBus.on, EVENTS.MAP_STYLE_CHANGE)()
342
+ handlerFor(map.once, 'idle')()
343
+ expect(map.triggerRepaint).not.toHaveBeenCalled()
344
+ await Promise.resolve()
345
+ expect(map.triggerRepaint).toHaveBeenCalled()
346
+ })
347
+
348
+ test('MAP_STYLE_CHANGE idle handler tolerates a missing edit container', () => {
349
+ const { map, eventBus } = setup()
350
+
351
+ handlerFor(eventBus.on, EVENTS.MAP_SET_STYLE)('dark')
352
+ handlerFor(eventBus.on, EVENTS.MAP_STYLE_CHANGE)()
224
353
  handlerFor(map.once, 'idle')()
225
354
 
226
355
  expect(applyTouchVertexColors).toHaveBeenCalledWith(undefined, 'dark', {})
227
356
  })
228
357
 
229
- test('MAP_SET_STYLE passes pluginConfig through to restyling and touch colours', () => {
358
+ test('MAP_STYLE_CHANGE passes pluginConfig through to restyling and touch colours', () => {
230
359
  const pluginConfig = { editStroke: '#custom' }
231
360
  const { map, eventBus } = setup({ pluginConfig })
232
361
 
233
362
  handlerFor(eventBus.on, EVENTS.MAP_SET_STYLE)('dark')
363
+ handlerFor(eventBus.on, EVENTS.MAP_STYLE_CHANGE)()
234
364
  handlerFor(map.once, 'idle')()
235
365
 
236
366
  expect(updateDrawStyles).toHaveBeenCalledWith(map, 'dark', pluginConfig)
@@ -249,6 +379,37 @@ describe('createMapboxDraw – event handlers', () => {
249
379
 
250
380
  expect(map.fire).toHaveBeenCalledWith('draw.scalechange', { scale: MAP_SIZE_SCALES.medium })
251
381
  })
382
+
383
+ // map.getPixelRatio() never updates itself from a plain map-size change — nothing in this
384
+ // app calls map.setPixelRatio() off MAP_SET_SIZE alone, so a point-symbol refresh here
385
+ // would run before the map actually knows about the new size. MAP_SET_PIXEL_RATIO (fired
386
+ // separately, after MAP_SET_SIZE, carrying the freshly computed value) owns that instead.
387
+ test('MAP_SET_SIZE does not refresh point symbols — MAP_SET_PIXEL_RATIO owns that', () => {
388
+ const { eventBus } = setup()
389
+ handlerFor(eventBus.on, EVENTS.MAP_SET_SIZE)('medium')
390
+ expect(refreshAllPointSymbols).not.toHaveBeenCalled()
391
+ })
392
+
393
+ test('MAP_SET_PIXEL_RATIO refreshes point symbols with the freshly computed pixel ratio', () => {
394
+ const { map, eventBus, mapProvider, result } = setup()
395
+ handlerFor(eventBus.on, EVENTS.MAP_SET_PIXEL_RATIO)(3)
396
+ expect(refreshAllPointSymbols).toHaveBeenCalledWith({ draw: result.draw, mapProvider, map, pixelRatioOverride: 3 })
397
+ })
398
+
399
+ test('MAP_SET_PIXEL_RATIO emits MAP_DATA_CHANGE once point symbols have actually finished re-resolving', async () => {
400
+ const { eventBus } = setup()
401
+ handlerFor(eventBus.on, EVENTS.MAP_SET_PIXEL_RATIO)(3)
402
+ await Promise.resolve()
403
+ expect(eventBus.emit).toHaveBeenCalledWith(EVENTS.MAP_DATA_CHANGE)
404
+ })
405
+
406
+ test('MAP_SET_PIXEL_RATIO triggers a repaint once point symbols have finished re-resolving', async () => {
407
+ const { map, eventBus } = setup()
408
+ handlerFor(eventBus.on, EVENTS.MAP_SET_PIXEL_RATIO)(3)
409
+ expect(map.triggerRepaint).not.toHaveBeenCalled()
410
+ await Promise.resolve()
411
+ expect(map.triggerRepaint).toHaveBeenCalled()
412
+ })
252
413
  })
253
414
 
254
415
  describe('createMapboxDraw – cleanup', () => {
@@ -260,7 +421,9 @@ describe('createMapboxDraw – cleanup', () => {
260
421
 
261
422
  expect(removeWorkaround).toHaveBeenCalledTimes(1)
262
423
  expect(eventBus.off).toHaveBeenCalledWith(EVENTS.MAP_SET_STYLE, expect.any(Function))
424
+ expect(eventBus.off).toHaveBeenCalledWith(EVENTS.MAP_STYLE_CHANGE, expect.any(Function))
263
425
  expect(eventBus.off).toHaveBeenCalledWith(EVENTS.MAP_SET_SIZE, expect.any(Function))
426
+ expect(eventBus.off).toHaveBeenCalledWith(EVENTS.MAP_SET_PIXEL_RATIO, expect.any(Function))
264
427
  expect(draw.changeMode).toHaveBeenCalledWith('disabled')
265
428
  expect(mapProvider.draw).toBeNull()
266
429
  expect(mapProvider._mapboxDrawInstance).toBe(draw)
@@ -229,6 +229,18 @@ describe('line mode mouse interactions', () => {
229
229
  })
230
230
  })
231
231
 
232
+ describe('_canPlaceVertex guard', () => {
233
+ test('allows placement when there is no sketch feature yet (nothing to validate against)', () => {
234
+ const { ctx, state } = setup(DrawPolygonMode)
235
+ expect(ctx._canPlaceVertex({ ...state, polygon: null }, [1, 2])).toBe(true)
236
+ })
237
+
238
+ test('allows placement when there is no candidate point yet', () => {
239
+ const { ctx, state } = setup(DrawPolygonMode)
240
+ expect(ctx._canPlaceVertex(state, null)).toBe(true)
241
+ })
242
+ })
243
+
232
244
  describe('onCreate (draw.create re-id)', () => {
233
245
  test('re-ids the created feature to the requested featureId', () => {
234
246
  const { ctx } = setup(DrawPolygonMode)
@@ -70,7 +70,15 @@ export const createLifecycle = ({ ParentMode, featureProp, excludeFeatureIdFromS
70
70
  onStop (state) {
71
71
  ParentMode.onStop.call(this, state)
72
72
  this._listeners.forEach(([t, e, h]) => t.removeEventListener ? t.removeEventListener(e, h) : t.off(e, h))
73
- this._hideCrossHair(state)
73
+ // A touch/keyboard session leaving draw mode is about to land in interact mode, which
74
+ // needs the same crosshair to select the just-placed feature — only a mouse session (which
75
+ // selects by direct click) has no further use for it. onCreate's mode change to 'disabled'
76
+ // is itself setTimeout-deferred, landing after interactPlugin's own re-enable has already
77
+ // re-shown the crosshair for touch/keyboard — hiding it here unconditionally would win that
78
+ // race and undo it.
79
+ if (!['touch', 'keyboard'].includes(state.interfaceType)) {
80
+ this._hideCrossHair(state)
81
+ }
74
82
  // Sync the final interfaceType from draw mode back to app state so crosshair
75
83
  // visibility is correct when exiting draw mode (e.g., if user switched from mouse to keyboard)
76
84
  this.map.fire('draw.interfacetypechange', { interfaceType: state.interfaceType })
@@ -33,11 +33,11 @@ describe('setup and crosshair', () => {
33
33
  })
34
34
 
35
35
  describe('onStop', () => {
36
- test('removes listeners, hides the crosshair and reports the final interface type', () => {
37
- const { ctx, state, marker, container } = setup(DrawPolygonMode, { interfaceType: 'keyboard' })
36
+ test('mouse session: removes listeners, hides the crosshair and reports the final interface type', () => {
37
+ const { ctx, state, marker, container } = setup(DrawPolygonMode, { interfaceType: 'mouse' })
38
38
  ctx.onStop(state)
39
39
  expect(marker.style.display).toBe('none')
40
- expect(firedWith(ctx.map, 'draw.interfacetypechange')).toEqual([{ interfaceType: 'keyboard' }])
40
+ expect(firedWith(ctx.map, 'draw.interfacetypechange')).toEqual([{ interfaceType: 'mouse' }])
41
41
 
42
42
  // Window/container/map listeners are gone: keydown no longer shows the crosshair
43
43
  container.focus()
@@ -45,4 +45,25 @@ describe('onStop', () => {
45
45
  expect(marker.style.display).toBe('none')
46
46
  expect(ctx.map.off).toHaveBeenCalledWith('draw.create', ctx.createHandler)
47
47
  })
48
+
49
+ // A touch/keyboard session leaving draw mode lands in interact mode, which needs the same
50
+ // crosshair to select the just-placed feature — unlike mouse, onStop must leave it visible.
51
+ test('keyboard session: still removes listeners and reports interface type, but leaves the crosshair visible', () => {
52
+ const { ctx, state, marker, container } = setup(DrawPolygonMode, { interfaceType: 'keyboard' })
53
+ ctx.onStop(state)
54
+ expect(marker.style.display).toBe('block')
55
+ expect(firedWith(ctx.map, 'draw.interfacetypechange')).toEqual([{ interfaceType: 'keyboard' }])
56
+ expect(ctx.map.off).toHaveBeenCalledWith('draw.create', ctx.createHandler)
57
+
58
+ container.focus()
59
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' }))
60
+ expect(marker.style.display).toBe('block')
61
+ })
62
+
63
+ test('touch session: leaves the crosshair visible too', () => {
64
+ const crossHair = { show: jest.fn(), hide: jest.fn() }
65
+ const { ctx, state } = setup(DrawPolygonMode, { interfaceType: 'touch', crossHair })
66
+ ctx.onStop(state)
67
+ expect(crossHair.hide).not.toHaveBeenCalled()
68
+ })
48
69
  })
@@ -17,7 +17,7 @@ export const createPointerHandlers = ({ ParentMode, getFeature, getCoords }) =>
17
17
  this.onMove(state, e)
18
18
  },
19
19
 
20
- // The global interface type (e.g. switching to touch and panning via MoveControl)
20
+ // The global interface type (e.g. switching to touch and panning via MoveControls)
21
21
  // can change mid-session without any touch/pointer/key event ever landing on the
22
22
  // map container, so this can't rely on onTouchStart/onPointerdown alone — refresh
23
23
  // the rubber band immediately rather than waiting for the next incidental 'move'.
@@ -44,7 +44,7 @@ describe('touch and pointer interface', () => {
44
44
  expect(marker.style.display).toBe('none')
45
45
  })
46
46
 
47
- test('draw.interfacetypechange (e.g. switching to touch and panning via MoveControl mid-session) updates the interface and refreshes the rubber band immediately', () => {
47
+ test('draw.interfacetypechange (e.g. switching to touch and panning via MoveControls mid-session) updates the interface and refreshes the rubber band immediately', () => {
48
48
  const { ctx, state } = setup(DrawPolygonMode)
49
49
  clickAt(ctx, state, 0, 0)
50
50
  ctx.map.fire('draw.interfacetypechange', { interfaceType: 'touch' })
@@ -0,0 +1,194 @@
1
+ import MapboxDraw from '@mapbox/mapbox-gl-draw'
2
+ import { createLifecycle } from './drawMode/lifecycle.js'
3
+ import { validatePlacement } from '../../../validation/validateGeometry.js'
4
+ import {
5
+ getSnapInstance, isSnapEnabled, isSnapActive, getSnapLngLat, triggerSnapAtPoint, triggerSnapAtCenter
6
+ } from '../utils/snapHelpers.js'
7
+
8
+ const MODE = 'draw_point'
9
+
10
+ // Only these keys signal a switch to keyboard drawing (matches drawMode/createDrawMode.js's
11
+ // INTERFACE_KEYS) — modifier keys and shortcut chords like cmd+z must not show the crosshair.
12
+ const INTERFACE_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter'])
13
+
14
+ // A point has no ring/rubber-band to build a candidate from — just the coordinate about to be
15
+ // placed. Mirrors validateGeometry.js's attemptPlacement, but that helper hardcodes
16
+ // Polygon/LineString via MODE_BY_GEOMETRY, so a Point candidate is built and validated directly.
17
+ const canPlace = (map, coord) => {
18
+ const candidate = { type: 'Feature', geometry: { type: 'Point', coordinates: coord }, properties: {} }
19
+ const { valid, reason } = validatePlacement(
20
+ candidate,
21
+ { mode: MODE, vertexIndex: 0 },
22
+ { onGeometryChange: map._drawGeometryValidator }
23
+ )
24
+ if (!valid) {
25
+ map.fire('draw.placementblocked', { feature: candidate, reason: reason ?? null, phase: 'place', mode: MODE, vertexIndex: 0 })
26
+ }
27
+ return valid
28
+ }
29
+
30
+ const ParentMode = MapboxDraw.modes.draw_point
31
+
32
+ // createLifecycle is the one createDrawMode cluster that IS reused here — its onSetup/onStop
33
+ // are genuinely geometry-agnostic (event binding/teardown, crosshair show/hide), not coupled
34
+ // to the ring-of-vertices shape the other clusters (clickHandlers, keyboardHandlers,
35
+ // pointerHandlers, undoHandlers, renderHelpers) are built around. See the mode's own handlers
36
+ // below for why those aren't reused.
37
+ const lifecycle = createLifecycle({ ParentMode, featureProp: 'point', excludeFeatureIdFromSetup: false })
38
+
39
+ const pointHandlers = {
40
+ // Neutralise the built-in's onKeyUp (Escape/Enter both cancel) — this mode's Escape/Enter
41
+ // handling lives entirely in onKeyup (lowercase, window-level) below, which fires for both
42
+ // keyboard-focused and mouse/touch sessions alike, so there is nothing left for the
43
+ // container-scoped capital-U hook to do.
44
+ onKeyUp () {},
45
+
46
+ _showCrossHair (state) {
47
+ if (state.crossHair) { state.crossHair.show() } else { state.vertexMarker.style.display = 'block' }
48
+ },
49
+
50
+ _hideCrossHair (state) {
51
+ if (state.crossHair) { state.crossHair.hide() } else { state.vertexMarker.style.display = 'none' }
52
+ },
53
+
54
+ _setInterface (state, type, show = true) {
55
+ state.interfaceType = type
56
+ if (show) { this._showCrossHair(state) }
57
+ },
58
+
59
+ // Re-id the freshly created feature to the caller's requested id — mapbox-gl-draw assigns
60
+ // its own id on creation, and the caller's featureId (passed through changeMode) must win.
61
+ // Mirrors drawMode/clickHandlers.js's reidCreatedFeature. Symbol-image resolution runs
62
+ // after the re-id (not before) so it writes to the feature under its final id — the
63
+ // resolver itself is injected by the adapter via changeMode's options so this mode has
64
+ // no direct dependency on symbolRegistry/mapProvider.
65
+ onCreate (state, e) {
66
+ const feature = e.features[0]
67
+ this._ctx.api.delete(feature.id)
68
+ feature.id = state.featureId
69
+ this._ctx.api.add(feature, { userProperties: true })
70
+ state.resolvePointSymbol?.(state.featureId, feature.properties)
71
+ },
72
+
73
+ onUndo () {}, // Nothing to undo before a single-click commit — satisfies createLifecycle's bind contract.
74
+ onPointerup () {}, // No vertex count to report — ditto.
75
+
76
+ // Keeps the snap indicator live at the crosshair as the map pans underneath it — the
77
+ // point-mode equivalent of drawMode/pointerHandlers.js's onMove, minus the rubber-band
78
+ // update (nothing to rubber-band for a single coordinate). Bound to MapLibre's own 'move'
79
+ // event by createLifecycle, so this fires continuously while panning; also called
80
+ // explicitly below wherever the interface type switches to touch/keyboard, so the
81
+ // indicator appears immediately rather than waiting for the first pan.
82
+ onMove (state) {
83
+ if (['touch', 'keyboard'].includes(state.interfaceType) && isSnapEnabled(state)) {
84
+ triggerSnapAtCenter(getSnapInstance(this.map), this.map)
85
+ }
86
+ },
87
+
88
+ onBlur (state, e) {
89
+ if (e.target !== state.container) { this._hideCrossHair(state) }
90
+ },
91
+
92
+ onPointerdown (state, e) {
93
+ if (e.pointerType !== 'touch') { this._setInterface(state, 'mouse', false) }
94
+ },
95
+
96
+ onPointermove (state, e) {
97
+ if (e.pointerType !== 'touch') { this._hideCrossHair(state) }
98
+ },
99
+
100
+ onTouchStart (state) { this._setInterface(state, 'touch'); this.onMove(state) },
101
+ onTouchEnd (state) { this._setInterface(state, 'touch'); this.onMove(state) },
102
+
103
+ onInterfaceTypeChange (state, e) {
104
+ this._setInterface(state, e.interfaceType, ['touch', 'keyboard'].includes(e.interfaceType))
105
+ this.onMove(state)
106
+ },
107
+
108
+ // Continuously track a snap candidate under the real cursor (mouse only — touch/keyboard
109
+ // placement tracks the crosshair instead, triggered once at commit time in _placeAtCrossHair).
110
+ onMouseMove (state, e) {
111
+ if (isSnapEnabled(state)) {
112
+ triggerSnapAtPoint(getSnapInstance(this.map), this.map, e.point)
113
+ }
114
+ },
115
+
116
+ onVertexButtonClick (state, e) {
117
+ if (state.addVertexButtonId && !this.map._undoInProgress && e.target.closest(`#${state.addVertexButtonId}`)) {
118
+ this._placeAtCrossHair(state)
119
+ }
120
+ },
121
+
122
+ onKeydown (state, e) {
123
+ if (document.activeElement !== state.container) { return }
124
+ if (e.key === 'Escape') { e.preventDefault(); return }
125
+ if (e.key === 'Enter') { state.isActive = true }
126
+ if (!INTERFACE_KEYS.has(e.key)) { return }
127
+ this._setInterface(state, 'keyboard')
128
+ this.onMove(state)
129
+ },
130
+
131
+ // Window-level: fires regardless of which element has focus, so mouse/touch users can
132
+ // Escape a pending placement without ever focusing the map, and keyboard users placing via
133
+ // crosshair can commit with Enter. A point has no partial ring to reinitialize the way
134
+ // line/polygon's Escape does, so Escape always routes to draw.cancel, keyboard-focused or not.
135
+ onKeyup (state, e) {
136
+ if (e.key === 'Escape') {
137
+ this.map.fire('draw.cancel')
138
+ return
139
+ }
140
+ if (document.activeElement !== state.container) { return }
141
+ if (!INTERFACE_KEYS.has(e.key)) { return }
142
+ this._setInterface(state, 'keyboard')
143
+ this.onMove(state)
144
+ if (e.key === 'Enter' && state.isActive) { this._placeAtCrossHair(state) }
145
+ },
146
+
147
+ onClick (state, e) {
148
+ if (e.originalEvent.button > 0 || e.originalEvent.target !== this.map.getCanvas()) { return }
149
+ const snap = getSnapInstance(this.map)
150
+ const snapped = isSnapEnabled(state) && isSnapActive(snap) ? getSnapLngLat(snap) : null
151
+ this._commit(state, snapped || e.lngLat)
152
+ },
153
+
154
+ onTap (state, e) { this.onClick(state, e) },
155
+
156
+ // Touch "Add point" button and keyboard Enter both place at the crosshair (map centre),
157
+ // snapping to a candidate under it if one exists. Deliberately bypasses onClick — its
158
+ // button/canvas-target guards are real-mouse-click-only and would reject a synthetic commit,
159
+ // the same reason drawMode/clickHandlers.js's doClick calls ParentMode.onClick directly
160
+ // rather than going through its own wrapped onClick.
161
+ _placeAtCrossHair (state) {
162
+ const snap = getSnapInstance(this.map)
163
+ if (isSnapEnabled(state)) { triggerSnapAtCenter(snap, this.map) }
164
+ const snapped = isSnapEnabled(state) && isSnapActive(snap) ? getSnapLngLat(snap) : null
165
+ this._commit(state, snapped || this.map.getCenter())
166
+ // Unlike a real click (dispatched through mapbox-gl-draw's own mode-handler loop, which
167
+ // re-renders automatically after every call), this runs from our own window/container
168
+ // listeners outside that loop, so the render has to be requested explicitly.
169
+ this._ctx.store.render()
170
+ },
171
+
172
+ // Hard gate: a vetoed placement never appears, mirroring drawMode/clickHandlers.js's
173
+ // _canPlaceVertex. Shared by both the real-click and crosshair-commit paths.
174
+ _commit (state, lngLat) {
175
+ if (!canPlace(this.map, [lngLat.lng, lngLat.lat])) { return }
176
+ state.point.updateCoordinate('', lngLat.lng, lngLat.lat)
177
+ this.map.fire('draw.create', { features: [state.point.toGeoJSON()] })
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Draw mode for placing a single point: mouse click, or crosshair + Enter/"Add point" button
183
+ * for touch/keyboard. Commits immediately on the first placement — unlike draw_line/draw_polygon,
184
+ * there is no rubber band, no multi-vertex undo, and no "Done" step; the mode self-exits via
185
+ * events.js's onCreate handler once mapbox-gl-draw fires 'draw.create'.
186
+ *
187
+ * Wraps mapbox-gl-draw's own built-in draw_point mode (MapboxDraw.modes.draw_point) for
188
+ * onSetup/onStop/toDisplayFeatures/onTrash — it already creates and tracks the in-progress
189
+ * point feature, and cleans up an uncommitted one on stop. Its onClick/onTap/onKeyUp are
190
+ * overridden outright: the built-in has no placement-veto hook, and treats both Escape and
191
+ * Enter as "abandon and revert to simple_select" — this app needs Enter to place, and a
192
+ * cancel path that goes through draw.cancel like every other mode's does.
193
+ */
194
+ export const DrawPointMode = { ...ParentMode, ...lifecycle, ...pointHandlers }