@defra/interactive-map 0.0.49-alpha → 0.0.50-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/esm/im-core.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/package.json +1 -1
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/src/components/Form/Form.module.scss +10 -1
- package/src/App/hooks/useLayoutMeasurements.js +1 -1
- package/src/App/hooks/useMapProviderOverrides.js +24 -8
- package/src/App/hooks/useMapProviderOverrides.test.js +11 -2
- package/src/utils/getSafeZoneInset.js +26 -62
- package/src/utils/getSafeZoneInset.test.js +36 -64
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.im-c-search-form{border-radius:0;display:none;filter:var(--search-drop-shadow);min-height:auto;padding:0}.im-c-search-form:before{border-radius:0;box-shadow:none}.im-o-app--mobile .im-c-search-form{margin:calc(var(--primary-gap)*-1) calc(var(--primary-gap)*-1) 0;overflow:visible;position:relative}.im-o-app--desktop .im-c-search-form,.im-o-app--tablet .im-c-search-form{border-radius:var(--button-border-radius);box-shadow:none;overflow:visible;position:relative}.im-o-app--desktop .im-c-search-form:before,.im-o-app--tablet .im-c-search-form:before{border-radius:var(--button-border-radius)}.im-o-app--desktop .im-c-search-form .im-c-search-suggestions,.im-o-app--tablet .im-c-search-form .im-c-search-suggestions{left:0;position:absolute;right:0;top:calc(100% + 2px);z-index:1}.im-c-search__status{background-color:var(--background-color);border-bottom-left-radius:var(--button-border-radius);border-bottom-right-radius:var(--button-border-radius);border-top:1px solid var(--button-hover-color);left:0;padding:var(--divider-gap) calc(var(--divider-gap) + 2px);position:absolute;right:0;top:100%}.im-o-app--mobile .im-c-search-form--default-expanded .im-c-search-suggestions{left:0;position:absolute;top:100%;width:100%}.im-c-search-close-button{position:relative}.im-c-search-close-button:before{box-shadow:none}.im-c-search-close-button:hover,.im-c-search-close-button[data-focus-visible=true]{z-index:1}.im-c-search-submit-button{position:relative}.im-c-search-submit-button:before{box-shadow:none}.im-c-search-submit-button[data-focus-visible=true]{z-index:1}.im-c-search-suggestions{background-color:var(--background-color);list-style:none;margin:0;max-height:calc(100vh - 66px);overflow-y:auto;padding:0;scroll-padding:0}.im-c-search-suggestions__item{border-top:1px solid var(--button-hover-color);color:var(--secondary-text-color);cursor:pointer;margin:0;padding:calc(var(--divider-gap) + 2px) var(--primary-gap);position:relative;user-select:none}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-c-search-suggestions__item{color:Highlight}}.im-c-search-suggestions__item mark{background-color:transparent;color:var(--foreground-color);font-weight:700;pointer-events:none}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-c-search-suggestions__item mark{color:Highlight}}@media (hover:hover) and (pointer:fine){.im-c-search-suggestions__item:hover:not([aria-selected=true]){background-color:var(--button-hover-color)}}.im-c-search-suggestions__item[aria-selected=true]{background-color:var(--button-hover-color)}.im-c-search-suggestions__item[aria-selected=true] mark{color:var(--fixed-foreground-color)}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-c-search-suggestions__item[aria-selected=true] mark{color:Highlight}}.im-c-search-suggestions__item:hover:not([aria-selected=true]) .im-c-search-suggestions__label{text-decoration:underline;text-decoration-color:var(--foreground-color);text-decoration-skip-ink:none;text-decoration-thickness:2px;text-underline-offset:2px}.im-c-search-suggestions__item[aria-selected=true] .im-c-search-suggestions__label{background-color:var(--focus-outline-color);box-shadow:0 -2px var(--focus-outline-color),0 4px var(--focus-border-color);color:var(--focus-border-color);outline:3px solid transparent;text-decoration:none}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-c-search-suggestions__item[aria-selected=true] .im-c-search-suggestions__label{color:Highlight;outline-color:Highlight}}.im-c-search-suggestions__label{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.im-o-app--mobile .im-c-search-form--default-expanded .im-c-search-suggestions{box-shadow:var(--panel-box-shadow);clip-path:inset(0 0 -20px 0)}.im-o-app--desktop .im-c-search-suggestions,.im-o-app--tablet .im-c-search-suggestions{border-bottom-left-radius:var(--button-border-radius);border-bottom-right-radius:var(--button-border-radius)}.im-o-app--desktop .im-c-search-suggestions__item,.im-o-app--tablet .im-c-search-suggestions__item{padding:var(--divider-gap) calc(var(--divider-gap) + 2px)}:root{--search-drop-shadow:drop-shadow(0 -2px 20px rgba(0,0,0,.1)) drop-shadow(0 0 12px rgba(0,0,0,.1))}.im-c-search{display:flow-root}.im-c-search--collapsed{display:none}.im-o-app__header:has(>.im-c-search--collapsed:only-child){margin-bottom:0}.im-c-search__input-container{display:flex;flex:1 1 auto;height:var(--button-size);min-height:auto;position:relative}.im-c-search__input-container:before{border:2px solid var(--foreground-color);border-radius:var(--button-border-radius);bottom:-2px;content:"";left:-2px;pointer-events:none;position:absolute;right:-2px;top:-2px;z-index:1}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-c-search__input-container:before{border-color:ButtonText}}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-c-search__input-container:hover:not(:has(.im-c-search-close-button:hover)):before{border-color:Highlight}}.im-c-search__input-container:after{border:var(--focus-border-width) solid var(--focus-border-color);border-radius:calc(var(--button-border-radius) - 2px);bottom:-1px;content:"";left:-1px;opacity:0;outline:var(--focus-box-shadow-width) solid var(--focus-outline-color);pointer-events:none;position:absolute;right:-1px;top:-1px;z-index:99}@media (prefers-reduced-motion:no-preference){.im-c-search__input-container:after{transition:opacity var(--duration) ease}}.im-o-app .im-c-search__input{background-color:transparent;border:0;color:var(--foreground-color);font-size:1rem;outline:0;padding:var(--divider-gap) calc(var(--divider-gap) + 2px);width:100%}.im-o-app .im-c-search__input:focus{box-shadow:none;outline:3px solid transparent}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-o-app .im-c-search__input:focus{outline:none}}.im-o-app .im-c-search__input::placeholder{color:var(--foreground-color)}.im-o-app .im-c-search__input::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none;display:none}.im-o-app .im-c-search__input::-moz-search-clear{display:none}.im-c-search__hint{display:none}.im-o-app--exclusive-control .im-o-app__right .im-c-button-wrapper,.im-o-app--exclusive-control .im-o-app__top .im-c-button-wrapper{opacity:0;z-index:-1}.im-o-app--exclusive-control .im-c-button-wrapper--search{pointer-events:none;position:absolute}.im-o-app--exclusive-control .im-o-app__left-bottom .im-c-panel,.im-o-app--exclusive-control .im-o-app__left-top .im-c-panel,.im-o-app--exclusive-control .im-o-app__right-bottom .im-c-panel,.im-o-app--exclusive-control .im-o-app__right-top .im-c-panel{opacity:0;z-index:-1}.im-o-app--exclusive-control .im-o-app__banner{opacity:0;z-index:-1}.im-o-app--mobile .im-c-search__input-container{margin:var(--primary-gap)}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-c-search__input-container--keyboard-focus-within:before{border-color:Highlight}}.im-c-search__input-container--keyboard-focus-within:after{opacity:1}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-c-search__input-container--keyboard-focus-within:after{opacity:0}.im-c-search__input-container--keyboard-focus-within{outline:var(--focus-highlight-width) solid Highlight}}
|
|
1
|
+
.im-c-search-form{border-radius:0;display:none;filter:var(--search-drop-shadow);min-height:auto;padding:0}.im-c-search-form:before{border-radius:0;box-shadow:none}.im-o-app--mobile .im-c-search-form{margin:calc(var(--primary-gap)*-1) calc(var(--primary-gap)*-1) 0;overflow:visible;position:relative}.im-o-app--desktop .im-c-search-form,.im-o-app--tablet .im-c-search-form{border-radius:var(--button-border-radius);box-shadow:none;overflow:visible;position:relative}.im-o-app--desktop .im-c-search-form:before,.im-o-app--tablet .im-c-search-form:before{border-radius:var(--button-border-radius)}.im-o-app--desktop .im-c-search-form .im-c-search-suggestions,.im-o-app--tablet .im-c-search-form .im-c-search-suggestions{left:0;position:absolute;right:0;top:calc(100% + 2px);z-index:1}.im-c-search__status{background-color:var(--background-color);border-bottom-left-radius:var(--button-border-radius);border-bottom-right-radius:var(--button-border-radius);border-top:1px solid var(--button-hover-color);left:0;padding:var(--divider-gap) calc(var(--divider-gap) + 2px);position:absolute;right:0;top:100%}.im-o-app--mobile .im-c-search-form--default-expanded .im-c-search-suggestions{left:0;position:absolute;top:100%;width:100%}.im-o-app--mobile .im-c-search-form:not(.im-c-search-form--default-expanded) .im-c-search-suggestions{left:0;position:absolute;top:100%;width:100%;z-index:1}.im-c-search-close-button{position:relative}.im-c-search-close-button:before{box-shadow:none}.im-c-search-close-button:hover,.im-c-search-close-button[data-focus-visible=true]{z-index:1}.im-c-search-submit-button{position:relative}.im-c-search-submit-button:before{box-shadow:none}.im-c-search-submit-button[data-focus-visible=true]{z-index:1}.im-c-search-suggestions{background-color:var(--background-color);list-style:none;margin:0;max-height:calc(100vh - 66px);overflow-y:auto;padding:0;scroll-padding:0}.im-c-search-suggestions__item{border-top:1px solid var(--button-hover-color);color:var(--secondary-text-color);cursor:pointer;margin:0;padding:calc(var(--divider-gap) + 2px) var(--primary-gap);position:relative;user-select:none}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-c-search-suggestions__item{color:Highlight}}.im-c-search-suggestions__item mark{background-color:transparent;color:var(--foreground-color);font-weight:700;pointer-events:none}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-c-search-suggestions__item mark{color:Highlight}}@media (hover:hover) and (pointer:fine){.im-c-search-suggestions__item:hover:not([aria-selected=true]){background-color:var(--button-hover-color)}}.im-c-search-suggestions__item[aria-selected=true]{background-color:var(--button-hover-color)}.im-c-search-suggestions__item[aria-selected=true] mark{color:var(--fixed-foreground-color)}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-c-search-suggestions__item[aria-selected=true] mark{color:Highlight}}.im-c-search-suggestions__item:hover:not([aria-selected=true]) .im-c-search-suggestions__label{text-decoration:underline;text-decoration-color:var(--foreground-color);text-decoration-skip-ink:none;text-decoration-thickness:2px;text-underline-offset:2px}.im-c-search-suggestions__item[aria-selected=true] .im-c-search-suggestions__label{background-color:var(--focus-outline-color);box-shadow:0 -2px var(--focus-outline-color),0 4px var(--focus-border-color);color:var(--focus-border-color);outline:3px solid transparent;text-decoration:none}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-c-search-suggestions__item[aria-selected=true] .im-c-search-suggestions__label{color:Highlight;outline-color:Highlight}}.im-c-search-suggestions__label{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.im-o-app--mobile .im-c-search-form--default-expanded .im-c-search-suggestions{box-shadow:var(--panel-box-shadow);clip-path:inset(0 0 -20px 0)}.im-o-app--desktop .im-c-search-suggestions,.im-o-app--tablet .im-c-search-suggestions{border-bottom-left-radius:var(--button-border-radius);border-bottom-right-radius:var(--button-border-radius)}.im-o-app--desktop .im-c-search-suggestions__item,.im-o-app--tablet .im-c-search-suggestions__item{padding:var(--divider-gap) calc(var(--divider-gap) + 2px)}:root{--search-drop-shadow:drop-shadow(0 -2px 20px rgba(0,0,0,.1)) drop-shadow(0 0 12px rgba(0,0,0,.1))}.im-c-search{display:flow-root}.im-c-search--collapsed{display:none}.im-o-app__header:has(>.im-c-search--collapsed:only-child){margin-bottom:0}.im-c-search__input-container{display:flex;flex:1 1 auto;height:var(--button-size);min-height:auto;position:relative}.im-c-search__input-container:before{border:2px solid var(--foreground-color);border-radius:var(--button-border-radius);bottom:-2px;content:"";left:-2px;pointer-events:none;position:absolute;right:-2px;top:-2px;z-index:1}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-c-search__input-container:before{border-color:ButtonText}}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-c-search__input-container:hover:not(:has(.im-c-search-close-button:hover)):before{border-color:Highlight}}.im-c-search__input-container:after{border:var(--focus-border-width) solid var(--focus-border-color);border-radius:calc(var(--button-border-radius) - 2px);bottom:-1px;content:"";left:-1px;opacity:0;outline:var(--focus-box-shadow-width) solid var(--focus-outline-color);pointer-events:none;position:absolute;right:-1px;top:-1px;z-index:99}@media (prefers-reduced-motion:no-preference){.im-c-search__input-container:after{transition:opacity var(--duration) ease}}.im-o-app .im-c-search__input{background-color:transparent;border:0;color:var(--foreground-color);font-size:1rem;outline:0;padding:var(--divider-gap) calc(var(--divider-gap) + 2px);width:100%}.im-o-app .im-c-search__input:focus{box-shadow:none;outline:3px solid transparent}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-o-app .im-c-search__input:focus{outline:none}}.im-o-app .im-c-search__input::placeholder{color:var(--foreground-color)}.im-o-app .im-c-search__input::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none;display:none}.im-o-app .im-c-search__input::-moz-search-clear{display:none}.im-c-search__hint{display:none}.im-o-app--exclusive-control .im-o-app__right .im-c-button-wrapper,.im-o-app--exclusive-control .im-o-app__top .im-c-button-wrapper{opacity:0;z-index:-1}.im-o-app--exclusive-control .im-c-button-wrapper--search{pointer-events:none;position:absolute}.im-o-app--exclusive-control .im-o-app__left-bottom .im-c-panel,.im-o-app--exclusive-control .im-o-app__left-top .im-c-panel,.im-o-app--exclusive-control .im-o-app__right-bottom .im-c-panel,.im-o-app--exclusive-control .im-o-app__right-top .im-c-panel{opacity:0;z-index:-1}.im-o-app--exclusive-control .im-o-app__banner{opacity:0;z-index:-1}.im-o-app--mobile .im-c-search__input-container{margin:var(--primary-gap)}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-c-search__input-container--keyboard-focus-within:before{border-color:Highlight}}.im-c-search__input-container--keyboard-focus-within:after{opacity:1}@media (-ms-high-contrast:active),screen and (forced-colors:active){.im-c-search__input-container--keyboard-focus-within:after{opacity:0}.im-c-search__input-container--keyboard-focus-within{outline:var(--focus-highlight-width) solid Highlight}}
|
|
@@ -57,4 +57,13 @@
|
|
|
57
57
|
top: 100%;
|
|
58
58
|
width: 100%;
|
|
59
59
|
left: 0;
|
|
60
|
-
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Tap-to-open mobile search reflows the header otherwise, inflating the safe zone while suggestions are open.
|
|
63
|
+
.im-o-app--mobile .im-c-search-form:not(.im-c-search-form--default-expanded) .im-c-search-suggestions {
|
|
64
|
+
position: absolute;
|
|
65
|
+
top: 100%;
|
|
66
|
+
width: 100%;
|
|
67
|
+
left: 0;
|
|
68
|
+
z-index: 1;
|
|
69
|
+
}
|
|
@@ -148,7 +148,7 @@ function applyAttributionsLayout ({ appContainer, bottom, attributions, dividerG
|
|
|
148
148
|
* for `fitBounds`/`setView`. Waits for `arePluginsEvaluated` so the inset reflects final
|
|
149
149
|
* button visibility rather than a mid-evaluation state, which would make the map jump.
|
|
150
150
|
*/
|
|
151
|
-
function calculateLayout (layoutRefs, breakpoint) {
|
|
151
|
+
export function calculateLayout (layoutRefs, breakpoint) {
|
|
152
152
|
const {
|
|
153
153
|
appContainerRef, mainRef, topRef, topLeftColRef, topRightColRef,
|
|
154
154
|
bottomRef, attributionsRef, bottomRightRef, leftTopRef, leftBottomRef,
|
|
@@ -4,18 +4,26 @@ import { useApp } from '../store/appContext.js'
|
|
|
4
4
|
import { useMap } from '../store/mapContext.js'
|
|
5
5
|
import { EVENTS as events } from '../../config/events.js'
|
|
6
6
|
import { getSafeZoneInset } from '../../utils/getSafeZoneInset.js'
|
|
7
|
+
import { calculateLayout } from './useLayoutMeasurements.js'
|
|
7
8
|
import { scalePoints } from '../../utils/scalePoints.js'
|
|
8
9
|
import { scaleFactor } from '../../config/appConfig.js'
|
|
9
10
|
|
|
10
11
|
export const useMapProviderOverrides = () => {
|
|
11
12
|
const { mapProvider, eventBus } = useConfig()
|
|
12
|
-
const { dispatch: appDispatch, layoutRefs } = useApp()
|
|
13
|
+
const { dispatch: appDispatch, layoutRefs, breakpoint } = useApp()
|
|
13
14
|
const { mapSize } = useMap()
|
|
14
15
|
|
|
15
16
|
const latestMapSize = useRef(mapSize)
|
|
16
17
|
latestMapSize.current = mapSize
|
|
18
|
+
const latestBreakpoint = useRef(breakpoint)
|
|
19
|
+
latestBreakpoint.current = breakpoint
|
|
17
20
|
|
|
18
21
|
const updatePadding = () => {
|
|
22
|
+
// Refresh the derived layout CSS vars (e.g. --left-offset-top) before reading them —
|
|
23
|
+
// callers often trigger this right after a state change that resizes the header/columns
|
|
24
|
+
// (e.g. collapsing mobile search), and those vars are only otherwise refreshed by a
|
|
25
|
+
// separate ResizeObserver callback that isn't guaranteed to have run yet.
|
|
26
|
+
calculateLayout(layoutRefs, latestBreakpoint.current)
|
|
19
27
|
const safeZoneInset = getSafeZoneInset(layoutRefs)
|
|
20
28
|
const padding = scalePoints(safeZoneInset, scaleFactor[latestMapSize.current])
|
|
21
29
|
|
|
@@ -43,12 +51,18 @@ export const useMapProviderOverrides = () => {
|
|
|
43
51
|
return undefined
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
updatePadding()
|
|
54
|
+
if (skipPaddingCalc) {
|
|
55
|
+
return originalFitToBounds.call(mapProvider, bounds)
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
|
|
58
|
+
// Callers often dispatch layout-affecting state (e.g. collapsing the mobile search
|
|
59
|
+
// form) immediately before calling this, so measuring on the same tick can capture
|
|
60
|
+
// the DOM mid-transition. Wait a frame so React has painted first.
|
|
61
|
+
requestAnimationFrame(() => {
|
|
62
|
+
updatePadding()
|
|
63
|
+
originalFitToBounds.call(mapProvider, bounds)
|
|
64
|
+
})
|
|
65
|
+
return undefined
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
const originalSetView = mapProvider.setView
|
|
@@ -58,9 +72,11 @@ export const useMapProviderOverrides = () => {
|
|
|
58
72
|
return undefined
|
|
59
73
|
}
|
|
60
74
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
75
|
+
requestAnimationFrame(() => {
|
|
76
|
+
updatePadding()
|
|
77
|
+
originalSetView.call(mapProvider, { center, zoom })
|
|
78
|
+
})
|
|
79
|
+
return undefined
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
return () => {
|
|
@@ -4,6 +4,7 @@ import { useConfig } from '../store/configContext.js'
|
|
|
4
4
|
import { useApp } from '../store/appContext.js'
|
|
5
5
|
import { useMap } from '../store/mapContext.js'
|
|
6
6
|
import { getSafeZoneInset } from '../../utils/getSafeZoneInset.js'
|
|
7
|
+
import { calculateLayout } from './useLayoutMeasurements.js'
|
|
7
8
|
import { scalePoints } from '../../utils/scalePoints.js'
|
|
8
9
|
import { scaleFactor } from '../../config/appConfig.js'
|
|
9
10
|
|
|
@@ -11,6 +12,7 @@ jest.mock('../store/configContext.js')
|
|
|
11
12
|
jest.mock('../store/appContext.js')
|
|
12
13
|
jest.mock('../store/mapContext.js')
|
|
13
14
|
jest.mock('../../utils/getSafeZoneInset.js')
|
|
15
|
+
jest.mock('./useLayoutMeasurements.js')
|
|
14
16
|
jest.mock('../../utils/scalePoints.js')
|
|
15
17
|
|
|
16
18
|
const setup = (overrides = {}) => {
|
|
@@ -30,7 +32,7 @@ const setup = (overrides = {}) => {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
useConfig.mockReturnValue({ mapProvider, eventBus, ...overrides.config })
|
|
33
|
-
useApp.mockReturnValue({ dispatch, layoutRefs, ...overrides.app })
|
|
35
|
+
useApp.mockReturnValue({ dispatch, layoutRefs, breakpoint: 'mobile', ...overrides.app })
|
|
34
36
|
useMap.mockReturnValue({ mapSize: 'md', ...overrides.map })
|
|
35
37
|
|
|
36
38
|
getSafeZoneInset.mockReturnValue({ top: 10, right: 5, bottom: 15, left: 5 })
|
|
@@ -40,7 +42,13 @@ const setup = (overrides = {}) => {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
describe('useMapProviderOverrides', () => {
|
|
43
|
-
beforeEach(
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
jest.clearAllMocks()
|
|
47
|
+
jest.spyOn(global, 'requestAnimationFrame').mockImplementation(cb => cb())
|
|
48
|
+
})
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
global.requestAnimationFrame.mockRestore()
|
|
51
|
+
})
|
|
44
52
|
|
|
45
53
|
test('early returns when mapProvider is null', () => {
|
|
46
54
|
setup({ config: { mapProvider: null } })
|
|
@@ -57,6 +65,7 @@ describe('useMapProviderOverrides', () => {
|
|
|
57
65
|
|
|
58
66
|
mapProvider.fitToBounds([0, 0, 1, 1])
|
|
59
67
|
|
|
68
|
+
expect(calculateLayout).toHaveBeenCalledWith(layoutRefs, 'mobile')
|
|
60
69
|
expect(getSafeZoneInset).toHaveBeenCalledWith(layoutRefs)
|
|
61
70
|
expect(scalePoints).toHaveBeenCalledWith(safeZone, scaleFactor.lg)
|
|
62
71
|
expect(dispatch).toHaveBeenCalledWith({
|
|
@@ -6,22 +6,15 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Each edge inset is driven by:
|
|
8
8
|
* - A structural baseline from the button columns, footer, and action bar.
|
|
9
|
-
* - A panel contribution
|
|
10
|
-
* of the available map
|
|
9
|
+
* - A panel contribution once a panel (or the two panels stacked in a side's
|
|
10
|
+
* slots) covers more than 1/RATIO of the available map height (widening
|
|
11
|
+
* that side's left/right inset) and/or more than 1/RATIO of the available
|
|
12
|
+
* map width (pushing the top/bottom inset down/up). Both can trigger at
|
|
13
|
+
* once for a panel that's both tall and wide — independently, not
|
|
14
|
+
* exclusively, since the MAX_RATIO cap below already bounds each edge.
|
|
11
15
|
* - A cap so no single inset exceeds 1/MAX_RATIO of the map dimension,
|
|
12
16
|
* preventing fitBounds from zooming out to a corner when panels are large.
|
|
13
17
|
*
|
|
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
18
|
* @param {Object} refs - React refs from layoutRefs.
|
|
26
19
|
* @param {React.RefObject} refs.mainRef - Main map container.
|
|
27
20
|
* @param {React.RefObject} refs.leftRef - Left button column.
|
|
@@ -36,7 +29,7 @@
|
|
|
36
29
|
* Pixel insets from each edge of the main area, or undefined if any required ref is missing.
|
|
37
30
|
*/
|
|
38
31
|
|
|
39
|
-
const RATIO = 2 // panels covering more than 1/RATIO of
|
|
32
|
+
const RATIO = 2 // panels covering more than 1/RATIO of the available height/width trigger that edge's inset
|
|
40
33
|
const MAX_RATIO = 3 // each inset is capped at 1/MAX_RATIO of its map dimension
|
|
41
34
|
|
|
42
35
|
// Query the panel element directly within its slot container.
|
|
@@ -61,50 +54,24 @@ const getButtonColWidth = (colRef) => {
|
|
|
61
54
|
return group ? group.offsetWidth : 0
|
|
62
55
|
}
|
|
63
56
|
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
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
|
|
57
|
+
// Determines the column (left/right) inset from the two panels stacked in that side's
|
|
58
|
+
// slots. Combined height exceeding the threshold triggers the inset, widened to fit
|
|
59
|
+
// whichever panel is wider.
|
|
60
|
+
const computeColumn = (panelA, panelB, hThreshold, columnLeft, gap) => {
|
|
95
61
|
const coverage = panelA.offsetHeight + panelB.offsetHeight + (panelA.offsetHeight > 0 && panelB.offsetHeight > 0 ? gap : 0)
|
|
96
|
-
const triggered = coverage > hThreshold
|
|
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
|
-
}
|
|
62
|
+
const triggered = coverage > hThreshold
|
|
63
|
+
return triggered ? columnLeft + Math.max(panelA.offsetWidth, panelB.offsetWidth) + gap : 0
|
|
102
64
|
}
|
|
103
65
|
|
|
104
|
-
// Determines the row
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
66
|
+
// Determines the row (top/bottom) inset from the panels docked in the left and right
|
|
67
|
+
// columns' near slot (both top, or both bottom). Combined width exceeding the threshold
|
|
68
|
+
// triggers the inset, deepened to fit whichever panel is taller. Evaluated independently
|
|
69
|
+
// of computeColumn — a panel can widen its own column AND push top/bottom down at once.
|
|
70
|
+
const computeRow = (leftPanel, rightPanel, wThreshold, baseInset, gap) => {
|
|
71
|
+
const coverage = leftPanel.offsetWidth + rightPanel.offsetWidth + (leftPanel.offsetWidth > 0 && rightPanel.offsetWidth > 0 ? gap : 0)
|
|
72
|
+
const triggered = coverage > wThreshold
|
|
73
|
+
return triggered ? baseInset + Math.max(leftPanel.offsetHeight, rightPanel.offsetHeight) + gap : 0
|
|
74
|
+
}
|
|
108
75
|
|
|
109
76
|
export const getSafeZoneInset = ({
|
|
110
77
|
mainRef, leftRef, rightRef, actionsRef, bottomRef,
|
|
@@ -135,13 +102,10 @@ export const getSafeZoneInset = ({
|
|
|
135
102
|
const availableH = main.offsetHeight - baseTop - baseBottom
|
|
136
103
|
const availableW = main.offsetWidth - (baseLeft - main.offsetLeft) - baseRight
|
|
137
104
|
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
const rightPanelInset = rightCol.panelInset
|
|
143
|
-
const topPanelInset = computeRow(leftCol.rowA.w, rightCol.rowA.w, leftCol.rowA.h, rightCol.rowA.h, availableW / RATIO, baseTop, gap)
|
|
144
|
-
const bottomPanelInset = computeRow(leftCol.rowB.w, rightCol.rowB.w, leftCol.rowB.h, rightCol.rowB.h, availableW / RATIO, bottomInset, gap)
|
|
105
|
+
const leftPanelInset = computeColumn(rawTL, rawBL, availableH / RATIO, main.offsetLeft + left.offsetLeft, gap)
|
|
106
|
+
const rightPanelInset = computeColumn(rawTR, rawBR, availableH / RATIO, left.offsetLeft, gap)
|
|
107
|
+
const topPanelInset = computeRow(rawTL, rawTR, availableW / RATIO, baseTop, gap)
|
|
108
|
+
const bottomPanelInset = computeRow(rawBL, rawBR, availableW / RATIO, bottomInset, gap)
|
|
145
109
|
|
|
146
110
|
const usableW = main.offsetWidth - 2 * gap
|
|
147
111
|
const usableH = main.offsetHeight - 2 * gap
|
|
@@ -25,13 +25,10 @@ const COMBINED_BELOW = 100 // two × 100 + gap = 208 < 236
|
|
|
25
25
|
|
|
26
26
|
// Width threshold: availableWidth / RATIO = (900-58-58)/2 = 392
|
|
27
27
|
const ABOVE_W_THRESHOLD = 400
|
|
28
|
-
const
|
|
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
|
|
28
|
+
const COMBINED_BELOW_W = 180
|
|
32
29
|
|
|
33
|
-
const ABOVE_CAP_TOP = 330 // 60+330+8=398 > CAP_HEIGHT ≈ 389.3 → capped
|
|
34
30
|
const BOTTOM_INSET = MAIN_HEIGHT - BOTTOM_TOP + GAP // 48: divider-gap above top of bottom container
|
|
31
|
+
const ABOVE_CAP_TOP = 330 // 60+330+8=398 > CAP_HEIGHT ≈ 389.3 → capped
|
|
35
32
|
const ABOVE_CAP_BOTTOM = 342 // 48+342+8=398 > CAP_HEIGHT → capped
|
|
36
33
|
|
|
37
34
|
const PANEL_W_STANDARD = 200
|
|
@@ -42,6 +39,7 @@ const PANEL_W_XLARGE = 600 // 10+600+8=618 > CAP_WIDTH ≈ 589.3 → capped
|
|
|
42
39
|
const leftInset = w => LEFT_LEFT + w + GAP
|
|
43
40
|
const rightInset = w => LEFT_LEFT + w + GAP
|
|
44
41
|
const topInset = h => BASE_TOP + h + GAP
|
|
42
|
+
const bottomInset = h => BOTTOM_INSET + h + GAP
|
|
45
43
|
|
|
46
44
|
const MAX_RATIO = 3
|
|
47
45
|
const CAP_WIDTH = (MAIN_WIDTH - 2 * GAP) * (MAX_RATIO - 1) / MAX_RATIO
|
|
@@ -192,46 +190,22 @@ describe('getSafeZoneInset — right edge', () => {
|
|
|
192
190
|
})
|
|
193
191
|
|
|
194
192
|
// ─── Top edge ────────────────────────────────────────────────────────────────
|
|
195
|
-
// Trigger is WIDTH-based
|
|
193
|
+
// Trigger is WIDTH-based, evaluated independently of the column (height-based) trigger.
|
|
196
194
|
|
|
197
195
|
describe('getSafeZoneInset — top edge', () => {
|
|
198
196
|
it('does not trigger when top panel is narrow, even if tall', () => {
|
|
199
|
-
// PANEL_W_STANDARD (200) < width threshold (392)
|
|
200
197
|
expect(getSafeZoneInset({ ...base(), rightTopRef: panel(PANEL_W_STANDARD, ABOVE_THRESHOLD) }).top).toBe(BASE_TOP)
|
|
201
198
|
})
|
|
202
|
-
it('triggers when a top panel
|
|
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
|
|
199
|
+
it('triggers when a top panel is wide enough, capped at (MAX_RATIO-1)/MAX_RATIO of usable height', () => {
|
|
208
200
|
const result = getSafeZoneInset({ ...base(), leftTopRef: panel(ABOVE_W_THRESHOLD, ABOVE_CAP_TOP) })
|
|
201
|
+
expect(result.top).toBe(CAP_HEIGHT)
|
|
209
202
|
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
203
|
})
|
|
230
|
-
it('does not trigger when
|
|
204
|
+
it('does not trigger when combined width of two top panels is below threshold', () => {
|
|
231
205
|
expect(getSafeZoneInset({
|
|
232
206
|
...base(),
|
|
233
|
-
leftTopRef: panel(COMBINED_BELOW_W,
|
|
234
|
-
rightTopRef: panel(COMBINED_BELOW_W,
|
|
207
|
+
leftTopRef: panel(COMBINED_BELOW_W, BELOW_THRESHOLD),
|
|
208
|
+
rightTopRef: panel(COMBINED_BELOW_W, BELOW_THRESHOLD)
|
|
235
209
|
}).top).toBe(BASE_TOP)
|
|
236
210
|
})
|
|
237
211
|
})
|
|
@@ -242,28 +216,21 @@ describe('getSafeZoneInset — bottom edge', () => {
|
|
|
242
216
|
it('does not trigger when bottom panel is narrow, even if tall', () => {
|
|
243
217
|
expect(getSafeZoneInset({ ...base(), rightBottomRef: panel(PANEL_W_STANDARD, ABOVE_THRESHOLD) }).bottom).toBe(BASE_BOTTOM)
|
|
244
218
|
})
|
|
245
|
-
it('triggers when a bottom panel
|
|
246
|
-
expect(getSafeZoneInset({ ...base(), leftBottomRef: panel(ABOVE_W_THRESHOLD, ABOVE_THRESHOLD) }).bottom)
|
|
247
|
-
.toBe(Math.min(BOTTOM_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
|
|
219
|
+
it('triggers when a bottom panel is wide enough, capped at (MAX_RATIO-1)/MAX_RATIO of usable height', () => {
|
|
251
220
|
const result = getSafeZoneInset({ ...base(), leftBottomRef: panel(ABOVE_W_THRESHOLD, ABOVE_CAP_BOTTOM) })
|
|
221
|
+
expect(result.bottom).toBe(CAP_HEIGHT)
|
|
252
222
|
expect(result.left).toBe(leftInset(ABOVE_W_THRESHOLD))
|
|
253
|
-
expect(result.bottom).toBe(BASE_BOTTOM)
|
|
254
223
|
})
|
|
255
|
-
it('triggers when
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
rightBottomRef: panel(COMBINED_ABOVE_W, PANEL_H_SHORT)
|
|
260
|
-
}).bottom).toBe(Math.min(BOTTOM_INSET + PANEL_H_TALL + GAP, CAP_HEIGHT))
|
|
224
|
+
it('triggers when a bottom panel is wide enough, using the taller of the two slots', () => {
|
|
225
|
+
const result = getSafeZoneInset({ ...base(), rightBottomRef: panel(ABOVE_W_THRESHOLD, ABOVE_THRESHOLD) })
|
|
226
|
+
expect(result.bottom).toBe(bottomInset(ABOVE_THRESHOLD))
|
|
227
|
+
expect(result.right).toBe(rightInset(ABOVE_W_THRESHOLD))
|
|
261
228
|
})
|
|
262
|
-
it('does not trigger when
|
|
229
|
+
it('does not trigger when combined width of two bottom panels is below threshold', () => {
|
|
263
230
|
expect(getSafeZoneInset({
|
|
264
231
|
...base(),
|
|
265
|
-
leftBottomRef: panel(COMBINED_BELOW_W,
|
|
266
|
-
rightBottomRef: panel(COMBINED_BELOW_W,
|
|
232
|
+
leftBottomRef: panel(COMBINED_BELOW_W, BELOW_THRESHOLD),
|
|
233
|
+
rightBottomRef: panel(COMBINED_BELOW_W, BELOW_THRESHOLD)
|
|
267
234
|
}).bottom).toBe(BASE_BOTTOM)
|
|
268
235
|
})
|
|
269
236
|
})
|
|
@@ -279,30 +246,35 @@ describe('getSafeZoneInset — MAX_RATIO cap', () => {
|
|
|
279
246
|
})
|
|
280
247
|
})
|
|
281
248
|
|
|
282
|
-
// ───
|
|
249
|
+
// ─── Row/column independence ───────────────────────────────────────────────────
|
|
250
|
+
// A panel that is both tall enough and wide enough triggers BOTH its column (left/
|
|
251
|
+
// right) and row (top/bottom) insets at once — not an exclusive either/or pick.
|
|
252
|
+
// MAX_RATIO (tested above) is what keeps this from consuming the whole viewport.
|
|
283
253
|
|
|
284
|
-
describe('getSafeZoneInset —
|
|
285
|
-
it('narrow-but-tall
|
|
286
|
-
// PANEL_W_STANDARD (200): tall enough for right, too narrow for top (< 392)
|
|
254
|
+
describe('getSafeZoneInset — row/column independence', () => {
|
|
255
|
+
it('narrow-but-tall panel triggers only its column inset, not the row', () => {
|
|
287
256
|
const result = getSafeZoneInset({ ...base(), rightTopRef: panel(PANEL_W_STANDARD, ABOVE_THRESHOLD) })
|
|
288
257
|
expect(result.right).toBe(rightInset(PANEL_W_STANDARD))
|
|
289
258
|
expect(result.top).toBe(BASE_TOP)
|
|
290
259
|
})
|
|
291
|
-
it('wide-
|
|
292
|
-
|
|
293
|
-
|
|
260
|
+
it('wide-but-short panel triggers only its row inset, not the column', () => {
|
|
261
|
+
const result = getSafeZoneInset({ ...base(), leftTopRef: panel(ABOVE_W_THRESHOLD, BELOW_THRESHOLD) })
|
|
262
|
+
expect(result.top).toBe(topInset(BELOW_THRESHOLD))
|
|
294
263
|
expect(result.left).toBe(BASE_LEFT)
|
|
264
|
+
})
|
|
265
|
+
it('wide-and-tall panel triggers both its column and row insets at once', () => {
|
|
266
|
+
const result = getSafeZoneInset({ ...base(), leftTopRef: panel(ABOVE_W_THRESHOLD, ABOVE_THRESHOLD) })
|
|
267
|
+
expect(result.left).toBe(leftInset(ABOVE_W_THRESHOLD))
|
|
295
268
|
expect(result.top).toBe(topInset(ABOVE_THRESHOLD))
|
|
296
269
|
})
|
|
297
|
-
it('two
|
|
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
|
|
270
|
+
it('two narrow panels in the same column collectively trigger left, independent of row', () => {
|
|
271
|
+
// Each h=COMBINED_ABOVE (120) < hThreshold individually, combined 248 > 236 → left triggers
|
|
300
272
|
const result = getSafeZoneInset({
|
|
301
273
|
...base(),
|
|
302
|
-
leftTopRef: panel(
|
|
303
|
-
leftBottomRef: panel(
|
|
274
|
+
leftTopRef: panel(PANEL_W_STANDARD, COMBINED_ABOVE),
|
|
275
|
+
leftBottomRef: panel(PANEL_W_STANDARD, COMBINED_ABOVE)
|
|
304
276
|
})
|
|
305
|
-
expect(result.left).toBe(leftInset(
|
|
277
|
+
expect(result.left).toBe(leftInset(PANEL_W_STANDARD))
|
|
306
278
|
expect(result.top).toBe(BASE_TOP)
|
|
307
279
|
expect(result.bottom).toBe(BASE_BOTTOM)
|
|
308
280
|
})
|