@defra/interactive-map 0.0.40-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 (351) 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/assets/images/slot-map.svg +61 -262
  9. package/docs/examples/draw-tools.mdx +3 -3
  10. package/docs/plugins/draw.md +79 -16
  11. package/docs/plugins/search.md +1 -1
  12. package/govuk-prototype-kit.config.json +2 -0
  13. package/jest.setup.js +5 -0
  14. package/package.json +9 -1
  15. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  16. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  17. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  18. package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
  19. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  20. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  21. package/plugins/datasets/dist/css/index.css +1 -1
  22. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  23. package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
  24. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  25. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  26. package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
  27. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  28. package/plugins/datasets/dist/umd/index.js +1 -1
  29. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
  30. package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
  31. package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
  32. package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +14 -0
  33. package/plugins/datasets/src/api/setOpacity.test.js +9 -0
  34. package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
  35. package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
  36. package/plugins/datasets/src/datasets.scss +0 -1
  37. package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
  38. package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
  39. package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
  40. package/plugins/datasets/src/index.test.js +36 -0
  41. package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
  42. package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
  43. package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -4
  44. package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
  45. package/plugins/datasets/src/manifest.js +0 -38
  46. package/plugins/datasets/src/manifest.test.js +49 -0
  47. package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
  48. package/plugins/datasets/src/reducers/pluginState.js +3 -16
  49. package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
  50. package/plugins/datasets/src/registry/dataset.js +13 -1
  51. package/plugins/datasets/src/registry/dataset.test.js +61 -0
  52. package/plugins/datasets/src/registry/datasetRegistry.js +12 -6
  53. package/plugins/datasets/src/registry/datasetRegistry.test.js +16 -4
  54. package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
  55. package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
  56. package/plugins/datasets/src/utils/bbox.test.js +125 -0
  57. package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
  58. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  59. package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
  60. package/plugins/draw/dist/esm/index.js +1 -1
  61. package/plugins/draw/dist/umd/im-draw-ml-adapter.js +2 -1
  62. package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
  63. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  64. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  65. package/plugins/draw/dist/umd/index.js +1 -1
  66. package/plugins/draw/src/DrawInit.jsx +10 -2
  67. package/plugins/draw/src/DrawInit.test.jsx +51 -10
  68. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +91 -11
  69. package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
  70. package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
  71. package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
  72. package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
  73. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
  74. package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
  75. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
  76. package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
  77. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
  78. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
  79. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
  80. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
  81. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
  82. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
  83. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
  84. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
  85. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
  86. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
  87. package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
  88. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
  89. package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
  90. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +4 -27
  91. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
  92. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
  93. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
  94. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
  95. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
  96. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
  97. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +3 -53
  98. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
  99. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
  100. package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +10 -3
  101. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
  102. package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
  103. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
  104. package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
  105. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
  106. package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
  107. package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
  108. package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
  109. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
  110. package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
  111. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
  112. package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
  113. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
  114. package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
  115. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
  116. package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
  117. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
  118. package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
  119. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
  120. package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
  121. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
  122. package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
  123. package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
  124. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
  125. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
  126. package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
  127. package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
  128. package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +12 -0
  129. package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +8 -2
  130. package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +31 -0
  131. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
  132. package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +33 -0
  133. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
  134. package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
  135. package/plugins/draw/src/adapters/openlayers/edit/nudge.js +10 -3
  136. package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +24 -1
  137. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
  138. package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
  139. package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
  140. package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
  141. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
  142. package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
  143. package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
  144. package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
  145. package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
  146. package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
  147. package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
  148. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
  149. package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
  150. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
  151. package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
  152. package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
  153. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
  154. package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
  155. package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
  156. package/plugins/draw/src/api/addFeature.js +16 -1
  157. package/plugins/draw/src/api/addFeature.test.js +62 -5
  158. package/plugins/draw/src/api/editFeature.js +7 -4
  159. package/plugins/draw/src/api/editFeature.test.js +16 -0
  160. package/plugins/draw/src/api/newPoint.js +42 -0
  161. package/plugins/draw/src/api/newPoint.test.js +117 -0
  162. package/plugins/draw/src/api/setStyle.js +43 -0
  163. package/plugins/draw/src/api/setStyle.test.js +75 -0
  164. package/plugins/draw/src/events.js +17 -10
  165. package/plugins/draw/src/events.test.js +14 -0
  166. package/plugins/draw/src/manifest.js +18 -6
  167. package/plugins/draw/src/manifest.test.js +32 -1
  168. package/plugins/draw/src/reducer.test.js +7 -0
  169. package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
  170. package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
  171. package/plugins/draw/src/utils/symbolKeys.js +5 -0
  172. package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
  173. package/plugins/draw/src/validation/liveStroke.test.js +64 -1
  174. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  175. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  176. package/plugins/interact/dist/umd/index.js +1 -1
  177. package/plugins/interact/src/hooks/useHighlightSync.js +33 -12
  178. package/plugins/interact/src/hooks/useHighlightSync.test.js +96 -25
  179. package/plugins/interact/src/utils/featureQueries.js +18 -0
  180. package/plugins/interact/src/utils/featureQueries.test.js +15 -0
  181. package/plugins/interact/src/utils/spatial.js +9 -3
  182. package/plugins/interact/src/utils/spatial.test.js +12 -1
  183. package/plugins/map-key/dist/css/index.css +62 -0
  184. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
  185. package/plugins/map-key/dist/esm/index.js +1 -0
  186. package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
  187. package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
  188. package/plugins/map-key/dist/umd/index.js +2 -0
  189. package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
  190. package/plugins/map-key/package.json +28 -0
  191. package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
  192. package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
  193. package/plugins/map-key/src/components/Key/Key.jsx +41 -0
  194. package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +15 -14
  195. package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
  196. package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
  197. package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
  198. package/plugins/map-key/src/components/Key/KeyItem.jsx +29 -0
  199. package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
  200. package/plugins/map-key/src/components/Key/KeySvg.jsx +42 -0
  201. package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
  202. package/plugins/map-key/src/components/Key/KeySvgLine.jsx +28 -0
  203. package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
  204. package/plugins/map-key/src/components/Key/KeySvgPattern.jsx +23 -0
  205. package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +2 -2
  206. package/plugins/map-key/src/components/Key/KeySvgRect.jsx +33 -0
  207. package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
  208. package/plugins/map-key/src/components/Key/KeySvgSymbol.jsx +27 -0
  209. package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +5 -6
  210. package/plugins/map-key/src/components/Key/MapKey.jsx +50 -0
  211. package/plugins/map-key/src/components/Key/MapKey.test.jsx +130 -0
  212. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
  213. package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
  214. package/plugins/map-key/src/index.js +13 -0
  215. package/plugins/map-key/src/index.test.js +31 -0
  216. package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
  217. package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
  218. package/plugins/map-key/src/manifest.js +30 -0
  219. package/plugins/map-key/src/manifest.test.js +63 -0
  220. package/plugins/map-key/src/mapKey.scss +4 -0
  221. package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
  222. package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
  223. package/plugins/map-key/src/registry/index.js +3 -0
  224. package/plugins/map-key/src/registry/index.test.js +15 -0
  225. package/plugins/menu/dist/css/index.css +80 -0
  226. package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
  227. package/plugins/menu/dist/esm/index.js +1 -0
  228. package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
  229. package/plugins/menu/dist/umd/index.js +2 -0
  230. package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
  231. package/plugins/menu/package.json +28 -0
  232. package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
  233. package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
  234. package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
  235. package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
  236. package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
  237. package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
  238. package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
  239. package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
  240. package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
  241. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
  242. package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
  243. package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
  244. package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
  245. package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
  246. package/plugins/menu/src/index.js +13 -0
  247. package/plugins/menu/src/index.test.js +31 -0
  248. package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
  249. package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
  250. package/plugins/menu/src/initialise/createMenu.js +10 -0
  251. package/plugins/menu/src/initialise/createMenu.test.js +37 -0
  252. package/plugins/menu/src/manifest.js +35 -0
  253. package/plugins/menu/src/manifest.test.js +63 -0
  254. package/plugins/menu/src/menu.scss +4 -0
  255. package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
  256. package/plugins/menu/src/reducers/pluginState.js +35 -0
  257. package/plugins/menu/src/reducers/pluginState.test.js +57 -0
  258. package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
  259. package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
  260. package/plugins/menu/src/registry/index.js +3 -0
  261. package/plugins/menu/src/registry/index.test.js +15 -0
  262. package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
  263. package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
  264. package/plugins/search/dist/css/index.css +1 -1
  265. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  266. package/plugins/search/dist/esm/index.js +1 -1
  267. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  268. package/plugins/search/dist/umd/index.js +1 -1
  269. package/plugins/search/src/Search.jsx +2 -3
  270. package/plugins/search/src/components/Form/Form.module.scss +9 -20
  271. package/plugins/search/src/index.js +0 -1
  272. package/plugins/search/src/index.test.js +2 -4
  273. package/plugins/search/src/manifest.js +1 -1
  274. package/plugins/search/src/search.scss +17 -16
  275. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  276. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  277. package/providers/beta/openlayers/dist/umd/index.js +1 -1
  278. package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
  279. package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
  280. package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
  281. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
  282. package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
  283. package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
  284. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  285. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  286. package/providers/maplibre/dist/umd/index.js +1 -1
  287. package/providers/maplibre/src/utils/drawLayerBuckets.js +14 -0
  288. package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
  289. package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
  290. package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
  291. package/providers/maplibre/src/utils/hoverCursor.js +30 -14
  292. package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
  293. package/providers/maplibre/src/utils/patternImages.js +1 -1
  294. package/providers/maplibre/src/utils/symbolImages.js +41 -3
  295. package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
  296. package/rollup.esm.mjs +11 -1
  297. package/src/App/components/Actions/Actions.test.jsx +47 -0
  298. package/src/App/components/Hints/Hints.test.jsx +13 -0
  299. package/src/App/components/MoveControls/MoveControls.module.scss +41 -10
  300. package/src/App/components/Panel/Panel.jsx +18 -3
  301. package/src/App/components/Panel/Panel.module.scss +18 -8
  302. package/src/App/components/Panel/Panel.test.jsx +34 -0
  303. package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
  304. package/src/App/hooks/useFeatureItems.js +16 -7
  305. package/src/App/hooks/useFeatureItems.test.js +57 -0
  306. package/src/App/hooks/useLayoutMeasurements.js +95 -84
  307. package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
  308. package/src/App/layout/Layout.jsx +9 -10
  309. package/src/App/layout/Layout.test.jsx +2 -0
  310. package/src/App/layout/layout.module.scss +31 -17
  311. package/src/App/registry/pluginRegistry.js +1 -1
  312. package/src/App/registry/pluginRegistry.test.js +12 -0
  313. package/src/App/renderer/HtmlElementHost.jsx +18 -5
  314. package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
  315. package/src/App/renderer/mapButtons.js +28 -12
  316. package/src/App/renderer/mapButtons.test.js +17 -4
  317. package/src/App/renderer/mapControls.js +3 -1
  318. package/src/App/renderer/mapControls.test.js +16 -0
  319. package/src/App/renderer/mapPanels.js +44 -9
  320. package/src/App/renderer/mapPanels.test.js +51 -2
  321. package/src/App/renderer/orderItems.js +28 -0
  322. package/src/App/renderer/orderItems.test.js +50 -0
  323. package/src/App/renderer/slotAggregator.js +2 -18
  324. package/src/App/renderer/slotHelpers.js +3 -1
  325. package/src/App/renderer/slotHelpers.test.js +5 -0
  326. package/src/App/renderer/slots.js +3 -0
  327. package/src/App/store/AppProvider.jsx +1 -0
  328. package/src/App/store/appDispatchMiddleware.js +1 -1
  329. package/src/App/store/appDispatchMiddleware.test.js +8 -0
  330. package/src/config/events.js +2 -0
  331. package/src/scss/settings/_colors.scss +1 -0
  332. package/src/scss/settings/_dimensions.scss +2 -3
  333. package/src/scss/settings/_transition.scss +1 -1
  334. package/src/services/eventBus.js +44 -0
  335. package/src/services/eventBus.test.js +83 -0
  336. package/src/services/symbolRegistry.js +1 -1
  337. package/src/types.js +11 -1
  338. package/webpack.umd.mjs +2 -0
  339. package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
  340. package/plugins/datasets/src/components/Key/Key.jsx +0 -50
  341. package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
  342. package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
  343. package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
  344. package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
  345. package/plugins/datasets/src/components/Key/KeySvgPattern.jsx +0 -13
  346. package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
  347. package/plugins/datasets/src/components/Key/KeySvgSymbol.jsx +0 -16
  348. package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
  349. /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
  350. /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
  351. /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
 
