@defra/interactive-map 0.0.12-alpha → 0.0.15-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/package.json +9 -4
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/src/manifest.js +4 -4
- package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/src/manifest.js +2 -2
- package/plugins/search/dist/css/index.css +1 -1
- 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 +10 -7
- package/plugins/search/src/events/fetchSuggestions.test.js +4 -4
- package/plugins/search/src/search.scss +8 -3
- package/providers/beta/esri/dist/css/index.css +4 -0
- package/providers/beta/esri/src/esriProvider.scss +5 -0
- package/src/App/components/MapButton/MapButton.jsx +1 -0
- package/src/App/components/Panel/Panel.jsx +14 -13
- package/src/App/components/Panel/Panel.module.scss +1 -0
- package/src/App/hooks/useLayoutMeasurements.js +31 -23
- package/src/App/hooks/useLayoutMeasurements.test.js +39 -10
- package/src/App/hooks/useModalPanelBehaviour.js +85 -21
- package/src/App/hooks/useModalPanelBehaviour.test.js +126 -18
- package/src/App/hooks/useVisibleGeometry.js +7 -13
- package/src/App/hooks/useVisibleGeometry.test.js +72 -47
- package/src/App/layout/Layout.jsx +11 -6
- package/src/App/layout/Layout.test.jsx +0 -1
- package/src/App/layout/layout.module.scss +83 -10
- package/src/App/renderer/HtmlElementHost.jsx +10 -4
- package/src/App/renderer/HtmlElementHost.test.jsx +32 -11
- package/src/App/renderer/SlotRenderer.jsx +1 -1
- package/src/App/renderer/mapPanels.js +1 -2
- package/src/App/renderer/mapPanels.test.js +3 -3
- package/src/App/renderer/slotHelpers.js +2 -2
- package/src/App/renderer/slotHelpers.test.js +3 -3
- package/src/App/renderer/slots.js +11 -8
- package/src/App/store/AppProvider.jsx +5 -2
- package/src/App/store/appDispatchMiddleware.test.js +2 -2
- package/src/config/appConfig.js +4 -4
- package/src/utils/getSafeZoneInset.js +139 -39
- package/src/utils/getSafeZoneInset.test.js +301 -81
|
@@ -31,14 +31,8 @@ export const getPointCoordinates = (geojson) => {
|
|
|
31
31
|
return null
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const SLOT_REFS = {
|
|
35
|
-
inset: 'insetRef',
|
|
36
|
-
bottom: 'bottomRef',
|
|
37
|
-
side: 'sideRef'
|
|
38
|
-
}
|
|
39
|
-
|
|
40
34
|
export const useVisibleGeometry = () => {
|
|
41
|
-
const { mapProvider, eventBus } = useConfig()
|
|
35
|
+
const { id, mapProvider, eventBus } = useConfig()
|
|
42
36
|
const { layoutRefs, panelConfig, panelRegistry, breakpoint } = useApp()
|
|
43
37
|
|
|
44
38
|
const latestRef = useRef({ layoutRefs, panelConfig, panelRegistry, breakpoint })
|
|
@@ -49,14 +43,13 @@ export const useVisibleGeometry = () => {
|
|
|
49
43
|
return undefined
|
|
50
44
|
}
|
|
51
45
|
|
|
52
|
-
const handlePanelOpened = ({ panelId,
|
|
53
|
-
const { panelConfig: config, panelRegistry: registry
|
|
46
|
+
const handlePanelOpened = ({ panelId, visibleGeometry: eventVisibleGeometry }) => {
|
|
47
|
+
const { panelConfig: config, panelRegistry: registry } = latestRef.current
|
|
54
48
|
const resolvedConfig = config?.[panelId] ? config : (registry?.getPanelConfig() ?? config)
|
|
55
49
|
const visibleGeometry = eventVisibleGeometry ?? resolvedConfig?.[panelId]?.visibleGeometry
|
|
56
|
-
const
|
|
57
|
-
const slotRef = refs[SLOT_REFS[slot]]
|
|
50
|
+
const panel = layoutRefs.appContainerRef.current?.querySelector(`#${id}-panel-${panelId}`)
|
|
58
51
|
|
|
59
|
-
if (!visibleGeometry
|
|
52
|
+
if (!visibleGeometry) {
|
|
60
53
|
return
|
|
61
54
|
}
|
|
62
55
|
if (typeof mapProvider.isGeometryObscured !== 'function') {
|
|
@@ -64,7 +57,8 @@ export const useVisibleGeometry = () => {
|
|
|
64
57
|
}
|
|
65
58
|
|
|
66
59
|
const waitForPanel = () => {
|
|
67
|
-
|
|
60
|
+
if (!panel) { return }
|
|
61
|
+
const panelRect = panel.getBoundingClientRect()
|
|
68
62
|
|
|
69
63
|
if (!panelRect || panelRect.width === 0 || panelRect.height === 0) {
|
|
70
64
|
// Not ready yet, check on the next animation frame
|
|
@@ -10,8 +10,16 @@ const pointFeature = { type: 'Feature', geometry: { type: 'Point', coordinates:
|
|
|
10
10
|
const multiPointFeature = { type: 'Feature', geometry: { type: 'MultiPoint', coordinates: [[1, 51], [2, 52]] }, properties: {} }
|
|
11
11
|
const polygonFeature = { type: 'Feature', geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 0]]] }, properties: {} }
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
const
|
|
13
|
+
const APP_ID = 'test'
|
|
14
|
+
const panelRect = { left: 600, top: 0, right: 1000, bottom: 800, width: 400, height: 800 }
|
|
15
|
+
|
|
16
|
+
// Creates a panel DOM element with id matching what useVisibleGeometry queries.
|
|
17
|
+
const makePanelEl = (panelId, rect = panelRect) => {
|
|
18
|
+
const el = document.createElement('div')
|
|
19
|
+
el.id = `${APP_ID}-panel-${panelId}`
|
|
20
|
+
el.getBoundingClientRect = jest.fn(() => rect)
|
|
21
|
+
return el
|
|
22
|
+
}
|
|
15
23
|
|
|
16
24
|
const setup = (overrides = {}) => {
|
|
17
25
|
const capturedHandlers = {}
|
|
@@ -27,19 +35,18 @@ const setup = (overrides = {}) => {
|
|
|
27
35
|
...overrides.eventBus
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
38
|
+
// appContainerRef holds panel elements that the hook queries by id
|
|
39
|
+
const appContainer = document.createElement('div')
|
|
40
|
+
const myPanelEl = makePanelEl('myPanel')
|
|
41
|
+
appContainer.appendChild(myPanelEl)
|
|
34
42
|
|
|
35
43
|
const layoutRefs = {
|
|
36
44
|
mainRef: { current: document.createElement('div') },
|
|
37
|
-
|
|
38
|
-
bottomRef: { current: bottomEl },
|
|
45
|
+
appContainerRef: { current: appContainer },
|
|
39
46
|
...overrides.layoutRefs
|
|
40
47
|
}
|
|
41
48
|
const panelConfig = {
|
|
42
|
-
myPanel: { visibleGeometry: polygonFeature, desktop: { slot: '
|
|
49
|
+
myPanel: { visibleGeometry: polygonFeature, desktop: { slot: 'left-top' } },
|
|
43
50
|
emptyPanel: {},
|
|
44
51
|
...overrides.panelConfig
|
|
45
52
|
}
|
|
@@ -48,10 +55,10 @@ const setup = (overrides = {}) => {
|
|
|
48
55
|
...overrides.panelRegistry
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
useConfig.mockReturnValue({ mapProvider, eventBus, ...overrides.config })
|
|
58
|
+
useConfig.mockReturnValue({ id: APP_ID, mapProvider, eventBus, ...overrides.config })
|
|
52
59
|
useApp.mockReturnValue({ layoutRefs, panelConfig, panelRegistry, breakpoint: 'desktop', ...overrides.app })
|
|
53
60
|
|
|
54
|
-
return { mapProvider, eventBus, capturedHandlers, layoutRefs, panelConfig,
|
|
61
|
+
return { mapProvider, eventBus, capturedHandlers, layoutRefs, panelConfig, myPanelEl, appContainer }
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
describe('useVisibleGeometry', () => {
|
|
@@ -102,12 +109,14 @@ describe('useVisibleGeometry', () => {
|
|
|
102
109
|
expect(mapProvider.isGeometryObscured).not.toHaveBeenCalled()
|
|
103
110
|
})
|
|
104
111
|
|
|
105
|
-
test('does nothing when panel
|
|
112
|
+
test('does nothing when panel element is not in the DOM', () => {
|
|
113
|
+
// Panel has visibleGeometry and slot config but its DOM element is not mounted yet
|
|
106
114
|
const { mapProvider, capturedHandlers } = setup({
|
|
107
|
-
panelConfig: {
|
|
115
|
+
panelConfig: { noElPanel: { visibleGeometry: polygonFeature, desktop: { slot: 'left-top' } } }
|
|
108
116
|
})
|
|
109
117
|
renderHook(() => useVisibleGeometry())
|
|
110
|
-
capturedHandlers['app:panelopened']({ panelId: '
|
|
118
|
+
capturedHandlers['app:panelopened']({ panelId: 'noElPanel' })
|
|
119
|
+
jest.runAllTimers()
|
|
111
120
|
expect(mapProvider.isGeometryObscured).not.toHaveBeenCalled()
|
|
112
121
|
})
|
|
113
122
|
|
|
@@ -118,19 +127,20 @@ describe('useVisibleGeometry', () => {
|
|
|
118
127
|
expect(mapProvider.fitToBounds).not.toHaveBeenCalled()
|
|
119
128
|
})
|
|
120
129
|
|
|
121
|
-
test('does nothing when
|
|
122
|
-
const
|
|
123
|
-
|
|
130
|
+
test('does nothing when panel element has zero dimensions (panel not yet visible)', () => {
|
|
131
|
+
const zeroRect = { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }
|
|
132
|
+
const appContainer = document.createElement('div')
|
|
133
|
+
appContainer.appendChild(makePanelEl('myPanel', zeroRect))
|
|
124
134
|
const { mapProvider, capturedHandlers } = setup({
|
|
125
135
|
layoutRefs: {
|
|
126
136
|
mainRef: { current: document.createElement('div') },
|
|
127
|
-
|
|
137
|
+
appContainerRef: { current: appContainer }
|
|
128
138
|
}
|
|
129
139
|
})
|
|
130
140
|
renderHook(() => useVisibleGeometry())
|
|
131
141
|
capturedHandlers['app:panelopened']({ panelId: 'myPanel' })
|
|
132
142
|
|
|
133
|
-
// Run the
|
|
143
|
+
// Run only the first pending animation frame — panel has zero size so it reschedules
|
|
134
144
|
jest.runOnlyPendingTimers()
|
|
135
145
|
|
|
136
146
|
expect(mapProvider.isGeometryObscured).not.toHaveBeenCalled()
|
|
@@ -151,14 +161,17 @@ describe('useVisibleGeometry', () => {
|
|
|
151
161
|
capturedHandlers['app:panelopened']({ panelId: 'myPanel' })
|
|
152
162
|
jest.runAllTimers()
|
|
153
163
|
|
|
154
|
-
expect(mapProvider.isGeometryObscured).toHaveBeenCalledWith(polygonFeature,
|
|
164
|
+
expect(mapProvider.isGeometryObscured).toHaveBeenCalledWith(polygonFeature, panelRect)
|
|
155
165
|
expect(mapProvider.fitToBounds).toHaveBeenCalledWith(polygonFeature)
|
|
156
166
|
expect(mapProvider.setView).not.toHaveBeenCalled()
|
|
157
167
|
})
|
|
158
168
|
|
|
159
169
|
test('calls setView with center for Point geometry when obscured', () => {
|
|
170
|
+
const appContainer = document.createElement('div')
|
|
171
|
+
appContainer.appendChild(makePanelEl('pointPanel'))
|
|
160
172
|
const { mapProvider, capturedHandlers } = setup({
|
|
161
|
-
panelConfig: { pointPanel: { visibleGeometry: pointFeature, desktop: { slot: '
|
|
173
|
+
panelConfig: { pointPanel: { visibleGeometry: pointFeature, desktop: { slot: 'left-top' } } },
|
|
174
|
+
layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
|
|
162
175
|
})
|
|
163
176
|
renderHook(() => useVisibleGeometry())
|
|
164
177
|
capturedHandlers['app:panelopened']({ panelId: 'pointPanel' })
|
|
@@ -169,8 +182,11 @@ describe('useVisibleGeometry', () => {
|
|
|
169
182
|
})
|
|
170
183
|
|
|
171
184
|
test('calls setView with first coordinate for MultiPoint geometry when obscured', () => {
|
|
185
|
+
const appContainer = document.createElement('div')
|
|
186
|
+
appContainer.appendChild(makePanelEl('mpPanel'))
|
|
172
187
|
const { mapProvider, capturedHandlers } = setup({
|
|
173
|
-
panelConfig: { mpPanel: { visibleGeometry: multiPointFeature, desktop: { slot: '
|
|
188
|
+
panelConfig: { mpPanel: { visibleGeometry: multiPointFeature, desktop: { slot: 'left-top' } } },
|
|
189
|
+
layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
|
|
174
190
|
})
|
|
175
191
|
renderHook(() => useVisibleGeometry())
|
|
176
192
|
capturedHandlers['app:panelopened']({ panelId: 'mpPanel' })
|
|
@@ -182,8 +198,11 @@ describe('useVisibleGeometry', () => {
|
|
|
182
198
|
|
|
183
199
|
test('calls fitToBounds for a raw non-Feature geometry (e.g. Polygon) when obscured', () => {
|
|
184
200
|
const rawPolygon = { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 0]]] }
|
|
201
|
+
const appContainer = document.createElement('div')
|
|
202
|
+
appContainer.appendChild(makePanelEl('geoPanel'))
|
|
185
203
|
const { mapProvider, capturedHandlers } = setup({
|
|
186
|
-
panelConfig: { geoPanel: { visibleGeometry: rawPolygon, desktop: { slot: '
|
|
204
|
+
panelConfig: { geoPanel: { visibleGeometry: rawPolygon, desktop: { slot: 'left-top' } } },
|
|
205
|
+
layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
|
|
187
206
|
})
|
|
188
207
|
renderHook(() => useVisibleGeometry())
|
|
189
208
|
capturedHandlers['app:panelopened']({ panelId: 'geoPanel' })
|
|
@@ -194,8 +213,11 @@ describe('useVisibleGeometry', () => {
|
|
|
194
213
|
|
|
195
214
|
test('calls setView for a raw Point geometry (not Feature-wrapped) when obscured', () => {
|
|
196
215
|
const rawPoint = { type: 'Point', coordinates: [1, 51] }
|
|
216
|
+
const appContainer = document.createElement('div')
|
|
217
|
+
appContainer.appendChild(makePanelEl('rawPointPanel'))
|
|
197
218
|
const { mapProvider, capturedHandlers } = setup({
|
|
198
|
-
panelConfig: { rawPointPanel: { visibleGeometry: rawPoint, desktop: { slot: '
|
|
219
|
+
panelConfig: { rawPointPanel: { visibleGeometry: rawPoint, desktop: { slot: 'left-top' } } },
|
|
220
|
+
layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
|
|
199
221
|
})
|
|
200
222
|
renderHook(() => useVisibleGeometry())
|
|
201
223
|
capturedHandlers['app:panelopened']({ panelId: 'rawPointPanel' })
|
|
@@ -206,8 +228,11 @@ describe('useVisibleGeometry', () => {
|
|
|
206
228
|
|
|
207
229
|
test('does not call setView when Point feature has null coordinates', () => {
|
|
208
230
|
const nullCoordsFeature = { type: 'Feature', geometry: { type: 'Point', coordinates: null }, properties: {} }
|
|
231
|
+
const appContainer = document.createElement('div')
|
|
232
|
+
appContainer.appendChild(makePanelEl('nullPanel'))
|
|
209
233
|
const { mapProvider, capturedHandlers } = setup({
|
|
210
|
-
panelConfig: { nullPanel: { visibleGeometry: nullCoordsFeature, desktop: { slot: '
|
|
234
|
+
panelConfig: { nullPanel: { visibleGeometry: nullCoordsFeature, desktop: { slot: 'left-top' } } },
|
|
235
|
+
layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
|
|
211
236
|
})
|
|
212
237
|
renderHook(() => useVisibleGeometry())
|
|
213
238
|
capturedHandlers['app:panelopened']({ panelId: 'nullPanel' })
|
|
@@ -216,26 +241,14 @@ describe('useVisibleGeometry', () => {
|
|
|
216
241
|
expect(mapProvider.fitToBounds).not.toHaveBeenCalled()
|
|
217
242
|
})
|
|
218
243
|
|
|
219
|
-
test('uses bottom slot ref when panel is in bottom slot', () => {
|
|
220
|
-
const { mapProvider, capturedHandlers } = setup({
|
|
221
|
-
panelConfig: { bottomPanel: { visibleGeometry: polygonFeature, desktop: { slot: 'bottom' } } }
|
|
222
|
-
})
|
|
223
|
-
renderHook(() => useVisibleGeometry())
|
|
224
|
-
capturedHandlers['app:panelopened']({ panelId: 'bottomPanel' })
|
|
225
|
-
jest.runAllTimers()
|
|
226
|
-
|
|
227
|
-
expect(mapProvider.isGeometryObscured).toHaveBeenCalledWith(polygonFeature, bottomPanelRect)
|
|
228
|
-
expect(mapProvider.fitToBounds).toHaveBeenCalledWith(polygonFeature)
|
|
229
|
-
})
|
|
230
|
-
|
|
231
244
|
test('uses latest panelConfig via ref when it changes between renders', () => {
|
|
232
|
-
const { mapProvider, capturedHandlers,
|
|
245
|
+
const { mapProvider, capturedHandlers, appContainer } = setup()
|
|
233
246
|
const { rerender } = renderHook(() => useVisibleGeometry())
|
|
234
247
|
|
|
235
248
|
const updatedGeometry = { type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} }
|
|
236
|
-
const updatedPanelConfig = { myPanel: { visibleGeometry: updatedGeometry, desktop: { slot: '
|
|
249
|
+
const updatedPanelConfig = { myPanel: { visibleGeometry: updatedGeometry, desktop: { slot: 'left-top' } } }
|
|
237
250
|
useApp.mockReturnValue({
|
|
238
|
-
layoutRefs: { mainRef: { current: document.createElement('div') },
|
|
251
|
+
layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } },
|
|
239
252
|
panelConfig: updatedPanelConfig,
|
|
240
253
|
panelRegistry: { getPanelConfig: jest.fn(() => updatedPanelConfig) },
|
|
241
254
|
breakpoint: 'desktop'
|
|
@@ -249,20 +262,26 @@ describe('useVisibleGeometry', () => {
|
|
|
249
262
|
|
|
250
263
|
test('uses slot from event payload when registry config lacks slot info', () => {
|
|
251
264
|
const freshGeometry = { type: 'Feature', geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 0]]] }, properties: {} }
|
|
265
|
+
const appContainer = document.createElement('div')
|
|
266
|
+
appContainer.appendChild(makePanelEl('freshPanel'))
|
|
252
267
|
const { mapProvider, capturedHandlers } = setup({
|
|
253
|
-
panelRegistry: { getPanelConfig: jest.fn(() => ({ freshPanel: { visibleGeometry: freshGeometry } })) }
|
|
268
|
+
panelRegistry: { getPanelConfig: jest.fn(() => ({ freshPanel: { visibleGeometry: freshGeometry } })) },
|
|
269
|
+
layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
|
|
254
270
|
})
|
|
255
271
|
renderHook(() => useVisibleGeometry())
|
|
256
272
|
// Event includes slot (as middleware provides for ADD_PANEL); registry config has no slot info
|
|
257
|
-
capturedHandlers['app:panelopened']({ panelId: 'freshPanel', slot: '
|
|
273
|
+
capturedHandlers['app:panelopened']({ panelId: 'freshPanel', slot: 'left-top' })
|
|
258
274
|
jest.runAllTimers()
|
|
259
275
|
expect(mapProvider.fitToBounds).toHaveBeenCalledWith(freshGeometry)
|
|
260
276
|
})
|
|
261
277
|
|
|
262
278
|
test('falls back to panelRegistry for panels not yet in stale panelConfig', () => {
|
|
263
279
|
const freshGeometry = { type: 'Feature', geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 0]]] }, properties: {} }
|
|
280
|
+
const appContainer = document.createElement('div')
|
|
281
|
+
appContainer.appendChild(makePanelEl('freshPanel'))
|
|
264
282
|
const { mapProvider, capturedHandlers } = setup({
|
|
265
|
-
panelRegistry: { getPanelConfig: jest.fn(() => ({ freshPanel: { visibleGeometry: freshGeometry, desktop: { slot: '
|
|
283
|
+
panelRegistry: { getPanelConfig: jest.fn(() => ({ freshPanel: { visibleGeometry: freshGeometry, desktop: { slot: 'left-top' } } })) },
|
|
284
|
+
layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
|
|
266
285
|
})
|
|
267
286
|
renderHook(() => useVisibleGeometry())
|
|
268
287
|
capturedHandlers['app:panelopened']({ panelId: 'freshPanel' })
|
|
@@ -271,13 +290,16 @@ describe('useVisibleGeometry', () => {
|
|
|
271
290
|
})
|
|
272
291
|
|
|
273
292
|
test('falls back to config when panel not in panelConfig and registry returns null', () => {
|
|
293
|
+
const appContainer = document.createElement('div')
|
|
294
|
+
appContainer.appendChild(makePanelEl('missingPanel'))
|
|
274
295
|
const { mapProvider, capturedHandlers } = setup({
|
|
275
296
|
panelConfig: {}, // panel not present
|
|
276
|
-
panelRegistry: { getPanelConfig: jest.fn(() => null) } // registry returns null
|
|
297
|
+
panelRegistry: { getPanelConfig: jest.fn(() => null) }, // registry returns null
|
|
298
|
+
layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
|
|
277
299
|
})
|
|
278
300
|
|
|
279
301
|
renderHook(() => useVisibleGeometry())
|
|
280
|
-
capturedHandlers['app:panelopened']({ panelId: 'missingPanel', visibleGeometry: polygonFeature, slot: '
|
|
302
|
+
capturedHandlers['app:panelopened']({ panelId: 'missingPanel', visibleGeometry: polygonFeature, slot: 'left-top' })
|
|
281
303
|
jest.runAllTimers()
|
|
282
304
|
// Should still call fitToBounds using visibleGeometry from event payload
|
|
283
305
|
expect(mapProvider.fitToBounds).toHaveBeenCalledWith(polygonFeature)
|
|
@@ -285,11 +307,14 @@ describe('useVisibleGeometry', () => {
|
|
|
285
307
|
|
|
286
308
|
test('uses visibleGeometry from event payload directly, bypassing registry (ADD_PANEL first-click case)', () => {
|
|
287
309
|
// Registry is empty — simulates first ADD_PANEL before React has processed the reducer
|
|
310
|
+
const appContainer = document.createElement('div')
|
|
311
|
+
appContainer.appendChild(makePanelEl('newPanel'))
|
|
288
312
|
const { mapProvider, capturedHandlers } = setup({
|
|
289
|
-
panelRegistry: { getPanelConfig: jest.fn(() => ({})) }
|
|
313
|
+
panelRegistry: { getPanelConfig: jest.fn(() => ({})) },
|
|
314
|
+
layoutRefs: { mainRef: { current: document.createElement('div') }, appContainerRef: { current: appContainer } }
|
|
290
315
|
})
|
|
291
316
|
renderHook(() => useVisibleGeometry())
|
|
292
|
-
capturedHandlers['app:panelopened']({ panelId: 'newPanel', slot: '
|
|
317
|
+
capturedHandlers['app:panelopened']({ panelId: 'newPanel', slot: 'left-top', visibleGeometry: polygonFeature })
|
|
293
318
|
jest.runAllTimers()
|
|
294
319
|
expect(mapProvider.fitToBounds).toHaveBeenCalledWith(polygonFeature)
|
|
295
320
|
})
|
|
@@ -63,20 +63,25 @@ export const Layout = () => {
|
|
|
63
63
|
<SlotRenderer slot={layoutSlots.TOP_RIGHT} />
|
|
64
64
|
</div>
|
|
65
65
|
</div>
|
|
66
|
-
<div className='im-o-
|
|
67
|
-
<
|
|
66
|
+
<div className='im-o-app__left' ref={layoutRefs.leftRef}>
|
|
67
|
+
<div className='im-o-app__left-top' ref={layoutRefs.leftTopRef}>
|
|
68
|
+
<SlotRenderer slot={layoutSlots.LEFT_TOP} />
|
|
69
|
+
</div>
|
|
70
|
+
<div className='im-o-app__left-bottom' ref={layoutRefs.leftBottomRef}>
|
|
71
|
+
<SlotRenderer slot={layoutSlots.LEFT_BOTTOM} />
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div className='im-o-app__middle' ref={layoutRefs.middleRef}>
|
|
75
|
+
<SlotRenderer slot={layoutSlots.MIDDLE} />
|
|
68
76
|
</div>
|
|
69
77
|
<div className='im-o-app__right' ref={layoutRefs.rightRef}>
|
|
70
|
-
<div className='im-o-app__right-top'>
|
|
78
|
+
<div className='im-o-app__right-top' ref={layoutRefs.rightTopRef}>
|
|
71
79
|
<SlotRenderer slot={layoutSlots.RIGHT_TOP} />
|
|
72
80
|
</div>
|
|
73
81
|
<div className='im-o-app__right-bottom' ref={layoutRefs.rightBottomRef}>
|
|
74
82
|
<SlotRenderer slot={layoutSlots.RIGHT_BOTTOM} />
|
|
75
83
|
</div>
|
|
76
84
|
</div>
|
|
77
|
-
<div className='im-o-app__middle' ref={layoutRefs.middleRef}>
|
|
78
|
-
<SlotRenderer slot={layoutSlots.MIDDLE} />
|
|
79
|
-
</div>
|
|
80
85
|
<div className='im-o-app__footer' ref={layoutRefs.footerRef}>
|
|
81
86
|
<div className='im-o-app__footer-col'>
|
|
82
87
|
<Logo />
|
|
@@ -139,12 +139,49 @@
|
|
|
139
139
|
flex-direction: column;
|
|
140
140
|
position: absolute;
|
|
141
141
|
gap: var(--divider-gap);
|
|
142
|
-
top: var(--
|
|
142
|
+
top: var(--left-offset-top);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
// ---------------------------------------------------
|
|
146
|
+
// Left: Buttons and panels
|
|
147
|
+
// ---------------------------------------------------
|
|
148
|
+
|
|
149
|
+
.im-o-app__left {
|
|
150
|
+
position: absolute;
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-direction: column;
|
|
153
|
+
left: var(--primary-gap);
|
|
154
|
+
top: var(--left-offset-top);
|
|
155
|
+
bottom: var(--left-offset-bottom);
|
|
156
|
+
gap: var(--divider-gap);
|
|
157
|
+
|
|
158
|
+
& > *:empty {
|
|
159
|
+
display: none;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
163
|
+
transition: bottom 0.15s ease;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.im-o-app__left-top {
|
|
168
|
+
display: flex;
|
|
169
|
+
flex-direction: column;
|
|
170
|
+
align-items: flex-start;
|
|
171
|
+
min-height: 0;
|
|
172
|
+
position: relative;
|
|
173
|
+
gap: var(--divider-gap);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.im-o-app__left-bottom {
|
|
177
|
+
display: flex;
|
|
178
|
+
flex-direction: column;
|
|
179
|
+
align-items: flex-start;
|
|
180
|
+
justify-content: flex-end;
|
|
181
|
+
min-height: 0;
|
|
182
|
+
margin-top: auto;
|
|
183
|
+
position: relative;
|
|
184
|
+
gap: var(--divider-gap);
|
|
148
185
|
}
|
|
149
186
|
|
|
150
187
|
// ---------------------------------------------------
|
|
@@ -164,7 +201,7 @@
|
|
|
164
201
|
}
|
|
165
202
|
|
|
166
203
|
// ---------------------------------------------------
|
|
167
|
-
// Right: Buttons and
|
|
204
|
+
// Right: Buttons and panels
|
|
168
205
|
// ---------------------------------------------------
|
|
169
206
|
|
|
170
207
|
.im-o-app__right {
|
|
@@ -174,18 +211,30 @@
|
|
|
174
211
|
right: var(--primary-gap);
|
|
175
212
|
top: var(--right-offset-top);
|
|
176
213
|
bottom: var(--right-offset-bottom);
|
|
214
|
+
gap: var(--divider-gap);
|
|
215
|
+
|
|
216
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
217
|
+
transition: bottom 0.15s ease;
|
|
218
|
+
}
|
|
177
219
|
}
|
|
178
220
|
|
|
179
221
|
.im-o-app__right-top {
|
|
180
222
|
display: flex;
|
|
181
223
|
flex-direction: column;
|
|
182
|
-
flex
|
|
224
|
+
flex: 0 0 auto;
|
|
183
225
|
align-items: flex-end;
|
|
226
|
+
position: relative;
|
|
184
227
|
gap: var(--divider-gap);
|
|
185
228
|
}
|
|
186
229
|
|
|
187
230
|
.im-o-app__right-bottom {
|
|
231
|
+
display: flex;
|
|
232
|
+
flex-direction: column;
|
|
233
|
+
flex: 0 0 auto;
|
|
188
234
|
margin-top: auto;
|
|
235
|
+
align-items: flex-end;
|
|
236
|
+
position: relative;
|
|
237
|
+
gap: var(--divider-gap);
|
|
189
238
|
}
|
|
190
239
|
|
|
191
240
|
// ---------------------------------------------------
|
|
@@ -301,10 +350,33 @@
|
|
|
301
350
|
}
|
|
302
351
|
|
|
303
352
|
.im-c-panel--inset {
|
|
304
|
-
|
|
305
|
-
left: var(--primary-gap);
|
|
353
|
+
inset: var(--modal-inset);
|
|
306
354
|
max-width: calc(100% - (var(--primary-gap) * 2));
|
|
307
|
-
max-height:
|
|
355
|
+
max-height: var(--modal-max-height);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.im-c-panel--left-top,
|
|
359
|
+
.im-c-panel.im-c-panel--left-top-button {
|
|
360
|
+
inset: var(--left-offset-top) auto auto var(--primary-gap);
|
|
361
|
+
max-height: calc(100% - var(--left-offset-top) - var(--primary-gap));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.im-c-panel--left-bottom,
|
|
365
|
+
.im-c-panel.im-c-panel--left-bottom-button {
|
|
366
|
+
inset: auto auto var(--left-offset-bottom) var(--primary-gap);
|
|
367
|
+
max-height: calc(100% - var(--left-offset-bottom) - var(--primary-gap));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.im-c-panel--right-top,
|
|
371
|
+
.im-c-panel.im-c-panel--right-top-button {
|
|
372
|
+
inset: var(--right-offset-top) var(--primary-gap) auto auto;
|
|
373
|
+
max-height: calc(100% - var(--right-offset-top) - var(--primary-gap));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.im-c-panel--right-bottom,
|
|
377
|
+
.im-c-panel.im-c-panel--right-bottom-button {
|
|
378
|
+
inset: auto var(--primary-gap) var(--right-offset-bottom) auto;
|
|
379
|
+
max-height: calc(100% - var(--right-offset-bottom) - var(--primary-gap));
|
|
308
380
|
}
|
|
309
381
|
|
|
310
382
|
.im-c-panel--middle {
|
|
@@ -323,7 +395,9 @@
|
|
|
323
395
|
|
|
324
396
|
[class*="im-c-panel--"][class*="-button"] { // Adjacent to button
|
|
325
397
|
inset: var(--modal-inset);
|
|
398
|
+
max-height: var(--modal-max-height);
|
|
326
399
|
}
|
|
400
|
+
|
|
327
401
|
}
|
|
328
402
|
|
|
329
403
|
// Mobile and tablet
|
|
@@ -378,7 +452,6 @@
|
|
|
378
452
|
width: 100%;
|
|
379
453
|
left: 0;
|
|
380
454
|
bottom: calc(var(--primary-gap) * 2);
|
|
381
|
-
padding-left: var(--offset-left);
|
|
382
455
|
|
|
383
456
|
.im-c-panel {
|
|
384
457
|
max-width: var(--action-bar-max-width);
|
|
@@ -4,7 +4,6 @@ import { useApp } from '../store/appContext.js'
|
|
|
4
4
|
import { Panel } from '../components/Panel/Panel.jsx'
|
|
5
5
|
import { resolveTargetSlot, isModeAllowed, isControlVisible, isConsumerHtml } from './slotHelpers.js'
|
|
6
6
|
import { allowedSlots } from './slots.js'
|
|
7
|
-
import { stringToKebab } from '../../utils/stringToKebab.js'
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* Maps slot names to their corresponding layout refs.
|
|
@@ -15,13 +14,20 @@ export const getSlotRef = (slot, layoutRefs) => {
|
|
|
15
14
|
banner: layoutRefs.bannerRef,
|
|
16
15
|
'top-left': layoutRefs.topLeftColRef,
|
|
17
16
|
'top-right': layoutRefs.topRightColRef,
|
|
18
|
-
|
|
17
|
+
'left-top': layoutRefs.leftTopRef,
|
|
18
|
+
'left-bottom': layoutRefs.leftBottomRef,
|
|
19
19
|
middle: layoutRefs.middleRef,
|
|
20
|
-
|
|
20
|
+
'right-top': layoutRefs.rightTopRef,
|
|
21
21
|
'right-bottom': layoutRefs.rightBottomRef,
|
|
22
|
+
bottom: layoutRefs.bottomRef,
|
|
22
23
|
actions: layoutRefs.actionsRef,
|
|
23
24
|
modal: layoutRefs.modalRef
|
|
24
25
|
}
|
|
26
|
+
if (slot?.endsWith('-button')) {
|
|
27
|
+
const el = document.querySelector(`[data-button-slot="${slot}"]`)
|
|
28
|
+
return el ? { current: el } : null
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
return slotRefMap[slot] || null
|
|
26
32
|
}
|
|
27
33
|
|
|
@@ -79,7 +85,7 @@ const PersistentPanel = ({ panelId, config, isOpen, openPanelProps, allowedModal
|
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
// 2. Slot Validation
|
|
82
|
-
const isNextToButton =
|
|
88
|
+
const isNextToButton = targetSlot.endsWith('-button')
|
|
83
89
|
const isSlotAllowed = allowedSlots.panel.includes(targetSlot) || isNextToButton
|
|
84
90
|
|
|
85
91
|
if (!isSlotAllowed) {
|
|
@@ -14,8 +14,8 @@ jest.mock('../components/Panel/Panel.jsx', () => ({
|
|
|
14
14
|
}))
|
|
15
15
|
jest.mock('./slots.js', () => ({
|
|
16
16
|
allowedSlots: {
|
|
17
|
-
panel: ['
|
|
18
|
-
control: ['
|
|
17
|
+
panel: ['left-top', 'side', 'modal', 'bottom'],
|
|
18
|
+
control: ['left-top', 'banner', 'bottom', 'actions']
|
|
19
19
|
}
|
|
20
20
|
}))
|
|
21
21
|
|
|
@@ -25,7 +25,7 @@ jest.mock('./slots.js', () => ({
|
|
|
25
25
|
*/
|
|
26
26
|
const SlotHarness = ({ layoutRefs, children }) => (
|
|
27
27
|
<div>
|
|
28
|
-
<div ref={layoutRefs.
|
|
28
|
+
<div ref={layoutRefs.leftTopRef} data-slot='left-top' />
|
|
29
29
|
<div ref={layoutRefs.sideRef} data-slot='side' />
|
|
30
30
|
<div ref={layoutRefs.modalRef} data-slot='modal' />
|
|
31
31
|
<div ref={layoutRefs.bottomRef} data-slot='bottom' />
|
|
@@ -45,7 +45,7 @@ describe('HtmlElementHost', () => {
|
|
|
45
45
|
bannerRef: { current: null },
|
|
46
46
|
topLeftColRef: { current: null },
|
|
47
47
|
topRightColRef: { current: null },
|
|
48
|
-
|
|
48
|
+
leftTopRef: { current: null },
|
|
49
49
|
middleRef: { current: null },
|
|
50
50
|
bottomRef: { current: null },
|
|
51
51
|
actionsRef: { current: null },
|
|
@@ -105,10 +105,10 @@ describe('HtmlElementHost', () => {
|
|
|
105
105
|
|
|
106
106
|
it('projects open panel into correct slot', () => {
|
|
107
107
|
const { container } = renderWithSlots({
|
|
108
|
-
panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: '
|
|
108
|
+
panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'left-top' } } },
|
|
109
109
|
openPanels: { p1: { props: {} } }
|
|
110
110
|
})
|
|
111
|
-
expect(container.querySelector('[data-slot="
|
|
111
|
+
expect(container.querySelector('[data-slot="left-top"] [data-testid="panel-p1"]')).toBeTruthy()
|
|
112
112
|
})
|
|
113
113
|
|
|
114
114
|
it('hides panel when closed and passes isOpen=false', () => {
|
|
@@ -130,19 +130,28 @@ describe('HtmlElementHost', () => {
|
|
|
130
130
|
|
|
131
131
|
it('hides panel with inline:false when not fullscreen', () => {
|
|
132
132
|
const { getByTestId } = renderWithSlots({
|
|
133
|
-
panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: '
|
|
133
|
+
panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'left-top' }, inline: false } },
|
|
134
134
|
openPanels: { p1: { props: {} } },
|
|
135
135
|
isFullscreen: false
|
|
136
136
|
})
|
|
137
137
|
expect(getByTestId('panel-p1').style.display).toBe('none')
|
|
138
138
|
})
|
|
139
139
|
|
|
140
|
-
it('
|
|
140
|
+
it('shows panel with inline:false when fullscreen', () => {
|
|
141
|
+
const { getByTestId } = renderWithSlots({
|
|
142
|
+
panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'left-top' }, inline: false } },
|
|
143
|
+
openPanels: { p1: { props: {} } },
|
|
144
|
+
isFullscreen: true
|
|
145
|
+
})
|
|
146
|
+
expect(getByTestId('panel-p1').dataset.open).toBe('true')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('resolves bottom slot to left-top on desktop', () => {
|
|
141
150
|
const { container } = renderWithSlots({
|
|
142
151
|
panelConfig: { p1: { html: '<p>Hi</p>', label: 'Test', desktop: { slot: 'bottom' } } },
|
|
143
152
|
openPanels: { p1: { props: {} } }
|
|
144
153
|
})
|
|
145
|
-
expect(container.querySelector('[data-slot="
|
|
154
|
+
expect(container.querySelector('[data-slot="left-top"] [data-testid="panel-p1"]')).toBeTruthy()
|
|
146
155
|
expect(container.querySelector('[data-slot="bottom"] [data-testid="panel-p1"]')).toBeNull()
|
|
147
156
|
})
|
|
148
157
|
|
|
@@ -169,9 +178,9 @@ describe('HtmlElementHost', () => {
|
|
|
169
178
|
|
|
170
179
|
it('projects visible control into correct slot', () => {
|
|
171
180
|
const { container } = renderWithSlots({
|
|
172
|
-
controlConfig: { c1: { id: 'c1', html: '<input type="checkbox">', desktop: { slot: '
|
|
181
|
+
controlConfig: { c1: { id: 'c1', html: '<input type="checkbox">', desktop: { slot: 'left-top' } } }
|
|
173
182
|
})
|
|
174
|
-
const control = container.querySelector('[data-slot="
|
|
183
|
+
const control = container.querySelector('[data-slot="left-top"] .im-c-control')
|
|
175
184
|
expect(control).toBeTruthy()
|
|
176
185
|
expect(control.innerHTML).toBe('<input type="checkbox">')
|
|
177
186
|
})
|
|
@@ -292,6 +301,18 @@ describe('HtmlElementHost', () => {
|
|
|
292
301
|
expect(getSlotRef('unknown-slot', {})).toBeNull()
|
|
293
302
|
})
|
|
294
303
|
|
|
304
|
+
test('getSlotRef returns wrapped element for button slot when element exists', () => {
|
|
305
|
+
const el = document.createElement('div')
|
|
306
|
+
el.dataset.buttonSlot = 'my-panel-button'
|
|
307
|
+
document.body.appendChild(el)
|
|
308
|
+
expect(getSlotRef('my-panel-button', {})).toEqual({ current: el })
|
|
309
|
+
el.remove()
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test('getSlotRef returns null for button slot when no element found', () => {
|
|
313
|
+
expect(getSlotRef('nonexistent-button', {})).toBeNull()
|
|
314
|
+
})
|
|
315
|
+
|
|
295
316
|
it('does not append child if slotRef exists but current is null', () => {
|
|
296
317
|
// 1. Setup refs where the slot exists in the map but the DOM node (current) is null
|
|
297
318
|
const incompleteRefs = {
|