@defra/interactive-map 0.0.11-alpha → 0.0.14-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/plugins/plugin-descriptor.md +37 -0
- package/package.json +15 -6
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/src/events.js +4 -14
- package/plugins/beta/draw-ml/src/modes/createDrawMode.js +1 -3
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +28 -6
- package/plugins/interact/src/InteractInit.test.js +19 -5
- package/plugins/interact/src/events.js +17 -15
- package/plugins/interact/src/events.test.js +25 -16
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/src/events/fetchSuggestions.js +9 -6
- package/providers/beta/esri/dist/css/index.css +4 -0
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/esriProvider.js +19 -3
- package/providers/beta/esri/src/esriProvider.scss +5 -0
- package/providers/beta/esri/src/mapEvents.js +34 -3
- package/providers/beta/esri/src/utils/coords.js +1 -0
- package/providers/beta/esri/src/utils/spatial.js +47 -1
- package/providers/beta/esri/src/utils/spatial.test.js +55 -0
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/src/maplibreProvider.js +12 -1
- package/providers/maplibre/src/maplibreProvider.test.js +14 -1
- package/providers/maplibre/src/utils/spatial.js +40 -0
- package/providers/maplibre/src/utils/spatial.test.js +35 -0
- package/src/App/components/MapButton/MapButton.jsx +1 -0
- package/src/App/components/Panel/Panel.jsx +14 -13
- package/src/App/components/Viewport/MapController.jsx +4 -0
- package/src/App/hooks/useLayoutMeasurements.js +37 -20
- package/src/App/hooks/useLayoutMeasurements.test.js +38 -6
- package/src/App/hooks/useMarkersAPI.js +5 -3
- package/src/App/hooks/useModalPanelBehaviour.js +91 -10
- package/src/App/hooks/useModalPanelBehaviour.test.js +185 -53
- package/src/App/hooks/useVisibleGeometry.js +100 -0
- package/src/App/hooks/useVisibleGeometry.test.js +331 -0
- package/src/App/layout/Layout.jsx +13 -5
- package/src/App/layout/layout.module.scss +149 -13
- package/src/App/renderer/HtmlElementHost.jsx +10 -2
- package/src/App/renderer/HtmlElementHost.test.jsx +12 -0
- package/src/App/renderer/SlotRenderer.jsx +1 -1
- package/src/App/renderer/mapPanels.js +1 -2
- package/src/App/renderer/pluginWrapper.js +3 -2
- package/src/App/renderer/slots.js +12 -6
- package/src/App/store/AppProvider.jsx +6 -1
- package/src/App/store/appDispatchMiddleware.js +19 -0
- package/src/App/store/appDispatchMiddleware.test.js +56 -0
- package/src/InteractiveMap/InteractiveMap.js +3 -3
- package/src/types.js +9 -0
- package/src/utils/getSafeZoneInset.js +12 -9
- package/src/utils/getSafeZoneInset.test.js +102 -58
|
@@ -6,9 +6,11 @@ export const layoutSlots = Object.freeze({
|
|
|
6
6
|
TOP_MIDDLE: 'top-middle',
|
|
7
7
|
TOP_RIGHT: 'top-right',
|
|
8
8
|
INSET: 'inset',
|
|
9
|
+
LEFT_TOP: 'left-top',
|
|
10
|
+
LEFT_BOTTOM: 'left-bottom',
|
|
11
|
+
MIDDLE: 'middle',
|
|
9
12
|
RIGHT_TOP: 'right-top',
|
|
10
13
|
RIGHT_BOTTOM: 'right-bottom',
|
|
11
|
-
MIDDLE: 'middle',
|
|
12
14
|
FOOTER_RIGHT: 'footer-right',
|
|
13
15
|
BOTTOM: 'bottom',
|
|
14
16
|
ACTIONS: 'actions',
|
|
@@ -29,17 +31,21 @@ export const allowedSlots = Object.freeze({
|
|
|
29
31
|
panel: [
|
|
30
32
|
layoutSlots.SIDE,
|
|
31
33
|
layoutSlots.BANNER,
|
|
32
|
-
layoutSlots.INSET,
|
|
34
|
+
layoutSlots.INSET, // Deprecate
|
|
35
|
+
layoutSlots.LEFT_TOP,
|
|
36
|
+
layoutSlots.LEFT_BOTTOM,
|
|
33
37
|
layoutSlots.MIDDLE,
|
|
34
|
-
layoutSlots.
|
|
35
|
-
layoutSlots.
|
|
36
|
-
layoutSlots.
|
|
37
|
-
layoutSlots.MODAL
|
|
38
|
+
layoutSlots.RIGHT_TOP,
|
|
39
|
+
layoutSlots.RIGHT_BOTTOM,
|
|
40
|
+
layoutSlots.BOTTOM, // Typicaly on mobile
|
|
41
|
+
layoutSlots.MODAL // Internal only
|
|
38
42
|
],
|
|
39
43
|
button: [
|
|
40
44
|
layoutSlots.TOP_LEFT,
|
|
41
45
|
layoutSlots.TOP_MIDDLE,
|
|
42
46
|
layoutSlots.TOP_RIGHT,
|
|
47
|
+
layoutSlots.LEFT_TOP,
|
|
48
|
+
layoutSlots.LEFT_BOTTOM,
|
|
43
49
|
layoutSlots.RIGHT_TOP,
|
|
44
50
|
layoutSlots.RIGHT_BOTTOM,
|
|
45
51
|
layoutSlots.ACTIONS
|
|
@@ -20,8 +20,13 @@ export const AppProvider = ({ options, children }) => {
|
|
|
20
20
|
topLeftColRef: useRef(null),
|
|
21
21
|
topRightColRef: useRef(null),
|
|
22
22
|
insetRef: useRef(null),
|
|
23
|
-
|
|
23
|
+
leftRef: useRef(null),
|
|
24
|
+
leftTopRef: useRef(null),
|
|
25
|
+
leftBottomRef: useRef(null),
|
|
24
26
|
middleRef: useRef(null),
|
|
27
|
+
rightRef: useRef(null),
|
|
28
|
+
rightTopRef: useRef(null),
|
|
29
|
+
rightBottomRef: useRef(null),
|
|
25
30
|
bottomRef: useRef(null),
|
|
26
31
|
footerRef: useRef(null),
|
|
27
32
|
actionsRef: useRef(null),
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// src/App/store/dispatchMiddleware.js
|
|
2
2
|
import { EVENTS as events } from '../../config/events.js'
|
|
3
|
+
import { defaultPanelConfig } from '../../config/appConfig.js'
|
|
4
|
+
import { deepMerge } from '../../utils/deepMerge.js'
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Determines which panels were implicitly closed when opening a new panel
|
|
@@ -78,4 +80,21 @@ export function handleActionSideEffects (action, previousState, panelConfig, eve
|
|
|
78
80
|
eventBus.emit(events.APP_PANEL_OPENED, { panelId, props })
|
|
79
81
|
})
|
|
80
82
|
}
|
|
83
|
+
|
|
84
|
+
if (type === 'ADD_PANEL') {
|
|
85
|
+
const { id, config } = payload
|
|
86
|
+
const mergedConfig = deepMerge(defaultPanelConfig, config)
|
|
87
|
+
const bpConfig = mergedConfig[previousState.breakpoint]
|
|
88
|
+
if (bpConfig?.open) {
|
|
89
|
+
queueMicrotask(() => {
|
|
90
|
+
const slot = bpConfig.slot
|
|
91
|
+
const { visibleGeometry } = mergedConfig
|
|
92
|
+
const eventPayload = { panelId: id, slot }
|
|
93
|
+
if (visibleGeometry) {
|
|
94
|
+
eventPayload.visibleGeometry = visibleGeometry
|
|
95
|
+
}
|
|
96
|
+
eventBus.emit(events.APP_PANEL_OPENED, eventPayload)
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
81
100
|
}
|
|
@@ -177,4 +177,60 @@ describe('appDispatchMiddleware', () => {
|
|
|
177
177
|
)
|
|
178
178
|
})
|
|
179
179
|
})
|
|
180
|
+
|
|
181
|
+
describe('ADD_PANEL', () => {
|
|
182
|
+
it('emits APP_PANEL_OPENED with slot when panel opens by default', async () => {
|
|
183
|
+
run(
|
|
184
|
+
{ type: 'ADD_PANEL', payload: { id: 'newPanel', config: {} } },
|
|
185
|
+
{ breakpoint: 'desktop' }
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
await flushMicrotasks()
|
|
189
|
+
|
|
190
|
+
expect(eventBus.emit).toHaveBeenCalledWith(
|
|
191
|
+
events.APP_PANEL_OPENED,
|
|
192
|
+
{ panelId: 'newPanel', slot: 'inset' }
|
|
193
|
+
)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('emits APP_PANEL_OPENED with visibleGeometry when provided in config', async () => {
|
|
197
|
+
const visibleGeometry = { type: 'Feature', geometry: { type: 'Point', coordinates: [1, 2] }, properties: {} }
|
|
198
|
+
run(
|
|
199
|
+
{ type: 'ADD_PANEL', payload: { id: 'geoPanel', config: { visibleGeometry } } },
|
|
200
|
+
{ breakpoint: 'desktop' }
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
await flushMicrotasks()
|
|
204
|
+
|
|
205
|
+
expect(eventBus.emit).toHaveBeenCalledWith(
|
|
206
|
+
events.APP_PANEL_OPENED,
|
|
207
|
+
{ panelId: 'geoPanel', slot: 'inset', visibleGeometry }
|
|
208
|
+
)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('does not emit APP_PANEL_OPENED when breakpoint config sets open: false', async () => {
|
|
212
|
+
run(
|
|
213
|
+
{ type: 'ADD_PANEL', payload: { id: 'hiddenPanel', config: { desktop: { open: false } } } },
|
|
214
|
+
{ breakpoint: 'desktop' }
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
await flushMicrotasks()
|
|
218
|
+
|
|
219
|
+
expect(eventBus.emit).not.toHaveBeenCalled()
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('emits APP_PANEL_OPENED with slot for mobile breakpoint', async () => {
|
|
223
|
+
run(
|
|
224
|
+
{ type: 'ADD_PANEL', payload: { id: 'mobilePanel', config: {} } },
|
|
225
|
+
{ breakpoint: 'mobile' }
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
await flushMicrotasks()
|
|
229
|
+
|
|
230
|
+
expect(eventBus.emit).toHaveBeenCalledWith(
|
|
231
|
+
events.APP_PANEL_OPENED,
|
|
232
|
+
{ panelId: 'mobilePanel', slot: 'bottom' }
|
|
233
|
+
)
|
|
234
|
+
})
|
|
235
|
+
})
|
|
180
236
|
})
|
|
@@ -408,10 +408,10 @@ export default class InteractiveMap {
|
|
|
408
408
|
/**
|
|
409
409
|
* Fit the map view to a bounding box or GeoJSON geometry, respecting the safe zone padding.
|
|
410
410
|
*
|
|
411
|
-
* @param {[number, number, number, number] | object}
|
|
411
|
+
* @param {[number, number, number, number] | object} target - Bounds as [west, south, east, north] or [minX, minY, maxX, maxY] depending on the crs, or a GeoJSON Feature, FeatureCollection, or geometry.
|
|
412
412
|
*/
|
|
413
|
-
fitToBounds (
|
|
414
|
-
this.eventBus.emit(events.MAP_FIT_TO_BOUNDS,
|
|
413
|
+
fitToBounds (target) {
|
|
414
|
+
this.eventBus.emit(events.MAP_FIT_TO_BOUNDS, target)
|
|
415
415
|
}
|
|
416
416
|
|
|
417
417
|
/**
|
package/src/types.js
CHANGED
|
@@ -247,6 +247,10 @@
|
|
|
247
247
|
*
|
|
248
248
|
* @property {() => void} [clearHighlightedLabel]
|
|
249
249
|
* @experimental Clear any highlighted label.
|
|
250
|
+
*
|
|
251
|
+
* @property {(geojson: object, panelRect: DOMRect) => boolean} [isGeometryObscured]
|
|
252
|
+
* Returns true if the geometry's screen bounding box overlaps the given panel element rectangle.
|
|
253
|
+
* Used internally by useVisibleGeometry to decide whether to pan/zoom when a panel opens.
|
|
250
254
|
*/
|
|
251
255
|
|
|
252
256
|
/**
|
|
@@ -373,6 +377,11 @@
|
|
|
373
377
|
*
|
|
374
378
|
* @property {PanelBreakpointConfig} tablet
|
|
375
379
|
* Tablet breakpoint configuration.
|
|
380
|
+
*
|
|
381
|
+
* @property {object} [visibleGeometry]
|
|
382
|
+
* GeoJSON Feature, FeatureCollection, or geometry to keep visible when this panel opens.
|
|
383
|
+
* If any part of the geometry's bounding box is obscured by the safe zone after the panel opens,
|
|
384
|
+
* the map automatically adjusts: Point or MultiPoint geometry routes to setView(), all other types to fitToBounds().
|
|
376
385
|
*/
|
|
377
386
|
|
|
378
387
|
/**
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* @param {Object} refs - React refs for the key layout elements.
|
|
11
11
|
* @param {React.RefObject} refs.mainRef - The main content area.
|
|
12
12
|
* @param {React.RefObject} refs.insetRef - The inset panel (e.g. search results).
|
|
13
|
+
* @param {React.RefObject} refs.leftRef - The left-hand button column.
|
|
13
14
|
* @param {React.RefObject} refs.rightRef - The right-hand button column.
|
|
14
15
|
* @param {React.RefObject} refs.actionsRef - The bottom action bar.
|
|
15
16
|
* @param {React.RefObject} refs.footerRef - The footer (logo, copyright etc).
|
|
@@ -19,29 +20,31 @@
|
|
|
19
20
|
export const getSafeZoneInset = ({
|
|
20
21
|
mainRef,
|
|
21
22
|
insetRef,
|
|
23
|
+
leftRef,
|
|
22
24
|
rightRef,
|
|
23
25
|
actionsRef,
|
|
24
26
|
footerRef
|
|
25
27
|
}) => {
|
|
26
|
-
const refs = [mainRef, insetRef, rightRef, actionsRef, footerRef]
|
|
28
|
+
const refs = [mainRef, insetRef, leftRef, rightRef, actionsRef, footerRef]
|
|
27
29
|
|
|
28
30
|
if (refs.some(ref => !ref.current)) {
|
|
29
31
|
return undefined
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
const [main, inset, right, actions, footer] = refs.map(ref => ref.current)
|
|
34
|
+
const [main, inset, left, right, actions, footer] = refs.map(ref => ref.current)
|
|
33
35
|
|
|
34
36
|
const root = document.documentElement
|
|
35
37
|
const dividerGap = Number.parseInt(getComputedStyle(root).getPropertyValue('--divider-gap'), 10)
|
|
36
38
|
|
|
37
39
|
// === Safe area logic ===
|
|
38
40
|
const availableHeight = actions.offsetTop - inset.offsetTop - dividerGap
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
41
|
+
const leftOffset = left.offsetLeft + left.offsetWidth + dividerGap
|
|
42
|
+
const rightOffset = left.offsetLeft + right.offsetWidth + dividerGap
|
|
43
|
+
const availableWidth = main.offsetWidth - (leftOffset + rightOffset)
|
|
44
|
+
const insetOverlapWidth = inset.offsetWidth - leftOffset + left.offsetLeft
|
|
42
45
|
const isLandscape = availableWidth - insetOverlapWidth > availableHeight - inset.offsetHeight
|
|
43
|
-
const topOffset =
|
|
44
|
-
const
|
|
46
|
+
const topOffset = left.offsetTop + (!isLandscape && inset.offsetHeight > 0 ? inset.offsetHeight + dividerGap : 0)
|
|
47
|
+
const combinedLeftOffset = isLandscape ? Math.max(inset.offsetWidth, left.offsetWidth) + left.offsetLeft + dividerGap : rightOffset
|
|
45
48
|
const actionsOffset = main.offsetHeight - actions.offsetTop
|
|
46
49
|
const footerOffset = main.offsetHeight - footer.offsetTop
|
|
47
50
|
|
|
@@ -49,8 +52,8 @@ export const getSafeZoneInset = ({
|
|
|
49
52
|
const hasRoom = insetOverlapWidth < availableWidth / RATIO && inset.offsetHeight < availableHeight / RATIO
|
|
50
53
|
|
|
51
54
|
const top = hasRoom ? inset.offsetTop : topOffset
|
|
52
|
-
const
|
|
55
|
+
const combinedLeft = main.offsetLeft + (hasRoom ? rightOffset : combinedLeftOffset)
|
|
53
56
|
const bottom = Math.max(actionsOffset, footerOffset) + dividerGap
|
|
54
57
|
|
|
55
|
-
return { top, right: rightOffset, left, bottom }
|
|
58
|
+
return { top, right: rightOffset, left: combinedLeft, bottom }
|
|
56
59
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getSafeZoneInset } from './getSafeZoneInset'
|
|
2
2
|
|
|
3
3
|
describe('getSafeZoneInset', () => {
|
|
4
|
-
let mainRef, insetRef, rightRef,
|
|
4
|
+
let mainRef, insetRef, leftRef, rightRef, actionsRef, footerRef
|
|
5
5
|
let originalGetComputedStyle
|
|
6
6
|
|
|
7
7
|
beforeAll(() => { originalGetComputedStyle = window.getComputedStyle })
|
|
@@ -10,80 +10,124 @@ describe('getSafeZoneInset', () => {
|
|
|
10
10
|
beforeEach(() => {
|
|
11
11
|
mainRef = { current: { offsetWidth: 800, offsetHeight: 600, offsetLeft: 0 } }
|
|
12
12
|
insetRef = { current: { offsetWidth: 100, offsetHeight: 50, offsetTop: 50, offsetLeft: 20 } }
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
leftRef = { current: { offsetWidth: 50, offsetLeft: 20, offsetTop: 10 } }
|
|
14
|
+
rightRef = { current: { offsetWidth: 50, offsetLeft: 730 } }
|
|
15
15
|
actionsRef = { current: { offsetTop: 520 } }
|
|
16
|
+
footerRef = { current: { offsetTop: 550 } }
|
|
16
17
|
|
|
17
|
-
//
|
|
18
|
+
// CSS var mock
|
|
18
19
|
window.getComputedStyle = jest.fn().mockReturnValue({ getPropertyValue: () => '10' })
|
|
19
20
|
})
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
it('returns undefined if any ref.current is null', () => {
|
|
23
|
+
const result = getSafeZoneInset({
|
|
24
|
+
mainRef: { current: null },
|
|
25
|
+
insetRef,
|
|
26
|
+
leftRef,
|
|
27
|
+
rightRef,
|
|
28
|
+
actionsRef,
|
|
29
|
+
footerRef
|
|
30
|
+
})
|
|
31
|
+
expect(result).toBeUndefined()
|
|
32
|
+
})
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
it('portrait mode shifts inset below itself when it does NOT have enough vertical room', () => {
|
|
35
|
+
// Mock layout
|
|
36
|
+
mainRef.current.offsetWidth = 200
|
|
37
|
+
mainRef.current.offsetHeight = 200
|
|
38
|
+
insetRef.current.offsetWidth = 100
|
|
39
|
+
insetRef.current.offsetHeight = 50
|
|
40
|
+
insetRef.current.offsetTop = 50
|
|
41
|
+
insetRef.current.offsetLeft = 20
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
leftRef.current.offsetWidth = 50
|
|
44
|
+
leftRef.current.offsetLeft = 10
|
|
45
|
+
leftRef.current.offsetTop = 10
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
expect(result.top).toBe(insetRef.current.offsetTop)
|
|
39
|
-
})
|
|
47
|
+
rightRef.current.offsetWidth = 50
|
|
48
|
+
rightRef.current.offsetLeft = 140
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
expect(result.top).toBe(insetRef.current.offsetTop)
|
|
44
|
-
expect(result.left).toBe(80) // rightOffset = 20 + 50 + 10
|
|
45
|
-
expect(result.left).toBe(result.right) // left equals returned rightOffset
|
|
46
|
-
})
|
|
50
|
+
actionsRef.current.offsetTop = 150
|
|
51
|
+
footerRef.current.offsetTop = 180
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
const dividerGap = 10
|
|
54
|
+
|
|
55
|
+
const result = getSafeZoneInset({ mainRef, insetRef, leftRef, rightRef, actionsRef, footerRef })
|
|
56
|
+
|
|
57
|
+
// Compute expected values exactly as function would
|
|
58
|
+
const availableHeight = actionsRef.current.offsetTop - insetRef.current.offsetTop - dividerGap // 150 - 50 - 10 = 90
|
|
59
|
+
const leftOffset = leftRef.current.offsetLeft + leftRef.current.offsetWidth + dividerGap // 10 + 50 + 10 = 70
|
|
60
|
+
const rightOffset = leftRef.current.offsetLeft + rightRef.current.offsetWidth + dividerGap // 10 + 50 + 10 = 70
|
|
61
|
+
const availableWidth = mainRef.current.offsetWidth - (leftOffset + rightOffset) // 200 - (70+70) = 60
|
|
62
|
+
const insetOverlapWidth = insetRef.current.offsetWidth - leftOffset + leftRef.current.offsetLeft // 100 - 70 + 10 = 40
|
|
63
|
+
const isLandscape = availableWidth - insetOverlapWidth > availableHeight - insetRef.current.offsetHeight // 60-40 > 90-50 => 20>40 false
|
|
64
|
+
|
|
65
|
+
const topOffset = leftRef.current.offsetTop + (!isLandscape && insetRef.current.offsetHeight > 0 ? insetRef.current.offsetHeight + dividerGap : 0) // 10 + 50 +10 = 70
|
|
66
|
+
const combinedLeftOffset = isLandscape ? Math.max(insetRef.current.offsetWidth, leftRef.current.offsetWidth) + leftRef.current.offsetLeft + dividerGap : rightOffset // isLandscape=false -> 70
|
|
67
|
+
const actionsOffset = mainRef.current.offsetHeight - actionsRef.current.offsetTop // 200-150=50
|
|
68
|
+
const footerOffset = mainRef.current.offsetHeight - footerRef.current.offsetTop // 200-180=20
|
|
69
|
+
const hasRoom = insetOverlapWidth < availableWidth / 2 && insetRef.current.offsetHeight < availableHeight / 2 // 40 < 60/2? 40<30 false
|
|
70
|
+
|
|
71
|
+
const expectedTop = hasRoom ? insetRef.current.offsetTop : topOffset // false -> topOffset = 70
|
|
72
|
+
const expectedLeft = mainRef.current.offsetLeft + (hasRoom ? rightOffset : combinedLeftOffset) // 0+70=70
|
|
73
|
+
const expectedRight = rightOffset // 70
|
|
74
|
+
const expectedBottom = Math.max(actionsOffset, footerOffset) + dividerGap // max(50,20)+10 = 60
|
|
75
|
+
|
|
76
|
+
expect(result.top).toBe(expectedTop)
|
|
77
|
+
expect(result.left).toBe(expectedLeft)
|
|
78
|
+
expect(result.right).toBe(expectedRight)
|
|
79
|
+
expect(result.bottom).toBe(expectedBottom)
|
|
53
80
|
})
|
|
54
81
|
|
|
55
|
-
it('
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
insetRef.current.offsetWidth = 100
|
|
82
|
+
it('landscape mode places inset beside panel when enough room', () => {
|
|
83
|
+
mainRef.current.offsetWidth = 1000
|
|
84
|
+
insetRef.current.offsetWidth = 200
|
|
59
85
|
insetRef.current.offsetHeight = 50
|
|
60
|
-
insetRef.current.offsetTop = 50
|
|
61
|
-
window.getComputedStyle = jest.fn().mockReturnValue({ getPropertyValue: () => '10' })
|
|
62
86
|
|
|
63
|
-
const result = getSafeZoneInset({ mainRef, insetRef,
|
|
87
|
+
const result = getSafeZoneInset({ mainRef, insetRef, leftRef, rightRef, actionsRef, footerRef })
|
|
88
|
+
|
|
89
|
+
const dividerGap = 10
|
|
90
|
+
const leftOffset = leftRef.current.offsetLeft + leftRef.current.offsetWidth + dividerGap
|
|
91
|
+
const rightOffset = leftRef.current.offsetLeft + rightRef.current.offsetWidth + dividerGap
|
|
92
|
+
const availableWidth = mainRef.current.offsetWidth - (leftOffset + rightOffset)
|
|
93
|
+
const insetOverlapWidth = insetRef.current.offsetWidth - leftOffset + leftRef.current.offsetLeft
|
|
94
|
+
const availableHeight = actionsRef.current.offsetTop - insetRef.current.offsetTop - dividerGap
|
|
95
|
+
const isLandscape = availableWidth - insetOverlapWidth > availableHeight - insetRef.current.offsetHeight
|
|
96
|
+
|
|
97
|
+
const topOffset = leftRef.current.offsetTop + (!isLandscape && insetRef.current.offsetHeight > 0 ? insetRef.current.offsetHeight + dividerGap : 0)
|
|
98
|
+
const combinedLeftOffset = isLandscape ? Math.max(insetRef.current.offsetWidth, leftRef.current.offsetWidth) + leftRef.current.offsetLeft + dividerGap : rightOffset
|
|
99
|
+
const actionsOffset = mainRef.current.offsetHeight - actionsRef.current.offsetTop
|
|
100
|
+
const footerOffset = mainRef.current.offsetHeight - footerRef.current.offsetTop
|
|
101
|
+
const hasRoom = insetOverlapWidth < availableWidth / 2 && insetRef.current.offsetHeight < availableHeight / 2
|
|
102
|
+
const top = hasRoom ? insetRef.current.offsetTop : topOffset
|
|
103
|
+
const combinedLeft = mainRef.current.offsetLeft + (hasRoom ? rightOffset : combinedLeftOffset)
|
|
104
|
+
const bottom = Math.max(actionsOffset, footerOffset) + dividerGap
|
|
64
105
|
|
|
65
|
-
|
|
66
|
-
expect(result.
|
|
67
|
-
|
|
68
|
-
expect(result.
|
|
69
|
-
expect(result.right).toBe(80)
|
|
106
|
+
expect(result.top).toBe(top)
|
|
107
|
+
expect(result.left).toBe(combinedLeft)
|
|
108
|
+
expect(result.right).toBe(rightOffset)
|
|
109
|
+
expect(result.bottom).toBe(bottom)
|
|
70
110
|
})
|
|
71
111
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*/
|
|
77
|
-
it('returns undefined if any ref.current is null (unattached)', () => {
|
|
78
|
-
const unattachedRefs = {
|
|
79
|
-
mainRef: { current: null },
|
|
80
|
-
insetRef: { current: null },
|
|
81
|
-
rightRef: { current: null },
|
|
82
|
-
actionsRef: { current: null },
|
|
83
|
-
footerRef: { current: null }
|
|
84
|
-
}
|
|
112
|
+
it('portrait mode with zero inset height leaves top unchanged', () => {
|
|
113
|
+
insetRef.current.offsetHeight = 0
|
|
114
|
+
mainRef.current.offsetWidth = 500
|
|
115
|
+
insetRef.current.offsetWidth = 100
|
|
85
116
|
|
|
86
|
-
const result = getSafeZoneInset(
|
|
87
|
-
expect(result).
|
|
117
|
+
const result = getSafeZoneInset({ mainRef, insetRef, leftRef, rightRef, actionsRef, footerRef })
|
|
118
|
+
expect(result.top).toBe(insetRef.current.offsetTop)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('calculates correct bottom using max of actions and footer offsets', () => {
|
|
122
|
+
mainRef.current.offsetHeight = 600
|
|
123
|
+
actionsRef.current.offsetTop = 500
|
|
124
|
+
footerRef.current.offsetTop = 550
|
|
125
|
+
|
|
126
|
+
const result = getSafeZoneInset({ mainRef, insetRef, leftRef, rightRef, actionsRef, footerRef })
|
|
127
|
+
|
|
128
|
+
const dividerGap = 10
|
|
129
|
+
const expectedBottom = Math.max(mainRef.current.offsetHeight - actionsRef.current.offsetTop,
|
|
130
|
+
mainRef.current.offsetHeight - footerRef.current.offsetTop) + dividerGap
|
|
131
|
+
expect(result.bottom).toBe(expectedBottom)
|
|
88
132
|
})
|
|
89
133
|
})
|