@defra/interactive-map 0.0.12-alpha → 0.0.15-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/package.json +9 -4
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/src/manifest.js +4 -4
- package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/src/manifest.js +2 -2
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/src/events/fetchSuggestions.js +10 -7
- package/plugins/search/src/events/fetchSuggestions.test.js +4 -4
- package/plugins/search/src/search.scss +8 -3
- 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/components/Panel/Panel.module.scss +1 -0
- package/src/App/hooks/useLayoutMeasurements.js +31 -23
- package/src/App/hooks/useLayoutMeasurements.test.js +39 -10
- package/src/App/hooks/useModalPanelBehaviour.js +85 -21
- package/src/App/hooks/useModalPanelBehaviour.test.js +126 -18
- package/src/App/hooks/useVisibleGeometry.js +7 -13
- package/src/App/hooks/useVisibleGeometry.test.js +72 -47
- package/src/App/layout/Layout.jsx +11 -6
- package/src/App/layout/Layout.test.jsx +0 -1
- package/src/App/layout/layout.module.scss +83 -10
- package/src/App/renderer/HtmlElementHost.jsx +10 -4
- package/src/App/renderer/HtmlElementHost.test.jsx +32 -11
- package/src/App/renderer/SlotRenderer.jsx +1 -1
- package/src/App/renderer/mapPanels.js +1 -2
- package/src/App/renderer/mapPanels.test.js +3 -3
- package/src/App/renderer/slotHelpers.js +2 -2
- package/src/App/renderer/slotHelpers.test.js +3 -3
- package/src/App/renderer/slots.js +11 -8
- package/src/App/store/AppProvider.jsx +5 -2
- package/src/App/store/appDispatchMiddleware.test.js +2 -2
- package/src/config/appConfig.js +4 -4
- package/src/utils/getSafeZoneInset.js +139 -39
- package/src/utils/getSafeZoneInset.test.js +301 -81
|
@@ -1,89 +1,309 @@
|
|
|
1
1
|
import { getSafeZoneInset } from './getSafeZoneInset'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
rightRef = { current: { offsetWidth: 50, offsetLeft: 0 } }
|
|
14
|
-
footerRef = { current: { offsetTop: 550 } }
|
|
15
|
-
actionsRef = { current: { offsetTop: 520 } }
|
|
16
|
-
|
|
17
|
-
// Mock CSS var --divider-gap = 10
|
|
18
|
-
window.getComputedStyle = jest.fn().mockReturnValue({ getPropertyValue: () => '10' })
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
const runScenario = ({ isLandscape, insetHeight }) => {
|
|
22
|
-
insetRef.current.offsetHeight = insetHeight
|
|
23
|
-
|
|
24
|
-
// Manipulate dimensions to influence landscape heuristic
|
|
25
|
-
if (isLandscape) {
|
|
26
|
-
mainRef.current.offsetWidth = 1000
|
|
27
|
-
insetRef.current.offsetWidth = 400
|
|
28
|
-
} else {
|
|
29
|
-
mainRef.current.offsetWidth = 600
|
|
30
|
-
insetRef.current.offsetWidth = 100
|
|
31
|
-
}
|
|
3
|
+
const MAIN_WIDTH = 900
|
|
4
|
+
const MAIN_HEIGHT = 600
|
|
5
|
+
const LEFT_LEFT = 10
|
|
6
|
+
const LEFT_WIDTH = 40
|
|
7
|
+
const LEFT_TOP = 60
|
|
8
|
+
const RIGHT_WIDTH = 40
|
|
9
|
+
const ACTIONS_TOP = 540
|
|
10
|
+
const FOOTER_TOP = 560
|
|
11
|
+
const EARLY_ACTIONS_TOP = 500
|
|
12
|
+
const GAP = 8
|
|
32
13
|
|
|
33
|
-
|
|
34
|
-
|
|
14
|
+
// Base insets: main.offsetLeft=0 in tests
|
|
15
|
+
const BASE_LEFT = LEFT_LEFT + LEFT_WIDTH + GAP // 58
|
|
16
|
+
const BASE_RIGHT = LEFT_LEFT + RIGHT_WIDTH + GAP // 58
|
|
17
|
+
const BASE_TOP = LEFT_TOP // 60
|
|
18
|
+
const BASE_BOTTOM = (MAIN_HEIGHT - ACTIONS_TOP) + GAP // 68
|
|
19
|
+
|
|
20
|
+
// Height threshold: availableHeight / RATIO = (600-60-68)/2 = 236
|
|
21
|
+
const ABOVE_THRESHOLD = 240
|
|
22
|
+
const BELOW_THRESHOLD = 230
|
|
23
|
+
const COMBINED_ABOVE = 120 // two × 120 + gap = 248 > 236
|
|
24
|
+
const COMBINED_BELOW = 100 // two × 100 + gap = 208 < 236
|
|
25
|
+
|
|
26
|
+
// Width threshold: availableWidth / RATIO = (900-58-58)/2 = 392
|
|
27
|
+
const ABOVE_W_THRESHOLD = 400
|
|
28
|
+
const COMBINED_ABOVE_W = 200 // two × 200 = 400 > 392 → triggers combined
|
|
29
|
+
const COMBINED_BELOW_W = 180 // two × 180 = 360 < 392 → does not trigger
|
|
30
|
+
const PANEL_H_TALL = 150
|
|
31
|
+
const PANEL_H_SHORT = 100
|
|
32
|
+
|
|
33
|
+
const ABOVE_CAP_TOP = 330 // 60+330+8=398 > CAP_HEIGHT ≈ 389.3 → capped
|
|
34
|
+
const FOOTER_INSET = MAIN_HEIGHT - FOOTER_TOP + GAP // 48
|
|
35
|
+
const ABOVE_CAP_BOTTOM = 342 // 48+342+8=398 > CAP_HEIGHT → capped
|
|
36
|
+
|
|
37
|
+
const PANEL_W_STANDARD = 200
|
|
38
|
+
const PANEL_W_WIDE = 250
|
|
39
|
+
const PANEL_W_NARROW = 100
|
|
40
|
+
const PANEL_W_XLARGE = 600 // 10+600+8=618 > CAP_WIDTH ≈ 589.3 → capped
|
|
41
|
+
|
|
42
|
+
const leftInset = w => LEFT_LEFT + w + GAP
|
|
43
|
+
const rightInset = w => LEFT_LEFT + w + GAP
|
|
44
|
+
const topInset = h => BASE_TOP + h + GAP
|
|
45
|
+
|
|
46
|
+
const MAX_RATIO = 3
|
|
47
|
+
const CAP_WIDTH = (MAIN_WIDTH - 2 * GAP) * (MAX_RATIO - 1) / MAX_RATIO
|
|
48
|
+
const CAP_HEIGHT = (MAIN_HEIGHT - 2 * GAP) * (MAX_RATIO - 1) / MAX_RATIO
|
|
49
|
+
|
|
50
|
+
// ─── Setup ──────────────────────────────────────────────────────────────────
|
|
35
51
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
expect(result.top).toBe(insetRef.current.offsetTop)
|
|
51
|
-
expect(result.left).toBe(80)
|
|
52
|
-
expect(result.left).toBe(result.right)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('portrait shifts inset below itself when it does NOT have enough vertical room', () => {
|
|
56
|
-
// Force a portrait overflow case
|
|
57
|
-
mainRef.current.offsetWidth = 200
|
|
58
|
-
insetRef.current.offsetWidth = 100
|
|
59
|
-
insetRef.current.offsetHeight = 50
|
|
60
|
-
insetRef.current.offsetTop = 50
|
|
61
|
-
window.getComputedStyle = jest.fn().mockReturnValue({ getPropertyValue: () => '10' })
|
|
62
|
-
|
|
63
|
-
const result = getSafeZoneInset({ mainRef, insetRef, rightRef, footerRef, actionsRef })
|
|
64
|
-
|
|
65
|
-
// topOffset = 50 + 50 + 10 = 110
|
|
66
|
-
expect(result.top).toBe(110)
|
|
67
|
-
// left = rightOffset = 20 + 50 + 10 = 80
|
|
68
|
-
expect(result.left).toBe(80)
|
|
69
|
-
expect(result.right).toBe(80)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Test to ensure coverage for the safety guardrail (Line 29).
|
|
74
|
-
* Validates that the function returns undefined if React refs are
|
|
75
|
-
* not yet attached to DOM elements.
|
|
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 }
|
|
52
|
+
let mainRef, leftRef, rightRef, actionsRef, footerRef
|
|
53
|
+
|
|
54
|
+
beforeAll(() => {
|
|
55
|
+
globalThis.getComputedStyle = jest.fn().mockReturnValue({ getPropertyValue: () => String(GAP) })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const colRef = (offsetWidth, offsetLeft, offsetTop) => {
|
|
59
|
+
const buttonGroup = { offsetWidth }
|
|
60
|
+
return {
|
|
61
|
+
current: {
|
|
62
|
+
offsetWidth,
|
|
63
|
+
offsetLeft,
|
|
64
|
+
offsetTop,
|
|
65
|
+
querySelector: (sel) => sel === '.im-c-button-group' ? buttonGroup : null
|
|
84
66
|
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
mainRef = { current: { offsetWidth: MAIN_WIDTH, offsetHeight: MAIN_HEIGHT, offsetLeft: 0 } }
|
|
72
|
+
leftRef = colRef(LEFT_WIDTH, LEFT_LEFT, LEFT_TOP)
|
|
73
|
+
rightRef = colRef(RIGHT_WIDTH)
|
|
74
|
+
actionsRef = { current: { offsetTop: ACTIONS_TOP } }
|
|
75
|
+
footerRef = { current: { offsetTop: FOOTER_TOP } }
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const base = () => ({ mainRef, leftRef, rightRef, actionsRef, footerRef })
|
|
79
|
+
|
|
80
|
+
// Slot container where an .im-c-panel is the first element child.
|
|
81
|
+
const panel = (offsetWidth, offsetHeight) => {
|
|
82
|
+
const panelEl = { offsetWidth, offsetHeight, classList: { contains: (c) => c === 'im-c-panel' } }
|
|
83
|
+
return { current: { firstElementChild: panelEl } }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Slot container where a button precedes the panel (panel should be ignored).
|
|
87
|
+
const panelAfterButton = () => ({ current: { firstElementChild: { classList: { contains: () => false } } } })
|
|
88
|
+
|
|
89
|
+
// ─── Missing refs ────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
describe('getSafeZoneInset — missing refs', () => {
|
|
92
|
+
it('returns undefined when mainRef.current is null', () => {
|
|
93
|
+
expect(getSafeZoneInset({ ...base(), mainRef: { current: null } })).toBeUndefined()
|
|
94
|
+
})
|
|
95
|
+
it('returns undefined when leftRef.current is null', () => {
|
|
96
|
+
expect(getSafeZoneInset({ ...base(), leftRef: { current: null } })).toBeUndefined()
|
|
97
|
+
})
|
|
98
|
+
it('returns undefined when actionsRef is undefined', () => {
|
|
99
|
+
expect(getSafeZoneInset({ mainRef, leftRef, rightRef, footerRef })).toBeUndefined()
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// ─── Base structural insets ──────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
describe('getSafeZoneInset — base structural insets', () => {
|
|
106
|
+
it('returns base insets when no panel refs are provided', () => {
|
|
107
|
+
expect(getSafeZoneInset(base())).toEqual({
|
|
108
|
+
left: BASE_LEFT, right: BASE_RIGHT, top: BASE_TOP, bottom: BASE_BOTTOM
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
it('ignores a panel that is not the first element in its slot (buttons precede it)', () => {
|
|
112
|
+
expect(getSafeZoneInset({ ...base(), leftTopRef: panelAfterButton() }).left).toBe(BASE_LEFT)
|
|
113
|
+
})
|
|
114
|
+
it('ignores a slot container with no children', () => {
|
|
115
|
+
expect(getSafeZoneInset({ ...base(), leftTopRef: { current: { firstElementChild: null } } }).left).toBe(BASE_LEFT)
|
|
116
|
+
})
|
|
117
|
+
it('ignores a panel with zero width', () => {
|
|
118
|
+
expect(getSafeZoneInset({ ...base(), leftTopRef: panel(0, ABOVE_THRESHOLD) }).left).toBe(BASE_LEFT)
|
|
119
|
+
})
|
|
120
|
+
it('uses zero button width when column ref has no button group', () => {
|
|
121
|
+
const noGroupLeft = { current: { offsetWidth: 0, offsetLeft: LEFT_LEFT, offsetTop: LEFT_TOP, querySelector: () => null } }
|
|
122
|
+
const noGroupRight = { current: { offsetWidth: 0, offsetLeft: 0, offsetTop: 0, querySelector: () => null } }
|
|
123
|
+
expect(getSafeZoneInset({ mainRef, leftRef: noGroupLeft, rightRef: noGroupRight, actionsRef, footerRef })).toEqual({
|
|
124
|
+
left: LEFT_LEFT + GAP, right: LEFT_LEFT + GAP, top: LEFT_TOP, bottom: BASE_BOTTOM
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
it('returns base insets when all panel slots are empty (height 0)', () => {
|
|
128
|
+
expect(getSafeZoneInset({
|
|
129
|
+
...base(),
|
|
130
|
+
leftTopRef: panel(PANEL_W_STANDARD, 0),
|
|
131
|
+
leftBottomRef: panel(PANEL_W_STANDARD, 0),
|
|
132
|
+
rightTopRef: panel(PANEL_W_STANDARD, 0),
|
|
133
|
+
rightBottomRef: panel(PANEL_W_STANDARD, 0)
|
|
134
|
+
})).toEqual({ left: BASE_LEFT, right: BASE_RIGHT, top: BASE_TOP, bottom: BASE_BOTTOM })
|
|
135
|
+
})
|
|
136
|
+
it('uses max of actions and footer for base bottom', () => {
|
|
137
|
+
actionsRef.current.offsetTop = EARLY_ACTIONS_TOP
|
|
138
|
+
expect(getSafeZoneInset(base()).bottom).toBe(MAIN_HEIGHT - EARLY_ACTIONS_TOP + GAP)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// ─── Left edge ───────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
describe('getSafeZoneInset — left edge', () => {
|
|
145
|
+
it('does not trigger when single panel height is below threshold', () => {
|
|
146
|
+
expect(getSafeZoneInset({ ...base(), leftTopRef: panel(PANEL_W_STANDARD, BELOW_THRESHOLD) }).left).toBe(BASE_LEFT)
|
|
147
|
+
})
|
|
148
|
+
it('triggers when single panel height exceeds threshold', () => {
|
|
149
|
+
expect(getSafeZoneInset({ ...base(), leftTopRef: panel(PANEL_W_STANDARD, ABOVE_THRESHOLD) }).left)
|
|
150
|
+
.toBe(leftInset(PANEL_W_STANDARD))
|
|
151
|
+
})
|
|
152
|
+
it('triggers when combined height of two panels exceeds threshold', () => {
|
|
153
|
+
expect(getSafeZoneInset({
|
|
154
|
+
...base(),
|
|
155
|
+
leftTopRef: panel(PANEL_W_STANDARD, COMBINED_ABOVE),
|
|
156
|
+
leftBottomRef: panel(PANEL_W_NARROW, COMBINED_ABOVE)
|
|
157
|
+
}).left).toBe(leftInset(PANEL_W_STANDARD))
|
|
158
|
+
})
|
|
159
|
+
it('does not trigger when combined height is below threshold', () => {
|
|
160
|
+
expect(getSafeZoneInset({
|
|
161
|
+
...base(),
|
|
162
|
+
leftTopRef: panel(PANEL_W_STANDARD, COMBINED_BELOW),
|
|
163
|
+
leftBottomRef: panel(PANEL_W_STANDARD, COMBINED_BELOW)
|
|
164
|
+
}).left).toBe(BASE_LEFT)
|
|
165
|
+
})
|
|
166
|
+
it('uses the wider panel for the inset amount', () => {
|
|
167
|
+
expect(getSafeZoneInset({
|
|
168
|
+
...base(),
|
|
169
|
+
leftTopRef: panel(PANEL_W_WIDE, COMBINED_ABOVE),
|
|
170
|
+
leftBottomRef: panel(PANEL_W_NARROW, COMBINED_ABOVE)
|
|
171
|
+
}).left).toBe(leftInset(PANEL_W_WIDE))
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// ─── Right edge ──────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
describe('getSafeZoneInset — right edge', () => {
|
|
178
|
+
it('triggers when combined height of right-column panels exceeds threshold', () => {
|
|
179
|
+
expect(getSafeZoneInset({
|
|
180
|
+
...base(),
|
|
181
|
+
rightTopRef: panel(PANEL_W_STANDARD, COMBINED_ABOVE),
|
|
182
|
+
rightBottomRef: panel(PANEL_W_NARROW, COMBINED_ABOVE)
|
|
183
|
+
}).right).toBe(rightInset(PANEL_W_STANDARD))
|
|
184
|
+
})
|
|
185
|
+
it('does not trigger when combined height is below threshold', () => {
|
|
186
|
+
expect(getSafeZoneInset({
|
|
187
|
+
...base(),
|
|
188
|
+
rightTopRef: panel(PANEL_W_STANDARD, COMBINED_BELOW),
|
|
189
|
+
rightBottomRef: panel(PANEL_W_STANDARD, COMBINED_BELOW)
|
|
190
|
+
}).right).toBe(BASE_RIGHT)
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// ─── Top edge ────────────────────────────────────────────────────────────────
|
|
195
|
+
// Trigger is WIDTH-based. A narrow panel must not add top padding even if tall.
|
|
196
|
+
|
|
197
|
+
describe('getSafeZoneInset — top edge', () => {
|
|
198
|
+
it('does not trigger when top panel is narrow, even if tall', () => {
|
|
199
|
+
// PANEL_W_STANDARD (200) < width threshold (392)
|
|
200
|
+
expect(getSafeZoneInset({ ...base(), rightTopRef: panel(PANEL_W_STANDARD, ABOVE_THRESHOLD) }).top).toBe(BASE_TOP)
|
|
201
|
+
})
|
|
202
|
+
it('triggers when a top panel width exceeds threshold', () => {
|
|
203
|
+
expect(getSafeZoneInset({ ...base(), leftTopRef: panel(ABOVE_W_THRESHOLD, ABOVE_THRESHOLD) }).top)
|
|
204
|
+
.toBe(topInset(ABOVE_THRESHOLD))
|
|
205
|
+
})
|
|
206
|
+
it('column-primary wide-and-tall top panel triggers left inset, not top', () => {
|
|
207
|
+
// panel(400,330): h/availableH≈0.699 > w/availableW≈0.510 → column-primary
|
|
208
|
+
const result = getSafeZoneInset({ ...base(), leftTopRef: panel(ABOVE_W_THRESHOLD, ABOVE_CAP_TOP) })
|
|
209
|
+
expect(result.left).toBe(leftInset(ABOVE_W_THRESHOLD))
|
|
210
|
+
expect(result.top).toBe(BASE_TOP)
|
|
211
|
+
})
|
|
212
|
+
it('when top panels have mixed primaries, each contributes to its own edge', () => {
|
|
213
|
+
// tl(400,100): row-primary → top; tr(400,330): column-primary → right
|
|
214
|
+
const result = getSafeZoneInset({
|
|
215
|
+
...base(),
|
|
216
|
+
leftTopRef: panel(ABOVE_W_THRESHOLD, COMBINED_BELOW),
|
|
217
|
+
rightTopRef: panel(ABOVE_W_THRESHOLD, ABOVE_CAP_TOP)
|
|
218
|
+
})
|
|
219
|
+
expect(result.top).toBe(topInset(COMBINED_BELOW))
|
|
220
|
+
expect(result.right).toBe(rightInset(ABOVE_W_THRESHOLD))
|
|
221
|
+
})
|
|
222
|
+
it('triggers when combined width of two top panels exceeds threshold; uses max height', () => {
|
|
223
|
+
// each COMBINED_ABOVE_W (200) < threshold (392), but 200+200=400 > 392
|
|
224
|
+
expect(getSafeZoneInset({
|
|
225
|
+
...base(),
|
|
226
|
+
leftTopRef: panel(COMBINED_ABOVE_W, PANEL_H_TALL),
|
|
227
|
+
rightTopRef: panel(COMBINED_ABOVE_W, PANEL_H_SHORT)
|
|
228
|
+
}).top).toBe(topInset(PANEL_H_TALL))
|
|
229
|
+
})
|
|
230
|
+
it('does not trigger when both top panels are below combined width threshold', () => {
|
|
231
|
+
expect(getSafeZoneInset({
|
|
232
|
+
...base(),
|
|
233
|
+
leftTopRef: panel(COMBINED_BELOW_W, ABOVE_THRESHOLD),
|
|
234
|
+
rightTopRef: panel(COMBINED_BELOW_W, ABOVE_THRESHOLD)
|
|
235
|
+
}).top).toBe(BASE_TOP)
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// ─── Bottom edge ─────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
describe('getSafeZoneInset — bottom edge', () => {
|
|
242
|
+
it('does not trigger when bottom panel is narrow, even if tall', () => {
|
|
243
|
+
expect(getSafeZoneInset({ ...base(), rightBottomRef: panel(PANEL_W_STANDARD, ABOVE_THRESHOLD) }).bottom).toBe(BASE_BOTTOM)
|
|
244
|
+
})
|
|
245
|
+
it('triggers when a bottom panel width exceeds threshold', () => {
|
|
246
|
+
expect(getSafeZoneInset({ ...base(), leftBottomRef: panel(ABOVE_W_THRESHOLD, ABOVE_THRESHOLD) }).bottom)
|
|
247
|
+
.toBe(Math.min(FOOTER_INSET + ABOVE_THRESHOLD + GAP, CAP_HEIGHT))
|
|
248
|
+
})
|
|
249
|
+
it('column-primary wide-and-tall bottom panel triggers left inset, not bottom', () => {
|
|
250
|
+
// panel(400,342): h/availableH≈0.724 > w/availableW≈0.510 → column-primary
|
|
251
|
+
const result = getSafeZoneInset({ ...base(), leftBottomRef: panel(ABOVE_W_THRESHOLD, ABOVE_CAP_BOTTOM) })
|
|
252
|
+
expect(result.left).toBe(leftInset(ABOVE_W_THRESHOLD))
|
|
253
|
+
expect(result.bottom).toBe(BASE_BOTTOM)
|
|
254
|
+
})
|
|
255
|
+
it('triggers when combined width of two bottom panels exceeds threshold; uses max height', () => {
|
|
256
|
+
expect(getSafeZoneInset({
|
|
257
|
+
...base(),
|
|
258
|
+
leftBottomRef: panel(COMBINED_ABOVE_W, PANEL_H_TALL),
|
|
259
|
+
rightBottomRef: panel(COMBINED_ABOVE_W, PANEL_H_SHORT)
|
|
260
|
+
}).bottom).toBe(Math.min(FOOTER_INSET + PANEL_H_TALL + GAP, CAP_HEIGHT))
|
|
261
|
+
})
|
|
262
|
+
it('does not trigger when both bottom panels are below combined width threshold', () => {
|
|
263
|
+
expect(getSafeZoneInset({
|
|
264
|
+
...base(),
|
|
265
|
+
leftBottomRef: panel(COMBINED_BELOW_W, ABOVE_THRESHOLD),
|
|
266
|
+
rightBottomRef: panel(COMBINED_BELOW_W, ABOVE_THRESHOLD)
|
|
267
|
+
}).bottom).toBe(BASE_BOTTOM)
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
// ─── MAX_RATIO cap ────────────────────────────────────────────────────────────
|
|
85
272
|
|
|
86
|
-
|
|
87
|
-
|
|
273
|
+
describe('getSafeZoneInset — MAX_RATIO cap', () => {
|
|
274
|
+
it('caps left inset at (MAX_RATIO-1)/MAX_RATIO of usable width', () => {
|
|
275
|
+
expect(getSafeZoneInset({ ...base(), leftTopRef: panel(PANEL_W_XLARGE, PANEL_W_XLARGE) }).left).toBe(CAP_WIDTH)
|
|
276
|
+
})
|
|
277
|
+
it('caps right inset at (MAX_RATIO-1)/MAX_RATIO of usable width', () => {
|
|
278
|
+
expect(getSafeZoneInset({ ...base(), rightTopRef: panel(PANEL_W_XLARGE, PANEL_W_XLARGE) }).right).toBe(CAP_WIDTH)
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
// ─── Corner panel independence ────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
describe('getSafeZoneInset — corner panel independence', () => {
|
|
285
|
+
it('narrow-but-tall corner panel triggers side inset only (not top)', () => {
|
|
286
|
+
// PANEL_W_STANDARD (200): tall enough for right, too narrow for top (< 392)
|
|
287
|
+
const result = getSafeZoneInset({ ...base(), rightTopRef: panel(PANEL_W_STANDARD, ABOVE_THRESHOLD) })
|
|
288
|
+
expect(result.right).toBe(rightInset(PANEL_W_STANDARD))
|
|
289
|
+
expect(result.top).toBe(BASE_TOP)
|
|
290
|
+
})
|
|
291
|
+
it('wide-and-tall corner panel triggers only its primary (row) edge inset', () => {
|
|
292
|
+
// panel(400, 240): w/availableW≈0.510, h/availableH≈0.508 → row-primary → top only
|
|
293
|
+
const result = getSafeZoneInset({ ...base(), leftTopRef: panel(ABOVE_W_THRESHOLD, ABOVE_THRESHOLD) })
|
|
294
|
+
expect(result.left).toBe(BASE_LEFT)
|
|
295
|
+
expect(result.top).toBe(topInset(ABOVE_THRESHOLD))
|
|
296
|
+
})
|
|
297
|
+
it('two wide panels in the same column collectively trigger left but not top or bottom', () => {
|
|
298
|
+
// Each h=COMBINED_ABOVE (120) < hThreshold individually, combined 248 > 236
|
|
299
|
+
// Each w=ABOVE_W_THRESHOLD (400) > wThreshold → left column triggers, excluding from top/bottom
|
|
300
|
+
const result = getSafeZoneInset({
|
|
301
|
+
...base(),
|
|
302
|
+
leftTopRef: panel(ABOVE_W_THRESHOLD, COMBINED_ABOVE),
|
|
303
|
+
leftBottomRef: panel(ABOVE_W_THRESHOLD, COMBINED_ABOVE)
|
|
304
|
+
})
|
|
305
|
+
expect(result.left).toBe(leftInset(ABOVE_W_THRESHOLD))
|
|
306
|
+
expect(result.top).toBe(BASE_TOP)
|
|
307
|
+
expect(result.bottom).toBe(BASE_BOTTOM)
|
|
88
308
|
})
|
|
89
309
|
})
|