@defra/interactive-map 0.0.14-alpha → 0.0.16-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/assets/css/docusaurus.css +104 -0
- package/assets/images/favicon.svg +1 -0
- package/assets/images/hero.png +0 -0
- 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/api/slot-map.svg +1 -0
- package/docs/api/slots.md +89 -6
- package/docs/api.md +1 -1
- package/docs/architecture.md +3 -1
- package/docs/{demo.mdx → examples.mdx} +1 -1
- package/docs/getting-started.md +1 -3
- package/docs/index.mdx +42 -0
- package/docs/plugins/interact.md +176 -55
- package/docs/plugins/map-styles.md +64 -7
- package/docs/plugins/search.md +207 -63
- package/docs/plugins.md +7 -15
- package/docusaurus.config.cjs +34 -34
- package/jest.setup.js +1 -1
- package/package.json +5 -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/DatasetsInit.jsx +1 -1
- package/plugins/beta/datasets/src/api/addDataset.js +1 -1
- package/plugins/beta/datasets/src/api/hideDataset.js +1 -1
- package/plugins/beta/datasets/src/api/hideFeatures.js +1 -1
- package/plugins/beta/datasets/src/api/removeDataset.js +1 -1
- package/plugins/beta/datasets/src/api/showDataset.js +1 -1
- package/plugins/beta/datasets/src/api/showFeatures.js +1 -1
- package/plugins/beta/datasets/src/datasets.js +4 -4
- package/plugins/beta/datasets/src/defaults.js +1 -1
- package/plugins/beta/datasets/src/fetch/createDynamicSource.js +5 -5
- package/plugins/beta/datasets/src/handleSetMapStyle.js +1 -1
- package/plugins/beta/datasets/src/manifest.js +7 -7
- package/plugins/beta/datasets/src/mapLayers.js +2 -3
- package/plugins/beta/datasets/src/panels/Key.jsx +31 -29
- package/plugins/beta/datasets/src/panels/Layers.jsx +8 -9
- package/plugins/beta/datasets/src/utils/bbox.js +4 -4
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/DrawInit.jsx +16 -16
- package/plugins/beta/draw-es/src/api/addFeature.js +3 -3
- package/plugins/beta/draw-es/src/api/deleteFeature.js +3 -3
- package/plugins/beta/draw-es/src/api/editFeature.js +3 -3
- package/plugins/beta/draw-es/src/api/newPolygon.js +3 -3
- package/plugins/beta/draw-es/src/events.js +52 -20
- package/plugins/beta/draw-es/src/events.test.js +301 -0
- package/plugins/beta/draw-es/src/graphic.js +1 -1
- package/plugins/beta/draw-es/src/manifest.js +4 -4
- package/plugins/beta/draw-es/src/reducer.js +1 -1
- package/plugins/beta/draw-es/src/sketchViewModel.js +1 -1
- 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/DrawInit.jsx +49 -52
- package/plugins/beta/draw-ml/src/api/deleteFeature.js +1 -1
- package/plugins/beta/draw-ml/src/api/editFeature.js +8 -5
- package/plugins/beta/draw-ml/src/api/newLine.js +0 -1
- package/plugins/beta/draw-ml/src/api/newPolygon.js +0 -1
- package/plugins/beta/draw-ml/src/api/split.js +4 -4
- package/plugins/beta/draw-ml/src/defaults.js +1 -1
- package/plugins/beta/draw-ml/src/events.js +8 -6
- package/plugins/beta/draw-ml/src/manifest.js +15 -15
- package/plugins/beta/draw-ml/src/mapboxDraw.js +1 -1
- package/plugins/beta/draw-ml/src/mapboxSnap.js +17 -18
- package/plugins/beta/draw-ml/src/modes/createDrawMode.js +31 -31
- package/plugins/beta/draw-ml/src/modes/disabledMode.js +1 -1
- package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +11 -11
- package/plugins/beta/draw-ml/src/modes/editVertex/undoHandlers.js +7 -7
- package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +8 -8
- package/plugins/beta/draw-ml/src/modes/editVertex/vertexQueries.js +7 -7
- package/plugins/beta/draw-ml/src/modes/editVertexMode.js +32 -24
- package/plugins/beta/draw-ml/src/reducer.js +1 -1
- package/plugins/beta/draw-ml/src/undoStack.js +4 -4
- package/plugins/beta/draw-ml/src/utils/snapHelpers.js +12 -12
- package/plugins/beta/draw-ml/src/utils/spatial.js +11 -11
- package/plugins/beta/frame/src/Frame.jsx +4 -4
- package/plugins/beta/frame/src/FrameInit.jsx +4 -4
- package/plugins/beta/frame/src/api/addFrame.js +1 -1
- package/plugins/beta/frame/src/api/editFeature.js +1 -1
- package/plugins/beta/frame/src/config.js +1 -1
- package/plugins/beta/frame/src/manifest.js +3 -3
- package/plugins/beta/frame/src/reducer.js +1 -1
- package/plugins/beta/frame/src/utils.js +1 -1
- 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/MapStyles.jsx +18 -18
- package/plugins/beta/map-styles/src/manifest.js +2 -2
- package/plugins/beta/scale-bar/src/ScaleBar.jsx +5 -5
- package/plugins/beta/use-location/src/UseLocation.jsx +1 -1
- package/plugins/beta/use-location/src/defaults.js +1 -1
- package/plugins/beta/use-location/src/events.js +3 -3
- package/plugins/interact/src/InteractInit.jsx +1 -2
- package/plugins/interact/src/api/enable.js +8 -5
- package/plugins/interact/src/api/enable.test.js +2 -2
- package/plugins/interact/src/api/selectFeature.js +4 -4
- package/plugins/interact/src/api/unselectFeature.js +5 -5
- package/plugins/interact/src/defaults.js +0 -1
- package/plugins/interact/src/events.test.js +15 -15
- package/plugins/interact/src/hooks/useHighlightSync.js +1 -1
- package/plugins/interact/src/hooks/useInteractionHandlers.js +2 -2
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +5 -5
- package/plugins/interact/src/manifest.js +2 -2
- package/plugins/interact/src/manifest.test.js +3 -4
- package/plugins/interact/src/reducer.js +3 -3
- package/plugins/interact/src/reducer.test.js +0 -1
- package/plugins/interact/src/utils/spatial.js +10 -10
- package/plugins/interact/src/utils/spatial.test.js +14 -14
- 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 +7 -6
- package/plugins/search/src/Search.test.jsx +23 -23
- package/plugins/search/src/components/CloseButton/CloseButton.jsx +15 -15
- package/plugins/search/src/components/CloseButton/CloseButton.test.jsx +2 -2
- package/plugins/search/src/components/Form/Form.jsx +14 -14
- package/plugins/search/src/components/Form/Form.test.jsx +11 -11
- package/plugins/search/src/components/OpenButton/OpenButton.jsx +16 -15
- package/plugins/search/src/components/OpenButton/OpenButton.test.jsx +6 -2
- package/plugins/search/src/components/SubmitButton/SubmitButton.jsx +15 -15
- package/plugins/search/src/components/Suggestions/Suggestions.jsx +6 -6
- package/plugins/search/src/components/Suggestions/Suggestions.test.jsx +4 -4
- package/plugins/search/src/datasets.js +12 -13
- package/plugins/search/src/datasets.test.js +1 -1
- package/plugins/search/src/defaults.js +1 -1
- package/plugins/search/src/events/fetchSuggestions.js +4 -4
- package/plugins/search/src/events/fetchSuggestions.test.js +5 -5
- package/plugins/search/src/events/formHandlers.js +3 -3
- package/plugins/search/src/events/formHandlers.test.js +1 -1
- package/plugins/search/src/events/index.js +2 -2
- package/plugins/search/src/events/index.test.js +2 -2
- package/plugins/search/src/events/inputHandlers.js +4 -4
- package/plugins/search/src/events/inputHandlers.test.js +1 -1
- package/plugins/search/src/events/suggestionHandlers.js +2 -2
- package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
- package/plugins/search/src/index.js +2 -1
- package/plugins/search/src/index.test.js +3 -3
- package/plugins/search/src/manifest.js +6 -4
- package/plugins/search/src/reducer.js +1 -2
- package/plugins/search/src/reducer.test.js +2 -2
- package/plugins/search/src/search.scss +18 -6
- package/plugins/search/src/utils/parseOsNamesResults.js +1 -2
- package/plugins/search/src/utils/parseOsNamesResults.test.js +2 -2
- package/plugins/search/src/utils/updateMap.js +1 -1
- package/plugins/search/src/utils/updateMap.test.js +5 -5
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/esriProvider.js +5 -5
- package/providers/beta/esri/src/utils/coords.js +1 -1
- package/providers/beta/esri/src/utils/esriFixes.js +1 -1
- package/providers/beta/esri/src/utils/query.js +4 -4
- package/providers/beta/esri/src/utils/spatial.js +1 -2
- package/providers/beta/esri/src/utils/spatial.test.js +4 -1
- package/providers/beta/open-names/src/utils/mapToLocationModel.test.js +1 -1
- package/providers/maplibre/src/appEvents.test.js +1 -1
- package/providers/maplibre/src/index.js +1 -1
- package/providers/maplibre/src/index.test.js +3 -5
- package/providers/maplibre/src/mapEvents.test.js +15 -5
- package/providers/maplibre/src/maplibreProvider.test.js +6 -2
- package/providers/maplibre/src/utils/calculateLinearTextSize.js +4 -4
- package/providers/maplibre/src/utils/calculateLinearTextSize.test.js +3 -3
- package/providers/maplibre/src/utils/detectWebgl.test.js +1 -1
- package/providers/maplibre/src/utils/highlightFeatures.js +2 -2
- package/providers/maplibre/src/utils/highlightFeatures.test.js +12 -6
- package/providers/maplibre/src/utils/labels.js +19 -20
- package/providers/maplibre/src/utils/labels.test.js +15 -13
- package/providers/maplibre/src/utils/maplibreFixes.test.js +1 -1
- package/providers/maplibre/src/utils/queryFeatures.js +6 -6
- package/providers/maplibre/src/utils/queryFeatures.test.js +13 -13
- package/providers/maplibre/src/utils/spatial.js +0 -1
- package/providers/maplibre/src/utils/spatial.test.js +26 -27
- package/src/App/components/Panel/Panel.module.scss +1 -0
- package/src/App/hooks/useLayoutMeasurements.js +1 -10
- package/src/App/hooks/useLayoutMeasurements.test.js +2 -5
- package/src/App/hooks/useVisibleGeometry.js +7 -13
- package/src/App/hooks/useVisibleGeometry.test.js +72 -47
- package/src/App/layout/Layout.jsx +0 -3
- package/src/App/layout/Layout.test.jsx +0 -1
- package/src/App/layout/layout.module.scss +11 -77
- package/src/App/registry/pluginRegistry.js +17 -0
- package/src/App/registry/pluginRegistry.test.js +33 -0
- package/src/App/renderer/HtmlElementHost.jsx +0 -1
- package/src/App/renderer/HtmlElementHost.test.jsx +20 -11
- package/src/App/renderer/mapButtons.js +3 -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 +0 -3
- package/src/App/store/AppProvider.jsx +0 -1
- package/src/App/store/appDispatchMiddleware.js +33 -1
- package/src/App/store/appDispatchMiddleware.test.js +250 -222
- package/src/config/appConfig.js +4 -4
- package/src/utils/getSafeZoneInset.js +139 -42
- package/src/utils/getSafeZoneInset.test.js +298 -122
- package/src/utils/logger.js +6 -0
- package/src/utils/logger.test.js +32 -0
- package/webpack.dev.mjs +22 -18
- package/docs/govuk-prototype.md +0 -23
- package/docs/index.md +0 -19
|
@@ -1,59 +1,156 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Calculates the safe zone inset — the unobscured region of the map viewport
|
|
3
|
-
* not hidden behind overlay panels
|
|
4
|
-
* for map operations like
|
|
3
|
+
* not hidden behind overlay panels or structural UI (button columns, footer,
|
|
4
|
+
* action bar). Used as padding for map operations like fitBounds or setView
|
|
5
|
+
* so the target location or extent is fully visible.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Each edge inset is driven by:
|
|
8
|
+
* - A structural baseline from the button columns, footer, and action bar.
|
|
9
|
+
* - A panel contribution when panels on that edge cover more than 1/RATIO
|
|
10
|
+
* of the available map dimension along that edge.
|
|
11
|
+
* - A cap so no single inset exceeds 1/MAX_RATIO of the map dimension,
|
|
12
|
+
* preventing fitBounds from zooming out to a corner when panels are large.
|
|
9
13
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Trigger logic per edge:
|
|
15
|
+
* - left / right : combined HEIGHT of panels in that column vs available height
|
|
16
|
+
* - top / bottom : WIDTH of panels in that row vs available width
|
|
17
|
+
*
|
|
18
|
+
* When a panel individually exceeds BOTH the height and width thresholds it
|
|
19
|
+
* would otherwise trigger both adjacent edges, pushing the safe zone into the
|
|
20
|
+
* opposite corner. To prevent this, such panels are classified as either
|
|
21
|
+
* "column-primary" (h/availableH ≥ w/availableW) or "row-primary" and only
|
|
22
|
+
* contribute to their dominant edge. Panels that exceed at most one threshold
|
|
23
|
+
* are never in conflict and contribute to both axes freely.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} refs - React refs from layoutRefs.
|
|
26
|
+
* @param {React.RefObject} refs.mainRef - Main map container.
|
|
27
|
+
* @param {React.RefObject} refs.leftRef - Left button column.
|
|
28
|
+
* @param {React.RefObject} refs.rightRef - Right button column.
|
|
29
|
+
* @param {React.RefObject} refs.actionsRef - Bottom action bar.
|
|
30
|
+
* @param {React.RefObject} refs.footerRef - Footer (logo, copyright, etc).
|
|
31
|
+
* @param {React.RefObject} [refs.leftTopRef] - Top-left panel slot.
|
|
32
|
+
* @param {React.RefObject} [refs.leftBottomRef] - Bottom-left panel slot.
|
|
33
|
+
* @param {React.RefObject} [refs.rightTopRef] - Top-right panel slot.
|
|
34
|
+
* @param {React.RefObject} [refs.rightBottomRef] - Bottom-right panel slot.
|
|
17
35
|
* @returns {{ top: number, right: number, left: number, bottom: number } | undefined}
|
|
18
|
-
* Pixel insets from each edge of the main area, or undefined if any ref is missing.
|
|
36
|
+
* Pixel insets from each edge of the main area, or undefined if any required ref is missing.
|
|
19
37
|
*/
|
|
38
|
+
|
|
39
|
+
const RATIO = 2 // panels covering more than 1/RATIO of an edge trigger that edge's inset
|
|
40
|
+
const MAX_RATIO = 3 // each inset is capped at 1/MAX_RATIO of its map dimension
|
|
41
|
+
|
|
42
|
+
// Query the panel element directly within its slot container.
|
|
43
|
+
// Only the first panel matters, and only when it is also the first element in the slot
|
|
44
|
+
// (i.e. no buttons or other elements precede it — a panel pushed down by buttons is
|
|
45
|
+
// already within the button-column structural inset and should not add extra padding).
|
|
46
|
+
const getPanelDimensions = (slotRef) => {
|
|
47
|
+
if (!slotRef?.current) {
|
|
48
|
+
return { offsetWidth: 0, offsetHeight: 0 }
|
|
49
|
+
}
|
|
50
|
+
const first = slotRef.current.firstElementChild
|
|
51
|
+
if (!first?.classList.contains('im-c-panel') || first.offsetWidth === 0 || first.offsetHeight === 0) {
|
|
52
|
+
return { offsetWidth: 0, offsetHeight: 0 }
|
|
53
|
+
}
|
|
54
|
+
return { offsetWidth: first.offsetWidth, offsetHeight: first.offsetHeight }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Panels are in normal flow inside slot containers, expanding left/right offsetWidth.
|
|
58
|
+
// Query button groups directly to recover the button-only structural column width.
|
|
59
|
+
const getButtonColWidth = (colRef) => {
|
|
60
|
+
const group = colRef?.current?.querySelector('.im-c-button-group')
|
|
61
|
+
return group ? group.offsetWidth : 0
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Returns the panel's contribution to column (left/right) and row (top/bottom) edges.
|
|
65
|
+
//
|
|
66
|
+
// A panel can only trigger BOTH adjacent edges simultaneously when it individually
|
|
67
|
+
// exceeds both the height threshold (h > availableH/RATIO) and the width threshold
|
|
68
|
+
// (w > availableW/RATIO). In that case, only the dominant direction is used, preventing
|
|
69
|
+
// a corner panel from pushing the safe zone into the opposite corner.
|
|
70
|
+
// When only one (or neither) threshold is individually exceeded, the panel contributes
|
|
71
|
+
// its dimensions to both axes freely (combined thresholds still apply later).
|
|
72
|
+
const panelContrib = (w, h, availableW, availableH) => {
|
|
73
|
+
const hFrac = h / availableH
|
|
74
|
+
const wFrac = w / availableW
|
|
75
|
+
if (hFrac >= 1 / RATIO && wFrac >= 1 / RATIO) {
|
|
76
|
+
return hFrac >= wFrac
|
|
77
|
+
? { colH: h, colW: w, rowW: 0, rowH: 0 } // column-primary (left/right)
|
|
78
|
+
: { colH: 0, colW: 0, rowW: w, rowH: h } // row-primary (top/bottom)
|
|
79
|
+
}
|
|
80
|
+
return { colH: h, colW: w, rowW: w, rowH: h }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Determines the column (left/right) inset and the row-axis contributions for both panels.
|
|
84
|
+
//
|
|
85
|
+
// Column coverage uses raw heights so that two panels whose combined height exceeds the
|
|
86
|
+
// threshold always trigger the column inset, even if one is individually row-primary.
|
|
87
|
+
// Exception: a single row-primary panel (no sibling, colH=0) is suppressed so it
|
|
88
|
+
// contributes to top/bottom instead — giving a larger safe zone in the horizontal direction.
|
|
89
|
+
// When triggered, row contributions for both panels are zeroed to prevent double-padding.
|
|
90
|
+
const computeColumn = (panelA, panelB, availableW, availableH, hThreshold, columnLeft, gap) => {
|
|
91
|
+
const a = panelContrib(panelA.offsetWidth, panelA.offsetHeight, availableW, availableH)
|
|
92
|
+
const b = panelContrib(panelB.offsetWidth, panelB.offsetHeight, availableW, availableH)
|
|
93
|
+
const aSingleRowPrimary = panelB.offsetHeight === 0 && a.colH === 0
|
|
94
|
+
const bSingleRowPrimary = panelA.offsetHeight === 0 && b.colH === 0
|
|
95
|
+
const coverage = panelA.offsetHeight + panelB.offsetHeight + (panelA.offsetHeight > 0 && panelB.offsetHeight > 0 ? gap : 0)
|
|
96
|
+
const triggered = coverage > hThreshold && !aSingleRowPrimary && !bSingleRowPrimary
|
|
97
|
+
return {
|
|
98
|
+
panelInset: triggered ? columnLeft + Math.max(panelA.offsetWidth, panelB.offsetWidth) + gap : 0,
|
|
99
|
+
rowA: triggered ? { w: 0, h: 0 } : { w: a.rowW, h: a.rowH },
|
|
100
|
+
rowB: triggered ? { w: 0, h: 0 } : { w: b.rowW, h: b.rowH }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Determines the row-axis (top/bottom) panel inset driven by combined panel width.
|
|
105
|
+
// The combined width test subsumes the individual checks (dimensions are non-negative).
|
|
106
|
+
const computeRow = (leftW, rightW, leftH, rightH, wThreshold, baseInset, gap) =>
|
|
107
|
+
leftW + rightW > wThreshold ? baseInset + Math.max(leftH, rightH) + gap : 0
|
|
108
|
+
|
|
20
109
|
export const getSafeZoneInset = ({
|
|
21
|
-
mainRef,
|
|
22
|
-
|
|
23
|
-
leftRef,
|
|
24
|
-
rightRef,
|
|
25
|
-
actionsRef,
|
|
26
|
-
footerRef
|
|
110
|
+
mainRef, leftRef, rightRef, actionsRef, footerRef,
|
|
111
|
+
leftTopRef, leftBottomRef, rightTopRef, rightBottomRef
|
|
27
112
|
}) => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (refs.some(ref => !ref.current)) {
|
|
113
|
+
if ([mainRef, leftRef, rightRef, actionsRef, footerRef].some(ref => !ref?.current)) {
|
|
31
114
|
return undefined
|
|
32
115
|
}
|
|
33
116
|
|
|
34
|
-
const
|
|
117
|
+
const main = mainRef.current; const left = leftRef.current
|
|
118
|
+
const actions = actionsRef.current; const footer = footerRef.current
|
|
119
|
+
|
|
120
|
+
const gap = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--divider-gap'), 10)
|
|
121
|
+
|
|
122
|
+
const rawTL = getPanelDimensions(leftTopRef); const rawBL = getPanelDimensions(leftBottomRef)
|
|
123
|
+
const rawTR = getPanelDimensions(rightTopRef); const rawBR = getPanelDimensions(rightBottomRef)
|
|
35
124
|
|
|
36
|
-
|
|
37
|
-
const
|
|
125
|
+
// Structural base insets — always present, never capped.
|
|
126
|
+
const colWidth = Math.max(getButtonColWidth(leftRef), getButtonColWidth(rightRef))
|
|
127
|
+
const baseLeft = main.offsetLeft + left.offsetLeft + colWidth + gap
|
|
128
|
+
const baseRight = left.offsetLeft + colWidth + gap
|
|
129
|
+
const baseTop = left.offsetTop
|
|
130
|
+
const footerInset = main.offsetHeight - footer.offsetTop + gap // mirrors --left/right-offset-bottom CSS var
|
|
131
|
+
const baseBottom = Math.max(main.offsetHeight - actions.offsetTop + gap, footerInset)
|
|
38
132
|
|
|
39
|
-
|
|
40
|
-
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
|
|
45
|
-
const isLandscape = availableWidth - insetOverlapWidth > availableHeight - inset.offsetHeight
|
|
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
|
|
48
|
-
const actionsOffset = main.offsetHeight - actions.offsetTop
|
|
49
|
-
const footerOffset = main.offsetHeight - footer.offsetTop
|
|
133
|
+
const availableH = main.offsetHeight - baseTop - baseBottom
|
|
134
|
+
const availableW = main.offsetWidth - (baseLeft - main.offsetLeft) - baseRight
|
|
50
135
|
|
|
51
|
-
const
|
|
52
|
-
const
|
|
136
|
+
const leftCol = computeColumn(rawTL, rawBL, availableW, availableH, availableH / RATIO, main.offsetLeft + left.offsetLeft, gap)
|
|
137
|
+
const rightCol = computeColumn(rawTR, rawBR, availableW, availableH, availableH / RATIO, left.offsetLeft, gap)
|
|
53
138
|
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
139
|
+
const leftPanelInset = leftCol.panelInset
|
|
140
|
+
const rightPanelInset = rightCol.panelInset
|
|
141
|
+
const topPanelInset = computeRow(leftCol.rowA.w, rightCol.rowA.w, leftCol.rowA.h, rightCol.rowA.h, availableW / RATIO, baseTop, gap)
|
|
142
|
+
const bottomPanelInset = computeRow(leftCol.rowB.w, rightCol.rowB.w, leftCol.rowB.h, rightCol.rowB.h, availableW / RATIO, footerInset, gap)
|
|
57
143
|
|
|
58
|
-
|
|
144
|
+
const usableW = main.offsetWidth - 2 * gap
|
|
145
|
+
const usableH = main.offsetHeight - 2 * gap
|
|
146
|
+
const maxL = main.offsetLeft + usableW * (MAX_RATIO - 1) / MAX_RATIO
|
|
147
|
+
const maxR = usableW * (MAX_RATIO - 1) / MAX_RATIO
|
|
148
|
+
const maxV = usableH * (MAX_RATIO - 1) / MAX_RATIO
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
left: Math.max(baseLeft, Math.min(leftPanelInset, maxL)),
|
|
152
|
+
right: Math.max(baseRight, Math.min(rightPanelInset, maxR)),
|
|
153
|
+
top: Math.max(baseTop, Math.min(topPanelInset, maxV)),
|
|
154
|
+
bottom: Math.max(baseBottom, Math.min(bottomPanelInset, maxV))
|
|
155
|
+
}
|
|
59
156
|
}
|
|
@@ -1,133 +1,309 @@
|
|
|
1
1
|
import { getSafeZoneInset } from './getSafeZoneInset'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
9
19
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
actionsRef = { current: { offsetTop: 520 } }
|
|
16
|
-
footerRef = { current: { offsetTop: 550 } }
|
|
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
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
|
|
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 ──────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
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
|
|
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()
|
|
20
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 ───────────────────────────────────────────────────────────────
|
|
21
143
|
|
|
22
|
-
|
|
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 ────────────────────────────────────────────────────────────
|
|
272
|
+
|
|
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
|
|
23
300
|
const result = getSafeZoneInset({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
rightRef,
|
|
28
|
-
actionsRef,
|
|
29
|
-
footerRef
|
|
301
|
+
...base(),
|
|
302
|
+
leftTopRef: panel(ABOVE_W_THRESHOLD, COMBINED_ABOVE),
|
|
303
|
+
leftBottomRef: panel(ABOVE_W_THRESHOLD, COMBINED_ABOVE)
|
|
30
304
|
})
|
|
31
|
-
expect(result).
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
leftRef.current.offsetWidth = 50
|
|
44
|
-
leftRef.current.offsetLeft = 10
|
|
45
|
-
leftRef.current.offsetTop = 10
|
|
46
|
-
|
|
47
|
-
rightRef.current.offsetWidth = 50
|
|
48
|
-
rightRef.current.offsetLeft = 140
|
|
49
|
-
|
|
50
|
-
actionsRef.current.offsetTop = 150
|
|
51
|
-
footerRef.current.offsetTop = 180
|
|
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)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('landscape mode places inset beside panel when enough room', () => {
|
|
83
|
-
mainRef.current.offsetWidth = 1000
|
|
84
|
-
insetRef.current.offsetWidth = 200
|
|
85
|
-
insetRef.current.offsetHeight = 50
|
|
86
|
-
|
|
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
|
|
105
|
-
|
|
106
|
-
expect(result.top).toBe(top)
|
|
107
|
-
expect(result.left).toBe(combinedLeft)
|
|
108
|
-
expect(result.right).toBe(rightOffset)
|
|
109
|
-
expect(result.bottom).toBe(bottom)
|
|
110
|
-
})
|
|
111
|
-
|
|
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
|
|
116
|
-
|
|
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)
|
|
305
|
+
expect(result.left).toBe(leftInset(ABOVE_W_THRESHOLD))
|
|
306
|
+
expect(result.top).toBe(BASE_TOP)
|
|
307
|
+
expect(result.bottom).toBe(BASE_BOTTOM)
|
|
132
308
|
})
|
|
133
309
|
})
|