@defra/interactive-map 0.0.43-fmp-experimental → 0.0.45-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.
- package/assets/templates/add-polygons.njk +11 -7
- package/assets/templates/add-symbols.njk +5 -1
- package/assets/templates/select-feature.njk +72 -15
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/context.md +24 -0
- package/docs/api/keyboard-shortcut-definition.md +102 -0
- package/docs/api.md +18 -8
- package/docs/building-a-plugin.md +1 -0
- package/docs/examples/add-marker-with-panel.mdx +4 -0
- package/docs/examples/add-polygons.mdx +16 -12
- package/docs/examples/add-symbols.mdx +4 -0
- package/docs/examples/basic-map.mdx +4 -0
- package/docs/examples/button-map.mdx +4 -0
- package/docs/examples/draw-tools.mdx +4 -0
- package/docs/examples/place-marker.mdx +4 -0
- package/docs/examples/search-control.mdx +4 -0
- package/docs/examples/select-feature.mdx +4 -0
- package/docs/examples/style-switcher.mdx +36 -44
- package/docs/examples/toggle-marker-label.mdx +4 -0
- package/docs/plugins/draw.md +2 -2
- package/docs/plugins/plugin-manifest.md +9 -0
- package/package.json +4 -2
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/api/newPolygon.js +1 -0
- package/plugins/beta/draw-es/src/events.js +17 -8
- package/plugins/beta/draw-es/src/reducer.js +8 -1
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ol/dist/css/index.css +1 -13
- package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
- package/plugins/beta/frame/dist/css/index.css +1 -11
- package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/src/Frame.jsx +28 -2
- package/plugins/beta/map-styles/dist/css/index.css +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/index.js +1 -1
- package/plugins/beta/scale-bar/dist/css/index.css +1 -8
- package/plugins/datasets/dist/css/5648.index.css +1 -5
- package/plugins/datasets/dist/css/index.css +1 -1
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/src/components/LayersMenu/Layers.module.scss +1 -0
- package/plugins/datasets/src/initialise/DatasetsInit.jsx +2 -2
- package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +6 -5
- package/plugins/datasets/src/registry/dataset.js +8 -12
- package/plugins/datasets/src/registry/dataset.test.js +17 -0
- package/plugins/datasets/src/registry/datasetRegistry.js +4 -5
- package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
- package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/esm/index.js +1 -1
- package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/umd/index.js +1 -1
- package/plugins/draw/src/DrawInit.jsx +41 -26
- package/plugins/draw/src/DrawInit.test.jsx +55 -12
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +113 -114
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +82 -9
- package/plugins/draw/src/adapters/maplibre/drawEvents.js +3 -1
- package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +18 -9
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +9 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +11 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +23 -2
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +53 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +29 -19
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +17 -2
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +35 -29
- package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +24 -6
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +22 -20
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +2 -2
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +3 -3
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +31 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +8 -1
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +38 -30
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +20 -3
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +5 -5
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +2 -2
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +2 -2
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +3 -3
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -7
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +4 -4
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +14 -14
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +12 -32
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +10 -10
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +48 -22
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +74 -6
- package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +2 -2
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +6 -0
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +9 -0
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +15 -2
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +27 -1
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +1 -1
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +40 -17
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +55 -1
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +13 -7
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +7 -15
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +36 -4
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +41 -1
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +35 -25
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +40 -2
- package/plugins/draw/src/adapters/openlayers/edit/nudge.js +2 -2
- package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +2 -2
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +40 -54
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +32 -13
- package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +1 -1
- package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +17 -1
- package/plugins/draw/src/events.js +4 -4
- package/plugins/draw/src/events.test.js +1 -1
- package/plugins/draw/src/hooks/useSpatialList.js +180 -0
- package/plugins/draw/src/hooks/useSpatialList.test.js +339 -0
- package/plugins/draw/src/manifest.js +4 -1
- package/plugins/draw/src/manifest.test.js +19 -7
- package/plugins/draw/src/utils/spatial.js +1 -31
- package/plugins/draw/src/utils/spatial.test.js +1 -24
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/index.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +2 -2
- package/plugins/interact/src/InteractInit.test.js +3 -3
- package/plugins/interact/src/events.js +11 -2
- package/plugins/interact/src/events.test.js +32 -0
- package/plugins/interact/src/hooks/useAttachEvents.js +20 -0
- package/plugins/interact/src/hooks/useAttachEvents.test.js +44 -1
- package/plugins/interact/src/hooks/useCrossHairVisibility.js +9 -4
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +7 -7
- package/plugins/interact/src/hooks/useSpatialList.js +251 -0
- package/plugins/interact/src/hooks/{useMapItemList.test.js → useSpatialList.test.js} +216 -70
- package/plugins/interact/src/manifest.js +12 -1
- package/plugins/interact/src/manifest.test.js +20 -0
- package/plugins/interact/src/utils/spatial.js +24 -1
- package/plugins/interact/src/utils/spatial.test.js +26 -1
- package/plugins/map-key/dist/css/index.css +1 -106
- package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -1
- package/plugins/map-key/dist/umd/im-map-key-plugin.js +1 -1
- package/plugins/map-key/dist/umd/index.js +1 -1
- package/plugins/map-key/src/components/Key/Key.jsx +3 -4
- package/plugins/map-key/src/components/Key/Key.module.scss +10 -8
- package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +2 -2
- package/plugins/map-key/src/components/Key/KeyItem.jsx +2 -2
- package/plugins/map-key/src/components/Key/KeySvg.jsx +9 -7
- package/plugins/map-key/src/components/Key/KeySvg.test.jsx +4 -4
- package/plugins/map-key/src/components/Key/MapKey.jsx +5 -3
- package/plugins/map-key/src/manifest.js +1 -1
- package/plugins/map-key/src/utils/groupStyles.js +7 -0
- package/plugins/map-key/src/utils/mergeKeyGroupItems.js +12 -0
- package/plugins/map-key/src/utils/mergeKeyGroupItems.test.js +53 -0
- package/plugins/menu/dist/css/index.css +1 -80
- package/plugins/menu/dist/esm/im-menu-plugin.js +1 -1
- package/plugins/menu/dist/umd/im-menu-plugin.js +1 -1
- package/plugins/menu/dist/umd/index.js +1 -1
- package/plugins/menu/src/initialise/MenuInit.jsx +15 -3
- package/plugins/menu/src/initialise/MenuInit.test.jsx +25 -0
- package/plugins/menu/src/initialise/createMenu.js +3 -1
- package/plugins/menu/src/initialise/createMenu.test.js +20 -5
- package/plugins/menu/src/initialise/updateUrl.js +44 -0
- package/plugins/menu/src/initialise/updateUrl.test.js +91 -0
- package/plugins/menu/src/reducers/menuStateReducer.js +19 -1
- package/plugins/menu/src/reducers/menuStateReducer.test.js +53 -20
- package/plugins/menu/src/reducers/pluginState.js +1 -6
- package/plugins/menu/src/reducers/pluginState.test.js +0 -25
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/src/components/Form/Form.jsx +3 -3
- package/plugins/search/src/components/Suggestions/Suggestions.jsx +2 -2
- package/plugins/search/src/components/Suggestions/Suggestions.test.jsx +1 -1
- package/plugins/search/src/manifest.js +5 -9
- package/plugins/search/src/manifest.test.js +5 -0
- package/providers/beta/esri/dist/css/index.css +1 -34
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/utils/spatial.js +15 -13
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/src/openlayersProvider.js +3 -3
- package/providers/beta/openlayers/src/openlayersProvider.test.js +6 -3
- package/providers/beta/openlayers/src/utils/queryFeatures.js +72 -0
- package/providers/beta/openlayers/src/utils/queryFeatures.test.js +122 -1
- package/providers/beta/openlayers/src/utils/spatial.js +16 -14
- package/providers/beta/openlayers/src/utils/spatial.test.js +10 -6
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/defaults.test.js +26 -0
- package/providers/maplibre/src/maplibreProvider.js +1 -1
- package/providers/maplibre/src/utils/labels.js +2 -2
- package/providers/maplibre/src/utils/labels.test.js +5 -5
- package/providers/maplibre/src/utils/spatial.js +19 -74
- package/providers/maplibre/src/utils/spatial.test.js +8 -32
- package/rollup.esm.mjs +2 -1
- package/scripts/publish-package.sh +36 -0
- package/src/App/App.jsx +1 -1
- package/src/App/App.test.jsx +6 -1
- package/src/App/components/CrossHair/CrossHair.jsx +25 -11
- package/src/App/components/CrossHair/CrossHair.module.scss +37 -12
- package/src/App/components/CrossHair/CrossHair.test.jsx +40 -11
- package/src/App/components/Hints/Hints.jsx +3 -3
- package/src/App/components/Hints/Hints.test.jsx +3 -3
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +4 -1
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +17 -0
- package/src/App/components/MapButton/MapButton.jsx +19 -7
- package/src/App/components/MapButton/MapButton.module.scss +1 -2
- package/src/App/components/MapButton/MapButton.test.jsx +42 -4
- package/src/App/components/{MoveControls/MoveControls.jsx → MapControls/MapControls.jsx} +10 -10
- package/src/App/components/{MoveControls/MoveControls.module.scss → MapControls/MapControls.module.scss} +25 -25
- package/src/App/components/{MoveControls/MoveControls.test.jsx → MapControls/MapControls.test.jsx} +48 -48
- package/src/App/components/Markers/Markers.jsx +2 -2
- package/src/App/components/Markers/Markers.test.jsx +3 -3
- package/src/App/components/Markers/SymbolMarker.jsx +2 -3
- package/src/App/components/Markers/SymbolMarker.test.jsx +8 -9
- package/src/App/components/Panel/Panel.jsx +19 -20
- package/src/App/components/Panel/Panel.module.scss +9 -0
- package/src/App/components/Panel/Panel.test.jsx +69 -4
- package/src/App/components/PopupMenu/PopupMenu.jsx +7 -3
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +71 -0
- package/src/App/components/PopupMenu/usePopupMenu.js +10 -9
- package/src/App/components/Viewport/MapStatus.jsx +1 -1
- package/src/App/components/Viewport/SpatialList.jsx +47 -0
- package/src/App/components/Viewport/{Features.module.scss → SpatialList.module.scss} +6 -6
- package/src/App/components/Viewport/SpatialList.test.jsx +285 -0
- package/src/App/components/Viewport/Viewport.jsx +21 -11
- package/src/App/components/Viewport/Viewport.test.jsx +18 -2
- package/src/App/controls/keyboardMappings.js +7 -2
- package/src/App/controls/keyboardMappings.test.js +21 -0
- package/src/App/controls/keyboardShortcuts.js +16 -2
- package/src/App/controls/keyboardShortcuts.test.js +21 -0
- package/src/App/hooks/useCrossHairAPI.js +29 -1
- package/src/App/hooks/useCrossHairAPI.test.js +17 -1
- package/src/App/hooks/useHintsAPI.js +5 -5
- package/src/App/hooks/useInterfaceAPI.js +2 -1
- package/src/App/hooks/useInterfaceAPI.test.js +9 -0
- package/src/App/hooks/useKeyboardShortcuts.js +66 -35
- package/src/App/hooks/useKeyboardShortcuts.test.js +52 -0
- package/src/App/hooks/useModalPanelBehaviour.js +48 -34
- package/src/App/hooks/useModalPanelBehaviour.test.js +59 -0
- package/src/App/hooks/useSpatialListFocus.js +296 -0
- package/src/App/hooks/useSpatialListFocus.test.js +957 -0
- package/src/App/hooks/useSpatialListItems.js +51 -0
- package/src/App/hooks/useSpatialListItems.test.js +217 -0
- package/src/App/initialiseApp.js +6 -3
- package/src/App/initialiseApp.test.js +11 -0
- package/src/App/layout/Layout.jsx +4 -2
- package/src/App/layout/Layout.test.jsx +21 -0
- package/src/App/layout/layout.module.scss +7 -1
- package/src/App/registry/keyboardShortcutRegistry.js +2 -2
- package/src/App/registry/keyboardShortcutRegistry.test.js +16 -0
- package/src/App/registry/pluginRegistry.js +13 -0
- package/src/App/registry/pluginRegistry.test.js +15 -0
- package/src/App/registry/spatialListRegistry.js +104 -0
- package/src/App/registry/spatialListRegistry.test.js +218 -0
- package/src/App/renderer/HtmlElementHost.jsx +12 -32
- package/src/App/renderer/mapButtons.js +4 -0
- package/src/App/renderer/mapButtons.test.js +20 -0
- package/src/App/renderer/mapPanels.js +33 -53
- package/src/App/renderer/mapPanels.test.js +29 -4
- package/src/App/renderer/slotHelpers.js +33 -0
- package/src/App/renderer/slotHelpers.test.js +79 -2
- package/src/App/store/AppProvider.jsx +3 -2
- package/src/App/store/ServiceProvider.jsx +5 -3
- package/src/App/store/ServiceProvider.test.jsx +20 -1
- package/src/InteractiveMap/InteractiveMap.js +13 -2
- package/src/InteractiveMap/InteractiveMap.test.js +33 -0
- package/src/config/appConfig.js +17 -17
- package/src/config/appConfig.test.js +19 -19
- package/src/config/defaults.js +2 -2
- package/src/config/events.js +8 -8
- package/src/scss/main.scss +2 -2
- package/src/scss/tools/_govuk-content.scss +29 -0
- package/src/scss/tools/_index.scss +2 -1
- package/src/test-utils.js +8 -0
- package/src/types.js +53 -11
- package/src/utils/findNearestItemInDirection.js +29 -0
- package/src/utils/findNearestItemInDirection.test.js +67 -0
- package/src/utils/getPanelElementId.js +4 -0
- package/src/utils/getPanelElementId.test.js +11 -0
- package/src/utils/getPanelRole.js +22 -0
- package/src/utils/getPanelRole.test.js +56 -0
- package/src/utils/globalAltShortcuts.js +12 -0
- package/src/utils/globalAltShortcuts.test.js +27 -0
- package/src/utils/spatialNavigate.js +49 -0
- package/src/utils/spatialNavigate.test.js +42 -0
- package/webpack.umd.mjs +6 -1
- package/plugins/interact/src/hooks/useMapItemList.js +0 -187
- package/src/App/components/Viewport/Features.jsx +0 -35
- package/src/App/components/Viewport/Features.test.jsx +0 -124
- package/src/App/hooks/useFeatureFocus.js +0 -199
- package/src/App/hooks/useFeatureFocus.test.js +0 -635
- package/src/App/hooks/useFeatureItems.js +0 -39
- package/src/App/hooks/useFeatureItems.test.js +0 -155
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
const SET_SPATIAL_LIST = 'map:setspatiallist'
|
|
4
|
+
const SET_SPATIAL_LIST_SUPPRESSED = 'map:setspatiallistsuppressed'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Subscribes to map:setspatiallist for the listbox item list, and
|
|
8
|
+
* map:setspatiallistsuppressed (any plugin can emit this) to report items as empty while
|
|
9
|
+
* suppressed regardless of what's actually visible — which is what drives <SpatialList>'s own
|
|
10
|
+
* tabIndex/aria-hidden.
|
|
11
|
+
*
|
|
12
|
+
* Items always carry at least { id, label, x?, y? } — SpatialList.jsx/useSpatialListFocus.js
|
|
13
|
+
* only ever read those. Whichever plugin currently owns the list (via spatialListRegistry —
|
|
14
|
+
* interact today, draw's edit-mode vertices in future) may include further fields of its own
|
|
15
|
+
* on each item; those simply ride along unused here. `label` is that same owner's own name for
|
|
16
|
+
* what the list currently represents (e.g. "Map features", or "Shape points" while draw holds
|
|
17
|
+
* an exclusive claim) — see spatialListRegistry.js.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} eventBus
|
|
20
|
+
* @returns {{ items: Array<{ id: string, label: string, x?: number, y?: number }>, multiselectable: boolean, label: string|undefined, focusable: boolean }}
|
|
21
|
+
*/
|
|
22
|
+
export function useSpatialListItems (eventBus) {
|
|
23
|
+
const [items, setItems] = useState([])
|
|
24
|
+
const [multiselectable, setMultiselectable] = useState(false)
|
|
25
|
+
const [label, setLabel] = useState(undefined)
|
|
26
|
+
const [focusable, setFocusable] = useState(true)
|
|
27
|
+
const [suppressed, setSuppressed] = useState(false)
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!eventBus) {
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
const handleSetSpatialList = ({ items: next = [], multiselectable: nextMultiselectable = false, label: nextLabel, focusable: nextFocusable = true } = {}) => {
|
|
34
|
+
setItems(next)
|
|
35
|
+
setMultiselectable(nextMultiselectable)
|
|
36
|
+
setLabel(nextLabel)
|
|
37
|
+
setFocusable(nextFocusable)
|
|
38
|
+
}
|
|
39
|
+
const handleSuppressed = ({ suppressed: next = false } = {}) => {
|
|
40
|
+
setSuppressed(next)
|
|
41
|
+
}
|
|
42
|
+
eventBus.on(SET_SPATIAL_LIST, handleSetSpatialList)
|
|
43
|
+
eventBus.on(SET_SPATIAL_LIST_SUPPRESSED, handleSuppressed)
|
|
44
|
+
return () => {
|
|
45
|
+
eventBus.off(SET_SPATIAL_LIST, handleSetSpatialList)
|
|
46
|
+
eventBus.off(SET_SPATIAL_LIST_SUPPRESSED, handleSuppressed)
|
|
47
|
+
}
|
|
48
|
+
}, [eventBus])
|
|
49
|
+
|
|
50
|
+
return { items: suppressed ? [] : items, multiselectable, label, focusable }
|
|
51
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react'
|
|
2
|
+
import { useSpatialListItems } from './useSpatialListItems.js'
|
|
3
|
+
|
|
4
|
+
const SET_FEATURES = 'map:setspatiallist' // NOSONAR
|
|
5
|
+
const SET_FEATURES_SUPPRESSED = 'map:setspatiallistsuppressed' // NOSONAR
|
|
6
|
+
|
|
7
|
+
const makeEventBus = () => {
|
|
8
|
+
const listeners = {}
|
|
9
|
+
return {
|
|
10
|
+
on: jest.fn((eventName, fn) => { listeners[eventName] = fn }),
|
|
11
|
+
off: jest.fn(),
|
|
12
|
+
emit: (eventName, payload) => listeners[eventName]?.(payload)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ─── useSpatialListItems — initial state ─────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
describe('useSpatialListItems — initial state', () => {
|
|
19
|
+
it('returns empty items and multiselectable false before any event', () => {
|
|
20
|
+
const { result } = renderHook(() => useSpatialListItems(makeEventBus()))
|
|
21
|
+
expect(result.current.items).toEqual([])
|
|
22
|
+
expect(result.current.multiselectable).toBe(false)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('returns empty items and multiselectable false when eventBus is undefined', () => {
|
|
26
|
+
const { result } = renderHook(() => useSpatialListItems(undefined))
|
|
27
|
+
expect(result.current.items).toEqual([])
|
|
28
|
+
expect(result.current.multiselectable).toBe(false)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// ─── useSpatialListItems — event subscription ────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
describe('useSpatialListItems — event subscription', () => {
|
|
35
|
+
it('subscribes to map:setfeatures on mount', () => {
|
|
36
|
+
const eb = makeEventBus()
|
|
37
|
+
renderHook(() => useSpatialListItems(eb))
|
|
38
|
+
expect(eb.on).toHaveBeenCalledWith(SET_FEATURES, expect.any(Function))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('subscribes to map:setfeaturessuppressed on mount', () => {
|
|
42
|
+
const eb = makeEventBus()
|
|
43
|
+
renderHook(() => useSpatialListItems(eb))
|
|
44
|
+
expect(eb.on).toHaveBeenCalledWith(SET_FEATURES_SUPPRESSED, expect.any(Function))
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('unsubscribes on unmount', () => {
|
|
48
|
+
const eb = makeEventBus()
|
|
49
|
+
const { unmount } = renderHook(() => useSpatialListItems(eb))
|
|
50
|
+
unmount()
|
|
51
|
+
expect(eb.off).toHaveBeenCalledWith(SET_FEATURES, expect.any(Function))
|
|
52
|
+
expect(eb.off).toHaveBeenCalledWith(SET_FEATURES_SUPPRESSED, expect.any(Function))
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('does not subscribe when eventBus is undefined', () => {
|
|
56
|
+
const spy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
57
|
+
renderHook(() => useSpatialListItems(undefined))
|
|
58
|
+
expect(spy).not.toHaveBeenCalled()
|
|
59
|
+
spy.mockRestore()
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// ─── useSpatialListItems — items updates ─────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
describe('useSpatialListItems — items updates', () => {
|
|
66
|
+
it('updates items when map:setfeatures is emitted', () => {
|
|
67
|
+
const eb = makeEventBus()
|
|
68
|
+
const { result } = renderHook(() => useSpatialListItems(eb))
|
|
69
|
+
const items = [{ id: 'a', label: 'Feature A' }, { id: 'b', label: 'Feature B' }]
|
|
70
|
+
act(() => eb.emit(SET_FEATURES, { items }))
|
|
71
|
+
expect(result.current.items).toEqual(items)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('clears items when emitted with an empty array', () => {
|
|
75
|
+
const eb = makeEventBus()
|
|
76
|
+
const { result } = renderHook(() => useSpatialListItems(eb))
|
|
77
|
+
act(() => eb.emit(SET_FEATURES, { items: [{ id: 'a', label: 'A' }] }))
|
|
78
|
+
act(() => eb.emit(SET_FEATURES, { items: [] }))
|
|
79
|
+
expect(result.current.items).toEqual([])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('defaults items to empty array when items key is missing from payload', () => {
|
|
83
|
+
const eb = makeEventBus()
|
|
84
|
+
const { result } = renderHook(() => useSpatialListItems(eb))
|
|
85
|
+
act(() => eb.emit(SET_FEATURES, {}))
|
|
86
|
+
expect(result.current.items).toEqual([])
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('defaults items to empty array when the event carries no payload at all', () => {
|
|
90
|
+
const eb = makeEventBus()
|
|
91
|
+
const { result } = renderHook(() => useSpatialListItems(eb))
|
|
92
|
+
act(() => eb.emit(SET_FEATURES))
|
|
93
|
+
expect(result.current.items).toEqual([])
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// ─── useSpatialListItems — label updates ─────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
describe('useSpatialListItems — label updates', () => {
|
|
100
|
+
it('returns undefined label before any event', () => {
|
|
101
|
+
const { result } = renderHook(() => useSpatialListItems(makeEventBus()))
|
|
102
|
+
expect(result.current.label).toBeUndefined()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('returns whatever label the current provider declared', () => {
|
|
106
|
+
const eb = makeEventBus()
|
|
107
|
+
const { result } = renderHook(() => useSpatialListItems(eb))
|
|
108
|
+
act(() => eb.emit(SET_FEATURES, { items: [], label: 'Shape points' }))
|
|
109
|
+
expect(result.current.label).toBe('Shape points')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('updates the label when a later event declares a different one', () => {
|
|
113
|
+
const eb = makeEventBus()
|
|
114
|
+
const { result } = renderHook(() => useSpatialListItems(eb))
|
|
115
|
+
act(() => eb.emit(SET_FEATURES, { items: [], label: 'Map features' }))
|
|
116
|
+
act(() => eb.emit(SET_FEATURES, { items: [], label: 'Shape points' }))
|
|
117
|
+
expect(result.current.label).toBe('Shape points')
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// ─── useSpatialListItems — focusable updates ─────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
describe('useSpatialListItems — focusable updates', () => {
|
|
124
|
+
it('defaults to focusable: true before any event', () => {
|
|
125
|
+
const { result } = renderHook(() => useSpatialListItems(makeEventBus()))
|
|
126
|
+
expect(result.current.focusable).toBe(true)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('defaults to focusable: true when the event omits it', () => {
|
|
130
|
+
const eb = makeEventBus()
|
|
131
|
+
const { result } = renderHook(() => useSpatialListItems(eb))
|
|
132
|
+
act(() => eb.emit(SET_FEATURES, { items: [] }))
|
|
133
|
+
expect(result.current.focusable).toBe(true)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('reflects an explicit focusable: false from the current provider', () => {
|
|
137
|
+
const eb = makeEventBus()
|
|
138
|
+
const { result } = renderHook(() => useSpatialListItems(eb))
|
|
139
|
+
act(() => eb.emit(SET_FEATURES, { items: [], focusable: false }))
|
|
140
|
+
expect(result.current.focusable).toBe(false)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('reverts to true once a later event omits it again (e.g. draw released its claim)', () => {
|
|
144
|
+
const eb = makeEventBus()
|
|
145
|
+
const { result } = renderHook(() => useSpatialListItems(eb))
|
|
146
|
+
act(() => eb.emit(SET_FEATURES, { items: [], focusable: false }))
|
|
147
|
+
act(() => eb.emit(SET_FEATURES, { items: [] }))
|
|
148
|
+
expect(result.current.focusable).toBe(true)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// ─── useSpatialListItems — multiselectable updates ───────────────────────────────
|
|
153
|
+
|
|
154
|
+
describe('useSpatialListItems — multiselectable updates', () => {
|
|
155
|
+
it('sets multiselectable true when emitted with multiselectable: true', () => {
|
|
156
|
+
const eb = makeEventBus()
|
|
157
|
+
const { result } = renderHook(() => useSpatialListItems(eb))
|
|
158
|
+
act(() => eb.emit(SET_FEATURES, { items: [], multiselectable: true }))
|
|
159
|
+
expect(result.current.multiselectable).toBe(true)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('defaults multiselectable to false when not present in payload', () => {
|
|
163
|
+
const eb = makeEventBus()
|
|
164
|
+
const { result } = renderHook(() => useSpatialListItems(eb))
|
|
165
|
+
act(() => eb.emit(SET_FEATURES, { items: [] }))
|
|
166
|
+
expect(result.current.multiselectable).toBe(false)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// ─── useSpatialListItems — suppression ───────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
describe('useSpatialListItems — suppression', () => {
|
|
173
|
+
const items = [{ id: 'a', label: 'Feature A' }]
|
|
174
|
+
|
|
175
|
+
it('reports empty items while suppressed, regardless of what was last set', () => {
|
|
176
|
+
const eb = makeEventBus()
|
|
177
|
+
const { result } = renderHook(() => useSpatialListItems(eb))
|
|
178
|
+
act(() => eb.emit(SET_FEATURES, { items }))
|
|
179
|
+
act(() => eb.emit(SET_FEATURES_SUPPRESSED, { suppressed: true }))
|
|
180
|
+
expect(result.current.items).toEqual([])
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('restores the last known items once suppression is lifted', () => {
|
|
184
|
+
const eb = makeEventBus()
|
|
185
|
+
const { result } = renderHook(() => useSpatialListItems(eb))
|
|
186
|
+
act(() => eb.emit(SET_FEATURES, { items }))
|
|
187
|
+
act(() => eb.emit(SET_FEATURES_SUPPRESSED, { suppressed: true }))
|
|
188
|
+
act(() => eb.emit(SET_FEATURES_SUPPRESSED, { suppressed: false }))
|
|
189
|
+
expect(result.current.items).toEqual(items)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('defaults suppressed to false when the payload omits it', () => {
|
|
193
|
+
const eb = makeEventBus()
|
|
194
|
+
const { result } = renderHook(() => useSpatialListItems(eb))
|
|
195
|
+
act(() => eb.emit(SET_FEATURES, { items }))
|
|
196
|
+
act(() => eb.emit(SET_FEATURES_SUPPRESSED, {}))
|
|
197
|
+
expect(result.current.items).toEqual(items)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('defaults suppressed to false when the event carries no payload at all', () => {
|
|
201
|
+
const eb = makeEventBus()
|
|
202
|
+
const { result } = renderHook(() => useSpatialListItems(eb))
|
|
203
|
+
act(() => eb.emit(SET_FEATURES, { items }))
|
|
204
|
+
act(() => eb.emit(SET_FEATURES_SUPPRESSED))
|
|
205
|
+
expect(result.current.items).toEqual(items)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('a features update received while suppressed is still applied once restored', () => {
|
|
209
|
+
const eb = makeEventBus()
|
|
210
|
+
const { result } = renderHook(() => useSpatialListItems(eb))
|
|
211
|
+
act(() => eb.emit(SET_FEATURES_SUPPRESSED, { suppressed: true }))
|
|
212
|
+
act(() => eb.emit(SET_FEATURES, { items }))
|
|
213
|
+
expect(result.current.items).toEqual([]) // still suppressed
|
|
214
|
+
act(() => eb.emit(SET_FEATURES_SUPPRESSED, { suppressed: false }))
|
|
215
|
+
expect(result.current.items).toEqual(items)
|
|
216
|
+
})
|
|
217
|
+
})
|
package/src/App/initialiseApp.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createPanelRegistry } from './registry/panelRegistry.js'
|
|
|
6
6
|
import { createControlRegistry } from './registry/controlRegistry.js'
|
|
7
7
|
import { createPluginRegistry } from './registry/pluginRegistry.js'
|
|
8
8
|
import { createKeyboardShortcutRegistry } from './registry/keyboardShortcutRegistry.js'
|
|
9
|
+
import { createSpatialListRegistry } from './registry/spatialListRegistry.js'
|
|
9
10
|
import { mergeManifests } from './registry/mergeManifests.js'
|
|
10
11
|
import { App } from './App.jsx'
|
|
11
12
|
|
|
@@ -13,13 +14,14 @@ const rootMap = new WeakMap()
|
|
|
13
14
|
const mapProviderMap = new WeakMap()
|
|
14
15
|
const registryMap = new WeakMap()
|
|
15
16
|
|
|
16
|
-
const getOrCreateRegistries = (rootElement) => {
|
|
17
|
+
const getOrCreateRegistries = (rootElement, eventBus) => {
|
|
17
18
|
let registries = registryMap.get(rootElement)
|
|
18
19
|
if (!registries) {
|
|
19
20
|
const buttonRegistry = createButtonRegistry()
|
|
20
21
|
const panelRegistry = createPanelRegistry()
|
|
21
22
|
const controlRegistry = createControlRegistry()
|
|
22
23
|
const keyboardShortcutRegistry = createKeyboardShortcutRegistry()
|
|
24
|
+
const spatialListRegistry = createSpatialListRegistry({ eventBus })
|
|
23
25
|
const pluginRegistry = createPluginRegistry({
|
|
24
26
|
registerButton: buttonRegistry.registerButton,
|
|
25
27
|
registerPanel: panelRegistry.registerPanel,
|
|
@@ -27,7 +29,7 @@ const getOrCreateRegistries = (rootElement) => {
|
|
|
27
29
|
registerKeyboardShortcut: keyboardShortcutRegistry.registerKeyboardShortcut
|
|
28
30
|
})
|
|
29
31
|
|
|
30
|
-
registries = { buttonRegistry, panelRegistry, controlRegistry, pluginRegistry, keyboardShortcutRegistry }
|
|
32
|
+
registries = { buttonRegistry, panelRegistry, controlRegistry, pluginRegistry, keyboardShortcutRegistry, spatialListRegistry }
|
|
31
33
|
registryMap.set(rootElement, registries)
|
|
32
34
|
}
|
|
33
35
|
return registries
|
|
@@ -73,7 +75,7 @@ export async function initialiseApp (rootElement, {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
// Reuse or create registries (persist across app open/close cycles)
|
|
76
|
-
const { buttonRegistry, panelRegistry, controlRegistry, pluginRegistry, keyboardShortcutRegistry } = getOrCreateRegistries(rootElement)
|
|
78
|
+
const { buttonRegistry, panelRegistry, controlRegistry, pluginRegistry, keyboardShortcutRegistry, spatialListRegistry } = getOrCreateRegistries(rootElement, eventBus)
|
|
77
79
|
const { registerPlugin } = pluginRegistry
|
|
78
80
|
|
|
79
81
|
// Register provider-supported shortcuts
|
|
@@ -124,6 +126,7 @@ export async function initialiseApp (rootElement, {
|
|
|
124
126
|
controlRegistry={controlRegistry}
|
|
125
127
|
pluginRegistry={pluginRegistry}
|
|
126
128
|
keyboardShortcutRegistry={keyboardShortcutRegistry}
|
|
129
|
+
spatialListRegistry={spatialListRegistry}
|
|
127
130
|
mapProvider={mapProvider}
|
|
128
131
|
/>)
|
|
129
132
|
|
|
@@ -50,6 +50,10 @@ jest.mock('./registry/keyboardShortcutRegistry.js', () => ({
|
|
|
50
50
|
createKeyboardShortcutRegistry: jest.fn(() => mockRegistries.keyboardShortcutRegistry)
|
|
51
51
|
}))
|
|
52
52
|
|
|
53
|
+
jest.mock('./registry/spatialListRegistry.js', () => ({
|
|
54
|
+
createSpatialListRegistry: jest.fn(() => mockRegistries.spatialListRegistry)
|
|
55
|
+
}))
|
|
56
|
+
|
|
53
57
|
jest.mock('./registry/mergeManifests.js', () => ({
|
|
54
58
|
mergeManifests: jest.fn((base, override) => ({ ...base, ...override }))
|
|
55
59
|
}))
|
|
@@ -193,4 +197,11 @@ describe('initialiseApp', () => {
|
|
|
193
197
|
const rendered = createRoot.mock.results[0].value.render.mock.calls[0][0]
|
|
194
198
|
expect(rendered.props.keyboardShortcutRegistry).toBe(mockRegistries.keyboardShortcutRegistry)
|
|
195
199
|
})
|
|
200
|
+
|
|
201
|
+
test('passes spatialListRegistry through to the rendered App', async () => {
|
|
202
|
+
await initApp()
|
|
203
|
+
|
|
204
|
+
const rendered = createRoot.mock.results[0].value.render.mock.calls[0][0]
|
|
205
|
+
expect(rendered.props.spatialListRegistry).toBe(mockRegistries.spatialListRegistry)
|
|
206
|
+
})
|
|
196
207
|
})
|
|
@@ -11,14 +11,16 @@ import { layoutSlots } from '../renderer/slots'
|
|
|
11
11
|
import { SlotRenderer } from '../renderer/SlotRenderer'
|
|
12
12
|
import { HtmlElementHost } from '../renderer/HtmlElementHost'
|
|
13
13
|
import { Hints } from '../components/Hints/Hints.jsx'
|
|
14
|
+
import { hasOpenModalPanel } from '../renderer/slotHelpers.js'
|
|
14
15
|
import { getMapThemeVars } from '../../config/mapTheme.js'
|
|
15
16
|
|
|
16
17
|
// eslint-disable-next-line camelcase, react/jsx-pascal-case
|
|
17
18
|
// sonarjs/disable-next-line function-name
|
|
18
19
|
export const Layout = () => {
|
|
19
20
|
const { id, mapLabel, mapHintText } = useConfig()
|
|
20
|
-
const { breakpoint, interfaceType, preferredColorScheme, layoutRefs, isLayoutReady, hasExclusiveControl, isFullscreen } = useApp()
|
|
21
|
+
const { breakpoint, interfaceType, preferredColorScheme, layoutRefs, isLayoutReady, hasExclusiveControl, isFullscreen, openPanels, panelConfig } = useApp()
|
|
21
22
|
const { mapStyle } = useMap()
|
|
23
|
+
const showModalBackdrop = hasOpenModalPanel(openPanels ?? {}, panelConfig ?? {}, breakpoint)
|
|
22
24
|
|
|
23
25
|
useLayoutMeasurements()
|
|
24
26
|
useFocusVisible()
|
|
@@ -108,7 +110,7 @@ export const Layout = () => {
|
|
|
108
110
|
</div>
|
|
109
111
|
<div className='im-o-app__modal' ref={layoutRefs.modalRef}>
|
|
110
112
|
<SlotRenderer slot={layoutSlots.MODAL} />
|
|
111
|
-
<div className='im-o-app__modal-backdrop' />
|
|
113
|
+
<div className={`im-o-app__modal-backdrop${showModalBackdrop ? ' im-o-app__modal-backdrop--visible' : ''}`} />
|
|
112
114
|
</div>
|
|
113
115
|
</div>
|
|
114
116
|
</div>
|
|
@@ -95,6 +95,27 @@ describe('Layout', () => {
|
|
|
95
95
|
expect(screen.getByTestId('slot-top-left')).toBeInTheDocument()
|
|
96
96
|
expect(screen.getByTestId('slot-bottom-right')).toBeInTheDocument()
|
|
97
97
|
expect(screen.getByTestId('slot-modal')).toBeInTheDocument()
|
|
98
|
+
|
|
99
|
+
const backdrop = root.querySelector('.im-o-app__modal-backdrop')
|
|
100
|
+
expect(backdrop).toBeInTheDocument()
|
|
101
|
+
expect(backdrop).not.toHaveClass('im-o-app__modal-backdrop--visible')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('shows the modal backdrop only while a modal-configured panel is open', () => {
|
|
105
|
+
useApp.mockReturnValue({
|
|
106
|
+
breakpoint: 'mobile',
|
|
107
|
+
interfaceType: 'map',
|
|
108
|
+
preferredColorScheme: 'dark',
|
|
109
|
+
layoutRefs: mockRefs,
|
|
110
|
+
isLayoutReady: true,
|
|
111
|
+
hasExclusiveControl: false,
|
|
112
|
+
isFullscreen: true,
|
|
113
|
+
openPanels: { settings: { props: {} } },
|
|
114
|
+
panelConfig: { settings: { mobile: { modal: true } } }
|
|
115
|
+
})
|
|
116
|
+
render(<Layout />)
|
|
117
|
+
const root = document.getElementById('myApp-im-app')
|
|
118
|
+
expect(root.querySelector('.im-o-app__modal-backdrop')).toHaveClass('im-o-app__modal-backdrop--visible')
|
|
98
119
|
})
|
|
99
120
|
|
|
100
121
|
test('applies "not-ready" class when layout is not ready', () => {
|
|
@@ -363,7 +363,13 @@
|
|
|
363
363
|
z-index: 1000;
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
-
|
|
366
|
+
// Visibility is JS-driven (Layout.jsx) since multiple panels can share this slot.
|
|
367
|
+
.im-o-app__modal-backdrop {
|
|
368
|
+
display: none;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.im-o-app__modal-backdrop--visible {
|
|
372
|
+
display: block;
|
|
367
373
|
position: absolute;
|
|
368
374
|
top: 0;
|
|
369
375
|
left: 0;
|
|
@@ -19,8 +19,8 @@ const _setProviderSupportedShortcuts = (state, ids = []) => {
|
|
|
19
19
|
|
|
20
20
|
const _getKeyboardShortcuts = (state, appConfig = {}) => {
|
|
21
21
|
const filteredCore = coreShortcuts.filter(s => {
|
|
22
|
-
// Must be supported by provider
|
|
23
|
-
if (!state.providerSupportedIds.has(s.id)) {
|
|
22
|
+
// Must be supported by the map provider, unless the shortcut is map-provider-agnostic (no capability dependency, so no adapter needs to declare it).
|
|
23
|
+
if (!s.mapProviderAgnostic && !state.providerSupportedIds.has(s.id)) {
|
|
24
24
|
return false
|
|
25
25
|
}
|
|
26
26
|
// Check requiredConfig - all specified config values must be truthy
|
|
@@ -93,6 +93,22 @@ describe('createKeyboardShortcutRegistry', () => {
|
|
|
93
93
|
])
|
|
94
94
|
})
|
|
95
95
|
|
|
96
|
+
test('a mapProviderAgnostic core shortcut is included even when no provider declares support for it', () => {
|
|
97
|
+
jest.resetModules()
|
|
98
|
+
jest.doMock('../controls/keyboardShortcuts.js', () => ({
|
|
99
|
+
coreShortcuts: [
|
|
100
|
+
{ id: 'ordinary', description: 'Ordinary' },
|
|
101
|
+
{ id: 'agnostic', description: 'Agnostic', mapProviderAgnostic: true }
|
|
102
|
+
]
|
|
103
|
+
}))
|
|
104
|
+
const registry = require('./keyboardShortcutRegistry.js').createKeyboardShortcutRegistry()
|
|
105
|
+
registry.setProviderSupportedShortcuts([]) // no provider support declared at all
|
|
106
|
+
|
|
107
|
+
expect(registry.getKeyboardShortcuts()).toEqual([
|
|
108
|
+
{ id: 'agnostic', description: 'Agnostic', mapProviderAgnostic: true }
|
|
109
|
+
])
|
|
110
|
+
})
|
|
111
|
+
|
|
96
112
|
test('two registry instances are fully isolated from each other', () => {
|
|
97
113
|
// This is the multi-map-on-a-page scenario: a shortcut registered by one map
|
|
98
114
|
// (e.g. a plugin) must never leak into another map's help panel, and each map's
|
|
@@ -73,9 +73,22 @@ export function createPluginRegistry ({ registerButton, registerPanel, registerC
|
|
|
73
73
|
registeredPlugins.length = 0
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Look up another registered plugin's instance by id. Returns `undefined`
|
|
78
|
+
* if not registered; found doesn't mean ready to call — see services.getPlugin
|
|
79
|
+
* in docs/api/context.md.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} id - Plugin id, e.g. `'draw'`.
|
|
82
|
+
* @returns {object|undefined}
|
|
83
|
+
*/
|
|
84
|
+
function getPlugin (id) {
|
|
85
|
+
return registeredPlugins.find(plugin => plugin.id === id)?._originalPlugin
|
|
86
|
+
}
|
|
87
|
+
|
|
76
88
|
return {
|
|
77
89
|
registeredPlugins,
|
|
78
90
|
registerPlugin,
|
|
91
|
+
getPlugin,
|
|
79
92
|
clear
|
|
80
93
|
}
|
|
81
94
|
}
|
|
@@ -185,6 +185,21 @@ describe('pluginRegistry', () => {
|
|
|
185
185
|
})
|
|
186
186
|
})
|
|
187
187
|
|
|
188
|
+
describe('getPlugin', () => {
|
|
189
|
+
it('returns the _originalPlugin reference for a registered plugin id', () => {
|
|
190
|
+
const originalPlugin = { newPolygon: jest.fn() }
|
|
191
|
+
const plugin = { id: 'draw', config: {}, manifest: {}, _originalPlugin: originalPlugin }
|
|
192
|
+
|
|
193
|
+
pluginRegistry.registerPlugin(plugin)
|
|
194
|
+
|
|
195
|
+
expect(pluginRegistry.getPlugin('draw')).toBe(originalPlugin)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('returns undefined for an id that is not registered', () => {
|
|
199
|
+
expect(pluginRegistry.getPlugin('does-not-exist')).toBeUndefined()
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
188
203
|
it('clears all registered plugins', () => {
|
|
189
204
|
const pluginA = { id: 'A', config: {}, manifest: {} }
|
|
190
205
|
const pluginB = { id: 'B', config: {}, manifest: {} }
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { EVENTS as events } from '../../config/events.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Central registry for plugins that contribute items to the shared spatial list (see
|
|
5
|
+
* SpatialList.jsx). Plugins register a live item provider — `{ getItems, exclusive? }` —
|
|
6
|
+
* instead of emitting the list event themselves, so exclusive takeover (draw mid-edit) and
|
|
7
|
+
* additive participation (interact's features/markers) both work safely with any number of
|
|
8
|
+
* contributors.
|
|
9
|
+
*
|
|
10
|
+
* `getItems()` returns `{ items, multiselectable, label, focusable }`, called fresh on every
|
|
11
|
+
* recompute rather than snapshotted at registration. `label` becomes the listbox's aria-label;
|
|
12
|
+
* `focusable: false` (draw's vertex/midpoint items) keeps every item out of the Tab order —
|
|
13
|
+
* see SpatialList.jsx. `exclusive: true` claims sole ownership while held (see
|
|
14
|
+
* claimExclusive/releaseExclusive); everything else is additive and gets concatenated
|
|
15
|
+
* together. Providers call notifyItemsChanged whenever their own items would differ — the
|
|
16
|
+
* registry never polls, it only recomputes in response to a caller flagging a change.
|
|
17
|
+
*/
|
|
18
|
+
export function createSpatialListRegistry ({ eventBus }) {
|
|
19
|
+
const providers = new Map() // pluginId -> { getItems, exclusive }
|
|
20
|
+
let exclusiveOwner = null
|
|
21
|
+
|
|
22
|
+
const recompute = () => {
|
|
23
|
+
let items = []
|
|
24
|
+
let multiselectable = false
|
|
25
|
+
let label
|
|
26
|
+
let focusable
|
|
27
|
+
|
|
28
|
+
if (exclusiveOwner && providers.has(exclusiveOwner)) {
|
|
29
|
+
const result = providers.get(exclusiveOwner).getItems() ?? {}
|
|
30
|
+
items = result.items ?? []
|
|
31
|
+
multiselectable = !!result.multiselectable
|
|
32
|
+
label = result.label
|
|
33
|
+
focusable = result.focusable
|
|
34
|
+
} else {
|
|
35
|
+
// Exclusive-capable providers only ever contribute while actually holding the
|
|
36
|
+
// claim (the branch above) — not just whenever nobody happens to hold it.
|
|
37
|
+
providers.forEach((provider) => {
|
|
38
|
+
if (provider.exclusive) { return }
|
|
39
|
+
const result = provider.getItems() ?? {}
|
|
40
|
+
items = items.concat(result.items ?? [])
|
|
41
|
+
multiselectable = multiselectable || !!result.multiselectable
|
|
42
|
+
// First additive provider (in registration order) with a label/focusable value
|
|
43
|
+
// wins — there's no principled way to merge two different values, and today
|
|
44
|
+
// only interact ever contributes additively, so this is just "whatever it
|
|
45
|
+
// declares".
|
|
46
|
+
label = label ?? result.label
|
|
47
|
+
focusable = focusable ?? result.focusable
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
eventBus.emit(events.MAP_SET_SPATIAL_LIST, { items, multiselectable, label, focusable })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function registerItemProvider (pluginId, { getItems, exclusive = false }) {
|
|
55
|
+
providers.set(pluginId, { getItems, exclusive })
|
|
56
|
+
recompute()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function unregisterItemProvider (pluginId) {
|
|
60
|
+
providers.delete(pluginId)
|
|
61
|
+
if (exclusiveOwner === pluginId) {
|
|
62
|
+
exclusiveOwner = null
|
|
63
|
+
}
|
|
64
|
+
recompute()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Mirrors mapProvider.activeMoveTarget's single-claim contract (MapControls.jsx),
|
|
68
|
+
// generalised to however many participants are registered rather than always
|
|
69
|
+
// exactly two — only one exclusive claim can be held at a time, and holding it
|
|
70
|
+
// means every other registered provider is ignored until it's released.
|
|
71
|
+
function claimExclusive (pluginId) {
|
|
72
|
+
exclusiveOwner = pluginId
|
|
73
|
+
recompute()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function releaseExclusive (pluginId) {
|
|
77
|
+
if (exclusiveOwner === pluginId) {
|
|
78
|
+
exclusiveOwner = null
|
|
79
|
+
recompute()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function notifyItemsChanged (pluginId) {
|
|
84
|
+
if (!providers.has(pluginId)) { return }
|
|
85
|
+
// If someone else holds the exclusive claim, this provider isn't feeding the
|
|
86
|
+
// list right now regardless of what changed for it — nothing to recompute.
|
|
87
|
+
if (exclusiveOwner && exclusiveOwner !== pluginId) { return }
|
|
88
|
+
recompute()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function clear () {
|
|
92
|
+
providers.clear()
|
|
93
|
+
exclusiveOwner = null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
registerItemProvider,
|
|
98
|
+
unregisterItemProvider,
|
|
99
|
+
claimExclusive,
|
|
100
|
+
releaseExclusive,
|
|
101
|
+
notifyItemsChanged,
|
|
102
|
+
clear
|
|
103
|
+
}
|
|
104
|
+
}
|