@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
|
@@ -76,16 +76,41 @@
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// ---------------------------------------------------
|
|
79
|
-
// Banner:
|
|
79
|
+
// Banner: docks centred between the side columns or stacks full-width; mode and position are set in JS.
|
|
80
80
|
// ---------------------------------------------------
|
|
81
81
|
|
|
82
82
|
.im-o-app__banner {
|
|
83
|
+
position: absolute;
|
|
84
|
+
top: var(--banner-top);
|
|
85
|
+
left: var(--banner-left);
|
|
86
|
+
right: var(--banner-right);
|
|
87
|
+
z-index: 9;
|
|
83
88
|
display: flex;
|
|
84
89
|
flex-direction: column;
|
|
85
90
|
gap: var(--divider-gap);
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
|
|
93
|
+
// Trailing gap below the banner is added in JS, not via a last-child margin, since closed consumer HTML panels stay in the DOM.
|
|
94
|
+
|
|
95
|
+
// Stacked fills full width; docked caps the width and centres it.
|
|
96
|
+
.im-o-app__banner--docked .im-c-panel--banner {
|
|
97
|
+
max-width: var(--banner-preferred-width);
|
|
98
|
+
margin-inline: auto;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------
|
|
102
|
+
// Header: normal flow, above the top row — pushes everything below it down for free.
|
|
103
|
+
// ---------------------------------------------------
|
|
104
|
+
|
|
105
|
+
// z-index only, no position — flex items get stacking priority from z-index alone, and must stay above .im-o-app__top's since header comes first in the DOM.
|
|
106
|
+
.im-o-app__header {
|
|
107
|
+
z-index: 11;
|
|
108
|
+
display: flex;
|
|
109
|
+
flex-direction: column;
|
|
110
|
+
width: 100%;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.im-o-app__header:not(:empty) {
|
|
89
114
|
margin-bottom: var(--divider-gap);
|
|
90
115
|
}
|
|
91
116
|
|
|
@@ -93,7 +118,9 @@
|
|
|
93
118
|
// Top: Includes a panel and buttons in colums
|
|
94
119
|
// ---------------------------------------------------
|
|
95
120
|
|
|
121
|
+
// z-index only (see .im-o-app__header's comment above) — must stay below its value.
|
|
96
122
|
.im-o-app__top {
|
|
123
|
+
z-index: 10;
|
|
97
124
|
display: flex;
|
|
98
125
|
margin-bottom: auto;
|
|
99
126
|
justify-content: space-between;
|
|
@@ -421,10 +448,6 @@
|
|
|
421
448
|
.im-o-app__modal .im-c-panel--banner {
|
|
422
449
|
margin: var(--primary-gap);
|
|
423
450
|
}
|
|
424
|
-
|
|
425
|
-
.im-o-app__banner {
|
|
426
|
-
z-index: 9;
|
|
427
|
-
}
|
|
428
451
|
}
|
|
429
452
|
|
|
430
453
|
// Desktop modal banner margin
|
|
@@ -447,13 +470,6 @@
|
|
|
447
470
|
opacity: 0;
|
|
448
471
|
}
|
|
449
472
|
|
|
450
|
-
// Banner on desktop
|
|
451
|
-
.im-o-app--desktop .im-c-panel--banner {
|
|
452
|
-
margin-left: auto;
|
|
453
|
-
margin-right: auto;
|
|
454
|
-
max-width: var(--banner-max-desktop-width);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
473
|
// Actions inset when width is greater
|
|
458
474
|
.im-o-app--tablet, .im-o-app--desktop {
|
|
459
475
|
.im-o-app__actions {
|
|
@@ -466,8 +482,7 @@
|
|
|
466
482
|
max-width: var(--action-bar-max-width);
|
|
467
483
|
}
|
|
468
484
|
|
|
469
|
-
//
|
|
470
|
-
// a slide-up. Override to opacity-only so the bar fades in instead.
|
|
485
|
+
// Opacity-only so it fades in instead of visually sliding up.
|
|
471
486
|
@media (prefers-reduced-motion: no-preference) {
|
|
472
487
|
.im-c-actions {
|
|
473
488
|
transition: opacity var(--duration) ease;
|
|
@@ -498,8 +513,7 @@
|
|
|
498
513
|
clip-path: inset(-20px 0 0 0);
|
|
499
514
|
}
|
|
500
515
|
|
|
501
|
-
// Inset focus ring
|
|
502
|
-
// so the ::after ring isn't clipped by the parent overflow:hidden
|
|
516
|
+
// Inset the focus ring so it isn't clipped by the parent's overflow:hidden.
|
|
503
517
|
.im-o-app--mobile .im-o-app__drawer .im-c-panel::after,
|
|
504
518
|
.im-o-app--mobile .im-o-app__actions .im-c-panel::after,
|
|
505
519
|
.im-o-app__modal .im-c-panel--drawer::after {
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
// src/core/registry/keyboardShortcutRegistry.js
|
|
2
2
|
import { coreShortcuts } from '../controls/keyboardShortcuts.js'
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
const pluginShortcutHelp = []
|
|
4
|
+
// State is passed in explicitly, rather than held at module scope, so each map instance gets its own registry and can't leak shortcuts or provider support into another map's help panel.
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
const pluginShortcutIds =
|
|
9
|
-
|
|
10
|
-
let providerSupportedIds = new Set()
|
|
11
|
-
|
|
12
|
-
export const registerKeyboardShortcut = ({ shortcut }) => {
|
|
6
|
+
const _registerKeyboardShortcut = (state, { shortcut }) => {
|
|
7
|
+
const { pluginShortcutHelp, pluginShortcutIds } = state
|
|
13
8
|
if (pluginShortcutIds.has(shortcut.id)) {
|
|
14
9
|
pluginShortcutHelp[pluginShortcutHelp.findIndex(s => s.id === shortcut.id)] = shortcut
|
|
15
10
|
} else {
|
|
@@ -18,14 +13,14 @@ export const registerKeyboardShortcut = ({ shortcut }) => {
|
|
|
18
13
|
}
|
|
19
14
|
}
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
providerSupportedIds = new Set(ids)
|
|
16
|
+
const _setProviderSupportedShortcuts = (state, ids = []) => {
|
|
17
|
+
state.providerSupportedIds = new Set(ids)
|
|
23
18
|
}
|
|
24
19
|
|
|
25
|
-
|
|
20
|
+
const _getKeyboardShortcuts = (state, appConfig = {}) => {
|
|
26
21
|
const filteredCore = coreShortcuts.filter(s => {
|
|
27
22
|
// Must be supported by provider
|
|
28
|
-
if (!providerSupportedIds.has(s.id)) {
|
|
23
|
+
if (!state.providerSupportedIds.has(s.id)) {
|
|
29
24
|
return false
|
|
30
25
|
}
|
|
31
26
|
// Check requiredConfig - all specified config values must be truthy
|
|
@@ -37,6 +32,23 @@ export const getKeyboardShortcuts = (appConfig = {}) => {
|
|
|
37
32
|
|
|
38
33
|
return [
|
|
39
34
|
...filteredCore, // supported core shortcuts
|
|
40
|
-
...pluginShortcutHelp // plugin-defined shortcuts (deduped)
|
|
35
|
+
...state.pluginShortcutHelp // plugin-defined shortcuts (deduped)
|
|
41
36
|
]
|
|
42
37
|
}
|
|
38
|
+
|
|
39
|
+
// One instance per map — see initialiseApp.js's getOrCreateRegistries.
|
|
40
|
+
export function createKeyboardShortcutRegistry () {
|
|
41
|
+
const state = {
|
|
42
|
+
// Stores the actual shortcut objects in insertion order
|
|
43
|
+
pluginShortcutHelp: [],
|
|
44
|
+
// Tracks only IDs for O(1) duplicate detection
|
|
45
|
+
pluginShortcutIds: new Set(),
|
|
46
|
+
providerSupportedIds: new Set()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
registerKeyboardShortcut: (args) => _registerKeyboardShortcut(state, args),
|
|
51
|
+
setProviderSupportedShortcuts: (ids) => _setProviderSupportedShortcuts(state, ids),
|
|
52
|
+
getKeyboardShortcuts: (appConfig) => _getKeyboardShortcuts(state, appConfig)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// src/core/registry/keyboardShortcutRegistry.test.js
|
|
2
|
-
describe('
|
|
3
|
-
let
|
|
4
|
-
let setProviderSupportedShortcuts
|
|
5
|
-
let getKeyboardShortcuts
|
|
2
|
+
describe('createKeyboardShortcutRegistry', () => {
|
|
3
|
+
let createKeyboardShortcutRegistry
|
|
6
4
|
let coreShortcutsMock
|
|
7
5
|
|
|
8
6
|
beforeEach(() => {
|
|
@@ -16,57 +14,55 @@ describe('keyboardShortcutRegistry', () => {
|
|
|
16
14
|
coreShortcuts: coreShortcutsMock
|
|
17
15
|
}))
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
registerKeyboardShortcut = module.registerKeyboardShortcut
|
|
21
|
-
setProviderSupportedShortcuts = module.setProviderSupportedShortcuts
|
|
22
|
-
getKeyboardShortcuts = module.getKeyboardShortcuts
|
|
17
|
+
createKeyboardShortcutRegistry = require('./keyboardShortcutRegistry.js').createKeyboardShortcutRegistry
|
|
23
18
|
})
|
|
24
19
|
|
|
25
20
|
test('registerKeyboardShortcut should add a plugin shortcut', () => {
|
|
21
|
+
const registry = createKeyboardShortcutRegistry()
|
|
26
22
|
const shortcut = { id: 'pluginShortcut', description: 'Plugin Shortcut' }
|
|
27
|
-
registerKeyboardShortcut({ shortcut })
|
|
28
|
-
|
|
29
|
-
expect(shortcuts).toContain(shortcut)
|
|
23
|
+
registry.registerKeyboardShortcut({ shortcut })
|
|
24
|
+
expect(registry.getKeyboardShortcuts()).toContain(shortcut)
|
|
30
25
|
})
|
|
31
26
|
|
|
32
27
|
test('registerKeyboardShortcut updates an existing shortcut when re-registered with the same id', () => {
|
|
28
|
+
const registry = createKeyboardShortcutRegistry()
|
|
33
29
|
const shortcut = { id: 'duplicate', description: 'First' }
|
|
34
30
|
const updatedShortcut = { id: 'duplicate', description: 'Second' }
|
|
35
|
-
registerKeyboardShortcut({ shortcut })
|
|
36
|
-
registerKeyboardShortcut({ shortcut: updatedShortcut })
|
|
37
|
-
const shortcuts = getKeyboardShortcuts()
|
|
31
|
+
registry.registerKeyboardShortcut({ shortcut })
|
|
32
|
+
registry.registerKeyboardShortcut({ shortcut: updatedShortcut })
|
|
33
|
+
const shortcuts = registry.getKeyboardShortcuts()
|
|
38
34
|
expect(shortcuts).toContain(updatedShortcut)
|
|
39
35
|
expect(shortcuts).not.toContain(shortcut)
|
|
40
36
|
expect(shortcuts.filter(s => s.id === 'duplicate')).toHaveLength(1)
|
|
41
37
|
})
|
|
42
38
|
|
|
43
39
|
test('setProviderSupportedShortcuts should filter core shortcuts', () => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
expect(
|
|
40
|
+
const registry = createKeyboardShortcutRegistry()
|
|
41
|
+
registry.setProviderSupportedShortcuts(['copy'])
|
|
42
|
+
expect(registry.getKeyboardShortcuts()).toEqual([{ id: 'copy', description: 'Copy' }])
|
|
47
43
|
})
|
|
48
44
|
|
|
49
45
|
test('setProviderSupportedShortcuts with no argument defaults to empty set', () => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
expect(
|
|
46
|
+
const registry = createKeyboardShortcutRegistry()
|
|
47
|
+
registry.setProviderSupportedShortcuts() // no ids argument
|
|
48
|
+
expect(registry.getKeyboardShortcuts()).toEqual([]) // default empty
|
|
53
49
|
})
|
|
54
50
|
|
|
55
51
|
test('getKeyboardShortcuts should merge core and plugin shortcuts', () => {
|
|
56
|
-
|
|
52
|
+
const registry = createKeyboardShortcutRegistry()
|
|
53
|
+
registry.setProviderSupportedShortcuts(['copy'])
|
|
57
54
|
const pluginShortcut = { id: 'plugin', description: 'Plugin' }
|
|
58
|
-
registerKeyboardShortcut({ shortcut: pluginShortcut })
|
|
59
|
-
|
|
60
|
-
expect(shortcuts).toEqual([
|
|
55
|
+
registry.registerKeyboardShortcut({ shortcut: pluginShortcut })
|
|
56
|
+
expect(registry.getKeyboardShortcuts()).toEqual([
|
|
61
57
|
{ id: 'copy', description: 'Copy' },
|
|
62
58
|
pluginShortcut
|
|
63
59
|
])
|
|
64
60
|
})
|
|
65
61
|
|
|
66
62
|
test('setProviderSupportedShortcuts with empty array returns no core shortcuts', () => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
expect(
|
|
63
|
+
const registry = createKeyboardShortcutRegistry()
|
|
64
|
+
registry.setProviderSupportedShortcuts([])
|
|
65
|
+
expect(registry.getKeyboardShortcuts()).toEqual([])
|
|
70
66
|
})
|
|
71
67
|
|
|
72
68
|
test('getKeyboardShortcuts filters by requiredConfig when appConfig provided', () => {
|
|
@@ -77,23 +73,44 @@ describe('keyboardShortcutRegistry', () => {
|
|
|
77
73
|
{ id: 'conditional', description: 'Conditional', requiredConfig: ['featureEnabled'] }
|
|
78
74
|
]
|
|
79
75
|
}))
|
|
80
|
-
const
|
|
81
|
-
|
|
76
|
+
const registry = require('./keyboardShortcutRegistry.js').createKeyboardShortcutRegistry()
|
|
77
|
+
registry.setProviderSupportedShortcuts(['always', 'conditional'])
|
|
82
78
|
|
|
83
79
|
// Without config, conditional shortcut is excluded
|
|
84
|
-
expect(
|
|
80
|
+
expect(registry.getKeyboardShortcuts()).toEqual([
|
|
85
81
|
{ id: 'always', description: 'Always shown' }
|
|
86
82
|
])
|
|
87
83
|
|
|
88
84
|
// With config false, conditional shortcut is excluded
|
|
89
|
-
expect(
|
|
85
|
+
expect(registry.getKeyboardShortcuts({ featureEnabled: false })).toEqual([
|
|
90
86
|
{ id: 'always', description: 'Always shown' }
|
|
91
87
|
])
|
|
92
88
|
|
|
93
89
|
// With config true, conditional shortcut is included
|
|
94
|
-
expect(
|
|
90
|
+
expect(registry.getKeyboardShortcuts({ featureEnabled: true })).toEqual([
|
|
95
91
|
{ id: 'always', description: 'Always shown' },
|
|
96
92
|
{ id: 'conditional', description: 'Conditional', requiredConfig: ['featureEnabled'] }
|
|
97
93
|
])
|
|
98
94
|
})
|
|
95
|
+
|
|
96
|
+
test('two registry instances are fully isolated from each other', () => {
|
|
97
|
+
// This is the multi-map-on-a-page scenario: a shortcut registered by one map
|
|
98
|
+
// (e.g. a plugin) must never leak into another map's help panel, and each map's
|
|
99
|
+
// provider-supported-shortcuts filtering must stay independent.
|
|
100
|
+
const registryA = createKeyboardShortcutRegistry()
|
|
101
|
+
const registryB = createKeyboardShortcutRegistry()
|
|
102
|
+
|
|
103
|
+
registryA.setProviderSupportedShortcuts(['copy'])
|
|
104
|
+
registryA.registerKeyboardShortcut({ shortcut: { id: 'onlyOnA', description: 'Only on A' } })
|
|
105
|
+
|
|
106
|
+
registryB.setProviderSupportedShortcuts(['paste'])
|
|
107
|
+
|
|
108
|
+
expect(registryA.getKeyboardShortcuts()).toEqual([
|
|
109
|
+
{ id: 'copy', description: 'Copy' },
|
|
110
|
+
{ id: 'onlyOnA', description: 'Only on A' }
|
|
111
|
+
])
|
|
112
|
+
expect(registryB.getKeyboardShortcuts()).toEqual([
|
|
113
|
+
{ id: 'paste', description: 'Paste' }
|
|
114
|
+
])
|
|
115
|
+
})
|
|
99
116
|
})
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// src/core/registry/pluginRegistry.js
|
|
2
2
|
import { registerIcon } from './iconRegistry.js'
|
|
3
|
-
import { registerKeyboardShortcut } from './keyboardShortcutRegistry.js'
|
|
4
3
|
import { allowedSlots } from '../renderer/slots.js'
|
|
5
4
|
import { logger } from '../../services/logger.js'
|
|
6
5
|
|
|
@@ -12,13 +11,13 @@ function validateSlots (item, type) {
|
|
|
12
11
|
const allowed = allowedSlots[type]
|
|
13
12
|
BREAKPOINTS.forEach(bp => {
|
|
14
13
|
const slot = item[bp]?.slot
|
|
15
|
-
if (slot && !allowed.includes(slot) && !(type === 'panel' && slot.endsWith('-button'))) {
|
|
14
|
+
if (slot && !allowed.includes(slot) && !(type === 'panel' && slot.endsWith('-button')) && !(type === 'control' && slot.endsWith('-panel'))) {
|
|
16
15
|
logger.warn(`${type} "${item.id}" has invalid slot "${slot}" at breakpoint "${bp}". Allowed slots: ${allowed.join(', ')}.`)
|
|
17
16
|
}
|
|
18
17
|
})
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
export function createPluginRegistry ({ registerButton, registerPanel, registerControl }) {
|
|
20
|
+
export function createPluginRegistry ({ registerButton, registerPanel, registerControl, registerKeyboardShortcut }) {
|
|
22
21
|
const registeredPlugins = []
|
|
23
22
|
|
|
24
23
|
function registerPlugin (plugin) {
|
|
@@ -5,7 +5,6 @@ jest.mock('./buttonRegistry.js', () => ({ registerButton: jest.fn() }))
|
|
|
5
5
|
jest.mock('./panelRegistry.js', () => ({ registerPanel: jest.fn() }))
|
|
6
6
|
jest.mock('./controlRegistry.js', () => ({ registerControl: jest.fn() }))
|
|
7
7
|
jest.mock('./iconRegistry.js', () => ({ registerIcon: jest.fn() }))
|
|
8
|
-
jest.mock('./keyboardShortcutRegistry.js', () => ({ registerKeyboardShortcut: jest.fn() }))
|
|
9
8
|
|
|
10
9
|
describe('pluginRegistry', () => {
|
|
11
10
|
let pluginRegistry
|
|
@@ -18,9 +17,11 @@ describe('pluginRegistry', () => {
|
|
|
18
17
|
registerPanel = require('./panelRegistry.js').registerPanel
|
|
19
18
|
registerControl = require('./controlRegistry.js').registerControl
|
|
20
19
|
registerIcon = require('./iconRegistry.js').registerIcon
|
|
21
|
-
registerKeyboardShortcut
|
|
20
|
+
// registerKeyboardShortcut is injected (per-instance registry), not imported from a module —
|
|
21
|
+
// see keyboardShortcutRegistry.js's createKeyboardShortcutRegistry.
|
|
22
|
+
registerKeyboardShortcut = jest.fn()
|
|
22
23
|
|
|
23
|
-
pluginRegistry = createPluginRegistry({ registerButton, registerPanel, registerControl })
|
|
24
|
+
pluginRegistry = createPluginRegistry({ registerButton, registerPanel, registerControl, registerKeyboardShortcut })
|
|
24
25
|
})
|
|
25
26
|
|
|
26
27
|
it('registers plugin and pushes to registeredPlugins', () => {
|
|
@@ -170,6 +171,18 @@ describe('pluginRegistry', () => {
|
|
|
170
171
|
pluginRegistry.registerPlugin(plugin)
|
|
171
172
|
expect(console.warn).not.toHaveBeenCalled()
|
|
172
173
|
})
|
|
174
|
+
|
|
175
|
+
it('does not warn for a control targeting a panel-body slot', () => {
|
|
176
|
+
const plugin = {
|
|
177
|
+
id: 'inject-plugin',
|
|
178
|
+
config: {},
|
|
179
|
+
manifest: {
|
|
180
|
+
controls: [{ id: 'ctrl1', desktop: { slot: 'map-styles-panel' } }]
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
pluginRegistry.registerPlugin(plugin)
|
|
184
|
+
expect(console.warn).not.toHaveBeenCalled()
|
|
185
|
+
})
|
|
173
186
|
})
|
|
174
187
|
|
|
175
188
|
it('clears all registered plugins', () => {
|
|
@@ -28,6 +28,10 @@ export const getSlotRef = (slot, layoutRefs) => {
|
|
|
28
28
|
const el = document.querySelector(`[data-button-slot="${slot}"]`)
|
|
29
29
|
return el ? { current: el } : null
|
|
30
30
|
}
|
|
31
|
+
if (slot?.endsWith('-panel')) {
|
|
32
|
+
const el = document.querySelector(`[data-panel-slot="${slot}"]`)
|
|
33
|
+
return el ? { current: el } : null
|
|
34
|
+
}
|
|
31
35
|
|
|
32
36
|
return slotRefMap[slot] || null
|
|
33
37
|
}
|
|
@@ -37,8 +41,13 @@ export const getSlotRef = (slot, layoutRefs) => {
|
|
|
37
41
|
* Moves the wrapper into the target slot when visible, hides it otherwise.
|
|
38
42
|
* Depends on breakpoint to handle conditionally rendered slot containers
|
|
39
43
|
* (e.g. the banner slot swaps DOM nodes between mobile and desktop).
|
|
44
|
+
*
|
|
45
|
+
* `anchorKey` is an optional extra dependency for anchors that aren't always present in the
|
|
46
|
+
* DOM — e.g. a `-panel` target only exists while that panel is open — so the projection is
|
|
47
|
+
* re-attempted whenever it changes, rather than only on mount. Stable/persistent anchors
|
|
48
|
+
* (named layout slots, `-button` targets) don't need it since they never come and go.
|
|
40
49
|
*/
|
|
41
|
-
export const useDomProjection = (wrapperRef, targetSlot, isVisible, layoutRefs, breakpoint) => {
|
|
50
|
+
export const useDomProjection = (wrapperRef, targetSlot, isVisible, layoutRefs, breakpoint, anchorKey) => {
|
|
42
51
|
const layoutRefsRef = useRef(layoutRefs)
|
|
43
52
|
layoutRefsRef.current = layoutRefs
|
|
44
53
|
|
|
@@ -66,7 +75,7 @@ export const useDomProjection = (wrapperRef, targetSlot, isVisible, layoutRefs,
|
|
|
66
75
|
return () => {
|
|
67
76
|
wrapper.style.display = 'none'
|
|
68
77
|
}
|
|
69
|
-
}, [isVisible, targetSlot, breakpoint, wrapperRef])
|
|
78
|
+
}, [isVisible, targetSlot, breakpoint, wrapperRef, anchorKey])
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
/**
|
|
@@ -126,13 +135,17 @@ const PersistentPanel = ({ panelId, config, isOpen, openPanelProps, focusOnOpen,
|
|
|
126
135
|
*/
|
|
127
136
|
const PersistentControl = ({ control, appState }) => {
|
|
128
137
|
const wrapperRef = useRef(null)
|
|
129
|
-
const { breakpoint, mode, isFullscreen, layoutRefs } = appState
|
|
138
|
+
const { breakpoint, mode, isFullscreen, layoutRefs, openPanels } = appState
|
|
130
139
|
|
|
131
140
|
const bpConfig = control[breakpoint]
|
|
132
141
|
const isVisible = isControlVisible(control, { breakpoint, mode, isFullscreen })
|
|
133
142
|
const targetSlot = bpConfig?.slot || null
|
|
134
143
|
|
|
135
|
-
|
|
144
|
+
// A control targeting a panel's body (`<panelId>-panel`) needs its DOM anchor re-resolved
|
|
145
|
+
// whenever panels open/close, since that anchor only exists while the target panel is open.
|
|
146
|
+
const anchorKey = targetSlot?.endsWith('-panel') ? Object.keys(openPanels || {}).sort((a, b) => a.localeCompare(b)).join(',') : null
|
|
147
|
+
|
|
148
|
+
useDomProjection(wrapperRef, targetSlot, isVisible, layoutRefs, breakpoint, anchorKey)
|
|
136
149
|
|
|
137
150
|
const innerHtml = useMemo(() => ({ __html: control.html }), [control.html])
|
|
138
151
|
|
|
@@ -174,7 +187,7 @@ export const HtmlElementHost = () => {
|
|
|
174
187
|
const cfg = panelConfig[panelId]?.[breakpoint]
|
|
175
188
|
return cfg?.modal
|
|
176
189
|
})
|
|
177
|
-
return modalPanels.length > 0 ? modalPanels[modalPanels.length - 1][0] : null
|
|
190
|
+
return modalPanels.length > 0 ? modalPanels[modalPanels.length - 1][0] : null // NOSONAR - .at() unsupported on Chrome < 92
|
|
178
191
|
}, [openPanels, panelConfig, breakpoint])
|
|
179
192
|
|
|
180
193
|
if (!htmlPanels.length && !htmlControls.length) {
|
|
@@ -313,6 +313,60 @@ describe('HtmlElementHost', () => {
|
|
|
313
313
|
expect(getSlotRef('nonexistent-button', {})).toBeNull()
|
|
314
314
|
})
|
|
315
315
|
|
|
316
|
+
test('getSlotRef returns wrapped element for panel slot when element exists', () => {
|
|
317
|
+
const el = document.createElement('div')
|
|
318
|
+
el.dataset.panelSlot = 'map-styles-panel'
|
|
319
|
+
document.body.appendChild(el)
|
|
320
|
+
expect(getSlotRef('map-styles-panel', {})).toEqual({ current: el })
|
|
321
|
+
el.remove()
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test('getSlotRef returns null for panel slot when no element found', () => {
|
|
325
|
+
expect(getSlotRef('nonexistent-panel', {})).toBeNull()
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('re-attempts DOM projection into a panel-body slot once its anchor appears (panel opens)', () => {
|
|
329
|
+
const controlConfig = {
|
|
330
|
+
mapStylesNote: { id: 'mapStylesNote', desktop: { slot: 'map-styles-panel' }, html: '<p>Injected note</p>' }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// A real React-rendered node, mimicking the real Panel.jsx body div — present in the DOM
|
|
334
|
+
// throughout, but only gains the data-panel-slot marker once the panel "opens".
|
|
335
|
+
const panelBodyRef = React.createRef()
|
|
336
|
+
|
|
337
|
+
mockApp({ controlConfig, openPanels: {} })
|
|
338
|
+
const { rerender } = render(
|
|
339
|
+
<div>
|
|
340
|
+
<div ref={panelBodyRef} />
|
|
341
|
+
<HtmlElementHost />
|
|
342
|
+
</div>
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
// Anchor isn't marked yet (panel closed) — nothing to project into
|
|
346
|
+
expect(panelBodyRef.current.querySelector('.im-c-control')).toBeNull()
|
|
347
|
+
|
|
348
|
+
// Panel opens: its body anchor is marked, and openPanels changes (two entries, so the
|
|
349
|
+
// anchorKey sort's localeCompare comparator actually runs)
|
|
350
|
+
panelBodyRef.current.setAttribute('data-panel-slot', 'map-styles-panel')
|
|
351
|
+
mockApp({ controlConfig, openPanels: { mapStyles: { props: {} }, other: { props: {} } } })
|
|
352
|
+
rerender(
|
|
353
|
+
<div>
|
|
354
|
+
<div ref={panelBodyRef} />
|
|
355
|
+
<HtmlElementHost />
|
|
356
|
+
</div>
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
expect(panelBodyRef.current.querySelector('.im-c-control')).toBeTruthy()
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('does not crash a panel-targeting control when appState.openPanels is missing', () => {
|
|
363
|
+
const controlConfig = {
|
|
364
|
+
mapStylesNote: { id: 'mapStylesNote', desktop: { slot: 'map-styles-panel' }, html: '<p>Injected note</p>' }
|
|
365
|
+
}
|
|
366
|
+
mockApp({ controlConfig, openPanels: undefined })
|
|
367
|
+
expect(() => render(<HtmlElementHost />)).not.toThrow()
|
|
368
|
+
})
|
|
369
|
+
|
|
316
370
|
it('does not append child if slotRef exists but current is null', () => {
|
|
317
371
|
// 1. Setup refs where the slot exists in the map but the DOM node (current) is null
|
|
318
372
|
const incompleteRefs = {
|
|
@@ -49,8 +49,7 @@ function getMatchingButtons ({ appState, buttonConfig, slot, evaluateProp }) {
|
|
|
49
49
|
})
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function createButtonClickHandler (
|
|
53
|
-
const [, config] = btn
|
|
52
|
+
function createButtonClickHandler (config, appState, evaluateProp) {
|
|
54
53
|
const isPanelOpen = !!(config.panelId && appState.openPanels[config.panelId])
|
|
55
54
|
const isToggle = config.isPressed !== undefined || !!config.pressedWhen
|
|
56
55
|
|
|
@@ -134,15 +133,33 @@ function applySlotExclusivity (matching, appState) {
|
|
|
134
133
|
return matching.filter(([_, config]) => config.pluginId === exclusivePluginId)
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Builds the props for a <SlotButton>. isHidden/variant are included here (not just derived
|
|
138
|
+
* inside SlotButton) because Actions.jsx also reads them directly off the 'actions' slot's
|
|
139
|
+
* immediate children via React.Children.toArray — they need to be top-level props on whatever
|
|
140
|
+
* element ends up there, not just used internally to build MapButton.
|
|
141
|
+
*/
|
|
142
|
+
const slotButtonProps = ({ buttonId, config, appState, appConfig, evaluateProp }) => ({
|
|
143
|
+
buttonId,
|
|
144
|
+
config,
|
|
145
|
+
appState,
|
|
146
|
+
appConfig,
|
|
147
|
+
evaluateProp,
|
|
148
|
+
isHidden: appState.hiddenButtons.has(buttonId),
|
|
149
|
+
variant: config.variant
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Renders a single button's MapButton element for its slot, computing its click handler
|
|
154
|
+
* and derived state (panel-open, showLabel fallback, etc.) from its config and appState.
|
|
155
|
+
*/
|
|
156
|
+
function SlotButton ({ buttonId, config, appState, appConfig, evaluateProp }) {
|
|
139
157
|
const bpConfig = config[appState.breakpoint] ?? {}
|
|
140
|
-
const handleClick = createButtonClickHandler(
|
|
158
|
+
const handleClick = createButtonClickHandler(config, appState, evaluateProp)
|
|
141
159
|
const isPanelOpen = !!(config.panelId && appState.openPanels[config.panelId])
|
|
142
160
|
|
|
143
161
|
return (
|
|
144
162
|
<MapButton
|
|
145
|
-
key={buttonId}
|
|
146
163
|
buttonId={buttonId}
|
|
147
164
|
iconId={evaluateProp(config.iconId, config.pluginId)}
|
|
148
165
|
iconSvgContent={evaluateProp(config.iconSvgContent, config.pluginId)}
|
|
@@ -219,21 +236,20 @@ function mapButtons ({ slot, appState, appConfig, evaluateProp }) {
|
|
|
219
236
|
id: buttonId,
|
|
220
237
|
type: 'button',
|
|
221
238
|
order,
|
|
222
|
-
element:
|
|
239
|
+
element: <SlotButton key={buttonId} {...slotButtonProps({ buttonId, config, appState, appConfig, evaluateProp })} />
|
|
223
240
|
})
|
|
224
241
|
}
|
|
225
242
|
|
|
226
243
|
for (const [groupName, { label, order: groupOrder, members }] of groupMap) {
|
|
227
244
|
if (members.length < 2) {
|
|
228
245
|
// Singleton group: degrade to a regular button using the group's slot order
|
|
229
|
-
const
|
|
230
|
-
const [buttonId, config] = btn
|
|
246
|
+
const [buttonId, config] = members[0]
|
|
231
247
|
const order = groupOrder || config[breakpoint]?.order || 0
|
|
232
248
|
result.push({
|
|
233
249
|
id: buttonId,
|
|
234
250
|
type: 'button',
|
|
235
251
|
order,
|
|
236
|
-
element:
|
|
252
|
+
element: <SlotButton key={buttonId} {...slotButtonProps({ buttonId, config, appState, appConfig, evaluateProp })} />
|
|
237
253
|
})
|
|
238
254
|
continue
|
|
239
255
|
}
|
|
@@ -251,7 +267,7 @@ function mapButtons ({ slot, appState, appConfig, evaluateProp }) {
|
|
|
251
267
|
order: groupOrder,
|
|
252
268
|
element: (
|
|
253
269
|
<div key={`group-${groupName}`} role='group' aria-label={label} className='im-c-button-group'>{/* NOSONAR - div with role="group" is correct for a button group */}
|
|
254
|
-
{sorted.map(
|
|
270
|
+
{sorted.map(([buttonId, config]) => <SlotButton key={buttonId} {...slotButtonProps({ buttonId, config, appState, appConfig, evaluateProp })} />)}
|
|
255
271
|
</div>
|
|
256
272
|
)
|
|
257
273
|
})
|
|
@@ -264,7 +280,7 @@ export {
|
|
|
264
280
|
mapButtons,
|
|
265
281
|
getMatchingButtons,
|
|
266
282
|
applySlotExclusivity,
|
|
267
|
-
|
|
283
|
+
SlotButton,
|
|
268
284
|
resolveGroupName,
|
|
269
285
|
resolveGroupLabel,
|
|
270
286
|
resolveGroupOrder
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { mapButtons, getMatchingButtons, applySlotExclusivity,
|
|
2
|
+
import { mapButtons, getMatchingButtons, applySlotExclusivity, SlotButton, resolveGroupName, resolveGroupLabel, resolveGroupOrder } from './mapButtons.js'
|
|
3
3
|
import { logger } from '../../services/logger.js'
|
|
4
4
|
import { getPanelConfig } from '../registry/panelRegistry.js'
|
|
5
5
|
|
|
@@ -156,11 +156,11 @@ describe('mapButtons module', () => {
|
|
|
156
156
|
})
|
|
157
157
|
|
|
158
158
|
// -------------------------
|
|
159
|
-
//
|
|
159
|
+
// SlotButton tests
|
|
160
160
|
// -------------------------
|
|
161
|
-
describe('
|
|
161
|
+
describe('SlotButton', () => {
|
|
162
162
|
const render = (config, state = appState) =>
|
|
163
|
-
|
|
163
|
+
SlotButton({ buttonId: 'id', config, appState: state, appConfig, evaluateProp })
|
|
164
164
|
|
|
165
165
|
it('renders a MapButton with correct basic props', () => {
|
|
166
166
|
const result = render(baseBtn)
|
|
@@ -404,5 +404,18 @@ describe('mapButtons module', () => {
|
|
|
404
404
|
expect(result).toHaveLength(1)
|
|
405
405
|
expect(result[0].id).toBe('parent')
|
|
406
406
|
})
|
|
407
|
+
|
|
408
|
+
// Actions.jsx (the 'actions' slot wrapper) reads isHidden/variant directly off each slot
|
|
409
|
+
// item's top-level element props via React.Children.toArray — these two guard that contract.
|
|
410
|
+
it('carries isHidden as a top-level prop on the slot element, not just inside MapButton', () => {
|
|
411
|
+
appState.buttonConfig = ({ b1: baseBtn })
|
|
412
|
+
appState.hiddenButtons = new Set(['b1'])
|
|
413
|
+
expect(map()[0].element.props.isHidden).toBe(true)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it('carries variant as a top-level prop on the slot element, not just inside MapButton', () => {
|
|
417
|
+
appState.buttonConfig = ({ b1: { ...baseBtn, variant: 'touch' } })
|
|
418
|
+
expect(map()[0].element.props.variant).toBe('touch')
|
|
419
|
+
})
|
|
407
420
|
})
|
|
408
421
|
})
|
|
@@ -28,7 +28,9 @@ export function mapControls ({ slot, appState, evaluateProp }) {
|
|
|
28
28
|
return false
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// A control may also target a panel's body directly via the `<panelId>-panel`
|
|
32
|
+
// slot convention (mirrors the `<buttonId>-button` convention panels already use).
|
|
33
|
+
const slotAllowed = allowedSlots.control.includes(bpConfig.slot) || bpConfig.slot?.endsWith('-panel')
|
|
32
34
|
const inModeWhitelist = control.includeModes?.includes(mode) ?? true
|
|
33
35
|
const inExcludeModes = control.excludeModes?.includes(mode) ?? false
|
|
34
36
|
|
|
@@ -164,4 +164,20 @@ describe('mapControls', () => {
|
|
|
164
164
|
const result = mapControls({ slot: 'header', appState: defaultAppState, evaluateProp: (p) => p })
|
|
165
165
|
expect(result.map(c => c.id)).toEqual(['ctrl1'])
|
|
166
166
|
})
|
|
167
|
+
|
|
168
|
+
it('matches a control targeting a panel-body slot via the <panelId>-panel convention', () => {
|
|
169
|
+
defaultAppState.controlConfig = ({
|
|
170
|
+
ctrl1: { id: 'ctrl1', desktop: { slot: 'map-styles-panel', order: 1 }, includeModes: ['view'] }
|
|
171
|
+
})
|
|
172
|
+
const result = mapControls({ slot: 'map-styles-panel', appState: defaultAppState, evaluateProp: (p) => p })
|
|
173
|
+
expect(result.map(c => c.id)).toEqual(['ctrl1'])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('does not match a panel-body-targeting control against an unrelated slot', () => {
|
|
177
|
+
defaultAppState.controlConfig = ({
|
|
178
|
+
ctrl1: { id: 'ctrl1', desktop: { slot: 'map-styles-panel', order: 1 }, includeModes: ['view'] }
|
|
179
|
+
})
|
|
180
|
+
const result = mapControls({ slot: 'header', appState: defaultAppState, evaluateProp: (p) => p })
|
|
181
|
+
expect(result).toEqual([])
|
|
182
|
+
})
|
|
167
183
|
})
|