@defra/interactive-map 0.0.3-alpha → 0.0.5-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 (407) hide show
  1. package/README.md +16 -57
  2. package/dist/css/index.css +1 -1
  3. package/dist/esm/im-core.js +1 -1
  4. package/dist/esm/index.js +1 -1
  5. package/dist/umd/im-core.js +1 -1
  6. package/dist/umd/index.js +1 -1
  7. package/docs/api/button-definition.md +176 -0
  8. package/docs/api/context.md +124 -0
  9. package/docs/api/control-definition.md +72 -0
  10. package/docs/api/icon-definition.md +28 -0
  11. package/docs/api/map-style-config.md +76 -0
  12. package/docs/api/marker-config.md +51 -0
  13. package/docs/api/panel-definition.md +117 -0
  14. package/docs/api/slots.md +25 -0
  15. package/docs/api.md +721 -0
  16. package/docs/architecture.md +139 -0
  17. package/docs/building-a-plugin.md +149 -0
  18. package/docs/getting-started.md +46 -0
  19. package/docs/govuk-prototype.md +0 -0
  20. package/docs/plugins/interact.md +205 -0
  21. package/docs/plugins/map-styles.md +84 -0
  22. package/docs/plugins/plugin-context.md +67 -0
  23. package/docs/plugins/plugin-descriptor.md +71 -0
  24. package/docs/plugins/plugin-manifest.md +118 -0
  25. package/docs/plugins/scale-bar.md +62 -0
  26. package/docs/plugins/search.md +162 -0
  27. package/docs/plugins.md +116 -0
  28. package/govuk-prototype-kit.config.json +7 -7
  29. package/package.json +16 -2
  30. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +2 -0
  31. package/plugins/beta/datasets/dist/esm/index.js +2 -0
  32. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +2 -0
  33. package/plugins/beta/datasets/dist/umd/index.js +2 -0
  34. package/plugins/beta/datasets/src/datasets.js +113 -0
  35. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +206 -0
  36. package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +38 -0
  37. package/plugins/{datasets → beta/datasets}/src/handleSetMapStyle.js +8 -1
  38. package/plugins/{datasets → beta/datasets}/src/manifest.js +1 -1
  39. package/plugins/{datasets → beta/datasets}/src/mapLayers.js +42 -4
  40. package/plugins/{datasets → beta/datasets}/src/panels/Key.jsx +1 -1
  41. package/plugins/beta/datasets/src/utils/bbox.js +115 -0
  42. package/plugins/beta/draw-ml/dist/css/index.css +1 -0
  43. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -0
  44. package/plugins/beta/draw-ml/dist/esm/index.js +2 -0
  45. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -0
  46. package/plugins/beta/draw-ml/dist/umd/index.js +2 -0
  47. package/plugins/beta/draw-ml/src/api/addFeature.js +27 -0
  48. package/plugins/{draw-ml → beta/draw-ml}/src/api/editFeature.js +17 -6
  49. package/plugins/beta/draw-ml/src/api/merge.js +9 -0
  50. package/plugins/beta/draw-ml/src/api/newLine.js +68 -0
  51. package/plugins/beta/draw-ml/src/api/newPolygon.js +68 -0
  52. package/plugins/beta/draw-ml/src/api/split.js +81 -0
  53. package/plugins/{draw-ml → beta/draw-ml}/src/defaults.js +3 -0
  54. package/plugins/beta/draw-ml/src/draw.scss +32 -0
  55. package/plugins/beta/draw-ml/src/events.js +171 -0
  56. package/plugins/beta/draw-ml/src/manifest.js +132 -0
  57. package/plugins/{draw-ml → beta/draw-ml}/src/mapboxDraw.js +51 -2
  58. package/plugins/{draw-ml → beta/draw-ml}/src/mapboxSnap.js +18 -12
  59. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +497 -0
  60. package/plugins/beta/draw-ml/src/modes/drawLineMode.js +18 -0
  61. package/plugins/beta/draw-ml/src/modes/drawPolygonMode.js +16 -0
  62. package/plugins/beta/draw-ml/src/modes/editVertex/geometryHelpers.js +135 -0
  63. package/plugins/beta/draw-ml/src/modes/editVertex/helpers.js +2 -0
  64. package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +134 -0
  65. package/plugins/beta/draw-ml/src/modes/editVertex/undoHandlers.js +133 -0
  66. package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +141 -0
  67. package/plugins/beta/draw-ml/src/modes/editVertex/vertexQueries.js +121 -0
  68. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +437 -0
  69. package/plugins/{draw-ml → beta/draw-ml}/src/reducer.js +30 -2
  70. package/plugins/{draw-ml → beta/draw-ml}/src/styles.js +66 -36
  71. package/plugins/beta/draw-ml/src/undoStack.js +50 -0
  72. package/plugins/beta/draw-ml/src/utils/debounce.js +16 -0
  73. package/plugins/beta/draw-ml/src/utils/flattenStyleProperties.js +49 -0
  74. package/plugins/beta/draw-ml/src/utils/spatial.js +258 -0
  75. package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -0
  76. package/plugins/beta/frame/dist/esm/index.js +2 -0
  77. package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -0
  78. package/plugins/beta/frame/dist/umd/index.js +2 -0
  79. package/plugins/{frame → beta/frame}/src/Frame.jsx +1 -1
  80. package/plugins/{map-styles → beta/map-styles}/dist/css/index.css +1 -1
  81. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -0
  82. package/plugins/beta/map-styles/dist/esm/index.js +2 -0
  83. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -0
  84. package/plugins/beta/map-styles/dist/umd/index.js +2 -0
  85. package/plugins/{map-styles → beta/map-styles}/src/MapStyles.jsx +1 -1
  86. package/plugins/{map-styles → beta/map-styles}/src/manifest.js +8 -10
  87. package/plugins/{map-styles → beta/map-styles}/src/mapStyles.scss +4 -1
  88. package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -0
  89. package/plugins/beta/scale-bar/dist/esm/index.js +2 -0
  90. package/plugins/beta/scale-bar/dist/umd/im-scale-bar-plugin.js +1 -0
  91. package/plugins/beta/scale-bar/dist/umd/index.js +2 -0
  92. package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -0
  93. package/plugins/beta/use-location/dist/esm/index.js +2 -0
  94. package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -0
  95. package/plugins/beta/use-location/dist/umd/index.js +2 -0
  96. package/plugins/interact/dist/css/index.css +1 -1
  97. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  98. package/plugins/interact/dist/esm/index.js +1 -1
  99. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  100. package/plugins/interact/dist/umd/index.js +1 -1
  101. package/plugins/interact/src/InteractInit.jsx +1 -1
  102. package/plugins/interact/src/InteractInit.test.js +97 -0
  103. package/plugins/interact/src/api/clear.test.js +18 -0
  104. package/plugins/interact/src/api/disable.test.js +12 -0
  105. package/plugins/interact/src/api/enable.test.js +48 -0
  106. package/plugins/interact/src/api/selectFeature.test.js +33 -0
  107. package/plugins/interact/src/api/unselectFeature.test.js +33 -0
  108. package/plugins/interact/src/defaults.js +2 -0
  109. package/plugins/interact/src/events.js +37 -52
  110. package/plugins/interact/src/events.test.js +186 -0
  111. package/plugins/interact/src/hooks/useHighlightSync.js +1 -1
  112. package/plugins/interact/src/hooks/useHighlightSync.test.js +212 -0
  113. package/plugins/interact/src/hooks/useInteractionHandlers.js +80 -41
  114. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +316 -0
  115. package/plugins/interact/src/index.test.js +22 -0
  116. package/plugins/interact/src/interact.scss +6 -0
  117. package/plugins/interact/src/manifest.js +1 -1
  118. package/plugins/interact/src/manifest.test.js +94 -0
  119. package/plugins/interact/src/reducer.js +28 -21
  120. package/plugins/interact/src/reducer.test.js +155 -0
  121. package/plugins/interact/src/utils/buildStylesMap.test.js +57 -0
  122. package/plugins/interact/src/utils/featureQueries.js +2 -2
  123. package/plugins/interact/src/utils/featureQueries.test.js +56 -0
  124. package/plugins/interact/src/utils/spatial.js +95 -0
  125. package/plugins/interact/src/utils/spatial.test.js +93 -0
  126. package/plugins/search/dist/css/index.css +1 -1
  127. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  128. package/plugins/search/dist/esm/index.js +1 -1
  129. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  130. package/plugins/search/dist/umd/index.js +1 -1
  131. package/plugins/search/src/Search.jsx +2 -2
  132. package/plugins/search/src/components/Form/Form.jsx +1 -1
  133. package/plugins/search/src/components/Suggestions/Suggestions.jsx +5 -5
  134. package/plugins/search/src/datasets.js +1 -1
  135. package/plugins/search/src/defaults.js +3 -0
  136. package/plugins/search/src/events/fetchSuggestions.js +48 -53
  137. package/plugins/search/src/events/formHandlers.js +3 -2
  138. package/plugins/search/src/events/index.js +1 -1
  139. package/plugins/search/src/events/inputHandlers.js +3 -1
  140. package/plugins/search/src/events/suggestionHandlers.js +12 -3
  141. package/plugins/search/src/search.scss +4 -1
  142. package/plugins/search/src/utils/parseOsNamesResults.js +15 -19
  143. package/providers/{esri → beta/esri}/src/esriProvider.js +1 -1
  144. package/providers/{esri → beta/esri}/src/index.js +1 -1
  145. package/providers/{esri → beta/esri}/src/mapEvents.js +2 -2
  146. package/providers/beta/open-names/dist/esm/im-reverse-geocode.js +2 -0
  147. package/providers/beta/open-names/dist/esm/index.js +2 -0
  148. package/providers/beta/open-names/dist/umd/im-reverse-geocode.js +2 -0
  149. package/providers/beta/open-names/dist/umd/im-reverse-geocode.js.LICENSE.txt +1 -0
  150. package/providers/beta/open-names/dist/umd/index.js +2 -0
  151. package/providers/beta/open-names/dist/umd/index.js.LICENSE.txt +1 -0
  152. package/providers/maplibre/dist/esm/im-maplibre-framework.js +1 -1
  153. package/providers/maplibre/dist/esm/im-maplibre-framework.js.LICENSE.txt +1 -1
  154. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  155. package/providers/maplibre/dist/esm/index.js +1 -1
  156. package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
  157. package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +1 -1
  158. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  159. package/providers/maplibre/dist/umd/index.js +1 -1
  160. package/providers/maplibre/src/defaults.js +3 -2
  161. package/providers/maplibre/src/index.js +36 -20
  162. package/providers/maplibre/src/maplibreProvider.js +140 -11
  163. package/providers/maplibre/src/utils/detectWebgl.js +1 -2
  164. package/providers/maplibre/src/utils/highlightFeatures.js +78 -67
  165. package/providers/maplibre/src/utils/labels.js +174 -120
  166. package/providers/maplibre/src/utils/maplibreFixes.js +2 -2
  167. package/providers/maplibre/src/utils/queryFeatures.js +146 -0
  168. package/providers/maplibre/src/utils/spatial.js +14 -17
  169. package/sonar-project.properties +40 -3
  170. package/src/App/components/Actions/Actions.jsx +2 -2
  171. package/src/App/components/Actions/Actions.module.scss +2 -2
  172. package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +3 -1
  173. package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +9 -0
  174. package/src/App/components/Logo/Logo.jsx +1 -1
  175. package/src/App/components/MapButton/MapButton.jsx +217 -41
  176. package/src/App/components/MapButton/MapButton.module.scss +32 -21
  177. package/src/App/components/MapButton/MapButton.test.jsx +128 -84
  178. package/src/App/components/Markers/Markers.jsx +1 -1
  179. package/src/App/components/Markers/Markers.module.scss +0 -5
  180. package/src/App/components/Panel/Panel.jsx +75 -45
  181. package/src/App/components/Panel/Panel.module.scss +18 -24
  182. package/src/App/components/Panel/Panel.test.jsx +18 -0
  183. package/src/App/components/PopupMenu/PopupMenu.jsx +246 -0
  184. package/src/App/components/PopupMenu/PopupMenu.module.scss +70 -0
  185. package/src/App/components/PopupMenu/PopupMenu.test.jsx +304 -0
  186. package/src/App/components/Tooltip/Tooltip.jsx +4 -2
  187. package/src/App/components/Tooltip/getTooltipPosition.js +1 -1
  188. package/src/App/components/Viewport/MapController.jsx +1 -1
  189. package/src/App/components/Viewport/MapStatus.jsx +1 -1
  190. package/src/App/components/Viewport/Viewport.jsx +2 -2
  191. package/src/App/components/Viewport/Viewport.module.scss +2 -2
  192. package/src/App/controls/keyboardActions.js +3 -2
  193. package/src/App/controls/keyboardShortcuts.js +4 -2
  194. package/src/App/hooks/useButtonStateEvaluator.js +58 -45
  195. package/src/App/hooks/useButtonStateEvaluator.test.js +36 -3
  196. package/src/App/hooks/useCrossHairAPI.js +49 -47
  197. package/src/App/hooks/useFocusVisible.js +2 -2
  198. package/src/App/hooks/useInterfaceAPI.js +36 -1
  199. package/src/App/hooks/useKeyboardHint.js +1 -1
  200. package/src/App/hooks/useKeyboardShortcuts.js +4 -2
  201. package/src/App/hooks/useMapAnnouncements.js +34 -32
  202. package/src/App/hooks/useMapEvents.js +12 -1
  203. package/src/App/hooks/useMapProviderOverrides.js +3 -3
  204. package/src/App/hooks/useMapStateSync.js +7 -1
  205. package/src/App/hooks/useMapURLSync.js +6 -1
  206. package/src/App/hooks/useMarkersAPI.js +88 -50
  207. package/src/App/hooks/useMediaQueryDispatch.test.js +48 -0
  208. package/src/App/hooks/useModalPanelBehaviour.js +46 -40
  209. package/src/App/hooks/useResizeObserver.js +2 -2
  210. package/src/App/initialiseApp.js +45 -38
  211. package/src/App/layout/Layout.jsx +6 -4
  212. package/src/App/layout/layout.module.scss +13 -17
  213. package/src/App/registry/keyboardShortcutRegistry.js +12 -2
  214. package/src/App/registry/keyboardShortcutRegistry.test.js +28 -0
  215. package/src/App/registry/mergeManifests.js +1 -1
  216. package/src/App/registry/panelRegistry.js +1 -1
  217. package/src/App/registry/panelRegistry.test.js +30 -1
  218. package/src/App/registry/pluginRegistry.js +14 -51
  219. package/src/App/registry/pluginRegistry.test.js +54 -6
  220. package/src/App/renderer/HtmlElementHost.jsx +186 -0
  221. package/src/App/renderer/HtmlElementHost.test.jsx +231 -0
  222. package/src/App/renderer/mapButtons.js +12 -9
  223. package/src/App/renderer/mapButtons.test.js +6 -4
  224. package/src/App/renderer/mapControls.js +6 -0
  225. package/src/App/renderer/mapControls.test.js +10 -2
  226. package/src/App/renderer/mapPanels.js +39 -26
  227. package/src/App/renderer/mapPanels.test.js +29 -0
  228. package/src/App/renderer/pluginWrapper.test.js +28 -0
  229. package/src/App/renderer/slotHelpers.js +60 -0
  230. package/src/App/renderer/slotHelpers.test.js +82 -0
  231. package/src/App/store/AppProvider.jsx +3 -0
  232. package/src/App/store/AppProvider.test.jsx +40 -0
  233. package/src/App/store/PluginProvider.jsx +7 -5
  234. package/src/App/store/appActionsMap.js +46 -1
  235. package/src/App/store/appActionsMap.test.js +21 -0
  236. package/src/App/store/appReducer.js +1 -0
  237. package/src/App/store/mapActionsMap.js +12 -24
  238. package/src/App/store/mapReducer.js +1 -1
  239. package/src/InteractiveMap/InteractiveMap.js +194 -13
  240. package/src/InteractiveMap/InteractiveMap.test.js +164 -2
  241. package/src/InteractiveMap/behaviourController.js +12 -2
  242. package/src/InteractiveMap/behaviourController.test.js +82 -1
  243. package/src/InteractiveMap/deviceChecker.js +1 -1
  244. package/src/InteractiveMap/deviceChecker.test.js +2 -2
  245. package/src/InteractiveMap/historyManager.js +41 -5
  246. package/src/InteractiveMap/historyManager.test.js +37 -6
  247. package/src/InteractiveMap/polyfills.js +39 -0
  248. package/src/InteractiveMap/polyfills.test.js +127 -0
  249. package/src/config/appConfig.js +5 -5
  250. package/src/config/appConfig.test.js +107 -42
  251. package/src/config/defaults.js +30 -20
  252. package/src/config/events.js +110 -4
  253. package/src/index.js +5 -2
  254. package/src/index.umd.js +1 -1
  255. package/src/scss/main.scss +1 -0
  256. package/src/scss/settings/_dimensions.scss +6 -2
  257. package/src/scss/tools/_border-focus.scss +20 -4
  258. package/src/services/eventBus.test.js +24 -1
  259. package/src/test-utils.js +1 -0
  260. package/src/types.js +590 -0
  261. package/src/utils/detectBreakpoint.js +78 -77
  262. package/src/utils/detectBreakpoint.test.js +50 -4
  263. package/src/utils/detectInterfaceType.js +4 -4
  264. package/src/utils/getIsFullscreen.js +3 -1
  265. package/src/utils/getSafeZoneInset.js +23 -7
  266. package/src/utils/getSafeZoneInset.test.js +18 -0
  267. package/src/utils/mapStateSync.js +3 -3
  268. package/src/utils/queryString.js +1 -1
  269. package/src/utils/stringToKebab.js +1 -1
  270. package/src/utils/toggleInertElements.js +37 -12
  271. package/webpack.dev.mjs +3 -4
  272. package/webpack.esm.mjs +8 -8
  273. package/webpack.umd.mjs +8 -8
  274. package/plugins/datasets/dist/esm/im-datasets-plugin.js +0 -1
  275. package/plugins/datasets/dist/esm/index.js +0 -2
  276. package/plugins/datasets/dist/umd/im-datasets-plugin.js +0 -1
  277. package/plugins/datasets/dist/umd/index.js +0 -2
  278. package/plugins/datasets/src/datasets.js +0 -60
  279. package/plugins/datasets/src/utils/bbox.js +0 -4
  280. package/plugins/draw-ml/dist/css/index.css +0 -1
  281. package/plugins/draw-ml/dist/esm/im-draw-ml-plugin.js +0 -1
  282. package/plugins/draw-ml/dist/esm/index.js +0 -2
  283. package/plugins/draw-ml/dist/umd/im-draw-ml-plugin.js +0 -1
  284. package/plugins/draw-ml/dist/umd/index.js +0 -2
  285. package/plugins/draw-ml/src/api/addFeature.js +0 -15
  286. package/plugins/draw-ml/src/api/newPolygon.js +0 -43
  287. package/plugins/draw-ml/src/draw.scss +0 -9
  288. package/plugins/draw-ml/src/events.js +0 -127
  289. package/plugins/draw-ml/src/manifest.js +0 -100
  290. package/plugins/draw-ml/src/modes/drawVertexMode.js +0 -275
  291. package/plugins/draw-ml/src/modes/editVertexMode.js +0 -426
  292. package/plugins/draw-ml/src/utils.js +0 -102
  293. package/plugins/frame/dist/esm/im-frame-plugin.js +0 -1
  294. package/plugins/frame/dist/esm/index.js +0 -2
  295. package/plugins/frame/dist/umd/im-frame-plugin.js +0 -1
  296. package/plugins/frame/dist/umd/index.js +0 -2
  297. package/plugins/map-styles/dist/esm/im-map-styles-plugin.js +0 -1
  298. package/plugins/map-styles/dist/esm/index.js +0 -2
  299. package/plugins/map-styles/dist/umd/im-map-styles-plugin.js +0 -1
  300. package/plugins/map-styles/dist/umd/index.js +0 -2
  301. package/plugins/scale-bar/dist/esm/im-scale-bar-plugin.js +0 -1
  302. package/plugins/scale-bar/dist/esm/index.js +0 -2
  303. package/plugins/scale-bar/dist/umd/im-scale-bar-plugin.js +0 -1
  304. package/plugins/scale-bar/dist/umd/index.js +0 -2
  305. package/plugins/search/src/utils/fetchDataset.js +0 -23
  306. package/plugins/use-location/dist/esm/im-use-location-plugin.js +0 -1
  307. package/plugins/use-location/dist/esm/index.js +0 -2
  308. package/plugins/use-location/dist/umd/im-use-location-plugin.js +0 -1
  309. package/plugins/use-location/dist/umd/index.js +0 -2
  310. package/providers/maplibre/dist/esm/im-maplibre-legacy-framework.js +0 -1
  311. package/providers/maplibre/dist/umd/im-maplibre-legacy-framework.js +0 -1
  312. package/providers/open-names/dist/esm/im-reverse-geocode.js +0 -2
  313. package/providers/open-names/dist/esm/index.js +0 -2
  314. package/providers/open-names/dist/umd/im-reverse-geocode.js +0 -2
  315. package/providers/open-names/dist/umd/index.js +0 -2
  316. /package/docs/architecture/{ARCHITECTURE_DIAGRAMS.md → architecture-diagrams.md} +0 -0
  317. /package/plugins/{datasets → beta/datasets}/dist/css/index.css +0 -0
  318. /package/plugins/{datasets/dist/esm/index.js.LICENSE.txt → beta/datasets/dist/esm/im-datasets-plugin.js.LICENSE.txt} +0 -0
  319. /package/plugins/{datasets/dist/umd → beta/datasets/dist/esm}/index.js.LICENSE.txt +0 -0
  320. /package/plugins/{draw-ml/dist/esm/index.js.LICENSE.txt → beta/datasets/dist/umd/im-datasets-plugin.js.LICENSE.txt} +0 -0
  321. /package/plugins/{draw-ml → beta/datasets}/dist/umd/index.js.LICENSE.txt +0 -0
  322. /package/plugins/{datasets → beta/datasets}/src/DatasetsInit.jsx +0 -0
  323. /package/plugins/{datasets → beta/datasets}/src/api/addDataset.js +0 -0
  324. /package/plugins/{datasets → beta/datasets}/src/api/hideDataset.js +0 -0
  325. /package/plugins/{datasets → beta/datasets}/src/api/hideFeatures.js +0 -0
  326. /package/plugins/{datasets → beta/datasets}/src/api/removeDataset.js +0 -0
  327. /package/plugins/{datasets → beta/datasets}/src/api/showDataset.js +0 -0
  328. /package/plugins/{datasets → beta/datasets}/src/api/showFeatures.js +0 -0
  329. /package/plugins/{datasets → beta/datasets}/src/datasets.scss +0 -0
  330. /package/plugins/{datasets → beta/datasets}/src/defaults.js +0 -0
  331. /package/plugins/{datasets → beta/datasets}/src/index.js +0 -0
  332. /package/plugins/{datasets → beta/datasets}/src/panels/Key.module.scss +0 -0
  333. /package/plugins/{datasets → beta/datasets}/src/panels/Layers.jsx +0 -0
  334. /package/plugins/{datasets → beta/datasets}/src/panels/Layers.module.scss +0 -0
  335. /package/plugins/{datasets → beta/datasets}/src/reducer.js +0 -0
  336. /package/plugins/{datasets → beta/datasets}/src/utils/debounce.js +0 -0
  337. /package/plugins/{datasets → beta/datasets}/src/utils/filters.js +0 -0
  338. /package/plugins/{draw-es → beta/draw-es}/src/DrawInit.jsx +0 -0
  339. /package/plugins/{draw-es → beta/draw-es}/src/api/addFeature.js +0 -0
  340. /package/plugins/{draw-es → beta/draw-es}/src/api/deleteFeature.js +0 -0
  341. /package/plugins/{draw-es → beta/draw-es}/src/api/editFeature.js +0 -0
  342. /package/plugins/{draw-es → beta/draw-es}/src/api/newPolygon.js +0 -0
  343. /package/plugins/{draw-es → beta/draw-es}/src/events.js +0 -0
  344. /package/plugins/{draw-es → beta/draw-es}/src/graphic.js +0 -0
  345. /package/plugins/{draw-es → beta/draw-es}/src/index.js +0 -0
  346. /package/plugins/{draw-es → beta/draw-es}/src/manifest.js +0 -0
  347. /package/plugins/{draw-es → beta/draw-es}/src/reducer.js +0 -0
  348. /package/plugins/{draw-es → beta/draw-es}/src/sketchViewModel.js +0 -0
  349. /package/plugins/{frame → beta/draw-ml}/dist/esm/index.js.LICENSE.txt +0 -0
  350. /package/plugins/{frame → beta/draw-ml}/dist/umd/index.js.LICENSE.txt +0 -0
  351. /package/plugins/{draw-ml → beta/draw-ml}/src/DrawInit.jsx +0 -0
  352. /package/plugins/{draw-ml → beta/draw-ml}/src/api/deleteFeature.js +0 -0
  353. /package/plugins/{draw-ml → beta/draw-ml}/src/index.js +0 -0
  354. /package/plugins/{draw-ml → beta/draw-ml}/src/modes/disabledMode.js +0 -0
  355. /package/plugins/{draw-ml/src → beta/draw-ml/src/utils}/snapHelpers.js +0 -0
  356. /package/plugins/{frame → beta/frame}/dist/css/index.css +0 -0
  357. /package/plugins/{map-styles → beta/frame}/dist/esm/index.js.LICENSE.txt +0 -0
  358. /package/plugins/{map-styles → beta/frame}/dist/umd/index.js.LICENSE.txt +0 -0
  359. /package/plugins/{frame → beta/frame}/package.json +0 -0
  360. /package/plugins/{frame → beta/frame}/src/FrameInit.jsx +0 -0
  361. /package/plugins/{frame → beta/frame}/src/api/addFrame.js +0 -0
  362. /package/plugins/{frame → beta/frame}/src/api/editFeature.js +0 -0
  363. /package/plugins/{frame → beta/frame}/src/config.js +0 -0
  364. /package/plugins/{frame → beta/frame}/src/frame.scss +0 -0
  365. /package/plugins/{frame → beta/frame}/src/index.js +0 -0
  366. /package/plugins/{frame → beta/frame}/src/manifest.js +0 -0
  367. /package/plugins/{frame → beta/frame}/src/reducer.js +0 -0
  368. /package/plugins/{frame → beta/frame}/src/utils.js +0 -0
  369. /package/plugins/{scale-bar → beta/map-styles}/dist/esm/index.js.LICENSE.txt +0 -0
  370. /package/plugins/{scale-bar → beta/map-styles}/dist/umd/index.js.LICENSE.txt +0 -0
  371. /package/plugins/{map-styles → beta/map-styles}/src/MapStylesInit.jsx +0 -0
  372. /package/plugins/{map-styles → beta/map-styles}/src/config.js +0 -0
  373. /package/plugins/{map-styles → beta/map-styles}/src/index.js +0 -0
  374. /package/plugins/{scale-bar → beta/scale-bar}/dist/css/index.css +0 -0
  375. /package/plugins/{use-location → beta/scale-bar}/dist/esm/index.js.LICENSE.txt +0 -0
  376. /package/plugins/{use-location → beta/scale-bar}/dist/umd/index.js.LICENSE.txt +0 -0
  377. /package/plugins/{scale-bar → beta/scale-bar}/package.json +0 -0
  378. /package/plugins/{scale-bar → beta/scale-bar}/src/ScaleBar.jsx +0 -0
  379. /package/plugins/{scale-bar → beta/scale-bar}/src/ScaleBar.test.jsx +0 -0
  380. /package/plugins/{scale-bar → beta/scale-bar}/src/index.js +0 -0
  381. /package/plugins/{scale-bar → beta/scale-bar}/src/index.test.js +0 -0
  382. /package/plugins/{scale-bar → beta/scale-bar}/src/manifest.js +0 -0
  383. /package/plugins/{scale-bar → beta/scale-bar}/src/scaleBar.scss +0 -0
  384. /package/plugins/{scale-bar → beta/scale-bar}/src/utils.js +0 -0
  385. /package/plugins/{scale-bar → beta/scale-bar}/src/utils.test.js +0 -0
  386. /package/{providers/open-names → plugins/beta/use-location}/dist/esm/index.js.LICENSE.txt +0 -0
  387. /package/{providers/open-names → plugins/beta/use-location}/dist/umd/index.js.LICENSE.txt +0 -0
  388. /package/plugins/{use-location → beta/use-location}/src/UseLocation.jsx +0 -0
  389. /package/plugins/{use-location → beta/use-location}/src/UseLocationInit.jsx +0 -0
  390. /package/plugins/{use-location → beta/use-location}/src/defaults.js +0 -0
  391. /package/plugins/{use-location → beta/use-location}/src/events.js +0 -0
  392. /package/plugins/{use-location → beta/use-location}/src/index.js +0 -0
  393. /package/plugins/{use-location → beta/use-location}/src/manifest.js +0 -0
  394. /package/plugins/{use-location → beta/use-location}/src/reducer.js +0 -0
  395. /package/providers/{esri → beta/esri}/src/appEvents.js +0 -0
  396. /package/providers/{esri → beta/esri}/src/defaults.js +0 -0
  397. /package/providers/{esri → beta/esri}/src/esriProvider.scss +0 -0
  398. /package/providers/{esri → beta/esri}/src/utils/coords.js +0 -0
  399. /package/providers/{esri → beta/esri}/src/utils/detectWebGL.js +0 -0
  400. /package/providers/{esri → beta/esri}/src/utils/esriFixes.js +0 -0
  401. /package/providers/{esri → beta/esri}/src/utils/query.js +0 -0
  402. /package/providers/{esri → beta/esri}/src/utils/spatial.js +0 -0
  403. /package/providers/{open-names → beta/open-names}/dist/esm/im-reverse-geocode.js.LICENSE.txt +0 -0
  404. /package/providers/{open-names/dist/umd/im-reverse-geocode.js.LICENSE.txt → beta/open-names/dist/esm/index.js.LICENSE.txt} +0 -0
  405. /package/providers/{open-names → beta/open-names}/src/index.js +0 -0
  406. /package/providers/{open-names → beta/open-names}/src/reverseGeocode.js +0 -0
  407. /package/providers/{open-names → beta/open-names}/src/utils/mapToLocationModel.js +0 -0
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Calculates the squared distance from a point (p) to a line segment (v to w).
3
+ */
4
+ const distToSegmentSquared = (p, v, w) => {
5
+ const l2 = (v.x - w.x) ** 2 + (v.y - w.y) ** 2
6
+ if (l2 === 0) {
7
+ return (p.x - v.x) ** 2 + (p.y - v.y) ** 2
8
+ }
9
+ let t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2
10
+ t = Math.max(0, Math.min(1, t))
11
+ return (p.x - (v.x + t * (w.x - v.x))) ** 2 + (p.y - (v.y + t * (w.y - v.y))) ** 2
12
+ }
13
+
14
+ /**
15
+ * Ray-casting algorithm to determine if a point is inside a polygon.
16
+ */
17
+ const isPointInPolygon = (point, ring) => {
18
+ const [px, py] = point
19
+ let inside = false
20
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i++) {
21
+ const [xi, yi] = ring[i]
22
+ const [xj, yj] = ring[j]
23
+ const isAboveI = yi > py
24
+ const isAboveJ = yj > py
25
+
26
+ if (isAboveI === isAboveJ) {
27
+ continue
28
+ }
29
+ // 2. Calculate horizontal intersection
30
+ const intersectX = ((xj - xi) * (py - yi)) / (yj - yi) + xi
31
+
32
+ if (px < intersectX) {
33
+ inside = !inside;
34
+ }
35
+ }
36
+ return inside
37
+ }
38
+
39
+ /**
40
+ * Calculates minimum squared pixel distance to the geometry.
41
+ */
42
+ const getMinDistToGeometry = (map, point, geometry) => {
43
+ const { coordinates: coords, type } = geometry
44
+ let minSqDist = Infinity
45
+ const getScreenPt = (lngLat) => map.project(lngLat)
46
+
47
+ const processLine = (lineCoords) => {
48
+ for (let i = 0; i < lineCoords.length - 1; i++) {
49
+ const d2 = distToSegmentSquared(point, getScreenPt(lineCoords[i]), getScreenPt(lineCoords[i + 1]))
50
+ if (d2 < minSqDist) {
51
+ minSqDist = d2
52
+ }
53
+ }
54
+ }
55
+
56
+ if (type === 'Point') {
57
+ const p = getScreenPt(coords)
58
+ minSqDist = (point.x - p.x) ** 2 + (point.y - p.y) ** 2
59
+ } else if (type === 'LineString' || type === 'MultiPoint') {
60
+ if (type === 'LineString') {
61
+ processLine(coords)
62
+ } else {
63
+ coords.forEach((pt) => {
64
+ const p = getScreenPt(pt)
65
+ const d2 = (point.x - p.x) ** 2 + (point.y - p.y) ** 2
66
+ if (d2 < minSqDist) {
67
+ minSqDist = d2
68
+ }
69
+ })
70
+ }
71
+ } else if (type === 'Polygon' || type === 'MultiLineString') {
72
+ coords.forEach(processLine)
73
+ } else if (type === 'MultiPolygon') {
74
+ coords.forEach((poly) => poly.forEach(processLine))
75
+ } else {
76
+ // No action
77
+ }
78
+ return minSqDist
79
+ }
80
+
81
+ /**
82
+ * Query features prioritizing Layer Order, then Containment for Polygons.
83
+ */
84
+ export const queryFeatures = (map, point, options = {}) => {
85
+ const { radius = 10 } = options
86
+ const queryArea = [[point.x - radius, point.y - radius], [point.x + radius, point.y + radius]]
87
+ const rawFeatures = map.queryRenderedFeatures(queryArea)
88
+ if (rawFeatures.length === 0) {
89
+ return []
90
+ }
91
+
92
+ // Identify layer visual hierarchy
93
+ const layerStack = []
94
+ rawFeatures.forEach(f => {
95
+ if (layerStack.includes(f.layer.id) === false) {
96
+ layerStack.push(f.layer.id)
97
+ }
98
+ })
99
+
100
+ // Deduplicate Bottom-Up to favor data layers over highlight layers
101
+ const seenIds = new Set()
102
+ const uniqueFeatures = []
103
+ for (let i = rawFeatures.length - 1; i >= 0; i--) {
104
+ const f = rawFeatures[i]
105
+ const featureId = f.id === undefined ? JSON.stringify(f.properties) : f.id
106
+ if (seenIds.has(featureId) === false) {
107
+ seenIds.add(featureId)
108
+ uniqueFeatures.push(f)
109
+ }
110
+ }
111
+
112
+ const clickLngLat = map.unproject(point)
113
+ const clickPt = [clickLngLat.lng, clickLngLat.lat]
114
+
115
+ return uniqueFeatures
116
+ .map((f) => {
117
+ let score = 0
118
+ const type = f.geometry.type
119
+ const pixelDistSq = getMinDistToGeometry(map, point, f.geometry)
120
+
121
+ // PRIORITY 1: LAYER ORDER
122
+ const layerRank = layerStack.indexOf(f.layer.id)
123
+ score += (layerRank * 1000000)
124
+
125
+ // PRIORITY 2: CONTAINMENT (Polygon Special Treatment)
126
+ if (type.includes('Polygon')) {
127
+ const polys = type === 'Polygon' ? [f.geometry.coordinates] : f.geometry.coordinates
128
+ const isInside = polys.some((ring) => isPointInPolygon(clickPt, ring[0]))
129
+
130
+ if (isInside === true) {
131
+ // Massive boost for polygons if we are actually inside them
132
+ score -= 500000 // NOSONAR - tolerance used only here
133
+ } else {
134
+ // If we are outside a polygon, it loses significantly to anything we ARE inside
135
+ score += 100000 // NOSONAR - tolerance used only here
136
+ }
137
+ }
138
+
139
+ // PRIORITY 3: DISTANCE (Final Tie-breaker)
140
+ score += pixelDistSq
141
+
142
+ return { f, score }
143
+ })
144
+ .sort((a, b) => a.score - b.score)
145
+ .map(({ f }) => f)
146
+ }
@@ -123,26 +123,23 @@ const getCardinalMove = (from, to) => {
123
123
  * @param {Array<[number, number]>} pixels - Array of pixel coordinates.
124
124
  * @returns {number} Index of the closest pixel in the given direction.
125
125
  */
126
+ const isInDirection = (direction, dx, dy) => {
127
+ switch (direction) {
128
+ case 'ArrowUp': return dy < 0 && Math.abs(dy) >= Math.abs(dx)
129
+ case 'ArrowDown': return dy > 0 && Math.abs(dy) >= Math.abs(dx)
130
+ case 'ArrowLeft': return dx < 0 && Math.abs(dx) > Math.abs(dy)
131
+ case 'ArrowRight': return dx > 0 && Math.abs(dx) > Math.abs(dy)
132
+ default: return false
133
+ }
134
+ }
135
+
126
136
  const spatialNavigate = (direction, start, pixels) => {
127
137
  const [sx, sy] = start
128
138
 
129
139
  // Direction filters
130
- const candidates = pixels.filter(([x, y]) => {
131
- if (x === sx && y === sy) {
132
- return false
133
- }
134
-
135
- const dx = x - sx
136
- const dy = y - sy
137
-
138
- switch (direction) {
139
- case 'ArrowUp': return dy < 0 && Math.abs(dy) >= Math.abs(dx)
140
- case 'ArrowDown': return dy > 0 && Math.abs(dy) >= Math.abs(dx)
141
- case 'ArrowLeft': return dx < 0 && Math.abs(dx) > Math.abs(dy)
142
- case 'ArrowRight': return dx > 0 && Math.abs(dx) > Math.abs(dy)
143
- default: return false
144
- }
145
- })
140
+ const candidates = pixels.filter(([x, y]) =>
141
+ (x !== sx || y !== sy) && isInDirection(direction, x - sx, y - sy)
142
+ )
146
143
 
147
144
  if (!candidates.length) {
148
145
  return pixels.findIndex(p => p[0] === sx && p[1] === sy)
@@ -169,7 +166,7 @@ const getResolution = (center, zoom) => {
169
166
  const TILE_SIZE = 512
170
167
  const lat = center.lat
171
168
  const scale = Math.pow(2, zoom)
172
- const resolution = (EARTH_CIRCUMFERENCE * Math.cos((lat * Math.PI) / 180)) / (scale * TILE_SIZE)
169
+ const resolution = (EARTH_CIRCUMFERENCE * Math.cos((lat * Math.PI) / 180)) / (scale * TILE_SIZE) // NOSONAR - 180 is degrees-to-radians conversion
173
170
  return resolution
174
171
  }
175
172
 
@@ -11,13 +11,50 @@ sonar.javascript.lcov.reportPaths=coverage/lcov.info
11
11
  sonar.sourceEncoding=UTF-8
12
12
 
13
13
  sonar.sources=src,plugins,providers
14
- sonar.exclusions=**/*.test.*,**/__mocks__/**,**/__stubs__/**,**/dist/**,**/node_modules/**
14
+ sonar.exclusions=**/*.test.*,**/__mocks__/**,**/__stubs__/**,**/dist/**,**/node_modules/**,plugins/beta/**,providers/beta/**
15
15
  sonar.tests=src,plugins,providers
16
16
  sonar.test.inclusions=**/*.test.*,**/__mocks__/**,**/__stubs__/**
17
17
  sonar.cpd.exclusions=**/*.test.*,**/__mocks__/**,**/__stubs__/**
18
18
 
19
- sonar.issue.ignore.multicriteria=reactPropsJs,reactPropsJsx
19
+ # Ignored rules
20
+ sonar.issue.ignore.multicriteria=reactPropsJs,reactPropsJsx,preferGlobalThisJs,preferGlobalThisJsx,preferAtJs,preferAtJsx,replaceAllJs,replaceAllJsx,stringReplaceAllJs,stringReplaceAllJsx,pascalCaseFunctionsJs,pascalCaseFunctionsJsx,pascalCaseFunctionsTs,pascalCaseFunctionsTsx
21
+
22
+ # S6774: React props validation - using TypeScript/JSDoc for prop types instead
20
23
  sonar.issue.ignore.multicriteria.reactPropsJs.ruleKey=javascript:S6774
21
24
  sonar.issue.ignore.multicriteria.reactPropsJs.resourceKey=**/*.js
22
25
  sonar.issue.ignore.multicriteria.reactPropsJsx.ruleKey=javascript:S6774
23
- sonar.issue.ignore.multicriteria.reactPropsJsx.resourceKey=**/*.jsx
26
+ sonar.issue.ignore.multicriteria.reactPropsJsx.resourceKey=**/*.jsx
27
+
28
+ # S7764: Prefer globalThis over window
29
+ sonar.issue.ignore.multicriteria.preferGlobalThisJs.ruleKey=javascript:S7764
30
+ sonar.issue.ignore.multicriteria.preferGlobalThisJs.resourceKey=**/*.js
31
+ sonar.issue.ignore.multicriteria.preferGlobalThisJsx.ruleKey=javascript:S7764
32
+ sonar.issue.ignore.multicriteria.preferGlobalThisJsx.resourceKey=**/*.jsx
33
+
34
+ # S7755: Prefer .at() over [index]
35
+ sonar.issue.ignore.multicriteria.preferAtJs.ruleKey=javascript:S7755
36
+ sonar.issue.ignore.multicriteria.preferAtJs.resourceKey=**/*.js
37
+ sonar.issue.ignore.multicriteria.preferAtJsx.ruleKey=javascript:S7755
38
+ sonar.issue.ignore.multicriteria.preferAtJsx.resourceKey=**/*.jsx
39
+
40
+ # S6316: Array.prototype.replaceAll may not be supported in all browsers
41
+ sonar.issue.ignore.multicriteria.replaceAllJs.ruleKey=javascript:S6316
42
+ sonar.issue.ignore.multicriteria.replaceAllJs.resourceKey=**/*.js
43
+ sonar.issue.ignore.multicriteria.replaceAllJsx.ruleKey=javascript:S6316
44
+ sonar.issue.ignore.multicriteria.replaceAllJsx.resourceKey=**/*.jsx
45
+
46
+ # S7781: Prefer .replaceAll() over .replace() with global regex
47
+ sonar.issue.ignore.multicriteria.stringReplaceAllJs.ruleKey=javascript:S7781
48
+ sonar.issue.ignore.multicriteria.stringReplaceAllJs.resourceKey=**/*.js
49
+ sonar.issue.ignore.multicriteria.stringReplaceAllJsx.ruleKey=javascript:S7781
50
+ sonar.issue.ignore.multicriteria.stringReplaceAllJsx.resourceKey=**/*.jsx
51
+
52
+ # S100: Function names should comply with a naming convention (PascalCase for React/Inits) - mustn't pics up React functions
53
+ sonar.issue.ignore.multicriteria.pascalCaseFunctionsJs.ruleKey=javascript:S100
54
+ sonar.issue.ignore.multicriteria.pascalCaseFunctionsJs.resourceKey=**/*.js
55
+ sonar.issue.ignore.multicriteria.pascalCaseFunctionsJsx.ruleKey=javascript:S100
56
+ sonar.issue.ignore.multicriteria.pascalCaseFunctionsJsx.resourceKey=**/*.jsx
57
+ sonar.issue.ignore.multicriteria.pascalCaseFunctionsTs.ruleKey=javascript:S100
58
+ sonar.issue.ignore.multicriteria.pascalCaseFunctionsTs.resourceKey=**/*.ts
59
+ sonar.issue.ignore.multicriteria.pascalCaseFunctionsTsx.ruleKey=javascript:S100
60
+ sonar.issue.ignore.multicriteria.pascalCaseFunctionsTsx.resourceKey=**/*.tsx
@@ -3,7 +3,7 @@ import { useApp } from '../../store/appContext'
3
3
 
4
4
  // eslint-disable-next-line camelcase, react/jsx-pascal-case
5
5
  // sonarjs/disable-next-line function-name
6
- export const Actions = ({ slot, children }) => {
6
+ export const Actions = ({ children }) => {
7
7
  const { openPanels, panelConfig, breakpoint } = useApp()
8
8
 
9
9
  const childArray = React.Children.toArray(children)
@@ -21,7 +21,7 @@ export const Actions = ({ slot, children }) => {
21
21
  ].filter(Boolean).join(' ')
22
22
 
23
23
  return (
24
- <div className={className} style={!visibleChild ? { display: 'none' } : undefined}>
24
+ <div className={className} style={visibleChild ? undefined : { display: 'none' }}>
25
25
  {children}
26
26
  </div>
27
27
  )
@@ -2,7 +2,7 @@
2
2
  display: flex;
3
3
  flex-direction: row;
4
4
  justify-content: center;
5
- gap: var(--divider-gap);
5
+ gap: calc(var(--divider-gap) / 2);
6
6
  padding: var(--panel-margin);
7
7
 
8
8
  .im-c-button-wrapper--wide {
@@ -22,6 +22,6 @@
22
22
 
23
23
  .im-o-app--tablet, .im-o-app--desktop {
24
24
  .im-c-actions {
25
- min-width: 280px;
25
+ min-width: 0;
26
26
  }
27
27
  }
@@ -1,11 +1,13 @@
1
1
  // src/components/KeyboardHelp.jsx
2
2
  import React from 'react'
3
+ import { useConfig } from '../../store/configContext'
3
4
  import { getKeyboardShortcuts } from '../../registry/keyboardShortcutRegistry.js'
4
5
 
5
6
  // eslint-disable-next-line camelcase, react/jsx-pascal-case
6
7
  // sonarjs/disable-next-line function-name
7
8
  export const KeyboardHelp = () => {
8
- const groups = getKeyboardShortcuts().reduce((acc, shortcut) => {
9
+ const appConfig = useConfig()
10
+ const groups = getKeyboardShortcuts(appConfig).reduce((acc, shortcut) => {
9
11
  acc[shortcut.group] = acc[shortcut.group] || []
10
12
  acc[shortcut.group].push(shortcut)
11
13
  return acc
@@ -3,12 +3,21 @@ import React from 'react'
3
3
  import { render, screen, within } from '@testing-library/react'
4
4
  import { KeyboardHelp } from './KeyboardHelp'
5
5
  import { getKeyboardShortcuts } from '../../registry/keyboardShortcutRegistry.js'
6
+ import { useConfig } from '../../store/configContext'
6
7
 
7
8
  jest.mock('../../registry/keyboardShortcutRegistry.js', () => ({
8
9
  getKeyboardShortcuts: jest.fn()
9
10
  }))
10
11
 
12
+ jest.mock('../../store/configContext', () => ({
13
+ useConfig: jest.fn()
14
+ }))
15
+
11
16
  describe('KeyboardHelp', () => {
17
+ beforeEach(() => {
18
+ useConfig.mockReturnValue({})
19
+ })
20
+
12
21
  afterEach(() => {
13
22
  jest.clearAllMocks()
14
23
  })
@@ -7,7 +7,7 @@ export const Logo = () => {
7
7
  const { mapStyle } = useMap()
8
8
 
9
9
  if (!mapStyle?.logo) {
10
- return
10
+ return undefined
11
11
  }
12
12
 
13
13
  return (
@@ -1,13 +1,169 @@
1
1
  // components/MapButton.jsx
2
+ import React, { useState, useRef } from 'react'
2
3
  import { stringToKebab } from '../../../utils/stringToKebab'
3
4
  import { Tooltip } from '../Tooltip/Tooltip'
4
5
  import { Icon } from '../Icon/Icon'
5
6
  import { SlotRenderer } from '../../renderer/SlotRenderer'
7
+ import { PopupMenu } from '../PopupMenu/PopupMenu'
6
8
  import { useConfig } from '../../store/configContext'
7
9
  import { useApp } from '../../store/appContext'
8
10
 
9
- // eslint-disable-next-line camelcase, react/jsx-pascal-case
10
- // sonarjs/disable-next-line function-name
11
+ /**
12
+ * Builds CSS class names for the map button element.
13
+ * @param {string} buttonId - Unique identifier for the button (kebab-cased)
14
+ * @param {string} variant - Visual variant of the button (e.g., 'primary', 'secondary')
15
+ * @param {boolean} showLabel - Whether the button label is displayed
16
+ * @returns {string} Space-separated CSS class names
17
+ */
18
+ const buildButtonClassNames = (buttonId, variant, showLabel) => [
19
+ 'im-c-map-button',
20
+ buttonId && `im-c-map-button--${stringToKebab(buttonId)}`,
21
+ variant && `im-c-map-button--${variant}`,
22
+ showLabel && 'im-c-map-button--with-label'
23
+ ].filter(Boolean).join(' ')
24
+
25
+ /**
26
+ * Builds CSS class names for the wrapper div that contains the button.
27
+ * @param {string} buttonId - Unique identifier for the button
28
+ * @param {boolean} showLabel - Whether the button label is displayed
29
+ * @param {boolean} groupStart - Whether this button is at the start of a button group
30
+ * @param {boolean} groupMiddle - Whether this button is in the middle of a button group
31
+ * @param {boolean} groupEnd - Whether this button is at the end of a button group
32
+ * @returns {string} Space-separated CSS class names for the wrapper
33
+ */
34
+ const buildWrapperClassNames = (buttonId, showLabel, groupStart, groupMiddle, groupEnd) => [
35
+ 'im-c-button-wrapper',
36
+ buttonId && `im-c-button-wrapper--${stringToKebab(buttonId)}`,
37
+ showLabel && 'im-c-button-wrapper--wide',
38
+ groupStart && 'im-c-button-wrapper--group-start',
39
+ groupMiddle && 'im-c-button-wrapper--group-middle',
40
+ groupEnd && 'im-c-button-wrapper--group-end'
41
+ ].filter(Boolean).join(' ')
42
+
43
+ /**
44
+ * Handles spacebar key presses on anchor links by triggering a click event.
45
+ * This ensures anchor buttons behave like standard buttons when spacebar is pressed.
46
+ * @param {KeyboardEvent} e - The keyboard event
47
+ */
48
+ const handleKeyUp = (e) => {
49
+ if (e.key === ' ' || e.key === 'Spacebar') {
50
+ e.preventDefault()
51
+ e.currentTarget.click()
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Determines the controlled element (panel or popup menu) for ARIA attributes.
57
+ * @param {Object} options - Configuration options
58
+ * @param {string} options.idPrefix - Prefix for generated IDs
59
+ * @param {string} options.panelId - ID of the controlled panel (if applicable)
60
+ * @param {string} options.buttonId - Unique button identifier
61
+ * @param {boolean} options.hasMenu - Whether the button has a popup menu
62
+ * @returns {Object|null} Object with id and type ('panel' or 'popup'), or null if no controlled element
63
+ */
64
+ const getControlledElement = ({ idPrefix, panelId, buttonId, hasMenu }) => {
65
+ if (panelId) {
66
+ return { id: `${idPrefix}-panel-${stringToKebab(panelId)}`, type: 'panel' }
67
+ }
68
+ if (hasMenu) {
69
+ return { id: `${idPrefix}-popup-${stringToKebab(buttonId)}`, type: 'popup' }
70
+ }
71
+ return null
72
+ }
73
+
74
+ /**
75
+ * Builds the complete props object for a button or anchor element.
76
+ * Handles ARIA attributes, event handlers, refs, and conditional styling.
77
+ * @param {Object} options - Configuration options
78
+ * @param {string} options.appId - Application identifier
79
+ * @param {string} options.buttonId - Unique button identifier
80
+ * @param {string} options.className - CSS classes for the button
81
+ * @param {Function} options.onClick - Click event handler
82
+ * @param {Function} options.onKeyUp - Key up event handler
83
+ * @param {Object} options.buttonRefs - React ref object for storing button references
84
+ * @param {boolean} options.isDisabled - Whether the button is disabled
85
+ * @param {boolean} options.isPressed - Whether the button is in pressed state
86
+ * @param {boolean} options.isExpanded - Whether content controlled by button is expanded
87
+ * @param {boolean} options.isPanelOpen - Whether the controlled panel is open
88
+ * @param {boolean} options.isPopupOpen - Whether the popup menu is open
89
+ * @param {Object|null} options.controlledElement - The element controlled by this button
90
+ * @param {string} options.href - URL for anchor element (if provided, renders as <a> instead of <button>)
91
+ * @returns {Object} Props object suitable for button or anchor element
92
+ */
93
+ const buildButtonProps = ({
94
+ appId,
95
+ buttonId,
96
+ className,
97
+ onClick,
98
+ onKeyUp,
99
+ buttonRefs,
100
+ isDisabled,
101
+ isPressed,
102
+ isExpanded,
103
+ isPanelOpen,
104
+ isPopupOpen,
105
+ controlledElement,
106
+ href
107
+ }) => {
108
+ let ariaExpanded
109
+ if (controlledElement?.type === 'panel') {
110
+ ariaExpanded = String(isPanelOpen)
111
+ } else if (controlledElement?.type === 'popup') {
112
+ ariaExpanded = isPopupOpen
113
+ } else if (typeof isExpanded === 'boolean') {
114
+ ariaExpanded = isExpanded
115
+ } else {
116
+ // No action
117
+ }
118
+
119
+ return {
120
+ id: `${appId}-${stringToKebab(buttonId)}`,
121
+ className,
122
+ onClick,
123
+ onKeyUp,
124
+ ref: (el) => {
125
+ if (buttonRefs.current && buttonId) {
126
+ buttonRefs.current[buttonId] = el
127
+ }
128
+ },
129
+ 'aria-disabled': isDisabled || undefined,
130
+ 'aria-expanded': ariaExpanded,
131
+ 'aria-pressed': typeof isPressed === 'boolean' ? isPressed : undefined,
132
+ 'aria-controls': controlledElement?.id,
133
+ 'aria-haspopup': controlledElement?.type === 'popup' || undefined,
134
+ ...(href
135
+ ? { href, target: '_blank', onKeyUp: handleKeyUp, role: 'button' }
136
+ : { type: 'button' })
137
+ }
138
+ }
139
+
140
+ /**
141
+ * MapButton component - A versatile button for map applications.
142
+ * Supports icons, labels, popups, panels, tooltips, and button grouping.
143
+ * Renders as either a <button> or <a> element depending on props.
144
+ * @component
145
+ * @param {Object} props - Component props
146
+ * @param {string} props.buttonId - Unique identifier for the button
147
+ * @param {string} [props.iconId] - Icon identifier to display
148
+ * @param {string} [props.iconSvgContent] - SVG content as an alternative to iconId
149
+ * @param {string} props.label - Button label/tooltip text
150
+ * @param {boolean} [props.showLabel=false] - Whether to display the label visually
151
+ * @param {boolean} [props.isDisabled=false] - Whether the button is disabled
152
+ * @param {boolean} [props.isPressed] - Whether the button is in pressed state (aria-pressed)
153
+ * @param {boolean} [props.isExpanded] - Whether content controlled by the button is expanded
154
+ * @param {boolean} [props.isHidden=false] - Whether to hide the button (CSS display: none)
155
+ * @param {boolean} [props.isPanelOpen=false] - Whether the controlled panel is open
156
+ * @param {string} [props.variant] - CSS variant class for styling (e.g., 'primary')
157
+ * @param {Function} [props.onClick] - Custom click handler
158
+ * @param {string} [props.panelId] - ID of the panel controlled by this button
159
+ * @param {Array<Object>} [props.menuItems] - Array of items for popup menu
160
+ * @param {string} [props.idPrefix=''] - Prefix for generated panel/popup IDs
161
+ * @param {string} [props.href] - URL for anchor element; if provided, renders as <a> instead of <button>
162
+ * @param {boolean} [props.groupStart=false] - Whether button is at start of button group
163
+ * @param {boolean} [props.groupMiddle=false] - Whether button is in middle of button group
164
+ * @param {boolean} [props.groupEnd=false] - Whether button is at end of button group
165
+ * @returns {JSX.Element} The rendered button component
166
+ */
11
167
  export const MapButton = ({
12
168
  buttonId,
13
169
  iconId,
@@ -15,13 +171,14 @@ export const MapButton = ({
15
171
  label,
16
172
  showLabel,
17
173
  isDisabled,
18
- isExpanded,
19
174
  isPressed,
175
+ isExpanded,
20
176
  isHidden,
21
- isOpen,
177
+ isPanelOpen,
22
178
  variant,
23
179
  onClick,
24
180
  panelId,
181
+ menuItems,
25
182
  idPrefix,
26
183
  href,
27
184
  groupMiddle,
@@ -30,42 +187,65 @@ export const MapButton = ({
30
187
  }) => {
31
188
  const { id: appId } = useConfig()
32
189
  const { buttonRefs } = useApp()
33
-
34
- const buttonClassNames = [
35
- 'im-c-map-button',
36
- buttonId && `im-c-map-button--${stringToKebab(buttonId)}`,
37
- variant && `im-c-map-button--${variant}`,
38
- showLabel && 'im-c-map-button--with-label'
39
- ].filter(Boolean).join(' ')
190
+ const [isPopupOpen, setIsPopupOpen] = useState(false)
191
+ const [menuStartPos, setMenuStartPos] = useState(null)
192
+ const menuRef = useRef(null)
40
193
 
41
194
  const Element = href ? 'a' : 'button'
195
+ const hasMenu = menuItems?.length >= 1
196
+ const controlledElement = getControlledElement({ idPrefix, panelId, buttonId, hasMenu })
42
197
 
43
- const handleKeyUp = (e) => {
44
- if (e.key === ' ' || e.key === 'Spacebar') {
45
- e.preventDefault() // prevent page scrolling
46
- e.currentTarget.click() // trigger click
198
+ /**
199
+ * Handles button click events.
200
+ * Toggles popup menu visibility if the button controls a popup.
201
+ * Calls the custom onClick handler if provided.
202
+ * @param {React.MouseEvent} e - The click event
203
+ */
204
+ const handleButtonClick = (e) => {
205
+ if (isDisabled) {
206
+ return
207
+ }
208
+ if (controlledElement?.type === 'popup') {
209
+ const isKeyboard = e.nativeEvent.pointerType === ''
210
+ /* istanbul ignore next as pointerType can't be tested in jest */
211
+ setMenuStartPos(isKeyboard ? 'first' : null)
212
+ setIsPopupOpen((prev) => !prev)
213
+ }
214
+ if (onClick) {
215
+ onClick(e)
47
216
  }
48
217
  }
49
218
 
50
- const buttonProps = {
51
- id: `${appId}-${stringToKebab(buttonId)}`,
52
- className: buttonClassNames,
53
- onClick,
54
- ref: (el) => {
55
- if (buttonRefs.current && buttonId) {
56
- buttonRefs.current[buttonId] = el
57
- }
58
- },
59
- 'aria-disabled': isDisabled || undefined,
60
- 'aria-expanded': typeof isExpanded === 'boolean' ? isExpanded : undefined,
61
- 'aria-pressed': panelId ? String(isOpen) : isPressed,
62
- 'aria-controls': panelId ? `${idPrefix}-panel-${stringToKebab(panelId)}` : undefined,
63
- ...(href
64
- ? { href, target: '_blank', onKeyUp: handleKeyUp, role: 'button' } // only <a>
65
- : { type: 'button' } // only <button>
66
- )
219
+ /**
220
+ * Handles key up events on buttons that control popup menus.
221
+ * ArrowDown opens the menu at the first item.
222
+ * ArrowUp opens the menu at the last item.
223
+ * @param {React.KeyboardEvent} e - The keyboard event
224
+ */
225
+ const handleButtonKeyUp = e => {
226
+ if (hasMenu && ['ArrowDown', 'ArrowUp'].includes(e.key)) {
227
+ e.preventDefault()
228
+ setMenuStartPos(e.key === 'ArrowUp' ? 'last' : 'first')
229
+ setIsPopupOpen(true)
230
+ }
67
231
  }
68
232
 
233
+ const buttonProps = buildButtonProps({
234
+ appId,
235
+ buttonId,
236
+ className: buildButtonClassNames(buttonId, variant, showLabel),
237
+ onClick: handleButtonClick,
238
+ onKeyUp: handleButtonKeyUp,
239
+ buttonRefs,
240
+ isDisabled,
241
+ isPressed,
242
+ isExpanded,
243
+ isPanelOpen,
244
+ isPopupOpen,
245
+ controlledElement,
246
+ href
247
+ })
248
+
69
249
  const buttonEl = (
70
250
  <Element {...buttonProps}>
71
251
  {(iconId || iconSvgContent) && <Icon id={iconId} svgContent={iconSvgContent} />}
@@ -73,18 +253,14 @@ export const MapButton = ({
73
253
  </Element>
74
254
  )
75
255
 
76
- const wrapperClassNames = [
77
- 'im-c-button-wrapper',
78
- showLabel && ' im-c-button-wrapper--wide',
79
- groupStart && 'im-c-button-wrapper--group-start',
80
- groupMiddle && 'im-c-button-wrapper--group-middle',
81
- groupEnd && 'im-c-button-wrapper--group-end'
82
- ].filter(Boolean).join(' ')
83
-
84
256
  return (
85
- <div className={wrapperClassNames} style={isHidden ? { display: 'none' } : undefined}>
257
+ <div
258
+ className={buildWrapperClassNames(buttonId, showLabel, groupStart, groupMiddle, groupEnd)}
259
+ style={isHidden ? { display: 'none' } : undefined}
260
+ >
86
261
  {showLabel ? buttonEl : <Tooltip content={label}>{buttonEl}</Tooltip>}
87
262
  {panelId && <SlotRenderer slot={`${stringToKebab(buttonId)}-button`} />}
263
+ {isPopupOpen && <PopupMenu popupMenuId={controlledElement.id} buttonId={buttonId} startPos={menuStartPos} menuRef={menuRef} items={menuItems} setIsOpen={setIsPopupOpen} />}
88
264
  </div>
89
265
  )
90
266
  }