@defra/interactive-map 0.0.39-alpha → 0.0.41-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/control-definition.md +9 -1
- package/docs/api/slots.md +26 -5
- package/docs/api.md +4 -4
- package/docs/assets/images/slot-map.svg +61 -262
- package/docs/examples/draw-tools.mdx +3 -3
- package/docs/plugins/draw.md +80 -17
- package/docs/plugins/search.md +1 -1
- package/govuk-prototype-kit.config.json +2 -0
- package/jest.setup.js +5 -0
- package/package.json +9 -1
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -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/esm/im-draw-ol-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +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/datasets/dist/css/index.css +1 -1
- package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
- package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/index.js +1 -1
- package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +2 -3
- package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
- package/plugins/datasets/src/adapters/esri/registry/esriDataset.js +1 -1
- package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
- package/plugins/datasets/src/adapters/loadLayerAdapter.js +1 -1
- package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
- package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +1 -3
- package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +17 -3
- package/plugins/datasets/src/api/setOpacity.test.js +9 -0
- package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
- package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
- package/plugins/datasets/src/datasets.scss +0 -1
- package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
- package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
- package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
- package/plugins/datasets/src/index.test.js +36 -0
- package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
- package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
- package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -5
- package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
- package/plugins/datasets/src/manifest.js +0 -38
- package/plugins/datasets/src/manifest.test.js +49 -0
- package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
- package/plugins/datasets/src/reducers/pluginState.js +3 -16
- package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
- package/plugins/datasets/src/registry/dataset.js +15 -4
- package/plugins/datasets/src/registry/dataset.test.js +61 -0
- package/plugins/datasets/src/registry/datasetRegistry.js +12 -9
- package/plugins/datasets/src/registry/datasetRegistry.test.js +17 -5
- package/plugins/datasets/src/registry/globalDataset.js +0 -3
- package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
- package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
- package/plugins/datasets/src/utils/bbox.test.js +125 -0
- 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 +2 -1
- package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
- 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 +12 -4
- package/plugins/draw/src/DrawInit.test.jsx +51 -10
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +92 -12
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
- package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
- package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +5 -28
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +4 -54
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +11 -4
- package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
- package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
- package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
- package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
- package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
- package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
- package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
- package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
- package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
- package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
- package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
- package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
- package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
- package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
- package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
- package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
- package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
- package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
- package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
- package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
- package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
- package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
- package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.js +1 -0
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +27 -1
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +24 -4
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +47 -3
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +1 -1
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +32 -1
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +34 -1
- package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
- package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
- package/plugins/draw/src/adapters/openlayers/edit/nudge.js +12 -5
- package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +26 -3
- package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
- package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
- package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
- package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
- package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
- package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
- package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
- package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
- package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
- package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
- package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
- package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
- package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
- package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
- package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
- package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
- package/plugins/draw/src/api/addFeature.js +16 -1
- package/plugins/draw/src/api/addFeature.test.js +62 -5
- package/plugins/draw/src/api/editFeature.js +7 -4
- package/plugins/draw/src/api/editFeature.test.js +16 -0
- package/plugins/draw/src/api/newPoint.js +42 -0
- package/plugins/draw/src/api/newPoint.test.js +117 -0
- package/plugins/draw/src/api/setStyle.js +43 -0
- package/plugins/draw/src/api/setStyle.test.js +75 -0
- package/plugins/draw/src/events.js +21 -14
- package/plugins/draw/src/events.test.js +15 -1
- package/plugins/draw/src/manifest.js +18 -6
- package/plugins/draw/src/manifest.test.js +32 -1
- package/plugins/draw/src/reducer.test.js +7 -0
- package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
- package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
- package/plugins/draw/src/utils/symbolKeys.js +5 -0
- package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
- package/plugins/draw/src/validation/liveStroke.test.js +64 -1
- 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/hooks/useCrossHairVisibility.js +12 -4
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +88 -1
- package/plugins/interact/src/hooks/useHighlightSync.js +33 -12
- package/plugins/interact/src/hooks/useHighlightSync.test.js +96 -25
- package/plugins/interact/src/utils/featureQueries.js +18 -0
- package/plugins/interact/src/utils/featureQueries.test.js +15 -0
- package/plugins/interact/src/utils/spatial.js +9 -3
- package/plugins/interact/src/utils/spatial.test.js +12 -1
- package/plugins/map-key/dist/css/index.css +62 -0
- package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
- package/plugins/map-key/dist/esm/index.js +1 -0
- package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
- package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
- package/plugins/map-key/dist/umd/index.js +2 -0
- package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
- package/plugins/map-key/package.json +28 -0
- package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
- package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
- package/plugins/map-key/src/components/Key/Key.jsx +41 -0
- package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +15 -14
- package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
- package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
- package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
- package/plugins/map-key/src/components/Key/KeyItem.jsx +29 -0
- package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
- package/plugins/map-key/src/components/Key/KeySvg.jsx +42 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
- package/plugins/map-key/src/components/Key/KeySvgLine.jsx +28 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
- package/plugins/map-key/src/components/Key/KeySvgPattern.jsx +23 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +2 -2
- package/plugins/map-key/src/components/Key/KeySvgRect.jsx +33 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
- package/plugins/map-key/src/components/Key/KeySvgSymbol.jsx +27 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +5 -6
- package/plugins/map-key/src/components/Key/MapKey.jsx +50 -0
- package/plugins/map-key/src/components/Key/MapKey.test.jsx +130 -0
- package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
- package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
- package/plugins/map-key/src/index.js +13 -0
- package/plugins/map-key/src/index.test.js +31 -0
- package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
- package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
- package/plugins/map-key/src/manifest.js +30 -0
- package/plugins/map-key/src/manifest.test.js +63 -0
- package/plugins/map-key/src/mapKey.scss +4 -0
- package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
- package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
- package/plugins/map-key/src/registry/index.js +3 -0
- package/plugins/map-key/src/registry/index.test.js +15 -0
- package/plugins/menu/dist/css/index.css +80 -0
- package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
- package/plugins/menu/dist/esm/index.js +1 -0
- package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
- package/plugins/menu/dist/umd/index.js +2 -0
- package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
- package/plugins/menu/package.json +28 -0
- package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
- package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
- package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
- package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
- package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
- package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
- package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
- package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
- package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
- package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
- package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
- package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
- package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
- package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
- package/plugins/menu/src/index.js +13 -0
- package/plugins/menu/src/index.test.js +31 -0
- package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
- package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
- package/plugins/menu/src/initialise/createMenu.js +10 -0
- package/plugins/menu/src/initialise/createMenu.test.js +37 -0
- package/plugins/menu/src/manifest.js +35 -0
- package/plugins/menu/src/manifest.test.js +63 -0
- package/plugins/menu/src/menu.scss +4 -0
- package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
- package/plugins/menu/src/reducers/pluginState.js +35 -0
- package/plugins/menu/src/reducers/pluginState.test.js +57 -0
- package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
- package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
- package/plugins/menu/src/registry/index.js +3 -0
- package/plugins/menu/src/registry/index.test.js +15 -0
- package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
- package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/esm/index.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/index.js +1 -1
- package/plugins/search/src/Search.jsx +7 -5
- package/plugins/search/src/components/Form/Form.module.scss +9 -20
- package/plugins/search/src/index.js +0 -1
- package/plugins/search/src/index.test.js +2 -4
- package/plugins/search/src/manifest.js +1 -1
- package/plugins/search/src/search.scss +17 -16
- 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/dist/umd/index.js +1 -1
- package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
- package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
- package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
- package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
- package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
- package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
- 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/utils/drawLayerBuckets.js +14 -0
- package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
- package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
- package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
- package/providers/maplibre/src/utils/hoverCursor.js +30 -14
- package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
- package/providers/maplibre/src/utils/patternImages.js +1 -1
- package/providers/maplibre/src/utils/symbolImages.js +41 -3
- package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
- package/rollup.esm.mjs +11 -1
- package/sonar-project.properties +7 -1
- package/src/App/components/Actions/Actions.test.jsx +47 -0
- package/src/App/components/Attributions/Attributions.module.scss +10 -0
- package/src/App/components/Hints/Hints.jsx +7 -2
- package/src/App/components/Hints/Hints.test.jsx +13 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +2 -3
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +3 -6
- package/src/App/components/{MoveControl/MoveControl.jsx → MoveControls/MoveControls.jsx} +10 -10
- package/src/App/components/{MoveControl/MoveControl.module.scss → MoveControls/MoveControls.module.scss} +60 -29
- package/src/App/components/{MoveControl/MoveControl.test.jsx → MoveControls/MoveControls.test.jsx} +48 -48
- package/src/App/components/Panel/Panel.jsx +18 -3
- package/src/App/components/Panel/Panel.module.scss +18 -8
- package/src/App/components/Panel/Panel.test.jsx +34 -0
- package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
- package/src/App/components/Viewport/Viewport.jsx +9 -1
- package/src/App/components/Viewport/Viewport.test.jsx +11 -0
- package/src/App/hooks/useFeatureItems.js +16 -7
- package/src/App/hooks/useFeatureItems.test.js +57 -0
- package/src/App/hooks/useLayoutMeasurements.js +95 -84
- package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
- package/src/App/initialiseApp.js +11 -8
- package/src/App/initialiseApp.test.js +15 -3
- package/src/App/layout/Layout.jsx +12 -11
- package/src/App/layout/Layout.test.jsx +2 -0
- package/src/App/layout/layout.module.scss +31 -17
- package/src/App/registry/keyboardShortcutRegistry.js +25 -13
- package/src/App/registry/keyboardShortcutRegistry.test.js +49 -32
- package/src/App/registry/pluginRegistry.js +2 -3
- package/src/App/registry/pluginRegistry.test.js +16 -3
- package/src/App/renderer/HtmlElementHost.jsx +18 -5
- package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
- package/src/App/renderer/mapButtons.js +28 -12
- package/src/App/renderer/mapButtons.test.js +17 -4
- package/src/App/renderer/mapControls.js +3 -1
- package/src/App/renderer/mapControls.test.js +16 -0
- package/src/App/renderer/mapPanels.js +44 -9
- package/src/App/renderer/mapPanels.test.js +51 -2
- package/src/App/renderer/orderItems.js +28 -0
- package/src/App/renderer/orderItems.test.js +50 -0
- package/src/App/renderer/slotAggregator.js +2 -18
- package/src/App/renderer/slotHelpers.js +3 -1
- package/src/App/renderer/slotHelpers.test.js +5 -0
- package/src/App/renderer/slots.js +3 -0
- package/src/App/store/AppProvider.jsx +4 -2
- package/src/App/store/appDispatchMiddleware.js +1 -1
- package/src/App/store/appDispatchMiddleware.test.js +8 -0
- package/src/InteractiveMap/InteractiveMap.js +8 -2
- package/src/InteractiveMap/InteractiveMap.test.js +20 -3
- package/src/config/appConfig.js +17 -15
- package/src/config/appConfig.test.js +22 -22
- package/src/config/defaults.js +3 -1
- package/src/config/events.js +2 -0
- package/src/config/mergeConfig.js +7 -0
- package/src/config/mergeConfig.test.js +20 -0
- package/src/scss/main.scss +1 -1
- package/src/scss/settings/_colors.scss +1 -0
- package/src/scss/settings/_dimensions.scss +2 -3
- package/src/scss/settings/_transition.scss +1 -1
- package/src/services/eventBus.js +44 -0
- package/src/services/eventBus.test.js +83 -0
- package/src/services/symbolRegistry.js +1 -1
- package/src/test-utils.js +5 -0
- package/src/types.js +25 -7
- package/webpack.umd.mjs +2 -0
- package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
- package/plugins/datasets/src/components/Key/Key.jsx +0 -50
- package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
- package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
- package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
- package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
- package/plugins/datasets/src/components/Key/KeySvgPattern.jsx +0 -13
- package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
- package/plugins/datasets/src/components/Key/KeySvgSymbol.jsx +0 -16
- package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
- /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
- /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
- /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.test.js +0 -0
|
@@ -4,6 +4,10 @@ import { withPluginContexts } from './pluginWrapper.js'
|
|
|
4
4
|
import { Panel } from '../components/Panel/Panel.jsx'
|
|
5
5
|
import { allowedSlots } from './slots.js'
|
|
6
6
|
import { resolveTargetSlot, isModeAllowed, isConsumerHtml } from './slotHelpers.js'
|
|
7
|
+
import { mapControls } from './mapControls.js'
|
|
8
|
+
import { orderItems } from './orderItems.js'
|
|
9
|
+
import { stringToKebab } from '../../utils/stringToKebab.js'
|
|
10
|
+
import { logger } from '../../services/logger.js'
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Determines whether a panel should be rendered in the given slot.
|
|
@@ -73,13 +77,7 @@ export function mapPanels ({ slot, appState, evaluateProp }) {
|
|
|
73
77
|
const plugin = pluginRegistry.registeredPlugins.find(p => p.id === config.pluginId)
|
|
74
78
|
const pluginId = plugin?.id
|
|
75
79
|
|
|
76
|
-
const
|
|
77
|
-
? withPluginContexts(config.render, {
|
|
78
|
-
...props,
|
|
79
|
-
pluginId,
|
|
80
|
-
pluginConfig: plugin?.config
|
|
81
|
-
})
|
|
82
|
-
: null
|
|
80
|
+
const html = pluginId ? evaluateProp(config.html, pluginId) : config.html
|
|
83
81
|
|
|
84
82
|
return {
|
|
85
83
|
id: panelId,
|
|
@@ -92,12 +90,49 @@ export function mapPanels ({ slot, appState, evaluateProp }) {
|
|
|
92
90
|
panelConfig={config}
|
|
93
91
|
props={props}
|
|
94
92
|
focusOnOpen={focusOnOpen}
|
|
95
|
-
|
|
93
|
+
items={buildPanelBodyItems({ panelId, config, props, plugin, pluginId, html, appState, evaluateProp })}
|
|
96
94
|
label={evaluateProp(config.label, pluginId)}
|
|
97
|
-
html={
|
|
95
|
+
html={html}
|
|
98
96
|
/>
|
|
99
97
|
)
|
|
100
98
|
}
|
|
101
99
|
})
|
|
102
100
|
.filter(Boolean)
|
|
103
101
|
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Builds the ordered list of body items for a panel: its own render content (if any) plus
|
|
105
|
+
* any controls registered against its `<panelId>-panel` slot by other plugins. Static-html
|
|
106
|
+
* panels don't build an items list — dangerouslySetInnerHTML can't host injected controls,
|
|
107
|
+
* so any controls targeting one are silently skipped (with a dev warning).
|
|
108
|
+
*/
|
|
109
|
+
function buildPanelBodyItems ({ panelId, config, props, plugin, pluginId, html, appState, evaluateProp }) {
|
|
110
|
+
const injectedItems = mapControls({
|
|
111
|
+
slot: `${stringToKebab(panelId)}-panel`,
|
|
112
|
+
appState,
|
|
113
|
+
evaluateProp
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
if (html) {
|
|
117
|
+
/* istanbul ignore next */
|
|
118
|
+
if (process.env.NODE_ENV !== 'production' && injectedItems.length > 0) {
|
|
119
|
+
logger.warn(`Panel "${panelId}" uses static html — controls targeting its slot are not rendered.`)
|
|
120
|
+
}
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!config.render) {
|
|
125
|
+
return injectedItems
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const WrappedChild = withPluginContexts(config.render, {
|
|
129
|
+
...props,
|
|
130
|
+
pluginId,
|
|
131
|
+
pluginConfig: plugin?.config
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
return orderItems([
|
|
135
|
+
{ id: panelId, order: 0, element: <WrappedChild {...props} /> },
|
|
136
|
+
...injectedItems
|
|
137
|
+
])
|
|
138
|
+
}
|
|
@@ -2,10 +2,14 @@ import React from 'react'
|
|
|
2
2
|
import { mapPanels } from './mapPanels.js'
|
|
3
3
|
import { registeredPlugins } from '../registry/pluginRegistry.js'
|
|
4
4
|
import { withPluginContexts } from './pluginWrapper.js'
|
|
5
|
+
import { mapControls } from './mapControls.js'
|
|
6
|
+
import { logger } from '../../services/logger.js'
|
|
5
7
|
|
|
8
|
+
jest.mock('../../services/logger.js')
|
|
6
9
|
jest.mock('../registry/panelRegistry.js')
|
|
7
10
|
jest.mock('../registry/pluginRegistry.js', () => ({ registeredPlugins: [] }))
|
|
8
11
|
jest.mock('./pluginWrapper.js', () => ({ withPluginContexts: jest.fn((c) => c) }))
|
|
12
|
+
jest.mock('./mapControls.js', () => ({ mapControls: jest.fn(() => []) }))
|
|
9
13
|
jest.mock('../components/Panel/Panel.jsx', () => ({ Panel: (props) => <div data-testid='panel' {...props} /> }))
|
|
10
14
|
jest.mock('./slots.js', () => ({ allowedSlots: { panel: ['header', 'modal', 'left-top'] } }))
|
|
11
15
|
|
|
@@ -23,6 +27,7 @@ describe('mapPanels', () => {
|
|
|
23
27
|
|
|
24
28
|
beforeEach(() => {
|
|
25
29
|
jest.clearAllMocks()
|
|
30
|
+
mapControls.mockReturnValue([])
|
|
26
31
|
registeredPlugins.length = 0
|
|
27
32
|
defaultAppState = {
|
|
28
33
|
breakpoint: 'desktop',
|
|
@@ -116,9 +121,53 @@ describe('mapPanels', () => {
|
|
|
116
121
|
)
|
|
117
122
|
})
|
|
118
123
|
|
|
119
|
-
it('
|
|
124
|
+
it('builds an empty items list if there is no render function and no injected controls', () => {
|
|
120
125
|
const result = map()
|
|
121
|
-
expect(result[0].element.props.
|
|
126
|
+
expect(result[0].element.props.items).toEqual([])
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('builds a single-item items list for a panel with its own render content', () => {
|
|
130
|
+
const renderFn = () => <div>child</div>
|
|
131
|
+
defaultAppState.panelConfig = ({ p1: { ...baseConfig, render: renderFn } })
|
|
132
|
+
const result = map()
|
|
133
|
+
expect(result[0].element.props.items).toEqual([
|
|
134
|
+
{ id: 'p1', order: 0, element: expect.anything() }
|
|
135
|
+
])
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('requests injected controls using the <panelId>-panel slot convention', () => {
|
|
139
|
+
map()
|
|
140
|
+
expect(mapControls).toHaveBeenCalledWith(expect.objectContaining({ slot: 'p1-panel' }))
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('merges and orders the panel\'s own content with controls injected via mapControls', () => {
|
|
144
|
+
const renderFn = () => <div>child</div>
|
|
145
|
+
mapControls.mockReturnValue([{ id: 'injected1', order: 1, element: <span>injected</span> }])
|
|
146
|
+
defaultAppState.panelConfig = ({ p1: { ...baseConfig, render: renderFn } })
|
|
147
|
+
const result = map()
|
|
148
|
+
// injected1 has order 1, so it's spliced ahead of the panel's own (unordered) content
|
|
149
|
+
expect(result[0].element.props.items.map(i => i.id)).toEqual(['injected1', 'p1'])
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('returns just the injected controls when the panel has neither render nor html', () => {
|
|
153
|
+
mapControls.mockReturnValue([{ id: 'injected1', order: 0, element: <span>injected</span> }])
|
|
154
|
+
defaultAppState.panelConfig = ({ p1: { desktop: { slot: 'header' }, includeModes: ['view'] } })
|
|
155
|
+
const result = map()
|
|
156
|
+
expect(result[0].element.props.items.map(i => i.id)).toEqual(['injected1'])
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('does not build an items list for a static-html panel (dangerouslySetInnerHTML can\'t host injected controls)', () => {
|
|
160
|
+
defaultAppState.panelConfig = ({ p1: { desktop: { slot: 'header' }, includeModes: ['view'], pluginId: 'plug1', html: '<p>Hi</p>' } })
|
|
161
|
+
const result = map()
|
|
162
|
+
expect(result[0].element.props.items).toBeUndefined()
|
|
163
|
+
expect(result[0].element.props.html).toBe('<p>Hi</p>')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('warns in dev when controls target a static-html panel', () => {
|
|
167
|
+
mapControls.mockReturnValue([{ id: 'injected1', order: 0, element: <span>injected</span> }])
|
|
168
|
+
defaultAppState.panelConfig = ({ p1: { desktop: { slot: 'header' }, includeModes: ['view'], pluginId: 'plug1', html: '<p>Hi</p>' } })
|
|
169
|
+
map()
|
|
170
|
+
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('p1'))
|
|
122
171
|
})
|
|
123
172
|
|
|
124
173
|
it('returns correct structure and defaults', () => {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/core/renderers/orderItems.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Orders a list of `{ id, order, element }` items for a single render target
|
|
5
|
+
* (a layout slot, or a panel body).
|
|
6
|
+
*
|
|
7
|
+
* Items without an explicit `order` (or `order === 0`) keep their natural/registration
|
|
8
|
+
* sequence. Items with an explicit `order` are spliced into that 1-based position among
|
|
9
|
+
* the unordered items, clamped to the valid range.
|
|
10
|
+
*/
|
|
11
|
+
export function orderItems (items) {
|
|
12
|
+
// Start with items that have no explicit order (in their natural sequence)
|
|
13
|
+
const withoutOrder = items.filter(item => item.order == null || item.order === 0)
|
|
14
|
+
const withOrder = items.filter(item => item.order != null && item.order !== 0)
|
|
15
|
+
|
|
16
|
+
// Sort items with order by their order value (stable sort preserves ties)
|
|
17
|
+
withOrder.sort((a, b) => a.order - b.order)
|
|
18
|
+
|
|
19
|
+
// Insert each ordered item at its specified position
|
|
20
|
+
const result = [...withoutOrder]
|
|
21
|
+
for (const item of withOrder) {
|
|
22
|
+
// Clamp position to valid range (1-based, so subtract 1 for array index)
|
|
23
|
+
const pos = Math.min(Math.max(item.order - 1, 0), result.length)
|
|
24
|
+
result.splice(pos, 0, item)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return result
|
|
28
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { orderItems } from './orderItems.js'
|
|
2
|
+
|
|
3
|
+
describe('orderItems', () => {
|
|
4
|
+
it('keeps unordered items in their natural sequence', () => {
|
|
5
|
+
const items = [{ id: 'a' }, { id: 'b' }, { id: 'c' }]
|
|
6
|
+
expect(orderItems(items).map(i => i.id)).toEqual(['a', 'b', 'c'])
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('treats order 0 the same as no order', () => {
|
|
10
|
+
const items = [{ id: 'a', order: 0 }, { id: 'b' }]
|
|
11
|
+
expect(orderItems(items).map(i => i.id)).toEqual(['a', 'b'])
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('inserts an ordered item at its 1-based position among unordered items', () => {
|
|
15
|
+
const items = [{ id: 'a' }, { id: 'b' }, { id: 'c', order: 1 }]
|
|
16
|
+
expect(orderItems(items).map(i => i.id)).toEqual(['c', 'a', 'b'])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('splices ordered items in ascending order, each relative to the growing result', () => {
|
|
20
|
+
const items = [
|
|
21
|
+
{ id: 'a' },
|
|
22
|
+
{ id: 'b', order: 2 },
|
|
23
|
+
{ id: 'c', order: 1 }
|
|
24
|
+
]
|
|
25
|
+
// c (order 1) is spliced in first, ahead of 'a': [c, a]
|
|
26
|
+
// b (order 2) is then spliced at index 1 of that 2-item result: [c, b, a]
|
|
27
|
+
expect(orderItems(items).map(i => i.id)).toEqual(['c', 'b', 'a'])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('clamps an order larger than the list to the end', () => {
|
|
31
|
+
const items = [{ id: 'a' }, { id: 'b' }, { id: 'c', order: 99 }]
|
|
32
|
+
expect(orderItems(items).map(i => i.id)).toEqual(['a', 'b', 'c'])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('splices equal-order items each at position 0, reversing their relative order', () => {
|
|
36
|
+
// The stable sort keeps a before b, but with no unordered items to anchor against,
|
|
37
|
+
// each splice(0, 0, item) inserts ahead of the previous one.
|
|
38
|
+
const items = [{ id: 'a', order: 1 }, { id: 'b', order: 1 }]
|
|
39
|
+
expect(orderItems(items).map(i => i.id)).toEqual(['b', 'a'])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('handles an all-ordered list', () => {
|
|
43
|
+
const items = [{ id: 'a', order: 2 }, { id: 'b', order: 1 }]
|
|
44
|
+
expect(orderItems(items).map(i => i.id)).toEqual(['b', 'a'])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('handles an empty list', () => {
|
|
48
|
+
expect(orderItems([])).toEqual([])
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -2,28 +2,12 @@
|
|
|
2
2
|
import { mapControls } from './mapControls.js'
|
|
3
3
|
import { mapPanels } from './mapPanels.js'
|
|
4
4
|
import { mapButtons } from './mapButtons.js'
|
|
5
|
+
import { orderItems } from './orderItems.js'
|
|
5
6
|
|
|
6
7
|
export function getSlotItems ({ evaluateProp, ...args }) {
|
|
7
8
|
const controls = mapControls({ evaluateProp, ...args })
|
|
8
9
|
const panels = mapPanels({ evaluateProp, ...args })
|
|
9
10
|
const buttons = mapButtons({ evaluateProp, ...args })
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// Start with items that have no explicit order (in their natural sequence)
|
|
14
|
-
const withoutOrder = items.filter(item => item.order == null || item.order === 0)
|
|
15
|
-
const withOrder = items.filter(item => item.order != null && item.order !== 0)
|
|
16
|
-
|
|
17
|
-
// Sort items with order by their order value (stable sort preserves ties)
|
|
18
|
-
withOrder.sort((a, b) => a.order - b.order)
|
|
19
|
-
|
|
20
|
-
// Insert each ordered item at its specified position
|
|
21
|
-
const result = [...withoutOrder]
|
|
22
|
-
for (const item of withOrder) {
|
|
23
|
-
// Clamp position to valid range (1-based, so subtract 1 for array index)
|
|
24
|
-
const pos = Math.min(Math.max(item.order - 1, 0), result.length)
|
|
25
|
-
result.splice(pos, 0, item)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return result
|
|
12
|
+
return orderItems([...controls, ...panels, ...buttons])
|
|
29
13
|
}
|
|
@@ -39,7 +39,9 @@ export const isControlVisible = (control, { breakpoint, mode, isFullscreen }) =>
|
|
|
39
39
|
if (!bpConfig) {
|
|
40
40
|
return false
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
// A control may also target a panel's body directly via the `<panelId>-panel`
|
|
43
|
+
// slot convention (mirrors the `<buttonId>-button` convention panels already use).
|
|
44
|
+
if (!allowedSlots.control.includes(bpConfig.slot) && !bpConfig.slot?.endsWith('-panel')) {
|
|
43
45
|
return false
|
|
44
46
|
}
|
|
45
47
|
if (!isModeAllowed(control, mode)) {
|
|
@@ -65,6 +65,11 @@ describe('isControlVisible', () => {
|
|
|
65
65
|
it('returns true when inline:false and fullscreen', () => {
|
|
66
66
|
expect(isControlVisible({ ...base, inline: false }, { breakpoint: 'desktop', mode: 'view', isFullscreen: true })).toBe(true)
|
|
67
67
|
})
|
|
68
|
+
|
|
69
|
+
it('returns true for a control targeting a panel-body slot via the <panelId>-panel convention', () => {
|
|
70
|
+
const panelTargeting = { desktop: { slot: 'map-styles-panel' } }
|
|
71
|
+
expect(isControlVisible(panelTargeting, { breakpoint: 'desktop', mode: 'view', isFullscreen: false })).toBe(true)
|
|
72
|
+
})
|
|
68
73
|
})
|
|
69
74
|
|
|
70
75
|
describe('isConsumerHtml', () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/core/renderers/slots.js
|
|
2
2
|
export const layoutSlots = Object.freeze({
|
|
3
3
|
SIDE: 'side',
|
|
4
|
+
HEADER: 'header',
|
|
4
5
|
BANNER: 'banner',
|
|
5
6
|
TOP_LEFT: 'top-left',
|
|
6
7
|
TOP_MIDDLE: 'top-middle',
|
|
@@ -19,6 +20,7 @@ export const layoutSlots = Object.freeze({
|
|
|
19
20
|
|
|
20
21
|
export const allowedSlots = Object.freeze({
|
|
21
22
|
control: [
|
|
23
|
+
layoutSlots.HEADER,
|
|
22
24
|
layoutSlots.BANNER,
|
|
23
25
|
layoutSlots.TOP_LEFT,
|
|
24
26
|
layoutSlots.TOP_RIGHT,
|
|
@@ -31,6 +33,7 @@ export const allowedSlots = Object.freeze({
|
|
|
31
33
|
],
|
|
32
34
|
panel: [
|
|
33
35
|
layoutSlots.SIDE,
|
|
36
|
+
layoutSlots.HEADER,
|
|
34
37
|
layoutSlots.BANNER,
|
|
35
38
|
layoutSlots.LEFT_TOP,
|
|
36
39
|
layoutSlots.LEFT_BOTTOM,
|
|
@@ -10,12 +10,13 @@ import { useMediaQueryDispatch } from '../hooks/useMediaQueryDispatch.js'
|
|
|
10
10
|
export const AppContext = createContext(null)
|
|
11
11
|
|
|
12
12
|
export const AppProvider = ({ options, children }) => {
|
|
13
|
-
const { pluginRegistry, buttonRegistry, panelRegistry, controlRegistry, eventBus, breakpointDetector } = options
|
|
13
|
+
const { pluginRegistry, buttonRegistry, panelRegistry, controlRegistry, keyboardShortcutRegistry, eventBus, breakpointDetector } = options
|
|
14
14
|
|
|
15
15
|
const layoutRefs = {
|
|
16
16
|
appContainerRef: useRef(null),
|
|
17
17
|
sideRef: useRef(null),
|
|
18
18
|
mainRef: useRef(null),
|
|
19
|
+
headerRef: useRef(null),
|
|
19
20
|
topRef: useRef(null),
|
|
20
21
|
topLeftColRef: useRef(null),
|
|
21
22
|
topRightColRef: useRef(null),
|
|
@@ -94,7 +95,8 @@ export const AppProvider = ({ options, children }) => {
|
|
|
94
95
|
pluginRegistry,
|
|
95
96
|
buttonRegistry,
|
|
96
97
|
panelRegistry,
|
|
97
|
-
controlRegistry
|
|
98
|
+
controlRegistry,
|
|
99
|
+
keyboardShortcutRegistry
|
|
98
100
|
}), [state, dispatch])
|
|
99
101
|
|
|
100
102
|
return (
|
|
@@ -101,7 +101,7 @@ export function handleActionSideEffects (action, previousState, panelConfig, eve
|
|
|
101
101
|
const mergedConfig = deepMerge(defaultControlConfig, config)
|
|
102
102
|
BREAKPOINTS.forEach(bp => {
|
|
103
103
|
const slot = mergedConfig[bp]?.slot
|
|
104
|
-
if (slot && !allowedSlots.control.includes(slot)) {
|
|
104
|
+
if (slot && !allowedSlots.control.includes(slot) && !slot.endsWith('-panel')) {
|
|
105
105
|
logger.warn(`control "${id}" has invalid slot "${slot}" at breakpoint "${bp}". Allowed slots: ${allowedSlots.control.join(', ')}.`)
|
|
106
106
|
}
|
|
107
107
|
})
|
|
@@ -185,6 +185,14 @@ describe('ADD_CONTROL', () => {
|
|
|
185
185
|
)
|
|
186
186
|
expect(console.warn).not.toHaveBeenCalled()
|
|
187
187
|
})
|
|
188
|
+
|
|
189
|
+
it('does not warn for a control targeting a panel-body slot', () => {
|
|
190
|
+
run(
|
|
191
|
+
{ type: 'ADD_CONTROL', payload: { id: 'myCtrl', config: { desktop: { slot: 'map-styles-panel' } } } },
|
|
192
|
+
{ breakpoint: 'desktop' }
|
|
193
|
+
)
|
|
194
|
+
expect(console.warn).not.toHaveBeenCalled()
|
|
195
|
+
})
|
|
188
196
|
})
|
|
189
197
|
|
|
190
198
|
describe('ADD_PANEL', () => {
|
|
@@ -111,13 +111,16 @@ export default class InteractiveMap {
|
|
|
111
111
|
history.pushState({ isBack: true }, '', e.currentTarget.getAttribute('href'))
|
|
112
112
|
}
|
|
113
113
|
this.showApp()
|
|
114
|
+
// App is already mounted, so focus the viewport directly rather than via a mount effect.
|
|
115
|
+
this.rootEl.querySelector('[role="application"]')?.focus()
|
|
114
116
|
} else if (this._root) {
|
|
115
117
|
// app already open — no-op, no push
|
|
116
118
|
} else {
|
|
117
119
|
if (this.config.manageHistoryState) {
|
|
118
120
|
history.pushState({ isBack: true }, '', e.currentTarget.getAttribute('href'))
|
|
119
121
|
}
|
|
120
|
-
|
|
122
|
+
// Only a genuine launcher click moves focus into the map — never a resize/breakpoint transition.
|
|
123
|
+
this.loadApp({ focusOnMount: true })
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
126
|
|
|
@@ -175,9 +178,11 @@ export default class InteractiveMap {
|
|
|
175
178
|
* Load and initialize the map application.
|
|
176
179
|
*
|
|
177
180
|
* @internal Not intended for end-user use.
|
|
181
|
+
* @param {Object} [options]
|
|
182
|
+
* @param {boolean} [options.focusOnMount=false] - Focus the viewport once mounted; only set by a genuine launcher click.
|
|
178
183
|
* @returns {Promise<void>}
|
|
179
184
|
*/
|
|
180
|
-
async loadApp () {
|
|
185
|
+
async loadApp ({ focusOnMount = false } = {}) {
|
|
181
186
|
if (this._root || this._isLoading) {
|
|
182
187
|
return
|
|
183
188
|
}
|
|
@@ -204,6 +209,7 @@ export default class InteractiveMap {
|
|
|
204
209
|
id: this.id,
|
|
205
210
|
initialBreakpoint: this._breakpointDetector.getBreakpoint(),
|
|
206
211
|
initialInterfaceType: getInterfaceType(),
|
|
212
|
+
focusOnMount,
|
|
207
213
|
...this.config,
|
|
208
214
|
MapProvider,
|
|
209
215
|
mapProviderConfig,
|
|
@@ -185,6 +185,18 @@ describe('InteractiveMap — loadApp', () => {
|
|
|
185
185
|
expect(typeof map.someMethod).toBe('function')
|
|
186
186
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:opened', { statePreserved: false, isFullscreen: false })
|
|
187
187
|
})
|
|
188
|
+
|
|
189
|
+
it('defaults focusOnMount to false when not passed', async () => {
|
|
190
|
+
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
191
|
+
await map.loadApp()
|
|
192
|
+
expect(initialiseApp).toHaveBeenCalledWith(rootEl, expect.objectContaining({ focusOnMount: false }))
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('passes focusOnMount through to initialiseApp when set', async () => {
|
|
196
|
+
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', mapProvider: mapProviderMock })
|
|
197
|
+
await map.loadApp({ focusOnMount: true })
|
|
198
|
+
expect(initialiseApp).toHaveBeenCalledWith(rootEl, expect.objectContaining({ focusOnMount: true }))
|
|
199
|
+
})
|
|
188
200
|
})
|
|
189
201
|
|
|
190
202
|
// --- _handleButtonClick ---
|
|
@@ -193,25 +205,30 @@ describe('InteractiveMap — _handleButtonClick', () => {
|
|
|
193
205
|
beforeEach(setupBeforeEach)
|
|
194
206
|
afterEach(() => jest.restoreAllMocks())
|
|
195
207
|
|
|
196
|
-
it('pushState and loadApp on first open', async () => {
|
|
208
|
+
it('pushState and loadApp (with focusOnMount) on first open', async () => {
|
|
197
209
|
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', manageHistoryState: true, mapProvider: mapProviderMock })
|
|
198
210
|
jest.spyOn(map, 'loadApp').mockResolvedValue()
|
|
199
211
|
jest.spyOn(history, 'pushState').mockImplementation(() => {})
|
|
200
212
|
await openButtonCallback({ currentTarget: { getAttribute: jest.fn().mockReturnValue('/?mv=map') } })
|
|
201
|
-
expect(map.loadApp).
|
|
213
|
+
expect(map.loadApp).toHaveBeenCalledWith({ focusOnMount: true })
|
|
202
214
|
expect(history.pushState).toHaveBeenCalledWith({ isBack: true }, '', '/?mv=map')
|
|
203
215
|
})
|
|
204
216
|
|
|
205
|
-
it('calls showApp (not loadApp) when map is hidden', async () => {
|
|
217
|
+
it('calls showApp (not loadApp) and focuses the viewport when map is hidden', async () => {
|
|
206
218
|
const map = new InteractiveMap('map', { behaviour: 'buttonFirst', manageHistoryState: true, mapProvider: mapProviderMock })
|
|
207
219
|
map._isHidden = true
|
|
208
220
|
jest.spyOn(map, 'showApp').mockImplementation(() => {})
|
|
209
221
|
jest.spyOn(map, 'loadApp').mockResolvedValue()
|
|
210
222
|
jest.spyOn(history, 'pushState').mockImplementation(() => {})
|
|
223
|
+
const viewport = document.createElement('div')
|
|
224
|
+
viewport.setAttribute('role', 'application')
|
|
225
|
+
viewport.setAttribute('tabindex', '0')
|
|
226
|
+
rootEl.appendChild(viewport)
|
|
211
227
|
await openButtonCallback({ currentTarget: { getAttribute: jest.fn().mockReturnValue('/?mv=map') } })
|
|
212
228
|
expect(map.showApp).toHaveBeenCalled()
|
|
213
229
|
expect(map.loadApp).not.toHaveBeenCalled()
|
|
214
230
|
expect(history.pushState).toHaveBeenCalled()
|
|
231
|
+
expect(viewport).toHaveFocus()
|
|
215
232
|
})
|
|
216
233
|
|
|
217
234
|
it('skips pushState when manageHistoryState is false', async () => {
|
package/src/config/appConfig.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { KeyboardHelp } from '../App/components/KeyboardHelp/KeyboardHelp.jsx'
|
|
2
|
-
import {
|
|
2
|
+
import { MoveControls } from '../App/components/MoveControls/MoveControls.jsx'
|
|
3
3
|
|
|
4
4
|
const keyboardBasePanelSlots = {
|
|
5
5
|
slot: 'middle',
|
|
@@ -13,13 +13,15 @@ const buttonSlots = {
|
|
|
13
13
|
showLabel: false
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const
|
|
16
|
+
const moveControlsSlot = {
|
|
17
17
|
slot: 'right-bottom'
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const exitButtonSlots = {
|
|
21
21
|
slot: 'top-left',
|
|
22
|
-
showLabel: false
|
|
22
|
+
showLabel: false,
|
|
23
|
+
// Explicit order so exit always sorts first in its slot.
|
|
24
|
+
order: 1
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const journeyBackSlots = { slot: 'actions', showLabel: true }
|
|
@@ -77,7 +79,7 @@ export const defaultAppConfig = {
|
|
|
77
79
|
iconId: 'plus',
|
|
78
80
|
keepFocus: true,
|
|
79
81
|
onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomIn(appConfig.zoomDelta),
|
|
80
|
-
excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appConfig.
|
|
82
|
+
excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appConfig.enableMoveControls || appState.interfaceType === 'touch',
|
|
81
83
|
enableWhen: ({ mapState }) => !mapState.isAtMaxZoom,
|
|
82
84
|
mobile: buttonSlots,
|
|
83
85
|
tablet: buttonSlots,
|
|
@@ -89,23 +91,23 @@ export const defaultAppConfig = {
|
|
|
89
91
|
iconId: 'minus',
|
|
90
92
|
keepFocus: true,
|
|
91
93
|
onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomOut(appConfig.zoomDelta),
|
|
92
|
-
excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appConfig.
|
|
94
|
+
excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appConfig.enableMoveControls || appState.interfaceType === 'touch',
|
|
93
95
|
enableWhen: ({ mapState }) => !mapState.isAtMinZoom,
|
|
94
96
|
mobile: buttonSlots,
|
|
95
97
|
tablet: buttonSlots,
|
|
96
98
|
desktop: buttonSlots
|
|
97
99
|
}, {
|
|
98
|
-
id: '
|
|
100
|
+
id: 'moveControls',
|
|
99
101
|
label: 'Move and zoom controls',
|
|
100
102
|
iconId: 'move',
|
|
101
103
|
keepFocus: true,
|
|
102
104
|
isExpanded: false,
|
|
103
|
-
ariaControls: ({ appConfig }) => `${appConfig.id}-move-
|
|
105
|
+
ariaControls: ({ appConfig }) => `${appConfig.id}-move-controls-content`,
|
|
104
106
|
onClick: (_e, { appState }) => appState.dispatch({
|
|
105
107
|
type: 'TOGGLE_BUTTON_EXPANDED',
|
|
106
|
-
payload: { id: '
|
|
108
|
+
payload: { id: 'moveControls', isExpanded: !appState.expandedButtons.has('moveControls') }
|
|
107
109
|
}),
|
|
108
|
-
excludeWhen: ({ appConfig }) => !appConfig.
|
|
110
|
+
excludeWhen: ({ appConfig }) => !appConfig.enableMoveControls,
|
|
109
111
|
mobile: buttonSlots,
|
|
110
112
|
tablet: buttonSlots,
|
|
111
113
|
desktop: buttonSlots
|
|
@@ -129,13 +131,13 @@ export const defaultAppConfig = {
|
|
|
129
131
|
}],
|
|
130
132
|
|
|
131
133
|
controls: [{
|
|
132
|
-
id: '
|
|
134
|
+
id: 'moveControls',
|
|
133
135
|
label: 'Move and zoom',
|
|
134
|
-
excludeWhen: ({ appConfig }) => !appConfig.
|
|
135
|
-
mobile:
|
|
136
|
-
tablet:
|
|
137
|
-
desktop:
|
|
138
|
-
render:
|
|
136
|
+
excludeWhen: ({ appConfig }) => !appConfig.enableMoveControls,
|
|
137
|
+
mobile: moveControlsSlot,
|
|
138
|
+
tablet: moveControlsSlot,
|
|
139
|
+
desktop: moveControlsSlot,
|
|
140
|
+
render: MoveControls
|
|
139
141
|
}],
|
|
140
142
|
|
|
141
143
|
icons: [{
|
|
@@ -2,7 +2,7 @@ import { render } from '@testing-library/react'
|
|
|
2
2
|
import { defaultAppConfig, defaultButtonConfig, scaleFactor } from './appConfig'
|
|
3
3
|
|
|
4
4
|
jest.mock('../App/store/appContext.js', () => ({
|
|
5
|
-
useApp: () => ({ listboxIsActive: false })
|
|
5
|
+
useApp: () => ({ listboxIsActive: false, keyboardShortcutRegistry: { getKeyboardShortcuts: () => [] } })
|
|
6
6
|
}))
|
|
7
7
|
|
|
8
8
|
describe('defaultAppConfig', () => {
|
|
@@ -19,7 +19,7 @@ describe('defaultAppConfig', () => {
|
|
|
19
19
|
const journeyContinueBtn = buttons.find(b => b.id === 'journeyContinue')
|
|
20
20
|
const zoomInBtn = buttons.find(b => b.id === 'zoomIn')
|
|
21
21
|
const zoomOutBtn = buttons.find(b => b.id === 'zoomOut')
|
|
22
|
-
const
|
|
22
|
+
const moveControlsBtn = buttons.find(b => b.id === 'moveControls')
|
|
23
23
|
|
|
24
24
|
// --- UI RENDER TESTS ---
|
|
25
25
|
it('renders KeyboardHelp panel', () => {
|
|
@@ -144,21 +144,21 @@ describe('defaultAppConfig', () => {
|
|
|
144
144
|
appState: { interfaceType: 'mouse' }
|
|
145
145
|
})).toBe(true)
|
|
146
146
|
|
|
147
|
-
// Branch B: Second part of OR is true (
|
|
147
|
+
// Branch B: Second part of OR is true (enableMoveControls)
|
|
148
148
|
expect(btn.excludeWhen({
|
|
149
|
-
appConfig: { enableZoomControls: true,
|
|
149
|
+
appConfig: { enableZoomControls: true, enableMoveControls: true },
|
|
150
150
|
appState: { interfaceType: 'mouse' }
|
|
151
151
|
})).toBe(true)
|
|
152
152
|
|
|
153
153
|
// Branch C: Third part of OR is true (interfaceType === 'touch')
|
|
154
154
|
expect(btn.excludeWhen({
|
|
155
|
-
appConfig: { enableZoomControls: true,
|
|
155
|
+
appConfig: { enableZoomControls: true, enableMoveControls: false },
|
|
156
156
|
appState: { interfaceType: 'touch' }
|
|
157
157
|
})).toBe(true)
|
|
158
158
|
|
|
159
159
|
// Branch D: All parts are false (Result: false)
|
|
160
160
|
expect(btn.excludeWhen({
|
|
161
|
-
appConfig: { enableZoomControls: true,
|
|
161
|
+
appConfig: { enableZoomControls: true, enableMoveControls: false },
|
|
162
162
|
appState: { interfaceType: 'mouse' }
|
|
163
163
|
})).toBe(false)
|
|
164
164
|
})
|
|
@@ -182,38 +182,38 @@ describe('defaultAppConfig', () => {
|
|
|
182
182
|
expect(mapProviderMock.zoomOut).toHaveBeenCalledWith(2)
|
|
183
183
|
})
|
|
184
184
|
|
|
185
|
-
// --- MOVE
|
|
186
|
-
it('covers all branches of
|
|
187
|
-
expect(
|
|
188
|
-
expect(
|
|
185
|
+
// --- MOVE CONTROLS TOGGLE BUTTON ---
|
|
186
|
+
it('covers all branches of moveControls excludeWhen', () => {
|
|
187
|
+
expect(moveControlsBtn.excludeWhen({ appConfig: { enableMoveControls: false } })).toBe(true)
|
|
188
|
+
expect(moveControlsBtn.excludeWhen({ appConfig: { enableMoveControls: true } })).toBe(false)
|
|
189
189
|
})
|
|
190
190
|
|
|
191
|
-
it('
|
|
192
|
-
expect(
|
|
191
|
+
it('moveControls ariaControls resolves the control element id', () => {
|
|
192
|
+
expect(moveControlsBtn.ariaControls({ appConfig: { id: 'im' } })).toBe('im-move-controls-content')
|
|
193
193
|
})
|
|
194
194
|
|
|
195
|
-
it('
|
|
195
|
+
it('moveControls onClick toggles TOGGLE_BUTTON_EXPANDED based on current state', () => {
|
|
196
196
|
const dispatch = jest.fn()
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
moveControlsBtn.onClick({}, { appState: { dispatch, expandedButtons: new Set() } })
|
|
199
199
|
expect(dispatch).toHaveBeenCalledWith({
|
|
200
200
|
type: 'TOGGLE_BUTTON_EXPANDED',
|
|
201
|
-
payload: { id: '
|
|
201
|
+
payload: { id: 'moveControls', isExpanded: true }
|
|
202
202
|
})
|
|
203
203
|
|
|
204
204
|
dispatch.mockClear()
|
|
205
|
-
|
|
205
|
+
moveControlsBtn.onClick({}, { appState: { dispatch, expandedButtons: new Set(['moveControls']) } })
|
|
206
206
|
expect(dispatch).toHaveBeenCalledWith({
|
|
207
207
|
type: 'TOGGLE_BUTTON_EXPANDED',
|
|
208
|
-
payload: { id: '
|
|
208
|
+
payload: { id: 'moveControls', isExpanded: false }
|
|
209
209
|
})
|
|
210
210
|
})
|
|
211
211
|
|
|
212
|
-
// --- MOVE
|
|
213
|
-
it('covers all branches of
|
|
214
|
-
const control = defaultAppConfig.controls.find(c => c.id === '
|
|
215
|
-
expect(control.excludeWhen({ appConfig: {
|
|
216
|
-
expect(control.excludeWhen({ appConfig: {
|
|
212
|
+
// --- MOVE CONTROLS ---
|
|
213
|
+
it('covers all branches of moveControls control excludeWhen', () => {
|
|
214
|
+
const control = defaultAppConfig.controls.find(c => c.id === 'moveControls')
|
|
215
|
+
expect(control.excludeWhen({ appConfig: { enableMoveControls: false } })).toBe(true)
|
|
216
|
+
expect(control.excludeWhen({ appConfig: { enableMoveControls: true } })).toBe(false)
|
|
217
217
|
})
|
|
218
218
|
|
|
219
219
|
// --- SUPPLEMENTARY CONFIGS ---
|