@defra/interactive-map 0.0.40-alpha → 0.0.42-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/button-definition.md +7 -8
- package/docs/api/control-definition.md +17 -1
- package/docs/api/panel-definition.md +5 -0
- package/docs/api/slots.md +50 -6
- package/docs/assets/images/slot-map.svg +61 -262
- package/docs/examples/add-polygons.mdx +8 -2
- package/docs/examples/add-symbols.mdx +8 -2
- package/docs/examples/draw-tools.mdx +3 -3
- package/docs/plugins/datasets.md +23 -15
- package/docs/plugins/draw.md +79 -16
- package/docs/plugins/map-key.md +147 -0
- package/docs/plugins/search.md +1 -1
- package/docs/plugins.md +5 -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/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/index.js +1 -1
- package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/src/manifest.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.test.js +175 -0
- package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
- package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
- package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +14 -0
- 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 -4
- 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 +13 -1
- package/plugins/datasets/src/registry/dataset.test.js +61 -0
- package/plugins/datasets/src/registry/datasetRegistry.js +12 -6
- package/plugins/datasets/src/registry/datasetRegistry.test.js +16 -4
- 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 +10 -2
- package/plugins/draw/src/DrawInit.test.jsx +51 -10
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +91 -11
- 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/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 +4 -27
- 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 +3 -53
- 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 +10 -3
- 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.test.js +12 -0
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +8 -2
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +31 -0
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +33 -0
- 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 +10 -3
- package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +24 -1
- 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/createNewShape.js +41 -0
- package/plugins/draw/src/api/createNewShape.test.js +63 -0
- package/plugins/draw/src/api/editFeature.js +7 -4
- package/plugins/draw/src/api/editFeature.test.js +16 -0
- package/plugins/draw/src/api/newLine.js +2 -40
- package/plugins/draw/src/api/newPoint.js +42 -0
- package/plugins/draw/src/api/newPoint.test.js +117 -0
- package/plugins/draw/src/api/newPolygon.js +2 -40
- 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 +17 -10
- package/plugins/draw/src/events.test.js +14 -0
- 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/useHighlightSync.js +33 -12
- package/plugins/interact/src/hooks/useHighlightSync.test.js +96 -25
- package/plugins/interact/src/manifest.js +1 -1
- 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 +68 -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 +43 -0
- package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +21 -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 +26 -0
- package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
- package/plugins/map-key/src/components/Key/KeySvg.jsx +41 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
- package/plugins/map-key/src/components/Key/KeySvgLine.jsx +22 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.jsx +7 -2
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +8 -2
- package/plugins/map-key/src/components/Key/KeySvgRect.jsx +26 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.jsx +8 -3
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +17 -6
- package/plugins/map-key/src/components/Key/MapKey.jsx +53 -0
- package/plugins/map-key/src/components/Key/MapKey.test.jsx +147 -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 +2 -3
- 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/src/App/components/Actions/Actions.test.jsx +47 -0
- package/src/App/components/Hints/Hints.test.jsx +13 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +25 -23
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +22 -5
- package/src/App/components/MoveControls/MoveControls.module.scss +41 -10
- package/src/App/components/Panel/Panel.jsx +65 -8
- package/src/App/components/Panel/Panel.module.scss +49 -15
- package/src/App/components/Panel/Panel.test.jsx +114 -1
- package/src/App/components/Tabs/Tabs.jsx +43 -17
- package/src/App/components/Tabs/Tabs.module.scss +32 -5
- package/src/App/components/Tabs/Tabs.test.jsx +40 -10
- package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
- 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/layout/Layout.jsx +9 -10
- package/src/App/layout/Layout.test.jsx +2 -0
- package/src/App/layout/layout.module.scss +31 -17
- package/src/App/registry/pluginRegistry.js +1 -1
- package/src/App/registry/pluginRegistry.test.js +12 -0
- package/src/App/renderer/HtmlElementHost.jsx +18 -5
- package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
- package/src/App/renderer/groupByKey.js +28 -0
- package/src/App/renderer/groupByKey.test.js +39 -0
- package/src/App/renderer/groupIntoTabs.js +42 -0
- package/src/App/renderer/groupIntoTabs.test.js +95 -0
- package/src/App/renderer/mapButtons.js +97 -129
- package/src/App/renderer/mapButtons.test.js +67 -83
- package/src/App/renderer/mapControls.js +4 -1
- package/src/App/renderer/mapControls.test.js +32 -0
- package/src/App/renderer/mapPanels.js +53 -11
- package/src/App/renderer/mapPanels.test.js +95 -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 +1 -0
- package/src/App/store/appDispatchMiddleware.js +1 -1
- package/src/App/store/appDispatchMiddleware.test.js +8 -0
- package/src/config/appConfig.js +2 -2
- package/src/config/events.js +2 -0
- 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/types.js +31 -3
- 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/KeySvgRect.jsx +0 -22
- 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
|
@@ -36,10 +36,48 @@ export const anchorToMaplibre = ([ax, ay]) => {
|
|
|
36
36
|
return (y + (x && y ? '-' : '') + x) || 'center'
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// The discrete fraction (0, 0.5 or 1) icon-anchor actually renders a given axis at — the
|
|
40
|
+
// same left/right/top/bottom/center snapping xAnchor/yAnchor above already do, expressed as
|
|
41
|
+
// a number instead of a string so it can be compared against the true, unsnapped fraction.
|
|
42
|
+
const discreteFraction = (a) => {
|
|
43
|
+
if (a <= ANCHOR_LOW) { return 0 }
|
|
44
|
+
if (a >= ANCHOR_HIGH) { return 1 }
|
|
45
|
+
return 0.5
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* MapLibre's icon-anchor only has 9 discrete positions — anchorToMaplibre above snaps a
|
|
50
|
+
* fractional anchor to the nearest one, which loses precision for anything off-grid (e.g.
|
|
51
|
+
* pin's [0.5, 0.9] renders as if it were 1.0/'bottom', shifting the whole icon ~10% of its
|
|
52
|
+
* height off from where a DOM marker using the same anchor value would place it —
|
|
53
|
+
* SymbolMarker.jsx positions markers with the exact fraction via CSS margins, with no such
|
|
54
|
+
* snapping). icon-offset corrects this: the pixel distance, in the icon's own display-space
|
|
55
|
+
* dimensions (derived from its viewBox), between the true anchor and the discrete one it
|
|
56
|
+
* snapped to — applied on top of icon-anchor, on both provider and dataset symbol layers.
|
|
57
|
+
*
|
|
58
|
+
* @param {number[]} anchor - [x, y] in 0–1 space (the true, unsnapped anchor)
|
|
59
|
+
* @param {string} viewBox - SVG viewBox string, e.g. '0 0 44 44'
|
|
60
|
+
* @returns {number[]} [offsetX, offsetY] in pixels, for the icon-offset layout property
|
|
61
|
+
*/
|
|
62
|
+
// Rounded to 2dp: sub-hundredth-of-a-pixel precision is meaningless for rendering, and
|
|
63
|
+
// without it fractions like 0.8 produce float noise (8.799999999999997, not 8.8).
|
|
64
|
+
const round2dp = (n) => Math.round(n * 100) / 100
|
|
65
|
+
|
|
66
|
+
export const anchorToMaplibreOffset = ([ax, ay], viewBox) => {
|
|
67
|
+
const [,, width, height] = viewBox.split(' ').map(Number)
|
|
68
|
+
return [
|
|
69
|
+
round2dp((discreteFraction(ax) - ax) * width),
|
|
70
|
+
round2dp((discreteFraction(ay) - ay) * height)
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
|
|
39
74
|
/**
|
|
40
75
|
* Register normal, active (both rings) and selected (black ring) symbol images.
|
|
41
76
|
* Skips images that are already registered (safe to call on style change).
|
|
42
|
-
*
|
|
77
|
+
* Merges into `map._activeSymbolImageMap` (normal→active) and `map._selectedSymbolImageMap`
|
|
78
|
+
* (normal→selected) rather than replacing them — more than one caller (e.g. the datasets
|
|
79
|
+
* and draw plugins) can register symbols on the same map, and each call must add to that
|
|
80
|
+
* shared registry, not wipe out whatever another caller already registered there.
|
|
43
81
|
*
|
|
44
82
|
* @param {Object} map - MapLibre map instance
|
|
45
83
|
* @param {Object[]} styleArray - an array of symbol configs
|
|
@@ -53,8 +91,8 @@ export const addSymbolsToMap = async (map, styleArray, mapStyle, symbolRegistry,
|
|
|
53
91
|
return
|
|
54
92
|
}
|
|
55
93
|
|
|
56
|
-
map._activeSymbolImageMap
|
|
57
|
-
map._selectedSymbolImageMap
|
|
94
|
+
map._activeSymbolImageMap ??= {}
|
|
95
|
+
map._selectedSymbolImageMap ??= {}
|
|
58
96
|
|
|
59
97
|
await Promise.all(styleArray.flatMap(config => {
|
|
60
98
|
const normalId = symbolRegistry.getSymbolImageId(config, mapStyle, false, pixelRatio)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { anchorToMaplibre, addSymbolsToMap } from './symbolImages.js'
|
|
1
|
+
import { anchorToMaplibre, anchorToMaplibreOffset, addSymbolsToMap } from './symbolImages.js'
|
|
2
2
|
import { symbolRegistry } from '../../../../src/services/symbolRegistry.js'
|
|
3
3
|
|
|
4
4
|
beforeAll(() => {
|
|
@@ -84,6 +84,36 @@ describe('anchorToMaplibre', () => {
|
|
|
84
84
|
})
|
|
85
85
|
})
|
|
86
86
|
|
|
87
|
+
// ─── anchorToMaplibreOffset ───────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
describe('anchorToMaplibreOffset', () => {
|
|
90
|
+
const VIEW_BOX = '0 0 44 44'
|
|
91
|
+
|
|
92
|
+
it('is zero for an anchor that already sits exactly on a discrete position', () => {
|
|
93
|
+
expect(anchorToMaplibreOffset([0.5, 0.5], VIEW_BOX)).toEqual([0, 0]) // center
|
|
94
|
+
expect(anchorToMaplibreOffset([0, 0], VIEW_BOX)).toEqual([0, 0]) // top-left
|
|
95
|
+
expect(anchorToMaplibreOffset([1, 1], VIEW_BOX)).toEqual([0, 0]) // bottom-right
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('corrects for pin-style [0.5, 0.9] snapping to bottom (1.0)', () => {
|
|
99
|
+
expect(anchorToMaplibreOffset([0.5, 0.9], VIEW_BOX)).toEqual([0, 4.4])
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('corrects both axes when both are off-grid', () => {
|
|
103
|
+
// 0.1 snaps to 'left'/'top' (0), 0.9 snaps to 'right'/'bottom' (1)
|
|
104
|
+
expect(anchorToMaplibreOffset([0.1, 0.1], VIEW_BOX)).toEqual([-4.4, -4.4])
|
|
105
|
+
expect(anchorToMaplibreOffset([0.9, 0.9], VIEW_BOX)).toEqual([4.4, 4.4])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('still corrects a value inside the centre band that is not exactly 0.5 — the whole band snaps to 0.5, not just its edges', () => {
|
|
109
|
+
expect(anchorToMaplibreOffset([0.4, 0.6], VIEW_BOX)).toEqual([4.4, -4.4])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('scales with the viewBox, not just the anchor fraction', () => {
|
|
113
|
+
expect(anchorToMaplibreOffset([0.5, 0.9], '0 0 100 100')).toEqual([0, 10])
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
87
117
|
// ─── addSymbolsToMap ──────────────────────────────────────────────────────────
|
|
88
118
|
|
|
89
119
|
const makeMap = (existingIds = []) => ({
|
|
@@ -103,13 +133,17 @@ describe('addSymbolsToMap — registration', () => {
|
|
|
103
133
|
expect(map.addImage).not.toHaveBeenCalled()
|
|
104
134
|
})
|
|
105
135
|
|
|
106
|
-
it('
|
|
136
|
+
it('merges into existing _activeSymbolImageMap/_selectedSymbolImageMap rather than replacing them', async () => {
|
|
137
|
+
// More than one caller (datasets, draw, ...) can register symbols on the same map —
|
|
138
|
+
// a later call must add to the shared registry, not wipe an earlier caller's entries.
|
|
107
139
|
const map = makeMap()
|
|
108
|
-
map._activeSymbolImageMap = {
|
|
109
|
-
map._selectedSymbolImageMap = {
|
|
140
|
+
map._activeSymbolImageMap = { 'other-normal-id': 'other-active-id' }
|
|
141
|
+
map._selectedSymbolImageMap = { 'other-normal-id': 'other-selected-id' }
|
|
110
142
|
await addSymbolsToMap(map, [{ symbol: 'pin' }], mapStyle, symbolRegistry)
|
|
111
|
-
expect(map._activeSymbolImageMap).
|
|
112
|
-
expect(map._selectedSymbolImageMap).
|
|
143
|
+
expect(map._activeSymbolImageMap['other-normal-id']).toBe('other-active-id')
|
|
144
|
+
expect(map._selectedSymbolImageMap['other-normal-id']).toBe('other-selected-id')
|
|
145
|
+
const normalId = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle, false)
|
|
146
|
+
expect(map._activeSymbolImageMap).toHaveProperty(normalId)
|
|
113
147
|
})
|
|
114
148
|
|
|
115
149
|
it('calls addImage for normal, active and selected variants', async () => {
|
package/rollup.esm.mjs
CHANGED
|
@@ -182,7 +182,7 @@ const createESMConfig = (entryPath, outDir, isCore = false, manualChunks = null,
|
|
|
182
182
|
open: false,
|
|
183
183
|
gzipSize: true
|
|
184
184
|
})]
|
|
185
|
-
|
|
185
|
+
: [])
|
|
186
186
|
],
|
|
187
187
|
|
|
188
188
|
output: {
|
|
@@ -303,6 +303,16 @@ const ALL_BUILDS = [
|
|
|
303
303
|
entryPath: './plugins/beta/frame/src/index.js',
|
|
304
304
|
outDir: 'plugins/beta/frame/dist/esm',
|
|
305
305
|
manualChunks: (id) => id.includes('/manifest') ? 'im-frame-plugin' : undefined
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
entryPath: './plugins/map-key/src/index.js',
|
|
309
|
+
outDir: 'plugins/map-key/dist/esm',
|
|
310
|
+
manualChunks: (id) => id.includes('/manifest') ? 'im-map-key-plugin' : undefined
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
entryPath: './plugins/menu/src/index.js',
|
|
314
|
+
outDir: 'plugins/menu/dist/esm',
|
|
315
|
+
manualChunks: (id) => id.includes('/manifest') ? 'im-menu-plugin' : undefined
|
|
306
316
|
}
|
|
307
317
|
]
|
|
308
318
|
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { render, screen } from '@testing-library/react'
|
|
3
3
|
import { Actions } from './Actions.jsx'
|
|
4
|
+
import { mapButtons } from '../../renderer/mapButtons.js'
|
|
4
5
|
import * as AppContext from '../../store/appContext' // import the module to mock
|
|
5
6
|
|
|
6
7
|
// Simple child component to handle isHidden
|
|
7
8
|
const TestChild = ({ isHidden, children, ...props }) => <div {...props}>{children}</div>
|
|
8
9
|
|
|
10
|
+
jest.mock('../MapButton/MapButton.jsx', () => ({
|
|
11
|
+
MapButton: ({ isHidden }) => <button data-testid='map-button' hidden={isHidden} />
|
|
12
|
+
}))
|
|
13
|
+
|
|
9
14
|
const ACTIONS_SELECTOR = '.im-c-actions'
|
|
10
15
|
const HIDDEN_CLASS = 'im-c-actions--hidden'
|
|
11
16
|
|
|
@@ -80,4 +85,46 @@ describe('Actions component', () => {
|
|
|
80
85
|
const container = screen.getByTestId('child1').closest(ACTIONS_SELECTOR)
|
|
81
86
|
expect(container).toHaveClass(HIDDEN_CLASS)
|
|
82
87
|
})
|
|
88
|
+
|
|
89
|
+
// Integration with the real mapButtons() output, not just the TestChild stand-in above —
|
|
90
|
+
// guards the isHidden/variant contract at the actual seam between the two modules, since
|
|
91
|
+
// mapButtons.js's own unit tests can't catch a break here on their own.
|
|
92
|
+
describe('with real mapButtons() output', () => {
|
|
93
|
+
const evaluateProp = (prop) => (typeof prop === 'function' ? prop() : prop)
|
|
94
|
+
const appConfig = { id: 'test' }
|
|
95
|
+
const baseAppState = {
|
|
96
|
+
breakpoint: 'desktop',
|
|
97
|
+
mode: 'view',
|
|
98
|
+
isFullscreen: true,
|
|
99
|
+
openPanels: {},
|
|
100
|
+
dispatch: jest.fn(),
|
|
101
|
+
disabledButtons: new Set(),
|
|
102
|
+
hiddenButtons: new Set(),
|
|
103
|
+
pressedButtons: new Set(),
|
|
104
|
+
expandedButtons: new Set(),
|
|
105
|
+
panelConfig: {},
|
|
106
|
+
layoutRefs: { viewportRef: { current: { focus: jest.fn() } } }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const renderActionsWith = (appState) => {
|
|
110
|
+
const items = mapButtons({ slot: 'actions', appState, appConfig, evaluateProp })
|
|
111
|
+
return render(<Actions slot='actions'>{items.map(i => i.element)}</Actions>)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
it('shows the actions bar when a real mapButtons() button is visible', () => {
|
|
115
|
+
const appState = { ...baseAppState, buttonConfig: { cancel: { label: 'Cancel', desktop: { slot: 'actions' } } } }
|
|
116
|
+
renderActionsWith(appState)
|
|
117
|
+
expect(screen.getByTestId('map-button').closest(ACTIONS_SELECTOR)).not.toHaveClass(HIDDEN_CLASS)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('hides the actions bar when the only real mapButtons() button is hidden', () => {
|
|
121
|
+
const appState = {
|
|
122
|
+
...baseAppState,
|
|
123
|
+
hiddenButtons: new Set(['cancel']),
|
|
124
|
+
buttonConfig: { cancel: { label: 'Cancel', desktop: { slot: 'actions' } } }
|
|
125
|
+
}
|
|
126
|
+
renderActionsWith(appState)
|
|
127
|
+
expect(screen.getByTestId('map-button').closest(ACTIONS_SELECTOR)).toHaveClass(HIDDEN_CLASS)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
83
130
|
})
|
|
@@ -52,6 +52,19 @@ describe('Hints — rendering', () => {
|
|
|
52
52
|
expect(desc.innerHTML).toBe('Shift + ?')
|
|
53
53
|
})
|
|
54
54
|
|
|
55
|
+
it('appends the move-controls hint text when enableMoveControls is true', () => {
|
|
56
|
+
const mainEl = setup()
|
|
57
|
+
useConfig.mockReturnValue({
|
|
58
|
+
id: 'test-map',
|
|
59
|
+
keyboardHintText: '<kbd>Shift</kbd> + <kbd>?</kbd>',
|
|
60
|
+
enableMoveControls: true,
|
|
61
|
+
moveControlsHintText: '<kbd>Arrow keys</kbd> to move'
|
|
62
|
+
})
|
|
63
|
+
render(<Hints />)
|
|
64
|
+
const desc = mainEl.querySelector('#test-map-keyboard-desc')
|
|
65
|
+
expect(desc.innerHTML).toBe('Shift + ? Arrow keys to move')
|
|
66
|
+
})
|
|
67
|
+
|
|
55
68
|
it('renders no hint content when there is no active hint', () => {
|
|
56
69
|
const mainEl = setup()
|
|
57
70
|
render(<Hints />)
|
|
@@ -3,6 +3,9 @@ import React from 'react'
|
|
|
3
3
|
import { useConfig } from '../../store/configContext'
|
|
4
4
|
import { useApp } from '../../store/appContext.js'
|
|
5
5
|
import { Tabs } from '../Tabs/Tabs.jsx'
|
|
6
|
+
import { groupIntoTabs } from '../../renderer/groupIntoTabs.js'
|
|
7
|
+
|
|
8
|
+
const DEFAULT_GROUP = 'Navigate'
|
|
6
9
|
|
|
7
10
|
const ShortcutList = ({ items }) => (
|
|
8
11
|
<dl className='im-c-keyboard-help__list'>
|
|
@@ -15,26 +18,22 @@ const ShortcutList = ({ items }) => (
|
|
|
15
18
|
</dl>
|
|
16
19
|
)
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const getDefaultTab = (groupEntries, context) => {
|
|
28
|
-
const exactMatch = groupEntries.find(([, items]) =>
|
|
29
|
-
items.some(s => (s.context ?? 'viewport') === context)
|
|
21
|
+
/**
|
|
22
|
+
* Picks which tab should be active on open: the first tab containing a shortcut whose own
|
|
23
|
+
* context matches exactly, else the first tab containing a global-context shortcut, else
|
|
24
|
+
* just the first tab. Domain-specific to keyboard shortcuts — not part of groupIntoTabs.
|
|
25
|
+
*/
|
|
26
|
+
const getDefaultTab = (tabs, context) => {
|
|
27
|
+
const exactMatch = tabs.find(tab =>
|
|
28
|
+
tab.items.some(({ shortcut }) => (shortcut.context ?? 'viewport') === context)
|
|
30
29
|
)
|
|
31
30
|
if (exactMatch) {
|
|
32
|
-
return exactMatch
|
|
31
|
+
return exactMatch.name
|
|
33
32
|
}
|
|
34
|
-
const globalMatch =
|
|
35
|
-
items.some(
|
|
33
|
+
const globalMatch = tabs.find(tab =>
|
|
34
|
+
tab.items.some(({ shortcut }) => shortcut.context === 'global')
|
|
36
35
|
)
|
|
37
|
-
return globalMatch?.
|
|
36
|
+
return globalMatch?.name ?? tabs[0].name
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
// eslint-disable-next-line camelcase, react/jsx-pascal-case
|
|
@@ -46,10 +45,13 @@ export const KeyboardHelp = ({ context = 'viewport' }) => { // NOSONAR: project
|
|
|
46
45
|
const shortcuts = listboxIsActive
|
|
47
46
|
? allShortcuts
|
|
48
47
|
: allShortcuts.filter(s => s.context !== 'listbox')
|
|
49
|
-
const groupMap = buildGroupMap(shortcuts)
|
|
50
|
-
const groupEntries = Object.entries(groupMap)
|
|
51
48
|
|
|
52
|
-
|
|
49
|
+
const tabs = groupIntoTabs({
|
|
50
|
+
items: shortcuts.map(shortcut => ({ id: shortcut.id, order: 0, tab: shortcut.group, shortcut })),
|
|
51
|
+
fallbackLabel: DEFAULT_GROUP
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
if (!tabs) {
|
|
53
55
|
return (
|
|
54
56
|
<div className='im-c-keyboard-help'>
|
|
55
57
|
<ShortcutList items={shortcuts} />
|
|
@@ -57,14 +59,14 @@ export const KeyboardHelp = ({ context = 'viewport' }) => { // NOSONAR: project
|
|
|
57
59
|
)
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
const
|
|
61
|
-
name,
|
|
62
|
-
content: <ShortcutList items={items} />
|
|
62
|
+
const tabProps = tabs.map(tab => ({
|
|
63
|
+
name: tab.name,
|
|
64
|
+
content: <ShortcutList items={tab.items.map(({ shortcut }) => shortcut)} />
|
|
63
65
|
}))
|
|
64
66
|
|
|
65
67
|
return (
|
|
66
68
|
<div className='im-c-keyboard-help'>
|
|
67
|
-
<Tabs tabs={
|
|
69
|
+
<Tabs tabs={tabProps} defaultTab={getDefaultTab(tabs, context)} />
|
|
68
70
|
</div>
|
|
69
71
|
)
|
|
70
72
|
}
|
|
@@ -116,11 +116,11 @@ describe('KeyboardHelp — tabs', () => {
|
|
|
116
116
|
expect(screen.getByRole('tab', { name: 'Group A' })).toHaveAttribute('aria-selected', 'true')
|
|
117
117
|
})
|
|
118
118
|
|
|
119
|
-
it('shows only the active tab panel content', () => {
|
|
119
|
+
it('shows only the active tab panel content — the inactive one stays in the DOM (so its aria-controls target exists) but hidden', () => {
|
|
120
120
|
getKeyboardShortcuts.mockReturnValue(allShortcuts)
|
|
121
121
|
render(<KeyboardHelp context='viewport' />)
|
|
122
|
-
expect(screen.getByText('Move')).
|
|
123
|
-
expect(screen.
|
|
122
|
+
expect(screen.getByText('Move')).toBeVisible()
|
|
123
|
+
expect(screen.getByText('Select')).not.toBeVisible()
|
|
124
124
|
})
|
|
125
125
|
|
|
126
126
|
it('switches tab and content on click', () => {
|
|
@@ -128,8 +128,8 @@ describe('KeyboardHelp — tabs', () => {
|
|
|
128
128
|
render(<KeyboardHelp context='viewport' />)
|
|
129
129
|
fireEvent.click(screen.getByRole('tab', { name: SELECT_FEATURES_GROUP }))
|
|
130
130
|
expect(screen.getByRole('tab', { name: SELECT_FEATURES_GROUP })).toHaveAttribute('aria-selected', 'true')
|
|
131
|
-
expect(screen.getByText('Select')).
|
|
132
|
-
expect(screen.
|
|
131
|
+
expect(screen.getByText('Select')).toBeVisible()
|
|
132
|
+
expect(screen.getByText('Move')).not.toBeVisible()
|
|
133
133
|
})
|
|
134
134
|
|
|
135
135
|
it('ungrouped shortcuts default to Navigate group', () => {
|
|
@@ -141,6 +141,23 @@ describe('KeyboardHelp — tabs', () => {
|
|
|
141
141
|
fireEvent.click(screen.getByRole('tab', { name: DEFAULT_GROUP }))
|
|
142
142
|
expect(screen.getByText('No group')).toBeInTheDocument()
|
|
143
143
|
})
|
|
144
|
+
|
|
145
|
+
it('merges group names that differ only by case/whitespace into one tab (groupIntoTabs)', () => {
|
|
146
|
+
getKeyboardShortcuts.mockReturnValue([
|
|
147
|
+
{ id: 'a', group: 'Select Features', context: 'listbox', title: 'Shortcut A', command: '<kbd>Ctrl+A</kbd>' },
|
|
148
|
+
{ id: 'b', group: 'select features', context: 'listbox', title: 'Shortcut B', command: '<kbd>Ctrl+B</kbd>' },
|
|
149
|
+
...VIEWPORT_SHORTCUTS
|
|
150
|
+
])
|
|
151
|
+
render(<KeyboardHelp context='listbox' />)
|
|
152
|
+
// Two groups present (Select Features, Navigate) — not one, so tabs render at all
|
|
153
|
+
expect(screen.getAllByRole('tab')).toHaveLength(2)
|
|
154
|
+
// First-encountered member's raw group string wins as the tab's displayed name
|
|
155
|
+
const tab = screen.getByRole('tab', { name: 'Select Features' })
|
|
156
|
+
expect(tab).toBeInTheDocument()
|
|
157
|
+
fireEvent.click(tab)
|
|
158
|
+
expect(screen.getByText('Shortcut A')).toBeVisible()
|
|
159
|
+
expect(screen.getByText('Shortcut B')).toBeVisible()
|
|
160
|
+
})
|
|
144
161
|
})
|
|
145
162
|
|
|
146
163
|
// ─── listboxIsActive filtering ────────────────────────────────────────────────
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
.im-c-move-controls__directions .im-c-button-wrapper--nudge-step-toggle {
|
|
47
47
|
grid-column: 2;
|
|
48
48
|
grid-row: 2;
|
|
49
|
+
z-index: 1;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
.im-c-move-controls__directions .im-c-map-button--pan-up svg {
|
|
@@ -66,24 +67,49 @@
|
|
|
66
67
|
// express a plus-shaped grid where the centre button touches all four neighbours —
|
|
67
68
|
// clip-path is set directly per button instead, clipping only the side(s) where it
|
|
68
69
|
// touches another button and leaving the exposed side(s) showing the shadow as normal.
|
|
69
|
-
.im-c-move-controls__directions .im-c-button-wrapper--pan-up .im-c-map-button
|
|
70
|
-
|
|
70
|
+
.im-c-move-controls__directions .im-c-button-wrapper--pan-up .im-c-map-button {
|
|
71
|
+
border-bottom-left-radius: 0;
|
|
72
|
+
border-bottom-right-radius: 0;
|
|
73
|
+
|
|
74
|
+
&::before {
|
|
75
|
+
clip-path: inset(-20px -20px 0 -20px); // touches nudge-step-toggle below
|
|
76
|
+
}
|
|
71
77
|
}
|
|
72
78
|
|
|
73
|
-
.im-c-move-controls__directions .im-c-button-wrapper--pan-left .im-c-map-button
|
|
74
|
-
|
|
79
|
+
.im-c-move-controls__directions .im-c-button-wrapper--pan-left .im-c-map-button {
|
|
80
|
+
border-top-right-radius: 0;
|
|
81
|
+
border-bottom-right-radius: 0;
|
|
82
|
+
|
|
83
|
+
&::before {
|
|
84
|
+
clip-path: inset(-20px 0 -20px -20px); // touches nudge-step-toggle to the right
|
|
85
|
+
}
|
|
75
86
|
}
|
|
76
87
|
|
|
77
|
-
.im-c-move-controls__directions .im-c-button-wrapper--pan-right .im-c-map-button
|
|
78
|
-
|
|
88
|
+
.im-c-move-controls__directions .im-c-button-wrapper--pan-right .im-c-map-button {
|
|
89
|
+
border-top-left-radius: 0;
|
|
90
|
+
border-bottom-left-radius: 0;
|
|
91
|
+
|
|
92
|
+
&::before {
|
|
93
|
+
clip-path: inset(-20px -20px -20px 0); // touches nudge-step-toggle to the left
|
|
94
|
+
}
|
|
79
95
|
}
|
|
80
96
|
|
|
81
|
-
.im-c-move-controls__directions .im-c-button-wrapper--pan-down .im-c-map-button
|
|
82
|
-
|
|
97
|
+
.im-c-move-controls__directions .im-c-button-wrapper--pan-down .im-c-map-button {
|
|
98
|
+
border-top-left-radius: 0;
|
|
99
|
+
border-top-right-radius: 0;
|
|
100
|
+
|
|
101
|
+
&::before {
|
|
102
|
+
clip-path: inset(0 -20px -20px -20px); // touches nudge-step-toggle above
|
|
103
|
+
}
|
|
83
104
|
}
|
|
84
105
|
|
|
85
|
-
.im-c-move-controls__directions .im-c-button-wrapper--nudge-step-toggle .im-c-map-button
|
|
86
|
-
|
|
106
|
+
.im-c-move-controls__directions .im-c-button-wrapper--nudge-step-toggle .im-c-map-button {
|
|
107
|
+
border-radius: 0;
|
|
108
|
+
outline: 1px solid var(--button-hover-color);
|
|
109
|
+
|
|
110
|
+
&::before {
|
|
111
|
+
clip-path: inset(0); // touches all four neighbours
|
|
112
|
+
}
|
|
87
113
|
}
|
|
88
114
|
|
|
89
115
|
// Same inverted "pressed" treatment already used for actions-slot toggle buttons
|
|
@@ -129,3 +155,8 @@
|
|
|
129
155
|
.im-c-move-controls__zoom .im-c-button-wrapper--nudge-zoom-out .im-c-map-button {
|
|
130
156
|
@include tools.border-focus-corner-override($corners: 'right');
|
|
131
157
|
}
|
|
158
|
+
|
|
159
|
+
// App dark modifiers
|
|
160
|
+
.im-o-app--dark-app .im-c-move-controls__directions .im-c-button-wrapper--nudge-step-toggle .im-c-map-button {
|
|
161
|
+
outline-offset: -1px;
|
|
162
|
+
}
|
|
@@ -5,6 +5,7 @@ import { stringToKebab } from '../../../utils/stringToKebab.js'
|
|
|
5
5
|
import { useModalPanelBehaviour } from '../../hooks/useModalPanelBehaviour.js'
|
|
6
6
|
import { useIsScrollable } from '../../hooks/useIsScrollable.js'
|
|
7
7
|
import { Icon } from '../Icon/Icon'
|
|
8
|
+
import { Tabs } from '../Tabs/Tabs.jsx'
|
|
8
9
|
|
|
9
10
|
const computePanelState = (bpConfig, triggeringElement, focus, focusOnOpen) => {
|
|
10
11
|
const isAside = bpConfig.slot === 'side' && bpConfig.open && !bpConfig.modal
|
|
@@ -56,9 +57,56 @@ const buildBodyProps = ({ bodyRef, panelBodyClass, isBodyScrollable, elementId }
|
|
|
56
57
|
'aria-labelledby': isBodyScrollable ? `${elementId}-label` : undefined
|
|
57
58
|
})
|
|
58
59
|
|
|
60
|
+
// Renders the panel body's content: an ordered list of items (own content plus any
|
|
61
|
+
// controls injected via the `<panelId>-panel` slot convention), or the legacy single
|
|
62
|
+
// WrappedChild/children shape for callers that don't build an items list (e.g. HtmlElementHost).
|
|
63
|
+
// Tabbed content is handled separately by Panel itself — see the tabs handling below — since
|
|
64
|
+
// the tablist needs to render outside this component's scrollable body, not inside it.
|
|
65
|
+
const BodyContent = ({ items, WrappedChild, props, children }) => { // NOSONAR
|
|
66
|
+
if (items) {
|
|
67
|
+
return items.map(item => <React.Fragment key={item.id}>{item.element}</React.Fragment>)
|
|
68
|
+
}
|
|
69
|
+
return WrappedChild ? <WrappedChild {...props} /> : children
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Chooses between the three body shapes: static html, tabbed, or the flat BodyContent above.
|
|
73
|
+
// .im-c-panel__body (bodyProps) always wraps everything, completely unchanged — same padding/
|
|
74
|
+
// overflow/box role it's always had, which is also what the tablist and tabpanel need to pick
|
|
75
|
+
// up their own inset from by simply being nested inside it. Only the scrollable-region
|
|
76
|
+
// behaviour (ref/tabIndex/role/aria-labelledby) moves down onto <Tabs>' own tabpanel div via
|
|
77
|
+
// panelProps when tabbed, since that's the part that should actually scroll/receive focus —
|
|
78
|
+
// the tablist (rendered by <Tabs> itself, ahead of the tabpanel) stays outside that inner
|
|
79
|
+
// scroll boundary so it can't scroll away with long tab content. The tabpanel's own focus ring
|
|
80
|
+
// clearance (so it isn't flush against its content) is a CSS-only concern — see
|
|
81
|
+
// Panel.module.scss's .im-c-tabs__panel override.
|
|
82
|
+
const PanelBody = ({ innerHtmlProp, tabs, items, WrappedChild, props, children, bodyProps, panelBodySlot }) => { // NOSONAR
|
|
83
|
+
if (innerHtmlProp) {
|
|
84
|
+
return <div {...bodyProps} dangerouslySetInnerHTML={innerHtmlProp} /> // nosonar
|
|
85
|
+
}
|
|
86
|
+
const { className, ...scrollableProps } = bodyProps
|
|
87
|
+
if (tabs) {
|
|
88
|
+
return (
|
|
89
|
+
<div className={className} data-panel-slot={panelBodySlot}>
|
|
90
|
+
<Tabs
|
|
91
|
+
tabs={tabs.map(tab => ({
|
|
92
|
+
name: tab.name,
|
|
93
|
+
content: tab.items.map(item => <React.Fragment key={item.id}>{item.element}</React.Fragment>)
|
|
94
|
+
}))}
|
|
95
|
+
panelProps={scrollableProps}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
return (
|
|
101
|
+
<div {...bodyProps} data-panel-slot={panelBodySlot}> {/* nosonar */}
|
|
102
|
+
<BodyContent items={items} WrappedChild={WrappedChild} props={props}>{children}</BodyContent>
|
|
103
|
+
</div>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
59
107
|
// eslint-disable-next-line camelcase, react/jsx-pascal-case
|
|
60
108
|
// sonarjs/disable-next-line function-name
|
|
61
|
-
export const Panel = ({ panelId, panelConfig, props, focusOnOpen, WrappedChild, label, html, children, isOpen = true, rootRef }) => {
|
|
109
|
+
export const Panel = ({ panelId, panelConfig, props, focusOnOpen, WrappedChild, items, tabs, label, html, children, isOpen = true, rootRef }) => {
|
|
62
110
|
const { id } = useConfig()
|
|
63
111
|
const { dispatch, breakpoint, layoutRefs, interfaceType } = useApp()
|
|
64
112
|
|
|
@@ -103,6 +151,10 @@ export const Panel = ({ panelId, panelConfig, props, focusOnOpen, WrappedChild,
|
|
|
103
151
|
|
|
104
152
|
const panelProps = buildPanelProps({ elementId, shouldFocus, isDialog, isDismissible, isModal, width: bpConfig.width, panelClass, slot: bpConfig.slot })
|
|
105
153
|
const bodyProps = buildBodyProps({ bodyRef, panelBodyClass, isBodyScrollable, elementId })
|
|
154
|
+
// DOM anchor for controls DOM-projected via the JS/consumer-HTML API — see HtmlElementHost.jsx.
|
|
155
|
+
// Only present on the items-capable body below: dangerouslySetInnerHTML owns the static-html
|
|
156
|
+
// body's children, so it can't also host controls injected via the panel-slot convention.
|
|
157
|
+
const panelBodySlot = `${stringToKebab(panelId)}-panel`
|
|
106
158
|
|
|
107
159
|
return (
|
|
108
160
|
<div // nosonar
|
|
@@ -118,6 +170,7 @@ export const Panel = ({ panelId, panelConfig, props, focusOnOpen, WrappedChild,
|
|
|
118
170
|
|
|
119
171
|
{isDismissible && (
|
|
120
172
|
<button
|
|
173
|
+
type='button'
|
|
121
174
|
aria-label={`Close ${label}`}
|
|
122
175
|
className='im-c-panel__close'
|
|
123
176
|
onClick={handleClose}
|
|
@@ -126,13 +179,17 @@ export const Panel = ({ panelId, panelConfig, props, focusOnOpen, WrappedChild,
|
|
|
126
179
|
</button>
|
|
127
180
|
)}
|
|
128
181
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
182
|
+
<PanelBody
|
|
183
|
+
innerHtmlProp={innerHtmlProp}
|
|
184
|
+
tabs={tabs}
|
|
185
|
+
items={items}
|
|
186
|
+
WrappedChild={WrappedChild}
|
|
187
|
+
props={props}
|
|
188
|
+
bodyProps={bodyProps}
|
|
189
|
+
panelBodySlot={panelBodySlot}
|
|
190
|
+
>
|
|
191
|
+
{children}
|
|
192
|
+
</PanelBody>
|
|
136
193
|
</div>
|
|
137
194
|
)
|
|
138
195
|
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
min-height: 0;
|
|
19
19
|
min-width: var(--min-panel-width);
|
|
20
20
|
pointer-events: auto;
|
|
21
|
+
color: var(--foreground-color);
|
|
21
22
|
background-color: var(--background-color);
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -58,15 +59,39 @@
|
|
|
58
59
|
.im-c-panel__body {
|
|
59
60
|
padding: 0 var(--panel-margin) var(--panel-margin);
|
|
60
61
|
overflow: auto;
|
|
62
|
+
// display: flex so a tabbed .im-c-tabs child (see Tabs.module.scss) has a flex context to
|
|
63
|
+
// size itself within — its tablist keeps its natural height, only its tabpanel grows and
|
|
64
|
+
// scrolls internally, rather than this whole element (tablist included) scrolling as one.
|
|
65
|
+
// A single non-tabbed child is unaffected — one flex item in a column sizes the same as a
|
|
66
|
+
// plain block child would.
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The tabpanel (see Tabs.module.scss) is its own scroll/focus region nested inside this
|
|
72
|
+
// already-padded wrapper — its content already sits correctly inset just by being nested, but
|
|
73
|
+
// its own border box (where the focus ring is drawn, and where its own overflow: auto clips)
|
|
74
|
+
// would otherwise sit tight against that content, with no room of its own. Reaching out with a
|
|
75
|
+
// negative margin — matching this wrapper's own padding — and re-padding by the same amount
|
|
76
|
+
// gives it that room without moving the content: the negative margin and the padding cancel
|
|
77
|
+
// out visually, but the border box itself is now larger. This is a panel-specific override —
|
|
78
|
+
// <Tabs> used anywhere else would need the same treatment if this clearance matters there too.
|
|
79
|
+
.im-c-panel__body .im-c-tabs__panel {
|
|
80
|
+
margin: 0 calc(-1 * var(--panel-margin)) calc(-1 * var(--panel-margin));
|
|
81
|
+
padding: 0 var(--panel-margin) var(--panel-margin);
|
|
82
|
+
}
|
|
61
83
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
84
|
+
// The scrollable-region focus ring — normally on .im-c-panel__body itself, but when tabbed,
|
|
85
|
+
// the tabpanel (see Tabs.module.scss's .im-c-tabs__panel) is the element that's actually
|
|
86
|
+
// focusable/scrollable instead (see Panel.jsx's PanelBody), so it needs the same treatment.
|
|
87
|
+
.im-c-panel__body[data-focus-visible="true"],
|
|
88
|
+
.im-c-tabs__panel[data-focus-visible="true"] {
|
|
89
|
+
outline: var(--scrollable-outline);
|
|
90
|
+
outline-offset: var(--scrollable-outline-offset);
|
|
91
|
+
box-shadow: var(--scrollable-box-shadow);
|
|
66
92
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
93
|
+
@media (-ms-high-contrast:active), screen and (forced-colors: active) {
|
|
94
|
+
outline-width: 4px;
|
|
70
95
|
}
|
|
71
96
|
}
|
|
72
97
|
|
|
@@ -98,19 +123,28 @@
|
|
|
98
123
|
);
|
|
99
124
|
}
|
|
100
125
|
|
|
101
|
-
//
|
|
102
|
-
.im-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
126
|
+
// Banner gets small rounded corners instead of the default square panel look.
|
|
127
|
+
.im-c-panel--banner {
|
|
128
|
+
&,
|
|
129
|
+
&::before {
|
|
130
|
+
border-radius: var(--banner-border-radius);
|
|
106
131
|
}
|
|
107
132
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
padding-bottom: 10px;
|
|
133
|
+
&::after {
|
|
134
|
+
border-radius: calc(var(--banner-border-radius) - 2px);
|
|
111
135
|
}
|
|
112
136
|
}
|
|
113
137
|
|
|
138
|
+
// No heading means no built-in top spacing, so add it back here.
|
|
139
|
+
.im-c-panel--no-heading .im-c-panel__body {
|
|
140
|
+
padding-top: var(--panel-margin);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Ensures the body is tall enough to contain the close button.
|
|
144
|
+
.im-c-panel--no-heading .im-c-panel__body--offset {
|
|
145
|
+
min-height: calc((var(--panel-close-button-gap) * 2) + var(--button-size));
|
|
146
|
+
}
|
|
147
|
+
|
|
114
148
|
.im-c-panel__body--offset {
|
|
115
149
|
padding-right: calc(var(--button-size) + var(--divider-gap));
|
|
116
150
|
}
|