@defra/interactive-map 0.0.40-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/assets/images/slot-map.svg +61 -262
- package/docs/examples/draw-tools.mdx +3 -3
- package/docs/plugins/draw.md +79 -16
- 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/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.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/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 +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/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 +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/MoveControls/MoveControls.module.scss +41 -10
- 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/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/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 +1 -0
- package/src/App/store/appDispatchMiddleware.js +1 -1
- package/src/App/store/appDispatchMiddleware.test.js +8 -0
- 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 +11 -1
- 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
|
@@ -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 />)
|
|
@@ -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
|
+
}
|
|
@@ -56,9 +56,19 @@ const buildBodyProps = ({ bodyRef, panelBodyClass, isBodyScrollable, elementId }
|
|
|
56
56
|
'aria-labelledby': isBodyScrollable ? `${elementId}-label` : undefined
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
+
// Renders the panel body's content: an ordered list of items (own content plus any
|
|
60
|
+
// controls injected via the `<panelId>-panel` slot convention), or the legacy single
|
|
61
|
+
// WrappedChild/children shape for callers that don't build an items list (e.g. HtmlElementHost).
|
|
62
|
+
const BodyContent = ({ items, WrappedChild, props, children }) => { // NOSONAR
|
|
63
|
+
if (items) {
|
|
64
|
+
return items.map(item => <React.Fragment key={item.id}>{item.element}</React.Fragment>)
|
|
65
|
+
}
|
|
66
|
+
return WrappedChild ? <WrappedChild {...props} /> : children
|
|
67
|
+
}
|
|
68
|
+
|
|
59
69
|
// eslint-disable-next-line camelcase, react/jsx-pascal-case
|
|
60
70
|
// sonarjs/disable-next-line function-name
|
|
61
|
-
export const Panel = ({ panelId, panelConfig, props, focusOnOpen, WrappedChild, label, html, children, isOpen = true, rootRef }) => {
|
|
71
|
+
export const Panel = ({ panelId, panelConfig, props, focusOnOpen, WrappedChild, items, label, html, children, isOpen = true, rootRef }) => {
|
|
62
72
|
const { id } = useConfig()
|
|
63
73
|
const { dispatch, breakpoint, layoutRefs, interfaceType } = useApp()
|
|
64
74
|
|
|
@@ -103,6 +113,10 @@ export const Panel = ({ panelId, panelConfig, props, focusOnOpen, WrappedChild,
|
|
|
103
113
|
|
|
104
114
|
const panelProps = buildPanelProps({ elementId, shouldFocus, isDialog, isDismissible, isModal, width: bpConfig.width, panelClass, slot: bpConfig.slot })
|
|
105
115
|
const bodyProps = buildBodyProps({ bodyRef, panelBodyClass, isBodyScrollable, elementId })
|
|
116
|
+
// DOM anchor for controls DOM-projected via the JS/consumer-HTML API — see HtmlElementHost.jsx.
|
|
117
|
+
// Only present on the items-capable body below: dangerouslySetInnerHTML owns the static-html
|
|
118
|
+
// body's children, so it can't also host controls injected via the panel-slot convention.
|
|
119
|
+
const panelBodySlot = `${stringToKebab(panelId)}-panel`
|
|
106
120
|
|
|
107
121
|
return (
|
|
108
122
|
<div // nosonar
|
|
@@ -118,6 +132,7 @@ export const Panel = ({ panelId, panelConfig, props, focusOnOpen, WrappedChild,
|
|
|
118
132
|
|
|
119
133
|
{isDismissible && (
|
|
120
134
|
<button
|
|
135
|
+
type='button'
|
|
121
136
|
aria-label={`Close ${label}`}
|
|
122
137
|
className='im-c-panel__close'
|
|
123
138
|
onClick={handleClose}
|
|
@@ -129,8 +144,8 @@ export const Panel = ({ panelId, panelConfig, props, focusOnOpen, WrappedChild,
|
|
|
129
144
|
{innerHtmlProp
|
|
130
145
|
? <div {...bodyProps} dangerouslySetInnerHTML={innerHtmlProp} /> // nosonar
|
|
131
146
|
: (
|
|
132
|
-
<div {...bodyProps}> {/* nosonar */}
|
|
133
|
-
{
|
|
147
|
+
<div {...bodyProps} data-panel-slot={panelBodySlot}> {/* nosonar */}
|
|
148
|
+
<BodyContent items={items} WrappedChild={WrappedChild} props={props}>{children}</BodyContent>
|
|
134
149
|
</div>
|
|
135
150
|
)}
|
|
136
151
|
</div>
|
|
@@ -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
|
|
|
@@ -98,19 +99,28 @@
|
|
|
98
99
|
);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
//
|
|
102
|
-
.im-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
// Banner gets small rounded corners instead of the default square panel look.
|
|
103
|
+
.im-c-panel--banner {
|
|
104
|
+
&,
|
|
105
|
+
&::before {
|
|
106
|
+
border-radius: var(--banner-border-radius);
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
padding-bottom: 10px;
|
|
109
|
+
&::after {
|
|
110
|
+
border-radius: calc(var(--banner-border-radius) - 2px);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// No heading means no built-in top spacing, so add it back here.
|
|
115
|
+
.im-c-panel--no-heading .im-c-panel__body {
|
|
116
|
+
padding-top: var(--panel-margin);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Ensures the body is tall enough to contain the close button.
|
|
120
|
+
.im-c-panel--no-heading .im-c-panel__body--offset {
|
|
121
|
+
min-height: calc((var(--panel-close-button-gap) * 2) + var(--button-size));
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
.im-c-panel__body--offset {
|
|
115
125
|
padding-right: calc(var(--button-size) + var(--divider-gap));
|
|
116
126
|
}
|
|
@@ -194,5 +194,39 @@ describe('Panel', () => {
|
|
|
194
194
|
renderPanel({}, { html: '<p>HTML content</p>' })
|
|
195
195
|
expect(screen.getByText('HTML content')).toBeInTheDocument()
|
|
196
196
|
})
|
|
197
|
+
|
|
198
|
+
it('renders each items entry in list order', () => {
|
|
199
|
+
const items = [
|
|
200
|
+
{ id: 'a', element: <p>First</p> },
|
|
201
|
+
{ id: 'b', element: <p>Second</p> }
|
|
202
|
+
]
|
|
203
|
+
renderPanel({}, { items })
|
|
204
|
+
expect(screen.getByText('First')).toBeInTheDocument()
|
|
205
|
+
expect(screen.getByText('Second')).toBeInTheDocument()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('prefers items over WrappedChild/children when provided', () => {
|
|
209
|
+
const Wrapped = () => <p>Wrapped</p>
|
|
210
|
+
renderPanel({}, {
|
|
211
|
+
items: [{ id: 'a', element: <p>Item</p> }],
|
|
212
|
+
WrappedChild: Wrapped,
|
|
213
|
+
children: <p>Child content</p>
|
|
214
|
+
})
|
|
215
|
+
expect(screen.getByText('Item')).toBeInTheDocument()
|
|
216
|
+
expect(screen.queryByText('Wrapped')).not.toBeInTheDocument()
|
|
217
|
+
expect(screen.queryByText('Child content')).not.toBeInTheDocument()
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('stamps data-panel-slot on the items-capable body, for controls DOM-projected via the JS API', () => {
|
|
221
|
+
const { container } = renderPanel({}, { items: [{ id: 'a', element: <p>Item</p> }] })
|
|
222
|
+
const body = container.querySelector('.im-c-panel__body')
|
|
223
|
+
expect(body).toHaveAttribute('data-panel-slot', 'settings-panel')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('does not stamp data-panel-slot on a static-html body', () => {
|
|
227
|
+
const { container } = renderPanel({}, { html: '<p>HTML content</p>' })
|
|
228
|
+
const body = container.querySelector('.im-c-panel__body')
|
|
229
|
+
expect(body).not.toHaveAttribute('data-panel-slot')
|
|
230
|
+
})
|
|
197
231
|
})
|
|
198
232
|
})
|
|
@@ -45,34 +45,32 @@ describe('Tooltip', () => {
|
|
|
45
45
|
|
|
46
46
|
it('shows and hides tooltip on mouse interactions', () => {
|
|
47
47
|
const { button, tooltip } = renderTooltip()
|
|
48
|
-
|
|
48
|
+
fireEvent.mouseEnter(button)
|
|
49
49
|
advanceTimers()
|
|
50
50
|
expect(tooltip).toHaveClass('im-c-tooltip--is-visible')
|
|
51
51
|
expect(getTooltipPosition).toHaveBeenCalled()
|
|
52
|
-
|
|
52
|
+
fireEvent.mouseLeave(button)
|
|
53
53
|
advanceTimers(0)
|
|
54
54
|
expect(tooltip).not.toHaveClass('im-c-tooltip--is-visible')
|
|
55
55
|
})
|
|
56
56
|
|
|
57
57
|
it('cancels tooltip on mouse down or key down', () => {
|
|
58
58
|
const { button, tooltip } = renderTooltip()
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
fireEvent.keyDown(button)
|
|
64
|
-
})
|
|
59
|
+
fireEvent.mouseEnter(button)
|
|
60
|
+
fireEvent.mouseDown(button)
|
|
61
|
+
fireEvent.mouseEnter(button)
|
|
62
|
+
fireEvent.keyDown(button)
|
|
65
63
|
advanceTimers()
|
|
66
64
|
expect(tooltip).not.toHaveClass('im-c-tooltip--is-visible')
|
|
67
65
|
})
|
|
68
66
|
|
|
69
67
|
it('handles focus interactions for keyboard interface', () => {
|
|
70
68
|
const { button, tooltip, wrapper } = renderTooltip({}, 'keyboard')
|
|
71
|
-
|
|
69
|
+
fireEvent.focus(button)
|
|
72
70
|
advanceTimers()
|
|
73
71
|
expect(wrapper).toHaveClass('im-c-tooltip-wrapper--has-focus')
|
|
74
72
|
expect(tooltip).toHaveClass('im-c-tooltip--is-visible')
|
|
75
|
-
|
|
73
|
+
fireEvent.blur(button)
|
|
76
74
|
advanceTimers(0)
|
|
77
75
|
expect(wrapper).not.toHaveClass('im-c-tooltip-wrapper--has-focus')
|
|
78
76
|
expect(tooltip).not.toHaveClass('im-c-tooltip--is-visible')
|
|
@@ -80,22 +78,22 @@ describe('Tooltip', () => {
|
|
|
80
78
|
|
|
81
79
|
it('does not show tooltip on focus if interfaceType is mouse', () => {
|
|
82
80
|
const { button, tooltip } = renderTooltip()
|
|
83
|
-
|
|
81
|
+
fireEvent.focus(button)
|
|
84
82
|
advanceTimers()
|
|
85
83
|
expect(tooltip).not.toHaveClass('im-c-tooltip--is-visible')
|
|
86
84
|
})
|
|
87
85
|
|
|
88
86
|
it('hides tooltip on Escape key', () => {
|
|
89
87
|
const { button, tooltip } = renderTooltip()
|
|
90
|
-
|
|
88
|
+
fireEvent.mouseEnter(button)
|
|
91
89
|
advanceTimers()
|
|
92
|
-
|
|
90
|
+
fireEvent.keyDown(window, { key: 'Escape' })
|
|
93
91
|
expect(tooltip).not.toHaveClass('im-c-tooltip--is-visible')
|
|
94
92
|
})
|
|
95
93
|
|
|
96
94
|
it('removes keydown listener on unmount', () => {
|
|
97
95
|
const { button, unmount } = renderTooltip({}, 'keyboard')
|
|
98
|
-
|
|
96
|
+
fireEvent.mouseEnter(button)
|
|
99
97
|
advanceTimers()
|
|
100
98
|
const removeSpy = jest.spyOn(window, 'removeEventListener')
|
|
101
99
|
unmount()
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
2
|
|
|
3
|
+
const SET_FEATURES = 'map:setfeatures'
|
|
4
|
+
const SET_FEATURES_SUPPRESSED = 'map:setfeaturessuppressed'
|
|
5
|
+
|
|
3
6
|
/**
|
|
4
|
-
* Subscribes to map:setfeatures
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
+
* Subscribes to map:setfeatures for the listbox item list, and map:setfeaturessuppressed
|
|
8
|
+
* (any plugin can emit this) to report items as empty while suppressed regardless of what's
|
|
9
|
+
* actually visible — which is what drives <Features>'s own tabIndex/aria-hidden.
|
|
7
10
|
*
|
|
8
11
|
* @param {object} eventBus
|
|
9
12
|
* @returns {{ items: Array<{ id: string, label: string }>, multiselectable: boolean }}
|
|
@@ -11,20 +14,26 @@ import { useState, useEffect } from 'react'
|
|
|
11
14
|
export function useFeatureItems (eventBus) {
|
|
12
15
|
const [items, setItems] = useState([])
|
|
13
16
|
const [multiselectable, setMultiselectable] = useState(false)
|
|
17
|
+
const [suppressed, setSuppressed] = useState(false)
|
|
14
18
|
|
|
15
19
|
useEffect(() => {
|
|
16
20
|
if (!eventBus) {
|
|
17
21
|
return undefined
|
|
18
22
|
}
|
|
19
|
-
const
|
|
23
|
+
const handleSetFeatures = ({ items: next = [], multiselectable: nextMultiselectable = false }) => {
|
|
20
24
|
setItems(next)
|
|
21
25
|
setMultiselectable(nextMultiselectable)
|
|
22
26
|
}
|
|
23
|
-
|
|
27
|
+
const handleSuppressed = ({ suppressed: next = false } = {}) => {
|
|
28
|
+
setSuppressed(next)
|
|
29
|
+
}
|
|
30
|
+
eventBus.on(SET_FEATURES, handleSetFeatures)
|
|
31
|
+
eventBus.on(SET_FEATURES_SUPPRESSED, handleSuppressed)
|
|
24
32
|
return () => {
|
|
25
|
-
eventBus.off(
|
|
33
|
+
eventBus.off(SET_FEATURES, handleSetFeatures)
|
|
34
|
+
eventBus.off(SET_FEATURES_SUPPRESSED, handleSuppressed)
|
|
26
35
|
}
|
|
27
36
|
}, [eventBus])
|
|
28
37
|
|
|
29
|
-
return { items, multiselectable }
|
|
38
|
+
return { items: suppressed ? [] : items, multiselectable }
|
|
30
39
|
}
|
|
@@ -2,6 +2,7 @@ import { renderHook, act } from '@testing-library/react'
|
|
|
2
2
|
import { useFeatureItems } from './useFeatureItems.js'
|
|
3
3
|
|
|
4
4
|
const SET_FEATURES = 'map:setfeatures' // NOSONAR
|
|
5
|
+
const SET_FEATURES_SUPPRESSED = 'map:setfeaturessuppressed' // NOSONAR
|
|
5
6
|
|
|
6
7
|
const makeEventBus = () => {
|
|
7
8
|
const listeners = {}
|
|
@@ -37,11 +38,18 @@ describe('useFeatureItems — event subscription', () => {
|
|
|
37
38
|
expect(eb.on).toHaveBeenCalledWith(SET_FEATURES, expect.any(Function))
|
|
38
39
|
})
|
|
39
40
|
|
|
41
|
+
it('subscribes to map:setfeaturessuppressed on mount', () => {
|
|
42
|
+
const eb = makeEventBus()
|
|
43
|
+
renderHook(() => useFeatureItems(eb))
|
|
44
|
+
expect(eb.on).toHaveBeenCalledWith(SET_FEATURES_SUPPRESSED, expect.any(Function))
|
|
45
|
+
})
|
|
46
|
+
|
|
40
47
|
it('unsubscribes on unmount', () => {
|
|
41
48
|
const eb = makeEventBus()
|
|
42
49
|
const { unmount } = renderHook(() => useFeatureItems(eb))
|
|
43
50
|
unmount()
|
|
44
51
|
expect(eb.off).toHaveBeenCalledWith(SET_FEATURES, expect.any(Function))
|
|
52
|
+
expect(eb.off).toHaveBeenCalledWith(SET_FEATURES_SUPPRESSED, expect.any(Function))
|
|
45
53
|
})
|
|
46
54
|
|
|
47
55
|
it('does not subscribe when eventBus is undefined', () => {
|
|
@@ -96,3 +104,52 @@ describe('useFeatureItems — multiselectable updates', () => {
|
|
|
96
104
|
expect(result.current.multiselectable).toBe(false)
|
|
97
105
|
})
|
|
98
106
|
})
|
|
107
|
+
|
|
108
|
+
// ─── useFeatureItems — suppression ───────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
describe('useFeatureItems — suppression', () => {
|
|
111
|
+
const items = [{ id: 'a', label: 'Feature A' }]
|
|
112
|
+
|
|
113
|
+
it('reports empty items while suppressed, regardless of what was last set', () => {
|
|
114
|
+
const eb = makeEventBus()
|
|
115
|
+
const { result } = renderHook(() => useFeatureItems(eb))
|
|
116
|
+
act(() => eb.emit(SET_FEATURES, { items }))
|
|
117
|
+
act(() => eb.emit(SET_FEATURES_SUPPRESSED, { suppressed: true }))
|
|
118
|
+
expect(result.current.items).toEqual([])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('restores the last known items once suppression is lifted', () => {
|
|
122
|
+
const eb = makeEventBus()
|
|
123
|
+
const { result } = renderHook(() => useFeatureItems(eb))
|
|
124
|
+
act(() => eb.emit(SET_FEATURES, { items }))
|
|
125
|
+
act(() => eb.emit(SET_FEATURES_SUPPRESSED, { suppressed: true }))
|
|
126
|
+
act(() => eb.emit(SET_FEATURES_SUPPRESSED, { suppressed: false }))
|
|
127
|
+
expect(result.current.items).toEqual(items)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('defaults suppressed to false when the payload omits it', () => {
|
|
131
|
+
const eb = makeEventBus()
|
|
132
|
+
const { result } = renderHook(() => useFeatureItems(eb))
|
|
133
|
+
act(() => eb.emit(SET_FEATURES, { items }))
|
|
134
|
+
act(() => eb.emit(SET_FEATURES_SUPPRESSED, {}))
|
|
135
|
+
expect(result.current.items).toEqual(items)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('defaults suppressed to false when the event carries no payload at all', () => {
|
|
139
|
+
const eb = makeEventBus()
|
|
140
|
+
const { result } = renderHook(() => useFeatureItems(eb))
|
|
141
|
+
act(() => eb.emit(SET_FEATURES, { items }))
|
|
142
|
+
act(() => eb.emit(SET_FEATURES_SUPPRESSED))
|
|
143
|
+
expect(result.current.items).toEqual(items)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('a features update received while suppressed is still applied once restored', () => {
|
|
147
|
+
const eb = makeEventBus()
|
|
148
|
+
const { result } = renderHook(() => useFeatureItems(eb))
|
|
149
|
+
act(() => eb.emit(SET_FEATURES_SUPPRESSED, { suppressed: true }))
|
|
150
|
+
act(() => eb.emit(SET_FEATURES, { items }))
|
|
151
|
+
expect(result.current.items).toEqual([]) // still suppressed
|
|
152
|
+
act(() => eb.emit(SET_FEATURES_SUPPRESSED, { suppressed: false }))
|
|
153
|
+
expect(result.current.items).toEqual(items)
|
|
154
|
+
})
|
|
155
|
+
})
|