@defra/interactive-map 0.0.6-alpha → 0.0.8-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 (83) hide show
  1. package/assets/templates/map.njk +2 -2
  2. package/dist/css/index.css +1 -1
  3. package/dist/esm/index.js +1 -1
  4. package/dist/umd/im-core.js +1 -1
  5. package/docs/api.md +10 -5
  6. package/package.json +56 -4
  7. package/plugins/beta/datasets/dist/esm/index.js +1 -1
  8. package/plugins/beta/draw-es/dist/esm/index.js +2 -0
  9. package/plugins/beta/draw-ml/dist/css/index.css +1 -1
  10. package/plugins/beta/draw-ml/dist/esm/index.js +1 -1
  11. package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
  12. package/plugins/beta/draw-ml/src/DrawInit.jsx +15 -0
  13. package/plugins/beta/draw-ml/src/api/deleteFeature.js +4 -4
  14. package/plugins/beta/draw-ml/src/api/editFeature.js +10 -5
  15. package/plugins/beta/draw-ml/src/draw.scss +1 -0
  16. package/plugins/beta/draw-ml/src/events.js +2 -0
  17. package/plugins/beta/draw-ml/src/manifest.js +18 -43
  18. package/plugins/beta/draw-ml/src/modes/createDrawMode.js +35 -1
  19. package/plugins/beta/draw-ml/src/modes/editVertexMode.js +17 -1
  20. package/plugins/beta/frame/dist/esm/index.js +1 -1
  21. package/plugins/beta/map-styles/dist/esm/index.js +1 -1
  22. package/plugins/beta/scale-bar/dist/esm/index.js +1 -1
  23. package/plugins/beta/use-location/dist/esm/index.js +1 -1
  24. package/plugins/interact/dist/css/index.css +1 -1
  25. package/plugins/interact/dist/esm/index.js +1 -1
  26. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  27. package/plugins/interact/src/defaults.js +1 -0
  28. package/plugins/interact/src/events.js +6 -1
  29. package/plugins/interact/src/events.test.js +19 -5
  30. package/plugins/interact/src/hooks/useInteractionHandlers.js +7 -2
  31. package/plugins/interact/src/hooks/useInteractionHandlers.test.js +28 -0
  32. package/plugins/interact/src/interact.scss +1 -0
  33. package/plugins/interact/src/manifest.js +22 -16
  34. package/plugins/interact/src/manifest.test.js +1 -1
  35. package/plugins/interact/src/reducer.js +1 -0
  36. package/plugins/interact/src/reducer.test.js +1 -0
  37. package/plugins/search/dist/esm/index.js +1 -1
  38. package/providers/beta/esri/dist/css/index.css +1 -0
  39. package/providers/beta/esri/dist/esm/index.js +2 -0
  40. package/providers/beta/esri/src/esriProvider.js +12 -0
  41. package/providers/beta/esri/src/mapEvents.js +2 -2
  42. package/providers/beta/open-names/dist/esm/index.js +1 -1
  43. package/providers/maplibre/dist/esm/index.js +1 -1
  44. package/providers/maplibre/dist/esm/index.js.LICENSE.txt +5 -0
  45. package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
  46. package/providers/maplibre/src/maplibreProvider.js +11 -1
  47. package/providers/maplibre/src/utils/highlightFeatures.js +6 -2
  48. package/src/App/components/Actions/Actions.jsx +2 -1
  49. package/src/App/components/Actions/Actions.module.scss +14 -0
  50. package/src/App/components/Actions/Actions.test.jsx +2 -2
  51. package/src/App/components/MapButton/MapButton.module.scss +19 -6
  52. package/src/App/components/PopupMenu/PopupMenu.jsx +69 -14
  53. package/src/App/components/PopupMenu/PopupMenu.module.scss +70 -7
  54. package/src/App/components/PopupMenu/PopupMenu.test.jsx +102 -16
  55. package/src/App/hooks/useButtonStateEvaluator.js +2 -2
  56. package/src/App/hooks/useInterfaceAPI.js +26 -32
  57. package/src/App/registry/pluginRegistry.js +2 -2
  58. package/src/App/renderer/mapButtons.js +6 -1
  59. package/src/App/renderer/mapButtons.test.js +20 -0
  60. package/src/App/store/MapProvider.jsx +2 -2
  61. package/src/App/store/mapActionsMap.js +3 -2
  62. package/src/App/store/mapReducer.js +1 -0
  63. package/src/scss/settings/_dimensions.scss +1 -0
  64. package/webpack.esm.mjs +21 -11
  65. package/webpack.umd.mjs +0 -1
  66. package/dist/esm/im-core.js +0 -2
  67. package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +0 -2
  68. package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +0 -1
  69. package/plugins/beta/frame/dist/esm/im-frame-plugin.js +0 -1
  70. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +0 -1
  71. package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +0 -1
  72. package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +0 -1
  73. package/plugins/interact/dist/esm/im-interact-plugin.js +0 -1
  74. package/plugins/search/dist/esm/im-search-plugin.js +0 -2
  75. package/plugins/search/dist/esm/im-search-plugin.js.LICENSE.txt +0 -1
  76. package/providers/beta/open-names/dist/esm/im-reverse-geocode.js +0 -2
  77. package/providers/beta/open-names/dist/esm/im-reverse-geocode.js.LICENSE.txt +0 -1
  78. package/providers/maplibre/dist/esm/im-maplibre-framework.js +0 -2
  79. package/providers/maplibre/dist/esm/im-maplibre-framework.js.LICENSE.txt +0 -4
  80. package/providers/maplibre/dist/esm/im-maplibre-provider.js +0 -2
  81. package/providers/maplibre/dist/esm/im-maplibre-provider.js.LICENSE.txt +0 -1
  82. /package/{dist/esm/im-core.js.LICENSE.txt → plugins/beta/draw-es/dist/esm/index.js.LICENSE.txt} +0 -0
  83. /package/{plugins/beta/datasets/dist/esm/im-datasets-plugin.js.LICENSE.txt → providers/beta/esri/dist/esm/index.js.LICENSE.txt} +0 -0