@@ -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
  })
@@ -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 />)
@@ -46,6 +46,7 @@
46
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
52
  .im-c-move-controls__directions .im-c-map-button--pan-up svg {
@@ -66,24 +67,49 @@
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-controls__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-controls__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-controls__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-controls__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-controls__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
@@ -129,3 +155,8 @@
129
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
+ }
@@ -56,9 +56,19 @@ const buildBodyProps = ({ bodyRef, panelBodyClass, isBodyScrollable, elementId }
56
56
  'aria-labelledby': isBodyScrollable ? `${elementId}-label` : undefined
57
57
  })
58
58
 
59
+ // Renders the panel body's content: an ordered list of items (own content plus any
60
+ // controls injected via the `<panelId>-panel` slot convention), or the legacy single
61
+ // WrappedChild/children shape for callers that don't build an items list (e.g. HtmlElementHost).
62
+ const BodyContent = ({ items, WrappedChild, props, children }) => { // NOSONAR
63
+ if (items) {
64
+ return items.map(item => <React.Fragment key={item.id}>{item.element}</React.Fragment>)
65
+ }
66
+ return WrappedChild ? <WrappedChild {...props} /> : children
67
+ }
68
+
59
69
  // eslint-disable-next-line camelcase, react/jsx-pascal-case
