@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
@@ -36,10 +36,48 @@ export const anchorToMaplibre = ([ax, ay]) => {
36
36
  return (y + (x && y ? '-' : '') + x) || 'center'
37
37
  }
38
38
 
39
+ // The discrete fraction (0, 0.5 or 1) icon-anchor actually renders a given axis at — the
40
+ // same left/right/top/bottom/center snapping xAnchor/yAnchor above already do, expressed as
41
+ // a number instead of a string so it can be compared against the true, unsnapped fraction.
42
+ const discreteFraction = (a) => {
43
+ if (a <= ANCHOR_LOW) { return 0 }
44
+ if (a >= ANCHOR_HIGH) { return 1 }
45
+ return 0.5
46
+ }
47
+
48
+ /**
49
+ * MapLibre's icon-anchor only has 9 discrete positions — anchorToMaplibre above snaps a
50
+ * fractional anchor to the nearest one, which loses precision for anything off-grid (e.g.
51
+ * pin's [0.5, 0.9] renders as if it were 1.0/'bottom', shifting the whole icon ~10% of its
52
+ * height off from where a DOM marker using the same anchor value would place it —
53
+ * SymbolMarker.jsx positions markers with the exact fraction via CSS margins, with no such
54
+ * snapping). icon-offset corrects this: the pixel distance, in the icon's own display-space
55
+ * dimensions (derived from its viewBox), between the true anchor and the discrete one it
56
+ * snapped to — applied on top of icon-anchor, on both provider and dataset symbol layers.
57
+ *
58
+ * @param {number[]} anchor - [x, y] in 0–1 space (the true, unsnapped anchor)
59
+ * @param {string} viewBox - SVG viewBox string, e.g. '0 0 44 44'
60
+ * @returns {number[]} [offsetX, offsetY] in pixels, for the icon-offset layout property
61
+ */
62
+ // Rounded to 2dp: sub-hundredth-of-a-pixel precision is meaningless for rendering, and
63
+ // without it fractions like 0.8 produce float noise (8.799999999999997, not 8.8).
64
+ const round2dp = (n) => Math.round(n * 100) / 100
65
+
66
+ export const anchorToMaplibreOffset = ([ax, ay], viewBox) => {
67
+ const [,, width, height] = viewBox.split(' ').map(Number)
68
+ return [
69
+ round2dp((discreteFraction(ax) - ax) * width),
70
+ round2dp((discreteFraction(ay) - ay) * height)
71
+ ]
72
+ }
73
+
39
74
  /**
40
75
  * Register normal, active (both rings) and selected (black ring) symbol images.
41
76
  * Skips images that are already registered (safe to call on style change).
42
- * Updates `map._activeSymbolImageMap` (normal→active) and `map._selectedSymbolImageMap` (normal→selected).
77
+ * Merges into `map._activeSymbolImageMap` (normal→active) and `map._selectedSymbolImageMap`
78
+ * (normal→selected) rather than replacing them — more than one caller (e.g. the datasets
79
+ * and draw plugins) can register symbols on the same map, and each call must add to that
80
+ * shared registry, not wipe out whatever another caller already registered there.
43
81
  *
44
82
  * @param {Object} map - MapLibre map instance
45
83
  * @param {Object[]} styleArray - an array of symbol configs
@@ -53,8 +91,8 @@ export const addSymbolsToMap = async (map, styleArray, mapStyle, symbolRegistry,
53
91
  return
54
92
  }
55
93
 
56
- map._activeSymbolImageMap = {}
57
- map._selectedSymbolImageMap = {}
94
+ map._activeSymbolImageMap ??= {}
95
+ map._selectedSymbolImageMap ??= {}
58
96
 
59
97
  await Promise.all(styleArray.flatMap(config => {
60
98
  const normalId = symbolRegistry.getSymbolImageId(config, mapStyle, false, pixelRatio)
@@ -1,4 +1,4 @@
1
- import { anchorToMaplibre, addSymbolsToMap } from './symbolImages.js'
1
+ import { anchorToMaplibre, anchorToMaplibreOffset, addSymbolsToMap } from './symbolImages.js'
2
2
  import { symbolRegistry } from '../../../../src/services/symbolRegistry.js'
3
3
 
4
4
  beforeAll(() => {
@@ -84,6 +84,36 @@ describe('anchorToMaplibre', () => {
84
84
  })
85
85
  })
86
86
 
87
+ // ─── anchorToMaplibreOffset ───────────────────────────────────────────────────
88
+
89
+ describe('anchorToMaplibreOffset', () => {
90
+ const VIEW_BOX = '0 0 44 44'
91
+
92
+ it('is zero for an anchor that already sits exactly on a discrete position', () => {
93
+ expect(anchorToMaplibreOffset([0.5, 0.5], VIEW_BOX)).toEqual([0, 0]) // center
94
+ expect(anchorToMaplibreOffset([0, 0], VIEW_BOX)).toEqual([0, 0]) // top-left
95
+ expect(anchorToMaplibreOffset([1, 1], VIEW_BOX)).toEqual([0, 0]) // bottom-right
96
+ })
97
+
98
+ it('corrects for pin-style [0.5, 0.9] snapping to bottom (1.0)', () => {
99
+ expect(anchorToMaplibreOffset([0.5, 0.9], VIEW_BOX)).toEqual([0, 4.4])
100
+ })
101
+
102
+ it('corrects both axes when both are off-grid', () => {
103
+ // 0.1 snaps to 'left'/'top' (0), 0.9 snaps to 'right'/'bottom' (1)
104
+ expect(anchorToMaplibreOffset([0.1, 0.1], VIEW_BOX)).toEqual([-4.4, -4.4])
105
+ expect(anchorToMaplibreOffset([0.9, 0.9], VIEW_BOX)).toEqual([4.4, 4.4])
106
+ })
107
+
108
+ it('still corrects a value inside the centre band that is not exactly 0.5 — the whole band snaps to 0.5, not just its edges', () => {
109
+ expect(anchorToMaplibreOffset([0.4, 0.6], VIEW_BOX)).toEqual([4.4, -4.4])
110
+ })
111
+
112
+ it('scales with the viewBox, not just the anchor fraction', () => {
113
+ expect(anchorToMaplibreOffset([0.5, 0.9], '0 0 100 100')).toEqual([0, 10])
114
+ })
115
+ })
116
+
87
117
  // ─── addSymbolsToMap ──────────────────────────────────────────────────────────
88
118
 
89
119
  const makeMap = (existingIds = []) => ({
@@ -103,13 +133,17 @@ describe('addSymbolsToMap — registration', () => {
103
133
  expect(map.addImage).not.toHaveBeenCalled()
104
134
  })
105
135
 
106
- it('resets _activeSymbolImageMap and _selectedSymbolImageMap before processing', async () => {
136
+ it('merges into existing _activeSymbolImageMap/_selectedSymbolImageMap rather than replacing them', async () => {
137
+ // More than one caller (datasets, draw, ...) can register symbols on the same map —
138
+ // a later call must add to the shared registry, not wipe an earlier caller's entries.
107
139
  const map = makeMap()
108
- map._activeSymbolImageMap = { stale: 'entry' }
109
- map._selectedSymbolImageMap = { stale: 'entry' }
140
+ map._activeSymbolImageMap = { 'other-normal-id': 'other-active-id' }
141
+ map._selectedSymbolImageMap = { 'other-normal-id': 'other-selected-id' }
110
142
  await addSymbolsToMap(map, [{ symbol: 'pin' }], mapStyle, symbolRegistry)
111
- expect(map._activeSymbolImageMap).not.toHaveProperty('stale')
112
- expect(map._selectedSymbolImageMap).not.toHaveProperty('stale')
143
+ expect(map._activeSymbolImageMap['other-normal-id']).toBe('other-active-id')
144
+ expect(map._selectedSymbolImageMap['other-normal-id']).toBe('other-selected-id')
145
+ const normalId = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle, false)
146
+ expect(map._activeSymbolImageMap).toHaveProperty(normalId)
113
147
  })
114
148
 
115
149
  it('calls addImage for normal, active and selected variants', async () => {
package/rollup.esm.mjs CHANGED
@@ -182,7 +182,7 @@ const createESMConfig = (entryPath, outDir, isCore = false, manualChunks = null,
182
182
  open: false,
183
183
  gzipSize: true
184
184
  })]
185
- : [])
185
+ : [])
186
186
  ],
187
187
 
188
188
  output: {
@@ -303,6 +303,16 @@ const ALL_BUILDS = [
303
303
  entryPath: './plugins/beta/frame/src/index.js',
304
304
  outDir: 'plugins/beta/frame/dist/esm',
305
305
  manualChunks: (id) => id.includes('/manifest') ? 'im-frame-plugin' : undefined
306
+ },
307
+ {
308
+ entryPath: './plugins/map-key/src/index.js',
309
+ outDir: 'plugins/map-key/dist/esm',
310
+ manualChunks: (id) => id.includes('/manifest') ? 'im-map-key-plugin' : undefined
311
+ },
312
+ {
313
+ entryPath: './plugins/menu/src/index.js',
314
+ outDir: 'plugins/menu/dist/esm',
315
+ manualChunks: (id) => id.includes('/manifest') ? 'im-menu-plugin' : undefined
306
316
  }
307
317
  ]
308
318
 
@@ -17,7 +17,7 @@ sonar.test.inclusions=**/*.test.*,**/__mocks__/**,**/__stubs__/**
17
17
  sonar.cpd.exclusions=**/*.test.*,**/__mocks__/**,**/__stubs__/**,**/__helpers__/**
18
18
 
19
19
  # Ignored rules
20
- sonar.issue.ignore.multicriteria=reactPropsJs,reactPropsJsx,preferGlobalThisJs,preferGlobalThisJsx,preferAtJs,preferAtJsx,replaceAllJs,replaceAllJsx,stringReplaceAllJs,stringReplaceAllJsx,pascalCaseFunctionsJs,pascalCaseFunctionsJsx,pascalCaseFunctionsTs,pascalCaseFunctionsTsx,cssScopingRootMixins
20
+ sonar.issue.ignore.multicriteria=reactPropsJs,reactPropsJsx,preferGlobalThisJs,preferGlobalThisJsx,preferAtJs,preferAtJsx,replaceAllJs,replaceAllJsx,stringReplaceAllJs,stringReplaceAllJsx,pascalCaseFunctionsJs,pascalCaseFunctionsJsx,pascalCaseFunctionsTs,pascalCaseFunctionsTsx,cssScopingRootMixins,parameterizedTestsJs,parameterizedTestsJsx
21
21
 
22
22
  # S6774: React props validation - using TypeScript/JSDoc for prop types instead
23
23
  sonar.issue.ignore.multicriteria.reactPropsJs.ruleKey=javascript:S6774
@@ -59,6 +59,12 @@ sonar.issue.ignore.multicriteria.pascalCaseFunctionsTs.resourceKey=**/*.ts
59
59
  sonar.issue.ignore.multicriteria.pascalCaseFunctionsTsx.ruleKey=javascript:S100
