@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,246 @@
1
+ import React, { useEffect, useState, useMemo } from 'react'
2
+ import { stringToKebab } from '../../../utils/stringToKebab'
3
+ import { useConfig } from '../../store/configContext'
4
+ import { useApp } from '../../store/appContext'
5
+ import { Icon } from '../Icon/Icon'
6
+ import { useEvaluateProp } from '../../hooks/useEvaluateProp.js'
7
+
8
+ /**
9
+ * PopupMenu — accessible keyboard-navigable dropdown menu component.
10
+ * Renders a role=menu list with selected item highlighting, keyboard nav (arrows/Home/End/Enter),
11
+ * and closes-on-outside-click/focus behavior. Skips hidden items during navigation.
12
+ *
13
+ * @component
14
+ * @param {string} popupMenuId
15
+ * Unique identifier for the menu element and item IDs (items get id={id}-item-{i})
16
+ * @param {string} pluginId
17
+ * Identifier for the owning plugin, passed to evaluateProp for context
18
+ * @param {string} buttonId
19
+ * Ref key of the button that triggered this menu; used to manage focus and detect outside clicks
20
+ * @param {string} [startPos]
21
+ * Initial selection strategy: 'first' (first visible), 'last' (last visible), or null/undefined (no selection)
22
+ * @param {number} [startIndex]
23
+ * Exact index to select on mount; takes precedence over startPos
24
+ * @param {MutableRefObject} menuRef
25
+ * Ref to the menu UL element; used for focus management and click-outside detection
26
+ * @param {Array} items
27
+ * Array of menu items {id, label, onClick, iconId?, iconSvgContent?, pressedWhen?} to render
28
+ * @param {Function} setIsOpen
29
+ * Callback to close the menu (called with false when user presses Escape/Tab or clicks outside)
30
+ * @returns {JSX.Element}
31
+ * A role=menu UL with keyboard handlers and visible=filtered LI children
32
+ */
33
+ // eslint-disable-next-line camelcase, react/jsx-pascal-case
34
+ // sonarjs/disable-next-line function-name
35
+ export const PopupMenu = ({ popupMenuId, buttonId, instigatorId, pluginId, startPos, startIndex, menuRef, items, setIsOpen }) => {
36
+ const { id } = useConfig()
37
+ const { buttonRefs, buttonConfig, hiddenButtons, disabledButtons, pressedButtons } = useApp()
38
+ const instigatorKey = buttonId ?? instigatorId
39
+ const instigator = buttonRefs.current[instigatorKey]
40
+ const evaluateProp = useEvaluateProp()
41
+
42
+ /**
43
+ * Compute visible item indices by filtering out items in hiddenButtons.
44
+ * Memoized on items and hiddenButtons to avoid recomputation.
45
+ */
46
+ const visibleIndices = useMemo(() => {
47
+ const visible = []
48
+ items.forEach((item, idx) => {
49
+ if (!hiddenButtons.has(item.id)) {
50
+ visible.push(idx)
51
+ }
52
+ })
53
+ return visible
54
+ }, [items, hiddenButtons])
55
+
56
+ /**
57
+ * Initialize selected index from startIndex (exact value) or startPos ('first'/'last')
58
+ * Falls back to -1 (no selection) if no initial value provided or all items are hidden.
59
+ */
60
+ const [index, setIndex] = useState(() => {
61
+ if (typeof startIndex === 'number') {
62
+ return startIndex
63
+ }
64
+ if (startPos === 'first') {
65
+ return visibleIndices[0] ?? -1
66
+ }
67
+ if (startPos === 'last') {
68
+ return visibleIndices[visibleIndices.length - 1] ?? -1
69
+ }
70
+ return -1
71
+ })
72
+
73
+ /**
74
+ * Helper: Close menu and return focus to instigator button.
75
+ * @param {Event} e
76
+ * The event that triggered the close (may have preventDefault called)
77
+ * @param {boolean} [preventDefault=false]
78
+ * Whether to call e.preventDefault() (true for Escape, false for Tab)
79
+ */
80
+ const closeAndFocus = (e, preventDefault = false) => {
81
+ if (preventDefault && e?.preventDefault) {
82
+ e.preventDefault()
83
+ }
84
+ instigator.focus()
85
+ setIsOpen(false)
86
+ }
87
+
88
+ /**
89
+ * Helper: Navigate visible items via ArrowDown/ArrowUp.
90
+ * ArrowDown moves forward (wraps at end); ArrowUp moves backward (wraps at start).
91
+ * When no selection (-1), ArrowDown picks first, ArrowUp picks last.
92
+ * @param {KeyboardEvent} e
93
+ * Keyboard event (checked for ArrowDown/ArrowUp)
94
+ */
95
+ const navigateVisible = (e) => {
96
+ e.preventDefault()
97
+ const vis = visibleIndices
98
+ const n = vis.length
99
+ if (n === 0) {
100
+ return
101
+ }
102
+ const pos = vis.indexOf(index)
103
+ let nextPos
104
+ if (e.key === 'ArrowDown') {
105
+ nextPos = pos === -1 ? 0 : (pos + 1) % n
106
+ } else if (pos === -1) {
107
+ nextPos = n - 1
108
+ } else {
109
+ nextPos = (pos - 1 + n) % n
110
+ }
111
+ setIndex(vis[nextPos])
112
+ }
113
+
114
+ /**
115
+ * Helper: Handle Enter key press.
116
+ * Calls onClick on selected item (by index), then closes menu and returns focus to instigator.
117
+ * @param {KeyboardEvent} e
118
+ * The Enter keydown event
119
+ */
120
+ const handleEnter = (e) => {
121
+ e.preventDefault()
122
+ instigator.focus()
123
+ setIsOpen(false)
124
+ items[index]?.onClick(e.nativeEvent)
125
+ }
126
+
127
+ /**
128
+ * Main keyboard handler for the menu.
129
+ * Dispatches to helpers or directly handles:
130
+ * - Escape/Esc: close & focus instigator
131
+ * - Tab: close & focus instigator (without preventDefault)
132
+ * - ArrowDown/ArrowUp: navigate visible items
133
+ * - Home: select first visible
134
+ * - End: select last visible
135
+ * - Enter: call selected item's onClick & close
136
+ * @param {KeyboardEvent} e
137
+ * Keyboard event from menu onKeyDown
138
+ */
139
+ const handleMenuKeyDown = (e) => {
140
+ if (['Escape', 'Esc'].includes(e.key)) {
141
+ closeAndFocus(e, true)
142
+ return
143
+ }
144
+ if (e.key === 'Tab') {
145
+ closeAndFocus(e)
146
+ return
147
+ }
148
+ if (['ArrowDown', 'ArrowUp'].includes(e.key)) {
149
+ navigateVisible(e)
150
+ return
151
+ }
152
+ if (e.key === 'Home' && visibleIndices.length) {
153
+ setIndex(visibleIndices[0])
154
+ return
155
+ }
156
+ if (e.key === 'End' && visibleIndices.length) {
157
+ setIndex(visibleIndices[visibleIndices.length - 1])
158
+ return
159
+ }
160
+ if (e.key === 'Enter') {
161
+ handleEnter(e)
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Helper: Close menu if click/focus originated outside menu and instigator.
167
+ * Registered on document focusin and pointerdown events to detect outside interactions.
168
+ * @param {Event} e
169
+ * The focusin or pointerdown event
170
+ */
171
+ const handleOutside = (e) => {
172
+ if (menuRef.current?.contains(e.target) || buttonRefs.current[instigatorKey]?.contains(e.target)) {
173
+ return
174
+ }
175
+ setIsOpen(false)
176
+ }
177
+
178
+ /**
179
+ * Helper: Handle item click.
180
+ * Looks up buttonConfig[item.id] to decide which onClick to call (item's or buttonConfig's).
181
+ * Closes menu after invoking onClick.
182
+ * @param {React.MouseEvent} e
183
+ * React synthetic event from the LI click
184
+ * @param {Object} item
185
+ * The clicked item object with {id, label, onClick, ...}
186
+ */
187
+ const handleItemClick = (e, item) => {
188
+ const menuItemConfig = buttonConfig[item.id]
189
+ setIsOpen(false)
190
+ if (typeof menuItemConfig?.onClick !== 'function') {
191
+ item.onClick?.(e.nativeEvent)
192
+ return
193
+ }
194
+ menuItemConfig.onClick(e, evaluateProp(ctx => ctx, pluginId))
195
+ }
196
+
197
+ useEffect(() => {
198
+ menuRef.current?.focus()
199
+
200
+ // If startPos changes on mount, ensure selection respects visible items.
201
+ if (startPos === 'first') {
202
+ setIndex(visibleIndices[0] ?? -1)
203
+ } else if (startPos === 'last') {
204
+ setIndex(visibleIndices[visibleIndices.length - 1] ?? -1)
205
+ } else {
206
+ // No action
207
+ }
208
+
209
+ document.addEventListener('focusin', handleOutside)
210
+ document.addEventListener('pointerdown', handleOutside)
211
+
212
+ return () => {
213
+ document.removeEventListener('focusin', handleOutside)
214
+ document.removeEventListener('pointerdown', handleOutside)
215
+ }
216
+ }, [])
217
+
218
+ return (
219
+ <ul // NOSONAR
220
+ ref={menuRef}
221
+ id={popupMenuId}
222
+ className='fm-c-popup-menu'
223
+ role='menu' // NOSONAR
224
+ tabIndex='-1'
225
+ aria-labelledby={instigatorKey}
226
+ aria-activedescendant={index >= 0 ? `${id}-${stringToKebab(items[index].id)}` : undefined}
227
+ onKeyDown={handleMenuKeyDown}
228
+ >
229
+ {items.map((item, i) => (
230
+ <li // NOSONAR
231
+ key={item.id}
232
+ id={`${id}-${stringToKebab(items[i].id)}`}
233
+ className={`fm-c-popup-menu__item${index === i ? ' fm-c-popup-menu__item--selected' : ''}`}
234
+ role='menuitem' // NOSONAR
235
+ aria-disabled={disabledButtons.has(items[i].id) || undefined} // NOSONAR
236
+ aria-pressed={(items[i].isPressed !== undefined || items[i].pressedWhen) ? pressedButtons.has(items[i].id) : undefined} // NOSONAR
237
+ style={hiddenButtons.has(items[i].id) ? { display: 'none' } : undefined}
238
+ onClick={(e) => handleItemClick(e, items[i])} // NOSONAR
239
+ >
240
+ {(item.iconId || item.iconSvgContent) && <Icon id={item.iconId} svgContent={item.iconSvgContent} />}
241
+ <span className='fm-c-popup-menu__item-label'>{item.label}</span>
242
+ </li>
243
+ ))}
244
+ </ul>
245
+ )
246
+ }
@@ -0,0 +1,70 @@
1
+ // ===================================================
2
+ // Component: PopupMenu
3
+ // ===================================================
4
+
5
+ @use '../../../scss/tools/index' as tools;
6
+
7
+ // 1. Base styles
8
+
9
+ .fm-c-popup-menu {
10
+ @include tools.border-focus-base(
11
+ $is-inset: false,
12
+ $type: button
13
+ );
14
+
15
+ background-color: var(--background-color);
16
+ position: absolute;
17
+ list-style: none;
18
+ margin: 0;
19
+ padding: 0;
20
+ }
21
+
22
+ // 2. Elements
23
+ .fm-c-popup-menu__item {
24
+ display: flex;
25
+ align-items: center;
26
+ padding: var(--divider-gap);
27
+ border-top: 1px solid var(--button-hover-color);
28
+ text-align: left;
29
+ cursor: pointer;
30
+ min-width: var(--popup-menu-min-width);
31
+ max-width: var(--popup-menu-max-width);
32
+
33
+ &:first-child {
34
+ border-top: 0;
35
+ }
36
+
37
+ &:hover:not(.fm-c-popup-menu__item--selected) {
38
+ @media (hover: hover) and (pointer: fine) {
39
+ background-color: var(--button-hover-color);
40
+ }
41
+ }
42
+
43
+ &:hover:not(.fm-c-popup-menu__item--selected) .fm-c-popup-menu__item-label {
44
+ text-decoration: underline;
45
+ text-underline-offset: 2px;
46
+ text-decoration-thickness: 2px;
47
+ text-decoration-color: var(--foreground-color);
48
+ text-decoration-skip-ink: none;
49
+ }
50
+
51
+ &--selected {
52
+ background-color: var(--button-hover-color);
53
+ }
54
+
55
+ &--selected .fm-c-popup-menu__item-label {
56
+ @include tools.link-focus;
57
+ }
58
+
59
+ svg + span {
60
+ margin-left: var(--button-icon-label-offset);
61
+ white-space: nowrap;
62
+ }
63
+ }
64
+
65
+ // 3. Modifiers
66
+ .im-o-app__actions .fm-c-popup-menu {
67
+ left: 50%;
68
+ transform: translateX(-50%);
69
+ bottom: calc(100% + (var(--primary-gap) * 2));
70
+ }
@@ -0,0 +1,304 @@
1
+ import React from 'react'
2
+ import { render, screen, fireEvent } from '@testing-library/react'
3
+ import { PopupMenu } from './PopupMenu'
4
+
5
+ // Mock Icon
6
+ jest.mock('../Icon/Icon', () => ({
7
+ Icon: ({ id, svgContent }) => <svg data-testid={id || 'custom-icon'} data-svg={svgContent} />
8
+ }))
9
+
10
+ // Mock useEvaluateProp
11
+ const mockEvaluateProp = jest.fn((fn) => fn({ /* mock context */ }))
12
+ jest.mock('../../hooks/useEvaluateProp.js', () => ({
13
+ useEvaluateProp: () => mockEvaluateProp
14
+ }))
15
+
16
+ // Mock useConfig to provide `id` used for stable item ids
17
+ jest.mock('../../store/configContext', () => ({ useConfig: () => ({ id: 'app' }) }))
18
+
19
+ // Mock useApp
20
+ const mockUseApp = {
21
+ buttonRefs: { current: { instigator: { focus: jest.fn(), contains: () => false } } },
22
+ buttonConfig: {},
23
+ hiddenButtons: new Set(),
24
+ disabledButtons: new Set(),
25
+ pressedButtons: new Set()
26
+ }
27
+ jest.mock('../../store/appContext', () => ({
28
+ useApp: jest.fn(() => mockUseApp)
29
+ }))
30
+
31
+ describe('PopupMenu', () => {
32
+ const items = [
33
+ { id: 'item1', label: 'Item 1', onClick: jest.fn() },
34
+ { id: 'item2', label: 'Item 2', onClick: jest.fn(), iconId: 'icon2', pressedWhen: () => true }
35
+ ]
36
+ let menuRef
37
+ const mockSetIsOpen = jest.fn()
38
+
39
+ beforeEach(() => {
40
+ jest.clearAllMocks()
41
+ menuRef = { current: { focus: jest.fn(), contains: () => false } }
42
+
43
+ // Reset mockUseApp state
44
+ mockUseApp.buttonRefs.current = { instigator: { focus: jest.fn(), contains: () => false } }
45
+ mockUseApp.buttonConfig = {}
46
+ mockUseApp.hiddenButtons = new Set()
47
+ mockUseApp.disabledButtons = new Set()
48
+ mockUseApp.pressedButtons = new Set()
49
+ })
50
+
51
+ const renderMenu = (props = {}) =>
52
+ render(
53
+ <PopupMenu
54
+ id='menu'
55
+ pluginId='plugin1'
56
+ instigatorId='instigator'
57
+ startIndex={0}
58
+ menuRef={menuRef}
59
+ items={items}
60
+ setIsOpen={mockSetIsOpen}
61
+ {...props}
62
+ />
63
+ )
64
+
65
+ // Test helpers to reduce duplication
66
+ const setHidden = (ids = []) => {
67
+ mockUseApp.hiddenButtons = new Set(ids)
68
+ }
69
+ const setPressed = (ids = []) => {
70
+ mockUseApp.pressedButtons = new Set(ids)
71
+ }
72
+ const renderDirect = (opts = {}) => {
73
+ const directMenuRef = { current: { focus: jest.fn(), contains: () => false } }
74
+ render(
75
+ <PopupMenu
76
+ id='direct'
77
+ pluginId='plugin1'
78
+ instigatorId='instigator'
79
+ menuRef={directMenuRef}
80
+ items={items}
81
+ setIsOpen={mockSetIsOpen}
82
+ {...opts}
83
+ />
84
+ )
85
+ return directMenuRef
86
+ }
87
+ const dispatchFocusIn = ({ menuContains = false, instigatorContains = false, target = document.body } = {}) => {
88
+ jest.spyOn(menuRef.current, 'contains').mockReturnValue(menuContains)
89
+ jest.spyOn(mockUseApp.buttonRefs.current.instigator, 'contains').mockReturnValue(instigatorContains)
90
+ const ev = new FocusEvent('focusin')
91
+ Object.defineProperty(ev, 'target', { value: target, enumerable: true })
92
+ document.dispatchEvent(ev)
93
+ }
94
+ const expectSelected = (text) => expect(screen.getByText(text).parentElement).toHaveClass('fm-c-popup-menu__item--selected')
95
+ const expectNotSelected = (text) => expect(screen.getByText(text).parentElement).not.toHaveClass('fm-c-popup-menu__item--selected')
96
+
97
+ it('renders items with labels and icons', () => {
98
+ renderMenu()
99
+ expect(screen.getByText('Item 1')).toBeInTheDocument()
100
+ expect(screen.getByText('Item 2')).toBeInTheDocument()
101
+ expect(screen.getByTestId('icon2')).toBeInTheDocument()
102
+ })
103
+
104
+ it('applies selected class to active index', () => {
105
+ renderMenu({ startIndex: 1 })
106
+ expect(screen.getByText('Item 2').parentElement).toHaveClass(
107
+ 'fm-c-popup-menu__item--selected'
108
+ )
109
+ })
110
+
111
+ it('handles ArrowDown, ArrowUp, Home, End keys', () => {
112
+ renderMenu()
113
+ const ul = screen.getByRole('menu')
114
+ fireEvent.keyDown(ul, { key: 'ArrowDown' })
115
+ expect(screen.getByText('Item 2').parentElement).toHaveClass('fm-c-popup-menu__item--selected')
116
+ fireEvent.keyDown(ul, { key: 'ArrowDown' })
117
+ expect(screen.getByText('Item 1').parentElement).toHaveClass('fm-c-popup-menu__item--selected')
118
+ fireEvent.keyDown(ul, { key: 'ArrowUp' })
119
+ expect(screen.getByText('Item 2').parentElement).toHaveClass('fm-c-popup-menu__item--selected')
120
+ fireEvent.keyDown(ul, { key: 'Home' })
121
+ expect(screen.getByText('Item 1').parentElement).toHaveClass('fm-c-popup-menu__item--selected')
122
+ fireEvent.keyDown(ul, { key: 'End' })
123
+ expect(screen.getByText('Item 2').parentElement).toHaveClass('fm-c-popup-menu__item--selected')
124
+ })
125
+
126
+ it('handles Enter key to call onClick and close menu', () => {
127
+ renderMenu({ startIndex: 1 })
128
+ const ul = screen.getByRole('menu')
129
+ fireEvent.keyDown(ul, { key: 'Enter' })
130
+ expect(items[1].onClick).toHaveBeenCalled()
131
+ expect(mockSetIsOpen).toHaveBeenCalled()
132
+ })
133
+
134
+ it('handles Escape and Tab to close menu', () => {
135
+ renderMenu()
136
+ const ul = screen.getByRole('menu')
137
+ fireEvent.keyDown(ul, { key: 'Escape' })
138
+ expect(mockSetIsOpen).toHaveBeenCalled()
139
+ fireEvent.keyDown(ul, { key: 'Tab' })
140
+ expect(mockSetIsOpen).toHaveBeenCalled()
141
+ })
142
+
143
+ it('calls item onClick directly if no buttonConfig', () => {
144
+ renderMenu()
145
+ fireEvent.click(screen.getByText('Item 1'))
146
+ expect(items[0].onClick).toHaveBeenCalled()
147
+ expect(mockSetIsOpen).toHaveBeenCalled()
148
+ })
149
+
150
+ it('calls buttonConfig.onClick with evaluateProp if defined', () => {
151
+ const mockOnClick = jest.fn()
152
+ mockUseApp.buttonConfig = { item2: { onClick: mockOnClick } }
153
+
154
+ renderMenu()
155
+ fireEvent.click(screen.getByText('Item 2'))
156
+
157
+ expect(mockOnClick).toHaveBeenCalled()
158
+ expect(mockEvaluateProp).toHaveBeenCalled()
159
+ expect(mockSetIsOpen).toHaveBeenCalled()
160
+ })
161
+
162
+ it('does nothing if click is inside menu', () => {
163
+ renderMenu()
164
+ const element = document.createElement('div')
165
+ dispatchFocusIn({ menuContains: true, target: element })
166
+ expect(mockSetIsOpen).not.toHaveBeenCalled()
167
+ })
168
+
169
+ it('does nothing if click is inside instigator', () => {
170
+ renderMenu()
171
+ dispatchFocusIn({ menuContains: false, instigatorContains: true, target: document.createElement('div') })
172
+ expect(mockSetIsOpen).not.toHaveBeenCalled()
173
+ })
174
+
175
+ it('returns early if click/focus target is inside menu or instigator', () => {
176
+ // menuRef contains → early return
177
+ renderMenu()
178
+ dispatchFocusIn({ menuContains: true, target: document.createElement('div') })
179
+ expect(mockSetIsOpen).not.toHaveBeenCalled()
180
+
181
+ // instigator contains → early return
182
+ jest.clearAllMocks()
183
+ jest.restoreAllMocks()
184
+ jest.spyOn(mockUseApp.buttonRefs.current.instigator, 'contains').mockReturnValue(true)
185
+ renderMenu()
186
+ dispatchFocusIn({ menuContains: false, instigatorContains: true, target: document.createElement('div') })
187
+ expect(mockSetIsOpen).not.toHaveBeenCalled()
188
+ })
189
+
190
+ it('evaluates line 51 but continues if target is outside', () => {
191
+ renderMenu()
192
+ dispatchFocusIn({ menuContains: false, instigatorContains: false, target: document.body })
193
+ expect(mockSetIsOpen).toHaveBeenCalled()
194
+ })
195
+
196
+ it('closes when clicking outside', () => {
197
+ renderMenu()
198
+ document.dispatchEvent(new MouseEvent('pointerdown', { target: document.body }))
199
+ expect(mockSetIsOpen).toHaveBeenCalled()
200
+ })
201
+
202
+ it('startPos="first" skips hidden items when selecting initial item', () => {
203
+ setHidden(['item1'])
204
+ renderMenu({ startPos: 'first', startIndex: undefined })
205
+ expectSelected('Item 2')
206
+ })
207
+
208
+ it('startPos="last" skips hidden items when selecting initial item', () => {
209
+ setHidden(['item2'])
210
+ renderMenu({ startPos: 'last', startIndex: undefined })
211
+ expectSelected('Item 1')
212
+ })
213
+
214
+ it('ArrowDown/ArrowUp from no selection picks first/last visible', () => {
215
+ renderMenu({ startIndex: -1 })
216
+ const ul = screen.getByRole('menu')
217
+ fireEvent.keyDown(ul, { key: 'ArrowDown' })
218
+ expect(screen.getByText('Item 1').parentElement).toHaveClass('fm-c-popup-menu__item--selected')
219
+ fireEvent.keyDown(ul, { key: 'ArrowUp' })
220
+ expect(screen.getByText('Item 2').parentElement).toHaveClass('fm-c-popup-menu__item--selected')
221
+ })
222
+
223
+ it('ArrowUp from no selection picks last visible', () => {
224
+ renderMenu({ startIndex: -1 })
225
+ const ul = screen.getByRole('menu')
226
+ fireEvent.keyDown(ul, { key: 'ArrowUp' })
227
+ expect(screen.getByText('Item 2').parentElement).toHaveClass('fm-c-popup-menu__item--selected')
228
+ })
229
+
230
+ it('initializes with no selection when neither startIndex nor startPos provided', () => {
231
+ renderMenu({ startIndex: undefined, startPos: undefined })
232
+ expect(screen.getByText('Item 1').parentElement).not.toHaveClass('fm-c-popup-menu__item--selected')
233
+ expect(screen.getByText('Item 2').parentElement).not.toHaveClass('fm-c-popup-menu__item--selected')
234
+ })
235
+
236
+ it('does nothing when navigating and no visible items', () => {
237
+ mockUseApp.hiddenButtons = new Set(['item1', 'item2'])
238
+ renderMenu({ startIndex: -1 })
239
+ const ul = screen.getByRole('menu')
240
+ fireEvent.keyDown(ul, { key: 'ArrowDown' })
241
+ expect(screen.getByText('Item 1').parentElement).not.toHaveClass('fm-c-popup-menu__item--selected')
242
+ expect(screen.getByText('Item 2').parentElement).not.toHaveClass('fm-c-popup-menu__item--selected')
243
+ })
244
+
245
+ it('Enter with no selection closes but does not call any item onClick', () => {
246
+ renderMenu({ startIndex: -1 })
247
+ const ul = screen.getByRole('menu')
248
+ fireEvent.keyDown(ul, { key: 'Enter' })
249
+ expect(items[0].onClick).not.toHaveBeenCalled()
250
+ expect(items[1].onClick).not.toHaveBeenCalled()
251
+ expect(mockSetIsOpen).toHaveBeenCalled()
252
+ })
253
+
254
+ it('Escape calls focus on instigator', () => {
255
+ const focusSpy = jest.fn()
256
+ mockUseApp.buttonRefs.current.instigator.focus = focusSpy
257
+ renderMenu()
258
+ const ul = screen.getByRole('menu')
259
+ fireEvent.keyDown(ul, { key: 'Escape' })
260
+ expect(focusSpy).toHaveBeenCalled()
261
+ })
262
+
263
+ it('direct render without startIndex respects startPos="first" (covers initializer and effect)', () => {
264
+ renderDirect({ startPos: 'first' })
265
+ expectSelected('Item 1')
266
+ })
267
+
268
+ it('direct render without startIndex respects startPos="last" (covers initializer and effect)', () => {
269
+ renderDirect({ startPos: 'last' })
270
+ expectSelected('Item 2')
271
+ })
272
+
273
+ it('startPos first with no visible items falls back to no selection', () => {
274
+ setHidden(['item1', 'item2'])
275
+ renderDirect({ startPos: 'first' })
276
+ expectNotSelected('Item 1')
277
+ expectNotSelected('Item 2')
278
+ })
279
+
280
+ it('startPos last with no visible items falls back to no selection', () => {
281
+ setHidden(['item1', 'item2'])
282
+ renderDirect({ startPos: 'last' })
283
+ expectNotSelected('Item 1')
284
+ expectNotSelected('Item 2')
285
+ })
286
+
287
+ it('sets aria-pressed and hides items based on pressedWhen and hiddenButtons', () => {
288
+ setPressed(['item2'])
289
+ setHidden(['item1'])
290
+ renderMenu()
291
+ const item1 = screen.getByText('Item 1').parentElement
292
+ const item2 = screen.getByText('Item 2').parentElement
293
+ expect(item1).toHaveStyle('display: none')
294
+ expect(item2).toHaveAttribute('aria-pressed', 'true')
295
+ })
296
+
297
+ it('ignores unrelated keys (covers Enter false branch)', () => {
298
+ renderMenu({ startIndex: 1 })
299
+ const ul = screen.getByRole('menu')
300
+ fireEvent.keyDown(ul, { key: 'a' })
301
+ expect(items[1].onClick).not.toHaveBeenCalled()
302
+ expect(mockSetIsOpen).not.toHaveBeenCalled()
303
+ })
304
+ })
@@ -71,6 +71,8 @@ export const Tooltip = ({ children, content }) => {
71
71
  childRef(node)
72
72
  } else if (childRef && typeof childRef === 'object') {
73
73
  childRef.current = node
74
+ } else {
75
+ // No action
74
76
  }
75
77
  }
76
78
 
@@ -80,13 +82,13 @@ export const Tooltip = ({ children, content }) => {
80
82
  onMouseLeave: hide,
81
83
  onMouseDown: cancel,
82
84
  onKeyDown: cancel,
83
- onFocus: (e) => {
85
+ onFocus: () => {
84
86
  if (interfaceType === 'keyboard') {
85
87
  show()
86
88
  }
87
89
  setFocused(true)
88
90
  },
89
- onBlur: (e) => {
91
+ onBlur: () => {
90
92
  hide()
91
93
  setFocused(false)
92
94
  },
@@ -21,7 +21,7 @@ export function getTooltipPosition (triggerEl, containerEl) {
21
21
 
22
22
  // Step 1: Find the actual smallest space
23
23
  const entries = Object.entries(space)
24
- const sorted = entries.sort(([, a], [, b]) => a - b)
24
+ const sorted = entries.toSorted(([, a], [, b]) => a - b)
25
25
  const [leastSide, leastValue] = sorted[0]
26
26
 
27
27
  // Step 2: Prefer horizontal if it's close in smallness
@@ -26,7 +26,7 @@ export const MapController = ({ mapContainerRef }) => {
26
26
  // Initialize map provider when props are available
27
27
  useEffect(() => {
28
28
  if (!safeZoneInset || !isLayoutReady || !mapStyle || !mapSize || isMapInitialized.current) {
29
- return
29
+ return undefined
30
30
  }
31
31
 
32
32
  requestAnimationFrame(() => {
@@ -8,7 +8,7 @@ export const MapStatus = () => {
8
8
  const { mapStatusRef } = useService()
9
9
 
10
10
  return (
11
- <div
11
+ <div // NOSONAR: div + status role is semantically correct here, <output> implies a calculation result
12
12
  ref={mapStatusRef}
13
13
  role='status'
14
14
  className='im-c-viewport__status'
@@ -30,7 +30,7 @@ export const Viewport = ({ keyboardHintPortalRef }) => {
30
30
 
31
31
  // Attach map events
32
32
  useMapEvents({
33
- [events.MAP_CLICK]: (e) => mapProvider?.clearHighlightedLabel?.()
33
+ [events.MAP_CLICK]: () => mapProvider?.clearHighlightedLabel?.()
34
34
  })
35
35
 
36
36
  // Manage keyboard hint visibility using local state
@@ -53,7 +53,7 @@ export const Viewport = ({ keyboardHintPortalRef }) => {
53
53
  useEffect(() => {
54
54
  const mainEl = layoutRefs.mainRef?.current
55
55
  if (!mainEl) {
56
- return
56
+ return undefined
57
57
  }
58
58
 
59
59
  mainEl.classList.toggle('im-o-app__main--keyboard-hint-visible', showHint)