@defra/interactive-map 0.0.12-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/package.json +9 -4
- 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/src/esriProvider.scss +5 -0
- package/src/App/components/MapButton/MapButton.jsx +1 -0
- package/src/App/components/Panel/Panel.jsx +14 -13
- package/src/App/hooks/useLayoutMeasurements.js +37 -20
- package/src/App/hooks/useLayoutMeasurements.test.js +38 -6
- package/src/App/hooks/useModalPanelBehaviour.js +85 -21
- package/src/App/hooks/useModalPanelBehaviour.test.js +126 -18
- package/src/App/layout/Layout.jsx +12 -4
- package/src/App/layout/layout.module.scss +148 -9
- package/src/App/renderer/HtmlElementHost.jsx +10 -3
- 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/slots.js +12 -6
- package/src/App/store/AppProvider.jsx +5 -1
- package/src/utils/getSafeZoneInset.js +12 -9
- package/src/utils/getSafeZoneInset.test.js +102 -58
|
@@ -139,12 +139,57 @@
|
|
|
139
139
|
flex-direction: column;
|
|
140
140
|
position: absolute;
|
|
141
141
|
gap: var(--divider-gap);
|
|
142
|
-
top: var(--
|
|
142
|
+
top: var(--left-offset-top);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
// ---------------------------------------------------
|
|
146
|
+
// Left: Buttons and panels
|
|
147
|
+
// ---------------------------------------------------
|
|
148
|
+
|
|
149
|
+
.im-o-app__left {
|
|
150
|
+
position: absolute;
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-direction: column;
|
|
153
|
+
left: var(--primary-gap);
|
|
154
|
+
top: var(--left-offset-top);
|
|
155
|
+
bottom: var(--left-offset-bottom);
|
|
156
|
+
|
|
157
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
158
|
+
transition: bottom 0.15s ease;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.im-o-app__left-top {
|
|
163
|
+
display: flex;
|
|
164
|
+
flex-direction: column;
|
|
165
|
+
flex: 0 0 auto;
|
|
166
|
+
align-items: flex-end;
|
|
167
|
+
position: relative;
|
|
168
|
+
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
|
+
}
|
|
177
|
+
|
|
178
|
+
.im-o-app__left-bottom {
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
flex: 0 0 auto;
|
|
182
|
+
margin-top: auto;
|
|
183
|
+
align-items: flex-end;
|
|
184
|
+
position: relative;
|
|
185
|
+
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
|
+
}
|
|
148
193
|
}
|
|
149
194
|
|
|
150
195
|
// ---------------------------------------------------
|
|
@@ -164,7 +209,7 @@
|
|
|
164
209
|
}
|
|
165
210
|
|
|
166
211
|
// ---------------------------------------------------
|
|
167
|
-
// Right: Buttons and
|
|
212
|
+
// Right: Buttons and panels
|
|
168
213
|
// ---------------------------------------------------
|
|
169
214
|
|
|
170
215
|
.im-o-app__right {
|
|
@@ -174,18 +219,87 @@
|
|
|
174
219
|
right: var(--primary-gap);
|
|
175
220
|
top: var(--right-offset-top);
|
|
176
221
|
bottom: var(--right-offset-bottom);
|
|
222
|
+
|
|
223
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
224
|
+
transition: bottom 0.15s ease;
|
|
225
|
+
}
|
|
177
226
|
}
|
|
178
227
|
|
|
179
228
|
.im-o-app__right-top {
|
|
180
229
|
display: flex;
|
|
181
230
|
flex-direction: column;
|
|
182
|
-
flex
|
|
231
|
+
flex: 0 0 auto;
|
|
183
232
|
align-items: flex-end;
|
|
233
|
+
position: relative;
|
|
184
234
|
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
|
+
}
|
|
185
242
|
}
|
|
186
243
|
|
|
187
244
|
.im-o-app__right-bottom {
|
|
245
|
+
display: flex;
|
|
246
|
+
flex-direction: column;
|
|
247
|
+
flex: 0 0 auto;
|
|
188
248
|
margin-top: auto;
|
|
249
|
+
align-items: flex-end;
|
|
250
|
+
position: relative;
|
|
251
|
+
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
|
+
}
|
|
189
303
|
}
|
|
190
304
|
|
|
191
305
|
// ---------------------------------------------------
|
|
@@ -301,10 +415,33 @@
|
|
|
301
415
|
}
|
|
302
416
|
|
|
303
417
|
.im-c-panel--inset {
|
|
304
|
-
|
|
305
|
-
left: var(--primary-gap);
|
|
418
|
+
inset: var(--modal-inset);
|
|
306
419
|
max-width: calc(100% - (var(--primary-gap) * 2));
|
|
307
|
-
max-height:
|
|
420
|
+
max-height: var(--modal-max-height);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.im-c-panel--left-top,
|
|
424
|
+
.im-c-panel.im-c-panel--left-top-button {
|
|
425
|
+
inset: var(--left-offset-top) auto auto var(--primary-gap);
|
|
426
|
+
max-height: calc(100% - var(--left-offset-top) - var(--primary-gap));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.im-c-panel--left-bottom,
|
|
430
|
+
.im-c-panel.im-c-panel--left-bottom-button {
|
|
431
|
+
inset: auto auto var(--left-offset-bottom) var(--primary-gap);
|
|
432
|
+
max-height: calc(100% - var(--left-offset-bottom) - var(--primary-gap));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.im-c-panel--right-top,
|
|
436
|
+
.im-c-panel.im-c-panel--right-top-button {
|
|
437
|
+
inset: var(--right-offset-top) var(--primary-gap) auto auto;
|
|
438
|
+
max-height: calc(100% - var(--right-offset-top) - var(--primary-gap));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.im-c-panel--right-bottom,
|
|
442
|
+
.im-c-panel.im-c-panel--right-bottom-button {
|
|
443
|
+
inset: auto var(--primary-gap) var(--right-offset-bottom) auto;
|
|
444
|
+
max-height: calc(100% - var(--right-offset-bottom) - var(--primary-gap));
|
|
308
445
|
}
|
|
309
446
|
|
|
310
447
|
.im-c-panel--middle {
|
|
@@ -323,7 +460,9 @@
|
|
|
323
460
|
|
|
324
461
|
[class*="im-c-panel--"][class*="-button"] { // Adjacent to button
|
|
325
462
|
inset: var(--modal-inset);
|
|
463
|
+
max-height: var(--modal-max-height);
|
|
326
464
|
}
|
|
465
|
+
|
|
327
466
|
}
|
|
328
467
|
|
|
329
468
|
// Mobile and tablet
|
|
@@ -4,7 +4,6 @@ import { useApp } from '../store/appContext.js'
|
|
|
4
4
|
import { Panel } from '../components/Panel/Panel.jsx'
|
|
5
5
|
import { resolveTargetSlot, isModeAllowed, isControlVisible, isConsumerHtml } from './slotHelpers.js'
|
|
6
6
|
import { allowedSlots } from './slots.js'
|
|
7
|
-
import { stringToKebab } from '../../utils/stringToKebab.js'
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* Maps slot names to their corresponding layout refs.
|
|
@@ -16,12 +15,20 @@ export const getSlotRef = (slot, layoutRefs) => {
|
|
|
16
15
|
'top-left': layoutRefs.topLeftColRef,
|
|
17
16
|
'top-right': layoutRefs.topRightColRef,
|
|
18
17
|
inset: layoutRefs.insetRef,
|
|
18
|
+
'left-top': layoutRefs.leftTopRef,
|
|
19
|
+
'left-bottom': layoutRefs.leftBottomRef,
|
|
19
20
|
middle: layoutRefs.middleRef,
|
|
20
|
-
|
|
21
|
+
'right-top': layoutRefs.rightTopRef,
|
|
21
22
|
'right-bottom': layoutRefs.rightBottomRef,
|
|
23
|
+
bottom: layoutRefs.bottomRef,
|
|
22
24
|
actions: layoutRefs.actionsRef,
|
|
23
25
|
modal: layoutRefs.modalRef
|
|
24
26
|
}
|
|
27
|
+
if (slot?.endsWith('-button')) {
|
|
28
|
+
const el = document.querySelector(`[data-button-slot="${slot}"]`)
|
|
29
|
+
return el ? { current: el } : null
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
return slotRefMap[slot] || null
|
|
26
33
|
}
|
|
27
34
|
|
|
@@ -79,7 +86,7 @@ const PersistentPanel = ({ panelId, config, isOpen, openPanelProps, allowedModal
|
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
// 2. Slot Validation
|
|
82
|
-
const isNextToButton =
|
|
89
|
+
const isNextToButton = targetSlot.endsWith('-button')
|
|
83
90
|
const isSlotAllowed = allowedSlots.panel.includes(targetSlot) || isNextToButton
|
|
84
91
|
|
|
85
92
|
if (!isSlotAllowed) {
|
|
@@ -292,6 +292,18 @@ describe('HtmlElementHost', () => {
|
|
|
292
292
|
expect(getSlotRef('unknown-slot', {})).toBeNull()
|
|
293
293
|
})
|
|
294
294
|
|
|
295
|
+
test('getSlotRef returns wrapped element for button slot when element exists', () => {
|
|
296
|
+
const el = document.createElement('div')
|
|
297
|
+
el.dataset.buttonSlot = 'my-panel-button'
|
|
298
|
+
document.body.appendChild(el)
|
|
299
|
+
expect(getSlotRef('my-panel-button', {})).toEqual({ current: el })
|
|
300
|
+
el.remove()
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test('getSlotRef returns null for button slot when no element found', () => {
|
|
304
|
+
expect(getSlotRef('nonexistent-button', {})).toBeNull()
|
|
305
|
+
})
|
|
306
|
+
|
|
295
307
|
it('does not append child if slotRef exists but current is null', () => {
|
|
296
308
|
// 1. Setup refs where the slot exists in the map but the DOM node (current) is null
|
|
297
309
|
const incompleteRefs = {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// src/core/renderers/mapPanels.js
|
|
2
2
|
import React from 'react'
|
|
3
|
-
import { stringToKebab } from '../../utils/stringToKebab.js'
|
|
4
3
|
import { withPluginContexts } from './pluginWrapper.js'
|
|
5
4
|
import { Panel } from '../components/Panel/Panel.jsx'
|
|
6
5
|
import { allowedSlots } from './slots.js'
|
|
@@ -12,7 +11,7 @@ import { resolveTargetSlot, isModeAllowed, isConsumerHtml } from './slotHelpers.
|
|
|
12
11
|
* and ensures only the topmost modal panel is shown.
|
|
13
12
|
*/
|
|
14
13
|
const isPanelVisible = (panelId, config, bpConfig, { targetSlot, slot, mode, isFullscreen, allowedModalPanelId }) => {
|
|
15
|
-
const isNextToButton =
|
|
14
|
+
const isNextToButton = targetSlot.endsWith('-button')
|
|
16
15
|
if (!allowedSlots.panel.includes(targetSlot) && !isNextToButton) {
|
|
17
16
|
return false
|
|
18
17
|
}
|
|
@@ -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,
|
|
33
|
-
layoutSlots.
|
|
34
|
+
layoutSlots.INSET, // Deprecate
|
|
35
|
+
layoutSlots.LEFT_TOP,
|
|
36
|
+
layoutSlots.LEFT_BOTTOM,
|
|
34
37
|
layoutSlots.MIDDLE,
|
|
35
|
-
layoutSlots.
|
|
36
|
-
layoutSlots.
|
|
37
|
-
layoutSlots.
|
|
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,9 +20,13 @@ export const AppProvider = ({ options, children }) => {
|
|
|
20
20
|
topLeftColRef: useRef(null),
|
|
21
21
|
topRightColRef: useRef(null),
|
|
22
22
|
insetRef: useRef(null),
|
|
23
|
+
leftRef: useRef(null),
|
|
24
|
+
leftTopRef: useRef(null),
|
|
25
|
+
leftBottomRef: useRef(null),
|
|
26
|
+
middleRef: useRef(null),
|
|
23
27
|
rightRef: useRef(null),
|
|
28
|
+
rightTopRef: useRef(null),
|
|
24
29
|
rightBottomRef: useRef(null),
|
|
25
|
-
middleRef: useRef(null),
|
|
26
30
|
bottomRef: useRef(null),
|
|
27
31
|
footerRef: useRef(null),
|
|
28
32
|
actionsRef: useRef(null),
|
|
@@ -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
|
})
|