@defra/interactive-map 0.0.30-alpha → 0.0.32-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/README.md +2 -2
- package/assets/images/bright-ofm-thumb.jpg +0 -0
- package/assets/images/dark-ofm-thumb.jpg +0 -0
- package/assets/images/road-ofm-thumb.jpg +0 -0
- package/assets/templates/add-polygons.njk +143 -0
- package/assets/templates/add-symbols.njk +119 -0
- package/assets/templates/{map.njk → basic-map.njk} +9 -17
- package/assets/templates/button-map.njk +56 -0
- package/assets/templates/draw-tools.njk +161 -0
- package/assets/templates/marker-panel.njk +91 -0
- package/assets/templates/place-marker.njk +104 -0
- package/assets/templates/search-control.njk +98 -0
- package/assets/templates/select-feature.njk +101 -0
- package/assets/templates/style-switcher.njk +87 -0
- package/assets/templates/toggle-marker-label.njk +74 -0
- 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/context.md +4 -4
- package/docs/api.md +166 -37
- package/docs/assets/css/docusaurus.scss +36 -4
- package/docs/assets/images/add-polygons.jpg +0 -0
- package/docs/assets/images/add-symbols.jpg +0 -0
- package/docs/assets/images/basic-map.jpg +0 -0
- package/docs/assets/images/button-first.jpg +0 -0
- package/docs/assets/images/deuteranopia-ozs-map-thumb.jpg +0 -0
- package/docs/assets/images/draw.jpg +0 -0
- package/docs/assets/images/marker-panel.jpg +0 -0
- package/docs/assets/images/night-ozs-map-thumb.jpg +0 -0
- package/docs/assets/images/outdoor-ozs-map-thumb.jpg +0 -0
- package/docs/assets/images/place-marker.jpg +0 -0
- package/docs/assets/images/screenshot.jpg +0 -0
- package/docs/assets/images/search.jpg +0 -0
- package/docs/assets/images/select-feature.jpg +0 -0
- package/docs/assets/images/style-switcher.jpg +0 -0
- package/docs/assets/images/toggle-marker-label.jpg +0 -0
- package/docs/assets/images/tritanopia-ozs-map-thumb.jpg +0 -0
- package/docs/examples/add-marker-with-panel.mdx +3 -3
- package/docs/examples/add-polygons.mdx +225 -0
- package/docs/examples/add-symbols.mdx +177 -0
- package/docs/examples/basic-map.mdx +2 -2
- package/docs/examples/draw-tools.mdx +271 -0
- package/docs/examples/index.mdx +95 -11
- package/docs/examples/place-marker.mdx +151 -0
- package/docs/examples/search-control.mdx +71 -0
- package/docs/examples/select-feature.mdx +146 -0
- package/docs/examples/style-switcher.mdx +135 -0
- package/docs/examples/toggle-marker-label.mdx +3 -3
- package/docs/plugins/datasets.md +157 -122
- package/docs/plugins/interact.md +1 -4
- package/docusaurus.config.cjs +9 -2
- package/govuk-prototype-kit.config.json +52 -2
- package/package.json +1 -1
- package/plugins/beta/datasets/dist/css/index.css +5 -2
- package/plugins/beta/datasets/dist/esm/im-datasets-ml-adapter.js +1 -0
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/esm/index.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-ml-adapter.js +2 -0
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +2 -0
- package/plugins/beta/datasets/dist/umd/index.js +2 -0
- package/plugins/beta/datasets/src/adapters/loadLayerAdapter.js +33 -0
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +44 -120
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +145 -245
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +516 -0
- package/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.js +209 -0
- package/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js +483 -0
- package/plugins/beta/datasets/src/api/addDataset.js +2 -5
- package/plugins/beta/datasets/src/api/addDataset.test.js +16 -0
- package/plugins/beta/datasets/src/api/getOpacity.js +14 -14
- package/plugins/beta/datasets/src/api/getOpacity.test.js +72 -0
- package/plugins/beta/datasets/src/api/getStyle.js +8 -9
- package/plugins/beta/datasets/src/api/getStyle.test.js +42 -0
- package/plugins/beta/datasets/src/api/removeDataset.js +7 -7
- package/plugins/beta/datasets/src/api/removeDataset.test.js +45 -0
- package/plugins/beta/datasets/src/api/setData.js +9 -3
- package/plugins/beta/datasets/src/api/setData.test.js +62 -0
- package/plugins/beta/datasets/src/api/setDatasetVisibility.js +5 -30
- package/plugins/beta/datasets/src/api/setDatasetVisibility.test.js +31 -0
- package/plugins/beta/datasets/src/api/setFeatureVisibility.js +9 -18
- package/plugins/beta/datasets/src/api/setFeatureVisibility.test.js +74 -0
- package/plugins/beta/datasets/src/api/setGlobals.js +20 -0
- package/plugins/beta/datasets/src/api/setGlobals.test.js +63 -0
- package/plugins/beta/datasets/src/api/setOpacity.js +8 -27
- package/plugins/beta/datasets/src/api/setOpacity.test.js +31 -0
- package/plugins/beta/datasets/src/api/setStyle.js +5 -21
- package/plugins/beta/datasets/src/api/setStyle.test.js +53 -0
- package/plugins/beta/datasets/src/components/Key/Key.jsx +51 -0
- package/plugins/beta/datasets/src/components/Key/KeyGroupItem.jsx +18 -0
- package/plugins/beta/datasets/src/components/Key/KeyItem.jsx +20 -0
- package/plugins/beta/datasets/src/components/Key/KeySvg.jsx +22 -0
- package/plugins/beta/datasets/src/components/Key/KeySvg.test.jsx +87 -0
- package/plugins/beta/datasets/src/components/Key/KeySvgLine.jsx +19 -0
- package/plugins/beta/datasets/src/components/Key/KeySvgLine.test.jsx +71 -0
- package/plugins/beta/datasets/src/components/{KeySvgPattern.jsx → Key/KeySvgPattern.jsx} +3 -4
- package/plugins/beta/datasets/src/components/Key/KeySvgPattern.test.jsx +55 -0
- package/plugins/beta/datasets/src/components/Key/KeySvgRect.jsx +22 -0
- package/plugins/beta/datasets/src/components/Key/KeySvgRect.test.jsx +85 -0
- package/plugins/beta/datasets/src/components/{KeySvgSymbol.jsx → Key/KeySvgSymbol.jsx} +5 -5
- package/plugins/beta/datasets/src/components/Key/KeySvgSymbol.test.jsx +80 -0
- package/plugins/beta/datasets/src/components/Key/svgProperties.test.js +61 -0
- package/plugins/beta/datasets/src/{panels → components/LayersMenu}/Layers.module.scss +15 -11
- package/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.jsx +33 -0
- package/plugins/beta/datasets/src/components/LayersMenu/LayersMenuCheckbox.jsx +31 -0
- package/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx +19 -0
- package/plugins/beta/datasets/src/datasets.scss +2 -2
- package/plugins/beta/datasets/src/fetch/createDynamicSource.js +8 -8
- package/plugins/beta/datasets/src/{DatasetsInit.jsx → initialise/DatasetsInit.jsx} +19 -21
- package/plugins/beta/datasets/src/{defaults.js → initialise/defaults.js} +17 -2
- package/plugins/beta/datasets/src/initialise/defaults.test.js +127 -0
- package/plugins/beta/datasets/src/{datasets.js → initialise/initialiseDatasets.js} +30 -26
- package/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js +273 -0
- package/plugins/beta/datasets/src/manifest.js +8 -6
- package/plugins/beta/datasets/src/reducers/__data__/demoDatasets.js +253 -0
- package/plugins/beta/datasets/src/reducers/datasetsToMenu.js +83 -0
- package/plugins/beta/datasets/src/reducers/datasetsToMenu.test.js +84 -0
- package/plugins/beta/datasets/src/reducers/mappedDatasetsReducer.js +41 -0
- package/plugins/beta/datasets/src/reducers/mappedDatasetsReducer.test.js +117 -0
- package/plugins/beta/datasets/src/reducers/pluginState.js +206 -0
- package/plugins/beta/datasets/src/reducers/pluginState.test.js +386 -0
- package/plugins/beta/datasets/src/registry/__mocks__/datasetRegistry.js +29 -0
- package/plugins/beta/datasets/src/registry/createDataset.js +5 -0
- package/plugins/beta/datasets/src/registry/dataset.js +176 -0
- package/plugins/beta/datasets/src/registry/dataset.test.js +497 -0
- package/plugins/beta/datasets/src/registry/datasetDefinitionCache.js +35 -0
- package/plugins/beta/datasets/src/registry/datasetRegistry.js +116 -0
- package/plugins/beta/datasets/src/registry/datasetRegistry.test.js +335 -0
- package/plugins/beta/datasets/src/registry/dynamicGeoJson.js +17 -0
- package/plugins/beta/datasets/src/registry/globalDataset.js +36 -0
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/api/addFeature.js +12 -3
- package/plugins/beta/draw-es/src/events.js +8 -3
- package/plugins/beta/draw-es/src/events.test.js +2 -0
- package/plugins/beta/draw-es/src/graphic.js +26 -2
- 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-ml/src/DrawInit.jsx +8 -0
- package/plugins/beta/draw-ml/src/api/newLine.js +1 -0
- package/plugins/beta/draw-ml/src/api/newPolygon.js +1 -0
- package/plugins/beta/draw-ml/src/events.js +13 -1
- package/plugins/beta/draw-ml/src/index.js +1 -1
- package/plugins/beta/draw-ml/src/manifest.js +3 -0
- package/plugins/beta/draw-ml/src/modes/createDrawMode.js +21 -12
- package/plugins/beta/draw-ml/src/modes/editVertexMode.js +4 -0
- package/plugins/beta/draw-ol/dist/esm/EditMode.js +1 -1
- package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
- package/plugins/beta/draw-ol/src/edit/keyboardHandler.js +4 -2
- package/plugins/beta/map-styles/src/mapStyles.scss +41 -41
- 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/InteractInit.jsx +3 -5
- package/plugins/interact/src/InteractInit.test.js +1 -3
- package/plugins/interact/src/events.js +2 -29
- package/plugins/interact/src/events.test.js +4 -102
- package/plugins/interact/src/hooks/useAttachEvents.js +3 -4
- package/plugins/interact/src/hooks/useAttachEvents.test.js +1 -3
- package/plugins/interact/src/hooks/useCrossHairVisibility.js +4 -10
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +5 -10
- package/plugins/interact/src/hooks/useHighlightSync.js +1 -10
- package/plugins/interact/src/hooks/useHighlightSync.test.js +2 -21
- package/plugins/interact/src/hooks/useInteractionHandlers.js +15 -12
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +28 -29
- package/plugins/interact/src/manifest.js +0 -35
- package/plugins/interact/src/manifest.test.js +3 -43
- package/plugins/interact/src/reducer.js +4 -22
- package/plugins/interact/src/reducer.test.js +11 -39
- package/plugins/search/dist/css/index.css +5 -2
- package/plugins/search/src/components/Form/Form.module.scss +5 -2
- package/plugins/search/src/search.scss +4 -4
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/esriProvider.js +4 -0
- 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/src/openlayersProvider.js +4 -0
- package/providers/beta/openlayers/src/openlayersProvider.test.js +5 -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/src/maplibreProvider.js +6 -4
- package/providers/maplibre/src/maplibreProvider.test.js +5 -0
- package/providers/maplibre/src/utils/highlightFeatures.js +74 -25
- package/providers/maplibre/src/utils/highlightFeatures.test.js +132 -0
- package/providers/maplibre/src/utils/patternImages.js +2 -4
- package/providers/maplibre/src/utils/symbolImages.js +1 -4
- package/rollup.esm.mjs +2 -8
- package/src/App/components/CrossHair/CrossHair.module.scss +8 -8
- package/src/App/components/MapButton/MapButton.jsx +16 -1
- package/src/App/components/MapButton/MapButton.module.scss +12 -1
- package/src/App/components/MapButton/MapButton.test.jsx +40 -1
- package/src/App/hooks/useContinueEnabledEvaluator.js +31 -0
- package/src/App/hooks/useContinueEnabledEvaluator.test.js +118 -0
- package/src/App/hooks/useEvaluateProp.js +15 -15
- package/src/App/hooks/useEvaluateProp.test.js +80 -76
- package/src/App/hooks/useLayoutMeasurements.js +11 -5
- package/src/App/hooks/useResizeObserver.js +15 -2
- package/src/App/hooks/useResizeObserver.test.js +1 -0
- package/src/App/renderer/PluginInits.jsx +2 -0
- package/src/App/renderer/mapButtons.js +21 -1
- package/src/App/renderer/mapButtons.test.js +46 -1
- package/src/App/store/appReducer.js +1 -1
- package/src/App/store/appReducer.test.js +37 -0
- package/src/InteractiveMap/InteractiveMap.js +14 -4
- package/src/InteractiveMap/InteractiveMap.test.js +11 -3
- package/src/InteractiveMap/behaviourController.js +3 -1
- package/src/InteractiveMap/behaviourController.test.js +6 -3
- package/src/InteractiveMap/domStateManager.js +2 -0
- package/src/config/appConfig.js +25 -0
- package/src/config/appConfig.test.js +50 -0
- package/src/config/defaults.js +1 -0
- package/src/config/events.js +21 -3
- package/src/scss/settings/_dimensions.scss +1 -0
- package/src/services/patternRegistry.js +8 -4
- package/src/types.js +7 -0
- package/src/utils/patternUtils.js +1 -2
- package/src/utils/patternUtils.test.js +9 -0
- package/webpack.dev.mjs +10 -0
- package/webpack.umd.mjs +1 -1
- package/docs/assets/images/screens-blue.jpg +0 -0
- package/docs/assets/images/screens-white.jpg +0 -0
- package/plugins/beta/datasets/dist/adapters/maplibre/esm/im-datasets-ml-adapter.js +0 -1
- package/plugins/beta/datasets/dist/adapters/maplibre/esm/index.js +0 -1
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-maplibre-adapter.js +0 -2
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-plugin.js +0 -2
- package/plugins/beta/datasets/dist/umd/maplibre/index.js +0 -2
- package/plugins/beta/datasets/src/adapters/maplibre/index.js +0 -18
- package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +0 -75
- package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +0 -27
- package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +0 -31
- package/plugins/beta/datasets/src/components/KeySvg.jsx +0 -24
- package/plugins/beta/datasets/src/components/KeySvgLine.jsx +0 -19
- package/plugins/beta/datasets/src/components/KeySvgRect.jsx +0 -22
- package/plugins/beta/datasets/src/index.maplibre.umd.js +0 -23
- package/plugins/beta/datasets/src/panels/Key.jsx +0 -62
- package/plugins/beta/datasets/src/panels/Layers.jsx +0 -141
- package/plugins/beta/datasets/src/reducer.js +0 -219
- package/plugins/beta/datasets/src/reducers/keyReducer.js +0 -34
- package/plugins/beta/datasets/src/utils/filters.js +0 -34
- package/plugins/beta/datasets/src/utils/mergeSublayer.js +0 -86
- package/plugins/beta/draw-ml/dist/css/index.css +0 -21
- /package/plugins/beta/datasets/dist/umd/{maplibre/im-datasets-maplibre-adapter.js.LICENSE.txt → im-datasets-ml-adapter.js.LICENSE.txt} +0 -0
- /package/plugins/beta/datasets/dist/umd/{maplibre/im-datasets-plugin.js.LICENSE.txt → im-datasets-plugin.js.LICENSE.txt} +0 -0
- /package/plugins/beta/datasets/dist/umd/{maplibre/index.js.LICENSE.txt → index.js.LICENSE.txt} +0 -0
- /package/plugins/beta/datasets/src/components/{EmptyKey.jsx → Key/EmptyKey.jsx} +0 -0
- /package/plugins/beta/datasets/src/components/{EmptyKey.test.jsx → Key/EmptyKey.test.jsx} +0 -0
- /package/plugins/beta/datasets/src/{panels → components/Key}/Key.module.scss +0 -0
- /package/plugins/beta/datasets/src/components/{svgProperties.js → Key/svgProperties.js} +0 -0
|
@@ -64,7 +64,8 @@ const clearPrefixSources = (map, prefix) => {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const applyHighlightLayer = (map, id, type, sourceId, srcLayer, paint, filter) => {
|
|
67
|
-
|
|
67
|
+
const existed = !!map.getLayer(id)
|
|
68
|
+
if (!existed) {
|
|
68
69
|
map.addLayer({
|
|
69
70
|
id,
|
|
70
71
|
type,
|
|
@@ -122,49 +123,89 @@ const applySymbolGeomHighlight = (map, base, sourceId, srcLayer, layerId, filter
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
// Always draws a line ring. Only draws a fill when isSelected — active (cursor) features get
|
|
127
|
+
// the ring only, so the fill isn't shown for every keyboard-navigated item.
|
|
125
128
|
const applyFillGeomHighlight = (map, base, sourceId, srcLayer, { isSelected, idExpression, fillIds, fill, lineColor, lineWidth, filter }) => {
|
|
126
129
|
if (isSelected) {
|
|
127
|
-
const
|
|
130
|
+
const fillIdsArray = []
|
|
131
|
+
fillIds.forEach(id => fillIdsArray.push(id))
|
|
132
|
+
const fillFilter = ['in', idExpression, ['literal', fillIdsArray]]
|
|
128
133
|
// Only apply fill highlight to polygon features, not to any co-selected line features
|
|
129
134
|
applyHighlightLayer(map, `${base}-fill`, 'fill', sourceId, srcLayer, { 'fill-color': fill }, fillFilter)
|
|
130
135
|
}
|
|
131
136
|
applyHighlightLayer(map, `${base}-line`, 'line', sourceId, srcLayer, { 'line-color': lineColor, 'line-width': lineWidth }, filter)
|
|
132
137
|
}
|
|
133
138
|
|
|
139
|
+
const applyFillExtrusionHighlight = (map, base, layerId, { ids, lineColor, lineWidth, idExpression }) => {
|
|
140
|
+
// forEach bypasses broken Set iterator polyfill in the Docusaurus/core-js environment
|
|
141
|
+
const newIds = []
|
|
142
|
+
ids.forEach(id => newIds.push(id))
|
|
143
|
+
|
|
144
|
+
const filter = ['in', idExpression, ['literal', newIds]]
|
|
145
|
+
const { source, sourceLayer } = map.getLayer(layerId)
|
|
146
|
+
|
|
147
|
+
// A line layer moved to the top of the stack renders above the fill-extrusion 3D pass.
|
|
148
|
+
// fill-extrusion has no outline/stroke property, so this is the only way to show a stroke.
|
|
149
|
+
applyHighlightLayer(map, `${base}-line`, 'line', source, sourceLayer, { 'line-color': lineColor, 'line-width': lineWidth }, filter)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const resolveLineStyle = (style, selectedStyle) => ({
|
|
153
|
+
lineColor: selectedStyle ? style.selectionStroke : style.stroke,
|
|
154
|
+
lineWidth: selectedStyle ? style.strokeWidth : style.activeStrokeWidth
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const applyLineHighlight = (map, base, sourceId, srcLayer, lineColor, lineWidth, filter) => {
|
|
158
|
+
if (map.getLayer(`${base}-fill`)) {
|
|
159
|
+
map.setFilter(`${base}-fill`, ['==', 'id', ''])
|
|
160
|
+
}
|
|
161
|
+
applyHighlightLayer(map, `${base}-line`, 'line', sourceId, srcLayer, { 'line-color': lineColor, 'line-width': lineWidth }, filter)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const applySymbolHighlight = (map, base, sourceId, srcLayer, layerId, filter, getSymbolImageId) => {
|
|
165
|
+
applySymbolGeomHighlight(map, base, sourceId, srcLayer, layerId, filter, getSymbolImageId)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Orchestrates highlight layers for a single source. prefix drives which visual style is used:
|
|
169
|
+
// active-highlight → yellow active ring (stroke only)
|
|
170
|
+
// active-highlight-inner → black thin ring drawn on top of active
|
|
171
|
+
// selected-highlight → black thin ring + fill for polygon features
|
|
134
172
|
const applySourceHighlight = (map, sourceId, featuresBySource, stylesMap, prefix, getSymbolImageId) => {
|
|
135
173
|
const { ids, fillIds, idProperty, layerId, hasFillGeometry } = featuresBySource[sourceId]
|
|
136
174
|
const baseLayer = map.getLayer(layerId)
|
|
137
|
-
const srcLayer = baseLayer.sourceLayer
|
|
138
|
-
const geom = hasFillGeometry ? 'fill' : baseLayer.type
|
|
139
|
-
const base = `${prefix}-${sourceId}`
|
|
140
|
-
const style = stylesMap[layerId]
|
|
141
175
|
|
|
142
|
-
if (!
|
|
176
|
+
if (!baseLayer || !stylesMap[layerId]) {
|
|
143
177
|
return
|
|
144
178
|
}
|
|
145
179
|
|
|
146
|
-
const
|
|
180
|
+
const style = stylesMap[layerId]
|
|
181
|
+
const srcLayer = baseLayer.sourceLayer
|
|
182
|
+
const geom = hasFillGeometry ? 'fill' : baseLayer.type
|
|
183
|
+
const base = `${prefix}-${sourceId}`
|
|
184
|
+
const { fill } = style
|
|
147
185
|
const isSelected = prefix === SELECTED_PREFIX
|
|
148
186
|
const selectedStyle = usesSelectedStyle(prefix)
|
|
149
|
-
const lineColor
|
|
150
|
-
const lineWidth = selectedStyle ? strokeWidth : activeStrokeWidth
|
|
187
|
+
const { lineColor, lineWidth } = resolveLineStyle(style, selectedStyle)
|
|
151
188
|
const idExpression = idProperty ? ['get', idProperty] : ['id']
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
if (geom === 'fill') {
|
|
155
|
-
applyFillGeomHighlight(map, base, sourceId, srcLayer, { isSelected, idExpression, fillIds, fill, lineColor, lineWidth, filter })
|
|
156
|
-
}
|
|
189
|
+
const idsArray = Array.from(ids)
|
|
190
|
+
const filter = ['in', idExpression, ['literal', idsArray]]
|
|
157
191
|
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
map.setFilter(`${base}-fill`, ['==', 'id', ''])
|
|
162
|
-
}
|
|
163
|
-
applyHighlightLayer(map, `${base}-line`, 'line', sourceId, srcLayer, { 'line-color': lineColor, 'line-width': lineWidth }, filter)
|
|
192
|
+
if (baseLayer.type === 'fill-extrusion') {
|
|
193
|
+
applyFillExtrusionHighlight(map, base, layerId, { ids, lineColor, lineWidth, idExpression })
|
|
194
|
+
return
|
|
164
195
|
}
|
|
165
196
|
|
|
166
|
-
|
|
167
|
-
|
|
197
|
+
switch (geom) {
|
|
198
|
+
case 'fill':
|
|
199
|
+
applyFillGeomHighlight(map, base, sourceId, srcLayer, { isSelected, idExpression, fillIds, fill, lineColor, lineWidth, filter })
|
|
200
|
+
break
|
|
201
|
+
case 'line':
|
|
202
|
+
applyLineHighlight(map, base, sourceId, srcLayer, lineColor, lineWidth, filter)
|
|
203
|
+
break
|
|
204
|
+
case 'symbol':
|
|
205
|
+
applySymbolHighlight(map, base, sourceId, srcLayer, layerId, filter, getSymbolImageId)
|
|
206
|
+
break
|
|
207
|
+
default:
|
|
208
|
+
break
|
|
168
209
|
}
|
|
169
210
|
}
|
|
170
211
|
|
|
@@ -182,16 +223,24 @@ const applyFeatureHighlights = (map, features, stylesMap, prefix, getSymbolImage
|
|
|
182
223
|
}
|
|
183
224
|
|
|
184
225
|
/**
|
|
185
|
-
* Update highlighted features using pure filters.
|
|
226
|
+
* Update highlighted features using pure filters (no cloned sources).
|
|
186
227
|
* activeFeatures (keyboard cursor) render with the active ring (yellow) plus a selected ring inner (black).
|
|
187
228
|
* selectedFeatures render with the selected ring (black) only.
|
|
188
229
|
* Supports fill, line and symbol geometry, multi-source, cleanup, and bounds.
|
|
230
|
+
*
|
|
231
|
+
* @param {object} opts
|
|
232
|
+
* @param {Function} opts.LngLatBounds - MapLibre LngLatBounds constructor
|
|
233
|
+
* @param {object} opts.map - MapLibre map instance
|
|
234
|
+
* @param {Array} opts.selectedFeatures - committed selection features
|
|
235
|
+
* @param {Array} opts.activeFeatures - keyboard-cursor features
|
|
236
|
+
* @param {object} opts.stylesMap - keyed by layerId; each value is
|
|
237
|
+
* { stroke, selectionStroke, strokeWidth, activeStrokeWidth, fill }
|
|
238
|
+
* @returns {number[]|null} [west, south, east, north] bounds or null if nothing is selected
|
|
189
239
|
*/
|
|
190
240
|
export function updateHighlightedFeatures ({ LngLatBounds, map, selectedFeatures, activeFeatures, stylesMap }) {
|
|
191
241
|
if (!map) {
|
|
192
242
|
return null
|
|
193
243
|
}
|
|
194
|
-
|
|
195
244
|
// Active cursor features — rendered first so selected layers appear on top
|
|
196
245
|
if (activeFeatures?.length) {
|
|
197
246
|
applyFeatureHighlights(map, activeFeatures, stylesMap, ACTIVE_PREFIX, getActiveImageId)
|
|
@@ -300,6 +300,95 @@ describe('Highlighting Utils — symbol layers (committed selection)', () => {
|
|
|
300
300
|
})
|
|
301
301
|
})
|
|
302
302
|
|
|
303
|
+
// ─── fill-extrusion layers (3D buildings) ────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
describe('Highlighting Utils — fill-extrusion layers', () => {
|
|
306
|
+
const LAYER_ID = 'buildings 3D'
|
|
307
|
+
const STYLES = { [LAYER_ID]: { stroke: '#aaa', selectionStroke: '#0073cc', strokeWidth: 3, activeStrokeWidth: 6 } }
|
|
308
|
+
const LINE_LAYER_ID = 'selected-highlight-composite-line'
|
|
309
|
+
|
|
310
|
+
let map
|
|
311
|
+
|
|
312
|
+
beforeEach(() => {
|
|
313
|
+
map = makeMap()
|
|
314
|
+
map.getLayer.mockImplementation(id => {
|
|
315
|
+
if (id === LAYER_ID) return { source: 'composite', sourceLayer: 'building', type: 'fill-extrusion' }
|
|
316
|
+
return null
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
test('adds a line overlay layer above the extrusion pass', () => {
|
|
321
|
+
updateHighlightedFeatures({
|
|
322
|
+
LngLatBounds,
|
|
323
|
+
map,
|
|
324
|
+
selectedFeatures: [{ featureId: 'uuid-abc', layerId: LAYER_ID, idProperty: 'uuid' }],
|
|
325
|
+
stylesMap: STYLES
|
|
326
|
+
})
|
|
327
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
|
|
328
|
+
id: LINE_LAYER_ID,
|
|
329
|
+
type: 'line',
|
|
330
|
+
source: 'composite',
|
|
331
|
+
'source-layer': 'building'
|
|
332
|
+
}))
|
|
333
|
+
expect(map.moveLayer).toHaveBeenCalledWith(LINE_LAYER_ID)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
test('filters by idProperty expression when set', () => {
|
|
337
|
+
updateHighlightedFeatures({
|
|
338
|
+
LngLatBounds,
|
|
339
|
+
map,
|
|
340
|
+
selectedFeatures: [{ featureId: 'uuid-abc', layerId: LAYER_ID, idProperty: 'uuid' }],
|
|
341
|
+
stylesMap: STYLES
|
|
342
|
+
})
|
|
343
|
+
expect(map.setFilter).toHaveBeenCalledWith(
|
|
344
|
+
LINE_LAYER_ID,
|
|
345
|
+
['in', ['get', 'uuid'], ['literal', ['uuid-abc']]]
|
|
346
|
+
)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
test('filters by feature id when no idProperty', () => {
|
|
350
|
+
updateHighlightedFeatures({
|
|
351
|
+
LngLatBounds,
|
|
352
|
+
map,
|
|
353
|
+
selectedFeatures: [{ featureId: 42, layerId: LAYER_ID }],
|
|
354
|
+
stylesMap: STYLES
|
|
355
|
+
})
|
|
356
|
+
expect(map.setFilter).toHaveBeenCalledWith(
|
|
357
|
+
LINE_LAYER_ID,
|
|
358
|
+
['in', ['id'], ['literal', [42]]]
|
|
359
|
+
)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
test('paints the line overlay with selectionStroke colour', () => {
|
|
363
|
+
updateHighlightedFeatures({
|
|
364
|
+
LngLatBounds,
|
|
365
|
+
map,
|
|
366
|
+
selectedFeatures: [{ featureId: 'uuid-abc', layerId: LAYER_ID, idProperty: 'uuid' }],
|
|
367
|
+
stylesMap: STYLES
|
|
368
|
+
})
|
|
369
|
+
const colorCall = map.setPaintProperty.mock.calls.find(
|
|
370
|
+
c => c[0] === LINE_LAYER_ID && c[1] === 'line-color'
|
|
371
|
+
)
|
|
372
|
+
expect(colorCall?.[2]).toBe('#0073cc')
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
test('reuses existing line layer without re-adding', () => {
|
|
376
|
+
map.getLayer.mockImplementation(id => {
|
|
377
|
+
if (id === LAYER_ID) return { source: 'composite', sourceLayer: 'building', type: 'fill-extrusion' }
|
|
378
|
+
if (id === LINE_LAYER_ID) return { type: 'line' }
|
|
379
|
+
return null
|
|
380
|
+
})
|
|
381
|
+
updateHighlightedFeatures({
|
|
382
|
+
LngLatBounds,
|
|
383
|
+
map,
|
|
384
|
+
selectedFeatures: [{ featureId: 'uuid-abc', layerId: LAYER_ID, idProperty: 'uuid' }],
|
|
385
|
+
stylesMap: STYLES
|
|
386
|
+
})
|
|
387
|
+
expect(map.addLayer).not.toHaveBeenCalled()
|
|
388
|
+
expect(map.setPaintProperty).toHaveBeenCalledWith(LINE_LAYER_ID, 'line-color', '#0073cc')
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
|
|
303
392
|
describe('Highlighting Utils — missing style entry', () => {
|
|
304
393
|
test('skips highlight when feature layerId has no entry in stylesMap', () => {
|
|
305
394
|
const map = makeMap()
|
|
@@ -315,4 +404,47 @@ describe('Highlighting Utils — missing style entry', () => {
|
|
|
315
404
|
expect(map.addLayer).not.toHaveBeenCalled()
|
|
316
405
|
expect(map.setFilter).not.toHaveBeenCalled()
|
|
317
406
|
})
|
|
407
|
+
|
|
408
|
+
test('returns early when base layer is not found', () => {
|
|
409
|
+
const map = makeMap()
|
|
410
|
+
let callCount = 0
|
|
411
|
+
map.getLayer.mockImplementation((id) => {
|
|
412
|
+
// First call from groupFeaturesBySource - return a valid layer
|
|
413
|
+
if (id === 'l1' && callCount === 0) {
|
|
414
|
+
callCount++
|
|
415
|
+
return { source: 's1', type: 'line' }
|
|
416
|
+
}
|
|
417
|
+
// Subsequent calls from applySourceHighlight - return null to trigger early return
|
|
418
|
+
return null
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
updateHighlightedFeatures({
|
|
422
|
+
LngLatBounds,
|
|
423
|
+
map,
|
|
424
|
+
selectedFeatures: [{ featureId: 1, layerId: 'l1' }],
|
|
425
|
+
stylesMap: { l1: { stroke: 'red' } }
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
expect(map.addLayer).not.toHaveBeenCalled()
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
test('handles unexpected geometry type gracefully', () => {
|
|
432
|
+
const map = makeMap()
|
|
433
|
+
map.getLayer.mockImplementation((id) => {
|
|
434
|
+
if (id === 'l1') {
|
|
435
|
+
return { source: 's1', type: 'raster' }
|
|
436
|
+
}
|
|
437
|
+
return null
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
updateHighlightedFeatures({
|
|
441
|
+
LngLatBounds,
|
|
442
|
+
map,
|
|
443
|
+
selectedFeatures: [{ featureId: 1, layerId: 'l1' }],
|
|
444
|
+
stylesMap: { l1: { stroke: 'red' } }
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
// Should not throw and not apply any highlighting for unexpected type
|
|
448
|
+
expect(map.addLayer).not.toHaveBeenCalled()
|
|
449
|
+
})
|
|
318
450
|
})
|
|
@@ -45,11 +45,9 @@ const rasterisePattern = async (dataset, mapStyleId, patternRegistry, pixelRatio
|
|
|
45
45
|
/**
|
|
46
46
|
* Register pattern images for the given pre-resolved pattern configs.
|
|
47
47
|
* Skips images that are already registered (safe to call on style change).
|
|
48
|
-
* Callers are responsible for sublayer merging before passing configs here
|
|
49
|
-
* (see `getPatternConfigs` in the datasets plugin adapter).
|
|
50
48
|
*
|
|
51
49
|
* @param {Object} map - MapLibre map instance
|
|
52
|
-
* @param {Object[]} styleArray -
|
|
50
|
+
* @param {Object[]} styleArray - an array of pattern style configs
|
|
53
51
|
* @param {string} mapStyleId
|
|
54
52
|
* @param {Object} patternRegistry
|
|
55
53
|
* @param {number} pixelRatio
|
|
@@ -67,7 +65,7 @@ export const addPatternsToMap = async (map, styleArray, mapStyleId, patternRegis
|
|
|
67
65
|
if (imageId && !imagesToAdd[imageId] && !map.hasImage(imageId)) {
|
|
68
66
|
imagesToAdd[imageId] = async () => {
|
|
69
67
|
const result = await rasterisePattern(style, mapStyleId, patternRegistry, pixelRatio)
|
|
70
|
-
if (result) {
|
|
68
|
+
if (result && !map.hasImage(result.imageId)) {
|
|
71
69
|
map.addImage(result.imageId, result.imageData, { pixelRatio: effectiveRatio })
|
|
72
70
|
}
|
|
73
71
|
}
|
|
@@ -41,11 +41,8 @@ export const anchorToMaplibre = ([ax, ay]) => {
|
|
|
41
41
|
* Skips images that are already registered (safe to call on style change).
|
|
42
42
|
* Updates `map._activeSymbolImageMap` (normal→active) and `map._selectedSymbolImageMap` (normal→selected).
|
|
43
43
|
*
|
|
44
|
-
* Callers are responsible for resolving sublayers before calling this function
|
|
45
|
-
* (see `getSymbolConfigs` in the datasets plugin adapter).
|
|
46
|
-
*
|
|
47
44
|
* @param {Object} map - MapLibre map instance
|
|
48
|
-
* @param {Object[]} styleArray -
|
|
45
|
+
* @param {Object[]} styleArray - an array of symbol configs
|
|
49
46
|
* @param {Object} mapStyle - Current map style config (provides id, selectedColor, haloColor)
|
|
50
47
|
* @param {Object} symbolRegistry
|
|
51
48
|
* @param {number} [pixelRatio=2] - Device pixel ratio × map size scale factor (computed by caller)
|
package/rollup.esm.mjs
CHANGED
|
@@ -264,15 +264,9 @@ const ALL_BUILDS = [
|
|
|
264
264
|
{
|
|
265
265
|
entryPath: './plugins/beta/datasets/src/index.js',
|
|
266
266
|
outDir: 'plugins/beta/datasets/dist/esm',
|
|
267
|
-
manualChunks: (id) => { if (id.includes('/manifest')) return 'im-datasets-plugin' }
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
entryPath: './plugins/beta/datasets/src/adapters/maplibre/index.js',
|
|
271
|
-
outDir: 'plugins/beta/datasets/dist/adapters/maplibre/esm',
|
|
272
267
|
manualChunks: (id) => {
|
|
273
|
-
if (id.includes('
|
|
274
|
-
|
|
275
|
-
}
|
|
268
|
+
if (id.includes('/manifest')) return 'im-datasets-plugin'
|
|
269
|
+
if (id.includes('maplibreLayerAdapter')) return 'im-datasets-ml-adapter'
|
|
276
270
|
}
|
|
277
271
|
},
|
|
278
272
|
{
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
.im-c-cross-hair {
|
|
2
|
-
|
|
2
|
+
position: absolute;
|
|
3
3
|
top: 0;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
left: 0;
|
|
5
|
+
pointer-events: none;
|
|
6
|
+
margin: -19px 0 0 -19px;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
.im-c-cross-hair path {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
stroke-width: 3;
|
|
11
|
+
stroke: var(--map-overlay-halo-color);
|
|
12
|
+
fill: var(--map-overlay-foreground-color);
|
|
13
|
+
paint-order: stroke fill;
|
|
14
14
|
}
|
|
@@ -54,6 +54,18 @@ const captureMenuRect = (buttonRefs, buttonId, setMenuRect) => {
|
|
|
54
54
|
setMenuRect(btn.getBoundingClientRect().toJSON())
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Returns a keydown handler for buttons that control a popup menu.
|
|
59
|
+
* Prevents default scrolling behavior for ArrowDown and ArrowUp keys.
|
|
60
|
+
* @param {boolean} hasMenu - Whether the button has a popup menu
|
|
61
|
+
* @returns {Function} Keyboard event handler
|
|
62
|
+
*/
|
|
63
|
+
const makePopupKeyDownHandler = (hasMenu) => (e) => {
|
|
64
|
+
if (hasMenu && ['ArrowDown', 'ArrowUp'].includes(e.key)) {
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
/**
|
|
58
70
|
* Returns a keyup handler for buttons that control a popup menu.
|
|
59
71
|
* ArrowDown opens the menu at the first item; ArrowUp opens at the last.
|
|
@@ -67,7 +79,6 @@ const captureMenuRect = (buttonRefs, buttonId, setMenuRect) => {
|
|
|
67
79
|
*/
|
|
68
80
|
const makePopupKeyUpHandler = (hasMenu, buttonRefs, buttonId, setMenuStartPos, setMenuRect, setIsPopupOpen) => (e) => {
|
|
69
81
|
if (hasMenu && ['ArrowDown', 'ArrowUp'].includes(e.key)) {
|
|
70
|
-
e.preventDefault()
|
|
71
82
|
setMenuStartPos(e.key === 'ArrowUp' ? 'last' : 'first')
|
|
72
83
|
captureMenuRect(buttonRefs, buttonId, setMenuRect)
|
|
73
84
|
setIsPopupOpen(true)
|
|
@@ -121,6 +132,7 @@ const buildButtonProps = ({
|
|
|
121
132
|
className,
|
|
122
133
|
onClick,
|
|
123
134
|
onKeyUp,
|
|
135
|
+
onKeyDown,
|
|
124
136
|
buttonRefs,
|
|
125
137
|
isDisabled,
|
|
126
138
|
isPressed,
|
|
@@ -146,6 +158,7 @@ const buildButtonProps = ({
|
|
|
146
158
|
className,
|
|
147
159
|
onClick,
|
|
148
160
|
onKeyUp,
|
|
161
|
+
onKeyDown,
|
|
149
162
|
ref: (el) => {
|
|
150
163
|
if (buttonRefs.current && buttonId) {
|
|
151
164
|
buttonRefs.current[buttonId] = el
|
|
@@ -242,6 +255,7 @@ export const MapButton = ({
|
|
|
242
255
|
}
|
|
243
256
|
|
|
244
257
|
const handleButtonKeyUp = makePopupKeyUpHandler(hasMenu, buttonRefs, buttonId, setMenuStartPos, setMenuRect, setIsPopupOpen)
|
|
258
|
+
const handleButtonKeyDown = makePopupKeyDownHandler(hasMenu)
|
|
245
259
|
|
|
246
260
|
const buttonProps = buildButtonProps({
|
|
247
261
|
appId,
|
|
@@ -249,6 +263,7 @@ export const MapButton = ({
|
|
|
249
263
|
className: buildButtonClassNames(buttonId, variant, showLabel),
|
|
250
264
|
onClick: handleButtonClick,
|
|
251
265
|
onKeyUp: handleButtonKeyUp,
|
|
266
|
+
onKeyDown: handleButtonKeyDown,
|
|
252
267
|
buttonRefs,
|
|
253
268
|
isDisabled,
|
|
254
269
|
isPressed,
|
|
@@ -189,4 +189,15 @@
|
|
|
189
189
|
background-color: var(--pressed-button-background-color);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
// 5. Responsive tweaks
|
|
192
|
+
// 5. Responsive tweaks
|
|
193
|
+
|
|
194
|
+
.im-o-app--tablet, .im-o-app--desktop {
|
|
195
|
+
.im-c-map-button--journey-back,
|
|
196
|
+
.im-c-map-button--journey-continue,
|
|
197
|
+
.im-c-map-button--select-at-target,
|
|
198
|
+
.im-c-map-button--draw-cancel,
|
|
199
|
+
.im-c-map-button--draw-done,
|
|
200
|
+
.im-c-map-button--draw-add-point {
|
|
201
|
+
min-width: var(--action-button-min-width);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { render, screen, fireEvent } from '@testing-library/react'
|
|
2
|
+
import { render, screen, fireEvent, createEvent } from '@testing-library/react'
|
|
3
3
|
import { MapButton } from './MapButton'
|
|
4
4
|
|
|
5
5
|
jest.mock('../../../utils/stringToKebab', () => ({
|
|
@@ -153,6 +153,45 @@ describe('MapButton', () => {
|
|
|
153
153
|
expect(menu).toHaveAttribute('data-selected-index', String(expectedIndex))
|
|
154
154
|
})
|
|
155
155
|
|
|
156
|
+
it('prevents default on keyDown for arrow keys with menu', () => {
|
|
157
|
+
renderButton({ menuItems: [{ label: 'Item' }] })
|
|
158
|
+
const button = getButton()
|
|
159
|
+
|
|
160
|
+
const event = createEvent.keyDown(button, { key: 'ArrowDown' })
|
|
161
|
+
const preventDefaultSpy = jest.spyOn(event, 'preventDefault')
|
|
162
|
+
|
|
163
|
+
fireEvent(button, event)
|
|
164
|
+
expect(preventDefaultSpy).toHaveBeenCalled()
|
|
165
|
+
preventDefaultSpy.mockRestore()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it.each([
|
|
169
|
+
['Enter'],
|
|
170
|
+
['Escape']
|
|
171
|
+
])('does not prevent default on keyDown for key %s', (key) => {
|
|
172
|
+
renderButton({ menuItems: [{ label: 'Item' }] })
|
|
173
|
+
const button = getButton()
|
|
174
|
+
|
|
175
|
+
const event = createEvent.keyDown(button, { key })
|
|
176
|
+
const preventDefaultSpy = jest.spyOn(event, 'preventDefault')
|
|
177
|
+
|
|
178
|
+
fireEvent(button, event)
|
|
179
|
+
expect(preventDefaultSpy).not.toHaveBeenCalled()
|
|
180
|
+
preventDefaultSpy.mockRestore()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('does not prevent default on keyDown for arrow keys without menu', () => {
|
|
184
|
+
renderButton()
|
|
185
|
+
const button = getButton()
|
|
186
|
+
|
|
187
|
+
const event = createEvent.keyDown(button, { key: 'ArrowDown' })
|
|
188
|
+
const preventDefaultSpy = jest.spyOn(event, 'preventDefault')
|
|
189
|
+
|
|
190
|
+
fireEvent(button, event)
|
|
191
|
+
expect(preventDefaultSpy).not.toHaveBeenCalled()
|
|
192
|
+
preventDefaultSpy.mockRestore()
|
|
193
|
+
})
|
|
194
|
+
|
|
156
195
|
it('passes buttonRect to popup menu on open', () => {
|
|
157
196
|
renderButton({ menuItems: [{ label: 'Item' }] })
|
|
158
197
|
fireEvent.click(getButton())
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useLayoutEffect, useContext, useRef } from 'react'
|
|
2
|
+
import { useApp } from '../store/appContext.js'
|
|
3
|
+
import { useConfig } from '../store/configContext.js'
|
|
4
|
+
import { useMap } from '../store/mapContext.js'
|
|
5
|
+
import { PluginContext } from '../store/PluginProvider.jsx'
|
|
6
|
+
|
|
7
|
+
export function useContinueEnabledEvaluator () {
|
|
8
|
+
const backAndContinue = useConfig()?.backAndContinue
|
|
9
|
+
const { dispatch } = useApp()
|
|
10
|
+
const mapState = useMap()
|
|
11
|
+
const pluginContext = useContext(PluginContext)
|
|
12
|
+
const lastIsDisabledRef = useRef(true)
|
|
13
|
+
|
|
14
|
+
useLayoutEffect(() => {
|
|
15
|
+
if (typeof backAndContinue?.continueEnabledWhen !== 'function') { return }
|
|
16
|
+
let isEnabled = false
|
|
17
|
+
try {
|
|
18
|
+
isEnabled = backAndContinue.continueEnabledWhen({
|
|
19
|
+
pluginStates: pluginContext?.state ?? {},
|
|
20
|
+
mapState
|
|
21
|
+
})
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.warn('continueEnabledWhen error:', err)
|
|
24
|
+
}
|
|
25
|
+
const isDisabled = !isEnabled
|
|
26
|
+
if (lastIsDisabledRef.current !== isDisabled) {
|
|
27
|
+
lastIsDisabledRef.current = isDisabled
|
|
28
|
+
dispatch({ type: 'TOGGLE_BUTTON_DISABLED', payload: { id: 'journeyContinue', isDisabled } })
|
|
29
|
+
}
|
|
30
|
+
}, [backAndContinue, pluginContext, mapState, dispatch])
|
|
31
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { renderHook } from '@testing-library/react'
|
|
3
|
+
import { useContinueEnabledEvaluator } from './useContinueEnabledEvaluator.js'
|
|
4
|
+
import { PluginContext } from '../store/PluginProvider.jsx'
|
|
5
|
+
import * as configStore from '../store/configContext.js'
|
|
6
|
+
import * as appStore from '../store/appContext.js'
|
|
7
|
+
import * as mapStore from '../store/mapContext.js'
|
|
8
|
+
|
|
9
|
+
jest.mock('../store/configContext.js', () => ({ useConfig: jest.fn() }))
|
|
10
|
+
jest.mock('../store/appContext.js', () => ({ useApp: jest.fn() }))
|
|
11
|
+
jest.mock('../store/mapContext.js', () => ({ useMap: jest.fn() }))
|
|
12
|
+
|
|
13
|
+
const pluginStates = { interact: { selectedFeatures: ['f1', 'f2'] } }
|
|
14
|
+
const mockMapState = { zoom: 10, center: [0, 0] }
|
|
15
|
+
|
|
16
|
+
const wrapper = ({ children }) => (
|
|
17
|
+
<PluginContext.Provider value={{ state: pluginStates, dispatch: jest.fn() }}>
|
|
18
|
+
{children}
|
|
19
|
+
</PluginContext.Provider>
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
let dispatch
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks()
|
|
26
|
+
dispatch = jest.fn()
|
|
27
|
+
appStore.useApp.mockReturnValue({ dispatch })
|
|
28
|
+
mapStore.useMap.mockReturnValue(mockMapState)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('useContinueEnabledEvaluator — no-op cases', () => {
|
|
32
|
+
it('does nothing when continueEnabledWhen is not provided', () => {
|
|
33
|
+
configStore.useConfig.mockReturnValue({ backAndContinue: { continueLabel: 'Continue' } })
|
|
34
|
+
renderHook(() => useContinueEnabledEvaluator(), { wrapper })
|
|
35
|
+
expect(dispatch).not.toHaveBeenCalled()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('does nothing when backAndContinue is null', () => {
|
|
39
|
+
configStore.useConfig.mockReturnValue({ backAndContinue: null })
|
|
40
|
+
renderHook(() => useContinueEnabledEvaluator(), { wrapper })
|
|
41
|
+
expect(dispatch).not.toHaveBeenCalled()
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('useContinueEnabledEvaluator — dispatching', () => {
|
|
46
|
+
it('dispatches enable on first render when continueEnabledWhen returns true', () => {
|
|
47
|
+
configStore.useConfig.mockReturnValue({
|
|
48
|
+
backAndContinue: { continueEnabledWhen: ({ pluginStates: ps }) => ps.interact.selectedFeatures.length > 1 }
|
|
49
|
+
})
|
|
50
|
+
renderHook(() => useContinueEnabledEvaluator(), { wrapper })
|
|
51
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
52
|
+
type: 'TOGGLE_BUTTON_DISABLED',
|
|
53
|
+
payload: { id: 'journeyContinue', isDisabled: false }
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('does not dispatch on first render when continueEnabledWhen returns false (already disabled)', () => {
|
|
58
|
+
configStore.useConfig.mockReturnValue({
|
|
59
|
+
backAndContinue: { continueEnabledWhen: () => false }
|
|
60
|
+
})
|
|
61
|
+
renderHook(() => useContinueEnabledEvaluator(), { wrapper })
|
|
62
|
+
expect(dispatch).not.toHaveBeenCalled()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('dispatches disable after button was enabled', () => {
|
|
66
|
+
let enabled = true
|
|
67
|
+
configStore.useConfig.mockReturnValue({
|
|
68
|
+
backAndContinue: { continueEnabledWhen: () => enabled }
|
|
69
|
+
})
|
|
70
|
+
const { rerender } = renderHook(() => useContinueEnabledEvaluator(), { wrapper })
|
|
71
|
+
dispatch.mockClear()
|
|
72
|
+
enabled = false
|
|
73
|
+
rerender()
|
|
74
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
75
|
+
type: 'TOGGLE_BUTTON_DISABLED',
|
|
76
|
+
payload: { id: 'journeyContinue', isDisabled: true }
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('does not dispatch again if the enabled state has not changed', () => {
|
|
81
|
+
configStore.useConfig.mockReturnValue({
|
|
82
|
+
backAndContinue: { continueEnabledWhen: () => true }
|
|
83
|
+
})
|
|
84
|
+
const { rerender } = renderHook(() => useContinueEnabledEvaluator(), { wrapper })
|
|
85
|
+
dispatch.mockClear()
|
|
86
|
+
rerender()
|
|
87
|
+
expect(dispatch).not.toHaveBeenCalled()
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('useContinueEnabledEvaluator — no PluginContext', () => {
|
|
92
|
+
it('passes empty pluginStates when PluginContext is not provided', () => {
|
|
93
|
+
const continueEnabledWhen = jest.fn(() => true)
|
|
94
|
+
configStore.useConfig.mockReturnValue({ backAndContinue: { continueEnabledWhen } })
|
|
95
|
+
renderHook(() => useContinueEnabledEvaluator())
|
|
96
|
+
expect(continueEnabledWhen).toHaveBeenCalledWith({ pluginStates: {}, mapState: mockMapState })
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('useContinueEnabledEvaluator — error handling and context', () => {
|
|
101
|
+
it('warns and leaves button disabled if continueEnabledWhen throws on first render', () => {
|
|
102
|
+
jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
103
|
+
configStore.useConfig.mockReturnValue({
|
|
104
|
+
backAndContinue: { continueEnabledWhen: () => { throw new Error('oops') } }
|
|
105
|
+
})
|
|
106
|
+
renderHook(() => useContinueEnabledEvaluator(), { wrapper })
|
|
107
|
+
expect(console.warn).toHaveBeenCalledWith('continueEnabledWhen error:', expect.any(Error))
|
|
108
|
+
expect(dispatch).not.toHaveBeenCalled()
|
|
109
|
+
console.warn.mockRestore()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('passes pluginStates and mapState to continueEnabledWhen', () => {
|
|
113
|
+
const continueEnabledWhen = jest.fn(() => true)
|
|
114
|
+
configStore.useConfig.mockReturnValue({ backAndContinue: { continueEnabledWhen } })
|
|
115
|
+
renderHook(() => useContinueEnabledEvaluator(), { wrapper })
|
|
116
|
+
expect(continueEnabledWhen).toHaveBeenCalledWith({ pluginStates, mapState: mockMapState })
|
|
117
|
+
})
|
|
118
|
+
})
|