@defra/interactive-map 0.0.10-alpha → 0.0.12-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 (121) hide show
  1. package/README.md +1 -1
  2. package/dist/css/index.css +1 -1
  3. package/dist/esm/im-core.js +1 -1
  4. package/dist/esm/im-shell.js +1 -1
  5. package/dist/umd/im-core.js +1 -1
  6. package/dist/umd/index.js +1 -1
  7. package/docs/api/button-definition.md +21 -3
  8. package/docs/api/panel-definition.md +10 -12
  9. package/docs/api.md +80 -7
  10. package/docs/demo.mdx +70 -0
  11. package/docs/index.md +0 -4
  12. package/docs/plugins/plugin-context.md +3 -3
  13. package/docs/plugins/plugin-descriptor.md +37 -0
  14. package/docs/plugins/plugin-manifest.md +1 -1
  15. package/docusaurus.config.cjs +55 -25
  16. package/package.json +18 -9
  17. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
  18. package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
  19. package/plugins/beta/datasets/src/manifest.js +3 -3
  20. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
  21. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  22. package/plugins/beta/draw-ml/src/events.js +4 -14
  23. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +1 -3
  24. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  25. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  26. package/plugins/beta/map-styles/src/manifest.js +3 -3
  27. package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
  28. package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
  29. package/plugins/beta/use-location/src/manifest.js +7 -7
  30. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  31. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  32. package/plugins/interact/src/InteractInit.jsx +28 -6
  33. package/plugins/interact/src/InteractInit.test.js +19 -5
  34. package/plugins/interact/src/events.js +17 -15
  35. package/plugins/interact/src/events.test.js +25 -16
  36. package/plugins/search/dist/css/index.css +1 -1
  37. package/plugins/search/dist/esm/im-search-plugin.js +1 -1
  38. package/plugins/search/dist/esm/index.js +1 -1
  39. package/plugins/search/dist/umd/im-search-plugin.js +1 -1
  40. package/plugins/search/dist/umd/index.js +1 -1
  41. package/plugins/search/src/Search.jsx +9 -3
  42. package/plugins/search/src/Search.test.jsx +26 -6
  43. package/plugins/search/src/components/Form/Form.jsx +35 -7
  44. package/plugins/search/src/components/Form/Form.module.scss +27 -0
  45. package/plugins/search/src/components/Form/Form.test.jsx +99 -2
  46. package/plugins/search/src/components/SubmitButton/SubmitButton.jsx +28 -0
  47. package/plugins/search/src/components/SubmitButton/SubmitButton.module.scss +8 -0
  48. package/plugins/search/src/components/SubmitButton/SubmitButton.test.jsx +33 -0
  49. package/plugins/search/src/datasets.js +15 -11
  50. package/plugins/search/src/datasets.test.js +17 -2
  51. package/plugins/search/src/events/fetchSuggestions.js +1 -1
  52. package/plugins/search/src/index.js +1 -1
  53. package/plugins/search/src/index.test.js +4 -4
  54. package/plugins/search/src/reducer.js +9 -4
  55. package/plugins/search/src/reducer.test.js +12 -7
  56. package/plugins/search/src/search.scss +5 -1
  57. package/plugins/search/src/utils/parseOsNamesResults.js +18 -2
  58. package/plugins/search/src/utils/parseOsNamesResults.test.js +33 -15
  59. package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
  60. package/providers/beta/esri/src/appEvents.js +8 -2
  61. package/providers/beta/esri/src/esriProvider.js +25 -17
  62. package/providers/beta/esri/src/mapEvents.js +41 -4
  63. package/providers/beta/esri/src/utils/coords.js +34 -1
  64. package/providers/beta/esri/src/utils/coords.test.js +126 -0
  65. package/providers/beta/esri/src/utils/spatial.js +47 -1
  66. package/providers/beta/esri/src/utils/spatial.test.js +55 -0
  67. package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
  68. package/providers/maplibre/dist/esm/index.js +1 -1
  69. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  70. package/providers/maplibre/dist/umd/index.js +1 -1
  71. package/providers/maplibre/src/appEvents.js +10 -1
  72. package/providers/maplibre/src/appEvents.test.js +13 -4
  73. package/providers/maplibre/src/index.js +5 -13
  74. package/providers/maplibre/src/index.test.js +34 -15
  75. package/providers/maplibre/src/mapEvents.js +9 -1
  76. package/providers/maplibre/src/maplibreProvider.js +25 -15
  77. package/providers/maplibre/src/maplibreProvider.test.js +28 -2
  78. package/providers/maplibre/src/utils/spatial.js +51 -0
  79. package/providers/maplibre/src/utils/spatial.test.js +47 -0
  80. package/src/App/components/Actions/Actions.module.scss +5 -4
  81. package/src/App/components/MapButton/MapButton.jsx +4 -16
  82. package/src/App/components/MapButton/MapButton.module.scss +12 -12
  83. package/src/App/components/MapButton/MapButton.test.jsx +0 -9
  84. package/src/App/components/Panel/Panel.jsx +6 -6
  85. package/src/App/components/Panel/Panel.test.jsx +14 -15
  86. package/src/App/components/Viewport/MapController.jsx +6 -1
  87. package/src/App/hooks/useLayoutMeasurements.js +1 -1
  88. package/src/App/hooks/useLayoutMeasurements.test.js +1 -1
  89. package/src/App/hooks/useMapProviderOverrides.js +21 -1
  90. package/src/App/hooks/useMapProviderOverrides.test.js +51 -2
  91. package/src/App/hooks/useMarkersAPI.js +5 -3
  92. package/src/App/hooks/useModalPanelBehaviour.js +19 -2
  93. package/src/App/hooks/useModalPanelBehaviour.test.js +84 -60
  94. package/src/App/hooks/useVisibleGeometry.js +100 -0
  95. package/src/App/hooks/useVisibleGeometry.test.js +331 -0
  96. package/src/App/layout/Layout.jsx +5 -5
  97. package/src/App/layout/layout.module.scss +2 -4
  98. package/src/App/registry/panelRegistry.js +1 -10
  99. package/src/App/registry/panelRegistry.test.js +6 -11
  100. package/src/App/renderer/HtmlElementHost.jsx +12 -3
  101. package/src/App/renderer/HtmlElementHost.test.jsx +89 -0
  102. package/src/App/renderer/mapButtons.js +128 -28
  103. package/src/App/renderer/mapButtons.test.js +119 -19
  104. package/src/App/renderer/pluginWrapper.js +3 -2
  105. package/src/App/renderer/slots.js +1 -1
  106. package/src/App/store/AppProvider.jsx +1 -0
  107. package/src/App/store/MapProvider.jsx +18 -5
  108. package/src/App/store/MapProvider.test.jsx +56 -1
  109. package/src/App/store/appActionsMap.js +17 -9
  110. package/src/App/store/appActionsMap.test.js +33 -7
  111. package/src/App/store/appDispatchMiddleware.js +19 -0
  112. package/src/App/store/appDispatchMiddleware.test.js +56 -0
  113. package/src/App/store/mapActionsMap.js +4 -7
  114. package/src/InteractiveMap/InteractiveMap.js +18 -0
  115. package/src/InteractiveMap/InteractiveMap.test.js +12 -0
  116. package/src/config/appConfig.js +17 -15
  117. package/src/config/events.js +41 -4
  118. package/src/config/getInitialOpenPanels.js +2 -2
  119. package/src/config/getInitialOpenPanels.test.js +7 -7
  120. package/src/types.js +22 -11
  121. package/src/utils/getValueForStyle.js +1 -1
