@defra/interactive-map 0.0.17-alpha → 0.0.19-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 +58 -34
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/context.md +53 -7
- package/docs/api/map-style-config.md +41 -2
- package/docs/api/marker-config.md +53 -11
- package/docs/api/panel-definition.md +16 -0
- package/docs/api/symbol-config.md +160 -0
- package/docs/api/symbol-registry.md +115 -0
- package/docs/api.md +50 -23
- package/docs/assets/basic-map.jpg +0 -0
- package/docs/assets/button-first.jpg +0 -0
- package/docs/assets/maker-panel.jpg +0 -0
- package/docs/examples/add-marker-with-panel.mdx +59 -0
- package/docs/examples/basic-map.mdx +24 -0
- package/docs/examples/button-map.mdx +24 -0
- package/docs/examples/index.mdx +49 -0
- package/docs/index.mdx +1 -1
- package/docs/plugins/datasets.md +105 -9
- package/docs/plugins/interact.md +100 -44
- package/docs/plugins/search.md +15 -3
- package/docs/plugins.md +1 -1
- package/docusaurus.config.cjs +9 -1
- package/package.json +1 -1
- package/plugins/beta/datasets/dist/css/index.css +32 -14
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/esm/index.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/index.js +1 -1
- package/plugins/beta/datasets/src/DatasetsInit.jsx +9 -4
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +57 -11
- package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +14 -8
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +155 -53
- package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +27 -0
- package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +31 -0
- package/plugins/beta/datasets/src/api/addDataset.js +1 -1
- package/plugins/beta/datasets/src/api/setData.js +4 -2
- package/plugins/beta/datasets/src/api/setStyle.js +2 -2
- package/plugins/beta/datasets/src/components/EmptyKey.jsx +7 -0
- package/plugins/beta/datasets/src/components/EmptyKey.test.jsx +21 -0
- package/plugins/beta/datasets/src/components/KeySvg.jsx +24 -0
- package/plugins/beta/datasets/src/components/KeySvgLine.jsx +19 -0
- package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +15 -0
- package/plugins/beta/datasets/src/components/KeySvgRect.jsx +22 -0
- package/plugins/beta/datasets/src/components/KeySvgSymbol.jsx +16 -0
- package/plugins/beta/datasets/src/components/svgProperties.js +20 -0
- package/plugins/beta/datasets/src/datasets.js +13 -4
- package/plugins/beta/datasets/src/defaults.js +4 -2
- package/plugins/beta/datasets/src/index.js +2 -1
- package/plugins/beta/datasets/src/manifest.js +1 -1
- package/plugins/beta/datasets/src/panels/Key.jsx +11 -89
- package/plugins/beta/datasets/src/panels/Key.module.scss +24 -13
- package/plugins/beta/datasets/src/panels/Layers.module.scss +13 -7
- package/plugins/beta/datasets/src/reducer.js +6 -0
- package/plugins/beta/datasets/src/reducers/keyReducer.js +34 -0
- package/plugins/beta/datasets/src/utils/mergeSublayer.js +8 -0
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/DrawInit.jsx +3 -2
- package/plugins/beta/draw-ml/dist/css/index.css +3 -0
- 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/dist/umd/index.js +1 -1
- package/plugins/beta/draw-ml/src/DrawInit.jsx +4 -3
- 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/dist/umd/index.js +1 -1
- package/plugins/beta/map-styles/src/MapStyles.jsx +5 -4
- package/plugins/beta/map-styles/src/MapStylesInit.jsx +5 -4
- package/plugins/beta/scale-bar/dist/css/index.css +1 -1
- package/plugins/beta/scale-bar/src/scaleBar.scss +1 -0
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/index.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +19 -8
- package/plugins/interact/src/InteractInit.test.js +26 -6
- package/plugins/interact/src/api/clear.js +1 -1
- package/plugins/interact/src/api/enable.test.js +7 -7
- package/plugins/interact/src/api/selectMarker.js +14 -0
- package/plugins/interact/src/api/selectMarker.test.js +25 -0
- package/plugins/interact/src/api/unselectMarker.js +14 -0
- package/plugins/interact/src/api/unselectMarker.test.js +14 -0
- package/plugins/interact/src/defaults.js +4 -6
- package/plugins/interact/src/events.js +27 -36
- package/plugins/interact/src/events.test.js +119 -90
- package/plugins/interact/src/hooks/useHighlightSync.js +3 -3
- package/plugins/interact/src/hooks/useHighlightSync.test.js +6 -6
- package/plugins/interact/src/hooks/useHoverCursor.js +10 -0
- package/plugins/interact/src/hooks/useHoverCursor.test.js +44 -0
- package/plugins/interact/src/hooks/useInteractionHandlers.js +111 -69
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +147 -32
- package/plugins/interact/src/manifest.js +10 -2
- package/plugins/interact/src/reducer.js +59 -5
- package/plugins/interact/src/reducer.test.js +100 -12
- package/plugins/interact/src/utils/buildStylesMap.js +17 -4
- package/plugins/interact/src/utils/buildStylesMap.test.js +16 -2
- package/plugins/interact/src/utils/featureQueries.js +11 -6
- package/plugins/interact/src/utils/featureQueries.test.js +8 -1
- package/plugins/interact/src/utils/interactionModes.js +12 -0
- 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/Search.jsx +3 -1
- package/plugins/search/src/events/fetchSuggestions.js +6 -4
- package/plugins/search/src/events/fetchSuggestions.test.js +26 -4
- package/plugins/search/src/events/formHandlers.js +3 -3
- package/plugins/search/src/events/formHandlers.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/utils/updateMap.js +3 -3
- package/plugins/search/src/utils/updateMap.test.js +3 -3
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/appEvents.js +7 -0
- package/providers/maplibre/src/appEvents.test.js +18 -4
- package/providers/maplibre/src/maplibreProvider.js +52 -0
- package/providers/maplibre/src/maplibreProvider.test.js +105 -1
- package/providers/maplibre/src/utils/highlightFeatures.js +36 -7
- package/providers/maplibre/src/utils/highlightFeatures.test.js +153 -96
- package/providers/maplibre/src/utils/hoverCursor.js +61 -0
- package/providers/maplibre/src/utils/hoverCursor.test.js +130 -0
- package/providers/maplibre/src/utils/patternImages.js +70 -0
- package/providers/maplibre/src/utils/patternImages.test.js +180 -0
- package/providers/maplibre/src/utils/queryFeatures.js +38 -16
- package/providers/maplibre/src/utils/queryFeatures.test.js +20 -3
- package/providers/maplibre/src/utils/rasteriseToImageData.js +30 -0
- package/providers/maplibre/src/utils/rasteriseToImageData.test.js +69 -0
- package/providers/maplibre/src/utils/symbolImages.js +147 -0
- package/providers/maplibre/src/utils/symbolImages.test.js +248 -0
- package/src/App/components/Markers/Markers.jsx +122 -27
- package/src/App/components/Markers/Markers.module.scss +0 -10
- package/src/App/components/Markers/Markers.test.jsx +246 -0
- package/src/App/components/Panel/Panel.jsx +6 -6
- package/src/App/components/Panel/Panel.test.jsx +37 -0
- package/src/App/components/Viewport/Viewport.jsx +5 -15
- package/src/App/components/Viewport/Viewport.module.scss +2 -0
- package/src/App/components/Viewport/Viewport.test.jsx +16 -33
- package/src/App/hooks/useInterfaceAPI.js +7 -7
- package/src/App/hooks/useInterfaceAPI.test.js +162 -0
- package/src/App/hooks/useLayoutMeasurements.js +64 -72
- package/src/App/hooks/useMarkersAPI.js +2 -5
- package/src/App/hooks/useMarkersAPI.test.js +4 -4
- package/src/App/layout/Layout.jsx +3 -3
- package/src/App/layout/Layout.test.jsx +4 -2
- package/src/App/layout/layout.module.scss +1 -8
- package/src/App/renderer/HtmlElementHost.jsx +10 -5
- package/src/App/renderer/mapPanels.js +2 -1
- package/src/App/store/ServiceProvider.jsx +7 -5
- package/src/App/store/appActionsMap.js +4 -4
- package/src/App/store/appActionsMap.test.js +10 -0
- package/src/App/store/mapActionsMap.js +4 -6
- package/src/App/store/mapActionsMap.test.js +3 -2
- package/src/App/store/mapReducer.js +2 -1
- package/src/InteractiveMap/InteractiveMap.js +59 -11
- package/src/InteractiveMap/InteractiveMap.test.js +126 -4
- package/src/InteractiveMap/domStateManager.js +18 -6
- package/src/InteractiveMap/domStateManager.test.js +21 -0
- package/src/InteractiveMap/historyManager.js +28 -16
- package/src/InteractiveMap/historyManager.test.js +17 -0
- package/src/config/appConfig.js +2 -7
- package/src/config/appConfig.test.js +4 -15
- package/src/config/defaults.js +2 -3
- package/src/config/events.js +20 -21
- package/src/config/mapTheme.js +56 -0
- package/src/config/patternConfig.js +16 -0
- package/src/config/symbolConfig.js +80 -0
- package/src/scss/settings/_colors.scss +0 -9
- package/src/services/closeApp.js +1 -10
- package/src/services/closeApp.test.js +3 -43
- package/src/services/patternRegistry.js +40 -0
- package/src/services/patternRegistry.test.js +48 -0
- package/src/services/symbolRegistry.js +113 -0
- package/src/services/symbolRegistry.test.js +262 -0
- package/src/types.js +99 -12
- package/src/utils/mapStateSync.js +48 -10
- package/src/utils/mapStateSync.test.js +29 -9
- package/src/utils/patternUtils.js +94 -0
- package/src/utils/patternUtils.test.js +160 -0
- package/src/utils/symbolUtils.js +85 -0
- package/src/utils/symbolUtils.test.js +156 -0
- package/docs/examples.mdx +0 -70
- package/plugins/beta/datasets/src/adapters/maplibre/patternRegistry.js +0 -48
- package/plugins/beta/datasets/src/styles/patterns.js +0 -157
|
@@ -6,12 +6,12 @@ import { useModalPanelBehaviour } from '../../hooks/useModalPanelBehaviour.js'
|
|
|
6
6
|
import { useIsScrollable } from '../../hooks/useIsScrollable.js'
|
|
7
7
|
import { Icon } from '../Icon/Icon'
|
|
8
8
|
|
|
9
|
-
const computePanelState = (bpConfig, triggeringElement) => {
|
|
9
|
+
const computePanelState = (bpConfig, triggeringElement, focus, focusOnOpen) => {
|
|
10
10
|
const isAside = bpConfig.slot === 'side' && bpConfig.open && !bpConfig.modal
|
|
11
11
|
const isDialog = !isAside && bpConfig.dismissible
|
|
12
12
|
const isModal = bpConfig.modal === true
|
|
13
13
|
const isDismissible = bpConfig.dismissible !== false
|
|
14
|
-
const shouldFocus =
|
|
14
|
+
const shouldFocus = isModal || (focusOnOpen !== false && (focusOnOpen === true || focus === true || Boolean(triggeringElement)))
|
|
15
15
|
const buttonContainerEl = bpConfig.slot.endsWith('button') ? triggeringElement?.parentNode : undefined
|
|
16
16
|
return { isAside, isDialog, isModal, isDismissible, shouldFocus, buttonContainerEl }
|
|
17
17
|
}
|
|
@@ -58,15 +58,15 @@ const buildBodyProps = ({ bodyRef, panelBodyClass, isBodyScrollable, elementId }
|
|
|
58
58
|
|
|
59
59
|
// eslint-disable-next-line camelcase, react/jsx-pascal-case
|
|
60
60
|
// sonarjs/disable-next-line function-name
|
|
61
|
-
export const Panel = ({ panelId, panelConfig, props, WrappedChild, label, html, children, isOpen = true, rootRef }) => {
|
|
61
|
+
export const Panel = ({ panelId, panelConfig, props, focusOnOpen, WrappedChild, label, html, children, isOpen = true, rootRef }) => {
|
|
62
62
|
const { id } = useConfig()
|
|
63
|
-
const { dispatch, breakpoint, layoutRefs } = useApp()
|
|
63
|
+
const { dispatch, breakpoint, layoutRefs, interfaceType } = useApp()
|
|
64
64
|
|
|
65
65
|
const rootEl = document.getElementById(`${id}-im-app`)
|
|
66
66
|
const bpConfig = panelConfig[breakpoint]
|
|
67
67
|
const elementId = `${id}-panel-${stringToKebab(panelId)}`
|
|
68
68
|
|
|
69
|
-
const { isAside, isDialog, isModal, isDismissible, shouldFocus, buttonContainerEl } = computePanelState(bpConfig, props?.triggeringElement)
|
|
69
|
+
const { isAside, isDialog, isModal, isDismissible, shouldFocus, buttonContainerEl } = computePanelState(bpConfig, props?.triggeringElement, panelConfig.focus, focusOnOpen) // nosonar
|
|
70
70
|
|
|
71
71
|
// For persistent panels, gate modal behaviour on open state
|
|
72
72
|
const isModalActive = isModal && isOpen
|
|
@@ -81,7 +81,7 @@ export const Panel = ({ panelId, panelConfig, props, WrappedChild, label, html,
|
|
|
81
81
|
const panelRef = rootRef || internalPanelRef
|
|
82
82
|
|
|
83
83
|
const handleClose = () => {
|
|
84
|
-
requestAnimationFrame(() => { (props?.triggeringElement || layoutRefs.viewportRef.current).focus?.() })
|
|
84
|
+
requestAnimationFrame(() => { (props?.triggeringElement || layoutRefs.viewportRef.current).focus?.({ preventScroll: interfaceType !== 'keyboard' }) })
|
|
85
85
|
dispatch({ type: 'CLOSE_PANEL', payload: panelId })
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -104,6 +104,43 @@ describe('Panel', () => {
|
|
|
104
104
|
})
|
|
105
105
|
})
|
|
106
106
|
|
|
107
|
+
describe('focus behaviour', () => {
|
|
108
|
+
it('focuses panel on mount when focus: true', () => {
|
|
109
|
+
const { container } = renderPanel({ focus: true })
|
|
110
|
+
const panel = container.firstChild
|
|
111
|
+
expect(panel).toHaveAttribute('tabIndex', '-1')
|
|
112
|
+
expect(document.activeElement).toBe(panel)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('does not focus panel on mount when focus: false and no triggering element or modal', () => {
|
|
116
|
+
const { container } = renderPanel({ focus: false })
|
|
117
|
+
const panel = container.firstChild
|
|
118
|
+
expect(panel).not.toHaveAttribute('tabIndex')
|
|
119
|
+
expect(document.activeElement).not.toBe(panel)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('focuses panel on mount when modal even if focus: false', () => {
|
|
123
|
+
const { container } = renderPanel({ focus: false, desktop: { slot: 'overlay', dismissible: true, modal: true } })
|
|
124
|
+
const panel = container.firstChild
|
|
125
|
+
expect(panel).toHaveAttribute('tabIndex', '-1')
|
|
126
|
+
expect(document.activeElement).toBe(panel)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('focuses panel when focusOnOpen is true regardless of panelConfig.focus', () => {
|
|
130
|
+
const { container } = renderPanel({ focus: false }, { focusOnOpen: true })
|
|
131
|
+
const panel = container.firstChild
|
|
132
|
+
expect(panel).toHaveAttribute('tabIndex', '-1')
|
|
133
|
+
expect(document.activeElement).toBe(panel)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('does not focus panel when focusOnOpen is false even if panelConfig.focus is true', () => {
|
|
137
|
+
const { container } = renderPanel({ focus: true }, { focusOnOpen: false })
|
|
138
|
+
const panel = container.firstChild
|
|
139
|
+
expect(panel).not.toHaveAttribute('tabIndex')
|
|
140
|
+
expect(document.activeElement).not.toBe(panel)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
107
144
|
describe('close functionality', () => {
|
|
108
145
|
it('focuses triggeringElement on close for button slots', () => {
|
|
109
146
|
const focusMock = jest.fn()
|
|
@@ -2,6 +2,7 @@ import React, { useRef, useEffect, useState } from 'react'
|
|
|
2
2
|
import { EVENTS as events } from '../../../config/events.js'
|
|
3
3
|
import { createPortal } from 'react-dom'
|
|
4
4
|
import { useConfig } from '../../store/configContext.js'
|
|
5
|
+
|
|
5
6
|
import { useApp } from '../../store/appContext.js'
|
|
6
7
|
import { useMap } from '../../store/mapContext.js'
|
|
7
8
|
import { MapController } from './MapController.jsx'
|
|
@@ -14,9 +15,10 @@ import { Markers } from '../Markers/Markers'
|
|
|
14
15
|
|
|
15
16
|
// eslint-disable-next-line camelcase, react/jsx-pascal-case
|
|
16
17
|
// sonarjs/disable-next-line function-name
|
|
17
|
-
export const Viewport = (
|
|
18
|
+
export const Viewport = () => {
|
|
18
19
|
const { id, mapProvider, mapLabel, keyboardHintText } = useConfig()
|
|
19
20
|
const { interfaceType, mode, previousMode, layoutRefs, safeZoneInset } = useApp()
|
|
21
|
+
const { mainRef } = layoutRefs
|
|
20
22
|
const { mapSize } = useMap()
|
|
21
23
|
|
|
22
24
|
const mapContainerRef = useRef(null)
|
|
@@ -49,18 +51,6 @@ export const Viewport = ({ keyboardHintPortalRef }) => {
|
|
|
49
51
|
}
|
|
50
52
|
}, [mode])
|
|
51
53
|
|
|
52
|
-
// Toggle external class based on keyboard hint
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
const mainEl = layoutRefs.mainRef?.current
|
|
55
|
-
if (!mainEl) {
|
|
56
|
-
return undefined
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
mainEl.classList.toggle('im-o-app__main--keyboard-hint-visible', showHint)
|
|
60
|
-
|
|
61
|
-
return () => mainEl?.classList.remove('im-o-app__main--keyboard-hint-visible')
|
|
62
|
-
}, [showHint])
|
|
63
|
-
|
|
64
54
|
return (
|
|
65
55
|
<>
|
|
66
56
|
<MapController mapContainerRef={mapContainerRef} />
|
|
@@ -74,14 +64,14 @@ export const Viewport = ({ keyboardHintPortalRef }) => {
|
|
|
74
64
|
onBlur={handleBlur}
|
|
75
65
|
ref={layoutRefs.viewportRef}
|
|
76
66
|
>
|
|
77
|
-
{showHint &&
|
|
67
|
+
{showHint && mainRef?.current && createPortal(
|
|
78
68
|
<div
|
|
79
69
|
className='im-c-viewport__keyboard-hint'
|
|
80
70
|
aria-hidden='true'
|
|
81
71
|
ref={keyboardHintRef}
|
|
82
72
|
dangerouslySetInnerHTML={{ __html: keyboardHintText }}
|
|
83
73
|
/>,
|
|
84
|
-
|
|
74
|
+
mainRef.current
|
|
85
75
|
)}
|
|
86
76
|
<div className='im-c-viewport__map-container' ref={mapContainerRef} />
|
|
87
77
|
<div className='im-c-viewport__features' />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { render,
|
|
2
|
+
import { render, cleanup } from '@testing-library/react'
|
|
3
3
|
import { Viewport } from './Viewport.jsx'
|
|
4
4
|
import { useConfig } from '../../store/configContext.js'
|
|
5
5
|
import { useApp } from '../../store/appContext.js'
|
|
@@ -23,15 +23,18 @@ jest.mock('../CrossHair/CrossHair', () => ({ CrossHair: jest.fn(() => <div data-
|
|
|
23
23
|
jest.mock('../Markers/Markers', () => ({ Markers: jest.fn(() => <div data-testid='markers' />) }))
|
|
24
24
|
|
|
25
25
|
describe('Viewport', () => {
|
|
26
|
-
let
|
|
26
|
+
let viewportEl
|
|
27
|
+
let mainEl
|
|
27
28
|
const mockMapProvider = { initMap: jest.fn(), updateMap: jest.fn(), clearHighlightedLabel: jest.fn() }
|
|
28
29
|
|
|
29
30
|
beforeEach(() => {
|
|
30
31
|
cleanup()
|
|
31
32
|
jest.clearAllMocks()
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
document.
|
|
34
|
+
viewportEl = document.createElement('div')
|
|
35
|
+
mainEl = document.createElement('div')
|
|
36
|
+
document.body.appendChild(viewportEl)
|
|
37
|
+
document.body.appendChild(mainEl)
|
|
35
38
|
|
|
36
39
|
// ---------------------------
|
|
37
40
|
// Hook mocks
|
|
@@ -47,7 +50,7 @@ describe('Viewport', () => {
|
|
|
47
50
|
interfaceType: 'desktop',
|
|
48
51
|
mode: 'default',
|
|
49
52
|
previousMode: 'default',
|
|
50
|
-
layoutRefs: {
|
|
53
|
+
layoutRefs: { mainRef: { current: mainEl }, viewportRef: { current: viewportEl }, safeZoneRef: { current: null } },
|
|
51
54
|
safeZoneInset: {}
|
|
52
55
|
})
|
|
53
56
|
|
|
@@ -75,14 +78,17 @@ describe('Viewport', () => {
|
|
|
75
78
|
useMapEvents.mockImplementation(() => {})
|
|
76
79
|
})
|
|
77
80
|
|
|
78
|
-
afterEach(() =>
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
viewportEl.remove()
|
|
83
|
+
mainEl.remove()
|
|
84
|
+
})
|
|
79
85
|
|
|
80
86
|
const renderViewport = () => {
|
|
81
|
-
const { container, rerender, unmount } = render(<Viewport
|
|
87
|
+
const { container, rerender, unmount } = render(<Viewport />)
|
|
82
88
|
const viewport = container.querySelector('.im-c-viewport')
|
|
83
89
|
const mapContainer = container.querySelector('.im-c-viewport__map-container')
|
|
84
90
|
const safeZone = container.querySelector('.im-c-viewport__safezone')
|
|
85
|
-
const keyboardHint =
|
|
91
|
+
const keyboardHint = mainEl.querySelector('.im-c-viewport__keyboard-hint')
|
|
86
92
|
const crossHair = container.querySelector('[data-testid="cross-hair"]')
|
|
87
93
|
const markers = container.querySelector('[data-testid="markers"]')
|
|
88
94
|
return { viewport, mapContainer, safeZone, keyboardHint, crossHair, markers, rerender, unmount }
|
|
@@ -109,14 +115,6 @@ describe('Viewport', () => {
|
|
|
109
115
|
expect(keyboardHint.innerHTML).toBe('Press arrow keys')
|
|
110
116
|
})
|
|
111
117
|
|
|
112
|
-
it('handles focus and blur events updating keyboard hint visibility', () => {
|
|
113
|
-
const { viewport, keyboardHint } = renderViewport()
|
|
114
|
-
fireEvent.focus(viewport)
|
|
115
|
-
fireEvent.blur(viewport)
|
|
116
|
-
expect(keyboardHint).toBeInTheDocument()
|
|
117
|
-
expect(keyboardHint.innerHTML).toBe('Press arrow keys')
|
|
118
|
-
})
|
|
119
|
-
|
|
120
118
|
it('attaches keyboard shortcuts', () => {
|
|
121
119
|
renderViewport()
|
|
122
120
|
expect(useKeyboardShortcuts).toHaveBeenCalled()
|
|
@@ -137,25 +135,10 @@ describe('Viewport', () => {
|
|
|
137
135
|
interfaceType: 'desktop',
|
|
138
136
|
mode: 'edit',
|
|
139
137
|
previousMode: 'default',
|
|
140
|
-
layoutRefs: {
|
|
138
|
+
layoutRefs: { mainRef: { current: mainEl }, viewportRef: { current: viewport }, safeZoneRef: { current: null } },
|
|
141
139
|
safeZoneInset: {}
|
|
142
140
|
})
|
|
143
|
-
rerender(<Viewport
|
|
141
|
+
rerender(<Viewport />)
|
|
144
142
|
expect(focusMock).toHaveBeenCalled()
|
|
145
143
|
})
|
|
146
|
-
|
|
147
|
-
it('toggles main element class for keyboard hint and cleans up on unmount', () => {
|
|
148
|
-
const mainEl = document.createElement('div')
|
|
149
|
-
useApp.mockReturnValueOnce({
|
|
150
|
-
interfaceType: 'desktop',
|
|
151
|
-
mode: 'default',
|
|
152
|
-
previousMode: 'default',
|
|
153
|
-
layoutRefs: { viewportRef: { current: null }, mainRef: { current: mainEl }, safeZoneRef: { current: null } },
|
|
154
|
-
safeZoneInset: {}
|
|
155
|
-
})
|
|
156
|
-
const { unmount } = renderViewport()
|
|
157
|
-
expect(mainEl.classList.contains('im-o-app__main--keyboard-hint-visible')).toBe(true)
|
|
158
|
-
unmount()
|
|
159
|
-
expect(mainEl.classList.contains('im-o-app__main--keyboard-hint-visible')).toBe(false)
|
|
160
|
-
})
|
|
161
144
|
})
|
|
@@ -54,16 +54,16 @@ export const useInterfaceAPI = () => {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
const
|
|
58
|
-
const
|
|
57
|
+
const handleAppOpened = () => dispatchRef.current({ type: 'TOGGLE_APP_VISIBLE', payload: true })
|
|
58
|
+
const handleAppClosed = () => dispatchRef.current({ type: 'TOGGLE_APP_VISIBLE', payload: false })
|
|
59
59
|
const handleAddPanel = ({ id, config }) => dispatchRef.current({ type: 'ADD_PANEL', payload: { id, config } })
|
|
60
60
|
const handleRemovePanel = (id) => dispatchRef.current({ type: 'REMOVE_PANEL', payload: id })
|
|
61
|
-
const handleShowPanel = (id) => dispatchRef.current({ type: 'OPEN_PANEL', payload: { panelId: id } })
|
|
61
|
+
const handleShowPanel = ({ id, focus = true }) => dispatchRef.current({ type: 'OPEN_PANEL', payload: { panelId: id, focusOnOpen: focus } })
|
|
62
62
|
const handleHidePanel = (id) => dispatchRef.current({ type: 'CLOSE_PANEL', payload: id })
|
|
63
63
|
const handleAddControl = ({ id, config }) => dispatchRef.current({ type: 'ADD_CONTROL', payload: { id, config } })
|
|
64
64
|
|
|
65
|
-
eventBus.on(events.
|
|
66
|
-
eventBus.on(events.
|
|
65
|
+
eventBus.on(events.APP_OPENED, handleAppOpened)
|
|
66
|
+
eventBus.on(events.APP_CLOSED, handleAppClosed)
|
|
67
67
|
eventBus.on(events.APP_ADD_BUTTON, handleAddButton)
|
|
68
68
|
eventBus.on(events.APP_TOGGLE_BUTTON_STATE, handleToggleButtonState)
|
|
69
69
|
eventBus.on(events.APP_ADD_PANEL, handleAddPanel)
|
|
@@ -73,8 +73,8 @@ export const useInterfaceAPI = () => {
|
|
|
73
73
|
eventBus.on(events.APP_ADD_CONTROL, handleAddControl)
|
|
74
74
|
|
|
75
75
|
return () => {
|
|
76
|
-
eventBus.off(events.
|
|
77
|
-
eventBus.off(events.
|
|
76
|
+
eventBus.off(events.APP_OPENED, handleAppOpened)
|
|
77
|
+
eventBus.off(events.APP_CLOSED, handleAppClosed)
|
|
78
78
|
eventBus.off(events.APP_ADD_BUTTON, handleAddButton)
|
|
79
79
|
eventBus.off(events.APP_TOGGLE_BUTTON_STATE, handleToggleButtonState)
|
|
80
80
|
eventBus.off(events.APP_ADD_PANEL, handleAddPanel)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react'
|
|
2
|
+
import { useInterfaceAPI } from './useInterfaceAPI.js'
|
|
3
|
+
import { useApp } from '../store/appContext.js'
|
|
4
|
+
import { useService } from '../store/serviceContext.js'
|
|
5
|
+
|
|
6
|
+
jest.mock('../store/appContext.js')
|
|
7
|
+
jest.mock('../store/serviceContext.js')
|
|
8
|
+
|
|
9
|
+
const makeEventBus = () => {
|
|
10
|
+
const handlers = {}
|
|
11
|
+
return {
|
|
12
|
+
on: jest.fn((event, handler) => { handlers[event] = handler }),
|
|
13
|
+
off: jest.fn(),
|
|
14
|
+
emit: (event, payload) => handlers[event]?.(payload),
|
|
15
|
+
_handlers: handlers
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('useInterfaceAPI', () => {
|
|
20
|
+
let mockDispatch, mockEventBus, mockState
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
mockDispatch = jest.fn()
|
|
24
|
+
mockEventBus = makeEventBus()
|
|
25
|
+
mockState = {
|
|
26
|
+
hiddenButtons: new Set(),
|
|
27
|
+
disabledButtons: new Set(),
|
|
28
|
+
pressedButtons: new Set(),
|
|
29
|
+
expandedButtons: new Set()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
useApp.mockReturnValue({ dispatch: mockDispatch, ...mockState })
|
|
33
|
+
useService.mockReturnValue({ eventBus: mockEventBus })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('dispatches ADD_BUTTON on app:addbutton', () => {
|
|
37
|
+
renderHook(() => useInterfaceAPI())
|
|
38
|
+
act(() => mockEventBus.emit('app:addbutton', { id: 'btn1', config: { label: 'Test' } }))
|
|
39
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: 'ADD_BUTTON', payload: { id: 'btn1', config: { label: 'Test' } } })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('also dispatches ADD_BUTTON for each menuItem', () => {
|
|
43
|
+
renderHook(() => useInterfaceAPI())
|
|
44
|
+
act(() => mockEventBus.emit('app:addbutton', {
|
|
45
|
+
id: 'btn1',
|
|
46
|
+
config: {
|
|
47
|
+
label: 'Parent',
|
|
48
|
+
menuItems: [
|
|
49
|
+
{ id: 'item1', label: 'Item 1' },
|
|
50
|
+
{ id: 'item2', label: 'Item 2' }
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}))
|
|
54
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: 'ADD_BUTTON', payload: { id: 'btn1', config: expect.objectContaining({ label: 'Parent' }) } })
|
|
55
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: 'ADD_BUTTON', payload: { id: 'item1', config: { id: 'item1', label: 'Item 1', isMenuItem: true } } })
|
|
56
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: 'ADD_BUTTON', payload: { id: 'item2', config: { id: 'item2', label: 'Item 2', isMenuItem: true } } })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('dispatches TOGGLE_APP_VISIBLE true on app:opened', () => {
|
|
60
|
+
renderHook(() => useInterfaceAPI())
|
|
61
|
+
act(() => mockEventBus.emit('app:opened'))
|
|
62
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: 'TOGGLE_APP_VISIBLE', payload: true })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('dispatches TOGGLE_APP_VISIBLE false on app:closed', () => {
|
|
66
|
+
renderHook(() => useInterfaceAPI())
|
|
67
|
+
act(() => mockEventBus.emit('app:closed'))
|
|
68
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: 'TOGGLE_APP_VISIBLE', payload: false })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('dispatches ADD_PANEL on app:addpanel', () => {
|
|
72
|
+
renderHook(() => useInterfaceAPI())
|
|
73
|
+
act(() => mockEventBus.emit('app:addpanel', { id: 'panel1', config: { title: 'My Panel' } }))
|
|
74
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: 'ADD_PANEL', payload: { id: 'panel1', config: { title: 'My Panel' } } })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('dispatches REMOVE_PANEL on app:removepanel', () => {
|
|
78
|
+
renderHook(() => useInterfaceAPI())
|
|
79
|
+
act(() => mockEventBus.emit('app:removepanel', 'panel1'))
|
|
80
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: 'REMOVE_PANEL', payload: 'panel1' })
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('dispatches OPEN_PANEL with focusOnOpen:true on app:showpanel by default', () => {
|
|
84
|
+
renderHook(() => useInterfaceAPI())
|
|
85
|
+
act(() => mockEventBus.emit('app:showpanel', { id: 'panel1' }))
|
|
86
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: 'OPEN_PANEL', payload: { panelId: 'panel1', focusOnOpen: true } })
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('dispatches OPEN_PANEL with focusOnOpen:false when focus:false', () => {
|
|
90
|
+
renderHook(() => useInterfaceAPI())
|
|
91
|
+
act(() => mockEventBus.emit('app:showpanel', { id: 'panel1', focus: false }))
|
|
92
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: 'OPEN_PANEL', payload: { panelId: 'panel1', focusOnOpen: false } })
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('dispatches CLOSE_PANEL on app:hidepanel', () => {
|
|
96
|
+
renderHook(() => useInterfaceAPI())
|
|
97
|
+
act(() => mockEventBus.emit('app:hidepanel', 'panel1'))
|
|
98
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: 'CLOSE_PANEL', payload: 'panel1' })
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('dispatches ADD_CONTROL on app:addcontrol', () => {
|
|
102
|
+
renderHook(() => useInterfaceAPI())
|
|
103
|
+
act(() => mockEventBus.emit('app:addcontrol', { id: 'ctrl1', config: { position: 'top-left' } }))
|
|
104
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: 'ADD_CONTROL', payload: { id: 'ctrl1', config: { position: 'top-left' } } })
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('handleToggleButtonState', () => {
|
|
108
|
+
it.each([
|
|
109
|
+
['hidden', 'TOGGLE_BUTTON_HIDDEN', 'isHidden'],
|
|
110
|
+
['disabled', 'TOGGLE_BUTTON_DISABLED', 'isDisabled'],
|
|
111
|
+
['pressed', 'TOGGLE_BUTTON_PRESSED', 'isPressed'],
|
|
112
|
+
['expanded', 'TOGGLE_BUTTON_EXPANDED', 'isExpanded']
|
|
113
|
+
])('sets %s to explicit boolean value when provided', (prop, actionType, payloadKey) => {
|
|
114
|
+
renderHook(() => useInterfaceAPI())
|
|
115
|
+
act(() => mockEventBus.emit('app:togglebuttonstate', { id: 'btn1', prop, value: true }))
|
|
116
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: actionType, payload: { id: 'btn1', [payloadKey]: true } })
|
|
117
|
+
|
|
118
|
+
mockDispatch.mockClear()
|
|
119
|
+
act(() => mockEventBus.emit('app:togglebuttonstate', { id: 'btn1', prop, value: false }))
|
|
120
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: actionType, payload: { id: 'btn1', [payloadKey]: false } })
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it.each([
|
|
124
|
+
['hidden', 'TOGGLE_BUTTON_HIDDEN', 'isHidden', 'hiddenButtons'],
|
|
125
|
+
['disabled', 'TOGGLE_BUTTON_DISABLED', 'isDisabled', 'disabledButtons'],
|
|
126
|
+
['pressed', 'TOGGLE_BUTTON_PRESSED', 'isPressed', 'pressedButtons'],
|
|
127
|
+
['expanded', 'TOGGLE_BUTTON_EXPANDED', 'isExpanded', 'expandedButtons']
|
|
128
|
+
])('toggles %s when no boolean value provided', (prop, actionType, payloadKey, stateKey) => {
|
|
129
|
+
renderHook(() => useInterfaceAPI())
|
|
130
|
+
|
|
131
|
+
// Not in set → toggles to true
|
|
132
|
+
act(() => mockEventBus.emit('app:togglebuttonstate', { id: 'btn1', prop }))
|
|
133
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: actionType, payload: { id: 'btn1', [payloadKey]: true } })
|
|
134
|
+
|
|
135
|
+
// Already in set → toggles to false
|
|
136
|
+
mockDispatch.mockClear()
|
|
137
|
+
mockState[stateKey].add('btn1')
|
|
138
|
+
act(() => mockEventBus.emit('app:togglebuttonstate', { id: 'btn1', prop }))
|
|
139
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: actionType, payload: { id: 'btn1', [payloadKey]: false } })
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('does nothing for unknown prop', () => {
|
|
143
|
+
renderHook(() => useInterfaceAPI())
|
|
144
|
+
act(() => mockEventBus.emit('app:togglebuttonstate', { id: 'btn1', prop: 'unknown', value: true }))
|
|
145
|
+
expect(mockDispatch).not.toHaveBeenCalled()
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('removes all event listeners on unmount', () => {
|
|
150
|
+
const { unmount } = renderHook(() => useInterfaceAPI())
|
|
151
|
+
unmount()
|
|
152
|
+
expect(mockEventBus.off).toHaveBeenCalledWith('app:opened', expect.any(Function))
|
|
153
|
+
expect(mockEventBus.off).toHaveBeenCalledWith('app:closed', expect.any(Function))
|
|
154
|
+
expect(mockEventBus.off).toHaveBeenCalledWith('app:addbutton', expect.any(Function))
|
|
155
|
+
expect(mockEventBus.off).toHaveBeenCalledWith('app:togglebuttonstate', expect.any(Function))
|
|
156
|
+
expect(mockEventBus.off).toHaveBeenCalledWith('app:addpanel', expect.any(Function))
|
|
157
|
+
expect(mockEventBus.off).toHaveBeenCalledWith('app:removepanel', expect.any(Function))
|
|
158
|
+
expect(mockEventBus.off).toHaveBeenCalledWith('app:showpanel', expect.any(Function))
|
|
159
|
+
expect(mockEventBus.off).toHaveBeenCalledWith('app:hidepanel', expect.any(Function))
|
|
160
|
+
expect(mockEventBus.off).toHaveBeenCalledWith('app:addcontrol', expect.any(Function))
|
|
161
|
+
})
|
|
162
|
+
})
|
|
@@ -51,80 +51,72 @@ const subSlotMaxHeight = (columnHeight, siblingButtons, gap) =>
|
|
|
51
51
|
* It does not dispatch the safe zone — safe zone dispatch is owned entirely by
|
|
52
52
|
* Effect 3 to prevent jumps on panel open/close and other non-structural resizes.
|
|
53
53
|
*/
|
|
54
|
-
|
|
55
|
-
const { dispatch, breakpoint, layoutRefs, arePluginsEvaluated, appVisible, isFullscreen } = useApp()
|
|
56
|
-
const { mapSize, isMapReady } = useMap()
|
|
57
|
-
|
|
54
|
+
function calculateLayout (layoutRefs) {
|
|
58
55
|
const {
|
|
59
|
-
appContainerRef,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
topRef,
|
|
63
|
-
topLeftColRef,
|
|
64
|
-
topRightColRef,
|
|
65
|
-
leftTopRef,
|
|
66
|
-
leftBottomRef,
|
|
67
|
-
rightTopRef,
|
|
68
|
-
rightBottomRef,
|
|
69
|
-
bottomRef,
|
|
70
|
-
bottomRightRef,
|
|
71
|
-
attributionsRef,
|
|
72
|
-
drawerRef,
|
|
73
|
-
actionsRef
|
|
56
|
+
appContainerRef, mainRef, topRef, topLeftColRef, topRightColRef,
|
|
57
|
+
bottomRef, attributionsRef, bottomRightRef, leftTopRef, leftBottomRef,
|
|
58
|
+
rightTopRef, rightBottomRef
|
|
74
59
|
} = layoutRefs
|
|
75
60
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const topLeftCol = topLeftColRef.current
|
|
84
|
-
const topRightCol = topRightColRef.current
|
|
85
|
-
const bottom = bottomRef.current
|
|
86
|
-
const attributions = attributionsRef.current
|
|
87
|
-
|
|
88
|
-
if ([main, top, bottom].some(r => !r)) {
|
|
89
|
-
return
|
|
90
|
-
}
|
|
61
|
+
const appContainer = appContainerRef.current
|
|
62
|
+
const main = mainRef.current
|
|
63
|
+
const top = topRef.current
|
|
64
|
+
const topLeftCol = topLeftColRef.current
|
|
65
|
+
const topRightCol = topRightColRef.current
|
|
66
|
+
const bottom = bottomRef.current
|
|
67
|
+
const attributions = attributionsRef.current
|
|
91
68
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// === Top column width ===
|
|
96
|
-
appContainer.style.setProperty('--top-col-width', `${topColWidth(topLeftCol.offsetWidth, topRightCol.offsetWidth)}px`)
|
|
97
|
-
|
|
98
|
-
// === Left container offsets ===
|
|
99
|
-
const leftOffsetTop = topLeftCol.offsetHeight + top.offsetTop
|
|
100
|
-
const leftColumnHeight = bottom.offsetTop - leftOffsetTop - dividerGap
|
|
101
|
-
appContainer.style.setProperty('--left-offset-top', `${leftOffsetTop}px`)
|
|
102
|
-
appContainer.style.setProperty('--left-offset-bottom', `${main.offsetHeight - bottom.offsetTop + dividerGap}px`)
|
|
103
|
-
appContainer.style.setProperty('--left-top-max-height', `${leftColumnHeight}px`)
|
|
104
|
-
|
|
105
|
-
// === Right container offsets ===
|
|
106
|
-
// Mirrors the top formula (topRightCol.offsetHeight + top.offsetTop):
|
|
107
|
-
// bottomRight.offsetHeight is 0 when no buttons so the offset collapses to just
|
|
108
|
-
// the padding between the bottom of the bottom container and the bottom of main.
|
|
109
|
-
const bottomRightHeight = bottomRightRef?.current?.offsetHeight ?? 0
|
|
110
|
-
const bottomContainerPad = main.offsetHeight - bottom.offsetTop - bottom.offsetHeight
|
|
111
|
-
const rightOffsetTop = topRightCol.offsetHeight + top.offsetTop
|
|
112
|
-
const rightEffectiveBottom = bottom.offsetTop + bottom.offsetHeight - bottomRightHeight
|
|
113
|
-
const rightColumnHeight = rightEffectiveBottom - rightOffsetTop - dividerGap
|
|
114
|
-
const rightOffsetBottom = bottomContainerPad + (bottomRightHeight > 0 ? (bottomRightHeight + dividerGap) : attributions.offsetHeight)
|
|
115
|
-
appContainer.style.setProperty('--right-offset-top', `${rightOffsetTop}px`)
|
|
116
|
-
appContainer.style.setProperty('--right-offset-bottom', `${rightOffsetBottom}px`)
|
|
117
|
-
appContainer.style.setProperty('--right-top-max-height', `${rightColumnHeight}px`)
|
|
118
|
-
|
|
119
|
-
// === Sub-slot panel max-heights ===
|
|
120
|
-
appContainer.style.setProperty('--left-top-panel-max-height', `${subSlotMaxHeight(leftColumnHeight, buttonHeight(leftBottomRef), dividerGap)}px`)
|
|
121
|
-
appContainer.style.setProperty('--left-bottom-panel-max-height', `${subSlotMaxHeight(leftColumnHeight, buttonHeight(leftTopRef), dividerGap)}px`)
|
|
122
|
-
appContainer.style.setProperty('--right-top-panel-max-height', `${subSlotMaxHeight(rightColumnHeight, buttonHeight(rightBottomRef), dividerGap)}px`)
|
|
123
|
-
appContainer.style.setProperty('--right-bottom-panel-max-height', `${subSlotMaxHeight(rightColumnHeight, buttonHeight(rightTopRef), dividerGap)}px`)
|
|
69
|
+
if ([main, top, bottom].some(r => !r)) {
|
|
70
|
+
return
|
|
124
71
|
}
|
|
125
72
|
|
|
73
|
+
const root = document.documentElement
|
|
74
|
+
const dividerGap = Number.parseInt(getComputedStyle(root).getPropertyValue('--divider-gap'), 10)
|
|
75
|
+
|
|
76
|
+
// === Top column width ===
|
|
77
|
+
appContainer.style.setProperty('--top-col-width', `${topColWidth(topLeftCol.offsetWidth, topRightCol.offsetWidth)}px`)
|
|
78
|
+
|
|
79
|
+
// === Left container offsets ===
|
|
80
|
+
const leftOffsetTop = topLeftCol.offsetHeight + top.offsetTop
|
|
81
|
+
const leftColumnHeight = bottom.offsetTop - leftOffsetTop - dividerGap
|
|
82
|
+
appContainer.style.setProperty('--left-offset-top', `${leftOffsetTop}px`)
|
|
83
|
+
appContainer.style.setProperty('--left-offset-bottom', `${main.offsetHeight - bottom.offsetTop + dividerGap}px`)
|
|
84
|
+
appContainer.style.setProperty('--left-top-max-height', `${leftColumnHeight}px`)
|
|
85
|
+
|
|
86
|
+
// === Right container offsets ===
|
|
87
|
+
// Mirrors the top formula (topRightCol.offsetHeight + top.offsetTop):
|
|
88
|
+
// bottomRight.offsetHeight is 0 when no buttons so the offset collapses to just
|
|
89
|
+
// the padding between the bottom of the bottom container and the bottom of main.
|
|
90
|
+
const bottomRightHeight = bottomRightRef?.current?.offsetHeight ?? 0
|
|
91
|
+
const bottomContainerPad = main.offsetHeight - bottom.offsetTop - bottom.offsetHeight
|
|
92
|
+
const rightOffsetTop = topRightCol.offsetHeight + top.offsetTop
|
|
93
|
+
const rightEffectiveBottom = bottom.offsetTop + bottom.offsetHeight - bottomRightHeight
|
|
94
|
+
const rightColumnHeight = rightEffectiveBottom - rightOffsetTop - dividerGap
|
|
95
|
+
const rightOffsetBottom = bottomContainerPad + (bottomRightHeight > 0 ? (bottomRightHeight + dividerGap) : attributions.offsetHeight)
|
|
96
|
+
appContainer.style.setProperty('--right-offset-top', `${rightOffsetTop}px`)
|
|
97
|
+
appContainer.style.setProperty('--right-offset-bottom', `${rightOffsetBottom}px`)
|
|
98
|
+
appContainer.style.setProperty('--right-top-max-height', `${rightColumnHeight}px`)
|
|
99
|
+
|
|
100
|
+
// === Keyboard hint bottom offset ===
|
|
101
|
+
// Distance from the bottom of im-o-app__bottom to the bottom of im-o-app__main.
|
|
102
|
+
// Used to position the hint above the bottom bar (and above drawers on mobile).
|
|
103
|
+
appContainer.style.setProperty('--keyboard-hint-bottom', `${main.offsetHeight - bottom.offsetTop - bottom.offsetHeight}px`)
|
|
104
|
+
|
|
105
|
+
// === Sub-slot panel max-heights ===
|
|
106
|
+
appContainer.style.setProperty('--left-top-panel-max-height', `${subSlotMaxHeight(leftColumnHeight, buttonHeight(leftBottomRef), dividerGap)}px`)
|
|
107
|
+
appContainer.style.setProperty('--left-bottom-panel-max-height', `${subSlotMaxHeight(leftColumnHeight, buttonHeight(leftTopRef), dividerGap)}px`)
|
|
108
|
+
appContainer.style.setProperty('--right-top-panel-max-height', `${subSlotMaxHeight(rightColumnHeight, buttonHeight(rightBottomRef), dividerGap)}px`)
|
|
109
|
+
appContainer.style.setProperty('--right-bottom-panel-max-height', `${subSlotMaxHeight(rightColumnHeight, buttonHeight(rightTopRef), dividerGap)}px`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function useLayoutMeasurements () {
|
|
113
|
+
const { dispatch, breakpoint, layoutRefs, arePluginsEvaluated, appVisible, isFullscreen } = useApp()
|
|
114
|
+
const { mapSize, isMapReady } = useMap()
|
|
115
|
+
|
|
116
|
+
const { bannerRef, mainRef, topRef, topLeftColRef, topRightColRef, bottomRef, bottomRightRef, leftTopRef, leftBottomRef, rightTopRef, rightBottomRef, drawerRef, actionsRef } = layoutRefs
|
|
117
|
+
|
|
126
118
|
// --------------------------------
|
|
127
|
-
//
|
|
119
|
+
// 1. Clear the evaluated flag when structural inputs change so the safe zone
|
|
128
120
|
// is not dispatched until useButtonStateEvaluator has completed a full
|
|
129
121
|
// pass with the new app/map state and set PLUGINS_EVALUATED.
|
|
130
122
|
// --------------------------------
|
|
@@ -133,7 +125,7 @@ export function useLayoutMeasurements () {
|
|
|
133
125
|
}, [breakpoint, mapSize, isMapReady, appVisible, isFullscreen])
|
|
134
126
|
|
|
135
127
|
// --------------------------------
|
|
136
|
-
//
|
|
128
|
+
// 2. Once all plugin button props have been evaluated (arePluginsEvaluated),
|
|
137
129
|
// recalculate layout and dispatch the safe zone inset.
|
|
138
130
|
// RAF required to ensure browser layout is committed before measuring.
|
|
139
131
|
// --------------------------------
|
|
@@ -142,7 +134,7 @@ export function useLayoutMeasurements () {
|
|
|
142
134
|
return
|
|
143
135
|
}
|
|
144
136
|
requestAnimationFrame(() => {
|
|
145
|
-
calculateLayout()
|
|
137
|
+
calculateLayout(layoutRefs)
|
|
146
138
|
const safeZoneInset = getSafeZoneInset(layoutRefs)
|
|
147
139
|
if (safeZoneInset) {
|
|
148
140
|
dispatch({ type: 'SET_SAFE_ZONE_INSET', payload: { safeZoneInset } })
|
|
@@ -151,13 +143,13 @@ export function useLayoutMeasurements () {
|
|
|
151
143
|
}, [arePluginsEvaluated])
|
|
152
144
|
|
|
153
145
|
// --------------------------------
|
|
154
|
-
//
|
|
146
|
+
// 3. Recalculate CSS vars whenever observed elements resize (panels, banner,
|
|
155
147
|
// actions buttons, etc.). Safe zone is intentionally not dispatched here —
|
|
156
|
-
// that is Effect
|
|
148
|
+
// that is Effect 2's responsibility.
|
|
157
149
|
// --------------------------------
|
|
158
150
|
useResizeObserver([bannerRef, mainRef, topRef, topLeftColRef, topRightColRef, actionsRef, bottomRef, bottomRightRef, leftTopRef, leftBottomRef, rightTopRef, rightBottomRef, drawerRef], () => {
|
|
159
151
|
requestAnimationFrame(() => {
|
|
160
|
-
calculateLayout()
|
|
152
|
+
calculateLayout(layoutRefs)
|
|
161
153
|
})
|
|
162
154
|
})
|
|
163
155
|
}
|
|
@@ -5,12 +5,9 @@ import { useService } from '../store/serviceContext.js'
|
|
|
5
5
|
import { scaleFactor } from '../../config/appConfig.js'
|
|
6
6
|
import { EVENTS as events } from '../../config/events.js'
|
|
7
7
|
|
|
8
|
-
// Vertical offset to align the marker tip with the coordinate point
|
|
9
|
-
const MARKER_ANCHOR_OFFSET_Y = 19
|
|
10
|
-
|
|
11
8
|
/**
|
|
12
9
|
* Projects geographic coordinates to screen pixel position, scaled for the
|
|
13
|
-
* current map size
|
|
10
|
+
* current map size. Anchor alignment is handled in CSS by the Markers component.
|
|
14
11
|
*
|
|
15
12
|
* @param {Array<number>} coords - [lng, lat] geographic coordinates
|
|
16
13
|
* @param {Object} mapProvider - Map provider instance with `mapToScreen` method
|
|
@@ -25,7 +22,7 @@ export const projectCoords = (coords, mapProvider, mapSize, isMapReady) => {
|
|
|
25
22
|
const { x, y } = mapProvider.mapToScreen(coords)
|
|
26
23
|
return {
|
|
27
24
|
x: x * scaleFactor[mapSize],
|
|
28
|
-
y: y * scaleFactor[mapSize]
|
|
25
|
+
y: y * scaleFactor[mapSize]
|
|
29
26
|
}
|
|
30
27
|
}
|
|
31
28
|
|