@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
|
@@ -6,6 +6,7 @@ import { useMapStateSync } from '../../hooks/useMapStateSync'
|
|
|
6
6
|
import { useMapURLSync } from '../../hooks/useMapURLSync'
|
|
7
7
|
import { useMapAnnouncements } from '../../hooks/useMapAnnouncements'
|
|
8
8
|
import { useMapProviderOverrides } from '../../hooks/useMapProviderOverrides'
|
|
9
|
+
import { useVisibleGeometry } from '../../hooks/useVisibleGeometry'
|
|
9
10
|
import { getInitialMapState } from '../../../utils/mapStateSync'
|
|
10
11
|
import { scaleFactor } from '../../../config/appConfig'
|
|
11
12
|
import { scalePoints } from '../../../utils/scalePoints.js'
|
|
@@ -38,7 +39,8 @@ export const MapController = ({ mapContainerRef }) => {
|
|
|
38
39
|
center: initialState.center,
|
|
39
40
|
zoom: initialState.zoom,
|
|
40
41
|
bounds: initialState.bounds,
|
|
41
|
-
mapStyle
|
|
42
|
+
mapStyle,
|
|
43
|
+
mapSize
|
|
42
44
|
})
|
|
43
45
|
})
|
|
44
46
|
|
|
@@ -57,6 +59,9 @@ export const MapController = ({ mapContainerRef }) => {
|
|
|
57
59
|
// Override mapProvider functions
|
|
58
60
|
useMapProviderOverrides()
|
|
59
61
|
|
|
62
|
+
// Pan/zoom to keep visibleGeometry visible when panels open
|
|
63
|
+
useVisibleGeometry()
|
|
64
|
+
|
|
60
65
|
// Update padding when breakpoint or mapSize change
|
|
61
66
|
useEffect(() => {
|
|
62
67
|
if (!isMapReady || !syncMapPadding) {
|
|
@@ -81,7 +81,7 @@ export function useLayoutMeasurements () {
|
|
|
81
81
|
// --------------------------------
|
|
82
82
|
// 3. Recaluclate CSS vars when elements resize
|
|
83
83
|
// --------------------------------
|
|
84
|
-
useResizeObserver([bannerRef, mainRef, topRef, actionsRef, footerRef], () => {
|
|
84
|
+
useResizeObserver([bannerRef, mainRef, topRef, topLeftColRef, topRightColRef, actionsRef, footerRef], () => {
|
|
85
85
|
requestAnimationFrame(() => {
|
|
86
86
|
calculateLayout()
|
|
87
87
|
})
|
|
@@ -114,7 +114,7 @@ describe('useLayoutMeasurements', () => {
|
|
|
114
114
|
const { layoutRefs } = setup()
|
|
115
115
|
renderHook(() => useLayoutMeasurements())
|
|
116
116
|
expect(useResizeObserver).toHaveBeenCalledWith(
|
|
117
|
-
[layoutRefs.bannerRef, layoutRefs.mainRef, layoutRefs.topRef, layoutRefs.actionsRef, layoutRefs.footerRef],
|
|
117
|
+
[layoutRefs.bannerRef, layoutRefs.mainRef, layoutRefs.topRef, layoutRefs.topLeftColRef, layoutRefs.topRightColRef, layoutRefs.actionsRef, layoutRefs.footerRef],
|
|
118
118
|
expect.any(Function)
|
|
119
119
|
)
|
|
120
120
|
layoutRefs.appContainerRef.current.style.setProperty.mockClear()
|
|
@@ -2,12 +2,13 @@ import { useEffect, useRef } from 'react'
|
|
|
2
2
|
import { useConfig } from '../store/configContext.js'
|
|
3
3
|
import { useApp } from '../store/appContext.js'
|
|
4
4
|
import { useMap } from '../store/mapContext.js'
|
|
5
|
+
import { EVENTS as events } from '../../config/events.js'
|
|
5
6
|
import { getSafeZoneInset } from '../../utils/getSafeZoneInset.js'
|
|
6
7
|
import { scalePoints } from '../../utils/scalePoints.js'
|
|
7
8
|
import { scaleFactor } from '../../config/appConfig.js'
|
|
8
9
|
|
|
9
10
|
export const useMapProviderOverrides = () => {
|
|
10
|
-
const { mapProvider } = useConfig()
|
|
11
|
+
const { mapProvider, eventBus } = useConfig()
|
|
11
12
|
const { dispatch: appDispatch, layoutRefs } = useApp()
|
|
12
13
|
const { mapSize } = useMap()
|
|
13
14
|
|
|
@@ -67,4 +68,23 @@ export const useMapProviderOverrides = () => {
|
|
|
67
68
|
mapProvider.setView = originalSetView
|
|
68
69
|
}
|
|
69
70
|
}, [mapProvider, appDispatch, layoutRefs, mapSize])
|
|
71
|
+
|
|
72
|
+
// Forward public API events to the (overridden) mapProvider methods so that
|
|
73
|
+
// interactiveMap.fitToBounds() and interactiveMap.setView() respect safe zone padding.
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!mapProvider || !eventBus) {
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const handleFitToBounds = (bbox) => mapProvider.fitToBounds(bbox)
|
|
80
|
+
const handleSetView = (opts) => mapProvider.setView(opts)
|
|
81
|
+
|
|
82
|
+
eventBus.on(events.MAP_FIT_TO_BOUNDS, handleFitToBounds)
|
|
83
|
+
eventBus.on(events.MAP_SET_VIEW, handleSetView)
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
eventBus.off(events.MAP_FIT_TO_BOUNDS, handleFitToBounds)
|
|
87
|
+
eventBus.off(events.MAP_SET_VIEW, handleSetView)
|
|
88
|
+
}
|
|
89
|
+
}, [mapProvider, eventBus])
|
|
70
90
|
}
|
|
@@ -22,15 +22,21 @@ const setup = (overrides = {}) => {
|
|
|
22
22
|
setPadding: jest.fn(),
|
|
23
23
|
...overrides.mapProvider
|
|
24
24
|
}
|
|
25
|
+
const capturedHandlers = {}
|
|
26
|
+
const eventBus = {
|
|
27
|
+
on: jest.fn((event, handler) => { capturedHandlers[event] = handler }),
|
|
28
|
+
off: jest.fn(),
|
|
29
|
+
...overrides.eventBus
|
|
30
|
+
}
|
|
25
31
|
|
|
26
|
-
useConfig.mockReturnValue({ mapProvider, ...overrides.config })
|
|
32
|
+
useConfig.mockReturnValue({ mapProvider, eventBus, ...overrides.config })
|
|
27
33
|
useApp.mockReturnValue({ dispatch, layoutRefs, ...overrides.app })
|
|
28
34
|
useMap.mockReturnValue({ mapSize: 'md', ...overrides.map })
|
|
29
35
|
|
|
30
36
|
getSafeZoneInset.mockReturnValue({ top: 10, right: 5, bottom: 15, left: 5 })
|
|
31
37
|
scalePoints.mockReturnValue({ top: 20, right: 10, bottom: 30, left: 10 })
|
|
32
38
|
|
|
33
|
-
return { dispatch, layoutRefs, mapProvider }
|
|
39
|
+
return { dispatch, layoutRefs, mapProvider, eventBus, capturedHandlers }
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
describe('useMapProviderOverrides', () => {
|
|
@@ -133,4 +139,47 @@ describe('useMapProviderOverrides', () => {
|
|
|
133
139
|
|
|
134
140
|
expect(mapProvider.fitToBounds).not.toBe(firstOverride)
|
|
135
141
|
})
|
|
142
|
+
|
|
143
|
+
test('subscribes to MAP_FIT_TO_BOUNDS and MAP_SET_VIEW on eventBus', () => {
|
|
144
|
+
const { eventBus } = setup()
|
|
145
|
+
renderHook(() => useMapProviderOverrides())
|
|
146
|
+
|
|
147
|
+
expect(eventBus.on).toHaveBeenCalledWith('map:fittobounds', expect.any(Function))
|
|
148
|
+
expect(eventBus.on).toHaveBeenCalledWith('map:setview', expect.any(Function))
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('MAP_FIT_TO_BOUNDS event forwards bbox to mapProvider.fitToBounds', () => {
|
|
152
|
+
const { mapProvider, capturedHandlers } = setup()
|
|
153
|
+
const originalFitToBounds = mapProvider.fitToBounds
|
|
154
|
+
renderHook(() => useMapProviderOverrides())
|
|
155
|
+
|
|
156
|
+
capturedHandlers['map:fittobounds']([0, 0, 1, 1])
|
|
157
|
+
|
|
158
|
+
expect(originalFitToBounds).toHaveBeenCalledWith([0, 0, 1, 1])
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('MAP_SET_VIEW event forwards opts to mapProvider.setView', () => {
|
|
162
|
+
const { mapProvider, capturedHandlers } = setup()
|
|
163
|
+
const originalSetView = mapProvider.setView
|
|
164
|
+
renderHook(() => useMapProviderOverrides())
|
|
165
|
+
|
|
166
|
+
capturedHandlers['map:setview']({ center: [1, 2], zoom: 10 })
|
|
167
|
+
|
|
168
|
+
expect(originalSetView).toHaveBeenCalledWith({ center: [1, 2], zoom: 10 })
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('unsubscribes from MAP_FIT_TO_BOUNDS and MAP_SET_VIEW on unmount', () => {
|
|
172
|
+
const { eventBus } = setup()
|
|
173
|
+
const { unmount } = renderHook(() => useMapProviderOverrides())
|
|
174
|
+
|
|
175
|
+
unmount()
|
|
176
|
+
|
|
177
|
+
expect(eventBus.off).toHaveBeenCalledWith('map:fittobounds', expect.any(Function))
|
|
178
|
+
expect(eventBus.off).toHaveBeenCalledWith('map:setview', expect.any(Function))
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('skips event subscriptions when eventBus is null', () => {
|
|
182
|
+
setup({ config: { eventBus: null } })
|
|
183
|
+
expect(() => renderHook(() => useMapProviderOverrides())).not.toThrow()
|
|
184
|
+
})
|
|
136
185
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react'
|
|
2
2
|
import { useConfig } from '../store/configContext.js'
|
|
3
3
|
import { useMap } from '../store/mapContext.js'
|
|
4
4
|
import { useService } from '../store/serviceContext.js'
|
|
@@ -99,8 +99,10 @@ export const useMarkers = () => {
|
|
|
99
99
|
const { markers, dispatch, mapSize, isMapReady } = useMap()
|
|
100
100
|
const markerRefs = useRef(new Map())
|
|
101
101
|
|
|
102
|
-
// Attach add, remove, and getMarker methods to the markers store object
|
|
103
|
-
|
|
102
|
+
// Attach add, remove, and getMarker methods to the markers store object.
|
|
103
|
+
// useLayoutEffect ensures these are assigned before paint so rapid clicks can't
|
|
104
|
+
// arrive between a render (new markers object) and the async useEffect assignment.
|
|
105
|
+
useLayoutEffect(() => {
|
|
104
106
|
if (!mapProvider) {
|
|
105
107
|
return
|
|
106
108
|
}
|
|
@@ -75,11 +75,28 @@ export function useModalPanelBehaviour ({
|
|
|
75
75
|
const dividerGap = Number.parseInt(getComputedStyle(root).getPropertyValue('--divider-gap'), 10)
|
|
76
76
|
|
|
77
77
|
useResizeObserver([mainRef], () => {
|
|
78
|
-
if (!isModal || !
|
|
78
|
+
if (!isModal || !mainRef.current) {
|
|
79
79
|
return
|
|
80
80
|
}
|
|
81
|
+
|
|
82
|
+
// buttonContainerEl is only defined for button-slot panels (bpConfig.slot ends with '-button').
|
|
83
|
+
// Skip positioning for all other modal types.
|
|
84
|
+
if (buttonContainerEl === undefined) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Dynamically query the current controlling button via aria-controls to handle the case
|
|
89
|
+
// where the button has remounted after a breakpoint change (stale triggeringElement).
|
|
90
|
+
const panelElId = panelRef.current?.id
|
|
91
|
+
const currentButtonEl = panelElId ? document.querySelector(`[aria-controls="${panelElId}"]`) : null
|
|
92
|
+
const effectiveContainer = currentButtonEl?.parentElement ?? (buttonContainerEl?.isConnected ? buttonContainerEl : null)
|
|
93
|
+
|
|
94
|
+
if (!effectiveContainer) {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
81
98
|
const mainRect = mainRef.current.getBoundingClientRect()
|
|
82
|
-
const buttonRect =
|
|
99
|
+
const buttonRect = effectiveContainer.getBoundingClientRect()
|
|
83
100
|
const offsetTop = buttonRect.top - mainRect.top
|
|
84
101
|
const offsetRight = Math.round(mainRect.right - buttonRect.right + buttonRect.width + dividerGap)
|
|
85
102
|
root.style.setProperty('--modal-inset', `${offsetTop}px ${offsetRight}px auto auto`)
|
|
@@ -17,12 +17,17 @@ describe('useModalPanelBehaviour', () => {
|
|
|
17
17
|
main: { current: document.createElement('div') },
|
|
18
18
|
panel: { current: document.createElement('div') }
|
|
19
19
|
}
|
|
20
|
+
// Give panel an ID for aria-controls tests
|
|
21
|
+
refs.panel.current.id = 'modal-panel-id'
|
|
22
|
+
|
|
20
23
|
elements = {
|
|
21
24
|
buttonContainer: document.createElement('div'),
|
|
22
25
|
root: document.createElement('div')
|
|
23
26
|
}
|
|
27
|
+
|
|
24
28
|
elements.root.appendChild(refs.panel.current)
|
|
25
29
|
document.body.appendChild(elements.root)
|
|
30
|
+
|
|
26
31
|
handleClose = jest.fn()
|
|
27
32
|
jest.clearAllMocks()
|
|
28
33
|
document.documentElement.style.setProperty('--modal-inset', '')
|
|
@@ -32,13 +37,17 @@ describe('useModalPanelBehaviour', () => {
|
|
|
32
37
|
document.body.innerHTML = ''
|
|
33
38
|
})
|
|
34
39
|
|
|
35
|
-
const TestComponent = ({
|
|
40
|
+
const TestComponent = ({
|
|
41
|
+
isModal = true,
|
|
42
|
+
buttonContainerEl,
|
|
43
|
+
rootEl = elements.root
|
|
44
|
+
}) => {
|
|
36
45
|
useModalPanelBehaviour({
|
|
37
46
|
mainRef: refs.main,
|
|
38
47
|
panelRef: refs.panel,
|
|
39
48
|
isModal,
|
|
40
|
-
rootEl
|
|
41
|
-
buttonContainerEl
|
|
49
|
+
rootEl,
|
|
50
|
+
buttonContainerEl,
|
|
42
51
|
handleClose
|
|
43
52
|
})
|
|
44
53
|
return null
|
|
@@ -63,66 +72,51 @@ describe('useModalPanelBehaviour', () => {
|
|
|
63
72
|
)
|
|
64
73
|
})
|
|
65
74
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
describe('positioning (--modal-inset)', () => {
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
// Force ResizeObserver to run the callback immediately
|
|
78
|
+
useResizeObserverModule.useResizeObserver.mockImplementation((_, cb) => cb())
|
|
79
|
+
|
|
80
|
+
Object.defineProperty(refs.main.current, 'getBoundingClientRect', {
|
|
81
|
+
value: () => ({ top: 0, right: 100, bottom: 50, left: 0, width: 100, height: 50 }),
|
|
82
|
+
configurable: true
|
|
83
|
+
})
|
|
84
|
+
Object.defineProperty(elements.buttonContainer, 'getBoundingClientRect', {
|
|
85
|
+
value: () => ({ top: 10, right: 80, bottom: 40, left: 20, width: 60, height: 30 }),
|
|
86
|
+
configurable: true
|
|
87
|
+
})
|
|
70
88
|
})
|
|
71
|
-
Object.defineProperty(elements.buttonContainer, 'getBoundingClientRect', {
|
|
72
|
-
value: () => ({ top: 10, right: 80, bottom: 40, left: 20, width: 60, height: 30 })
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
render(<TestComponent />)
|
|
76
|
-
|
|
77
|
-
const inset = getComputedStyle(document.documentElement).getPropertyValue('--modal-inset')
|
|
78
|
-
expect(inset).toContain('10px')
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
describe('backdrop clicks', () => {
|
|
82
|
-
const createBackdrop = (appendTo) => {
|
|
83
|
-
const backdrop = document.createElement('div')
|
|
84
|
-
backdrop.className = 'im-o-app__modal-backdrop'
|
|
85
|
-
appendTo.appendChild(backdrop)
|
|
86
|
-
return backdrop
|
|
87
|
-
}
|
|
88
89
|
|
|
89
|
-
it('
|
|
90
|
-
|
|
90
|
+
it('hits the buttonContainerEl === undefined branch', () => {
|
|
91
|
+
refs.main.current = document.createElement('div') // mainRef must exist
|
|
91
92
|
render(<TestComponent />)
|
|
92
|
-
fireEvent.click(backdrop)
|
|
93
|
-
expect(handleClose).toHaveBeenCalled()
|
|
94
|
-
})
|
|
95
93
|
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
fireEvent.click(backdrop)
|
|
100
|
-
expect(handleClose).not.toHaveBeenCalled()
|
|
101
|
-
})
|
|
94
|
+
// Manually trigger ResizeObserver callback (if mocked)
|
|
95
|
+
const callback = useResizeObserverModule.useResizeObserver.mock.calls[0][1]
|
|
96
|
+
callback()
|
|
102
97
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
fireEvent.click(elements.root.firstChild)
|
|
107
|
-
expect(handleClose).not.toHaveBeenCalled()
|
|
98
|
+
// Expect CSS variable not set, just to assert callback ran
|
|
99
|
+
const inset = document.documentElement.style.getPropertyValue('--modal-inset')
|
|
100
|
+
expect(inset).toBe('')
|
|
108
101
|
})
|
|
109
|
-
})
|
|
110
102
|
|
|
111
|
-
|
|
112
|
-
|
|
103
|
+
it('updates --modal-inset via aria-controls when buttonContainerEl is stale', () => {
|
|
104
|
+
const button = document.createElement('button')
|
|
105
|
+
button.setAttribute('aria-controls', 'modal-panel-id')
|
|
106
|
+
elements.buttonContainer.appendChild(button)
|
|
107
|
+
document.body.appendChild(elements.buttonContainer)
|
|
113
108
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
isFullscreen: true,
|
|
117
|
-
boundaryEl: elements.root
|
|
118
|
-
})
|
|
109
|
+
const staleEl = document.createElement('div') // detached
|
|
110
|
+
render(<TestComponent buttonContainerEl={staleEl} />)
|
|
119
111
|
|
|
120
|
-
|
|
112
|
+
const inset = document.documentElement.style.getPropertyValue('--modal-inset')
|
|
113
|
+
expect(inset).toContain('10px')
|
|
114
|
+
})
|
|
121
115
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
116
|
+
it('skips update when effectiveContainer cannot be resolved', () => {
|
|
117
|
+
render(<TestComponent buttonContainerEl={null} />)
|
|
118
|
+
const inset = document.documentElement.style.getPropertyValue('--modal-inset')
|
|
119
|
+
expect(inset).toBe('')
|
|
126
120
|
})
|
|
127
121
|
})
|
|
128
122
|
|
|
@@ -138,6 +132,20 @@ describe('useModalPanelBehaviour', () => {
|
|
|
138
132
|
expect(refs.panel.current.focus).toHaveBeenCalled()
|
|
139
133
|
})
|
|
140
134
|
|
|
135
|
+
// COVERS LINE 44 (The early return branch)
|
|
136
|
+
it('does not redirect focus when focus moves completely outside the app root', () => {
|
|
137
|
+
refs.panel.current.focus = jest.fn()
|
|
138
|
+
render(<TestComponent />)
|
|
139
|
+
|
|
140
|
+
const externalEl = document.createElement('button')
|
|
141
|
+
document.body.appendChild(externalEl) // Outside elements.root
|
|
142
|
+
|
|
143
|
+
dispatchFocusIn(externalEl)
|
|
144
|
+
|
|
145
|
+
// Since isInsideApp is false, it should hit the "return" and not call focus()
|
|
146
|
+
expect(refs.panel.current.focus).not.toHaveBeenCalled()
|
|
147
|
+
})
|
|
148
|
+
|
|
141
149
|
it('does not redirect focus when focus is already inside panel', () => {
|
|
142
150
|
refs.panel.current.focus = jest.fn()
|
|
143
151
|
render(<TestComponent />)
|
|
@@ -149,23 +157,39 @@ describe('useModalPanelBehaviour', () => {
|
|
|
149
157
|
expect(refs.panel.current.focus).not.toHaveBeenCalled()
|
|
150
158
|
})
|
|
151
159
|
|
|
152
|
-
it('handles
|
|
160
|
+
it('handles null focus targets gracefully', () => {
|
|
153
161
|
render(<TestComponent />)
|
|
154
|
-
|
|
155
162
|
dispatchFocusIn(null)
|
|
163
|
+
expect(true).toBe(true)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('backdrop and inert', () => {
|
|
168
|
+
it('calls handleClose when backdrop inside rootEl is clicked', () => {
|
|
169
|
+
const backdrop = document.createElement('div')
|
|
170
|
+
backdrop.className = 'im-o-app__modal-backdrop'
|
|
171
|
+
elements.root.appendChild(backdrop)
|
|
156
172
|
|
|
157
|
-
|
|
158
|
-
|
|
173
|
+
render(<TestComponent />)
|
|
174
|
+
fireEvent.click(backdrop)
|
|
175
|
+
expect(handleClose).toHaveBeenCalled()
|
|
176
|
+
})
|
|
159
177
|
|
|
160
|
-
|
|
178
|
+
it('toggles inert elements on mount and cleanup', () => {
|
|
179
|
+
const { unmount } = render(<TestComponent />)
|
|
180
|
+
expect(toggleInertModule.toggleInertElements).toHaveBeenCalledWith(
|
|
181
|
+
expect.objectContaining({ isFullscreen: true })
|
|
182
|
+
)
|
|
183
|
+
unmount()
|
|
184
|
+
expect(toggleInertModule.toggleInertElements).toHaveBeenCalledWith(
|
|
185
|
+
expect.objectContaining({ isFullscreen: false })
|
|
186
|
+
)
|
|
161
187
|
})
|
|
162
188
|
})
|
|
163
189
|
|
|
164
190
|
it('does nothing when isModal is false', () => {
|
|
165
191
|
render(<TestComponent isModal={false} />)
|
|
166
|
-
|
|
167
192
|
fireEvent.keyDown(refs.panel.current, { key: 'Escape' })
|
|
168
193
|
expect(handleClose).not.toHaveBeenCalled()
|
|
169
|
-
expect(toggleInertModule.toggleInertElements).not.toHaveBeenCalled()
|
|
170
194
|
})
|
|
171
195
|
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { useConfig } from '../store/configContext.js'
|
|
3
|
+
import { useApp } from '../store/appContext.js'
|
|
4
|
+
import { EVENTS as events } from '../../config/events.js'
|
|
5
|
+
|
|
6
|
+
export const getGeometryType = (geojson) => {
|
|
7
|
+
if (!geojson) {
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
if (geojson.type === 'Feature') {
|
|
11
|
+
return geojson.geometry?.type
|
|
12
|
+
}
|
|
13
|
+
return geojson.type
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const isPointGeometry = (geojson) => {
|
|
17
|
+
const type = getGeometryType(geojson)
|
|
18
|
+
return type === 'Point' || type === 'MultiPoint'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const getPointCoordinates = (geojson) => {
|
|
22
|
+
if (geojson.type === 'Feature') {
|
|
23
|
+
return getPointCoordinates(geojson.geometry)
|
|
24
|
+
}
|
|
25
|
+
if (geojson.type === 'Point') {
|
|
26
|
+
return geojson.coordinates
|
|
27
|
+
}
|
|
28
|
+
if (geojson.type === 'MultiPoint') {
|
|
29
|
+
return geojson.coordinates[0]
|
|
30
|
+
}
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SLOT_REFS = {
|
|
35
|
+
inset: 'insetRef',
|
|
36
|
+
bottom: 'bottomRef',
|
|
37
|
+
side: 'sideRef'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const useVisibleGeometry = () => {
|
|
41
|
+
const { mapProvider, eventBus } = useConfig()
|
|
42
|
+
const { layoutRefs, panelConfig, panelRegistry, breakpoint } = useApp()
|
|
43
|
+
|
|
44
|
+
const latestRef = useRef({ layoutRefs, panelConfig, panelRegistry, breakpoint })
|
|
45
|
+
latestRef.current = { layoutRefs, panelConfig, panelRegistry, breakpoint }
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!mapProvider || !eventBus) {
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const handlePanelOpened = ({ panelId, slot: eventSlot, visibleGeometry: eventVisibleGeometry }) => {
|
|
53
|
+
const { panelConfig: config, panelRegistry: registry, layoutRefs: refs, breakpoint: bp } = latestRef.current
|
|
54
|
+
const resolvedConfig = config?.[panelId] ? config : (registry?.getPanelConfig() ?? config)
|
|
55
|
+
const visibleGeometry = eventVisibleGeometry ?? resolvedConfig?.[panelId]?.visibleGeometry
|
|
56
|
+
const slot = eventSlot ?? resolvedConfig?.[panelId]?.[bp]?.slot
|
|
57
|
+
const slotRef = refs[SLOT_REFS[slot]]
|
|
58
|
+
|
|
59
|
+
if (!visibleGeometry || !slotRef) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
if (typeof mapProvider.isGeometryObscured !== 'function') {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const waitForPanel = () => {
|
|
67
|
+
const panelRect = slotRef.current?.getBoundingClientRect()
|
|
68
|
+
|
|
69
|
+
if (!panelRect || panelRect.width === 0 || panelRect.height === 0) {
|
|
70
|
+
// Not ready yet, check on the next animation frame
|
|
71
|
+
requestAnimationFrame(waitForPanel)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Panel now exists and has size, safe to measure
|
|
76
|
+
if (!mapProvider.isGeometryObscured(visibleGeometry, panelRect)) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (isPointGeometry(visibleGeometry)) {
|
|
81
|
+
const center = getPointCoordinates(visibleGeometry)
|
|
82
|
+
if (center) {
|
|
83
|
+
mapProvider.setView({ center })
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
mapProvider.fitToBounds(visibleGeometry)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Start waiting for panel to exist with a measurable size
|
|
91
|
+
requestAnimationFrame(waitForPanel)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
eventBus.on(events.APP_PANEL_OPENED, handlePanelOpened)
|
|
95
|
+
|
|
96
|
+
return () => {
|
|
97
|
+
eventBus.off(events.APP_PANEL_OPENED, handlePanelOpened)
|
|
98
|
+
}
|
|
99
|
+
}, [mapProvider, eventBus])
|
|
100
|
+
}
|