60
60
  sonar.issue.ignore.multicriteria.pascalCaseFunctionsTsx.resourceKey=**/*.tsx
61
61
 
62
+ # S5976: Replace these N tests with a single Parameterized one - jest.each is preferred over parameterized tests
63
+ sonar.issue.ignore.multicriteria.parameterizedTestsJs.ruleKey=javascript:S5976
64
+ sonar.issue.ignore.multicriteria.parameterizedTestsJs.resourceKey=**/*.test.js
65
+ sonar.issue.ignore.multicriteria.parameterizedTestsJsx.ruleKey=javascript:S5976
66
+ sonar.issue.ignore.multicriteria.parameterizedTestsJsx.resourceKey=**/*.test.jsx
67
+
62
68
  # S8776: Missing scoping root for & - false positive on Sass mixins, which are always
63
69
  # @included inside a real selector so & resolves correctly once compiled
64
70
  sonar.issue.ignore.multicriteria.cssScopingRootMixins.ruleKey=css:S8776
@@ -1,11 +1,16 @@
1
1
  import React from 'react'
2
2
  import { render, screen } from '@testing-library/react'
3
3
  import { Actions } from './Actions.jsx'
4
+ import { mapButtons } from '../../renderer/mapButtons.js'
4
5
  import * as AppContext from '../../store/appContext' // import the module to mock
5
6
 
6
7
  // Simple child component to handle isHidden
7
8
  const TestChild = ({ isHidden, children, ...props }) => <div {...props}>{children}</div>
8
9
 
10
+ jest.mock('../MapButton/MapButton.jsx', () => ({
11
+ MapButton: ({ isHidden }) => <button data-testid='map-button' hidden={isHidden} />
12
+ }))
13
+
9
14
  const ACTIONS_SELECTOR = '.im-c-actions'
10
15
  const HIDDEN_CLASS = 'im-c-actions--hidden'
11
16
 
@@ -80,4 +85,46 @@ describe('Actions component', () => {
80
85
  const container = screen.getByTestId('child1').closest(ACTIONS_SELECTOR)
81
86
  expect(container).toHaveClass(HIDDEN_CLASS)
82
87
  })
88
+
89
+ // Integration with the real mapButtons() output, not just the TestChild stand-in above —
90
+ // guards the isHidden/variant contract at the actual seam between the two modules, since
91
+ // mapButtons.js's own unit tests can't catch a break here on their own.
92
+ describe('with real mapButtons() output', () => {
93
+ const evaluateProp = (prop) => (typeof prop === 'function' ? prop() : prop)
94
+ const appConfig = { id: 'test' }
95
+ const baseAppState = {
96
+ breakpoint: 'desktop',
97
+ mode: 'view',
98
+ isFullscreen: true,
99
+ openPanels: {},
100
+ dispatch: jest.fn(),
101
+ disabledButtons: new Set(),
102
+ hiddenButtons: new Set(),
103
+ pressedButtons: new Set(),
104
+ expandedButtons: new Set(),
105
+ panelConfig: {},
106
+ layoutRefs: { viewportRef: { current: { focus: jest.fn() } } }
107
+ }
108
+
109
+ const renderActionsWith = (appState) => {
110
+ const items = mapButtons({ slot: 'actions', appState, appConfig, evaluateProp })
111
+ return render(<Actions slot='actions'>{items.map(i => i.element)}</Actions>)
112
+ }
113
+
114
+ it('shows the actions bar when a real mapButtons() button is visible', () => {
115
+ const appState = { ...baseAppState, buttonConfig: { cancel: { label: 'Cancel', desktop: { slot: 'actions' } } } }
116
+ renderActionsWith(appState)
117
+ expect(screen.getByTestId('map-button').closest(ACTIONS_SELECTOR)).not.toHaveClass(HIDDEN_CLASS)
118
+ })
119
+
120
+ it('hides the actions bar when the only real mapButtons() button is hidden', () => {
121
+ const appState = {
122
+ ...baseAppState,
123
+ hiddenButtons: new Set(['cancel']),
124
+ buttonConfig: { cancel: { label: 'Cancel', desktop: { slot: 'actions' } } }
125
+ }
126
+ renderActionsWith(appState)
127
+ expect(screen.getByTestId('map-button').closest(ACTIONS_SELECTOR)).toHaveClass(HIDDEN_CLASS)
128
+ })
129
+ })
83
130
  })
@@ -1,3 +1,5 @@
1
+ @use '../../../scss/tools/index' as tools;
2
+
1
3
  .im-c-attributions {
2
4
  color: var(--attributions-foreground-color);
3
5
  background-color: var(--attributions-background-color);
@@ -7,4 +9,12 @@
7
9
  overflow: hidden;
8
10
  text-overflow: ellipsis;
9
11
  pointer-events: auto;
12
+ }
13
+
14
+ .im-c-attributions a {
15
+ color: inherit;
16
+
17
+ &[data-focus-visible="true"] {
18
+ @include tools.link-focus;
19
+ }
10
20
  }
@@ -17,7 +17,7 @@ import { htmlToPlainText } from '../../../utils/htmlToPlainText.js'
17
17
  // eslint-disable-next-line camelcase, react/jsx-pascal-case
18
18
  // sonarjs/disable-next-line function-name
19
19
  export const Hints = () => {
20
- const { id, keyboardHintText } = useConfig()
20
+ const { id, keyboardHintText, enableMoveControls, moveControlsHintText } = useConfig()
21
21
  const { hints } = useService()
22
22
  const { layoutRefs } = useApp()
23
23
  const [activeHint, setActiveHint] = useState(null)
@@ -32,6 +32,11 @@ export const Hints = () => {
32
32
  return null
33
33
  }
34
34
 
35
+ // Composed at render time so overriding one text doesn't drop the other.
36
+ const keyboardDescriptionHtml = enableMoveControls
37
+ ? `${keyboardHintText} ${moveControlsHintText}`
38
+ : keyboardHintText
39
+
35
40
  return createPortal(
36
41
  <div className='im-o-hints'>
37
42
  <div id={`${id}-hints`} className='im-c-hints'>
@@ -45,7 +50,7 @@ export const Hints = () => {
45
50
  <div
46
51
  id={`${id}-keyboard-desc`}
47
52
  className='im-u-visually-hidden'
48
- dangerouslySetInnerHTML={{ __html: htmlToPlainText(keyboardHintText) }}
53
+ dangerouslySetInnerHTML={{ __html: htmlToPlainText(keyboardDescriptionHtml) }}
49
54
  />
50
55
  </div>,
51
56
  layoutRefs.mainRef.current
@@ -52,6 +52,19 @@ describe('Hints — rendering', () => {
52
52
  expect(desc.innerHTML).toBe('Shift + ?')
53
53
  })
54
54
 
55
+ it('appends the move-controls hint text when enableMoveControls is true', () => {
56
+ const mainEl = setup()
57
+ useConfig.mockReturnValue({
58
+ id: 'test-map',
59
+ keyboardHintText: '<kbd>Shift</kbd> + <kbd>?</kbd>',
60
+ enableMoveControls: true,
61
+ moveControlsHintText: '<kbd>Arrow keys</kbd> to move'
62
+ })
63
+ render(<Hints />)
64
+ const desc = mainEl.querySelector('#test-map-keyboard-desc')
65
+ expect(desc.innerHTML).toBe('Shift + ? Arrow keys to move')
66
+ })
67
+
55
68
  it('renders no hint content when there is no active hint', () => {
56
69
  const mainEl = setup()
57
70
  render(<Hints />)
@@ -2,7 +2,6 @@
2
2
  import React from 'react'
3
3
  import { useConfig } from '../../store/configContext'
4
4
  import { useApp } from '../../store/appContext.js'
5
- import { getKeyboardShortcuts } from '../../registry/keyboardShortcutRegistry.js'
6
5
  import { Tabs } from '../Tabs/Tabs.jsx'
7
6
 
8
7
  const ShortcutList = ({ items }) => (
@@ -42,8 +41,8 @@ const getDefaultTab = (groupEntries, context) => {
42
41
  // sonarjs/disable-next-line function-name
43
42
  export const KeyboardHelp = ({ context = 'viewport' }) => { // NOSONAR: project does not use PropTypes
44
43
  const appConfig = useConfig()
45
- const { listboxIsActive } = useApp()
46
- const allShortcuts = getKeyboardShortcuts(appConfig)
44
+ const { listboxIsActive, keyboardShortcutRegistry } = useApp()
45
+ const allShortcuts = keyboardShortcutRegistry.getKeyboardShortcuts(appConfig)
47
46
  const shortcuts = listboxIsActive
48
47
  ? allShortcuts
49
48
  : allShortcuts.filter(s => s.context !== 'listbox')
@@ -2,13 +2,10 @@
2
2
  import React from 'react'
3
3
  import { render, screen, fireEvent } from '@testing-library/react'
4
4
  import { KeyboardHelp } from './KeyboardHelp'
5
- import { getKeyboardShortcuts } from '../../registry/keyboardShortcutRegistry.js'
6
5
  import { useConfig } from '../../store/configContext'
7
6
  import { useApp } from '../../store/appContext.js'
8
7
 
9
- jest.mock('../../registry/keyboardShortcutRegistry.js', () => ({
10
- getKeyboardShortcuts: jest.fn()
11
- }))
8
+ const getKeyboardShortcuts = jest.fn()
12
9
 
13
10
  jest.mock('../../store/configContext', () => ({
14
11
  useConfig: jest.fn()
@@ -40,7 +37,7 @@ const GLOBAL_SHORTCUTS = [
40
37
 
41
38
  beforeEach(() => {
42
39
  useConfig.mockReturnValue({})
43
- useApp.mockReturnValue({ listboxIsActive: false })
40
+ useApp.mockReturnValue({ listboxIsActive: false, keyboardShortcutRegistry: { getKeyboardShortcuts } })
44
41
  })
45
42
 
46
43
  afterEach(() => {
@@ -79,7 +76,7 @@ describe('KeyboardHelp — tabs', () => {
79
76
  const allShortcuts = [...GLOBAL_SHORTCUTS, ...VIEWPORT_SHORTCUTS, ...LISTBOX_SHORTCUTS]
80
77
 
81
78
  beforeEach(() => {
82
- useApp.mockReturnValue({ listboxIsActive: true })
79
+ useApp.mockReturnValue({ listboxIsActive: true, keyboardShortcutRegistry: { getKeyboardShortcuts } })
83
80
  })
84
81
 
85
82
  it('renders a tab for each group when listboxIsActive is true', () => {
@@ -18,13 +18,13 @@ const ZOOM_ACTIONS = [
18
18
  { id: 'nudgeZoomOut', label: 'Zoom out', announceLabel: 'Zoomed out', method: 'zoomOut' }
19
19
  ]
20
20
 
21
- export const MoveControl = () => {
21
+ export const MoveControls = () => {
22
22
  const { id: appId, mapProvider, panDelta, nudgePanDelta, zoomDelta, nudgeZoomDelta } = useConfig()
23
23
  const { dispatch, expandedButtons, nudgeStepSize, interfaceType, layoutRefs } = useApp()
24
24
  const { isAtMaxZoom, isAtMinZoom } = useMap()
25
25
  const { announce } = useService()
26
26
 
27
- const isOpen = expandedButtons.has('moveControl')
27
+ const isOpen = expandedButtons.has('moveControls')
28
28
  const firstDirectionButtonId = `${appId}-pan-up`
29
29
 
30
30
  useEffect(() => {
@@ -41,7 +41,7 @@ export const MoveControl = () => {
41
41
  // Registry buttons (declared via a plugin's manifest) auto-return focus to the map
42
42
  // viewport after a click (see createButtonClickHandler in mapButtons.js) unless
43
43
  // marked keepFocus — which is how e.g. draw's own toolbar buttons never leave
44
- // keyboard shortcuts (like arrow-key vertex nudging) stranded afterward. MoveControl
44
+ // keyboard shortcuts (like arrow-key vertex nudging) stranded afterward. MoveControls
45
45
  // renders its own buttons directly, bypassing that mechanism entirely, so it has to
46
46
  // replicate the same return-to-viewport behaviour itself. But unlike a one-off
47
47
  // action button, the D-pad is meant to be pressed repeatedly — a keyboard/switch
@@ -70,7 +70,7 @@ export const MoveControl = () => {
70
70
 
71
71
  // Any plugin can claim the D-pad for something other than panning the map (e.g.
72
72
  // moving a selected draw vertex) by setting mapProvider.activeMoveTarget — a
73
- // generic { move(dx, dy, isLargeStep), label? } contract. MoveControl doesn't
73
+ // generic { move(dx, dy, isLargeStep), label? } contract. MoveControls doesn't
74
74
  // know or care what's claiming it; it just checks for a claimant before
75
75
  // defaulting to its own panBy behaviour. Read fresh on every click rather than
76
76
  // subscribed to reactively, since it's a plain mapProvider property, not state.
@@ -107,7 +107,7 @@ export const MoveControl = () => {
107
107
  // first tabbing all the way back to the map. Handling arrow keys here — while
108
108
  // focus is anywhere within this control, not just on a direction button — covers
109
109
  // that directly, without needing draw's (or the app's own) keyboard-shortcut
110
- // guards to special-case MoveControl's buttons. Only arrow keys are handled;
110
+ // guards to special-case MoveControls' buttons. Only arrow keys are handled;
111
111
  // other shortcuts (delete/escape/undo) still require focus on the viewport,
112
112
  // which mouse/touch users already get back automatically after any click here.
113
113
  const handleContainerKeyDown = (e) => {
@@ -120,12 +120,12 @@ export const MoveControl = () => {
120
120
  }
121
121
 
122
122
  const containerClassName = [
123
- 'im-c-move-control',
124
- !isOpen && 'im-c-move-control--collapsed'
123
+ 'im-c-move-controls',
124
+ !isOpen && 'im-c-move-controls--collapsed'
125
125
  ].filter(Boolean).join(' ')
126
126
 
127
127
  const directionsGroup = (
128
- <div key='directions' role='group' aria-label='Direction controls' className='im-c-move-control__directions'>{/* NOSONAR - div with role="group" is correct for a button group */}
128
+ <div key='directions' role='group' aria-label='Direction controls' className='im-c-move-controls__directions'>{/* NOSONAR - div with role="group" is correct for a button group */}
129
129
  {DIRECTIONS.map(({ id, verb, dx, dy }) => (
130
130
  <MapButton
131
131
  key={id}
@@ -154,7 +154,7 @@ export const MoveControl = () => {
154
154
  )
155
155
 
156
156
  const zoomGroup = (
157
- <div key='zoom' role='group' aria-label='Zoom controls' className='im-c-move-control__zoom'>{/* NOSONAR - div with role="group" is correct for a button group */}
157
+ <div key='zoom' role='group' aria-label='Zoom controls' className='im-c-move-controls__zoom'>{/* NOSONAR - div with role="group" is correct for a button group */}
158
158
  {ZOOM_ACTIONS.map(({ id, label, announceLabel, method }) => (
159
159
  <MapButton
160
160
  key={id}
@@ -169,7 +169,7 @@ export const MoveControl = () => {
169
169
  )
170
170
 
171
171
  return (
172
- <div id={`${appId}-move-control-content`} className={containerClassName} onKeyDown={handleContainerKeyDown}>{/* NOSONAR - only catches arrow-key bubbling from the real, already-focusable button descendants below; the div itself needs no role/tabIndex */}
172
+ <div id={`${appId}-move-controls-content`} className={containerClassName} onKeyDown={handleContainerKeyDown} data-map-keyboard-scope>{/* NOSONAR - only catches arrow-key bubbling from the real, already-focusable button descendants below; the div itself needs no role/tabIndex */}
173
173
  {directionsGroup}
174
174
  {zoomGroup}
175
175
  </div>
@@ -1,62 +1,63 @@
1
1
  @use '../../../scss/tools/index' as tools;
2
2
 
3
3
  // ===================================================
4
- // Component: MoveControl
4
+ // Component: MoveControls
5
5
  // ===================================================
6
6
 
7
7
  // 1. Base styles
8
- .im-c-move-control {
8
+ .im-c-move-controls {
9
9
  display: flex;
10
10
  flex-direction: column;
11
11
  align-items: center;
12
12
  gap: var(--divider-gap);
13
13
  }
14
14
 
15
- .im-c-move-control.im-c-move-control--collapsed {
15
+ .im-c-move-controls.im-c-move-controls--collapsed {
16
16
  display: none;
17
17
  }
18
18
 
19
19
  // 2. Elements
20
- .im-c-move-control__directions {
20
+ .im-c-move-controls__directions {
21
21
  display: grid;
22
22
  grid-template-columns: repeat(3, var(--button-size));
23
23
  grid-template-rows: repeat(3, var(--button-size));
24
24
  }
25
25
 
26
- .im-c-move-control__directions .im-c-button-wrapper--pan-up {
26
+ .im-c-move-controls__directions .im-c-button-wrapper--pan-up {
27
27
  grid-column: 2;
28
28
  grid-row: 1;
29
29
  }
30
30
 
31
- .im-c-move-control__directions .im-c-button-wrapper--pan-left {
31
+ .im-c-move-controls__directions .im-c-button-wrapper--pan-left {
32
32
  grid-column: 1;
33
33
  grid-row: 2;
34
34
  }
35
35
 
36
- .im-c-move-control__directions .im-c-button-wrapper--pan-right {
36
+ .im-c-move-controls__directions .im-c-button-wrapper--pan-right {
37
37
  grid-column: 3;
38
38
  grid-row: 2;
39
39
  }
40
40
 
41
- .im-c-move-control__directions .im-c-button-wrapper--pan-down {
41
+ .im-c-move-controls__directions .im-c-button-wrapper--pan-down {
42
42
  grid-column: 2;
43
43
  grid-row: 3;
44
44
  }
45
45
 
46
- .im-c-move-control__directions .im-c-button-wrapper--nudge-step-toggle {
46
+ .im-c-move-controls__directions .im-c-button-wrapper--nudge-step-toggle {
47
47
  grid-column: 2;
48
48
  grid-row: 2;
49
+ z-index: 1;
49
50
  }
50
51
 
51
- .im-c-move-control__directions .im-c-map-button--pan-up svg {
52
+ .im-c-move-controls__directions .im-c-map-button--pan-up svg {
52
53
  transform: rotate(180deg);
53
54
  }
54
55
 
55
- .im-c-move-control__directions .im-c-map-button--pan-left svg {
56
+ .im-c-move-controls__directions .im-c-map-button--pan-left svg {
56
57
  transform: rotate(90deg);
57
58
  }
58
59
 
59
- .im-c-move-control__directions .im-c-map-button--pan-right svg {
60
+ .im-c-move-controls__directions .im-c-map-button--pan-right svg {
60
61
  transform: rotate(-90deg);
61
62
  }
62
63
 
@@ -66,35 +67,60 @@
66
67
  // express a plus-shaped grid where the centre button touches all four neighbours —
67
68
  // clip-path is set directly per button instead, clipping only the side(s) where it
68
69
  // touches another button and leaving the exposed side(s) showing the shadow as normal.
69
- .im-c-move-control__directions .im-c-button-wrapper--pan-up .im-c-map-button::before {
70
- clip-path: inset(-20px -20px 0 -20px); // touches nudge-step-toggle below
70
+ .im-c-move-controls__directions .im-c-button-wrapper--pan-up .im-c-map-button {
71
+ border-bottom-left-radius: 0;
72
+ border-bottom-right-radius: 0;
73
+
74
+ &::before {
75
+ clip-path: inset(-20px -20px 0 -20px); // touches nudge-step-toggle below
76
+ }
71
77
  }
72
78
 
73
- .im-c-move-control__directions .im-c-button-wrapper--pan-left .im-c-map-button::before {
74
- clip-path: inset(-20px 0 -20px -20px); // touches nudge-step-toggle to the right
79
+ .im-c-move-controls__directions .im-c-button-wrapper--pan-left .im-c-map-button {
80
+ border-top-right-radius: 0;
81
+ border-bottom-right-radius: 0;
82
+
83
+ &::before {
84
+ clip-path: inset(-20px 0 -20px -20px); // touches nudge-step-toggle to the right
85
+ }
75
86
  }
76
87
 
77
- .im-c-move-control__directions .im-c-button-wrapper--pan-right .im-c-map-button::before {
78
- clip-path: inset(-20px -20px -20px 0); // touches nudge-step-toggle to the left
88
+ .im-c-move-controls__directions .im-c-button-wrapper--pan-right .im-c-map-button {
89
+ border-top-left-radius: 0;
90
+ border-bottom-left-radius: 0;
91
+
92
+ &::before {
93
+ clip-path: inset(-20px -20px -20px 0); // touches nudge-step-toggle to the left
94
+ }
79
95
  }
80
96
 
81
- .im-c-move-control__directions .im-c-button-wrapper--pan-down .im-c-map-button::before {
82
- clip-path: inset(0 -20px -20px -20px); // touches nudge-step-toggle above
97
+ .im-c-move-controls__directions .im-c-button-wrapper--pan-down .im-c-map-button {
98
+ border-top-left-radius: 0;
99
+ border-top-right-radius: 0;
100
+
101
+ &::before {
102
+ clip-path: inset(0 -20px -20px -20px); // touches nudge-step-toggle above
103
+ }
83
104
  }
84
105
 
85
- .im-c-move-control__directions .im-c-button-wrapper--nudge-step-toggle .im-c-map-button::before {
86
- clip-path: inset(0); // touches all four neighbours
106
+ .im-c-move-controls__directions .im-c-button-wrapper--nudge-step-toggle .im-c-map-button {
107
+ border-radius: 0;
108
+ outline: 1px solid var(--button-hover-color);
109
+
110
+ &::before {
111
+ clip-path: inset(0); // touches all four neighbours
112
+ }
87
113
  }
88
114
 
89
115
  // Same inverted "pressed" treatment already used for actions-slot toggle buttons
90
116
  // (e.g. draw's Snap toggle) — reused here so Nudge mode reads consistently active.
91
- .im-c-move-control .im-c-map-button[aria-pressed="true"] {
117
+ .im-c-move-controls .im-c-map-button[aria-pressed="true"] {
92
118
  color: var(--pressed-button-foreground-color);
93
119
  border-color: var(--pressed-button-border-color);
94
120
  background-color: var(--pressed-button-background-color);
95
121
  }
96
122
 
97
- // The trigger lives in the right-top slot, outside .im-c-move-control, so this is
123
+ // The trigger lives in the right-top slot, outside .im-c-move-controls, so this is
98
124
  // its own top-level rule. Same colour treatment as above, but keyed off
99
125
  // aria-expanded rather than aria-pressed — the trigger is correctly a disclosure
100
126
  // button (it reveals the control below), not a toggle button, so it doesn't carry
@@ -106,26 +132,31 @@
106
132
  // hovering the expanded trigger button was a coin-flip decided by cascade order —
107
133
  // one cssnano mergeRules reshuffle in production and hover started winning,
108
134
  // showing the hover tint instead of the pressed colours. The sibling
109
- // .im-c-move-control .im-c-map-button[aria-pressed="true"] rule above never had
135
+ // .im-c-move-controls .im-c-map-button[aria-pressed="true"] rule above never had
110
136
  // this problem (it's already (0,3,0), safely ahead of hover); this one needed the
111
137
  // same headroom.
112
- .im-c-map-button.im-c-map-button--move-control[aria-expanded="true"] {
138
+ .im-c-map-button.im-c-map-button--move-controls[aria-expanded="true"] {
113
139
  color: var(--pressed-button-foreground-color);
114
140
  border-color: var(--pressed-button-border-color);
115
141
  background-color: var(--pressed-button-background-color);
116
142
  }
117
143
 
118
- .im-c-move-control__zoom {
144
+ .im-c-move-controls__zoom {
119
145
  display: flex;
120
146
  flex-direction: row;
121
147
  }
122
148
 
123
149
  // A simple two-item linear row, so the standard button-group corner/shadow treatment
124
150
  // applies directly (matches .im-o-app__top's first/last-child pattern).
125
- .im-c-move-control__zoom .im-c-button-wrapper--nudge-zoom-in .im-c-map-button {
151
+ .im-c-move-controls__zoom .im-c-button-wrapper--nudge-zoom-in .im-c-map-button {
126
152
  @include tools.border-focus-corner-override($corners: 'left');
127
153
  }
128
154
 
129
- .im-c-move-control__zoom .im-c-button-wrapper--nudge-zoom-out .im-c-map-button {
155
+ .im-c-move-controls__zoom .im-c-button-wrapper--nudge-zoom-out .im-c-map-button {
130
156
  @include tools.border-focus-corner-override($corners: 'right');
131
157
  }
158
+
159
+ // App dark modifiers
160
+ .im-o-app--dark-app .im-c-move-controls__directions .im-c-button-wrapper--nudge-step-toggle .im-c-map-button {
161
+ outline-offset: -1px;
162
+ }