@@ -91,8 +91,8 @@ describe('PopupMenu', () => {
91
91
  Object.defineProperty(ev, 'target', { value: target, enumerable: true })
92
92
  document.dispatchEvent(ev)
93
93
  }
94
- const expectSelected = (text) => expect(screen.getByText(text).parentElement).toHaveClass('fm-c-popup-menu__item--selected')
95
- const expectNotSelected = (text) => expect(screen.getByText(text).parentElement).not.toHaveClass('fm-c-popup-menu__item--selected')
94
+ const expectSelected = (text) => expect(screen.getByText(text).parentElement).toHaveClass('im-c-popup-menu__item--selected')
95
+ const expectNotSelected = (text) => expect(screen.getByText(text).parentElement).not.toHaveClass('im-c-popup-menu__item--selected')
96
96
 
97
97
  it('renders items with labels and icons', () => {
98
98
  renderMenu()
@@ -104,7 +104,7 @@ describe('PopupMenu', () => {
104
104
  it('applies selected class to active index', () => {
105
105
  renderMenu({ startIndex: 1 })
106
106
  expect(screen.getByText('Item 2').parentElement).toHaveClass(
107
- 'fm-c-popup-menu__item--selected'
107
+ 'im-c-popup-menu__item--selected'
108
108
  )
109
109
  })
110
110
 
@@ -112,15 +112,15 @@ describe('PopupMenu', () => {
112
112
  renderMenu()
113
113
  const ul = screen.getByRole('menu')
114
114
  fireEvent.keyDown(ul, { key: 'ArrowDown' })
115
- expect(screen.getByText('Item 2').parentElement).toHaveClass('fm-c-popup-menu__item--selected')
115
+ expect(screen.getByText('Item 2').parentElement).toHaveClass('im-c-popup-menu__item--selected')
116
116
  fireEvent.keyDown(ul, { key: 'ArrowDown' })
117
- expect(screen.getByText('Item 1').parentElement).toHaveClass('fm-c-popup-menu__item--selected')
117
+ expect(screen.getByText('Item 1').parentElement).toHaveClass('im-c-popup-menu__item--selected')
118
118
  fireEvent.keyDown(ul, { key: 'ArrowUp' })
119
- expect(screen.getByText('Item 2').parentElement).toHaveClass('fm-c-popup-menu__item--selected')
119
+ expect(screen.getByText('Item 2').parentElement).toHaveClass('im-c-popup-menu__item--selected')
120
120
  fireEvent.keyDown(ul, { key: 'Home' })
121
- expect(screen.getByText('Item 1').parentElement).toHaveClass('fm-c-popup-menu__item--selected')
121
+ expect(screen.getByText('Item 1').parentElement).toHaveClass('im-c-popup-menu__item--selected')
122
122
  fireEvent.keyDown(ul, { key: 'End' })
123
- expect(screen.getByText('Item 2').parentElement).toHaveClass('fm-c-popup-menu__item--selected')
123
+ expect(screen.getByText('Item 2').parentElement).toHaveClass('im-c-popup-menu__item--selected')
124
124
  })
125
125
 
126
126
  it('handles Enter key to call onClick and close menu', () => {
@@ -159,6 +159,32 @@ describe('PopupMenu', () => {
159
159
  expect(mockSetIsOpen).toHaveBeenCalled()
160
160
  })
161
161
 
162
+ it('does not call onClick or close menu when clicking a disabled item', () => {
163
+ mockUseApp.disabledButtons = new Set(['item1'])
164
+ renderMenu()
165
+ fireEvent.click(screen.getByText('Item 1'))
166
+ expect(items[0].onClick).not.toHaveBeenCalled()
167
+ expect(mockSetIsOpen).not.toHaveBeenCalled()
168
+ })
169
+
170
+ it('Enter calls buttonConfig.onClick with evaluateProp context', () => {
171
+ const mockOnClick = jest.fn()
172
+ mockUseApp.buttonConfig = { item1: { onClick: mockOnClick } }
173
+ renderMenu({ startIndex: 0 })
174
+ fireEvent.keyDown(screen.getByRole('menu'), { key: 'Enter' })
175
+ expect(mockOnClick).toHaveBeenCalled()
176
+ expect(mockEvaluateProp).toHaveBeenCalled()
177
+ expect(mockSetIsOpen).toHaveBeenCalled()
178
+ })
179
+
180
+ it('Enter on a disabled item closes menu but does not call onClick', () => {
181
+ mockUseApp.disabledButtons = new Set(['item1'])
182
+ renderMenu({ startIndex: 0 })
183
+ fireEvent.keyDown(screen.getByRole('menu'), { key: 'Enter' })
184
+ expect(items[0].onClick).not.toHaveBeenCalled()
185
+ expect(mockSetIsOpen).toHaveBeenCalled()
186
+ })
187
+
162
188
  it('does nothing if click is inside menu', () => {
163
189
  renderMenu()
164
190
  const element = document.createElement('div')
@@ -215,22 +241,22 @@ describe('PopupMenu', () => {
215
241
  renderMenu({ startIndex: -1 })
216
242
  const ul = screen.getByRole('menu')
217
243
  fireEvent.keyDown(ul, { key: 'ArrowDown' })
218
- expect(screen.getByText('Item 1').parentElement).toHaveClass('fm-c-popup-menu__item--selected')
244
+ expect(screen.getByText('Item 1').parentElement).toHaveClass('im-c-popup-menu__item--selected')
219
245
  fireEvent.keyDown(ul, { key: 'ArrowUp' })
220
- expect(screen.getByText('Item 2').parentElement).toHaveClass('fm-c-popup-menu__item--selected')
246
+ expect(screen.getByText('Item 2').parentElement).toHaveClass('im-c-popup-menu__item--selected')
221
247
  })
222
248
 
223
249
  it('ArrowUp from no selection picks last visible', () => {
224
250
  renderMenu({ startIndex: -1 })
225
251
  const ul = screen.getByRole('menu')
226
252
  fireEvent.keyDown(ul, { key: 'ArrowUp' })
227
- expect(screen.getByText('Item 2').parentElement).toHaveClass('fm-c-popup-menu__item--selected')
253
+ expect(screen.getByText('Item 2').parentElement).toHaveClass('im-c-popup-menu__item--selected')
228
254
  })
229
255
 
230
256
  it('initializes with no selection when neither startIndex nor startPos provided', () => {
231
257
  renderMenu({ startIndex: undefined, startPos: undefined })
232
- expect(screen.getByText('Item 1').parentElement).not.toHaveClass('fm-c-popup-menu__item--selected')
233
- expect(screen.getByText('Item 2').parentElement).not.toHaveClass('fm-c-popup-menu__item--selected')
258
+ expect(screen.getByText('Item 1').parentElement).not.toHaveClass('im-c-popup-menu__item--selected')
259
+ expect(screen.getByText('Item 2').parentElement).not.toHaveClass('im-c-popup-menu__item--selected')
234
260
  })
235
261
 
236
262
  it('does nothing when navigating and no visible items', () => {
@@ -238,8 +264,8 @@ describe('PopupMenu', () => {
238
264
  renderMenu({ startIndex: -1 })
239
265
  const ul = screen.getByRole('menu')
240
266
  fireEvent.keyDown(ul, { key: 'ArrowDown' })
241
- expect(screen.getByText('Item 1').parentElement).not.toHaveClass('fm-c-popup-menu__item--selected')
242
- expect(screen.getByText('Item 2').parentElement).not.toHaveClass('fm-c-popup-menu__item--selected')
267
+ expect(screen.getByText('Item 1').parentElement).not.toHaveClass('im-c-popup-menu__item--selected')
268
+ expect(screen.getByText('Item 2').parentElement).not.toHaveClass('im-c-popup-menu__item--selected')
243
269
  })
244
270
 
245
271
  it('Enter with no selection closes but does not call any item onClick', () => {
@@ -291,7 +317,7 @@ describe('PopupMenu', () => {
291
317
  const item1 = screen.getByText('Item 1').parentElement
292
318
  const item2 = screen.getByText('Item 2').parentElement
293
319
  expect(item1).toHaveStyle('display: none')
294
- expect(item2).toHaveAttribute('aria-pressed', 'true')
320
+ expect(item2).toHaveAttribute('aria-checked', 'true')
295
321
  })
296
322
 
297
323
  it('ignores unrelated keys (covers Enter false branch)', () => {
@@ -301,4 +327,64 @@ describe('PopupMenu', () => {
301
327
  expect(items[1].onClick).not.toHaveBeenCalled()
302
328
  expect(mockSetIsOpen).not.toHaveBeenCalled()
303
329
  })
330
+
331
+ it('activateItem does nothing when item has no onClick and no buttonConfig.onClick (line 123 false branch)', () => {
332
+ const noHandlerItems = [{ id: 'noHandler', label: 'No Handler' }]
333
+ render(
334
+ <PopupMenu
335
+ pluginId='plugin1'
336
+ instigatorId='instigator'
337
+ startIndex={0}
338
+ menuRef={menuRef}
339
+ items={noHandlerItems}
340
+ setIsOpen={mockSetIsOpen}
341
+ />
342
+ )
343
+ fireEvent.keyDown(screen.getByRole('menu'), { key: 'Enter' })
344
+ expect(mockSetIsOpen).toHaveBeenCalledWith(false)
345
+ })
346
+
347
+ it('activateItem skips synthetic click when element is not found in DOM (line 132 false branch)', () => {
348
+ const getByIdSpy = jest.spyOn(document, 'getElementById').mockReturnValue(null)
349
+ renderMenu({ startIndex: 0 })
350
+ fireEvent.keyDown(screen.getByRole('menu'), { key: 'Enter' })
351
+ expect(mockSetIsOpen).toHaveBeenCalledWith(false)
352
+ getByIdSpy.mockRestore()
353
+ })
354
+
355
+ describe('Space key', () => {
356
+ it('Space on a menuitem calls onClick and closes menu', () => {
357
+ renderMenu({ startIndex: 0 })
358
+ const ul = screen.getByRole('menu')
359
+ fireEvent.keyDown(ul, { key: ' ' })
360
+ expect(items[0].onClick).toHaveBeenCalled()
361
+ expect(mockSetIsOpen).toHaveBeenCalledWith(false)
362
+ })
363
+
364
+ it('Space on a menuitemcheckbox calls onClick but does not close menu', () => {
365
+ renderMenu({ startIndex: 1 })
366
+ const ul = screen.getByRole('menu')
367
+ fireEvent.keyDown(ul, { key: ' ' })
368
+ expect(items[1].onClick).toHaveBeenCalled()
369
+ expect(mockSetIsOpen).not.toHaveBeenCalled()
370
+ })
371
+
372
+ it('Space on a disabled item does nothing', () => {
373
+ mockUseApp.disabledButtons = new Set(['item1'])
374
+ renderMenu({ startIndex: 0 })
375
+ const ul = screen.getByRole('menu')
376
+ fireEvent.keyDown(ul, { key: ' ' })
377
+ expect(items[0].onClick).not.toHaveBeenCalled()
378
+ expect(mockSetIsOpen).not.toHaveBeenCalled()
379
+ })
380
+
381
+ it('Space with no selection does nothing', () => {
382
+ renderMenu({ startIndex: -1 })
383
+ const ul = screen.getByRole('menu')
384
+ fireEvent.keyDown(ul, { key: ' ' })
385
+ expect(items[0].onClick).not.toHaveBeenCalled()
386
+ expect(items[1].onClick).not.toHaveBeenCalled()
387
+ expect(mockSetIsOpen).not.toHaveBeenCalled()
388
+ })
389
+ })
304
390
  })
@@ -1,5 +1,5 @@
1
1
  // src/core/hooks/useButtonStateEvaluator.js
2
- import { useEffect, useContext } from 'react'
2
+ import { useLayoutEffect, useContext } from 'react'
3
3
  import { useApp } from '../store/appContext.js'
4
4
  import { useConfig } from '../store/configContext.js'
5
5
  import { PluginContext } from '../store/PluginProvider.jsx'
@@ -55,7 +55,7 @@ export function useButtonStateEvaluator (evaluateProp) {
55
55
  const { pluginRegistry } = useConfig()
56
56
  const pluginContext = useContext(PluginContext)
57
57
 
58
- useEffect(() => {
58
+ useLayoutEffect(() => {
59
59
  if (!appState?.dispatch || !pluginContext) {
60
60
  return
61
61
  }
@@ -1,5 +1,5 @@
1
1
  // src/hooks/usePanels.js
2
- import { useEffect } from 'react'
2
+ import { useEffect, useRef } from 'react'
3
3
  import { EVENTS as events } from '../../config/events.js'
4
4
  import { useApp } from '../store/appContext.js'
5
5
  import { useService } from '../store/serviceContext.js'
@@ -8,38 +8,45 @@ export const useInterfaceAPI = () => {
8
8
  const { dispatch, hiddenButtons, disabledButtons, pressedButtons, expandedButtons } = useApp()
9
9
  const { eventBus } = useService()
10
10
 
11
+ // Refs so event handlers always use the latest dispatch/state without re-subscribing
12
+ const dispatchRef = useRef(dispatch)
13
+ const stateRef = useRef({ hiddenButtons, disabledButtons, pressedButtons, expandedButtons })
14
+ dispatchRef.current = dispatch
15
+ stateRef.current = { hiddenButtons, disabledButtons, pressedButtons, expandedButtons }
16
+
11
17
  useEffect(() => {
12
18
  const handleAddButton = ({ id, config }) => {
13
19
  // Add the button
14
- dispatch({ type: 'ADD_BUTTON', payload: { id, config } })
15
- // Add all optional menu items as individual buttons
20
+ dispatchRef.current({ type: 'ADD_BUTTON', payload: { id, config } })
21
+ // Add all optional menu items as individual buttons (isMenuItem prevents slot rendering)
16
22
  if (Array.isArray(config.menuItems)) {
17
23
  config.menuItems.forEach(item => {
18
- dispatch({ type: 'ADD_BUTTON', payload: { id: item.id, config: item } })
24
+ dispatchRef.current({ type: 'ADD_BUTTON', payload: { id: item.id, config: { ...item, isMenuItem: true } } })
19
25
  })
20
26
  }
21
27
  }
22
28
 
23
29
  const handleToggleButtonState = ({ id, prop, value }) => {
30
+ const { hiddenButtons: hidden, disabledButtons: disabled, pressedButtons: pressed, expandedButtons: expanded } = stateRef.current
24
31
  switch (prop) {
25
32
  case 'hidden': {
26
- const isHidden = typeof value === 'boolean' ? value : !hiddenButtons.has(id)
27
- dispatch({ type: 'TOGGLE_BUTTON_HIDDEN', payload: { id, isHidden } })
33
+ const isHidden = typeof value === 'boolean' ? value : !hidden.has(id)
34
+ dispatchRef.current({ type: 'TOGGLE_BUTTON_HIDDEN', payload: { id, isHidden } })
28
35
  break
29
36
  }
30
37
  case 'disabled': {
31
- const isDisabled = typeof value === 'boolean' ? value : !disabledButtons.has(id)
32
- dispatch({ type: 'TOGGLE_BUTTON_DISABLED', payload: { id, isDisabled } })
38
+ const isDisabled = typeof value === 'boolean' ? value : !disabled.has(id)
39
+ dispatchRef.current({ type: 'TOGGLE_BUTTON_DISABLED', payload: { id, isDisabled } })
33
40
  break
34
41
  }
35
42
  case 'pressed': {
36
- const isPressed = typeof value === 'boolean' ? value : !pressedButtons.has(id)
37
- dispatch({ type: 'TOGGLE_BUTTON_PRESSED', payload: { id, isPressed } })
43
+ const isPressed = typeof value === 'boolean' ? value : !pressed.has(id)
44
+ dispatchRef.current({ type: 'TOGGLE_BUTTON_PRESSED', payload: { id, isPressed } })
38
45
  break
39
46
  }
40
47
  case 'expanded': {
41
- const isExpanded = typeof value === 'boolean' ? value : !expandedButtons.has(id)
42
- dispatch({ type: 'TOGGLE_BUTTON_EXPANDED', payload: { id, isExpanded } })
48
+ const isExpanded = typeof value === 'boolean' ? value : !expanded.has(id)
49
+ dispatchRef.current({ type: 'TOGGLE_BUTTON_EXPANDED', payload: { id, isExpanded } })
43
50
  break
44
51
  }
45
52
  default:
@@ -47,25 +54,11 @@ export const useInterfaceAPI = () => {
47
54
  }
48
55
  }
49
56
 
50
- const handleAddPanel = ({ id, config }) => {
51
- dispatch({ type: 'ADD_PANEL', payload: { id, config } })
52
- }
53
-
54
- const handleRemovePanel = (id) => {
55
- dispatch({ type: 'REMOVE_PANEL', payload: id })
56
- }
57
-
58
- const handleShowPanel = (id) => {
59
- dispatch({ type: 'OPEN_PANEL', payload: { panelId: id } })
60
- }
61
-
62
- const handleHidePanel = (id) => {
63
- dispatch({ type: 'CLOSE_PANEL', payload: id })
64
- }
65
-
66
- const handleAddControl = ({ id, config }) => {
67
- dispatch({ type: 'ADD_CONTROL', payload: { id, config } })
68
- }
57
+ const handleAddPanel = ({ id, config }) => dispatchRef.current({ type: 'ADD_PANEL', payload: { id, config } })
58
+ const handleRemovePanel = (id) => dispatchRef.current({ type: 'REMOVE_PANEL', payload: id })
59
+ const handleShowPanel = (id) => dispatchRef.current({ type: 'OPEN_PANEL', payload: { panelId: id } })
60
+ const handleHidePanel = (id) => dispatchRef.current({ type: 'CLOSE_PANEL', payload: id })
61
+ const handleAddControl = ({ id, config }) => dispatchRef.current({ type: 'ADD_CONTROL', payload: { id, config } })
69
62
 
70
63
  eventBus.on(events.APP_ADD_BUTTON, handleAddButton)
71
64
  eventBus.on(events.APP_TOGGLE_BUTTON_STATE, handleToggleButtonState)
@@ -77,11 +70,12 @@ export const useInterfaceAPI = () => {
77
70
 
78
71
  return () => {
79
72
  eventBus.off(events.APP_ADD_BUTTON, handleAddButton)
73
+ eventBus.off(events.APP_TOGGLE_BUTTON_STATE, handleToggleButtonState)
80
74
  eventBus.off(events.APP_ADD_PANEL, handleAddPanel)
81
75
  eventBus.off(events.APP_REMOVE_PANEL, handleRemovePanel)
82
76
  eventBus.off(events.APP_SHOW_PANEL, handleShowPanel)
83
77
  eventBus.off(events.APP_HIDE_PANEL, handleHidePanel)
84
78
  eventBus.off(events.APP_ADD_CONTROL, handleAddControl)
85
79
  }
86
- }, [dispatch, eventBus])
80
+ }, [eventBus])
87
81
  }
@@ -19,9 +19,9 @@ export function createPluginRegistry ({ registerButton, registerPanel, registerC
19
19
  if (manifest.buttons) {
20
20
  asArray(manifest.buttons).forEach(button => {
21
21
  registerButton({ [button.id]: { ...pluginConfig, ...button } })
22
- // Flat button registry including anu menu items
22
+ // Flat button registry including any menu items (isMenuItem prevents slot rendering)
23
23
  button?.menuItems?.forEach(menuItem => {
24
- registerButton({ [menuItem.id]: { ...pluginConfig, ...menuItem } })
24
+ registerButton({ [menuItem.id]: { ...pluginConfig, ...menuItem, isMenuItem: true } })
25
25
  })
26
26
  })
27
27
  }
@@ -8,9 +8,14 @@ function getMatchingButtons ({ appState, buttonConfig, slot, evaluateProp }) {
8
8
  return []
9
9
  }
10
10
 
11
- return Object.entries(buttonConfig).filter(([_, config]) => {
11
+ return Object.entries(buttonConfig).filter(([_, config]) => { // NOSONAR, extractig to a hleper wouldn't necessarily improve readability
12
12
  const bpConfig = config[breakpoint]
13
13
 
14
+ // Skip menu items — they render inside a parent button's popup, not in a slot
15
+ if (config.isMenuItem) {
16
+ return false
17
+ }
18
+
14
19
  // Dynamic exclusion
15
20
  if (typeof config.excludeWhen === 'function' && evaluateProp(config.excludeWhen, config.pluginId)) {
16
21
  return false
@@ -82,6 +82,16 @@ describe('mapButtons module', () => {
82
82
  const state = { ...appState, isFullscreen: false }
83
83
  expect(getMatchingButtons({ buttonConfig: config, slot: 'header', appState: state, evaluateProp }).length).toBe(1)
84
84
  })
85
+
86
+ it('filters out buttons with isMenuItem:true', () => {
87
+ const config = { b1: { ...baseBtn, isMenuItem: true } }
88
+ expect(getMatchingButtons({ buttonConfig: config, slot: 'header', appState, evaluateProp }).length).toBe(0)
89
+ })
90
+
91
+ it('does not filter out buttons without isMenuItem', () => {
92
+ const config = { b1: baseBtn, b2: { ...baseBtn, isMenuItem: false } }
93
+ expect(getMatchingButtons({ buttonConfig: config, slot: 'header', appState, evaluateProp }).length).toBe(2)
94
+ })
85
95
  })
86
96
 
87
97
  // -------------------------
@@ -203,5 +213,15 @@ describe('mapButtons module', () => {
203
213
  appState.buttonConfig = ({ b1: { ...baseBtn, desktop: { slot: 'header' } } })
204
214
  expect(map()[0].order).toBe(0)
205
215
  })
216
+
217
+ it('excludes menu items from slot rendering even when they have a matching slot', () => {
218
+ appState.buttonConfig = ({
219
+ parent: baseBtn,
220
+ child: { ...baseBtn, isMenuItem: true }
221
+ })
222
+ const result = map()
223
+ expect(result).toHaveLength(1)
224
+ expect(result[0].id).toBe('parent')
225
+ })
206
226
  })
207
227
  })
@@ -8,8 +8,8 @@ export const MapContext = createContext(null)
8
8
  export const MapProvider = ({ options, children }) => {
9
9
  const [state, dispatch] = useReducer(reducer, initialState(options))
10
10
 
11
- const handleMapReady = () => {
12
- dispatch({ type: 'SET_MAP_READY' })
11
+ const handleMapReady = (mapProvider) => {
12
+ dispatch({ type: 'SET_MAP_READY', payload: mapProvider })
13
13
  }
14
14
 
15
15
  const handleInitMapStyles = (mapStyles) => {
@@ -7,10 +7,11 @@ const mergePayload = (state, payload) => ({
7
7
  ...payload
8
8
  })
9
9
 
10
- const setMapReady = (state) => {
10
+ const setMapReady = (state, mapProvider) => {
11
11
  return {
12
12
  ...state,
13
- isMapReady: true
13
+ isMapReady: true,
14
+ mapProvider
14
15
  }
15
16
  }
16
17
 
@@ -19,6 +19,7 @@ export const initialState = (config) => {
19
19
 
20
20
  return {
21
21
  isMapReady: false,
22
+ mapProvider: null,
22
23
  mapStyle: pluginHandlesMapStyles ? null : mapStyle,
23
24
  mapSize,
24
25
  center,
@@ -9,6 +9,7 @@
9
9
  --button-padding: 8px;
10
10
  --button-icon-label-offset: 7px;
11
11
  --label-button-padding: 10px;
12
+ --touch-button-size: 50px;
12
13
 
13
14
  // Borders and focus
14
15
  --app-border-width: 1px;
package/webpack.esm.mjs CHANGED
@@ -72,7 +72,8 @@ const createESMConfig = (entryName, entryPath, outDir, isCore = false) => {
72
72
  path: path.resolve(__dirname, outDir, '../css'),
73
73
  filename: '../esm/[name].js',
74
74
  chunkFilename: '../esm/[name].js',
75
- library: { type: 'module' }
75
+ library: { type: 'module' },
76
+ asyncChunks: false
76
77
  },
77
78
  resolve: {
78
79
  extensions: ['.js', '.jsx'],
@@ -86,15 +87,23 @@ const createESMConfig = (entryName, entryPath, outDir, isCore = false) => {
86
87
  },
87
88
  externals: isCore
88
89
  ? {}
89
- : {
90
- react: 'react',
91
- 'react-dom': 'react-dom',
92
- 'react/jsx-runtime': 'react/jsx-runtime',
93
- preact: 'preact',
94
- 'preact/compat': 'preact/compat',
95
- 'preact/hooks': 'preact/hooks',
96
- 'preact/jsx-runtime': 'preact/jsx-runtime'
97
- },
90
+ : [
91
+ {
92
+ react: 'react',
93
+ 'react-dom': 'react-dom',
94
+ 'react/jsx-runtime': 'react/jsx-runtime',
95
+ preact: 'preact',
96
+ 'preact/compat': 'preact/compat',
97
+ 'preact/hooks': 'preact/hooks',
98
+ 'preact/jsx-runtime': 'preact/jsx-runtime'
99
+ },
100
+ ({ request }, callback) => {
101
+ if (request.startsWith('@arcgis/core')) {
102
+ return callback(null, `module ${request}`)
103
+ }
104
+ return callback()
105
+ }
106
+ ],
98
107
  module: {
99
108
  rules: [
100
109
  { test: /\.jsx?$/, loader: 'babel-loader', exclude: /node_modules/ },
@@ -119,7 +128,7 @@ const ALL_BUILDS = [
119
128
  // Providers
120
129
  { entryPath: './providers/maplibre/src/index.js', outDir: 'providers/maplibre/dist/esm' },
121
130
  { entryPath: './providers/beta/open-names/src/index.js', outDir: 'providers/beta/open-names/dist/esm' },
122
- // { entryPath: './providers/beta/esri/src/index.js', outDir: 'providers/beta/esri/dist/esm' },
131
+ { entryPath: './providers/beta/esri/src/index.js', outDir: 'providers/beta/esri/dist/esm' },
123
132
 
124
133
  // Plugins
125
134
  { entryPath: './plugins/beta/scale-bar/src/index.js', outDir: 'plugins/beta/scale-bar/dist/esm' },
@@ -129,6 +138,7 @@ const ALL_BUILDS = [
129
138
  { entryPath: './plugins/beta/datasets/src/index.js', outDir: 'plugins/beta/datasets/dist/esm' },
130
139
  { entryPath: './plugins/beta/map-styles/src/index.js', outDir: 'plugins/beta/map-styles/dist/esm' },
131
140
  { entryPath: './plugins/beta/draw-ml/src/index.js', outDir: 'plugins/beta/draw-ml/dist/esm' },
141
+ { entryPath: './plugins/beta/draw-es/src/index.js', outDir: 'plugins/beta/draw-es/dist/esm' },
132
142
  { entryPath: './plugins/beta/frame/src/index.js', outDir: 'plugins/beta/frame/dist/esm' }
133
143
  ]
134
144
 
package/webpack.umd.mjs CHANGED
@@ -134,7 +134,6 @@ const ALL_BUILDS = [
134
134
  // Providers
135
135
  { entryPath: './providers/maplibre/src/index.js', libraryPath: 'maplibreProvider', outDir: 'providers/maplibre/dist/umd' },
136
136
  { entryPath: './providers/beta/open-names/src/index.js', libraryPath: 'openNamesProvider', outDir: 'providers/beta/open-names/dist/umd' },
137
- // { entryPath: './providers/beta/esri/src/index.js', libraryPath: 'esriProvider', outDir: 'providers/beta/esri/dist/umd' },
138
137
 
139
138
  // Plugins
140
139
  { entryPath: './plugins/beta/scale-bar/src/index.js', libraryPath: 'scaleBarPlugin', outDir: 'plugins/beta/scale-bar/dist/umd' },