@defra/interactive-map 0.0.14-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 +1 -1
- 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 +1 -1
- package/plugins/search/src/events/fetchSuggestions.test.js +4 -4
- package/plugins/search/src/search.scss +8 -3
- package/src/App/components/Panel/Panel.module.scss +1 -0
- package/src/App/hooks/useLayoutMeasurements.js +1 -10
- package/src/App/hooks/useLayoutMeasurements.test.js +2 -5
- package/src/App/hooks/useVisibleGeometry.js +7 -13
- package/src/App/hooks/useVisibleGeometry.test.js +72 -47
- package/src/App/layout/Layout.jsx +0 -3
- package/src/App/layout/Layout.test.jsx +0 -1
- package/src/App/layout/layout.module.scss +11 -77
- package/src/App/renderer/HtmlElementHost.jsx +0 -1
- package/src/App/renderer/HtmlElementHost.test.jsx +20 -11
- 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 +0 -3
- package/src/App/store/AppProvider.jsx +0 -1
- package/src/App/store/appDispatchMiddleware.test.js +2 -2
- package/src/config/appConfig.js +4 -4
- package/src/utils/getSafeZoneInset.js +139 -42
- package/src/utils/getSafeZoneInset.test.js +298 -122
|
@@ -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,9 +63,6 @@ export const Layout = () => {
|
|
|
63
63
|
<SlotRenderer slot={layoutSlots.TOP_RIGHT} />
|
|
64
64
|
</div>
|
|
65
65
|
</div>
|
|
66
|
-
<div className='im-o-app__inset' ref={layoutRefs.insetRef}>
|
|
67
|
-
<SlotRenderer slot={layoutSlots.INSET} />
|
|
68
|
-
</div>
|
|
69
66
|
<div className='im-o-app__left' ref={layoutRefs.leftRef}>
|
|
70
67
|
<div className='im-o-app__left-top' ref={layoutRefs.leftTopRef}>
|
|
71
68
|
<SlotRenderer slot={layoutSlots.LEFT_TOP} />
|
|
@@ -153,6 +153,11 @@
|
|
|
153
153
|
left: var(--primary-gap);
|
|
154
154
|
top: var(--left-offset-top);
|
|
155
155
|
bottom: var(--left-offset-bottom);
|
|
156
|
+
gap: var(--divider-gap);
|
|
157
|
+
|
|
158
|
+
& > *:empty {
|
|
159
|
+
display: none;
|
|
160
|
+
}
|
|
156
161
|
|
|
157
162
|
@media (prefers-reduced-motion: no-preference) {
|
|
158
163
|
transition: bottom 0.15s ease;
|
|
@@ -162,34 +167,21 @@
|
|
|
162
167
|
.im-o-app__left-top {
|
|
163
168
|
display: flex;
|
|
164
169
|
flex-direction: column;
|
|
165
|
-
|
|
166
|
-
|
|
170
|
+
align-items: flex-start;
|
|
171
|
+
min-height: 0;
|
|
167
172
|
position: relative;
|
|
168
173
|
gap: var(--divider-gap);
|
|
169
|
-
|
|
170
|
-
.im-c-panel {
|
|
171
|
-
position: absolute;
|
|
172
|
-
top: 0;
|
|
173
|
-
left: 0;
|
|
174
|
-
max-height: var(--left-top-panel-max-height);
|
|
175
|
-
}
|
|
176
174
|
}
|
|
177
175
|
|
|
178
176
|
.im-o-app__left-bottom {
|
|
179
177
|
display: flex;
|
|
180
178
|
flex-direction: column;
|
|
181
|
-
|
|
179
|
+
align-items: flex-start;
|
|
180
|
+
justify-content: flex-end;
|
|
181
|
+
min-height: 0;
|
|
182
182
|
margin-top: auto;
|
|
183
|
-
align-items: flex-end;
|
|
184
183
|
position: relative;
|
|
185
184
|
gap: var(--divider-gap);
|
|
186
|
-
|
|
187
|
-
.im-c-panel {
|
|
188
|
-
position: absolute;
|
|
189
|
-
bottom: 0;
|
|
190
|
-
left: 0;
|
|
191
|
-
max-height: var(--left-bottom-panel-max-height);
|
|
192
|
-
}
|
|
193
185
|
}
|
|
194
186
|
|
|
195
187
|
// ---------------------------------------------------
|
|
@@ -219,6 +211,7 @@
|
|
|
219
211
|
right: var(--primary-gap);
|
|
220
212
|
top: var(--right-offset-top);
|
|
221
213
|
bottom: var(--right-offset-bottom);
|
|
214
|
+
gap: var(--divider-gap);
|
|
222
215
|
|
|
223
216
|
@media (prefers-reduced-motion: no-preference) {
|
|
224
217
|
transition: bottom 0.15s ease;
|
|
@@ -232,13 +225,6 @@
|
|
|
232
225
|
align-items: flex-end;
|
|
233
226
|
position: relative;
|
|
234
227
|
gap: var(--divider-gap);
|
|
235
|
-
|
|
236
|
-
.im-c-panel {
|
|
237
|
-
position: absolute;
|
|
238
|
-
top: 0;
|
|
239
|
-
right: 0;
|
|
240
|
-
max-height: var(--right-top-panel-max-height);
|
|
241
|
-
}
|
|
242
228
|
}
|
|
243
229
|
|
|
244
230
|
.im-o-app__right-bottom {
|
|
@@ -249,57 +235,6 @@
|
|
|
249
235
|
align-items: flex-end;
|
|
250
236
|
position: relative;
|
|
251
237
|
gap: var(--divider-gap);
|
|
252
|
-
|
|
253
|
-
.im-c-panel {
|
|
254
|
-
position: absolute;
|
|
255
|
-
bottom: 0;
|
|
256
|
-
right: 0;
|
|
257
|
-
max-height: var(--right-bottom-panel-max-height);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// When both left sub-slots have panels, split the column proportionally
|
|
262
|
-
// so panels don't overlap. flex: 1 1 auto gives each sub-slot a share of the
|
|
263
|
-
// column; max-height: 100% caps the panel at its sub-slot height (not forced).
|
|
264
|
-
.im-o-app__left:has(.im-o-app__left-top > .im-c-panel:not([style*="display: none"])):has(.im-o-app__left-bottom > .im-c-panel:not([style*="display: none"])) {
|
|
265
|
-
gap: var(--divider-gap);
|
|
266
|
-
|
|
267
|
-
.im-o-app__left-top,
|
|
268
|
-
.im-o-app__left-bottom {
|
|
269
|
-
flex: 1 1 auto;
|
|
270
|
-
min-height: 0;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
.im-o-app__left-bottom {
|
|
274
|
-
margin-top: 0;
|
|
275
|
-
justify-content: flex-end;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
.im-o-app__left-top .im-c-panel,
|
|
279
|
-
.im-o-app__left-bottom .im-c-panel {
|
|
280
|
-
max-height: 100%;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Same for right column
|
|
285
|
-
.im-o-app__right:has(.im-o-app__right-top > .im-c-panel:not([style*="display: none"])):has(.im-o-app__right-bottom > .im-c-panel:not([style*="display: none"])) {
|
|
286
|
-
gap: var(--divider-gap);
|
|
287
|
-
|
|
288
|
-
.im-o-app__right-top,
|
|
289
|
-
.im-o-app__right-bottom {
|
|
290
|
-
flex: 1 1 auto;
|
|
291
|
-
min-height: 0;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
.im-o-app__right-bottom {
|
|
295
|
-
margin-top: 0;
|
|
296
|
-
justify-content: flex-end;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
.im-o-app__right-top .im-c-panel,
|
|
300
|
-
.im-o-app__right-bottom .im-c-panel {
|
|
301
|
-
max-height: 100%;
|
|
302
|
-
}
|
|
303
238
|
}
|
|
304
239
|
|
|
305
240
|
// ---------------------------------------------------
|
|
@@ -517,7 +452,6 @@
|
|
|
517
452
|
width: 100%;
|
|
518
453
|
left: 0;
|
|
519
454
|
bottom: calc(var(--primary-gap) * 2);
|
|
520
|
-
padding-left: var(--offset-left);
|
|
521
455
|
|
|
522
456
|
.im-c-panel {
|
|
523
457
|
max-width: var(--action-bar-max-width);
|
|
@@ -14,7 +14,6 @@ export const getSlotRef = (slot, layoutRefs) => {
|
|
|
14
14
|
banner: layoutRefs.bannerRef,
|
|
15
15
|
'top-left': layoutRefs.topLeftColRef,
|
|
16
16
|
'top-right': layoutRefs.topRightColRef,
|
|
17
|
-
inset: layoutRefs.insetRef,
|
|
18
17
|
'left-top': layoutRefs.leftTopRef,
|
|
19
18
|
'left-bottom': layoutRefs.leftBottomRef,
|
|
20
19
|
middle: layoutRefs.middleRef,
|
|
@@ -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
|
})
|
|
@@ -7,7 +7,7 @@ jest.mock('../registry/panelRegistry.js')
|
|
|
7
7
|
jest.mock('../registry/pluginRegistry.js', () => ({ registeredPlugins: [] }))
|
|
8
8
|
jest.mock('./pluginWrapper.js', () => ({ withPluginContexts: jest.fn((c) => c) }))
|
|
9
9
|
jest.mock('../components/Panel/Panel.jsx', () => ({ Panel: (props) => <div data-testid='panel' {...props} /> }))
|
|
10
|
-
jest.mock('./slots.js', () => ({ allowedSlots: { panel: ['header', 'modal', '
|
|
10
|
+
jest.mock('./slots.js', () => ({ allowedSlots: { panel: ['header', 'modal', 'left-top'] } }))
|
|
11
11
|
|
|
12
12
|
describe('mapPanels', () => {
|
|
13
13
|
const baseConfig = {
|
|
@@ -145,7 +145,7 @@ describe('mapPanels', () => {
|
|
|
145
145
|
expect(map()).toHaveLength(1)
|
|
146
146
|
})
|
|
147
147
|
|
|
148
|
-
it('replaces bottom slot with
|
|
148
|
+
it('replaces bottom slot with left-top on non-mobile breakpoints', () => {
|
|
149
149
|
defaultAppState.panelConfig = ({
|
|
150
150
|
p1: {
|
|
151
151
|
desktop: { slot: 'bottom' },
|
|
@@ -153,7 +153,7 @@ describe('mapPanels', () => {
|
|
|
153
153
|
}
|
|
154
154
|
})
|
|
155
155
|
|
|
156
|
-
const result = map(defaultAppState, '
|
|
156
|
+
const result = map(defaultAppState, 'left-top')
|
|
157
157
|
expect(result).toHaveLength(1)
|
|
158
158
|
expect(result[0].id).toBe('p1')
|
|
159
159
|
})
|
|
@@ -4,14 +4,14 @@ import { allowedSlots } from './slots.js'
|
|
|
4
4
|
/**
|
|
5
5
|
* Resolves the target slot for a panel based on its breakpoint config.
|
|
6
6
|
* Modal panels always render in the 'modal' slot, and the bottom slot
|
|
7
|
-
* is only available on mobile — tablet and desktop fall back to '
|
|
7
|
+
* is only available on mobile — tablet and desktop fall back to 'left-top'.
|
|
8
8
|
*/
|
|
9
9
|
export const resolveTargetSlot = (bpConfig, breakpoint) => {
|
|
10
10
|
if (bpConfig.modal) {
|
|
11
11
|
return 'modal'
|
|
12
12
|
}
|
|
13
13
|
if (bpConfig.slot === 'bottom' && ['tablet', 'desktop'].includes(breakpoint)) {
|
|
14
|
-
return '
|
|
14
|
+
return 'left-top'
|
|
15
15
|
}
|
|
16
16
|
return bpConfig.slot
|
|
17
17
|
}
|
|
@@ -7,9 +7,9 @@ describe('resolveTargetSlot', () => {
|
|
|
7
7
|
expect(resolveTargetSlot({ modal: true, slot: 'side' }, 'desktop')).toBe('modal')
|
|
8
8
|
})
|
|
9
9
|
|
|
10
|
-
it('replaces bottom with
|
|
11
|
-
expect(resolveTargetSlot({ slot: 'bottom' }, 'tablet')).toBe('
|
|
12
|
-
expect(resolveTargetSlot({ slot: 'bottom' }, 'desktop')).toBe('
|
|
10
|
+
it('replaces bottom with left-top on tablet and desktop', () => {
|
|
11
|
+
expect(resolveTargetSlot({ slot: 'bottom' }, 'tablet')).toBe('left-top')
|
|
12
|
+
expect(resolveTargetSlot({ slot: 'bottom' }, 'desktop')).toBe('left-top')
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
it('keeps bottom on mobile', () => {
|
|
@@ -5,7 +5,6 @@ export const layoutSlots = Object.freeze({
|
|
|
5
5
|
TOP_LEFT: 'top-left',
|
|
6
6
|
TOP_MIDDLE: 'top-middle',
|
|
7
7
|
TOP_RIGHT: 'top-right',
|
|
8
|
-
INSET: 'inset',
|
|
9
8
|
LEFT_TOP: 'left-top',
|
|
10
9
|
LEFT_BOTTOM: 'left-bottom',
|
|
11
10
|
MIDDLE: 'middle',
|
|
@@ -22,7 +21,6 @@ export const allowedSlots = Object.freeze({
|
|
|
22
21
|
layoutSlots.BANNER,
|
|
23
22
|
layoutSlots.TOP_LEFT,
|
|
24
23
|
layoutSlots.TOP_RIGHT,
|
|
25
|
-
layoutSlots.INSET,
|
|
26
24
|
layoutSlots.MIDDLE,
|
|
27
25
|
layoutSlots.FOOTER_RIGHT,
|
|
28
26
|
layoutSlots.BOTTOM,
|
|
@@ -31,7 +29,6 @@ export const allowedSlots = Object.freeze({
|
|
|
31
29
|
panel: [
|
|
32
30
|
layoutSlots.SIDE,
|
|
33
31
|
layoutSlots.BANNER,
|
|
34
|
-
layoutSlots.INSET, // Deprecate
|
|
35
32
|
layoutSlots.LEFT_TOP,
|
|
36
33
|
layoutSlots.LEFT_BOTTOM,
|
|
37
34
|
layoutSlots.MIDDLE,
|
|
@@ -189,7 +189,7 @@ describe('appDispatchMiddleware', () => {
|
|
|
189
189
|
|
|
190
190
|
expect(eventBus.emit).toHaveBeenCalledWith(
|
|
191
191
|
events.APP_PANEL_OPENED,
|
|
192
|
-
{ panelId: 'newPanel', slot: '
|
|
192
|
+
{ panelId: 'newPanel', slot: 'left-top' }
|
|
193
193
|
)
|
|
194
194
|
})
|
|
195
195
|
|
|
@@ -204,7 +204,7 @@ describe('appDispatchMiddleware', () => {
|
|
|
204
204
|
|
|
205
205
|
expect(eventBus.emit).toHaveBeenCalledWith(
|
|
206
206
|
events.APP_PANEL_OPENED,
|
|
207
|
-
{ panelId: 'geoPanel', slot: '
|
|
207
|
+
{ panelId: 'geoPanel', slot: 'left-top', visibleGeometry }
|
|
208
208
|
)
|
|
209
209
|
})
|
|
210
210
|
|
package/src/config/appConfig.js
CHANGED
|
@@ -120,14 +120,14 @@ export const defaultPanelConfig = {
|
|
|
120
120
|
showLabel: true
|
|
121
121
|
},
|
|
122
122
|
tablet: {
|
|
123
|
-
slot: '
|
|
123
|
+
slot: 'left-top',
|
|
124
124
|
open: true,
|
|
125
125
|
dismissible: true,
|
|
126
126
|
modal: false,
|
|
127
127
|
showLabel: true
|
|
128
128
|
},
|
|
129
129
|
desktop: {
|
|
130
|
-
slot: '
|
|
130
|
+
slot: 'left-top',
|
|
131
131
|
open: true,
|
|
132
132
|
dismissible: true,
|
|
133
133
|
modal: false,
|
|
@@ -144,10 +144,10 @@ export const defaultControlConfig = {
|
|
|
144
144
|
slot: 'bottom'
|
|
145
145
|
},
|
|
146
146
|
tablet: {
|
|
147
|
-
slot: '
|
|
147
|
+
slot: 'left-top'
|
|
148
148
|
},
|
|
149
149
|
desktop: {
|
|
150
|
-
slot: '
|
|
150
|
+
slot: 'left-top'
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|