@@ -11,9 +11,10 @@ describe('actionsMap full coverage', () => {
11
11
 
12
12
  beforeEach(() => {
13
13
  const mockPanelConfig = {
14
- panel1: { desktop: { exclusive: true, modal: false, initiallyOpen: true }, mobile: { exclusive: true, modal: false } },
14
+ panel1: { desktop: { exclusive: true, modal: false, open: true }, mobile: { exclusive: true, modal: false } },
15
15
  panel2: { desktop: { exclusive: false, modal: true }, mobile: { exclusive: false, modal: true } },
16
- panel3: { desktop: { exclusive: false, modal: false }, mobile: { exclusive: false, modal: false } }
16
+ panel3: { desktop: { exclusive: false, modal: false }, mobile: { exclusive: false, modal: false } },
17
+ panel4: { desktop: { open: true, dismissible: false }, mobile: { open: true, dismissible: true } }
17
18
  }
18
19
 
19
20
  state = {
@@ -171,15 +172,15 @@ describe('actionsMap full coverage', () => {
171
172
  expect(result.panelConfig.panelX).toBeDefined()
172
173
  })
173
174
 
174
- test('ADD_PANEL adds panelConfig and opens initiallyOpen panel', () => {
175
- const payload = { id: 'panelY', config: { desktop: { initiallyOpen: true } } }
175
+ test('ADD_PANEL adds panelConfig and opens panel when open=true', () => {
176
+ const payload = { id: 'panelY', config: { desktop: { open: true } } }
176
177
  const result = actionsMap.ADD_PANEL(state, payload)
177
178
  expect(result.panelConfig.panelY).toBeDefined()
178
179
  expect(result.openPanels.panelY).toBeDefined()
179
180
  })
180
181
 
181
- test('ADD_PANEL does not open if initiallyOpen false', () => {
182
- const payload = { id: 'panelZ', config: { desktop: { initiallyOpen: false } } }
182
+ test('ADD_PANEL does not open if open=false', () => {
183
+ const payload = { id: 'panelZ', config: { desktop: { open: false } } }
183
184
  const result = actionsMap.ADD_PANEL(state, payload)
184
185
  expect(result.panelConfig.panelZ).toBeDefined()
185
186
  expect(result.openPanels.panelZ).toBeUndefined()
@@ -274,6 +275,31 @@ describe('actionsMap full coverage', () => {
274
275
  expect(result.isFullscreen).toBe(true)
275
276
  })
276
277
 
278
+ test('SET_BREAKPOINT restores non-dismissible open panel at new breakpoint', () => {
279
+ const tmp = { ...state, openPanels: {} }
280
+ const result = actionsMap.SET_BREAKPOINT(tmp, { breakpoint: 'desktop', behaviour: 'responsive', hybridWidth: null, maxMobileWidth: 640 })
281
+ expect(result.openPanels.panel4).toBeDefined()
282
+ })
283
+
284
+ test('SET_BREAKPOINT does not force-open a non-dismissible panel where it is dismissible', () => {
285
+ const tmp = { ...state, openPanels: {} }
286
+ const result = actionsMap.SET_BREAKPOINT(tmp, { breakpoint: 'mobile', behaviour: 'responsive', hybridWidth: null, maxMobileWidth: 640 })
287
+ expect(result.openPanels.panel4).toBeUndefined()
288
+ })
289
+
290
+ test('SET_BREAKPOINT preserves existing props when restoring a non-dismissible panel', () => {
291
+ const props = { myProp: 'value' }
292
+ const tmp = { ...state, openPanels: { panel4: { props } } }
293
+ const result = actionsMap.SET_BREAKPOINT(tmp, { breakpoint: 'desktop', behaviour: 'responsive', hybridWidth: null, maxMobileWidth: 640 })
294
+ expect(result.openPanels.panel4.props).toEqual(props)
295
+ })
296
+
297
+ test('SET_BREAKPOINT uses panelRegistry.getPanelConfig() when panelConfig missing', () => {
298
+ const tmp = { ...state, panelConfig: undefined, openPanels: {} }
299
+ const result = actionsMap.SET_BREAKPOINT(tmp, { breakpoint: 'desktop', behaviour: 'responsive', hybridWidth: null, maxMobileWidth: 640 })
300
+ expect(result.openPanels.panel4).toBeDefined()
301
+ })
302
+
277
303
  test('SET_HYBRID_FULLSCREEN updates isFullscreen', () => {
278
304
  const tmp = { ...state, isFullscreen: false }
279
305
  const result = actionsMap.SET_HYBRID_FULLSCREEN(tmp, true)
@@ -294,7 +320,7 @@ describe('actionsMap full coverage', () => {
294
320
 
295
321
  test('ADD_PANEL skips registry if panelRegistry missing', () => {
296
322
  const tmp = { ...state, panelRegistry: undefined }
297
- const payload = { id: 'panelY', config: { desktop: { initiallyOpen: true } } }
323
+ const payload = { id: 'panelY', config: { desktop: { open: true } } }
298
324
  const result = actionsMap.ADD_PANEL(tmp, payload)
299
325
  expect(result.panelConfig.panelY).toBeDefined()
300
326
  expect(result.openPanels.panelY).toBeDefined()
@@ -1,5 +1,7 @@
1
1
  // src/App/store/dispatchMiddleware.js
2
2
  import { EVENTS as events } from '../../config/events.js'
3
+ import { defaultPanelConfig } from '../../config/appConfig.js'
4
+ import { deepMerge } from '../../utils/deepMerge.js'
3
5
 
4
6
  /**
5
7
  * Determines which panels were implicitly closed when opening a new panel
@@ -78,4 +80,21 @@ export function handleActionSideEffects (action, previousState, panelConfig, eve
78
80
  eventBus.emit(events.APP_PANEL_OPENED, { panelId, props })
79
81
  })
80
82
  }
83
+
84
+ if (type === 'ADD_PANEL') {
85
+ const { id, config } = payload
86
+ const mergedConfig = deepMerge(defaultPanelConfig, config)
87
+ const bpConfig = mergedConfig[previousState.breakpoint]
88
+ if (bpConfig?.open) {
89
+ queueMicrotask(() => {
90
+ const slot = bpConfig.slot
91
+ const { visibleGeometry } = mergedConfig
92
+ const eventPayload = { panelId: id, slot }
93
+ if (visibleGeometry) {
94
+ eventPayload.visibleGeometry = visibleGeometry
95
+ }
96
+ eventBus.emit(events.APP_PANEL_OPENED, eventPayload)
97
+ })
98
+ }
99
+ }
81
100
  }
@@ -177,4 +177,60 @@ describe('appDispatchMiddleware', () => {
177
177
  )
178
178
  })
179
179
  })
180
+
181
+ describe('ADD_PANEL', () => {
182
+ it('emits APP_PANEL_OPENED with slot when panel opens by default', async () => {
183
+ run(
184
+ { type: 'ADD_PANEL', payload: { id: 'newPanel', config: {} } },
185
+ { breakpoint: 'desktop' }
186
+ )
187
+
188
+ await flushMicrotasks()
189
+
190
+ expect(eventBus.emit).toHaveBeenCalledWith(
191
+ events.APP_PANEL_OPENED,
192
+ { panelId: 'newPanel', slot: 'inset' }
193
+ )
194
+ })
195
+
196
+ it('emits APP_PANEL_OPENED with visibleGeometry when provided in config', async () => {
197
+ const visibleGeometry = { type: 'Feature', geometry: { type: 'Point', coordinates: [1, 2] }, properties: {} }
198
+ run(
199
+ { type: 'ADD_PANEL', payload: { id: 'geoPanel', config: { visibleGeometry } } },
200
+ { breakpoint: 'desktop' }
201
+ )
202
+
203
+ await flushMicrotasks()
204
+
205
+ expect(eventBus.emit).toHaveBeenCalledWith(
206
+ events.APP_PANEL_OPENED,
207
+ { panelId: 'geoPanel', slot: 'inset', visibleGeometry }
208
+ )
209
+ })
210
+
211
+ it('does not emit APP_PANEL_OPENED when breakpoint config sets open: false', async () => {
212
+ run(
213
+ { type: 'ADD_PANEL', payload: { id: 'hiddenPanel', config: { desktop: { open: false } } } },
214
+ { breakpoint: 'desktop' }
215
+ )
216
+
217
+ await flushMicrotasks()
218
+
219
+ expect(eventBus.emit).not.toHaveBeenCalled()
220
+ })
221
+
222
+ it('emits APP_PANEL_OPENED with slot for mobile breakpoint', async () => {
223
+ run(
224
+ { type: 'ADD_PANEL', payload: { id: 'mobilePanel', config: {} } },
225
+ { breakpoint: 'mobile' }
226
+ )
227
+
228
+ await flushMicrotasks()
229
+
230
+ expect(eventBus.emit).toHaveBeenCalledWith(
231
+ events.APP_PANEL_OPENED,
232
+ { panelId: 'mobilePanel', slot: 'bottom' }
233
+ )
234
+ })
235
+ })
180
236
  })
@@ -7,13 +7,10 @@ const mergePayload = (state, payload) => ({
7
7
  ...payload
8
8
  })
9
9
 
10
- const setMapReady = (state, mapProvider) => {
11
- return {
12
- ...state,
13
- isMapReady: true,
14
- mapProvider
15
- }
16
- }
10
+ const setMapReady = (state) => ({
11
+ ...state,
12
+ isMapReady: true
13
+ })
17
14
 
18
15
  const setMapStyle = (state, payload) => {
19
16
  return {
@@ -404,4 +404,22 @@ export default class InteractiveMap {
404
404
  addControl (id, config) {
405
405
  this.eventBus.emit(events.APP_ADD_CONTROL, { id, config })
406
406
  }
407
+
408
+ /**
409
+ * Fit the map view to a bounding box or GeoJSON geometry, respecting the safe zone padding.
410
+ *
411
+ * @param {[number, number, number, number] | object} target - Bounds as [west, south, east, north] or [minX, minY, maxX, maxY] depending on the crs, or a GeoJSON Feature, FeatureCollection, or geometry.
412
+ */
413
+ fitToBounds (target) {
414
+ this.eventBus.emit(events.MAP_FIT_TO_BOUNDS, target)
415
+ }
416
+
417
+ /**
418
+ * Set the map center and zoom, respecting the safe zone padding.
419
+ *
420
+ * @param {{ center?: [number, number], zoom?: number }} opts - View options.
421
+ */
422
+ setView (opts) {
423
+ this.eventBus.emit(events.MAP_SET_VIEW, opts)
424
+ }
407
425
  }
@@ -509,4 +509,16 @@ describe('InteractiveMap Public API Methods', () => {
509
509
  { id: 'btn-1', prop: 'disabled', value: true }
510
510
  )
511
511
  })
512
+
513
+ it('fitToBounds emits MAP_FIT_TO_BOUNDS with bbox', () => {
514
+ const bbox = [-0.489, 51.28, 0.236, 51.686]
515
+ map.fitToBounds(bbox)
516
+ expect(map.eventBus.emit).toHaveBeenCalledWith('map:fittobounds', bbox)
517
+ })
518
+
519
+ it('setView emits MAP_SET_VIEW with opts', () => {
520
+ const opts = { center: [-0.1276, 51.5074], zoom: 12 }
521
+ map.setView(opts)
522
+ expect(map.eventBus.emit).toHaveBeenCalledWith('map:setview', opts)
523
+ })
512
524
  })
@@ -2,8 +2,8 @@ import { KeyboardHelp } from '../App/components/KeyboardHelp/KeyboardHelp.jsx'
2
2
 
3
3
  const keyboardBasePanelSlots = {
4
4
  slot: 'middle',
5
- initiallyOpen: false,
6
- dismissable: true,
5
+ open: false,
6
+ dismissible: true,
7
7
  modal: true
8
8
  }
9
9
 
@@ -42,7 +42,7 @@ export const defaultAppConfig = {
42
42
  desktop: buttonSlots
43
43
  }, {
44
44
  id: 'zoomIn',
45
- group: 'zoom',
45
+ group: { name: 'zoom', label: 'Zoom controls', order: 0 },
46
46
  label: 'Zoom in',
47
47
  iconId: 'plus',
48
48
  onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomIn(appConfig.zoomDelta),
@@ -53,7 +53,7 @@ export const defaultAppConfig = {
53
53
  desktop: buttonSlots
54
54
  }, {
55
55
  id: 'zoomOut',
56
- group: 'zoom',
56
+ group: { name: 'zoom', label: 'Zoom controls', order: 0 },
57
57
  label: 'Zoom out',
58
58
  iconId: 'minus',
59
59
  onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomOut(appConfig.zoomDelta),
@@ -99,7 +99,7 @@ export const defaultAppConfig = {
99
99
  // Used by addButton
100
100
  const defaultButtonSlots = {
101
101
  slot: 'right-top',
102
- showLabel: false
102
+ showLabel: true
103
103
  }
104
104
 
105
105
  export const defaultButtonConfig = {
@@ -111,25 +111,27 @@ export const defaultButtonConfig = {
111
111
 
112
112
  // Used by addPanel
113
113
  export const defaultPanelConfig = {
114
- showLabel: true,
115
114
  label: 'Panel',
116
115
  mobile: {
117
116
  slot: 'bottom',
118
- initiallyOpen: true,
119
- dismissable: true,
120
- modal: false
117
+ open: true,
118
+ dismissible: true,
119
+ modal: false,
120
+ showLabel: true
121
121
  },
122
122
  tablet: {
123
123
  slot: 'inset',
124
- initiallyOpen: true,
125
- dismissable: true,
126
- modal: false
124
+ open: true,
125
+ dismissible: true,
126
+ modal: false,
127
+ showLabel: true
127
128
  },
128
129
  desktop: {
129
130
  slot: 'inset',
130
- initiallyOpen: true,
131
- dismissable: true,
132
- modal: false
131
+ open: true,
132
+ dismissible: true,
133
+ modal: false,
134
+ showLabel: true
133
135
  },
134
136
  render: null,
135
137
  html: null
@@ -87,12 +87,16 @@ export const EVENTS = {
87
87
  // Map commands (internal / plugin authors)
88
88
  // ============================================
89
89
 
90
- /** @internal Set map style. Payload: styleId */
90
+ /** @internal Set map style. Payload: MapStyleConfig */
91
91
  MAP_SET_STYLE: 'map:setstyle',
92
92
  /** @internal Set map size. Payload: { width, height } */
93
93
  MAP_SET_SIZE: 'map:setsize',
94
94
  /** @internal Set pixel ratio. Payload: pixelRatio */
95
95
  MAP_SET_PIXEL_RATIO: 'map:setpixelratio',
96
+ /** @internal Fit the map to a bounding box. Payload: [west, south, east, north] */
97
+ MAP_FIT_TO_BOUNDS: 'map:fittobounds',
98
+ /** @internal Set the map center and zoom. Payload: { center: [number, number], zoom?: number } */
99
+ MAP_SET_VIEW: 'map:setview',
96
100
 
97
101
  // ============================================
98
102
  // Map responses (advanced / subscribe)
@@ -101,15 +105,48 @@ export const EVENTS = {
101
105
  /** @internal Emitted when map styles are initialized. */
102
106
  MAP_INIT_MAP_STYLES: 'map:initmapstyles',
103
107
 
104
- /** Emitted when the map style changes. Payload: { styleId: string } */
108
+ /**
109
+ * Emitted when the map style has finished loading.
110
+ * Payload: `{ mapStyleId: string }`
111
+ *
112
+ * @example
113
+ * map.on(EVENTS.MAP_STYLE_CHANGE, ({ mapStyleId }) => {
114
+ * console.log('Style changed to', mapStyleId)
115
+ * })
116
+ */
105
117
  MAP_STYLE_CHANGE: 'map:stylechange',
106
118
 
107
- /** Emitted when the map style has fully loaded. */
119
+ /** Emitted when the map has fully loaded. */
108
120
  MAP_LOADED: 'map:loaded',
109
121
 
110
- /** Emitted when the map is ready for interaction. Payload: { map } */
122
+ /**
123
+ * Emitted when the map is ready for interaction and initial app state is settled.
124
+ *
125
+ * Payload:
126
+ * - `map` — the underlying map instance
127
+ * - `view` — the map view (ESRI SDK only)
128
+ * - `crs` — coordinate reference system string (e.g. `'EPSG:4326'`)
129
+ * - `mapStyleId` — the ID of the active map style (e.g. `'outdoor'`, `'dark'`)
130
+ * - `mapSize` — the active map size string (e.g. `'small'`, `'medium'`, `'large'`)
131
+ *
132
+ * @example
133
+ * map.on(EVENTS.MAP_READY, ({ map, mapStyleId, mapSize }) => {
134
+ * console.log('Map ready, style:', mapStyleId, 'size:', mapSize)
135
+ * })
136
+ */
111
137
  MAP_READY: 'map:ready',
112
138
 
139
+ /**
140
+ * Emitted when the map size changes.
141
+ * Payload: `{ mapSize: string }`
142
+ *
143
+ * @example
144
+ * map.on(EVENTS.MAP_SIZE_CHANGE, ({ mapSize }) => {
145
+ * console.log('Map size changed to', mapSize)
146
+ * })
147
+ */
148
+ MAP_SIZE_CHANGE: 'map:sizechange',
149
+
113
150
  /** Emitted once after the map first becomes idle following initial load. */
114
151
  MAP_FIRST_IDLE: 'map:firstidle',
115
152
 
@@ -5,9 +5,9 @@ export function getInitialOpenPanels (panelConfig, breakpoint, prevOpenPanels =
5
5
  const configPanel = panelConfig[panelId]
6
6
  const bpConfig = configPanel[breakpoint]
7
7
 
8
- const isInitiallyOpen = bpConfig?.initiallyOpen ?? false
8
+ const isOpen = bpConfig?.open ?? false
9
9
 
10
- if (isInitiallyOpen) {
10
+ if (isOpen) {
11
11
  // Preserve any props that were already set in state
12
12
  openPanels[panelId] = prevOpenPanels[panelId] || { props: {} }
13
13
  }
@@ -8,17 +8,17 @@ describe('getInitialOpenPanels', () => {
8
8
  expect(getInitialOpenPanels({}, breakpoint)).toEqual({})
9
9
  })
10
10
 
11
- it('includes panels with initiallyOpen = true and no prev state', () => {
11
+ it('includes panels with open = true and no prev state', () => {
12
12
  const config = {
13
- PanelA: { desktop: { initiallyOpen: true } }
13
+ PanelA: { desktop: { open: true } }
14
14
  }
15
15
  const result = getInitialOpenPanels(config, breakpoint)
16
16
  expect(result).toEqual({ PanelA: { props: {} } })
17
17
  })
18
18
 
19
- it('skips panels with initiallyOpen = false', () => {
19
+ it('skips panels with open = false', () => {
20
20
  const config = {
21
- PanelA: { desktop: { initiallyOpen: false } }
21
+ PanelA: { desktop: { open: false } }
22
22
  }
23
23
  const result = getInitialOpenPanels(config, breakpoint)
24
24
  expect(result).toEqual({})
@@ -34,7 +34,7 @@ describe('getInitialOpenPanels', () => {
34
34
 
35
35
  it('preserves prevOpenPanels state if exists', () => {
36
36
  const config = {
37
- PanelA: { desktop: { initiallyOpen: true } }
37
+ PanelA: { desktop: { open: true } }
38
38
  }
39
39
  const prevOpenPanels = {
40
40
  PanelA: { props: { some: 'value' } }
@@ -45,8 +45,8 @@ describe('getInitialOpenPanels', () => {
45
45
 
46
46
  it('uses empty props object if no prevOpenPanels entry exists', () => {
47
47
  const config = {
48
- PanelA: { desktop: { initiallyOpen: true } },
49
- PanelB: { desktop: { initiallyOpen: true } }
48
+ PanelA: { desktop: { open: true } },
49
+ PanelB: { desktop: { open: true } }
50
50
  }
51
51
  const prevOpenPanels = {
52
52
  PanelA: { props: { existing: true } }
package/src/types.js CHANGED
@@ -16,8 +16,8 @@
16
16
  * @property {number} [order]
17
17
  * The order the button appears within its slot.
18
18
  *
19
- * @property {boolean} [showLabel]
20
- * Whether to a show label, if false then a tooltip is generated from the the label.
19
+ * @property {boolean} [showLabel=true]
20
+ * Whether to show a label. If false, a tooltip is generated from the label instead. Defaults to true.
21
21
  *
22
22
  * @property {string} slot
23
23
  * The slot that the button should appear in at this breakpoint.
@@ -37,14 +37,19 @@
37
37
  *
38
38
  * @typedef {Object} PanelBreakpointConfig
39
39
  *
40
- * @property {boolean} [dismissable]
41
- * Whether panel can be dismissed.
40
+ * @property {boolean} [dismissible]
41
+ * Whether panel can be dismissed. When `false` and `open` is `true`, the panel is always visible at this
42
+ * breakpoint and any associated panel-toggle button is automatically suppressed.
42
43
  *
43
44
  * @property {boolean} [exclusive]
44
45
  * Whether panel is exclusive. An exclusive panel will hide other panels when it is visible.
45
46
  *
46
- * @property {boolean} [initiallyOpen]
47
- * Whether panel is initially open.
47
+ * @property {boolean} [open]
48
+ * Whether the panel is open. When `true` and combined with `dismissible: false`, the panel is always visible at this
49
+ * breakpoint and will be restored automatically when the breakpoint is entered.
50
+ *
51
+ * @property {boolean} [showLabel]
52
+ * Whether to show the panel heading. Defaults to true. The heading is visually hidden if false.
48
53
  *
49
54
  * @property {boolean} [modal]
50
55
  * Whether panel is modal.
@@ -198,8 +203,8 @@
198
203
  * @property {(offset: [number, number]) => void} panBy
199
204
  * Pan map by pixel offset [x, y]. Positive x pans right, positive y pans down.
200
205
  *
201
- * @property {(bounds: [number, number, number, number]) => void} fitToBounds
202
- * Fit map view to the specified bounds [west, south, east, north] or [minX, minY, maxX, maxY] depending on the crs of the map provider.
206
+ * @property {(bounds: [number, number, number, number] | object) => void} fitToBounds
207
+ * Fit map view to the specified bounds [west, south, east, north] or [minX, minY, maxX, maxY] depending on the crs, or a GeoJSON Feature, FeatureCollection, or geometry.
203
208
  *
204
209
  * @property {(padding: { top?: number, bottom?: number, left?: number, right?: number }) => void} setPadding
205
210
  * Set map padding as pixel insets from the top, bottom, left and right edges of the map.
@@ -242,6 +247,10 @@
242
247
  *
243
248
  * @property {() => void} [clearHighlightedLabel]
244
249
  * @experimental Clear any highlighted label.
250
+ *
251
+ * @property {(geojson: object, panelRect: DOMRect) => boolean} [isGeometryObscured]
252
+ * Returns true if the geometry's screen bounding box overlaps the given panel element rectangle.
253
+ * Used internally by useVisibleGeometry to decide whether to pan/zoom when a panel opens.
245
254
  */
246
255
 
247
256
  /**
@@ -366,11 +375,13 @@
366
375
  * @property {ComponentType} [render]
367
376
  * Render component.
368
377
  *
369
- * @property {boolean} [showLabel]
370
- * Whether to show the panel heading. The panel heading is visually hidden if false.
371
- *
372
378
  * @property {PanelBreakpointConfig} tablet
373
379
  * Tablet breakpoint configuration.
380
+ *
381
+ * @property {object} [visibleGeometry]
382
+ * GeoJSON Feature, FeatureCollection, or geometry to keep visible when this panel opens.
383
+ * If any part of the geometry's bounding box is obscured by the safe zone after the panel opens,
384
+ * the map automatically adjusts: Point or MultiPoint geometry routes to setView(), all other types to fitToBounds().
374
385
  */
375
386
 
376
387
  /**
@@ -2,7 +2,7 @@
2
2
  * Returns the appropriate color for the given mapStyleId.
3
3
  * Supports:
4
4
  * - Simple string colors (#fff, rgba(...))
5
- * - Object maps of styleId → color
5
+ * - Object maps of mapStyleId → color
6
6
  *
7
7
  * @param {string|object} colors - Color string or style-mapped color object
8
8
  * @param {string} mapStyleId - Current style/theme identifier