@defra/interactive-map 0.0.11-alpha → 0.0.14-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/plugins/plugin-descriptor.md +37 -0
- package/package.json +15 -6
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/src/events.js +4 -14
- package/plugins/beta/draw-ml/src/modes/createDrawMode.js +1 -3
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +28 -6
- package/plugins/interact/src/InteractInit.test.js +19 -5
- package/plugins/interact/src/events.js +17 -15
- package/plugins/interact/src/events.test.js +25 -16
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/src/events/fetchSuggestions.js +9 -6
- package/providers/beta/esri/dist/css/index.css +4 -0
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/esriProvider.js +19 -3
- package/providers/beta/esri/src/esriProvider.scss +5 -0
- package/providers/beta/esri/src/mapEvents.js +34 -3
- package/providers/beta/esri/src/utils/coords.js +1 -0
- package/providers/beta/esri/src/utils/spatial.js +47 -1
- package/providers/beta/esri/src/utils/spatial.test.js +55 -0
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/src/maplibreProvider.js +12 -1
- package/providers/maplibre/src/maplibreProvider.test.js +14 -1
- package/providers/maplibre/src/utils/spatial.js +40 -0
- package/providers/maplibre/src/utils/spatial.test.js +35 -0
- package/src/App/components/MapButton/MapButton.jsx +1 -0
- package/src/App/components/Panel/Panel.jsx +14 -13
- package/src/App/components/Viewport/MapController.jsx +4 -0
- package/src/App/hooks/useLayoutMeasurements.js +37 -20
- package/src/App/hooks/useLayoutMeasurements.test.js +38 -6
- package/src/App/hooks/useMarkersAPI.js +5 -3
- package/src/App/hooks/useModalPanelBehaviour.js +91 -10
- package/src/App/hooks/useModalPanelBehaviour.test.js +185 -53
- package/src/App/hooks/useVisibleGeometry.js +100 -0
- package/src/App/hooks/useVisibleGeometry.test.js +331 -0
- package/src/App/layout/Layout.jsx +13 -5
- package/src/App/layout/layout.module.scss +149 -13
- package/src/App/renderer/HtmlElementHost.jsx +10 -2
- package/src/App/renderer/HtmlElementHost.test.jsx +12 -0
- package/src/App/renderer/SlotRenderer.jsx +1 -1
- package/src/App/renderer/mapPanels.js +1 -2
- package/src/App/renderer/pluginWrapper.js +3 -2
- package/src/App/renderer/slots.js +12 -6
- package/src/App/store/AppProvider.jsx +6 -1
- package/src/App/store/appDispatchMiddleware.js +19 -0
- package/src/App/store/appDispatchMiddleware.test.js +56 -0
- package/src/InteractiveMap/InteractiveMap.js +3 -3
- package/src/types.js +9 -0
- package/src/utils/getSafeZoneInset.js +12 -9
- package/src/utils/getSafeZoneInset.test.js +102 -58
|
@@ -4,10 +4,17 @@ import { useModalPanelBehaviour } from './useModalPanelBehaviour.js'
|
|
|
4
4
|
import * as useResizeObserverModule from './useResizeObserver.js'
|
|
5
5
|
import * as constrainFocusModule from '../../utils/constrainKeyboardFocus.js'
|
|
6
6
|
import * as toggleInertModule from '../../utils/toggleInertElements.js'
|
|
7
|
+
import { useApp } from '../store/appContext.js'
|
|
7
8
|
|
|
8
9
|
jest.mock('./useResizeObserver.js')
|
|
9
10
|
jest.mock('../../utils/constrainKeyboardFocus.js')
|
|
10
11
|
jest.mock('../../utils/toggleInertElements.js')
|
|
12
|
+
jest.mock('../store/appContext.js')
|
|
13
|
+
|
|
14
|
+
const MODAL_INSET = '--modal-inset'
|
|
15
|
+
const MODAL_MAX_HEIGHT = '--modal-max-height'
|
|
16
|
+
const PANEL_ID = 'modal-panel-id'
|
|
17
|
+
const ARIA_CONTROLS = 'aria-controls'
|
|
11
18
|
|
|
12
19
|
describe('useModalPanelBehaviour', () => {
|
|
13
20
|
let refs, elements, handleClose
|
|
@@ -17,28 +24,40 @@ describe('useModalPanelBehaviour', () => {
|
|
|
17
24
|
main: { current: document.createElement('div') },
|
|
18
25
|
panel: { current: document.createElement('div') }
|
|
19
26
|
}
|
|
27
|
+
// Give panel an ID for aria-controls tests and a slot for setSlotCSSVar
|
|
28
|
+
refs.panel.current.id = PANEL_ID
|
|
29
|
+
refs.panel.current.dataset.slot = 'inset'
|
|
30
|
+
|
|
20
31
|
elements = {
|
|
21
32
|
buttonContainer: document.createElement('div'),
|
|
22
33
|
root: document.createElement('div')
|
|
23
34
|
}
|
|
35
|
+
|
|
24
36
|
elements.root.appendChild(refs.panel.current)
|
|
25
37
|
document.body.appendChild(elements.root)
|
|
38
|
+
|
|
26
39
|
handleClose = jest.fn()
|
|
27
40
|
jest.clearAllMocks()
|
|
28
|
-
document.documentElement.style.setProperty(
|
|
41
|
+
document.documentElement.style.setProperty(MODAL_INSET, '')
|
|
42
|
+
document.documentElement.style.setProperty(MODAL_MAX_HEIGHT, '')
|
|
43
|
+
useApp.mockReturnValue({ layoutRefs: {} })
|
|
29
44
|
})
|
|
30
45
|
|
|
31
46
|
afterEach(() => {
|
|
32
47
|
document.body.innerHTML = ''
|
|
33
48
|
})
|
|
34
49
|
|
|
35
|
-
const TestComponent = ({
|
|
50
|
+
const TestComponent = ({
|
|
51
|
+
isModal = true,
|
|
52
|
+
buttonContainerEl,
|
|
53
|
+
rootEl = elements.root
|
|
54
|
+
}) => {
|
|
36
55
|
useModalPanelBehaviour({
|
|
37
56
|
mainRef: refs.main,
|
|
38
57
|
panelRef: refs.panel,
|
|
39
58
|
isModal,
|
|
40
|
-
rootEl
|
|
41
|
-
buttonContainerEl
|
|
59
|
+
rootEl,
|
|
60
|
+
buttonContainerEl,
|
|
42
61
|
handleClose
|
|
43
62
|
})
|
|
44
63
|
return null
|
|
@@ -63,66 +82,139 @@ describe('useModalPanelBehaviour', () => {
|
|
|
63
82
|
)
|
|
64
83
|
})
|
|
65
84
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
85
|
+
describe('positioning (--modal-inset, --modal-max-height)', () => {
|
|
86
|
+
const buttonSlot = 'map-styles-button'
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
// Force ResizeObserver to run the callback immediately
|
|
90
|
+
useResizeObserverModule.useResizeObserver.mockImplementation((_, cb) => cb())
|
|
91
|
+
jest.spyOn(globalThis, 'getComputedStyle').mockReturnValue({ getPropertyValue: () => '8' })
|
|
92
|
+
|
|
93
|
+
Object.defineProperty(refs.main.current, 'getBoundingClientRect', {
|
|
94
|
+
value: () => ({ top: 0, right: 100, bottom: 50, left: 0, width: 100, height: 50 }),
|
|
95
|
+
configurable: true
|
|
96
|
+
})
|
|
97
|
+
Object.defineProperty(elements.buttonContainer, 'getBoundingClientRect', {
|
|
98
|
+
value: () => ({ top: 10, right: 80, bottom: 40, left: 20, width: 60, height: 30 }),
|
|
99
|
+
configurable: true
|
|
100
|
+
})
|
|
70
101
|
})
|
|
71
|
-
|
|
72
|
-
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
jest.restoreAllMocks()
|
|
73
105
|
})
|
|
74
106
|
|
|
75
|
-
|
|
107
|
+
it('sets --modal-inset via SLOT_MODAL_VARS for top slot', () => {
|
|
108
|
+
refs.panel.current.dataset.slot = 'left-top'
|
|
109
|
+
render(<TestComponent />)
|
|
110
|
+
expect(document.documentElement.style.getPropertyValue(MODAL_INSET))
|
|
111
|
+
.toBe('var(--left-offset-top) auto auto var(--primary-gap)')
|
|
112
|
+
expect(document.documentElement.style.getPropertyValue(MODAL_MAX_HEIGHT))
|
|
113
|
+
.toBe('var(--left-top-max-height)')
|
|
114
|
+
})
|
|
76
115
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
116
|
+
it('sets --modal-inset via SLOT_MODAL_VARS for bottom slot', () => {
|
|
117
|
+
refs.panel.current.dataset.slot = 'left-bottom'
|
|
118
|
+
render(<TestComponent />)
|
|
119
|
+
expect(document.documentElement.style.getPropertyValue(MODAL_INSET))
|
|
120
|
+
.toBe('auto auto var(--left-offset-bottom) var(--primary-gap)')
|
|
121
|
+
expect(document.documentElement.style.getPropertyValue(MODAL_MAX_HEIGHT))
|
|
122
|
+
.toBe('var(--left-top-max-height)')
|
|
123
|
+
})
|
|
80
124
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
125
|
+
it('sets --modal-inset and --modal-max-height from slot container when no buttonContainerEl', () => {
|
|
126
|
+
const insetEl = document.createElement('div')
|
|
127
|
+
Object.defineProperty(insetEl, 'getBoundingClientRect', {
|
|
128
|
+
value: () => ({ top: 60, left: 8, right: 200, bottom: 200 }),
|
|
129
|
+
configurable: true
|
|
130
|
+
})
|
|
131
|
+
Object.defineProperty(insetEl, 'offsetWidth', { value: 192, configurable: true })
|
|
132
|
+
useApp.mockReturnValue({ layoutRefs: { insetRef: { current: insetEl }, mainRef: refs.main } })
|
|
88
133
|
|
|
89
|
-
it('calls handleClose when backdrop inside rootEl is clicked', () => {
|
|
90
|
-
const backdrop = createBackdrop(elements.root)
|
|
91
134
|
render(<TestComponent />)
|
|
92
|
-
|
|
93
|
-
expect(
|
|
135
|
+
|
|
136
|
+
expect(document.documentElement.style.getPropertyValue(MODAL_INSET)).toBe('60px auto auto 8px')
|
|
137
|
+
expect(document.documentElement.style.getPropertyValue(MODAL_MAX_HEIGHT)).toContain('px')
|
|
94
138
|
})
|
|
95
139
|
|
|
96
|
-
it('
|
|
97
|
-
const backdrop = createBackdrop(document.body)
|
|
140
|
+
it('leaves --modal-inset unset when slot ref cannot be resolved', () => {
|
|
98
141
|
render(<TestComponent />)
|
|
99
|
-
|
|
100
|
-
expect(handleClose).not.toHaveBeenCalled()
|
|
142
|
+
expect(document.documentElement.style.getPropertyValue(MODAL_INSET)).toBe('')
|
|
101
143
|
})
|
|
102
144
|
|
|
103
|
-
it('
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
145
|
+
it('updates --modal-inset and --modal-max-height via aria-controls when buttonContainerEl is stale', () => {
|
|
146
|
+
refs.panel.current.dataset.slot = buttonSlot
|
|
147
|
+
|
|
148
|
+
const button = document.createElement('button')
|
|
149
|
+
button.setAttribute(ARIA_CONTROLS, PANEL_ID)
|
|
150
|
+
elements.buttonContainer.appendChild(button)
|
|
151
|
+
document.body.appendChild(elements.buttonContainer)
|
|
152
|
+
|
|
153
|
+
const staleEl = document.createElement('div') // detached
|
|
154
|
+
render(<TestComponent buttonContainerEl={staleEl} />)
|
|
155
|
+
|
|
156
|
+
expect(document.documentElement.style.getPropertyValue(MODAL_INSET)).toContain('10px')
|
|
157
|
+
expect(document.documentElement.style.getPropertyValue(MODAL_MAX_HEIGHT)).toContain('px')
|
|
108
158
|
})
|
|
109
|
-
})
|
|
110
159
|
|
|
111
|
-
|
|
112
|
-
|
|
160
|
+
it('uses data-button-slot fallback when no aria-controls button and no buttonContainerEl', () => {
|
|
161
|
+
refs.panel.current.dataset.slot = buttonSlot
|
|
162
|
+
elements.buttonContainer.dataset.buttonSlot = buttonSlot
|
|
163
|
+
document.body.appendChild(elements.buttonContainer)
|
|
164
|
+
|
|
165
|
+
render(<TestComponent buttonContainerEl={undefined} />)
|
|
113
166
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
isFullscreen: true,
|
|
117
|
-
boundaryEl: elements.root
|
|
167
|
+
expect(document.documentElement.style.getPropertyValue(MODAL_INSET)).toContain('10px')
|
|
168
|
+
expect(document.documentElement.style.getPropertyValue(MODAL_MAX_HEIGHT)).toContain('px')
|
|
118
169
|
})
|
|
119
170
|
|
|
120
|
-
|
|
171
|
+
it('uses connected buttonContainerEl when panel has no ID', () => {
|
|
172
|
+
refs.panel.current.id = '' // falsy panelElId → currentButtonEl = null (line 152 false branch)
|
|
173
|
+
refs.panel.current.dataset.slot = buttonSlot
|
|
174
|
+
document.body.appendChild(elements.buttonContainer) // isConnected = true (line 154 true branch)
|
|
175
|
+
|
|
176
|
+
render(<TestComponent buttonContainerEl={elements.buttonContainer} />)
|
|
177
|
+
|
|
178
|
+
expect(document.documentElement.style.getPropertyValue(MODAL_INSET)).toContain('10px')
|
|
179
|
+
})
|
|
121
180
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
181
|
+
it('skips update when effectiveContainer cannot be resolved', () => {
|
|
182
|
+
refs.panel.current.dataset.slot = buttonSlot
|
|
183
|
+
render(<TestComponent buttonContainerEl={null} />)
|
|
184
|
+
expect(document.documentElement.style.getPropertyValue(MODAL_INSET)).toBe('')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('anchors to bottom when button is in a bottom sub-slot', () => {
|
|
188
|
+
refs.panel.current.dataset.slot = buttonSlot
|
|
189
|
+
const button = document.createElement('button')
|
|
190
|
+
button.setAttribute(ARIA_CONTROLS, PANEL_ID)
|
|
191
|
+
elements.buttonContainer.appendChild(button)
|
|
192
|
+
const bottomSlot = document.createElement('div')
|
|
193
|
+
bottomSlot.className = 'im-o-app__right-bottom'
|
|
194
|
+
bottomSlot.appendChild(elements.buttonContainer)
|
|
195
|
+
document.body.appendChild(bottomSlot)
|
|
196
|
+
|
|
197
|
+
render(<TestComponent />)
|
|
198
|
+
|
|
199
|
+
// Bottom slot: insetTop='auto', insetBottom = mainRect.bottom - buttonRect.bottom = 50 - 40 = 10px
|
|
200
|
+
expect(document.documentElement.style.getPropertyValue(MODAL_INSET)).toMatch(/^auto/)
|
|
201
|
+
expect(document.documentElement.style.getPropertyValue(MODAL_INSET)).toContain('10px')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('uses left inset when button is in a left sub-slot', () => {
|
|
205
|
+
refs.panel.current.dataset.slot = buttonSlot
|
|
206
|
+
const button = document.createElement('button')
|
|
207
|
+
button.setAttribute(ARIA_CONTROLS, PANEL_ID)
|
|
208
|
+
elements.buttonContainer.appendChild(button)
|
|
209
|
+
const leftSlot = document.createElement('div')
|
|
210
|
+
leftSlot.className = 'im-o-app__left-top'
|
|
211
|
+
leftSlot.appendChild(elements.buttonContainer)
|
|
212
|
+
document.body.appendChild(leftSlot)
|
|
213
|
+
|
|
214
|
+
render(<TestComponent />)
|
|
215
|
+
|
|
216
|
+
// Left slot: insetTop = buttonRect.top - mainRect.top = 10, insetLeft = buttonRect.right - mainRect.left + dividerGap = 80 + 8 = 88
|
|
217
|
+
expect(document.documentElement.style.getPropertyValue(MODAL_INSET)).toBe('10px auto auto 88px')
|
|
126
218
|
})
|
|
127
219
|
})
|
|
128
220
|
|
|
@@ -138,6 +230,20 @@ describe('useModalPanelBehaviour', () => {
|
|
|
138
230
|
expect(refs.panel.current.focus).toHaveBeenCalled()
|
|
139
231
|
})
|
|
140
232
|
|
|
233
|
+
// COVERS LINE 44 (The early return branch)
|
|
234
|
+
it('does not redirect focus when focus moves completely outside the app root', () => {
|
|
235
|
+
refs.panel.current.focus = jest.fn()
|
|
236
|
+
render(<TestComponent />)
|
|
237
|
+
|
|
238
|
+
const externalEl = document.createElement('button')
|
|
239
|
+
document.body.appendChild(externalEl) // Outside elements.root
|
|
240
|
+
|
|
241
|
+
dispatchFocusIn(externalEl)
|
|
242
|
+
|
|
243
|
+
// Since isInsideApp is false, it should hit the "return" and not call focus()
|
|
244
|
+
expect(refs.panel.current.focus).not.toHaveBeenCalled()
|
|
245
|
+
})
|
|
246
|
+
|
|
141
247
|
it('does not redirect focus when focus is already inside panel', () => {
|
|
142
248
|
refs.panel.current.focus = jest.fn()
|
|
143
249
|
render(<TestComponent />)
|
|
@@ -149,23 +255,49 @@ describe('useModalPanelBehaviour', () => {
|
|
|
149
255
|
expect(refs.panel.current.focus).not.toHaveBeenCalled()
|
|
150
256
|
})
|
|
151
257
|
|
|
152
|
-
it('handles
|
|
258
|
+
it('handles null focus targets gracefully', () => {
|
|
153
259
|
render(<TestComponent />)
|
|
154
|
-
|
|
155
260
|
dispatchFocusIn(null)
|
|
261
|
+
expect(true).toBe(true)
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
describe('backdrop and inert', () => {
|
|
266
|
+
it('calls handleClose when backdrop inside rootEl is clicked', () => {
|
|
267
|
+
const backdrop = document.createElement('div')
|
|
268
|
+
backdrop.className = 'im-o-app__modal-backdrop'
|
|
269
|
+
elements.root.appendChild(backdrop)
|
|
270
|
+
|
|
271
|
+
render(<TestComponent />)
|
|
272
|
+
fireEvent.click(backdrop)
|
|
273
|
+
expect(handleClose).toHaveBeenCalled()
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('does not close when backdrop is outside rootEl', () => {
|
|
277
|
+
const externalBackdrop = document.createElement('div')
|
|
278
|
+
externalBackdrop.className = 'im-o-app__modal-backdrop'
|
|
279
|
+
document.body.appendChild(externalBackdrop)
|
|
156
280
|
|
|
157
|
-
|
|
158
|
-
|
|
281
|
+
render(<TestComponent />)
|
|
282
|
+
fireEvent.click(externalBackdrop)
|
|
283
|
+
expect(handleClose).not.toHaveBeenCalled()
|
|
284
|
+
})
|
|
159
285
|
|
|
160
|
-
|
|
286
|
+
it('toggles inert elements on mount and cleanup', () => {
|
|
287
|
+
const { unmount } = render(<TestComponent />)
|
|
288
|
+
expect(toggleInertModule.toggleInertElements).toHaveBeenCalledWith(
|
|
289
|
+
expect.objectContaining({ isFullscreen: true })
|
|
290
|
+
)
|
|
291
|
+
unmount()
|
|
292
|
+
expect(toggleInertModule.toggleInertElements).toHaveBeenCalledWith(
|
|
293
|
+
expect.objectContaining({ isFullscreen: false })
|
|
294
|
+
)
|
|
161
295
|
})
|
|
162
296
|
})
|
|
163
297
|
|
|
164
298
|
it('does nothing when isModal is false', () => {
|
|
165
299
|
render(<TestComponent isModal={false} />)
|
|
166
|
-
|
|
167
300
|
fireEvent.keyDown(refs.panel.current, { key: 'Escape' })
|
|
168
301
|
expect(handleClose).not.toHaveBeenCalled()
|
|
169
|
-
expect(toggleInertModule.toggleInertElements).not.toHaveBeenCalled()
|
|
170
302
|
})
|
|
171
303
|
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { useConfig } from '../store/configContext.js'
|
|
3
|
+
import { useApp } from '../store/appContext.js'
|
|
4
|
+
import { EVENTS as events } from '../../config/events.js'
|
|
5
|
+
|
|
6
|
+
export const getGeometryType = (geojson) => {
|
|
7
|
+
if (!geojson) {
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
if (geojson.type === 'Feature') {
|
|
11
|
+
return geojson.geometry?.type
|
|
12
|
+
}
|
|
13
|
+
return geojson.type
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const isPointGeometry = (geojson) => {
|
|
17
|
+
const type = getGeometryType(geojson)
|
|
18
|
+
return type === 'Point' || type === 'MultiPoint'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const getPointCoordinates = (geojson) => {
|
|
22
|
+
if (geojson.type === 'Feature') {
|
|
23
|
+
return getPointCoordinates(geojson.geometry)
|
|
24
|
+
}
|
|
25
|
+
if (geojson.type === 'Point') {
|
|
26
|
+
return geojson.coordinates
|
|
27
|
+
}
|
|
28
|
+
if (geojson.type === 'MultiPoint') {
|
|
29
|
+
return geojson.coordinates[0]
|
|
30
|
+
}
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SLOT_REFS = {
|
|
35
|
+
inset: 'insetRef',
|
|
36
|
+
bottom: 'bottomRef',
|
|
37
|
+
side: 'sideRef'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const useVisibleGeometry = () => {
|
|
41
|
+
const { mapProvider, eventBus } = useConfig()
|
|
42
|
+
const { layoutRefs, panelConfig, panelRegistry, breakpoint } = useApp()
|
|
43
|
+
|
|
44
|
+
const latestRef = useRef({ layoutRefs, panelConfig, panelRegistry, breakpoint })
|
|
45
|
+
latestRef.current = { layoutRefs, panelConfig, panelRegistry, breakpoint }
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!mapProvider || !eventBus) {
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const handlePanelOpened = ({ panelId, slot: eventSlot, visibleGeometry: eventVisibleGeometry }) => {
|
|
53
|
+
const { panelConfig: config, panelRegistry: registry, layoutRefs: refs, breakpoint: bp } = latestRef.current
|
|
54
|
+
const resolvedConfig = config?.[panelId] ? config : (registry?.getPanelConfig() ?? config)
|
|
55
|
+
const visibleGeometry = eventVisibleGeometry ?? resolvedConfig?.[panelId]?.visibleGeometry
|
|
56
|
+
const slot = eventSlot ?? resolvedConfig?.[panelId]?.[bp]?.slot
|
|
57
|
+
const slotRef = refs[SLOT_REFS[slot]]
|
|
58
|
+
|
|
59
|
+
if (!visibleGeometry || !slotRef) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
if (typeof mapProvider.isGeometryObscured !== 'function') {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const waitForPanel = () => {
|
|
67
|
+
const panelRect = slotRef.current?.getBoundingClientRect()
|
|
68
|
+
|
|
69
|
+
if (!panelRect || panelRect.width === 0 || panelRect.height === 0) {
|
|
70
|
+
// Not ready yet, check on the next animation frame
|
|
71
|
+
requestAnimationFrame(waitForPanel)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Panel now exists and has size, safe to measure
|
|
76
|
+
if (!mapProvider.isGeometryObscured(visibleGeometry, panelRect)) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (isPointGeometry(visibleGeometry)) {
|
|
81
|
+
const center = getPointCoordinates(visibleGeometry)
|
|
82
|
+
if (center) {
|
|
83
|
+
mapProvider.setView({ center })
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
mapProvider.fitToBounds(visibleGeometry)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Start waiting for panel to exist with a measurable size
|
|
91
|
+
requestAnimationFrame(waitForPanel)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
eventBus.on(events.APP_PANEL_OPENED, handlePanelOpened)
|
|
95
|
+
|
|
96
|
+
return () => {
|
|
97
|
+
eventBus.off(events.APP_PANEL_OPENED, handlePanelOpened)
|
|
98
|
+
}
|
|
99
|
+
}, [mapProvider, eventBus])
|
|
100
|
+
}
|