@defra/interactive-map 0.0.30-alpha → 0.0.31-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.
Files changed (192) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/esm/im-shell.js +1 -1
  4. package/dist/umd/im-core.js +1 -1
  5. package/dist/umd/index.js +1 -1
  6. package/docs/api/context.md +4 -4
  7. package/docs/api.md +111 -37
  8. package/docs/assets/css/docusaurus.scss +20 -4
  9. package/docs/assets/images/basic-map.jpg +0 -0
  10. package/docs/assets/images/button-first.jpg +0 -0
  11. package/docs/assets/images/deuteranopia-ozs-map-thumb.jpg +0 -0
  12. package/docs/assets/images/draw.jpg +0 -0
  13. package/docs/assets/images/marker-panel.jpg +0 -0
  14. package/docs/assets/images/night-ozs-map-thumb.jpg +0 -0
  15. package/docs/assets/images/outdoor-ozs-map-thumb.jpg +0 -0
  16. package/docs/assets/images/search.jpg +0 -0
  17. package/docs/assets/images/select-feature.jpg +0 -0
  18. package/docs/assets/images/style-switcher.jpg +0 -0
  19. package/docs/assets/images/toggle-marker-label.jpg +0 -0
  20. package/docs/assets/images/tritanopia-ozs-map-thumb.jpg +0 -0
  21. package/docs/examples/add-marker-with-panel.mdx +2 -2
  22. package/docs/examples/basic-map.mdx +2 -2
  23. package/docs/examples/draw.mdx +271 -0
  24. package/docs/examples/index.mdx +58 -10
  25. package/docs/examples/search-control.mdx +71 -0
  26. package/docs/examples/select-feature.mdx +146 -0
  27. package/docs/examples/style-switcher.mdx +135 -0
  28. package/docs/examples/toggle-marker-label.mdx +3 -3
  29. package/docs/plugins/datasets.md +15 -28
  30. package/docusaurus.config.cjs +5 -1
  31. package/package.json +1 -1
  32. package/plugins/beta/datasets/dist/css/index.css +4 -0
  33. package/plugins/beta/datasets/dist/esm/im-datasets-ml-adapter.js +1 -0
  34. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  35. package/plugins/beta/datasets/dist/esm/index.js +1 -1
  36. package/plugins/beta/datasets/dist/umd/im-datasets-ml-adapter.js +2 -0
  37. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +2 -0
  38. package/plugins/beta/datasets/dist/umd/index.js +2 -0
  39. package/plugins/beta/datasets/src/adapters/loadLayerAdapter.js +33 -0
  40. package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +44 -120
  41. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +145 -245
  42. package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +516 -0
  43. package/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.js +191 -0
  44. package/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js +441 -0
  45. package/plugins/beta/datasets/src/api/addDataset.js +2 -5
  46. package/plugins/beta/datasets/src/api/addDataset.test.js +16 -0
  47. package/plugins/beta/datasets/src/api/getOpacity.js +14 -14
  48. package/plugins/beta/datasets/src/api/getOpacity.test.js +72 -0
  49. package/plugins/beta/datasets/src/api/getStyle.js +8 -9
  50. package/plugins/beta/datasets/src/api/getStyle.test.js +42 -0
  51. package/plugins/beta/datasets/src/api/removeDataset.js +7 -7
  52. package/plugins/beta/datasets/src/api/removeDataset.test.js +45 -0
  53. package/plugins/beta/datasets/src/api/setData.js +9 -3
  54. package/plugins/beta/datasets/src/api/setData.test.js +62 -0
  55. package/plugins/beta/datasets/src/api/setDatasetVisibility.js +5 -30
  56. package/plugins/beta/datasets/src/api/setDatasetVisibility.test.js +31 -0
  57. package/plugins/beta/datasets/src/api/setFeatureVisibility.js +3 -21
  58. package/plugins/beta/datasets/src/api/setFeatureVisibility.test.js +32 -0
  59. package/plugins/beta/datasets/src/api/setGlobals.js +20 -0
  60. package/plugins/beta/datasets/src/api/setGlobals.test.js +63 -0
  61. package/plugins/beta/datasets/src/api/setOpacity.js +8 -27
  62. package/plugins/beta/datasets/src/api/setOpacity.test.js +31 -0
  63. package/plugins/beta/datasets/src/api/setStyle.js +5 -21
  64. package/plugins/beta/datasets/src/api/setStyle.test.js +53 -0
  65. package/plugins/beta/datasets/src/components/Key/Key.jsx +51 -0
  66. package/plugins/beta/datasets/src/components/Key/KeyGroupItem.jsx +18 -0
  67. package/plugins/beta/datasets/src/components/Key/KeyItem.jsx +20 -0
  68. package/plugins/beta/datasets/src/components/Key/KeySvg.jsx +22 -0
  69. package/plugins/beta/datasets/src/components/Key/KeySvg.test.jsx +87 -0
  70. package/plugins/beta/datasets/src/components/Key/KeySvgLine.jsx +19 -0
  71. package/plugins/beta/datasets/src/components/Key/KeySvgLine.test.jsx +71 -0
  72. package/plugins/beta/datasets/src/components/{KeySvgPattern.jsx → Key/KeySvgPattern.jsx} +3 -4
  73. package/plugins/beta/datasets/src/components/Key/KeySvgPattern.test.jsx +55 -0
  74. package/plugins/beta/datasets/src/components/Key/KeySvgRect.jsx +22 -0
  75. package/plugins/beta/datasets/src/components/Key/KeySvgRect.test.jsx +85 -0
  76. package/plugins/beta/datasets/src/components/{KeySvgSymbol.jsx → Key/KeySvgSymbol.jsx} +5 -5
  77. package/plugins/beta/datasets/src/components/Key/KeySvgSymbol.test.jsx +80 -0
  78. package/plugins/beta/datasets/src/components/Key/svgProperties.test.js +61 -0
  79. package/plugins/beta/datasets/src/{panels → components/LayersMenu}/Layers.module.scss +4 -0
  80. package/plugins/beta/datasets/src/components/LayersMenu/LayersMenu.jsx +33 -0
  81. package/plugins/beta/datasets/src/components/LayersMenu/LayersMenuCheckbox.jsx +31 -0
  82. package/plugins/beta/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx +19 -0
  83. package/plugins/beta/datasets/src/datasets.scss +2 -2
  84. package/plugins/beta/datasets/src/fetch/createDynamicSource.js +8 -8
  85. package/plugins/beta/datasets/src/{DatasetsInit.jsx → initialise/DatasetsInit.jsx} +19 -21
  86. package/plugins/beta/datasets/src/{defaults.js → initialise/defaults.js} +17 -2
  87. package/plugins/beta/datasets/src/initialise/defaults.test.js +127 -0
  88. package/plugins/beta/datasets/src/{datasets.js → initialise/initialiseDatasets.js} +30 -26
  89. package/plugins/beta/datasets/src/initialise/initialiseDatasets.test.js +273 -0
  90. package/plugins/beta/datasets/src/manifest.js +8 -6
  91. package/plugins/beta/datasets/src/reducers/__data__/demoDatasets.js +253 -0
  92. package/plugins/beta/datasets/src/reducers/datasetsToMenu.js +78 -0
  93. package/plugins/beta/datasets/src/reducers/datasetsToMenu.test.js +35 -0
  94. package/plugins/beta/datasets/src/reducers/mappedDatasetsReducer.js +34 -0
  95. package/plugins/beta/datasets/src/reducers/mappedDatasetsReducer.test.js +117 -0
  96. package/plugins/beta/datasets/src/reducers/pluginState.js +206 -0
  97. package/plugins/beta/datasets/src/reducers/pluginState.test.js +386 -0
  98. package/plugins/beta/datasets/src/registry/__mocks__/datasetRegistry.js +29 -0
  99. package/plugins/beta/datasets/src/registry/createDataset.js +5 -0
  100. package/plugins/beta/datasets/src/registry/dataset.js +172 -0
  101. package/plugins/beta/datasets/src/registry/dataset.test.js +483 -0
  102. package/plugins/beta/datasets/src/registry/datasetDefinitionCache.js +35 -0
  103. package/plugins/beta/datasets/src/registry/datasetRegistry.js +116 -0
  104. package/plugins/beta/datasets/src/registry/datasetRegistry.test.js +335 -0
  105. package/plugins/beta/datasets/src/registry/dynamicGeoJson.js +17 -0
  106. package/plugins/beta/datasets/src/registry/globalDataset.js +36 -0
  107. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  108. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  109. package/plugins/beta/draw-ml/src/events.js +13 -1
  110. package/plugins/beta/draw-ml/src/index.js +1 -1
  111. package/plugins/beta/draw-ml/src/manifest.js +3 -0
  112. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  113. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  114. package/plugins/interact/src/InteractInit.jsx +2 -2
  115. package/plugins/interact/src/InteractInit.test.js +1 -1
  116. package/plugins/interact/src/events.js +2 -29
  117. package/plugins/interact/src/events.test.js +4 -102
  118. package/plugins/interact/src/hooks/useAttachEvents.js +3 -4
  119. package/plugins/interact/src/hooks/useAttachEvents.test.js +1 -3
  120. package/plugins/interact/src/hooks/useInteractionHandlers.js +10 -1
  121. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +23 -4
  122. package/plugins/interact/src/manifest.js +0 -35
  123. package/plugins/interact/src/manifest.test.js +3 -43
  124. package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
  125. package/providers/beta/esri/src/esriProvider.js +4 -0
  126. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  127. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  128. package/providers/beta/openlayers/src/openlayersProvider.js +4 -0
  129. package/providers/beta/openlayers/src/openlayersProvider.test.js +5 -0
  130. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  131. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  132. package/providers/maplibre/src/maplibreProvider.js +6 -4
  133. package/providers/maplibre/src/maplibreProvider.test.js +5 -0
  134. package/providers/maplibre/src/utils/highlightFeatures.js +74 -25
  135. package/providers/maplibre/src/utils/highlightFeatures.test.js +132 -0
  136. package/providers/maplibre/src/utils/patternImages.js +2 -4
  137. package/providers/maplibre/src/utils/symbolImages.js +1 -4
  138. package/rollup.esm.mjs +2 -8
  139. package/src/App/components/MapButton/MapButton.jsx +16 -1
  140. package/src/App/components/MapButton/MapButton.module.scss +12 -1
  141. package/src/App/components/MapButton/MapButton.test.jsx +40 -1
  142. package/src/App/hooks/useContinueEnabledEvaluator.js +31 -0
  143. package/src/App/hooks/useContinueEnabledEvaluator.test.js +118 -0
  144. package/src/App/hooks/useEvaluateProp.js +15 -15
  145. package/src/App/hooks/useEvaluateProp.test.js +80 -76
  146. package/src/App/hooks/useLayoutMeasurements.js +1 -3
  147. package/src/App/hooks/useResizeObserver.js +12 -1
  148. package/src/App/hooks/useResizeObserver.test.js +1 -0
  149. package/src/App/renderer/PluginInits.jsx +2 -0
  150. package/src/App/renderer/mapButtons.js +21 -1
  151. package/src/App/renderer/mapButtons.test.js +46 -1
  152. package/src/App/store/appReducer.js +1 -1
  153. package/src/App/store/appReducer.test.js +37 -0
  154. package/src/InteractiveMap/InteractiveMap.js +10 -0
  155. package/src/InteractiveMap/InteractiveMap.test.js +8 -0
  156. package/src/config/appConfig.js +25 -0
  157. package/src/config/appConfig.test.js +50 -0
  158. package/src/config/defaults.js +1 -0
  159. package/src/scss/settings/_dimensions.scss +1 -0
  160. package/src/services/patternRegistry.js +8 -4
  161. package/src/types.js +7 -0
  162. package/src/utils/patternUtils.js +1 -2
  163. package/src/utils/patternUtils.test.js +9 -0
  164. package/webpack.dev.mjs +10 -0
  165. package/webpack.umd.mjs +1 -1
  166. package/plugins/beta/datasets/dist/adapters/maplibre/esm/im-datasets-ml-adapter.js +0 -1
  167. package/plugins/beta/datasets/dist/adapters/maplibre/esm/index.js +0 -1
  168. package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-maplibre-adapter.js +0 -2
  169. package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-plugin.js +0 -2
  170. package/plugins/beta/datasets/dist/umd/maplibre/index.js +0 -2
  171. package/plugins/beta/datasets/src/adapters/maplibre/index.js +0 -18
  172. package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +0 -75
  173. package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +0 -27
  174. package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +0 -31
  175. package/plugins/beta/datasets/src/components/KeySvg.jsx +0 -24
  176. package/plugins/beta/datasets/src/components/KeySvgLine.jsx +0 -19
  177. package/plugins/beta/datasets/src/components/KeySvgRect.jsx +0 -22
  178. package/plugins/beta/datasets/src/index.maplibre.umd.js +0 -23
  179. package/plugins/beta/datasets/src/panels/Key.jsx +0 -62
  180. package/plugins/beta/datasets/src/panels/Layers.jsx +0 -141
  181. package/plugins/beta/datasets/src/reducer.js +0 -219
  182. package/plugins/beta/datasets/src/reducers/keyReducer.js +0 -34
  183. package/plugins/beta/datasets/src/utils/filters.js +0 -34
  184. package/plugins/beta/datasets/src/utils/mergeSublayer.js +0 -86
  185. package/plugins/beta/draw-ml/dist/css/index.css +0 -21
  186. /package/plugins/beta/datasets/dist/umd/{maplibre/im-datasets-maplibre-adapter.js.LICENSE.txt → im-datasets-ml-adapter.js.LICENSE.txt} +0 -0
  187. /package/plugins/beta/datasets/dist/umd/{maplibre/im-datasets-plugin.js.LICENSE.txt → im-datasets-plugin.js.LICENSE.txt} +0 -0
  188. /package/plugins/beta/datasets/dist/umd/{maplibre/index.js.LICENSE.txt → index.js.LICENSE.txt} +0 -0
  189. /package/plugins/beta/datasets/src/components/{EmptyKey.jsx → Key/EmptyKey.jsx} +0 -0
  190. /package/plugins/beta/datasets/src/components/{EmptyKey.test.jsx → Key/EmptyKey.test.jsx} +0 -0
  191. /package/plugins/beta/datasets/src/{panels → components/Key}/Key.module.scss +0 -0
  192. /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
