@defra/interactive-map 0.0.15-alpha → 0.0.17-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/assets/images/slot-map.svg +264 -0
- 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/slots.md +90 -6
- package/docs/api.md +4 -4
- package/docs/architecture.md +3 -1
- package/docs/{demo.mdx → examples.mdx} +1 -1
- package/docs/getting-started.md +5 -4
- package/docs/index.mdx +42 -0
- package/docs/plugins/datasets.md +561 -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 +8 -16
- package/docusaurus.config.cjs +34 -34
- package/jest.setup.js +1 -1
- package/package.json +6 -5
- package/plugins/beta/datasets/dist/css/index.css +85 -15
- 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/dist/umd/index.js +1 -1
- package/plugins/beta/datasets/src/DatasetsInit.jsx +24 -9
- package/plugins/beta/datasets/src/adapters/maplibre/index.js +18 -0
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +113 -0
- package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +69 -0
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +338 -0
- package/plugins/beta/datasets/src/adapters/maplibre/patternRegistry.js +48 -0
- package/plugins/beta/datasets/src/api/addDataset.js +3 -9
- package/plugins/beta/datasets/src/api/getOpacity.js +17 -0
- package/plugins/beta/datasets/src/api/getStyle.js +13 -0
- package/plugins/beta/datasets/src/api/removeDataset.js +3 -45
- package/plugins/beta/datasets/src/api/setData.js +8 -0
- package/plugins/beta/datasets/src/api/setDatasetVisibility.js +37 -0
- package/plugins/beta/datasets/src/api/setFeatureVisibility.js +22 -0
- package/plugins/beta/datasets/src/api/setOpacity.js +29 -0
- package/plugins/beta/datasets/src/api/setStyle.js +22 -0
- package/plugins/beta/datasets/src/datasets.js +33 -59
- package/plugins/beta/datasets/src/defaults.js +43 -9
- package/plugins/beta/datasets/src/fetch/createDynamicSource.js +39 -30
- package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +2 -2
- package/plugins/beta/datasets/src/manifest.js +27 -19
- package/plugins/beta/datasets/src/panels/Key.jsx +129 -49
- package/plugins/beta/datasets/src/panels/Key.module.scss +48 -9
- package/plugins/beta/datasets/src/panels/Layers.jsx +131 -29
- package/plugins/beta/datasets/src/panels/Layers.module.scss +50 -8
- package/plugins/beta/datasets/src/reducer.js +128 -9
- package/plugins/beta/datasets/src/styles/patterns.js +157 -0
- package/plugins/beta/datasets/src/utils/bbox.js +8 -6
- package/plugins/beta/datasets/src/utils/filters.js +5 -2
- package/plugins/beta/datasets/src/utils/mergeSublayer.js +78 -0
- 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/css/index.css +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/draw.scss +0 -7
- package/plugins/beta/draw-ml/src/events.js +8 -6
- package/plugins/beta/draw-ml/src/manifest.js +29 -29
- 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/dist/esm/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/src/Frame.jsx +9 -9
- 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 +1 -1
- package/plugins/beta/scale-bar/dist/css/index.css +1 -1
- package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -1
- package/plugins/beta/scale-bar/dist/umd/im-scale-bar-plugin.js +1 -1
- package/plugins/beta/scale-bar/src/ScaleBar.jsx +5 -5
- package/plugins/beta/scale-bar/src/index.test.js +3 -3
- package/plugins/beta/scale-bar/src/manifest.js +3 -3
- package/plugins/beta/scale-bar/src/scaleBar.scss +2 -1
- 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/dist/css/index.css +1 -1
- 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/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/interact.scss +0 -7
- package/plugins/interact/src/manifest.js +15 -19
- package/plugins/interact/src/manifest.test.js +6 -5
- 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.module.scss +2 -1
- 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 +3 -3
- package/plugins/search/src/events/fetchSuggestions.test.js +1 -1
- 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 +10 -3
- 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/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.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 +3 -2
- package/providers/maplibre/src/utils/highlightFeatures.test.js +13 -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/Actions/Actions.jsx +2 -2
- package/src/App/components/Actions/Actions.module.scss +0 -7
- package/src/App/components/Actions/Actions.test.jsx +1 -1
- package/src/App/components/Icon/Icon.jsx +3 -2
- package/src/App/components/Icon/Icon.module.scss +4 -0
- package/src/App/components/Icon/Icon.test.jsx +43 -4
- package/src/App/components/MapButton/MapButton.jsx +42 -17
- package/src/App/components/MapButton/MapButton.module.scss +4 -13
- package/src/App/components/MapButton/MapButton.test.jsx +27 -3
- package/src/App/components/PopupMenu/PopupMenu.jsx +51 -274
- package/src/App/components/PopupMenu/PopupMenu.module.scss +14 -7
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +70 -1
- package/src/App/components/PopupMenu/usePopupMenu.js +258 -0
- package/src/App/hooks/useButtonStateEvaluator.js +12 -2
- package/src/App/hooks/useButtonStateEvaluator.test.js +38 -4
- package/src/App/hooks/useInterfaceAPI.js +6 -0
- package/src/App/hooks/useLayoutMeasurements.js +84 -18
- package/src/App/hooks/useLayoutMeasurements.test.js +124 -17
- package/src/App/layout/Layout.jsx +12 -7
- package/src/App/layout/Layout.test.jsx +2 -2
- package/src/App/layout/layout.module.scss +67 -29
- package/src/App/registry/pluginRegistry.js +17 -0
- package/src/App/registry/pluginRegistry.test.js +33 -0
- package/src/App/renderer/HtmlElementHost.jsx +2 -1
- package/src/App/renderer/HtmlElementHost.test.jsx +7 -7
- package/src/App/renderer/mapButtons.js +3 -2
- package/src/App/renderer/mapPanels.test.js +2 -2
- package/src/App/renderer/slotHelpers.js +2 -2
- package/src/App/renderer/slotHelpers.test.js +5 -5
- package/src/App/renderer/slots.js +9 -5
- package/src/App/store/AppProvider.jsx +3 -1
- package/src/App/store/AppProvider.test.jsx +1 -1
- package/src/App/store/ServiceProvider.jsx +3 -1
- package/src/App/store/appActionsMap.js +16 -0
- package/src/App/store/appActionsMap.test.js +27 -0
- package/src/App/store/appDispatchMiddleware.js +33 -1
- package/src/App/store/appDispatchMiddleware.test.js +250 -222
- package/src/App/store/appReducer.js +2 -0
- package/src/InteractiveMap/InteractiveMap.js +4 -0
- package/src/config/appConfig.js +7 -4
- package/src/config/events.js +28 -0
- package/src/scss/main.scss +1 -0
- package/src/scss/settings/_dimensions.scss +0 -1
- package/src/services/logger.js +6 -0
- package/src/services/logger.test.js +32 -0
- package/src/utils/getSafeZoneInset.js +9 -7
- package/src/utils/getSafeZoneInset.test.js +10 -10
- package/webpack.dev.mjs +23 -19
- package/docs/govuk-prototype.md +0 -23
- package/docs/index.md +0 -19
- package/plugins/beta/datasets/src/api/hideDataset.js +0 -14
- package/plugins/beta/datasets/src/api/hideFeatures.js +0 -41
- package/plugins/beta/datasets/src/api/showDataset.js +0 -14
- package/plugins/beta/datasets/src/api/showFeatures.js +0 -44
- package/plugins/beta/datasets/src/handleSetMapStyle.js +0 -54
- package/plugins/beta/datasets/src/mapLayers.js +0 -165
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { useState, useMemo, useEffect } from 'react'
|
|
2
|
+
import { stringToKebab } from '../../../utils/stringToKebab.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Computes the position and alignment style for the popup menu based on the
|
|
6
|
+
* triggering button's bounding rect. Positions above/below and left/center/right
|
|
7
|
+
* depending on which third of the screen the button centre falls in.
|
|
8
|
+
*
|
|
9
|
+
* @param {DOMRect|null} buttonRect - Bounding rect of the trigger button, or null.
|
|
10
|
+
* @returns {{ style: object, direction: string, halign: string }}
|
|
11
|
+
*/
|
|
12
|
+
const getMenuStyle = (buttonRect) => {
|
|
13
|
+
if (!buttonRect) {
|
|
14
|
+
return { style: {}, direction: 'below' }
|
|
15
|
+
}
|
|
16
|
+
const style = {}
|
|
17
|
+
let direction
|
|
18
|
+
if (buttonRect.top >= window.innerHeight / 2) {
|
|
19
|
+
style.bottom = `${window.innerHeight - buttonRect.top}px`
|
|
20
|
+
direction = 'above'
|
|
21
|
+
} else {
|
|
22
|
+
style.top = `${buttonRect.bottom}px`
|
|
23
|
+
direction = 'below'
|
|
24
|
+
}
|
|
25
|
+
const buttonCenterX = (buttonRect.left + buttonRect.right) / 2
|
|
26
|
+
let halign
|
|
27
|
+
if (buttonCenterX > (window.innerWidth * 2) / 3) { // NOSONAR, third of a page width
|
|
28
|
+
style.right = `${window.innerWidth - buttonRect.right}px`
|
|
29
|
+
halign = 'right'
|
|
30
|
+
} else if (buttonCenterX < window.innerWidth / 3) { // NOSONAR, third of a page width
|
|
31
|
+
style.left = `${buttonRect.left}px`
|
|
32
|
+
halign = 'left'
|
|
33
|
+
} else {
|
|
34
|
+
style.left = `${buttonCenterX}px`
|
|
35
|
+
halign = 'center'
|
|
36
|
+
}
|
|
37
|
+
return { style, direction, halign }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Invokes an item's action via buttonConfig.onClick (if configured) or item.onClick.
|
|
42
|
+
* For keyboard-triggered activations also dispatches a synthetic MouseEvent so that
|
|
43
|
+
* any window-level click listeners (e.g. editVertexMode) fire as expected.
|
|
44
|
+
* The synthetic event is marked _fromKeyboardActivation so handleItemClick can
|
|
45
|
+
* ignore it and avoid double-activation.
|
|
46
|
+
*
|
|
47
|
+
* @param {React.SyntheticEvent} e - The triggering React event.
|
|
48
|
+
* @param {object} item - The item being activated.
|
|
49
|
+
* @param {object} ctx - Dependencies: { buttonConfig, evaluateProp, pluginId, id }.
|
|
50
|
+
*/
|
|
51
|
+
const activateItem = (e, item, { buttonConfig, evaluateProp, pluginId, id }) => {
|
|
52
|
+
const menuItemConfig = buttonConfig[item.id]
|
|
53
|
+
if (typeof menuItemConfig?.onClick === 'function') {
|
|
54
|
+
menuItemConfig.onClick(e, evaluateProp(ctx => ctx, pluginId))
|
|
55
|
+
} else if (typeof item.onClick === 'function') {
|
|
56
|
+
item.onClick(e.nativeEvent)
|
|
57
|
+
} else {
|
|
58
|
+
// No action
|
|
59
|
+
}
|
|
60
|
+
if (e.nativeEvent instanceof KeyboardEvent) {
|
|
61
|
+
const el = document.getElementById(`${id}-${stringToKebab(item.id)}`)
|
|
62
|
+
if (el) {
|
|
63
|
+
const click = new MouseEvent('click', { bubbles: true, cancelable: true })
|
|
64
|
+
click._fromKeyboardActivation = true
|
|
65
|
+
el.dispatchEvent(click)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Builds the keydown handler for the menu UL. Handles Escape/Tab (close & focus),
|
|
72
|
+
* ArrowDown/Up (navigate visible items), Home/End (jump to ends),
|
|
73
|
+
* Enter (activate and close), Space (activate; close only for non-checkbox items).
|
|
74
|
+
*
|
|
75
|
+
* @param {object} p
|
|
76
|
+
* @param {Array} p.items - All menu item descriptors.
|
|
77
|
+
* @param {number[]} p.visibleIndices - Indices of non-hidden items.
|
|
78
|
+
* @param {number} p.index - Currently highlighted index.
|
|
79
|
+
* @param {Function} p.setIndex - State setter for highlighted index.
|
|
80
|
+
* @param {Set} p.disabledButtons - IDs of disabled items.
|
|
81
|
+
* @param {object} p.instigator - DOM node of the trigger button.
|
|
82
|
+
* @param {Function} p.setIsOpen - Callback to close the menu.
|
|
83
|
+
* @param {object} p.activateCtx - Context passed through to activateItem.
|
|
84
|
+
* @returns {Function} onKeyDown handler for the menu element.
|
|
85
|
+
*/
|
|
86
|
+
const createMenuKeyDownHandler = ({ items, visibleIndices, index, setIndex, disabledButtons, instigator, setIsOpen, activateCtx }) => {
|
|
87
|
+
const closeAndFocus = (e, preventDefault = false) => {
|
|
88
|
+
if (preventDefault && e?.preventDefault) {
|
|
89
|
+
e.preventDefault()
|
|
90
|
+
}
|
|
91
|
+
instigator.focus()
|
|
92
|
+
setIsOpen(false)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const navigateVisible = (e) => {
|
|
96
|
+
e.preventDefault()
|
|
97
|
+
const n = visibleIndices.length
|
|
98
|
+
if (n === 0) {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
const pos = visibleIndices.indexOf(index)
|
|
102
|
+
let nextPos
|
|
103
|
+
if (e.key === 'ArrowDown') {
|
|
104
|
+
nextPos = pos === -1 ? 0 : (pos + 1) % n
|
|
105
|
+
} else if (pos === -1) {
|
|
106
|
+
nextPos = n - 1
|
|
107
|
+
} else {
|
|
108
|
+
nextPos = (pos - 1 + n) % n
|
|
109
|
+
}
|
|
110
|
+
setIndex(visibleIndices[nextPos])
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const handleEnter = (e) => {
|
|
114
|
+
e.preventDefault()
|
|
115
|
+
const item = items[index]
|
|
116
|
+
if (item && !disabledButtons.has(item.id)) {
|
|
117
|
+
activateItem(e, item, activateCtx)
|
|
118
|
+
}
|
|
119
|
+
instigator.focus()
|
|
120
|
+
setIsOpen(false)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const handleSpace = (e) => {
|
|
124
|
+
e.preventDefault()
|
|
125
|
+
const item = items[index]
|
|
126
|
+
if (!item || disabledButtons.has(item.id)) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
activateItem(e, item, activateCtx)
|
|
130
|
+
if (!(item.isPressed !== undefined || item.pressedWhen)) {
|
|
131
|
+
instigator.focus()
|
|
132
|
+
setIsOpen(false)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (e) => {
|
|
137
|
+
if (['Escape', 'Esc'].includes(e.key)) {
|
|
138
|
+
closeAndFocus(e, true)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
if (e.key === 'Tab') {
|
|
142
|
+
closeAndFocus(e)
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
if (['ArrowDown', 'ArrowUp'].includes(e.key)) {
|
|
146
|
+
navigateVisible(e)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
if (e.key === 'Home' && visibleIndices.length) {
|
|
150
|
+
setIndex(visibleIndices[0])
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
if (e.key === 'End' && visibleIndices.length) {
|
|
154
|
+
setIndex(visibleIndices[visibleIndices.length - 1])
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
if (e.key === 'Enter') {
|
|
158
|
+
handleEnter(e)
|
|
159
|
+
}
|
|
160
|
+
if (e.key === ' ') {
|
|
161
|
+
handleSpace(e)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Custom hook encapsulating all state and event-handler logic for PopupMenu.
|
|
168
|
+
*
|
|
169
|
+
* @param {object} params
|
|
170
|
+
* @param {Array} params.items - Menu item descriptors.
|
|
171
|
+
* @param {Set} params.hiddenButtons - IDs of items that should not be visible.
|
|
172
|
+
* @param {number} [params.startIndex] - Exact index to select on mount; takes precedence over startPos.
|
|
173
|
+
* @param {string} [params.startPos] - 'first' | 'last' — initial selection strategy.
|
|
174
|
+
* @param {object} params.instigator - DOM node of the button that opened the menu.
|
|
175
|
+
* @param {string} params.instigatorKey - Key used to look up instigator in buttonRefs.
|
|
176
|
+
* @param {object} params.buttonRefs - Ref map of all registered button DOM nodes.
|
|
177
|
+
* @param {object} params.buttonConfig - Config map that may override item onClick handlers.
|
|
178
|
+
* @param {Set} params.disabledButtons - IDs of currently disabled items.
|
|
179
|
+
* @param {string} params.pluginId - Plugin context passed to evaluateProp.
|
|
180
|
+
* @param {Function} params.evaluateProp - Context evaluator from useEvaluateProp.
|
|
181
|
+
* @param {string} params.id - App-level ID prefix for DOM element IDs.
|
|
182
|
+
* @param {object} params.menuRef - Ref to the menu UL element.
|
|
183
|
+
* @param {Function} params.setIsOpen - Callback to open/close the menu.
|
|
184
|
+
* @param {DOMRect} params.buttonRect - Bounding rect of the trigger button for positioning.
|
|
185
|
+
* @returns {{ index: number, handleMenuKeyDown: Function, handleItemClick: Function,
|
|
186
|
+
* menuStyle: object, menuDirection: string, menuHAlign: string }}
|
|
187
|
+
*/
|
|
188
|
+
export const usePopupMenu = ({
|
|
189
|
+
items, hiddenButtons, startIndex, startPos, instigator, instigatorKey,
|
|
190
|
+
buttonRefs, buttonConfig, disabledButtons, pluginId, evaluateProp, id, menuRef, setIsOpen, buttonRect
|
|
191
|
+
}) => {
|
|
192
|
+
const visibleIndices = useMemo(() => {
|
|
193
|
+
const visible = []
|
|
194
|
+
items.forEach((item, idx) => {
|
|
195
|
+
if (!hiddenButtons.has(item.id)) {
|
|
196
|
+
visible.push(idx)
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
return visible
|
|
200
|
+
}, [items, hiddenButtons])
|
|
201
|
+
|
|
202
|
+
const [index, setIndex] = useState(() => {
|
|
203
|
+
if (typeof startIndex === 'number') {
|
|
204
|
+
return startIndex
|
|
205
|
+
}
|
|
206
|
+
if (startPos === 'first') {
|
|
207
|
+
return visibleIndices[0] ?? -1
|
|
208
|
+
}
|
|
209
|
+
if (startPos === 'last') {
|
|
210
|
+
return visibleIndices[visibleIndices.length - 1] ?? -1
|
|
211
|
+
}
|
|
212
|
+
return -1
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
const activateCtx = { buttonConfig, evaluateProp, pluginId, id }
|
|
216
|
+
|
|
217
|
+
const handleMenuKeyDown = createMenuKeyDownHandler({
|
|
218
|
+
items, visibleIndices, index, setIndex, disabledButtons, instigator, setIsOpen, activateCtx
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
const handleOutside = (e) => {
|
|
222
|
+
if (menuRef.current?.contains(e.target) || buttonRefs.current[instigatorKey]?.contains(e.target)) {
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
setIsOpen(false)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const handleItemClick = (e, item) => {
|
|
229
|
+
if (e.nativeEvent._fromKeyboardActivation || disabledButtons.has(item.id)) {
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
setIsOpen(false)
|
|
233
|
+
activateItem(e, item, activateCtx)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
menuRef.current?.focus()
|
|
238
|
+
if (startPos === 'first') {
|
|
239
|
+
setIndex(visibleIndices[0] ?? -1)
|
|
240
|
+
} else if (startPos === 'last') {
|
|
241
|
+
setIndex(visibleIndices[visibleIndices.length - 1] ?? -1)
|
|
242
|
+
} else {
|
|
243
|
+
// No action
|
|
244
|
+
}
|
|
245
|
+
const handleResize = () => setIsOpen(false)
|
|
246
|
+
document.addEventListener('focusin', handleOutside)
|
|
247
|
+
document.addEventListener('pointerdown', handleOutside)
|
|
248
|
+
window.addEventListener('resize', handleResize)
|
|
249
|
+
return () => {
|
|
250
|
+
document.removeEventListener('focusin', handleOutside)
|
|
251
|
+
document.removeEventListener('pointerdown', handleOutside)
|
|
252
|
+
window.removeEventListener('resize', handleResize)
|
|
253
|
+
}
|
|
254
|
+
}, [])
|
|
255
|
+
|
|
256
|
+
const { style: menuStyle, direction: menuDirection, halign: menuHAlign } = getMenuStyle(buttonRect)
|
|
257
|
+
return { index, handleMenuKeyDown, handleItemClick, menuStyle, menuDirection, menuHAlign }
|
|
258
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// src/core/hooks/useButtonStateEvaluator.js
|
|
2
1
|
import { useLayoutEffect, useContext } from 'react'
|
|
3
2
|
import { useApp } from '../store/appContext.js'
|
|
4
3
|
import { useConfig } from '../store/configContext.js'
|
|
@@ -61,6 +60,12 @@ export function useButtonStateEvaluator (evaluateProp) {
|
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
const { dispatch } = appState
|
|
63
|
+
let dispatchCount = 0
|
|
64
|
+
|
|
65
|
+
const trackingDispatch = (action) => {
|
|
66
|
+
dispatchCount++
|
|
67
|
+
dispatch(action)
|
|
68
|
+
}
|
|
64
69
|
|
|
65
70
|
pluginRegistry.registeredPlugins.forEach(plugin => {
|
|
66
71
|
const buttons = (plugin?.manifest?.buttons ?? []).flatMap(b => [b, ...(b.menuItems ?? [])])
|
|
@@ -70,10 +75,15 @@ export function useButtonStateEvaluator (evaluateProp) {
|
|
|
70
75
|
btn,
|
|
71
76
|
pluginId: plugin.id,
|
|
72
77
|
appState,
|
|
73
|
-
dispatch,
|
|
78
|
+
dispatch: trackingDispatch,
|
|
74
79
|
evaluateProp
|
|
75
80
|
})
|
|
76
81
|
)
|
|
77
82
|
})
|
|
83
|
+
|
|
84
|
+
if (dispatchCount === 0 && !appState.arePluginsEvaluated) {
|
|
85
|
+
// No changes and flag not yet set — all button states have settled.
|
|
86
|
+
dispatch({ type: 'PLUGINS_EVALUATED' })
|
|
87
|
+
}
|
|
78
88
|
}, [appState, pluginContext, evaluateProp])
|
|
79
89
|
}
|
|
@@ -22,6 +22,7 @@ describe('useButtonStateEvaluator', () => {
|
|
|
22
22
|
hiddenButtons: new Set(),
|
|
23
23
|
pressedButtons: new Set(),
|
|
24
24
|
expandedButtons: new Set(),
|
|
25
|
+
arePluginsEvaluated: true, // stable by default; override in settlement tests
|
|
25
26
|
dispatch: mockDispatch
|
|
26
27
|
}
|
|
27
28
|
useApp.mockReturnValue(mockAppState)
|
|
@@ -149,8 +150,6 @@ describe('useButtonStateEvaluator', () => {
|
|
|
149
150
|
})
|
|
150
151
|
|
|
151
152
|
it('covers fallback to empty array when manifest or buttons is missing', () => {
|
|
152
|
-
// Branch 1: Plugin exists but manifest is missing
|
|
153
|
-
// Branch 2: Manifest exists but buttons is missing
|
|
154
153
|
mockPluginRegistry.registeredPlugins = [
|
|
155
154
|
{ id: 'p1' },
|
|
156
155
|
{ id: 'p2', manifest: {} },
|
|
@@ -158,9 +157,44 @@ describe('useButtonStateEvaluator', () => {
|
|
|
158
157
|
]
|
|
159
158
|
|
|
160
159
|
renderHook(() => useButtonStateEvaluator((fn) => fn()))
|
|
160
|
+
expect(mockDispatch).not.toHaveBeenCalled()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// --- Plugin evaluation settlement ---
|
|
164
|
+
|
|
165
|
+
it('dispatches PLUGINS_EVALUATED when no button states changed and arePluginsEvaluated is false', () => {
|
|
166
|
+
mockAppState.arePluginsEvaluated = false
|
|
167
|
+
renderHook(() => useButtonStateEvaluator((fn) => fn()))
|
|
168
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: 'PLUGINS_EVALUATED' })
|
|
169
|
+
})
|
|
161
170
|
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
it('does not dispatch PLUGINS_EVALUATED when arePluginsEvaluated is already true', () => {
|
|
172
|
+
mockAppState.arePluginsEvaluated = true
|
|
173
|
+
renderHook(() => useButtonStateEvaluator((fn) => fn()))
|
|
164
174
|
expect(mockDispatch).not.toHaveBeenCalled()
|
|
165
175
|
})
|
|
176
|
+
|
|
177
|
+
it('does not dispatch CLEAR_PLUGINS_EVALUATED or PLUGINS_EVALUATED when button states change', () => {
|
|
178
|
+
mockAppState.arePluginsEvaluated = false
|
|
179
|
+
mockPluginRegistry.registeredPlugins = [{
|
|
180
|
+
id: 'p1',
|
|
181
|
+
manifest: { buttons: [{ id: 'btn1', hiddenWhen: () => true }] }
|
|
182
|
+
}]
|
|
183
|
+
|
|
184
|
+
renderHook(() => useButtonStateEvaluator((fn) => fn()))
|
|
185
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: 'TOGGLE_BUTTON_HIDDEN', payload: { id: 'btn1', isHidden: true } })
|
|
186
|
+
expect(mockDispatch).not.toHaveBeenCalledWith({ type: 'CLEAR_PLUGINS_EVALUATED' })
|
|
187
|
+
expect(mockDispatch).not.toHaveBeenCalledWith({ type: 'PLUGINS_EVALUATED' })
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('does not dispatch CLEAR_PLUGINS_EVALUATED when button states change and already evaluated', () => {
|
|
191
|
+
mockAppState.arePluginsEvaluated = true
|
|
192
|
+
mockPluginRegistry.registeredPlugins = [{
|
|
193
|
+
id: 'p1',
|
|
194
|
+
manifest: { buttons: [{ id: 'btn1', hiddenWhen: () => true }] }
|
|
195
|
+
}]
|
|
196
|
+
|
|
197
|
+
renderHook(() => useButtonStateEvaluator((fn) => fn()))
|
|
198
|
+
expect(mockDispatch).not.toHaveBeenCalledWith({ type: 'CLEAR_PLUGINS_EVALUATED' })
|
|
199
|
+
})
|
|
166
200
|
})
|
|
@@ -54,12 +54,16 @@ export const useInterfaceAPI = () => {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
const handleAppVisible = () => dispatchRef.current({ type: 'TOGGLE_APP_VISIBLE', payload: true })
|
|
58
|
+
const handleAppHidden = () => dispatchRef.current({ type: 'TOGGLE_APP_VISIBLE', payload: false })
|
|
57
59
|
const handleAddPanel = ({ id, config }) => dispatchRef.current({ type: 'ADD_PANEL', payload: { id, config } })
|
|
58
60
|
const handleRemovePanel = (id) => dispatchRef.current({ type: 'REMOVE_PANEL', payload: id })
|
|
59
61
|
const handleShowPanel = (id) => dispatchRef.current({ type: 'OPEN_PANEL', payload: { panelId: id } })
|
|
60
62
|
const handleHidePanel = (id) => dispatchRef.current({ type: 'CLOSE_PANEL', payload: id })
|
|
61
63
|
const handleAddControl = ({ id, config }) => dispatchRef.current({ type: 'ADD_CONTROL', payload: { id, config } })
|
|
62
64
|
|
|
65
|
+
eventBus.on(events.APP_VISIBLE, handleAppVisible)
|
|
66
|
+
eventBus.on(events.APP_HIDDEN, handleAppHidden)
|
|
63
67
|
eventBus.on(events.APP_ADD_BUTTON, handleAddButton)
|
|
64
68
|
eventBus.on(events.APP_TOGGLE_BUTTON_STATE, handleToggleButtonState)
|
|
65
69
|
eventBus.on(events.APP_ADD_PANEL, handleAddPanel)
|
|
@@ -69,6 +73,8 @@ export const useInterfaceAPI = () => {
|
|
|
69
73
|
eventBus.on(events.APP_ADD_CONTROL, handleAddControl)
|
|
70
74
|
|
|
71
75
|
return () => {
|
|
76
|
+
eventBus.off(events.APP_VISIBLE, handleAppVisible)
|
|
77
|
+
eventBus.off(events.APP_HIDDEN, handleAppHidden)
|
|
72
78
|
eventBus.off(events.APP_ADD_BUTTON, handleAddButton)
|
|
73
79
|
eventBus.off(events.APP_TOGGLE_BUTTON_STATE, handleToggleButtonState)
|
|
74
80
|
eventBus.off(events.APP_ADD_PANEL, handleAddPanel)
|
|
@@ -12,8 +12,47 @@ const topColWidth = (left, right) =>
|
|
|
12
12
|
const subSlotMaxHeight = (columnHeight, siblingButtons, gap) =>
|
|
13
13
|
columnHeight - (siblingButtons ? siblingButtons + gap : 0)
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Manages all layout measurements for the map overlay and dispatches the safe
|
|
17
|
+
* zone inset used by the map to pad `fitBounds` / `setView` operations.
|
|
18
|
+
*
|
|
19
|
+
* ## Lifecycle
|
|
20
|
+
*
|
|
21
|
+
* The safe zone must only be dispatched once every plugin button's reactive
|
|
22
|
+
* props (`hiddenWhen`, `enableWhen`, `pressedWhen`, `expandedWhen`) have been
|
|
23
|
+
* evaluated for the current app/map state. Dispatching too early — before
|
|
24
|
+
* buttons that affect layout (e.g. the actions bar) have their correct
|
|
25
|
+
* visibility — produces a stale inset that causes the map to jump when the UI
|
|
26
|
+
* then settles into its real state.
|
|
27
|
+
*
|
|
28
|
+
* ### Trigger events
|
|
29
|
+
* The following state changes can alter which buttons are visible and therefore
|
|
30
|
+
* how much space the UI occupies:
|
|
31
|
+
* - `breakpoint` — responsive layout changes (desktop ↔ mobile / tablet)
|
|
32
|
+
* - `mapSize` — map container size variant changes
|
|
33
|
+
* - `isMapReady` — plugins are enabled on `map:ready`, changing button visibility
|
|
34
|
+
* - `isFullscreen` — fullscreen entry/exit changes which buttons are visible
|
|
35
|
+
* - `appVisible` — app shown/hidden by parent HTML outside React (hybrid mode)
|
|
36
|
+
*
|
|
37
|
+
* When any of these change, `CLEAR_PLUGINS_EVALUATED` is dispatched (Effect 2),
|
|
38
|
+
* which prevents the safe zone from being re-dispatched until
|
|
39
|
+
* `useButtonStateEvaluator` has completed a full pass with no button state
|
|
40
|
+
* changes and sets `PLUGINS_EVALUATED` again.
|
|
41
|
+
*
|
|
42
|
+
* ### Safe zone dispatch
|
|
43
|
+
* Effect 3 fires whenever `arePluginsEvaluated` transitions to `true`, at which
|
|
44
|
+
* point DOM dimensions are stable and `getSafeZoneInset` can be read reliably.
|
|
45
|
+
* A `requestAnimationFrame` is used to ensure the browser has committed all
|
|
46
|
+
* layout changes before measuring.
|
|
47
|
+
*
|
|
48
|
+
* ### Resize observer
|
|
49
|
+
* Effect 4 keeps CSS custom properties up to date whenever any observed element
|
|
50
|
+
* resizes (e.g. panels opening, banner appearing, actions buttons toggling).
|
|
51
|
+
* It does not dispatch the safe zone — safe zone dispatch is owned entirely by
|
|
52
|
+
* Effect 3 to prevent jumps on panel open/close and other non-structural resizes.
|
|
53
|
+
*/
|
|
15
54
|
export function useLayoutMeasurements () {
|
|
16
|
-
const { dispatch, breakpoint, layoutRefs } = useApp()
|
|
55
|
+
const { dispatch, breakpoint, layoutRefs, arePluginsEvaluated, appVisible, isFullscreen } = useApp()
|
|
17
56
|
const { mapSize, isMapReady } = useMap()
|
|
18
57
|
|
|
19
58
|
const {
|
|
@@ -23,24 +62,28 @@ export function useLayoutMeasurements () {
|
|
|
23
62
|
topRef,
|
|
24
63
|
topLeftColRef,
|
|
25
64
|
topRightColRef,
|
|
26
|
-
footerRef,
|
|
27
|
-
actionsRef,
|
|
28
65
|
leftTopRef,
|
|
29
66
|
leftBottomRef,
|
|
30
67
|
rightTopRef,
|
|
31
|
-
rightBottomRef
|
|
68
|
+
rightBottomRef,
|
|
69
|
+
bottomRef,
|
|
70
|
+
bottomRightRef,
|
|
71
|
+
attributionsRef,
|
|
72
|
+
drawerRef,
|
|
73
|
+
actionsRef
|
|
32
74
|
} = layoutRefs
|
|
33
75
|
|
|
34
|
-
//
|
|
35
|
-
// 1. Calculate layout CSS vars (side effect)
|
|
36
|
-
//
|
|
76
|
+
// --------------------------------
|
|
77
|
+
// 1. Calculate layout CSS vars (pure side effect, no dispatch)
|
|
78
|
+
// --------------------------------
|
|
37
79
|
const calculateLayout = () => {
|
|
38
80
|
const appContainer = appContainerRef.current
|
|
39
81
|
const main = mainRef.current
|
|
40
82
|
const top = topRef.current
|
|
41
83
|
const topLeftCol = topLeftColRef.current
|
|
42
84
|
const topRightCol = topRightColRef.current
|
|
43
|
-
const bottom =
|
|
85
|
+
const bottom = bottomRef.current
|
|
86
|
+
const attributions = attributionsRef.current
|
|
44
87
|
|
|
45
88
|
if ([main, top, bottom].some(r => !r)) {
|
|
46
89
|
return
|
|
@@ -60,10 +103,17 @@ export function useLayoutMeasurements () {
|
|
|
60
103
|
appContainer.style.setProperty('--left-top-max-height', `${leftColumnHeight}px`)
|
|
61
104
|
|
|
62
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
|
|
63
111
|
const rightOffsetTop = topRightCol.offsetHeight + top.offsetTop
|
|
64
|
-
const
|
|
112
|
+
const rightEffectiveBottom = bottom.offsetTop + bottom.offsetHeight - bottomRightHeight
|
|
113
|
+
const rightColumnHeight = rightEffectiveBottom - rightOffsetTop - dividerGap
|
|
114
|
+
const rightOffsetBottom = bottomContainerPad + (bottomRightHeight > 0 ? (bottomRightHeight + dividerGap) : attributions.offsetHeight)
|
|
65
115
|
appContainer.style.setProperty('--right-offset-top', `${rightOffsetTop}px`)
|
|
66
|
-
appContainer.style.setProperty('--right-offset-bottom', `${
|
|
116
|
+
appContainer.style.setProperty('--right-offset-bottom', `${rightOffsetBottom}px`)
|
|
67
117
|
appContainer.style.setProperty('--right-top-max-height', `${rightColumnHeight}px`)
|
|
68
118
|
|
|
69
119
|
// === Sub-slot panel max-heights ===
|
|
@@ -74,22 +124,38 @@ export function useLayoutMeasurements () {
|
|
|
74
124
|
}
|
|
75
125
|
|
|
76
126
|
// --------------------------------
|
|
77
|
-
// 2.
|
|
127
|
+
// 2. Clear the evaluated flag when structural inputs change so the safe zone
|
|
128
|
+
// is not dispatched until useButtonStateEvaluator has completed a full
|
|
129
|
+
// pass with the new app/map state and set PLUGINS_EVALUATED.
|
|
78
130
|
// --------------------------------
|
|
79
131
|
useLayoutEffect(() => {
|
|
80
|
-
|
|
81
|
-
|
|
132
|
+
dispatch({ type: 'CLEAR_PLUGINS_EVALUATED' })
|
|
133
|
+
}, [breakpoint, mapSize, isMapReady, appVisible, isFullscreen])
|
|
82
134
|
|
|
83
|
-
|
|
135
|
+
// --------------------------------
|
|
136
|
+
// 3. Once all plugin button props have been evaluated (arePluginsEvaluated),
|
|
137
|
+
// recalculate layout and dispatch the safe zone inset.
|
|
138
|
+
// RAF required to ensure browser layout is committed before measuring.
|
|
139
|
+
// --------------------------------
|
|
140
|
+
useLayoutEffect(() => {
|
|
141
|
+
if (!arePluginsEvaluated) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
requestAnimationFrame(() => {
|
|
145
|
+
calculateLayout()
|
|
84
146
|
const safeZoneInset = getSafeZoneInset(layoutRefs)
|
|
85
|
-
|
|
147
|
+
if (safeZoneInset) {
|
|
148
|
+
dispatch({ type: 'SET_SAFE_ZONE_INSET', payload: { safeZoneInset } })
|
|
149
|
+
}
|
|
86
150
|
})
|
|
87
|
-
}, [
|
|
151
|
+
}, [arePluginsEvaluated])
|
|
88
152
|
|
|
89
153
|
// --------------------------------
|
|
90
|
-
//
|
|
154
|
+
// 4. Recalculate CSS vars whenever observed elements resize (panels, banner,
|
|
155
|
+
// actions buttons, etc.). Safe zone is intentionally not dispatched here —
|
|
156
|
+
// that is Effect 3's responsibility.
|
|
91
157
|
// --------------------------------
|
|
92
|
-
useResizeObserver([bannerRef, mainRef, topRef, topLeftColRef, topRightColRef, actionsRef,
|
|
158
|
+
useResizeObserver([bannerRef, mainRef, topRef, topLeftColRef, topRightColRef, actionsRef, bottomRef, bottomRightRef, leftTopRef, leftBottomRef, rightTopRef, rightBottomRef, drawerRef], () => {
|
|
93
159
|
requestAnimationFrame(() => {
|
|
94
160
|
calculateLayout()
|
|
95
161
|
})
|