@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
|
@@ -4,58 +4,61 @@ import { useApp } from '../store/appContext.js'
|
|
|
4
4
|
import { useMap } from '../store/mapContext.js'
|
|
5
5
|
import { getSafeZoneInset } from '../../utils/getSafeZoneInset.js'
|
|
6
6
|
|
|
7
|
+
const BANNER_DOCKED_CLASS = 'im-o-app__banner--docked'
|
|
8
|
+
const BANNER_PANEL_SELECTOR = '.im-c-panel--banner'
|
|
9
|
+
|
|
7
10
|
const buttonHeight = (ref) => ref?.current?.offsetHeight ?? 0
|
|
11
|
+
const buttonWidth = (ref) => ref?.current?.offsetWidth ?? 0
|
|
12
|
+
|
|
13
|
+
// Max of both sides, so centred content doesn't lean toward the emptier one.
|
|
14
|
+
const symmetricWidth = (left, right) => left || right ? Math.max(left, right) : 0
|
|
15
|
+
|
|
16
|
+
const subSlotMaxHeight = (columnHeight, siblingButtons, gap) => columnHeight - (siblingButtons ? siblingButtons + gap : 0)
|
|
17
|
+
|
|
18
|
+
// bottomRightHeight is 0 when empty, falling back to the attributions' own height for spacing.
|
|
19
|
+
const rightOffsetBottom = (containerPad, bottomRightHeight, attributionsHeight, gap) =>
|
|
20
|
+
containerPad + (bottomRightHeight > 0 ? bottomRightHeight + gap : attributionsHeight)
|
|
21
|
+
|
|
22
|
+
// Mobile's actions bar sits in flow (already reflected in baseBottom); tablet/desktop's floats, so this clears it too.
|
|
23
|
+
const hintBottom = (main, bottom, actionsEl, gap) => {
|
|
24
|
+
const baseBottom = main.offsetHeight - bottom.offsetTop - bottom.offsetHeight
|
|
25
|
+
const actionsHeight = actionsEl?.offsetHeight ?? 0
|
|
26
|
+
const actionsOffset = actionsHeight > 0 ? main.offsetHeight - actionsEl.offsetTop : 0
|
|
27
|
+
return Math.max(baseBottom, actionsOffset + gap)
|
|
28
|
+
}
|
|
8
29
|
|
|
9
|
-
|
|
10
|
-
|
|
30
|
+
// Space between .im-o-app__left/.im-o-app__right for the banner to dock in.
|
|
31
|
+
const bannerGutterWidth = (mainWidth, sideColWidth, gap) => mainWidth - (sideColWidth * 2) - (gap * 2)
|
|
11
32
|
|
|
12
|
-
const
|
|
13
|
-
|
|
33
|
+
const isBannerDocked = (gutterWidth, preferredWidth) => gutterWidth >= preferredWidth
|
|
34
|
+
|
|
35
|
+
// Widest explicit width configured on a banner panel, if any, overriding the default.
|
|
36
|
+
const bannerConfiguredWidth = (bannerEl) => {
|
|
37
|
+
const widths = Array.from(bannerEl?.querySelectorAll(BANNER_PANEL_SELECTOR) ?? [])
|
|
38
|
+
.map(el => Number.parseInt(el.style.width, 10))
|
|
39
|
+
.filter(w => !Number.isNaN(w))
|
|
40
|
+
return widths.length ? Math.max(...widths) : null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Mobile always stacks full-width, so any inline width Panel.jsx applied is ignored.
|
|
44
|
+
const clearBannerPanelWidths = (bannerEl) => {
|
|
45
|
+
bannerEl?.querySelectorAll(BANNER_PANEL_SELECTOR).forEach(el => { el.style.width = '' })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Docked insets to the side-column width; stacked is full-bleed.
|
|
49
|
+
const bannerInset = (isDocked, primaryGap, sideColWidth, gap) =>
|
|
50
|
+
isDocked ? primaryGap + sideColWidth + gap : primaryGap
|
|
14
51
|
|
|
15
52
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* ## Lifecycle
|
|
20
|
-
*
|
|
21
|
-
* The safe zone must only be dispatched once every plugin button's reactive
|
|
22
|
-
* props (`hiddenWhen`, `enableWhen`, `pressedWhen`, `expandedWhen`) have been
|
|
23
|
-
* evaluated for the current app/map state. Dispatching too early — before
|
|
24
|
-
* buttons that affect layout (e.g. the actions bar) have their correct
|
|
25
|
-
* visibility — produces a stale inset that causes the map to jump when the UI
|
|
26
|
-
* then settles into its real state.
|
|
27
|
-
*
|
|
28
|
-
* ### Trigger events
|
|
29
|
-
* The following state changes can alter which buttons are visible and therefore
|
|
30
|
-
* how much space the UI occupies:
|
|
31
|
-
* - `breakpoint` — responsive layout changes (desktop ↔ mobile / tablet)
|
|
32
|
-
* - `mapSize` — map container size variant changes
|
|
33
|
-
* - `isMapReady` — plugins are enabled on `map:ready`, changing button visibility
|
|
34
|
-
* - `isFullscreen` — fullscreen entry/exit changes which buttons are visible
|
|
35
|
-
* - `appVisible` — app shown/hidden by parent HTML outside React (hybrid mode)
|
|
36
|
-
*
|
|
37
|
-
* When any of these change, `CLEAR_PLUGINS_EVALUATED` is dispatched (Effect 2),
|
|
38
|
-
* which prevents the safe zone from being re-dispatched until
|
|
39
|
-
* `useButtonStateEvaluator` has completed a full pass with no button state
|
|
40
|
-
* changes and sets `PLUGINS_EVALUATED` again.
|
|
41
|
-
*
|
|
42
|
-
* ### Safe zone dispatch
|
|
43
|
-
* Effect 3 fires whenever `arePluginsEvaluated` transitions to `true`, at which
|
|
44
|
-
* point DOM dimensions are stable and `getSafeZoneInset` can be read reliably.
|
|
45
|
-
* A `requestAnimationFrame` is used to ensure the browser has committed all
|
|
46
|
-
* layout changes before measuring.
|
|
47
|
-
*
|
|
48
|
-
* ### Resize observer
|
|
49
|
-
* Effect 4 keeps CSS custom properties up to date whenever any observed element
|
|
50
|
-
* resizes (e.g. panels opening, banner appearing, actions buttons toggling).
|
|
51
|
-
* It does not dispatch the safe zone — safe zone dispatch is owned entirely by
|
|
52
|
-
* Effect 3 to prevent jumps on panel open/close and other non-structural resizes.
|
|
53
|
+
* Computes layout CSS vars for the map overlay and dispatches the safe zone inset used
|
|
54
|
+
* for `fitBounds`/`setView`. Waits for `arePluginsEvaluated` so the inset reflects final
|
|
55
|
+
* button visibility rather than a mid-evaluation state, which would make the map jump.
|
|
53
56
|
*/
|
|
54
|
-
function calculateLayout (layoutRefs) {
|
|
57
|
+
function calculateLayout (layoutRefs, breakpoint) {
|
|
55
58
|
const {
|
|
56
59
|
appContainerRef, mainRef, topRef, topLeftColRef, topRightColRef,
|
|
57
60
|
bottomRef, attributionsRef, bottomRightRef, leftTopRef, leftBottomRef,
|
|
58
|
-
rightTopRef, rightBottomRef, actionsRef
|
|
61
|
+
rightTopRef, rightBottomRef, actionsRef, bannerRef, leftRef, rightRef
|
|
59
62
|
} = layoutRefs
|
|
60
63
|
|
|
61
64
|
const appContainer = appContainerRef.current
|
|
@@ -65,6 +68,7 @@ function calculateLayout (layoutRefs) {
|
|
|
65
68
|
const topRightCol = topRightColRef.current
|
|
66
69
|
const bottom = bottomRef.current
|
|
67
70
|
const attributions = attributionsRef.current
|
|
71
|
+
const banner = bannerRef?.current
|
|
68
72
|
|
|
69
73
|
if ([main, top, bottom].some(r => !r)) {
|
|
70
74
|
return
|
|
@@ -72,41 +76,62 @@ function calculateLayout (layoutRefs) {
|
|
|
72
76
|
|
|
73
77
|
const root = document.documentElement
|
|
74
78
|
const dividerGap = Number.parseInt(getComputedStyle(root).getPropertyValue('--divider-gap'), 10)
|
|
79
|
+
const primaryGap = Number.parseInt(getComputedStyle(root).getPropertyValue('--primary-gap'), 10)
|
|
75
80
|
|
|
76
81
|
// === Top column width ===
|
|
77
|
-
|
|
82
|
+
const topColWidthPx = symmetricWidth(topLeftCol.offsetWidth, topRightCol.offsetWidth)
|
|
83
|
+
appContainer.style.setProperty('--top-col-width', `${topColWidthPx}px`)
|
|
84
|
+
|
|
85
|
+
// === Banner: docks centred between the side columns when there's room, otherwise
|
|
86
|
+
// stacks full-width. Mobile always stacks. ===
|
|
87
|
+
const isMobile = breakpoint === 'mobile'
|
|
88
|
+
if (isMobile) {
|
|
89
|
+
clearBannerPanelWidths(banner)
|
|
90
|
+
}
|
|
91
|
+
const bannerHeight = buttonHeight(bannerRef)
|
|
92
|
+
const hasBanner = bannerHeight > 0
|
|
93
|
+
const bannerSideColWidth = symmetricWidth(buttonWidth(leftRef), buttonWidth(rightRef))
|
|
94
|
+
const defaultPreferredWidth = Number.parseInt(getComputedStyle(root).getPropertyValue('--banner-preferred-width'), 10)
|
|
95
|
+
const preferredWidth = bannerConfiguredWidth(banner) ?? defaultPreferredWidth
|
|
96
|
+
appContainer.style.setProperty('--banner-preferred-width', `${preferredWidth}px`)
|
|
97
|
+
const gutterWidth = bannerGutterWidth(top.offsetWidth, bannerSideColWidth, dividerGap)
|
|
98
|
+
const isDocked = !isMobile && isBannerDocked(gutterWidth, preferredWidth)
|
|
99
|
+
banner?.classList.toggle(BANNER_DOCKED_CLASS, isDocked)
|
|
100
|
+
|
|
101
|
+
const bannerSideInset = `${bannerInset(isDocked, primaryGap, bannerSideColWidth, dividerGap)}px`
|
|
102
|
+
appContainer.style.setProperty('--banner-left', bannerSideInset)
|
|
103
|
+
appContainer.style.setProperty('--banner-right', bannerSideInset)
|
|
104
|
+
|
|
105
|
+
// Sits at the top row's bottom edge; top.offsetHeight already includes a trailing gap.
|
|
106
|
+
const isBannerStacked = hasBanner && !isDocked
|
|
107
|
+
const bannerTop = hasBanner ? top.offsetTop + top.offsetHeight : 0
|
|
108
|
+
appContainer.style.setProperty('--banner-top', `${bannerTop}px`)
|
|
109
|
+
|
|
110
|
+
// Stacked pushes the side columns below the banner plus a trailing gap — added here, not
|
|
111
|
+
// via a CSS last-child margin, since closed consumer HTML panels stay in the DOM (display:none).
|
|
112
|
+
const sideOffsetTop = (colHeight) => isBannerStacked
|
|
113
|
+
? bannerTop + bannerHeight + dividerGap
|
|
114
|
+
: colHeight + top.offsetTop
|
|
78
115
|
|
|
79
116
|
// === Left container offsets ===
|
|
80
|
-
const leftOffsetTop = topLeftCol.offsetHeight
|
|
117
|
+
const leftOffsetTop = sideOffsetTop(topLeftCol.offsetHeight)
|
|
81
118
|
const leftColumnHeight = bottom.offsetTop - leftOffsetTop - dividerGap
|
|
82
119
|
appContainer.style.setProperty('--left-offset-top', `${leftOffsetTop}px`)
|
|
83
120
|
appContainer.style.setProperty('--left-offset-bottom', `${main.offsetHeight - bottom.offsetTop + dividerGap}px`)
|
|
84
121
|
appContainer.style.setProperty('--left-top-max-height', `${leftColumnHeight}px`)
|
|
85
122
|
|
|
86
|
-
// === Right container offsets ===
|
|
87
|
-
|
|
88
|
-
// bottomRight.offsetHeight is 0 when no buttons so the offset collapses to just
|
|
89
|
-
// the padding between the bottom of the bottom container and the bottom of main.
|
|
90
|
-
const bottomRightHeight = bottomRightRef?.current?.offsetHeight ?? 0
|
|
123
|
+
// === Right container offsets === (mirrors the left formula)
|
|
124
|
+
const bottomRightHeight = buttonHeight(bottomRightRef)
|
|
91
125
|
const bottomContainerPad = main.offsetHeight - bottom.offsetTop - bottom.offsetHeight
|
|
92
|
-
const rightOffsetTop = topRightCol.offsetHeight
|
|
126
|
+
const rightOffsetTop = sideOffsetTop(topRightCol.offsetHeight)
|
|
93
127
|
const rightEffectiveBottom = bottom.offsetTop + bottom.offsetHeight - bottomRightHeight
|
|
94
128
|
const rightColumnHeight = rightEffectiveBottom - rightOffsetTop - dividerGap
|
|
95
|
-
const rightOffsetBottom = bottomContainerPad + (bottomRightHeight > 0 ? (bottomRightHeight + dividerGap) : attributions.offsetHeight)
|
|
96
129
|
appContainer.style.setProperty('--right-offset-top', `${rightOffsetTop}px`)
|
|
97
|
-
appContainer.style.setProperty('--right-offset-bottom', `${rightOffsetBottom}px`)
|
|
130
|
+
appContainer.style.setProperty('--right-offset-bottom', `${rightOffsetBottom(bottomContainerPad, bottomRightHeight, attributions.offsetHeight, dividerGap)}px`)
|
|
98
131
|
appContainer.style.setProperty('--right-top-max-height', `${rightColumnHeight}px`)
|
|
99
132
|
|
|
100
133
|
// === Keyboard hint bottom offset ===
|
|
101
|
-
|
|
102
|
-
// On tablet/desktop the actions bar is position:absolute (not in flow), so baseBottom
|
|
103
|
-
// only sees the bottom padding. actionsOffset measures the gap from the actions bar's
|
|
104
|
-
// top edge to the bottom of main, ensuring the hint always clears the floating bar.
|
|
105
|
-
const actionsEl = actionsRef?.current
|
|
106
|
-
const actionsHeight = actionsEl?.offsetHeight ?? 0
|
|
107
|
-
const baseBottom = main.offsetHeight - bottom.offsetTop - bottom.offsetHeight
|
|
108
|
-
const actionsOffset = actionsHeight > 0 ? main.offsetHeight - actionsEl.offsetTop : 0
|
|
109
|
-
appContainer.style.setProperty('--hint-bottom', `${Math.max(baseBottom, actionsOffset + dividerGap)}px`)
|
|
134
|
+
appContainer.style.setProperty('--hint-bottom', `${hintBottom(main, bottom, actionsRef?.current, dividerGap)}px`)
|
|
110
135
|
|
|
111
136
|
// === Sub-slot panel max-heights ===
|
|
112
137
|
appContainer.style.setProperty('--left-top-panel-max-height', `${subSlotMaxHeight(leftColumnHeight, buttonHeight(leftBottomRef), dividerGap)}px`)
|
|
@@ -119,28 +144,20 @@ export function useLayoutMeasurements () {
|
|
|
119
144
|
const { dispatch, breakpoint, layoutRefs, arePluginsEvaluated, appVisible, isFullscreen } = useApp()
|
|
120
145
|
const { mapSize, isMapReady } = useMap()
|
|
121
146
|
|
|
122
|
-
const { bannerRef, mainRef, topRef, topLeftColRef, topRightColRef, bottomRef, bottomRightRef, leftTopRef, leftBottomRef, rightTopRef, rightBottomRef, drawerRef, actionsRef } = layoutRefs
|
|
147
|
+
const { bannerRef, mainRef, headerRef, topRef, topLeftColRef, topRightColRef, bottomRef, bottomRightRef, leftTopRef, leftBottomRef, rightTopRef, rightBottomRef, drawerRef, actionsRef, leftRef, rightRef } = layoutRefs
|
|
123
148
|
|
|
124
|
-
//
|
|
125
|
-
// 1. Clear the evaluated flag when structural inputs change so the safe zone
|
|
126
|
-
// is not dispatched until useButtonStateEvaluator has completed a full
|
|
127
|
-
// pass with the new app/map state and set PLUGINS_EVALUATED.
|
|
128
|
-
// --------------------------------
|
|
149
|
+
// 1. Clear the evaluated flag on structural changes, gating the safe zone until re-evaluated.
|
|
129
150
|
useLayoutEffect(() => {
|
|
130
151
|
dispatch({ type: 'CLEAR_PLUGINS_EVALUATED' })
|
|
131
152
|
}, [breakpoint, mapSize, isMapReady, appVisible, isFullscreen])
|
|
132
153
|
|
|
133
|
-
//
|
|
134
|
-
// 2. Once all plugin button props have been evaluated (arePluginsEvaluated),
|
|
135
|
-
// recalculate layout and dispatch the safe zone inset.
|
|
136
|
-
// RAF required to ensure browser layout is committed before measuring.
|
|
137
|
-
// --------------------------------
|
|
154
|
+
// 2. Once evaluated, recalculate layout and dispatch the safe zone (RAF waits for layout to commit).
|
|
138
155
|
useLayoutEffect(() => {
|
|
139
156
|
if (!arePluginsEvaluated) {
|
|
140
157
|
return
|
|
141
158
|
}
|
|
142
159
|
requestAnimationFrame(() => {
|
|
143
|
-
calculateLayout(layoutRefs)
|
|
160
|
+
calculateLayout(layoutRefs, breakpoint)
|
|
144
161
|
const safeZoneInset = getSafeZoneInset(layoutRefs)
|
|
145
162
|
if (safeZoneInset) {
|
|
146
163
|
dispatch({ type: 'SET_SAFE_ZONE_INSET', payload: { safeZoneInset } })
|
|
@@ -148,20 +165,14 @@ export function useLayoutMeasurements () {
|
|
|
148
165
|
})
|
|
149
166
|
}, [arePluginsEvaluated])
|
|
150
167
|
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// actions buttons, etc.). Safe zone is intentionally not dispatched here —
|
|
154
|
-
// that is Effect 2's responsibility.
|
|
155
|
-
// --------------------------------
|
|
156
|
-
// Stable reference — all entries are ref objects from useApp that never change.
|
|
157
|
-
// Prevents useResizeObserver's effect from re-running (and cancelling its RAF)
|
|
158
|
-
// on every render due to a new array literal being passed each time.
|
|
168
|
+
// 3. Recalculate CSS vars on resize; safe zone dispatch stays Effect 2's job.
|
|
169
|
+
// Memoized so useResizeObserver doesn't re-run (and cancel its RAF) on every render.
|
|
159
170
|
const observedRefs = useMemo(
|
|
160
|
-
() => [bannerRef, mainRef, topRef, topLeftColRef, topRightColRef, actionsRef, bottomRef, bottomRightRef, leftTopRef, leftBottomRef, rightTopRef, rightBottomRef, drawerRef],
|
|
171
|
+
() => [bannerRef, mainRef, headerRef, topRef, topLeftColRef, topRightColRef, actionsRef, bottomRef, bottomRightRef, leftTopRef, leftBottomRef, rightTopRef, rightBottomRef, drawerRef, leftRef, rightRef],
|
|
161
172
|
[]
|
|
162
173
|
)
|
|
163
174
|
|
|
164
175
|
useResizeObserver(observedRefs, () => {
|
|
165
|
-
calculateLayout(layoutRefs)
|
|
176
|
+
calculateLayout(layoutRefs, breakpoint)
|
|
166
177
|
})
|
|
167
178
|
}
|
|
@@ -20,10 +20,13 @@ const el = (props = {}) => {
|
|
|
20
20
|
const refs = (o = {}) => ({
|
|
21
21
|
appContainerRef: { current: o.appContainer || el() },
|
|
22
22
|
mainRef: { current: o.main === null ? null : el({ offsetHeight: 500, ...o.main }) },
|
|
23
|
+
headerRef: { current: el(o.header) },
|
|
23
24
|
bannerRef: { current: el(o.banner) },
|
|
24
25
|
topRef: { current: o.top === null ? null : el({ offsetTop: 10, ...o.top }) },
|
|
25
26
|
topLeftColRef: { current: el({ offsetHeight: 50, offsetWidth: 200, ...o.topLeftCol }) },
|
|
26
27
|
topRightColRef: { current: el({ offsetHeight: 40, offsetWidth: 180, ...o.topRightCol }) },
|
|
28
|
+
leftRef: { current: el(o.left) },
|
|
29
|
+
rightRef: { current: el(o.right) },
|
|
27
30
|
bottomRef: { current: o.bottom === null ? null : el({ offsetTop: 400, ...o.bottom }) },
|
|
28
31
|
bottomRightRef: { current: el({ offsetTop: 400, ...o.bottomRight }) },
|
|
29
32
|
leftTopRef: { current: el({ offsetHeight: 0, ...o.leftTop }) },
|
|
@@ -140,6 +143,63 @@ describe('useLayoutMeasurements', () => {
|
|
|
140
143
|
expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--top-col-width', expected)
|
|
141
144
|
})
|
|
142
145
|
|
|
146
|
+
test('clears inline banner panel widths on mobile', () => {
|
|
147
|
+
const { layoutRefs } = setup({ app: { breakpoint: 'mobile' } })
|
|
148
|
+
const panel = document.createElement('div')
|
|
149
|
+
panel.className = 'im-c-panel--banner'
|
|
150
|
+
panel.style.width = '250px'
|
|
151
|
+
layoutRefs.bannerRef.current.appendChild(panel)
|
|
152
|
+
renderHook(() => useLayoutMeasurements())
|
|
153
|
+
expect(panel.style.width).toBe('')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('uses widest parseable banner panel width, ignoring unparseable ones', () => {
|
|
157
|
+
const { layoutRefs } = setup()
|
|
158
|
+
const banner = layoutRefs.bannerRef.current
|
|
159
|
+
const panel = (width) => {
|
|
160
|
+
const p = document.createElement('div')
|
|
161
|
+
p.className = 'im-c-panel--banner'
|
|
162
|
+
p.style.width = width
|
|
163
|
+
return p
|
|
164
|
+
}
|
|
165
|
+
banner.appendChild(panel('250px'))
|
|
166
|
+
banner.appendChild(panel('auto')) // unparseable, filtered out
|
|
167
|
+
banner.appendChild(panel('400px'))
|
|
168
|
+
renderHook(() => useLayoutMeasurements())
|
|
169
|
+
expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--banner-preferred-width', '400px')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('falls back to default preferred width when banner element is absent', () => {
|
|
173
|
+
const { layoutRefs } = setup()
|
|
174
|
+
layoutRefs.bannerRef.current = null
|
|
175
|
+
renderHook(() => useLayoutMeasurements())
|
|
176
|
+
expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--banner-preferred-width', '8px')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('docks the banner when the gutter is wide enough for the preferred width', () => {
|
|
180
|
+
const { layoutRefs } = setup({ refs: { top: { offsetWidth: 1000 } } })
|
|
181
|
+
renderHook(() => useLayoutMeasurements())
|
|
182
|
+
// docked: primaryGap (8) + sideColWidth (0) + dividerGap (8)
|
|
183
|
+
expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--banner-left', '16px')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('treats a null leftRef current as 0 width in the banner gutter calc', () => {
|
|
187
|
+
const { layoutRefs } = setup({ refs: { top: { offsetWidth: 1000 }, right: { offsetWidth: 20 } } })
|
|
188
|
+
layoutRefs.leftRef.current = null
|
|
189
|
+
renderHook(() => useLayoutMeasurements())
|
|
190
|
+
// symmetricWidth(0, 20) = 20; docked: primaryGap (8) + sideColWidth (20) + dividerGap (8)
|
|
191
|
+
expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--banner-left', '36px')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('stacks a present banner below the top row when not docked', () => {
|
|
195
|
+
const { layoutRefs } = setup({ refs: { banner: { offsetHeight: 30 } } })
|
|
196
|
+
renderHook(() => useLayoutMeasurements())
|
|
197
|
+
// bannerTop = top.offsetTop (10) + top.offsetHeight (0)
|
|
198
|
+
expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--banner-top', '10px')
|
|
199
|
+
// stacked: left column pushed below the banner (bannerTop 10 + bannerHeight 30 + dividerGap 8)
|
|
200
|
+
expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--left-offset-top', '48px')
|
|
201
|
+
})
|
|
202
|
+
|
|
143
203
|
test('uses 0 when bottomRightRef current is null', () => {
|
|
144
204
|
const { layoutRefs } = setup()
|
|
145
205
|
layoutRefs.bottomRightRef.current = null
|
|
@@ -264,7 +324,7 @@ describe('useLayoutMeasurements', () => {
|
|
|
264
324
|
const { layoutRefs } = setup()
|
|
265
325
|
renderHook(() => useLayoutMeasurements())
|
|
266
326
|
expect(useResizeObserver).toHaveBeenCalledWith(
|
|
267
|
-
[layoutRefs.bannerRef, layoutRefs.mainRef, layoutRefs.topRef, layoutRefs.topLeftColRef, layoutRefs.topRightColRef, layoutRefs.actionsRef, layoutRefs.bottomRef, layoutRefs.bottomRightRef, layoutRefs.leftTopRef, layoutRefs.leftBottomRef, layoutRefs.rightTopRef, layoutRefs.rightBottomRef, layoutRefs.drawerRef],
|
|
327
|
+
[layoutRefs.bannerRef, layoutRefs.mainRef, layoutRefs.headerRef, layoutRefs.topRef, layoutRefs.topLeftColRef, layoutRefs.topRightColRef, layoutRefs.actionsRef, layoutRefs.bottomRef, layoutRefs.bottomRightRef, layoutRefs.leftTopRef, layoutRefs.leftBottomRef, layoutRefs.rightTopRef, layoutRefs.rightBottomRef, layoutRefs.drawerRef, layoutRefs.leftRef, layoutRefs.rightRef],
|
|
268
328
|
expect.any(Function)
|
|
269
329
|
)
|
|
270
330
|
layoutRefs.appContainerRef.current.style.setProperty.mockClear()
|
|
@@ -45,27 +45,26 @@ export const Layout = () => {
|
|
|
45
45
|
<SlotRenderer slot={layoutSlots.SIDE} />
|
|
46
46
|
</div>
|
|
47
47
|
<div className='im-o-app__main' ref={layoutRefs.mainRef}>
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
</div>
|
|
52
|
-
)}
|
|
48
|
+
<div className='im-o-app__header' ref={layoutRefs.headerRef}>
|
|
49
|
+
<SlotRenderer slot={layoutSlots.HEADER} />
|
|
50
|
+
</div>
|
|
53
51
|
<div className='im-o-app__top' ref={layoutRefs.topRef}>
|
|
54
52
|
<div className='im-o-app__top-col' ref={layoutRefs.topLeftColRef}>
|
|
55
53
|
<SlotRenderer slot={layoutSlots.TOP_LEFT} />
|
|
56
54
|
</div>
|
|
57
55
|
<div className='im-o-app__top-col'>
|
|
58
56
|
<SlotRenderer slot={layoutSlots.TOP_MIDDLE} />
|
|
59
|
-
{['desktop'].includes(breakpoint) && (
|
|
60
|
-
<div className='im-o-app__banner' ref={layoutRefs.bannerRef}>
|
|
61
|
-
<SlotRenderer slot={layoutSlots.BANNER} />
|
|
62
|
-
</div>
|
|
63
|
-
)}
|
|
64
57
|
</div>
|
|
65
58
|
<div className='im-o-app__top-col' ref={layoutRefs.topRightColRef}>
|
|
66
59
|
<SlotRenderer slot={layoutSlots.TOP_RIGHT} />
|
|
67
60
|
</div>
|
|
68
61
|
</div>
|
|
62
|
+
{/* Docked (centred in the top-column gutter) vs stacked (full-width, below the
|
|
63
|
+
whole top row) is decided in useLayoutMeasurements — same DOM position either
|
|
64
|
+
way, so reading/tab order stays identical across breakpoints and modes. */}
|
|
65
|
+
<div className='im-o-app__banner' ref={layoutRefs.bannerRef}>
|
|
66
|
+
<SlotRenderer slot={layoutSlots.BANNER} />
|
|
67
|
+
</div>
|
|
69
68
|
<div className='im-o-app__left' ref={layoutRefs.leftRef}>
|
|
70
69
|
<div className='im-o-app__left-top' ref={layoutRefs.leftTopRef}>
|
|
71
70
|
<SlotRenderer slot={layoutSlots.LEFT_TOP} />
|
|
@@ -36,6 +36,7 @@ describe('Layout', () => {
|
|
|
36
36
|
const mockRefs = {
|
|
37
37
|
appContainerRef: React.createRef(),
|
|
38
38
|
mainRef: React.createRef(),
|
|
39
|
+
headerRef: React.createRef(),
|
|
39
40
|
bannerRef: React.createRef(),
|
|
40
41
|
topRef: React.createRef(),
|
|
41
42
|
topLeftColRef: React.createRef(),
|
|
@@ -89,6 +90,7 @@ describe('Layout', () => {
|
|
|
89
90
|
expect(screen.getByTestId('attributions')).toBeInTheDocument()
|
|
90
91
|
|
|
91
92
|
expect(screen.getByTestId('slot-side')).toBeInTheDocument()
|
|
93
|
+
expect(screen.getByTestId('slot-header')).toBeInTheDocument()
|
|
92
94
|
expect(screen.getByTestId('slot-banner')).toBeInTheDocument()
|
|
93
95
|
expect(screen.getByTestId('slot-top-left')).toBeInTheDocument()
|
|
94
96
|
expect(screen.getByTestId('slot-bottom-right')).toBeInTheDocument()
|
|
@@ -76,16 +76,41 @@
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// ---------------------------------------------------
|
|
79
|
-
// Banner:
|
|
79
|
+
// Banner: docks centred between the side columns or stacks full-width; mode and position are set in JS.
|
|
80
80
|
// ---------------------------------------------------
|
|
81
81
|
|
|
82
82
|
.im-o-app__banner {
|
|
83
|
+
position: absolute;
|
|
84
|
+
top: var(--banner-top);
|
|
85
|
+
left: var(--banner-left);
|
|
86
|
+
right: var(--banner-right);
|
|
87
|
+
z-index: 9;
|
|
83
88
|
display: flex;
|
|
84
89
|
flex-direction: column;
|
|
85
90
|
gap: var(--divider-gap);
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
|
|
93
|
+
// Trailing gap below the banner is added in JS, not via a last-child margin, since closed consumer HTML panels stay in the DOM.
|
|
94
|
+
|
|
95
|
+
// Stacked fills full width; docked caps the width and centres it.
|
|
96
|
+
.im-o-app__banner--docked .im-c-panel--banner {
|
|
97
|
+
max-width: var(--banner-preferred-width);
|
|
98
|
+
margin-inline: auto;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------
|
|
102
|
+
// Header: normal flow, above the top row — pushes everything below it down for free.
|
|
103
|
+
// ---------------------------------------------------
|
|
104
|
+
|
|
105
|
+
// z-index only, no position — flex items get stacking priority from z-index alone, and must stay above .im-o-app__top's since header comes first in the DOM.
|
|
106
|
+
.im-o-app__header {
|
|
107
|
+
z-index: 11;
|
|
108
|
+
display: flex;
|
|
109
|
+
flex-direction: column;
|
|
110
|
+
width: 100%;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.im-o-app__header:not(:empty) {
|
|
89
114
|
margin-bottom: var(--divider-gap);
|
|
90
115
|
}
|
|
91
116
|
|
|
@@ -93,7 +118,9 @@
|
|
|
93
118
|
// Top: Includes a panel and buttons in colums
|
|
94
119
|
// ---------------------------------------------------
|
|
95
120
|
|
|
121
|
+
// z-index only (see .im-o-app__header's comment above) — must stay below its value.
|
|
96
122
|
.im-o-app__top {
|
|
123
|
+
z-index: 10;
|
|
97
124
|
display: flex;
|
|
98
125
|
margin-bottom: auto;
|
|
99
126
|
justify-content: space-between;
|
|
@@ -421,10 +448,6 @@
|
|
|
421
448
|
.im-o-app__modal .im-c-panel--banner {
|
|
422
449
|
margin: var(--primary-gap);
|
|
423
450
|
}
|
|
424
|
-
|
|
425
|
-
.im-o-app__banner {
|
|
426
|
-
z-index: 9;
|
|
427
|
-
}
|
|
428
451
|
}
|
|
429
452
|
|
|
430
453
|
// Desktop modal banner margin
|
|
@@ -447,13 +470,6 @@
|
|
|
447
470
|
opacity: 0;
|
|
448
471
|
}
|
|
449
472
|
|
|
450
|
-
// Banner on desktop
|
|
451
|
-
.im-o-app--desktop .im-c-panel--banner {
|
|
452
|
-
margin-left: auto;
|
|
453
|
-
margin-right: auto;
|
|
454
|
-
max-width: var(--banner-max-desktop-width);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
473
|
// Actions inset when width is greater
|
|
458
474
|
.im-o-app--tablet, .im-o-app--desktop {
|
|
459
475
|
.im-o-app__actions {
|
|
@@ -466,8 +482,7 @@
|
|
|
466
482
|
max-width: var(--action-bar-max-width);
|
|
467
483
|
}
|
|
468
484
|
|
|
469
|
-
//
|
|
470
|
-
// a slide-up. Override to opacity-only so the bar fades in instead.
|
|
485
|
+
// Opacity-only so it fades in instead of visually sliding up.
|
|
471
486
|
@media (prefers-reduced-motion: no-preference) {
|
|
472
487
|
.im-c-actions {
|
|
473
488
|
transition: opacity var(--duration) ease;
|
|
@@ -498,8 +513,7 @@
|
|
|
498
513
|
clip-path: inset(-20px 0 0 0);
|
|
499
514
|
}
|
|
500
515
|
|
|
501
|
-
// Inset focus ring
|
|
502
|
-
// so the ::after ring isn't clipped by the parent overflow:hidden
|
|
516
|
+
// Inset the focus ring so it isn't clipped by the parent's overflow:hidden.
|
|
503
517
|
.im-o-app--mobile .im-o-app__drawer .im-c-panel::after,
|
|
504
518
|
.im-o-app--mobile .im-o-app__actions .im-c-panel::after,
|
|
505
519
|
.im-o-app__modal .im-c-panel--drawer::after {
|
|
@@ -11,7 +11,7 @@ function validateSlots (item, type) {
|
|
|
11
11
|
const allowed = allowedSlots[type]
|
|
12
12
|
BREAKPOINTS.forEach(bp => {
|
|
13
13
|
const slot = item[bp]?.slot
|
|
14
|
-
if (slot && !allowed.includes(slot) && !(type === 'panel' && slot.endsWith('-button'))) {
|
|
14
|
+
if (slot && !allowed.includes(slot) && !(type === 'panel' && slot.endsWith('-button')) && !(type === 'control' && slot.endsWith('-panel'))) {
|
|
15
15
|
logger.warn(`${type} "${item.id}" has invalid slot "${slot}" at breakpoint "${bp}". Allowed slots: ${allowed.join(', ')}.`)
|
|
16
16
|
}
|
|
17
17
|
})
|
|
@@ -171,6 +171,18 @@ describe('pluginRegistry', () => {
|
|
|
171
171
|
pluginRegistry.registerPlugin(plugin)
|
|
172
172
|
expect(console.warn).not.toHaveBeenCalled()
|
|
173
173
|
})
|
|
174
|
+
|
|
175
|
+
it('does not warn for a control targeting a panel-body slot', () => {
|
|
176
|
+
const plugin = {
|
|
177
|
+
id: 'inject-plugin',
|
|
178
|
+
config: {},
|
|
179
|
+
manifest: {
|
|
180
|
+
controls: [{ id: 'ctrl1', desktop: { slot: 'map-styles-panel' } }]
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
pluginRegistry.registerPlugin(plugin)
|
|
184
|
+
expect(console.warn).not.toHaveBeenCalled()
|
|
185
|
+
})
|
|
174
186
|
})
|
|
175
187
|
|
|
176
188
|
it('clears all registered plugins', () => {
|
|
@@ -28,6 +28,10 @@ export const getSlotRef = (slot, layoutRefs) => {
|
|
|
28
28
|
const el = document.querySelector(`[data-button-slot="${slot}"]`)
|
|
29
29
|
return el ? { current: el } : null
|
|
30
30
|
}
|
|
31
|
+
if (slot?.endsWith('-panel')) {
|
|
32
|
+
const el = document.querySelector(`[data-panel-slot="${slot}"]`)
|
|
33
|
+
return el ? { current: el } : null
|
|
34
|
+
}
|
|
31
35
|
|
|
32
36
|
return slotRefMap[slot] || null
|
|
33
37
|
}
|
|
@@ -37,8 +41,13 @@ export const getSlotRef = (slot, layoutRefs) => {
|
|
|
37
41
|
* Moves the wrapper into the target slot when visible, hides it otherwise.
|
|
38
42
|
* Depends on breakpoint to handle conditionally rendered slot containers
|
|
39
43
|
* (e.g. the banner slot swaps DOM nodes between mobile and desktop).
|
|
44
|
+
*
|
|
45
|
+
* `anchorKey` is an optional extra dependency for anchors that aren't always present in the
|
|
46
|
+
* DOM — e.g. a `-panel` target only exists while that panel is open — so the projection is
|
|
47
|
+
* re-attempted whenever it changes, rather than only on mount. Stable/persistent anchors
|
|
48
|
+
* (named layout slots, `-button` targets) don't need it since they never come and go.
|
|
40
49
|
*/
|
|
41
|
-
export const useDomProjection = (wrapperRef, targetSlot, isVisible, layoutRefs, breakpoint) => {
|
|
50
|
+
export const useDomProjection = (wrapperRef, targetSlot, isVisible, layoutRefs, breakpoint, anchorKey) => {
|
|
42
51
|
const layoutRefsRef = useRef(layoutRefs)
|
|
43
52
|
layoutRefsRef.current = layoutRefs
|
|
44
53
|
|
|
@@ -66,7 +75,7 @@ export const useDomProjection = (wrapperRef, targetSlot, isVisible, layoutRefs,
|
|
|
66
75
|
return () => {
|
|
67
76
|
wrapper.style.display = 'none'
|
|
68
77
|
}
|
|
69
|
-
}, [isVisible, targetSlot, breakpoint, wrapperRef])
|
|
78
|
+
}, [isVisible, targetSlot, breakpoint, wrapperRef, anchorKey])
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
/**
|
|
@@ -126,13 +135,17 @@ const PersistentPanel = ({ panelId, config, isOpen, openPanelProps, focusOnOpen,
|
|
|
126
135
|
*/
|
|
127
136
|
const PersistentControl = ({ control, appState }) => {
|
|
128
137
|
const wrapperRef = useRef(null)
|
|
129
|
-
const { breakpoint, mode, isFullscreen, layoutRefs } = appState
|
|
138
|
+
const { breakpoint, mode, isFullscreen, layoutRefs, openPanels } = appState
|
|
130
139
|
|
|
131
140
|
const bpConfig = control[breakpoint]
|
|
132
141
|
const isVisible = isControlVisible(control, { breakpoint, mode, isFullscreen })
|
|
133
142
|
const targetSlot = bpConfig?.slot || null
|
|
134
143
|
|
|
135
|
-
|
|
144
|
+
// A control targeting a panel's body (`<panelId>-panel`) needs its DOM anchor re-resolved
|
|
145
|
+
// whenever panels open/close, since that anchor only exists while the target panel is open.
|
|
146
|
+
const anchorKey = targetSlot?.endsWith('-panel') ? Object.keys(openPanels || {}).sort((a, b) => a.localeCompare(b)).join(',') : null
|
|
147
|
+
|
|
148
|
+
useDomProjection(wrapperRef, targetSlot, isVisible, layoutRefs, breakpoint, anchorKey)
|
|
136
149
|
|
|
137
150
|
const innerHtml = useMemo(() => ({ __html: control.html }), [control.html])
|
|
138
151
|
|
|
@@ -174,7 +187,7 @@ export const HtmlElementHost = () => {
|
|
|
174
187
|
const cfg = panelConfig[panelId]?.[breakpoint]
|
|
175
188
|
return cfg?.modal
|
|
176
189
|
})
|
|
177
|
-
return modalPanels.length > 0 ? modalPanels[modalPanels.length - 1][0] : null
|
|
190
|
+
return modalPanels.length > 0 ? modalPanels[modalPanels.length - 1][0] : null // NOSONAR - .at() unsupported on Chrome < 92
|
|
178
191
|
}, [openPanels, panelConfig, breakpoint])
|
|
179
192
|
|
|
180
193
|
if (!htmlPanels.length && !htmlControls.length) {
|
|
@@ -313,6 +313,60 @@ describe('HtmlElementHost', () => {
|
|
|
313
313
|
expect(getSlotRef('nonexistent-button', {})).toBeNull()
|
|
314
314
|
})
|
|
315
315
|
|
|
316
|
+
test('getSlotRef returns wrapped element for panel slot when element exists', () => {
|
|
317
|
+
const el = document.createElement('div')
|
|
318
|
+
el.dataset.panelSlot = 'map-styles-panel'
|
|
319
|
+
document.body.appendChild(el)
|
|
320
|
+
expect(getSlotRef('map-styles-panel', {})).toEqual({ current: el })
|
|
321
|
+
el.remove()
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test('getSlotRef returns null for panel slot when no element found', () => {
|
|
325
|
+
expect(getSlotRef('nonexistent-panel', {})).toBeNull()
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('re-attempts DOM projection into a panel-body slot once its anchor appears (panel opens)', () => {
|
|
329
|
+
const controlConfig = {
|
|
330
|
+
mapStylesNote: { id: 'mapStylesNote', desktop: { slot: 'map-styles-panel' }, html: '<p>Injected note</p>' }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// A real React-rendered node, mimicking the real Panel.jsx body div — present in the DOM
|
|
334
|
+
// throughout, but only gains the data-panel-slot marker once the panel "opens".
|
|
335
|
+
const panelBodyRef = React.createRef()
|
|
336
|
+
|
|
337
|
+
mockApp({ controlConfig, openPanels: {} })
|
|
338
|
+
const { rerender } = render(
|
|
339
|
+
<div>
|
|
340
|
+
<div ref={panelBodyRef} />
|
|
341
|
+
<HtmlElementHost />
|
|
342
|
+
</div>
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
// Anchor isn't marked yet (panel closed) — nothing to project into
|
|
346
|
+
expect(panelBodyRef.current.querySelector('.im-c-control')).toBeNull()
|
|
347
|
+
|
|
348
|
+
// Panel opens: its body anchor is marked, and openPanels changes (two entries, so the
|
|
349
|
+
// anchorKey sort's localeCompare comparator actually runs)
|
|
350
|
+
panelBodyRef.current.setAttribute('data-panel-slot', 'map-styles-panel')
|
|
351
|
+
mockApp({ controlConfig, openPanels: { mapStyles: { props: {} }, other: { props: {} } } })
|
|
352
|
+
rerender(
|
|
353
|
+
<div>
|
|
354
|
+
<div ref={panelBodyRef} />
|
|
355
|
+
<HtmlElementHost />
|
|
356
|
+
</div>
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
expect(panelBodyRef.current.querySelector('.im-c-control')).toBeTruthy()
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('does not crash a panel-targeting control when appState.openPanels is missing', () => {
|
|
363
|
+
const controlConfig = {
|
|
364
|
+
mapStylesNote: { id: 'mapStylesNote', desktop: { slot: 'map-styles-panel' }, html: '<p>Injected note</p>' }
|
|
365
|
+
}
|
|
366
|
+
mockApp({ controlConfig, openPanels: undefined })
|
|
367
|
+
expect(() => render(<HtmlElementHost />)).not.toThrow()
|
|
368
|
+
})
|
|
369
|
+
|
|
316
370
|
it('does not append child if slotRef exists but current is null', () => {
|
|
317
371
|
// 1. Setup refs where the slot exists in the map but the DOM node (current) is null
|
|
318
372
|
const incompleteRefs = {
|