@defra/interactive-map 0.0.10-alpha → 0.0.12-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/button-definition.md +21 -3
- package/docs/api/panel-definition.md +10 -12
- package/docs/api.md +80 -7
- package/docs/demo.mdx +70 -0
- package/docs/index.md +0 -4
- package/docs/plugins/plugin-context.md +3 -3
- package/docs/plugins/plugin-descriptor.md +37 -0
- package/docs/plugins/plugin-manifest.md +1 -1
- package/docusaurus.config.cjs +55 -25
- package/package.json +18 -9
- 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 +3 -3
- 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/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 +3 -3
- package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/src/manifest.js +7 -7
- 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/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/esm/index.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/index.js +1 -1
- package/plugins/search/src/Search.jsx +9 -3
- package/plugins/search/src/Search.test.jsx +26 -6
- package/plugins/search/src/components/Form/Form.jsx +35 -7
- package/plugins/search/src/components/Form/Form.module.scss +27 -0
- package/plugins/search/src/components/Form/Form.test.jsx +99 -2
- package/plugins/search/src/components/SubmitButton/SubmitButton.jsx +28 -0
- package/plugins/search/src/components/SubmitButton/SubmitButton.module.scss +8 -0
- package/plugins/search/src/components/SubmitButton/SubmitButton.test.jsx +33 -0
- package/plugins/search/src/datasets.js +15 -11
- package/plugins/search/src/datasets.test.js +17 -2
- package/plugins/search/src/events/fetchSuggestions.js +1 -1
- package/plugins/search/src/index.js +1 -1
- package/plugins/search/src/index.test.js +4 -4
- package/plugins/search/src/reducer.js +9 -4
- package/plugins/search/src/reducer.test.js +12 -7
- package/plugins/search/src/search.scss +5 -1
- package/plugins/search/src/utils/parseOsNamesResults.js +18 -2
- package/plugins/search/src/utils/parseOsNamesResults.test.js +33 -15
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/appEvents.js +8 -2
- package/providers/beta/esri/src/esriProvider.js +25 -17
- package/providers/beta/esri/src/mapEvents.js +41 -4
- package/providers/beta/esri/src/utils/coords.js +34 -1
- package/providers/beta/esri/src/utils/coords.test.js +126 -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/esm/index.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/appEvents.js +10 -1
- package/providers/maplibre/src/appEvents.test.js +13 -4
- package/providers/maplibre/src/index.js +5 -13
- package/providers/maplibre/src/index.test.js +34 -15
- package/providers/maplibre/src/mapEvents.js +9 -1
- package/providers/maplibre/src/maplibreProvider.js +25 -15
- package/providers/maplibre/src/maplibreProvider.test.js +28 -2
- package/providers/maplibre/src/utils/spatial.js +51 -0
- package/providers/maplibre/src/utils/spatial.test.js +47 -0
- package/src/App/components/Actions/Actions.module.scss +5 -4
- package/src/App/components/MapButton/MapButton.jsx +4 -16
- package/src/App/components/MapButton/MapButton.module.scss +12 -12
- package/src/App/components/MapButton/MapButton.test.jsx +0 -9
- package/src/App/components/Panel/Panel.jsx +6 -6
- package/src/App/components/Panel/Panel.test.jsx +14 -15
- package/src/App/components/Viewport/MapController.jsx +6 -1
- package/src/App/hooks/useLayoutMeasurements.js +1 -1
- package/src/App/hooks/useLayoutMeasurements.test.js +1 -1
- package/src/App/hooks/useMapProviderOverrides.js +21 -1
- package/src/App/hooks/useMapProviderOverrides.test.js +51 -2
- package/src/App/hooks/useMarkersAPI.js +5 -3
- package/src/App/hooks/useModalPanelBehaviour.js +19 -2
- package/src/App/hooks/useModalPanelBehaviour.test.js +84 -60
- package/src/App/hooks/useVisibleGeometry.js +100 -0
- package/src/App/hooks/useVisibleGeometry.test.js +331 -0
- package/src/App/layout/Layout.jsx +5 -5
- package/src/App/layout/layout.module.scss +2 -4
- package/src/App/registry/panelRegistry.js +1 -10
- package/src/App/registry/panelRegistry.test.js +6 -11
- package/src/App/renderer/HtmlElementHost.jsx +12 -3
- package/src/App/renderer/HtmlElementHost.test.jsx +89 -0
- package/src/App/renderer/mapButtons.js +128 -28
- package/src/App/renderer/mapButtons.test.js +119 -19
- package/src/App/renderer/pluginWrapper.js +3 -2
- package/src/App/renderer/slots.js +1 -1
- package/src/App/store/AppProvider.jsx +1 -0
- package/src/App/store/MapProvider.jsx +18 -5
- package/src/App/store/MapProvider.test.jsx +56 -1
- package/src/App/store/appActionsMap.js +17 -9
- package/src/App/store/appActionsMap.test.js +33 -7
- package/src/App/store/appDispatchMiddleware.js +19 -0
- package/src/App/store/appDispatchMiddleware.test.js +56 -0
- package/src/App/store/mapActionsMap.js +4 -7
- package/src/InteractiveMap/InteractiveMap.js +18 -0
- package/src/InteractiveMap/InteractiveMap.test.js +12 -0
- package/src/config/appConfig.js +17 -15
- package/src/config/events.js +41 -4
- package/src/config/getInitialOpenPanels.js +2 -2
- package/src/config/getInitialOpenPanels.test.js +7 -7
- package/src/types.js +22 -11
- package/src/utils/getValueForStyle.js +1 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react'
|
|
2
|
+
import { useVisibleGeometry, getGeometryType, getPointCoordinates } from './useVisibleGeometry'
|
|
3
|
+
import { useConfig } from '../store/configContext.js'
|
|
4
|
+
import { useApp } from '../store/appContext.js'
|
|
5
|
+
|
|
6
|
+
jest.mock('../store/configContext.js')
|
|
7
|
+
jest.mock('../store/appContext.js')
|
|
8
|
+
|
|
9
|
+
const pointFeature = { type: 'Feature', geometry: { type: 'Point', coordinates: [1, 51] }, properties: {} }
|
|
10
|
+
const multiPointFeature = { type: 'Feature', geometry: { type: 'MultiPoint', coordinates: [[1, 51], [2, 52]] }, properties: {} }
|
|
11
|
+
const polygonFeature = { type: 'Feature', geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 0]]] }, properties: {} }
|
|
12
|
+
|
|
13
|
+
const insetPanelRect = { left: 600, top: 0, right: 1000, bottom: 800, width: 400, height: 800 }
|
|
14
|
+
const bottomPanelRect = { left: 0, top: 500, right: 1000, bottom: 800, width: 1000, height: 300 }
|
|
15
|
+
|
|
16
|
+
const setup = (overrides = {}) => {
|
|
17
|
+
const capturedHandlers = {}
|
|
18
|
+
const mapProvider = {
|
|
19
|
+
isGeometryObscured: jest.fn(() => true),
|
|
20
|
+
fitToBounds: jest.fn(),
|
|
21
|
+
setView: jest.fn(),
|
|
22
|
+
...overrides.mapProvider
|
|
23
|
+
}
|
|
24
|
+
const eventBus = {
|
|
25
|
+
on: jest.fn((event, handler) => { capturedHandlers[event] = handler }),
|
|
26
|
+
off: jest.fn(),
|
|
27
|
+
...overrides.eventBus
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const insetEl = document.createElement('div')
|
|
31
|
+
insetEl.getBoundingClientRect = jest.fn(() => insetPanelRect)
|
|
32
|
+
const bottomEl = document.createElement('div')
|
|
33
|
+
bottomEl.getBoundingClientRect = jest.fn(() => bottomPanelRect)
|
|
34
|
+
|
|
35
|
+
const layoutRefs = {
|
|
36
|
+
mainRef: { current: document.createElement('div') },
|
|
37
|
+
insetRef: { current: insetEl },
|
|
38
|
+
bottomRef: { current: bottomEl },
|
|
39
|
+
...overrides.layoutRefs
|
|
40
|
+
}
|
|
41
|
+
const panelConfig = {
|
|
42
|
+
myPanel: { visibleGeometry: polygonFeature, desktop: { slot: 'inset' } },
|
|
43
|
+
emptyPanel: {},
|
|
44
|
+
...overrides.panelConfig
|
|
45
|
+
}
|
|
46
|
+
const panelRegistry = {
|
|
47
|
+
getPanelConfig: jest.fn(() => panelConfig),
|
|
48
|
+
...overrides.panelRegistry
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
useConfig.mockReturnValue({ mapProvider, eventBus, ...overrides.config })
|
|
52
|
+
useApp.mockReturnValue({ layoutRefs, panelConfig, panelRegistry, breakpoint: 'desktop', ...overrides.app })
|
|
53
|
+
|
|
54
|
+
return { mapProvider, eventBus, capturedHandlers, layoutRefs, panelConfig, insetEl, bottomEl }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('useVisibleGeometry', () => {
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
jest.clearAllMocks()
|
|
60
|
+
jest.useFakeTimers()
|
|
61
|
+
})
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
jest.useRealTimers()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('early returns when mapProvider is null', () => {
|
|
67
|
+
setup({ config: { mapProvider: null } })
|
|
68
|
+
const { result } = renderHook(() => useVisibleGeometry())
|
|
69
|
+
expect(result.error).toBeUndefined()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('early returns when eventBus is null', () => {
|
|
73
|
+
setup({ config: { eventBus: null } })
|
|
74
|
+
const { result } = renderHook(() => useVisibleGeometry())
|
|
75
|
+
expect(result.error).toBeUndefined()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('subscribes to APP_PANEL_OPENED on eventBus', () => {
|
|
79
|
+
const { eventBus } = setup()
|
|
80
|
+
renderHook(() => useVisibleGeometry())
|
|
81
|
+
expect(eventBus.on).toHaveBeenCalledWith('app:panelopened', expect.any(Function))
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('unsubscribes from APP_PANEL_OPENED on unmount', () => {
|
|
85
|
+
const { eventBus } = setup()
|
|
86
|
+
const { unmount } = renderHook(() => useVisibleGeometry())
|
|
87
|
+
unmount()
|
|
88
|
+
expect(eventBus.off).toHaveBeenCalledWith('app:panelopened', expect.any(Function))
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('does nothing when panel has no visibleGeometry', () => {
|
|
92
|
+
const { mapProvider, capturedHandlers } = setup()
|
|
93
|
+
renderHook(() => useVisibleGeometry())
|
|
94
|
+
capturedHandlers['app:panelopened']({ panelId: 'emptyPanel' })
|
|
95
|
+
expect(mapProvider.isGeometryObscured).not.toHaveBeenCalled()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('does nothing when panel does not exist in config', () => {
|
|
99
|
+
const { mapProvider, capturedHandlers } = setup()
|
|
100
|
+
renderHook(() => useVisibleGeometry())
|
|
101
|
+
capturedHandlers['app:panelopened']({ panelId: 'unknownPanel' })
|
|
102
|
+
expect(mapProvider.isGeometryObscured).not.toHaveBeenCalled()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('does nothing when panel has visibleGeometry but no slot config', () => {
|
|
106
|
+
const { mapProvider, capturedHandlers } = setup({
|
|
107
|
+
panelConfig: { noSlotPanel: { visibleGeometry: polygonFeature } }
|
|
108
|
+
})
|
|
109
|
+
renderHook(() => useVisibleGeometry())
|
|
110
|
+
capturedHandlers['app:panelopened']({ panelId: 'noSlotPanel' })
|
|
111
|
+
expect(mapProvider.isGeometryObscured).not.toHaveBeenCalled()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('does nothing when mapProvider has no isGeometryObscured method', () => {
|
|
115
|
+
const { capturedHandlers, mapProvider } = setup({ mapProvider: { isGeometryObscured: null, fitToBounds: jest.fn(), setView: jest.fn() } })
|
|
116
|
+
renderHook(() => useVisibleGeometry())
|
|
117
|
+
capturedHandlers['app:panelopened']({ panelId: 'myPanel' })
|
|
118
|
+
expect(mapProvider.fitToBounds).not.toHaveBeenCalled()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('does nothing when slot ref has zero dimensions (panel not visible)', () => {
|
|
122
|
+
const zeroEl = document.createElement('div')
|
|
123
|
+
zeroEl.getBoundingClientRect = jest.fn(() => ({ left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }))
|
|
124
|
+
const { mapProvider, capturedHandlers } = setup({
|
|
125
|
+
layoutRefs: {
|
|
126
|
+
mainRef: { current: document.createElement('div') },
|
|
127
|
+
insetRef: { current: zeroEl }
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
renderHook(() => useVisibleGeometry())
|
|
131
|
+
capturedHandlers['app:panelopened']({ panelId: 'myPanel' })
|
|
132
|
+
|
|
133
|
+
// Run the current pending animation frame
|
|
134
|
+
jest.runOnlyPendingTimers()
|
|
135
|
+
|
|
136
|
+
expect(mapProvider.isGeometryObscured).not.toHaveBeenCalled()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('does nothing when geometry is not obscured', () => {
|
|
140
|
+
const { mapProvider, capturedHandlers } = setup({ mapProvider: { isGeometryObscured: jest.fn(() => false), fitToBounds: jest.fn(), setView: jest.fn() } })
|
|
141
|
+
renderHook(() => useVisibleGeometry())
|
|
142
|
+
capturedHandlers['app:panelopened']({ panelId: 'myPanel' })
|
|
143
|
+
jest.runAllTimers()
|
|
144
|
+
expect(mapProvider.fitToBounds).not.toHaveBeenCalled()
|
|
145
|
+
expect(mapProvider.setView).not.toHaveBeenCalled()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('calls fitToBounds with visibleGeometry for non-point geometry when obscured', () => {
|
|
149
|
+
const { mapProvider, capturedHandlers } = setup()
|
|
150
|
+
renderHook(() => useVisibleGeometry())
|
|
151
|
+
capturedHandlers['app:panelopened']({ panelId: 'myPanel' })
|
|
152
|
+
jest.runAllTimers()
|
|
153
|
+
|
|
154
|
+
expect(mapProvider.isGeometryObscured).toHaveBeenCalledWith(polygonFeature, insetPanelRect)
|
|
155
|
+
expect(mapProvider.fitToBounds).toHaveBeenCalledWith(polygonFeature)
|
|
156
|
+
expect(mapProvider.setView).not.toHaveBeenCalled()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('calls setView with center for Point geometry when obscured', () => {
|
|
160
|
+
const { mapProvider, capturedHandlers } = setup({
|
|
161
|
+
panelConfig: { pointPanel: { visibleGeometry: pointFeature, desktop: { slot: 'inset' } } }
|
|
162
|
+
})
|
|
163
|
+
renderHook(() => useVisibleGeometry())
|
|
164
|
+
capturedHandlers['app:panelopened']({ panelId: 'pointPanel' })
|
|
165
|
+
jest.runAllTimers()
|
|
166
|
+
|
|
167
|
+
expect(mapProvider.setView).toHaveBeenCalledWith({ center: [1, 51] })
|
|
168
|
+
expect(mapProvider.fitToBounds).not.toHaveBeenCalled()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('calls setView with first coordinate for MultiPoint geometry when obscured', () => {
|
|
172
|
+
const { mapProvider, capturedHandlers } = setup({
|
|
173
|
+
panelConfig: { mpPanel: { visibleGeometry: multiPointFeature, desktop: { slot: 'inset' } } }
|
|
174
|
+
})
|
|
175
|
+
renderHook(() => useVisibleGeometry())
|
|
176
|
+
capturedHandlers['app:panelopened']({ panelId: 'mpPanel' })
|
|
177
|
+
jest.runAllTimers()
|
|
178
|
+
|
|
179
|
+
expect(mapProvider.setView).toHaveBeenCalledWith({ center: [1, 51] })
|
|
180
|
+
expect(mapProvider.fitToBounds).not.toHaveBeenCalled()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('calls fitToBounds for a raw non-Feature geometry (e.g. Polygon) when obscured', () => {
|
|
184
|
+
const rawPolygon = { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 0]]] }
|
|
185
|
+
const { mapProvider, capturedHandlers } = setup({
|
|
186
|
+
panelConfig: { geoPanel: { visibleGeometry: rawPolygon, desktop: { slot: 'inset' } } }
|
|
187
|
+
})
|
|
188
|
+
renderHook(() => useVisibleGeometry())
|
|
189
|
+
capturedHandlers['app:panelopened']({ panelId: 'geoPanel' })
|
|
190
|
+
jest.runAllTimers()
|
|
191
|
+
expect(mapProvider.fitToBounds).toHaveBeenCalledWith(rawPolygon)
|
|
192
|
+
expect(mapProvider.setView).not.toHaveBeenCalled()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('calls setView for a raw Point geometry (not Feature-wrapped) when obscured', () => {
|
|
196
|
+
const rawPoint = { type: 'Point', coordinates: [1, 51] }
|
|
197
|
+
const { mapProvider, capturedHandlers } = setup({
|
|
198
|
+
panelConfig: { rawPointPanel: { visibleGeometry: rawPoint, desktop: { slot: 'inset' } } }
|
|
199
|
+
})
|
|
200
|
+
renderHook(() => useVisibleGeometry())
|
|
201
|
+
capturedHandlers['app:panelopened']({ panelId: 'rawPointPanel' })
|
|
202
|
+
jest.runAllTimers()
|
|
203
|
+
expect(mapProvider.setView).toHaveBeenCalledWith({ center: [1, 51] })
|
|
204
|
+
expect(mapProvider.fitToBounds).not.toHaveBeenCalled()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('does not call setView when Point feature has null coordinates', () => {
|
|
208
|
+
const nullCoordsFeature = { type: 'Feature', geometry: { type: 'Point', coordinates: null }, properties: {} }
|
|
209
|
+
const { mapProvider, capturedHandlers } = setup({
|
|
210
|
+
panelConfig: { nullPanel: { visibleGeometry: nullCoordsFeature, desktop: { slot: 'inset' } } }
|
|
211
|
+
})
|
|
212
|
+
renderHook(() => useVisibleGeometry())
|
|
213
|
+
capturedHandlers['app:panelopened']({ panelId: 'nullPanel' })
|
|
214
|
+
jest.runAllTimers()
|
|
215
|
+
expect(mapProvider.setView).not.toHaveBeenCalled()
|
|
216
|
+
expect(mapProvider.fitToBounds).not.toHaveBeenCalled()
|
|
217
|
+
})
|
|
218
|
+
|
|
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
|
+
test('uses latest panelConfig via ref when it changes between renders', () => {
|
|
232
|
+
const { mapProvider, capturedHandlers, insetEl } = setup()
|
|
233
|
+
const { rerender } = renderHook(() => useVisibleGeometry())
|
|
234
|
+
|
|
235
|
+
const updatedGeometry = { type: 'Feature', geometry: { type: 'LineString', coordinates: [[0, 0], [1, 1]] }, properties: {} }
|
|
236
|
+
const updatedPanelConfig = { myPanel: { visibleGeometry: updatedGeometry, desktop: { slot: 'inset' } } }
|
|
237
|
+
useApp.mockReturnValue({
|
|
238
|
+
layoutRefs: { mainRef: { current: document.createElement('div') }, insetRef: { current: insetEl } },
|
|
239
|
+
panelConfig: updatedPanelConfig,
|
|
240
|
+
panelRegistry: { getPanelConfig: jest.fn(() => updatedPanelConfig) },
|
|
241
|
+
breakpoint: 'desktop'
|
|
242
|
+
})
|
|
243
|
+
rerender()
|
|
244
|
+
|
|
245
|
+
capturedHandlers['app:panelopened']({ panelId: 'myPanel' })
|
|
246
|
+
jest.runAllTimers()
|
|
247
|
+
expect(mapProvider.fitToBounds).toHaveBeenCalledWith(updatedGeometry)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('uses slot from event payload when registry config lacks slot info', () => {
|
|
251
|
+
const freshGeometry = { type: 'Feature', geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 0]]] }, properties: {} }
|
|
252
|
+
const { mapProvider, capturedHandlers } = setup({
|
|
253
|
+
panelRegistry: { getPanelConfig: jest.fn(() => ({ freshPanel: { visibleGeometry: freshGeometry } })) }
|
|
254
|
+
})
|
|
255
|
+
renderHook(() => useVisibleGeometry())
|
|
256
|
+
// Event includes slot (as middleware provides for ADD_PANEL); registry config has no slot info
|
|
257
|
+
capturedHandlers['app:panelopened']({ panelId: 'freshPanel', slot: 'inset' })
|
|
258
|
+
jest.runAllTimers()
|
|
259
|
+
expect(mapProvider.fitToBounds).toHaveBeenCalledWith(freshGeometry)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('falls back to panelRegistry for panels not yet in stale panelConfig', () => {
|
|
263
|
+
const freshGeometry = { type: 'Feature', geometry: { type: 'Polygon', coordinates: [[[0, 0], [1, 0], [1, 1], [0, 0]]] }, properties: {} }
|
|
264
|
+
const { mapProvider, capturedHandlers } = setup({
|
|
265
|
+
panelRegistry: { getPanelConfig: jest.fn(() => ({ freshPanel: { visibleGeometry: freshGeometry, desktop: { slot: 'inset' } } })) }
|
|
266
|
+
})
|
|
267
|
+
renderHook(() => useVisibleGeometry())
|
|
268
|
+
capturedHandlers['app:panelopened']({ panelId: 'freshPanel' })
|
|
269
|
+
jest.runAllTimers()
|
|
270
|
+
expect(mapProvider.fitToBounds).toHaveBeenCalledWith(freshGeometry)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
test('falls back to config when panel not in panelConfig and registry returns null', () => {
|
|
274
|
+
const { mapProvider, capturedHandlers } = setup({
|
|
275
|
+
panelConfig: {}, // panel not present
|
|
276
|
+
panelRegistry: { getPanelConfig: jest.fn(() => null) } // registry returns null
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
renderHook(() => useVisibleGeometry())
|
|
280
|
+
capturedHandlers['app:panelopened']({ panelId: 'missingPanel', visibleGeometry: polygonFeature, slot: 'inset' })
|
|
281
|
+
jest.runAllTimers()
|
|
282
|
+
// Should still call fitToBounds using visibleGeometry from event payload
|
|
283
|
+
expect(mapProvider.fitToBounds).toHaveBeenCalledWith(polygonFeature)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
test('uses visibleGeometry from event payload directly, bypassing registry (ADD_PANEL first-click case)', () => {
|
|
287
|
+
// Registry is empty — simulates first ADD_PANEL before React has processed the reducer
|
|
288
|
+
const { mapProvider, capturedHandlers } = setup({
|
|
289
|
+
panelRegistry: { getPanelConfig: jest.fn(() => ({})) }
|
|
290
|
+
})
|
|
291
|
+
renderHook(() => useVisibleGeometry())
|
|
292
|
+
capturedHandlers['app:panelopened']({ panelId: 'newPanel', slot: 'inset', visibleGeometry: polygonFeature })
|
|
293
|
+
jest.runAllTimers()
|
|
294
|
+
expect(mapProvider.fitToBounds).toHaveBeenCalledWith(polygonFeature)
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
describe('getGeometryType', () => {
|
|
299
|
+
test('returns null for falsy input', () => {
|
|
300
|
+
expect(getGeometryType(null)).toBeNull()
|
|
301
|
+
expect(getGeometryType(undefined)).toBeNull()
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('returns geometry type for a Feature', () => {
|
|
305
|
+
expect(getGeometryType({ type: 'Feature', geometry: { type: 'Polygon' } })).toBe('Polygon')
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('returns type directly for a raw geometry', () => {
|
|
309
|
+
expect(getGeometryType({ type: 'Point' })).toBe('Point')
|
|
310
|
+
expect(getGeometryType({ type: 'FeatureCollection' })).toBe('FeatureCollection')
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
describe('getPointCoordinates', () => {
|
|
315
|
+
test('returns null for unrecognised geometry type', () => {
|
|
316
|
+
expect(getPointCoordinates({ type: 'Polygon', coordinates: [] })).toBeNull()
|
|
317
|
+
expect(getPointCoordinates({ type: 'LineString', coordinates: [] })).toBeNull()
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
test('returns coordinates for a Point', () => {
|
|
321
|
+
expect(getPointCoordinates(pointFeature.geometry)).toEqual(pointFeature.geometry.coordinates)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test('returns first coordinate for a MultiPoint', () => {
|
|
325
|
+
expect(getPointCoordinates(multiPointFeature.geometry)).toEqual(multiPointFeature.geometry.coordinates[0])
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test('recurses into Feature geometry', () => {
|
|
329
|
+
expect(getPointCoordinates(pointFeature)).toEqual(pointFeature.geometry.coordinates)
|
|
330
|
+
})
|
|
331
|
+
})
|
|
@@ -70,7 +70,7 @@ export const Layout = () => {
|
|
|
70
70
|
<div className='im-o-app__right-top'>
|
|
71
71
|
<SlotRenderer slot={layoutSlots.RIGHT_TOP} />
|
|
72
72
|
</div>
|
|
73
|
-
<div className='im-o-app__right-bottom'>
|
|
73
|
+
<div className='im-o-app__right-bottom' ref={layoutRefs.rightBottomRef}>
|
|
74
74
|
<SlotRenderer slot={layoutSlots.RIGHT_BOTTOM} />
|
|
75
75
|
</div>
|
|
76
76
|
</div>
|
|
@@ -94,13 +94,13 @@ export const Layout = () => {
|
|
|
94
94
|
<div className='im-o-app__actions' ref={layoutRefs.actionsRef}>
|
|
95
95
|
<SlotRenderer slot={layoutSlots.ACTIONS} />
|
|
96
96
|
</div>
|
|
97
|
+
<div className='im-o-app__modal' ref={layoutRefs.modalRef}>
|
|
98
|
+
<SlotRenderer slot={layoutSlots.MODAL} />
|
|
99
|
+
<div className='im-o-app__modal-backdrop' />
|
|
100
|
+
</div>
|
|
97
101
|
</div>
|
|
98
102
|
</div>
|
|
99
103
|
<HtmlElementHost />
|
|
100
|
-
<div className='im-o-app__modal' ref={layoutRefs.modalRef}>
|
|
101
|
-
<SlotRenderer slot={layoutSlots.MODAL} />
|
|
102
|
-
<div className='im-o-app__modal-backdrop' />
|
|
103
|
-
</div>
|
|
104
104
|
</div>
|
|
105
105
|
)
|
|
106
106
|
}
|
|
@@ -261,6 +261,7 @@
|
|
|
261
261
|
display: flex;
|
|
262
262
|
position: absolute;
|
|
263
263
|
top: 0;
|
|
264
|
+
left: 0;
|
|
264
265
|
width: 100%;
|
|
265
266
|
height: 100%;
|
|
266
267
|
z-index: 99;
|
|
@@ -321,10 +322,7 @@
|
|
|
321
322
|
}
|
|
322
323
|
|
|
323
324
|
[class*="im-c-panel--"][class*="-button"] { // Adjacent to button
|
|
324
|
-
|
|
325
|
-
right: var(--modal-inset);
|
|
326
|
-
bottom: var(--modal-inset);
|
|
327
|
-
left: var(--modal-inset);
|
|
325
|
+
inset: var(--modal-inset);
|
|
328
326
|
}
|
|
329
327
|
}
|
|
330
328
|
|
|
@@ -4,16 +4,7 @@ import { deepMerge } from '../../utils/deepMerge.js'
|
|
|
4
4
|
|
|
5
5
|
// Pure utility functions for panel registry operations
|
|
6
6
|
export const registerPanel = (currentConfig, panel) => {
|
|
7
|
-
|
|
8
|
-
Object.entries(panel).map(([key, value]) => [
|
|
9
|
-
key,
|
|
10
|
-
{
|
|
11
|
-
showLabel: true,
|
|
12
|
-
...value
|
|
13
|
-
}
|
|
14
|
-
])
|
|
15
|
-
)
|
|
16
|
-
return { ...currentConfig, ...normalizedPanelConfig }
|
|
7
|
+
return { ...currentConfig, ...panel }
|
|
17
8
|
}
|
|
18
9
|
|
|
19
10
|
export const addPanel = (currentConfig, id, config) => {
|
|
@@ -2,31 +2,28 @@ import { createPanelRegistry, registerPanel, addPanel, removePanel, getPanelConf
|
|
|
2
2
|
import { defaultPanelConfig } from '../../config/appConfig.js'
|
|
3
3
|
|
|
4
4
|
describe('panelRegistry', () => {
|
|
5
|
-
test('registerPanel should store a panel
|
|
5
|
+
test('registerPanel should store a panel', () => {
|
|
6
6
|
const panel = { settings: { title: 'Settings Panel' } }
|
|
7
7
|
const config = registerPanel({}, panel)
|
|
8
8
|
expect(config).toEqual({
|
|
9
9
|
settings: {
|
|
10
|
-
title: 'Settings Panel'
|
|
11
|
-
showLabel: true
|
|
10
|
+
title: 'Settings Panel'
|
|
12
11
|
}
|
|
13
12
|
})
|
|
14
13
|
})
|
|
15
14
|
|
|
16
15
|
test('registerPanel should merge multiple panels', () => {
|
|
17
16
|
const panel1 = { settings: { title: 'Settings Panel' } }
|
|
18
|
-
const panel2 = { dashboard: { title: 'Dashboard Panel'
|
|
17
|
+
const panel2 = { dashboard: { title: 'Dashboard Panel' } }
|
|
19
18
|
let config = {}
|
|
20
19
|
config = registerPanel(config, panel1)
|
|
21
20
|
config = registerPanel(config, panel2)
|
|
22
21
|
expect(config).toEqual({
|
|
23
22
|
settings: {
|
|
24
|
-
title: 'Settings Panel'
|
|
25
|
-
showLabel: true
|
|
23
|
+
title: 'Settings Panel'
|
|
26
24
|
},
|
|
27
25
|
dashboard: {
|
|
28
|
-
title: 'Dashboard Panel'
|
|
29
|
-
showLabel: false
|
|
26
|
+
title: 'Dashboard Panel'
|
|
30
27
|
}
|
|
31
28
|
})
|
|
32
29
|
})
|
|
@@ -37,8 +34,7 @@ describe('panelRegistry', () => {
|
|
|
37
34
|
const result = getPanelConfig(config)
|
|
38
35
|
expect(result).toEqual({
|
|
39
36
|
reports: {
|
|
40
|
-
title: 'Reports Panel'
|
|
41
|
-
showLabel: true
|
|
37
|
+
title: 'Reports Panel'
|
|
42
38
|
}
|
|
43
39
|
})
|
|
44
40
|
})
|
|
@@ -100,7 +96,6 @@ describe('panelRegistry', () => {
|
|
|
100
96
|
// Test registerPanel state
|
|
101
97
|
registry.registerPanel({ p1: { title: 'P1' } })
|
|
102
98
|
expect(registry.getPanelConfig()).toHaveProperty('p1')
|
|
103
|
-
expect(registry.getPanelConfig().p1.showLabel).toBe(true)
|
|
104
99
|
|
|
105
100
|
// Test addPanel state and return value
|
|
106
101
|
const added = registry.addPanel('p2', { title: 'P2' })
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/App/renderer/HtmlElementHost.jsx
|
|
2
|
-
import React, { useRef,
|
|
2
|
+
import React, { useRef, useLayoutEffect, useMemo } from 'react'
|
|
3
3
|
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'
|
|
@@ -18,6 +18,7 @@ export const getSlotRef = (slot, layoutRefs) => {
|
|
|
18
18
|
inset: layoutRefs.insetRef,
|
|
19
19
|
middle: layoutRefs.middleRef,
|
|
20
20
|
bottom: layoutRefs.bottomRef,
|
|
21
|
+
'right-bottom': layoutRefs.rightBottomRef,
|
|
21
22
|
actions: layoutRefs.actionsRef,
|
|
22
23
|
modal: layoutRefs.modalRef
|
|
23
24
|
}
|
|
@@ -31,16 +32,24 @@ export const getSlotRef = (slot, layoutRefs) => {
|
|
|
31
32
|
* (e.g. the banner slot swaps DOM nodes between mobile and desktop).
|
|
32
33
|
*/
|
|
33
34
|
export const useDomProjection = (wrapperRef, targetSlot, isVisible, layoutRefs, breakpoint) => {
|
|
34
|
-
|
|
35
|
+
useLayoutEffect(() => {
|
|
35
36
|
const wrapper = wrapperRef.current
|
|
36
37
|
|
|
37
38
|
if (isVisible) {
|
|
38
39
|
const slotRef = getSlotRef(targetSlot, layoutRefs)
|
|
39
40
|
if (slotRef?.current) {
|
|
40
|
-
slotRef.current.
|
|
41
|
+
const backdrop = slotRef.current.querySelector(':scope > .im-o-app__modal-backdrop')
|
|
42
|
+
if (backdrop) {
|
|
43
|
+
slotRef.current.insertBefore(wrapper, backdrop)
|
|
44
|
+
} else {
|
|
45
|
+
slotRef.current.appendChild(wrapper)
|
|
46
|
+
}
|
|
41
47
|
wrapper.style.display = ''
|
|
42
48
|
}
|
|
43
49
|
} else {
|
|
50
|
+
if (wrapper.parentElement === layoutRefs.modalRef?.current) {
|
|
51
|
+
layoutRefs.appContainerRef?.current?.appendChild(wrapper)
|
|
52
|
+
}
|
|
44
53
|
wrapper.style.display = 'none'
|
|
45
54
|
}
|
|
46
55
|
|
|
@@ -199,6 +199,95 @@ describe('HtmlElementHost', () => {
|
|
|
199
199
|
expect(container.firstChild).toBeNull() // still renders safely
|
|
200
200
|
})
|
|
201
201
|
|
|
202
|
+
it('inserts panel before backdrop when modal slot contains a backdrop element', () => {
|
|
203
|
+
const modalRef = React.createRef()
|
|
204
|
+
const appContainerRef = React.createRef()
|
|
205
|
+
const refs = { ...layoutRefs, modalRef, appContainerRef }
|
|
206
|
+
|
|
207
|
+
mockApp({
|
|
208
|
+
panelConfig: { p1: { html: '<p>Hi</p>', label: 'Menu', desktop: { slot: 'side', modal: true } } },
|
|
209
|
+
openPanels: { p1: { props: {} } },
|
|
210
|
+
layoutRefs: refs
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
render(
|
|
214
|
+
<div>
|
|
215
|
+
<div ref={appContainerRef}>
|
|
216
|
+
<div ref={modalRef}>
|
|
217
|
+
<div className='im-o-app__modal-backdrop' />
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
<HtmlElementHost />
|
|
221
|
+
</div>
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
const children = Array.from(modalRef.current.children)
|
|
225
|
+
const panelIndex = children.findIndex(el => el.dataset.testid === 'panel-p1')
|
|
226
|
+
const backdropIndex = children.findIndex(el => el.classList.contains('im-o-app__modal-backdrop'))
|
|
227
|
+
|
|
228
|
+
expect(panelIndex).toBeGreaterThanOrEqual(0)
|
|
229
|
+
expect(panelIndex).toBeLessThan(backdropIndex)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('moves panel from modal to appContainerRef when hidden', () => {
|
|
233
|
+
// Create refs
|
|
234
|
+
const modalRef = React.createRef()
|
|
235
|
+
const appContainerRef = React.createRef()
|
|
236
|
+
const refs = { ...layoutRefs, modalRef, appContainerRef }
|
|
237
|
+
|
|
238
|
+
// Panel config: modal slot
|
|
239
|
+
const panelConfig = {
|
|
240
|
+
p1: { html: '<p>Hi</p>', label: 'Menu', desktop: { slot: 'modal', modal: true } }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// App state: panel initially open
|
|
244
|
+
mockApp({
|
|
245
|
+
panelConfig,
|
|
246
|
+
openPanels: { p1: { props: {} } },
|
|
247
|
+
layoutRefs: refs
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// Render a harness that includes both refs
|
|
251
|
+
const { rerender } = render(
|
|
252
|
+
<div>
|
|
253
|
+
<div ref={appContainerRef} />
|
|
254
|
+
<div ref={modalRef} />
|
|
255
|
+
<HtmlElementHost />
|
|
256
|
+
</div>
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
// Now it should actually be inside modalRef
|
|
260
|
+
const panelInModal = modalRef.current.querySelector('[data-testid="panel-p1"]')
|
|
261
|
+
expect(panelInModal).toBeTruthy()
|
|
262
|
+
expect(modalRef.current.contains(panelInModal)).toBe(true)
|
|
263
|
+
|
|
264
|
+
// Close the panel
|
|
265
|
+
useApp.mockReturnValue({
|
|
266
|
+
breakpoint: 'desktop',
|
|
267
|
+
mode: 'view',
|
|
268
|
+
isFullscreen: false,
|
|
269
|
+
panelConfig,
|
|
270
|
+
controlConfig: {},
|
|
271
|
+
openPanels: {}, // closed now
|
|
272
|
+
layoutRefs: refs,
|
|
273
|
+
dispatch: jest.fn()
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
rerender(
|
|
277
|
+
<div>
|
|
278
|
+
<div ref={appContainerRef} />
|
|
279
|
+
<div ref={modalRef} />
|
|
280
|
+
<HtmlElementHost />
|
|
281
|
+
</div>
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
// It should have moved to appContainerRef
|
|
285
|
+
const panelInApp = appContainerRef.current.querySelector('[data-testid="panel-p1"]')
|
|
286
|
+
expect(panelInApp).toBeTruthy()
|
|
287
|
+
expect(appContainerRef.current.contains(panelInApp)).toBe(true)
|
|
288
|
+
expect(modalRef.current.contains(panelInApp)).toBe(false)
|
|
289
|
+
})
|
|
290
|
+
|
|
202
291
|
test('getSlotRef returns null for unknown slot', () => {
|
|
203
292
|
expect(getSlotRef('unknown-slot', {})).toBeNull()
|
|
204
293
|
})
|