- if (!map.getLayer(id)) {
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 fillFilter = ['in', idExpression, ['literal', [...fillIds]]]
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 (!style) {
176
+ if (!baseLayer || !stylesMap[layerId]) {
143
177
  return
144
178
  }
145
179
 
146
- const { stroke, selectionStroke, strokeWidth, activeStrokeWidth, fill } = style
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 = selectedStyle ? selectionStroke : stroke
150
- const lineWidth = selectedStyle ? strokeWidth : activeStrokeWidth
187
+ const { lineColor, lineWidth } = resolveLineStyle(style, selectedStyle)
151
188
  const idExpression = idProperty ? ['get', idProperty] : ['id']
152
- const filter = ['in', idExpression, ['literal', [...ids]]]
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 (geom === 'line') {
159
- if (map.getLayer(`${base}-fill`)) {
160
- // Clear any fill highlight from a previous polygon on the same source
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
- if (geom === 'symbol') {
167
- applySymbolGeomHighlight(map, base, sourceId, srcLayer, layerId, filter, getSymbolImageId)
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 - Flat list of datasets/merged-sublayers with a pattern config
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 - Flat list of datasets/merged-sublayers that have a symbol config
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('maplibreLayerAdapter')) {
274
- return 'im-datasets-ml-adapter'
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
  {
@@ -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
+ })
@@ -23,25 +23,25 @@ export function useEvaluateProp () {
23
23
  iconRegistry: getIconRegistry()
24
24
  }
25
25
 
26
- function evaluateProp (prop, pluginId) {
27
- let pluginConfig
28
- let pluginState
29
-
30
- if (pluginId) {
31
- const pluginEntry = appConfig.pluginRegistry.registeredPlugins.find(p => p.id === pluginId)
32
- pluginConfig = pluginEntry
33
- ? {
34
- pluginId: pluginEntry.id,
35
- ...pluginEntry.config
36
- }
37
- : {}
38
- // Only include this plugin's state + dispatch
26
+ function buildPluginContext (pluginId) {
27
+ if (!pluginId) { return { pluginStates: pluginContext?.state ?? {} } }
28
+
29
+ const pluginEntry = appConfig.pluginRegistry.registeredPlugins.find(p => p.id === pluginId)
30
+ const pluginConfig = pluginEntry ? { pluginId: pluginEntry.id, ...pluginEntry.config } : {}
31
+
32
+ // Only include isolated plugin state when the plugin has registered a reducer.
33
+ // Framework entries like 'appConfig' have no reducer so their buttons get pluginStates instead.
34
+ if (Object.hasOwn(pluginContext?.state ?? {}, pluginId)) {
39
35
  const stateForPlugin = pluginContext?.state?.[pluginId] ?? {}
40
- pluginState = { ...stateForPlugin, dispatch: pluginContext?.dispatch }
36
+ const pluginState = { ...stateForPlugin, dispatch: pluginContext?.dispatch }
37
+ return { pluginConfig, pluginState }
41
38
  }
42
39
 
43
- const fullContext = { ...ctx, pluginConfig, pluginState }
40
+ return { pluginConfig, pluginStates: pluginContext?.state ?? {} }
41
+ }
44
42
 
43
+ function evaluateProp (prop, pluginId) {
44
+ const fullContext = { ...ctx, ...buildPluginContext(pluginId) }
45
45
  return typeof prop === 'function' ? prop(fullContext) : prop
46
46
  }
47
47