60
70
  // sonarjs/disable-next-line function-name
61
- export const Panel = ({ panelId, panelConfig, props, focusOnOpen, WrappedChild, label, html, children, isOpen = true, rootRef }) => {
71
+ export const Panel = ({ panelId, panelConfig, props, focusOnOpen, WrappedChild, items, label, html, children, isOpen = true, rootRef }) => {
62
72
  const { id } = useConfig()
63
73
  const { dispatch, breakpoint, layoutRefs, interfaceType } = useApp()
64
74
 
@@ -103,6 +113,10 @@ export const Panel = ({ panelId, panelConfig, props, focusOnOpen, WrappedChild,
103
113
 
104
114
  const panelProps = buildPanelProps({ elementId, shouldFocus, isDialog, isDismissible, isModal, width: bpConfig.width, panelClass, slot: bpConfig.slot })
105
115
  const bodyProps = buildBodyProps({ bodyRef, panelBodyClass, isBodyScrollable, elementId })
116
+ // DOM anchor for controls DOM-projected via the JS/consumer-HTML API — see HtmlElementHost.jsx.
117
+ // Only present on the items-capable body below: dangerouslySetInnerHTML owns the static-html
118
+ // body's children, so it can't also host controls injected via the panel-slot convention.
119
+ const panelBodySlot = `${stringToKebab(panelId)}-panel`
106
120
 
107
121
  return (
108
122
  <div // nosonar
@@ -118,6 +132,7 @@ export const Panel = ({ panelId, panelConfig, props, focusOnOpen, WrappedChild,
118
132
 
119
133
  {isDismissible && (
120
134
  <button
135
+ type='button'
121
136
  aria-label={`Close ${label}`}
122
137
  className='im-c-panel__close'
123
138
  onClick={handleClose}
@@ -129,8 +144,8 @@ export const Panel = ({ panelId, panelConfig, props, focusOnOpen, WrappedChild,
129
144
  {innerHtmlProp
130
145
  ? <div {...bodyProps} dangerouslySetInnerHTML={innerHtmlProp} /> // nosonar
131
146
  : (
132
- <div {...bodyProps}> {/* nosonar */}
133
- {WrappedChild ? <WrappedChild {...props} /> : children}
147
+ <div {...bodyProps} data-panel-slot={panelBodySlot}> {/* nosonar */}
148
+ <BodyContent items={items} WrappedChild={WrappedChild} props={props}>{children}</BodyContent>
134
149
  </div>
135
150
  )}
136
151
  </div>
@@ -18,6 +18,7 @@
18
18
  min-height: 0;
19
19
  min-width: var(--min-panel-width);
20
20
  pointer-events: auto;
21
+ color: var(--foreground-color);
21
22
  background-color: var(--background-color);
22
23
  }
23
24
 
@@ -98,19 +99,28 @@
98
99
  );
99
100
  }
100
101
 
101
- // When banner heading is hidden we need to align with the close button
102
- .im-o-app--desktop .im-c-panel--banner.im-c-panel--no-heading {
103
- .im-c-panel__close {
104
- top: var(--banner-close-button-gap);
105
- right: var(--banner-close-button-gap);
102
+ // Banner gets small rounded corners instead of the default square panel look.
103
+ .im-c-panel--banner {
104
+ &,
105
+ &::before {
106
+ border-radius: var(--banner-border-radius);
106
107
  }
107
108
 
108
- .im-c-panel__body--offset {
109
- padding-top: 10px;
110
- padding-bottom: 10px;
109
+ &::after {
110
+ border-radius: calc(var(--banner-border-radius) - 2px);
111
111
  }
112
112
  }
113
113
 
114
+ // No heading means no built-in top spacing, so add it back here.
115
+ .im-c-panel--no-heading .im-c-panel__body {
116
+ padding-top: var(--panel-margin);
117
+ }
118
+
119
+ // Ensures the body is tall enough to contain the close button.
120
+ .im-c-panel--no-heading .im-c-panel__body--offset {
121
+ min-height: calc((var(--panel-close-button-gap) * 2) + var(--button-size));
122
+ }
123
+
114
124
  .im-c-panel__body--offset {
115
125
  padding-right: calc(var(--button-size) + var(--divider-gap));
116
126
  }
@@ -194,5 +194,39 @@ describe('Panel', () => {
194
194
  renderPanel({}, { html: '<p>HTML content</p>' })
195
195
  expect(screen.getByText('HTML content')).toBeInTheDocument()
196
196
  })
197
+
198
+ it('renders each items entry in list order', () => {
199
+ const items = [
200
+ { id: 'a', element: <p>First</p> },
201
+ { id: 'b', element: <p>Second</p> }
202
+ ]
203
+ renderPanel({}, { items })
204
+ expect(screen.getByText('First')).toBeInTheDocument()
205
+ expect(screen.getByText('Second')).toBeInTheDocument()
206
+ })
207
+
208
+ it('prefers items over WrappedChild/children when provided', () => {
209
+ const Wrapped = () => <p>Wrapped</p>
210
+ renderPanel({}, {
211
+ items: [{ id: 'a', element: <p>Item</p> }],
212
+ WrappedChild: Wrapped,
213
+ children: <p>Child content</p>
214
+ })
215
+ expect(screen.getByText('Item')).toBeInTheDocument()
216
+ expect(screen.queryByText('Wrapped')).not.toBeInTheDocument()
217
+ expect(screen.queryByText('Child content')).not.toBeInTheDocument()
218
+ })
219
+
220
+ it('stamps data-panel-slot on the items-capable body, for controls DOM-projected via the JS API', () => {
221
+ const { container } = renderPanel({}, { items: [{ id: 'a', element: <p>Item</p> }] })
222
+ const body = container.querySelector('.im-c-panel__body')
223
+ expect(body).toHaveAttribute('data-panel-slot', 'settings-panel')
224
+ })
225
+
226
+ it('does not stamp data-panel-slot on a static-html body', () => {
227
+ const { container } = renderPanel({}, { html: '<p>HTML content</p>' })
228
+ const body = container.querySelector('.im-c-panel__body')
229
+ expect(body).not.toHaveAttribute('data-panel-slot')
230
+ })
197
231
  })
198
232
  })
@@ -45,34 +45,32 @@ describe('Tooltip', () => {
45
45
 
46
46
  it('shows and hides tooltip on mouse interactions', () => {
47
47
  const { button, tooltip } = renderTooltip()
48
- act(() => fireEvent.mouseEnter(button))
48
+ fireEvent.mouseEnter(button)
49
49
  advanceTimers()
50
50
  expect(tooltip).toHaveClass('im-c-tooltip--is-visible')
51
51
  expect(getTooltipPosition).toHaveBeenCalled()
52
- act(() => fireEvent.mouseLeave(button))
52
+ fireEvent.mouseLeave(button)
53
53
  advanceTimers(0)
54
54
  expect(tooltip).not.toHaveClass('im-c-tooltip--is-visible')
55
55
  })
56
56
 
57
57
  it('cancels tooltip on mouse down or key down', () => {
58
58
  const { button, tooltip } = renderTooltip()
59
- act(() => {
60
- fireEvent.mouseEnter(button)
61
- fireEvent.mouseDown(button)
62
- fireEvent.mouseEnter(button)
63
- fireEvent.keyDown(button)
64
- })
59
+ fireEvent.mouseEnter(button)
60
+ fireEvent.mouseDown(button)
61
+ fireEvent.mouseEnter(button)
62
+ fireEvent.keyDown(button)
65
63
  advanceTimers()
66
64
  expect(tooltip).not.toHaveClass('im-c-tooltip--is-visible')
67
65
  })
68
66
 
69
67
  it('handles focus interactions for keyboard interface', () => {
70
68
  const { button, tooltip, wrapper } = renderTooltip({}, 'keyboard')
71
- act(() => fireEvent.focus(button))
69
+ fireEvent.focus(button)
72
70
  advanceTimers()
73
71
  expect(wrapper).toHaveClass('im-c-tooltip-wrapper--has-focus')
74
72
  expect(tooltip).toHaveClass('im-c-tooltip--is-visible')
75
- act(() => fireEvent.blur(button))
73
+ fireEvent.blur(button)
76
74
  advanceTimers(0)
77
75
  expect(wrapper).not.toHaveClass('im-c-tooltip-wrapper--has-focus')
78
76
  expect(tooltip).not.toHaveClass('im-c-tooltip--is-visible')
@@ -80,22 +78,22 @@ describe('Tooltip', () => {
80
78
 
81
79
  it('does not show tooltip on focus if interfaceType is mouse', () => {
82
80
  const { button, tooltip } = renderTooltip()
83
- act(() => fireEvent.focus(button))
81
+ fireEvent.focus(button)
84
82
  advanceTimers()
85
83
  expect(tooltip).not.toHaveClass('im-c-tooltip--is-visible')
86
84
  })
87
85
 
88
86
  it('hides tooltip on Escape key', () => {
89
87
  const { button, tooltip } = renderTooltip()
90
- act(() => fireEvent.mouseEnter(button))
88
+ fireEvent.mouseEnter(button)
91
89
  advanceTimers()
92
- act(() => fireEvent.keyDown(window, { key: 'Escape' }))
90
+ fireEvent.keyDown(window, { key: 'Escape' })
93
91
  expect(tooltip).not.toHaveClass('im-c-tooltip--is-visible')
94
92
  })
95
93
 
96
94
  it('removes keydown listener on unmount', () => {
97
95
  const { button, unmount } = renderTooltip({}, 'keyboard')
98
- act(() => fireEvent.mouseEnter(button))
96
+ fireEvent.mouseEnter(button)
99
97
  advanceTimers()
100
98
  const removeSpy = jest.spyOn(window, 'removeEventListener')
101
99
  unmount()
@@ -1,9 +1,12 @@
1
1
  import { useState, useEffect } from 'react'
2
2
 
3
+ const SET_FEATURES = 'map:setfeatures'
4
+ const SET_FEATURES_SUPPRESSED = 'map:setfeaturessuppressed'
5
+
3
6
  /**
4
- * Subscribes to map:setfeatures and exposes the current listbox item list and multiselectable flag.
5
- * The list is produced by useMapItemList in the interact plugin and pushed via the event bus
6
- * whenever the visible set of markers or features changes.
7
+ * Subscribes to map:setfeatures for the listbox item list, and map:setfeaturessuppressed
8
+ * (any plugin can emit this) to report items as empty while suppressed regardless of what's
9
+ * actually visible which is what drives <Features>'s own tabIndex/aria-hidden.
7
10
  *
8
11
  * @param {object} eventBus
9
12
  * @returns {{ items: Array<{ id: string, label: string }>, multiselectable: boolean }}
@@ -11,20 +14,26 @@ import { useState, useEffect } from 'react'
11
14
  export function useFeatureItems (eventBus) {
12
15
  const [items, setItems] = useState([])
13
16
  const [multiselectable, setMultiselectable] = useState(false)
17
+ const [suppressed, setSuppressed] = useState(false)
14
18
 
15
19
  useEffect(() => {
16
20
  if (!eventBus) {
17
21
  return undefined
18
22
  }
19
- const handle = ({ items: next = [], multiselectable: nextMultiselectable = false }) => {
23
+ const handleSetFeatures = ({ items: next = [], multiselectable: nextMultiselectable = false }) => {
20
24
  setItems(next)
21
25
  setMultiselectable(nextMultiselectable)
22
26
  }
23
- eventBus.on('map:setfeatures', handle)
27
+ const handleSuppressed = ({ suppressed: next = false } = {}) => {
28
+ setSuppressed(next)
29
+ }
30
+ eventBus.on(SET_FEATURES, handleSetFeatures)
31
+ eventBus.on(SET_FEATURES_SUPPRESSED, handleSuppressed)
24
32
  return () => {
25
- eventBus.off('map:setfeatures', handle)
33
+ eventBus.off(SET_FEATURES, handleSetFeatures)
34
+ eventBus.off(SET_FEATURES_SUPPRESSED, handleSuppressed)
26
35
  }
27
36
  }, [eventBus])
28
37
 
29
- return { items, multiselectable }
38
+ return { items: suppressed ? [] : items, multiselectable }
30
39
  }
@@ -2,6 +2,7 @@ import { renderHook, act } from '@testing-library/react'
2
2
  import { useFeatureItems } from './useFeatureItems.js'
3
3
 
4
4
  const SET_FEATURES = 'map:setfeatures' // NOSONAR
5
+ const SET_FEATURES_SUPPRESSED = 'map:setfeaturessuppressed' // NOSONAR
5
6
 
6
7
  const makeEventBus = () => {
7
8
  const listeners = {}
@@ -37,11 +38,18 @@ describe('useFeatureItems — event subscription', () => {
37
38
  expect(eb.on).toHaveBeenCalledWith(SET_FEATURES, expect.any(Function))
38
39
  })
39
40
 
41
+ it('subscribes to map:setfeaturessuppressed on mount', () => {
42
+ const eb = makeEventBus()
43
+ renderHook(() => useFeatureItems(eb))
44
+ expect(eb.on).toHaveBeenCalledWith(SET_FEATURES_SUPPRESSED, expect.any(Function))
45
+ })
46
+
40
47
  it('unsubscribes on unmount', () => {
41
48
  const eb = makeEventBus()
42
49
  const { unmount } = renderHook(() => useFeatureItems(eb))
43
50
  unmount()
44
51
  expect(eb.off).toHaveBeenCalledWith(SET_FEATURES, expect.any(Function))
52
+ expect(eb.off).toHaveBeenCalledWith(SET_FEATURES_SUPPRESSED, expect.any(Function))
45
53
  })
46
54
 
47
55
  it('does not subscribe when eventBus is undefined', () => {
@@ -96,3 +104,52 @@ describe('useFeatureItems — multiselectable updates', () => {
96
104
  expect(result.current.multiselectable).toBe(false)
97
105
  })
98
106
  })
107
+
108
+ // ─── useFeatureItems — suppression ───────────────────────────────────────────
109
+
110
+ describe('useFeatureItems — suppression', () => {
111
+ const items = [{ id: 'a', label: 'Feature A' }]
112
+
113
+ it('reports empty items while suppressed, regardless of what was last set', () => {
114
+ const eb = makeEventBus()
115
+ const { result } = renderHook(() => useFeatureItems(eb))
116
+ act(() => eb.emit(SET_FEATURES, { items }))
117
+ act(() => eb.emit(SET_FEATURES_SUPPRESSED, { suppressed: true }))
118
+ expect(result.current.items).toEqual([])
119
+ })
120
+
121
+ it('restores the last known items once suppression is lifted', () => {
122
+ const eb = makeEventBus()
123
+ const { result } = renderHook(() => useFeatureItems(eb))
124
+ act(() => eb.emit(SET_FEATURES, { items }))
125
+ act(() => eb.emit(SET_FEATURES_SUPPRESSED, { suppressed: true }))
126
+ act(() => eb.emit(SET_FEATURES_SUPPRESSED, { suppressed: false }))
127
+ expect(result.current.items).toEqual(items)
128
+ })
129
+
130
+ it('defaults suppressed to false when the payload omits it', () => {
131
+ const eb = makeEventBus()
132
+ const { result } = renderHook(() => useFeatureItems(eb))
133
+ act(() => eb.emit(SET_FEATURES, { items }))
134
+ act(() => eb.emit(SET_FEATURES_SUPPRESSED, {}))
135
+ expect(result.current.items).toEqual(items)
136
+ })
137
+
138
+ it('defaults suppressed to false when the event carries no payload at all', () => {
139
+ const eb = makeEventBus()
140
+ const { result } = renderHook(() => useFeatureItems(eb))
141
+ act(() => eb.emit(SET_FEATURES, { items }))
142
+ act(() => eb.emit(SET_FEATURES_SUPPRESSED))
143
+ expect(result.current.items).toEqual(items)
144
+ })
145
+
146
+ it('a features update received while suppressed is still applied once restored', () => {
147
+ const eb = makeEventBus()
148
+ const { result } = renderHook(() => useFeatureItems(eb))
149
+ act(() => eb.emit(SET_FEATURES_SUPPRESSED, { suppressed: true }))
150
+ act(() => eb.emit(SET_FEATURES, { items }))
151
+ expect(result.current.items).toEqual([]) // still suppressed
152
+ act(() => eb.emit(SET_FEATURES_SUPPRESSED, { suppressed: false }))
153
+ expect(result.current.items).toEqual(items)
154
+ })
155
+ })