@defra/interactive-map 0.0.4-alpha → 0.0.6-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api.md +8 -0
- package/package.json +3 -2
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +2 -1
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js.LICENSE.txt +1 -0
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +2 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js.LICENSE.txt +1 -0
- package/plugins/beta/datasets/dist/umd/index.js +1 -1
- package/plugins/beta/datasets/src/datasets.js +55 -2
- package/plugins/beta/datasets/src/fetch/createDynamicSource.js +206 -0
- package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +38 -0
- package/plugins/beta/datasets/src/handleSetMapStyle.js +8 -1
- package/plugins/beta/datasets/src/mapLayers.js +41 -3
- package/plugins/beta/datasets/src/utils/bbox.js +113 -2
- 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/dist/umd/index.js +1 -1
- package/plugins/beta/draw-ml/src/api/addFeature.js +15 -3
- package/plugins/beta/draw-ml/src/api/editFeature.js +16 -5
- package/plugins/beta/draw-ml/src/api/newLine.js +32 -7
- package/plugins/beta/draw-ml/src/api/newPolygon.js +33 -8
- package/plugins/beta/draw-ml/src/defaults.js +1 -0
- package/plugins/beta/draw-ml/src/draw.scss +23 -0
- package/plugins/beta/draw-ml/src/events.js +55 -113
- package/plugins/beta/draw-ml/src/manifest.js +64 -38
- package/plugins/beta/draw-ml/src/mapboxSnap.js +16 -12
- package/plugins/beta/draw-ml/src/modes/createDrawMode.js +4 -3
- package/plugins/beta/draw-ml/src/modes/editVertex/geometryHelpers.js +135 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/helpers.js +2 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +134 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/undoHandlers.js +133 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +141 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/vertexQueries.js +121 -0
- package/plugins/beta/draw-ml/src/modes/editVertexMode.js +51 -406
- package/plugins/beta/draw-ml/src/utils/flattenStyleProperties.js +49 -0
- 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 +1 -1
- package/plugins/beta/map-styles/dist/css/index.css +1 -1
- package/plugins/beta/map-styles/dist/esm/index.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.scss +4 -1
- 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/dist/umd/index.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +1 -1
- package/plugins/interact/src/defaults.js +1 -0
- package/plugins/interact/src/events.js +37 -52
- package/plugins/interact/src/events.test.js +35 -0
- package/plugins/interact/src/hooks/useHighlightSync.js +1 -1
- package/plugins/interact/src/hooks/useInteractionHandlers.js +80 -48
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +64 -8
- package/plugins/interact/src/interact.scss +6 -0
- package/plugins/interact/src/reducer.js +25 -27
- package/plugins/interact/src/reducer.test.js +19 -6
- package/plugins/interact/src/utils/featureQueries.js +2 -2
- package/plugins/interact/src/utils/spatial.js +99 -0
- package/plugins/interact/src/utils/spatial.test.js +100 -0
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/index.js +1 -1
- package/plugins/search/src/Search.jsx +2 -2
- package/plugins/search/src/components/Form/Form.jsx +1 -1
- package/plugins/search/src/components/Suggestions/Suggestions.jsx +5 -5
- package/plugins/search/src/datasets.js +1 -1
- package/plugins/search/src/defaults.js +3 -0
- package/plugins/search/src/events/fetchSuggestions.js +48 -53
- package/plugins/search/src/events/formHandlers.js +3 -2
- package/plugins/search/src/events/index.js +1 -1
- package/plugins/search/src/events/inputHandlers.js +3 -1
- package/plugins/search/src/events/suggestionHandlers.js +12 -3
- package/plugins/search/src/search.scss +4 -1
- package/plugins/search/src/utils/parseOsNamesResults.js +12 -12
- package/providers/beta/esri/src/esriProvider.js +1 -1
- package/providers/beta/esri/src/index.js +1 -1
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/esm/index.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-framework.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/defaults.js +3 -2
- package/providers/maplibre/src/index.js +22 -19
- package/providers/maplibre/src/maplibreProvider.js +13 -11
- package/providers/maplibre/src/utils/detectWebgl.js +1 -2
- package/providers/maplibre/src/utils/highlightFeatures.js +78 -67
- package/providers/maplibre/src/utils/labels.js +174 -120
- package/providers/maplibre/src/utils/maplibreFixes.js +2 -2
- package/providers/maplibre/src/utils/queryFeatures.js +146 -0
- package/providers/maplibre/src/utils/spatial.js +14 -17
- package/sonar-project.properties +39 -2
- package/src/App/components/Actions/Actions.jsx +2 -2
- package/src/App/components/Actions/Actions.module.scss +2 -2
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +3 -1
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +9 -0
- package/src/App/components/Logo/Logo.jsx +1 -1
- package/src/App/components/MapButton/MapButton.jsx +217 -41
- package/src/App/components/MapButton/MapButton.module.scss +32 -21
- package/src/App/components/MapButton/MapButton.test.jsx +128 -84
- package/src/App/components/Markers/Markers.jsx +1 -1
- package/src/App/components/Markers/Markers.module.scss +0 -5
- package/src/App/components/Panel/Panel.jsx +75 -45
- package/src/App/components/Panel/Panel.module.scss +18 -24
- package/src/App/components/Panel/Panel.test.jsx +18 -0
- package/src/App/components/PopupMenu/PopupMenu.jsx +246 -0
- package/src/App/components/PopupMenu/PopupMenu.module.scss +70 -0
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +304 -0
- package/src/App/components/Tooltip/Tooltip.jsx +4 -2
- package/src/App/components/Tooltip/getTooltipPosition.js +1 -1
- package/src/App/components/Viewport/MapController.jsx +1 -1
- package/src/App/components/Viewport/MapStatus.jsx +1 -1
- package/src/App/components/Viewport/Viewport.jsx +2 -2
- package/src/App/components/Viewport/Viewport.module.scss +2 -2
- package/src/App/controls/keyboardActions.js +3 -2
- package/src/App/controls/keyboardShortcuts.js +4 -2
- package/src/App/hooks/useButtonStateEvaluator.js +58 -45
- package/src/App/hooks/useButtonStateEvaluator.test.js +36 -3
- package/src/App/hooks/useCrossHairAPI.js +49 -47
- package/src/App/hooks/useFocusVisible.js +2 -2
- package/src/App/hooks/useInterfaceAPI.js +16 -4
- package/src/App/hooks/useKeyboardHint.js +1 -1
- package/src/App/hooks/useKeyboardShortcuts.js +4 -2
- package/src/App/hooks/useMapAnnouncements.js +34 -32
- package/src/App/hooks/useMapEvents.js +12 -1
- package/src/App/hooks/useMapProviderOverrides.js +3 -3
- package/src/App/hooks/useMapStateSync.js +7 -1
- package/src/App/hooks/useMapURLSync.js +6 -1
- package/src/App/hooks/useMarkersAPI.js +88 -50
- package/src/App/hooks/useMediaQueryDispatch.test.js +48 -0
- package/src/App/hooks/useModalPanelBehaviour.js +46 -40
- package/src/App/hooks/useResizeObserver.js +2 -2
- package/src/App/initialiseApp.js +45 -38
- package/src/App/layout/Layout.jsx +6 -4
- package/src/App/layout/layout.module.scss +13 -17
- package/src/App/registry/keyboardShortcutRegistry.js +12 -2
- package/src/App/registry/keyboardShortcutRegistry.test.js +28 -0
- package/src/App/registry/mergeManifests.js +1 -1
- package/src/App/registry/panelRegistry.js +1 -1
- package/src/App/registry/panelRegistry.test.js +30 -1
- package/src/App/registry/pluginRegistry.js +14 -51
- package/src/App/registry/pluginRegistry.test.js +54 -6
- package/src/App/renderer/HtmlElementHost.jsx +186 -0
- package/src/App/renderer/HtmlElementHost.test.jsx +231 -0
- package/src/App/renderer/mapButtons.js +11 -8
- package/src/App/renderer/mapButtons.test.js +6 -4
- package/src/App/renderer/mapControls.js +6 -0
- package/src/App/renderer/mapControls.test.js +10 -2
- package/src/App/renderer/mapPanels.js +39 -26
- package/src/App/renderer/mapPanels.test.js +29 -0
- package/src/App/renderer/pluginWrapper.test.js +28 -0
- package/src/App/renderer/slotHelpers.js +60 -0
- package/src/App/renderer/slotHelpers.test.js +82 -0
- package/src/App/store/AppProvider.jsx +21 -18
- package/src/App/store/AppProvider.test.jsx +40 -0
- package/src/App/store/PluginProvider.jsx +7 -5
- package/src/App/store/appActionsMap.js +28 -1
- package/src/App/store/appActionsMap.test.js +26 -0
- package/src/App/store/appReducer.js +1 -0
- package/src/App/store/mapActionsMap.js +12 -24
- package/src/App/store/mapReducer.js +1 -1
- package/src/InteractiveMap/InteractiveMap.js +79 -6
- package/src/InteractiveMap/InteractiveMap.test.js +164 -2
- package/src/InteractiveMap/behaviourController.js +12 -2
- package/src/InteractiveMap/behaviourController.test.js +82 -1
- package/src/InteractiveMap/deviceChecker.js +1 -1
- package/src/InteractiveMap/deviceChecker.test.js +2 -2
- package/src/InteractiveMap/historyManager.js +41 -5
- package/src/InteractiveMap/historyManager.test.js +37 -6
- package/src/InteractiveMap/polyfills.js +39 -0
- package/src/InteractiveMap/polyfills.test.js +127 -0
- package/src/config/appConfig.js +5 -5
- package/src/config/appConfig.test.js +107 -42
- package/src/config/defaults.js +1 -0
- package/src/index.umd.js +1 -1
- package/src/scss/main.scss +1 -0
- package/src/scss/settings/_colors.scss +1 -1
- package/src/scss/settings/_dimensions.scss +4 -0
- package/src/services/eventBus.test.js +24 -1
- package/src/test-utils.js +1 -0
- package/src/types.js +5 -0
- package/src/utils/detectBreakpoint.js +78 -77
- package/src/utils/detectBreakpoint.test.js +50 -4
- package/src/utils/detectInterfaceType.js +6 -5
- package/src/utils/getIsFullscreen.js +3 -1
- package/src/utils/getSafeZoneInset.js +23 -7
- package/src/utils/getSafeZoneInset.test.js +18 -0
- package/src/utils/mapStateSync.js +3 -3
- package/src/utils/queryString.js +1 -1
- package/src/utils/stringToKebab.js +1 -1
- package/src/utils/toggleInertElements.js +37 -12
- package/webpack.dev.mjs +3 -4
- package/plugins/interact/src/utils/turfHelpers.js +0 -30
- package/plugins/interact/src/utils/turfHelpers.test.js +0 -30
- package/plugins/search/src/utils/fetchDataset.js +0 -23
- package/providers/maplibre/dist/esm/im-maplibre-legacy-framework.js +0 -1
- package/providers/maplibre/dist/umd/im-maplibre-legacy-framework.js +0 -1
|
@@ -1,106 +1,107 @@
|
|
|
1
|
-
function
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
let cleanup = null
|
|
5
|
-
|
|
6
|
-
const getBreakpointType = (width) => {
|
|
7
|
-
if (width <= maxMobileWidth) {
|
|
8
|
-
return 'mobile'
|
|
9
|
-
}
|
|
10
|
-
if (width >= minDesktopWidth) {
|
|
11
|
-
return 'desktop'
|
|
12
|
-
}
|
|
13
|
-
return 'tablet'
|
|
1
|
+
function getBreakpointType (width, maxMobileWidth, minDesktopWidth) {
|
|
2
|
+
if (width <= maxMobileWidth) {
|
|
3
|
+
return 'mobile'
|
|
14
4
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (type !== lastBreakpoint) {
|
|
18
|
-
lastBreakpoint = type // Set synchronously BEFORE RAF
|
|
19
|
-
requestAnimationFrame(() => {
|
|
20
|
-
// Double-check it hasn't changed again
|
|
21
|
-
if (lastBreakpoint === type) {
|
|
22
|
-
listeners.forEach(fn => fn(type))
|
|
23
|
-
}
|
|
24
|
-
})
|
|
25
|
-
}
|
|
5
|
+
if (width >= minDesktopWidth) {
|
|
6
|
+
return 'desktop'
|
|
26
7
|
}
|
|
8
|
+
return 'tablet'
|
|
9
|
+
}
|
|
27
10
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
containerEl.style.containerType = 'inline-size'
|
|
31
|
-
|
|
32
|
-
// Set initial detection BEFORE observing to prevent double notification
|
|
33
|
-
const initialWidth = containerEl.getBoundingClientRect().width
|
|
34
|
-
const initialType = getBreakpointType(initialWidth)
|
|
35
|
-
containerEl.setAttribute('data-breakpoint', initialType)
|
|
36
|
-
lastBreakpoint = initialType // Set this directly, don't notify yet
|
|
11
|
+
function createContainerDetector (containerEl, getType, notifyListeners) {
|
|
12
|
+
containerEl.style.containerType = 'inline-size'
|
|
37
13
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
containerEl.setAttribute('data-breakpoint', type)
|
|
42
|
-
notifyListeners(type)
|
|
43
|
-
})
|
|
14
|
+
const initialWidth = containerEl.getBoundingClientRect().width
|
|
15
|
+
const initialType = getType(initialWidth)
|
|
16
|
+
containerEl.dataset.breakpoint = initialType
|
|
44
17
|
|
|
45
|
-
|
|
18
|
+
const observer = new window.ResizeObserver((entries) => {
|
|
19
|
+
const width = entries[0]?.borderBoxSize?.[0]?.inlineSize || entries[0]?.contentRect.width
|
|
20
|
+
const type = getType(width)
|
|
21
|
+
containerEl.dataset.breakpoint = type
|
|
22
|
+
notifyListeners(type)
|
|
23
|
+
})
|
|
46
24
|
|
|
47
|
-
|
|
48
|
-
notifyListeners(initialType)
|
|
25
|
+
observer.observe(containerEl)
|
|
49
26
|
|
|
50
|
-
|
|
27
|
+
return {
|
|
28
|
+
initialType,
|
|
29
|
+
cleanup: () => {
|
|
51
30
|
observer.disconnect()
|
|
52
31
|
containerEl.style.containerType = ''
|
|
53
|
-
containerEl.
|
|
32
|
+
delete containerEl.dataset.breakpoint
|
|
54
33
|
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const mq = {
|
|
58
|
-
mobile: window.matchMedia(`(max-width: ${maxMobileWidth}px)`),
|
|
59
|
-
desktop: window.matchMedia(`(min-width: ${minDesktopWidth}px)`)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const detect = () => {
|
|
63
|
-
let type
|
|
34
|
+
}
|
|
35
|
+
}
|
|
64
36
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
type = 'tablet'
|
|
71
|
-
}
|
|
37
|
+
function createViewportDetector (maxMobileWidth, minDesktopWidth, notifyListeners) {
|
|
38
|
+
const mq = {
|
|
39
|
+
mobile: window.matchMedia(`(max-width: ${maxMobileWidth}px)`),
|
|
40
|
+
desktop: window.matchMedia(`(min-width: ${minDesktopWidth}px)`)
|
|
41
|
+
}
|
|
72
42
|
|
|
73
|
-
|
|
43
|
+
const detect = () => {
|
|
44
|
+
let type = 'tablet'
|
|
45
|
+
if (mq.mobile.matches) {
|
|
46
|
+
type = 'mobile'
|
|
47
|
+
} else if (mq.desktop.matches) {
|
|
48
|
+
type = 'desktop'
|
|
49
|
+
} else {
|
|
50
|
+
// No action
|
|
74
51
|
}
|
|
52
|
+
notifyListeners(type)
|
|
53
|
+
}
|
|
75
54
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
55
|
+
mq.mobile.addEventListener('change', detect)
|
|
56
|
+
mq.desktop.addEventListener('change', detect)
|
|
57
|
+
detect()
|
|
79
58
|
|
|
80
|
-
|
|
59
|
+
return {
|
|
60
|
+
cleanup: () => {
|
|
81
61
|
mq.mobile.removeEventListener('change', detect)
|
|
82
62
|
mq.desktop.removeEventListener('change', detect)
|
|
83
63
|
}
|
|
84
64
|
}
|
|
65
|
+
}
|
|
85
66
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
67
|
+
function createBreakpointDetector ({ maxMobileWidth, minDesktopWidth, containerEl }) {
|
|
68
|
+
let lastBreakpoint = 'unknown'
|
|
69
|
+
const listeners = new Set()
|
|
90
70
|
|
|
91
|
-
const
|
|
92
|
-
|
|
71
|
+
const notifyListeners = (type) => {
|
|
72
|
+
if (type !== lastBreakpoint) {
|
|
73
|
+
lastBreakpoint = type
|
|
74
|
+
requestAnimationFrame(() => {
|
|
75
|
+
if (lastBreakpoint === type) {
|
|
76
|
+
listeners.forEach(fn => fn(type))
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
93
80
|
}
|
|
94
81
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
82
|
+
const getType = (width) => getBreakpointType(width, maxMobileWidth, minDesktopWidth)
|
|
83
|
+
|
|
84
|
+
let cleanup
|
|
85
|
+
if (containerEl) {
|
|
86
|
+
const detector = createContainerDetector(containerEl, getType, notifyListeners)
|
|
87
|
+
lastBreakpoint = detector.initialType
|
|
88
|
+
notifyListeners(detector.initialType)
|
|
89
|
+
cleanup = detector.cleanup
|
|
90
|
+
} else {
|
|
91
|
+
const detector = createViewportDetector(maxMobileWidth, minDesktopWidth, notifyListeners)
|
|
92
|
+
cleanup = detector.cleanup
|
|
98
93
|
}
|
|
99
94
|
|
|
100
95
|
return {
|
|
101
|
-
subscribe
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
subscribe: (fn) => {
|
|
97
|
+
listeners.add(fn)
|
|
98
|
+
return () => listeners.delete(fn)
|
|
99
|
+
},
|
|
100
|
+
getBreakpoint: () => lastBreakpoint,
|
|
101
|
+
destroy: () => {
|
|
102
|
+
cleanup?.()
|
|
103
|
+
listeners.clear()
|
|
104
|
+
}
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
@@ -12,7 +12,7 @@ let mockedQueries = {}
|
|
|
12
12
|
|
|
13
13
|
class MockResizeObserver {
|
|
14
14
|
constructor (callback) { this.callback = callback }
|
|
15
|
-
observe (
|
|
15
|
+
observe () { MockResizeObserver.instance = this }
|
|
16
16
|
disconnect () { MockResizeObserver.instance = null }
|
|
17
17
|
trigger (width, fallback) {
|
|
18
18
|
this.callback([fallback ? { contentRect: { width } } : { borderBoxSize: [{ inlineSize: width }], contentRect: { width } }])
|
|
@@ -35,7 +35,9 @@ const mockMatchMedia = (query) => {
|
|
|
35
35
|
window.matchMedia = jest.fn(mockMatchMedia)
|
|
36
36
|
|
|
37
37
|
const triggerMQ = (query, matches) => {
|
|
38
|
-
if (mockedQueries[query])
|
|
38
|
+
if (mockedQueries[query]) {
|
|
39
|
+
mockedQueries[query].matches = matches
|
|
40
|
+
}
|
|
39
41
|
mediaListeners[query]?.forEach(fn => fn({ matches }))
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -68,6 +70,46 @@ describe('detectBreakpoint', () => {
|
|
|
68
70
|
detector.destroy()
|
|
69
71
|
})
|
|
70
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Test to ensure coverage for the concurrency guard (Line 75).
|
|
75
|
+
* Validates that if multiple breakpoint changes occur before a RAF
|
|
76
|
+
* frame fires, only the most recent (current) state notifies listeners.
|
|
77
|
+
*/
|
|
78
|
+
it('does not notify listeners if the breakpoint changed again before RAF fired', () => {
|
|
79
|
+
const listener = jest.fn()
|
|
80
|
+
const detector = createBreakpointDetector(cfg)
|
|
81
|
+
detector.subscribe(listener)
|
|
82
|
+
|
|
83
|
+
// 1. Initial flush to clear the "mount" notification
|
|
84
|
+
flushRAF()
|
|
85
|
+
listener.mockClear()
|
|
86
|
+
|
|
87
|
+
// 2. Trigger 'mobile'.
|
|
88
|
+
// This sets lastBreakpoint = 'mobile' and queues RAF #1 (type: 'mobile')
|
|
89
|
+
triggerMQ('(max-width: 768px)', true)
|
|
90
|
+
|
|
91
|
+
// 3. IMMEDIATELY switch to 'desktop' before flushing.
|
|
92
|
+
// IMPORTANT: We must turn mobile OFF so the detector sees 'desktop'.
|
|
93
|
+
triggerMQ('(max-width: 768px)', false)
|
|
94
|
+
triggerMQ('(min-width: 1024px)', true)
|
|
95
|
+
|
|
96
|
+
// At this point:
|
|
97
|
+
// - lastBreakpoint is now 'desktop'
|
|
98
|
+
// - There are 3 callbacks in the RAF queue:
|
|
99
|
+
// - RAF #1 from the mobile trigger (type: 'mobile')
|
|
100
|
+
// - RAF #2 from the mobile=false trigger (type: 'tablet')
|
|
101
|
+
// - RAF #3 from the desktop trigger (type: 'desktop')
|
|
102
|
+
|
|
103
|
+
flushRAF()
|
|
104
|
+
|
|
105
|
+
// RAF #1: ('mobile' === 'desktop') is false -> skip listener
|
|
106
|
+
// RAF #2: ('tablet' === 'desktop') is false -> skip listener
|
|
107
|
+
// RAF #3: ('desktop' === 'desktop') is true -> call listener
|
|
108
|
+
expect(listener).toHaveBeenCalledTimes(1)
|
|
109
|
+
expect(listener).toHaveBeenCalledWith('desktop')
|
|
110
|
+
expect(detector.getBreakpoint()).toBe('desktop')
|
|
111
|
+
})
|
|
112
|
+
|
|
71
113
|
describe('viewport mode', () => {
|
|
72
114
|
it.each([
|
|
73
115
|
['mobile', true, false],
|
|
@@ -76,8 +118,12 @@ describe('detectBreakpoint', () => {
|
|
|
76
118
|
])('detects %s', (name, mobile, desktop) => {
|
|
77
119
|
window.matchMedia.mockImplementation(q => {
|
|
78
120
|
const mq = mockMatchMedia(q)
|
|
79
|
-
if (q === '(max-width: 768px)')
|
|
80
|
-
|
|
121
|
+
if (q === '(max-width: 768px)') {
|
|
122
|
+
mq.matches = mobile
|
|
123
|
+
}
|
|
124
|
+
if (q === '(min-width: 1024px)') {
|
|
125
|
+
mq.matches = desktop
|
|
126
|
+
}
|
|
81
127
|
return mq
|
|
82
128
|
})
|
|
83
129
|
const detector = createBreakpointDetector(cfg)
|
|
@@ -33,7 +33,7 @@ function notifyListeners (newType) {
|
|
|
33
33
|
function createInterfaceDetector () {
|
|
34
34
|
const mql = window.matchMedia('(pointer: coarse)')
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// System pointer type changes
|
|
37
37
|
const handleMediaChange = e => {
|
|
38
38
|
notifyListeners(e.matches ? 'touch' : 'mouse')
|
|
39
39
|
}
|
|
@@ -53,19 +53,20 @@ function createInterfaceDetector () {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
window.addEventListener('pointerdown', handlePointer, { passive: true })
|
|
57
|
+
window.addEventListener('keydown', handleKeyDown, { passive: true })
|
|
58
58
|
|
|
59
59
|
// cleanup
|
|
60
60
|
return () => {
|
|
61
61
|
mql.removeEventListener('change', handleMediaChange)
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
window.removeEventListener('pointerdown', handlePointer)
|
|
63
|
+
window.removeEventListener('keydown', handleKeyDown)
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
function getInterfaceType () {
|
|
68
68
|
if (lastInterfaceType === 'unknown') {
|
|
69
|
+
lastInterfaceType = 'mouse'
|
|
69
70
|
return 'mouse'
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export const isHybridFullscreen = (config) => {
|
|
12
12
|
const { behaviour, hybridWidth, maxMobileWidth } = config
|
|
13
|
-
if (behaviour !== 'hybrid')
|
|
13
|
+
if (behaviour !== 'hybrid') {
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
14
16
|
const threshold = hybridWidth ?? maxMobileWidth
|
|
15
17
|
return window.matchMedia(`(max-width: ${threshold}px)`).matches
|
|
16
18
|
}
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculates the safe zone inset — the unobscured region of the map viewport
|
|
3
|
+
* not hidden behind overlay panels, action bars or the footer. Used as padding
|
|
4
|
+
* for map operations like setCenter or fitBounds so the full extent is visible.
|
|
5
|
+
*
|
|
6
|
+
* The algorithm measures the available space around the inset panel and decides
|
|
7
|
+
* whether to push the safe area below or beside it, depending on whether the
|
|
8
|
+
* layout is landscape or portrait oriented.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} refs - React refs for the key layout elements.
|
|
11
|
+
* @param {React.RefObject} refs.mainRef - The main content area.
|
|
12
|
+
* @param {React.RefObject} refs.insetRef - The inset panel (e.g. search results).
|
|
13
|
+
* @param {React.RefObject} refs.rightRef - The right-hand button column.
|
|
14
|
+
* @param {React.RefObject} refs.actionsRef - The bottom action bar.
|
|
15
|
+
* @param {React.RefObject} refs.footerRef - The footer (logo, copyright etc).
|
|
16
|
+
* @returns {{ top: number, right: number, left: number, bottom: number } | undefined}
|
|
17
|
+
* Pixel insets from each edge of the main area, or undefined if any ref is missing.
|
|
18
|
+
*/
|
|
1
19
|
export const getSafeZoneInset = ({
|
|
2
20
|
mainRef,
|
|
3
21
|
insetRef,
|
|
@@ -5,16 +23,14 @@ export const getSafeZoneInset = ({
|
|
|
5
23
|
actionsRef,
|
|
6
24
|
footerRef
|
|
7
25
|
}) => {
|
|
8
|
-
const
|
|
9
|
-
const inset = insetRef.current
|
|
10
|
-
const right = rightRef.current
|
|
11
|
-
const actions = actionsRef.current
|
|
12
|
-
const footer = footerRef.current
|
|
26
|
+
const refs = [mainRef, insetRef, rightRef, actionsRef, footerRef]
|
|
13
27
|
|
|
14
|
-
if (
|
|
15
|
-
return
|
|
28
|
+
if (refs.some(ref => !ref.current)) {
|
|
29
|
+
return undefined
|
|
16
30
|
}
|
|
17
31
|
|
|
32
|
+
const [main, inset, right, actions, footer] = refs.map(ref => ref.current)
|
|
33
|
+
|
|
18
34
|
const root = document.documentElement
|
|
19
35
|
const dividerGap = Number.parseInt(getComputedStyle(root).getPropertyValue('--divider-gap'), 10)
|
|
20
36
|
|
|
@@ -68,4 +68,22 @@ describe('getSafeZoneInset', () => {
|
|
|
68
68
|
expect(result.left).toBe(80)
|
|
69
69
|
expect(result.right).toBe(80)
|
|
70
70
|
})
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Test to ensure coverage for the safety guardrail (Line 29).
|
|
74
|
+
* Validates that the function returns undefined if React refs are
|
|
75
|
+
* not yet attached to DOM elements.
|
|
76
|
+
*/
|
|
77
|
+
it('returns undefined if any ref.current is null (unattached)', () => {
|
|
78
|
+
const unattachedRefs = {
|
|
79
|
+
mainRef: { current: null },
|
|
80
|
+
insetRef: { current: null },
|
|
81
|
+
rightRef: { current: null },
|
|
82
|
+
actionsRef: { current: null },
|
|
83
|
+
footerRef: { current: null }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = getSafeZoneInset(unattachedRefs)
|
|
87
|
+
expect(result).toBeUndefined()
|
|
88
|
+
})
|
|
71
89
|
})
|
|
@@ -11,7 +11,7 @@ const getMapStateFromURL = (id, search) => {
|
|
|
11
11
|
return { center: [lng, lat], zoom }
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const setMapStateInURL = (id, state, currentHref =
|
|
14
|
+
const setMapStateInURL = (id, state, currentHref = window.location.href) => {
|
|
15
15
|
// Use the passed href or the global one
|
|
16
16
|
const url = new URL(currentHref || 'http://localhost')
|
|
17
17
|
const params = [...new URLSearchParams(url.search)].map(([key, value]) => `${key}=${value}`)
|
|
@@ -30,10 +30,10 @@ const setMapStateInURL = (id, state, currentHref = globalThis.location.href) =>
|
|
|
30
30
|
|
|
31
31
|
const hash = url.hash || ''
|
|
32
32
|
const newUrl = `${url.origin}${url.pathname}?${[...filteredParams, ...newParams].join('&')}${hash}`
|
|
33
|
-
|
|
33
|
+
window.history.replaceState(window.history.state, '', newUrl)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const getInitialMapState = ({ id, center, zoom, bounds }, search =
|
|
36
|
+
const getInitialMapState = ({ id, center, zoom, bounds }, search = window.location.search) => {
|
|
37
37
|
// Pass search string down to the internal function
|
|
38
38
|
const savedState = getMapStateFromURL(id, search)
|
|
39
39
|
if (savedState) {
|
package/src/utils/queryString.js
CHANGED
|
@@ -1,31 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks whether a sibling element should be marked as inert.
|
|
3
|
+
* @param {Element} sibling - The sibling element to evaluate
|
|
4
|
+
* @param {Element} el - The current element being walked up from
|
|
5
|
+
* @param {Element|null} containerEl - The container to preserve (not make inert)
|
|
6
|
+
* @param {Element} boundaryEl - The outermost boundary for the traversal
|
|
7
|
+
* @returns {boolean}
|
|
8
|
+
*/
|
|
9
|
+
const shouldMakeInert = (sibling, el, containerEl, boundaryEl) =>
|
|
10
|
+
sibling !== el &&
|
|
11
|
+
!containerEl?.contains(sibling) &&
|
|
12
|
+
sibling.matches(':not([aria-hidden]):not([data-fm-inert])') &&
|
|
13
|
+
boundaryEl.contains(sibling)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Toggles inert state on elements outside a container for fullscreen/modal behaviour.
|
|
17
|
+
*
|
|
18
|
+
* When entering fullscreen, walks up from `containerEl` to `boundaryEl`, marking all
|
|
19
|
+
* sibling branches as `aria-hidden` and flagging them with `data-fm-inert` so they
|
|
20
|
+
* can be restored later. When exiting fullscreen, restores all previously inerted elements.
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} options
|
|
23
|
+
* @param {Element|null} options.containerEl - The element to keep interactive
|
|
24
|
+
* @param {boolean} options.isFullscreen - Whether fullscreen mode is active
|
|
25
|
+
* @param {Element} [options.boundaryEl=document.body] - The outermost ancestor to traverse up to
|
|
26
|
+
*/
|
|
1
27
|
export function toggleInertElements ({ containerEl, isFullscreen, boundaryEl = document.body }) {
|
|
28
|
+
// Restore any previously inerted elements
|
|
2
29
|
let inertElements = Array.from(boundaryEl.querySelectorAll('[data-fm-inert]'))
|
|
3
30
|
|
|
4
31
|
if (containerEl) {
|
|
5
|
-
inertElements = inertElements.filter(
|
|
32
|
+
inertElements = inertElements.filter(inertEl => !containerEl.contains(inertEl))
|
|
6
33
|
}
|
|
7
34
|
|
|
8
|
-
inertElements.forEach(
|
|
9
|
-
|
|
10
|
-
|
|
35
|
+
inertElements.forEach(inertEl => {
|
|
36
|
+
inertEl.removeAttribute('aria-hidden')
|
|
37
|
+
delete inertEl.dataset.fmInert
|
|
11
38
|
})
|
|
12
39
|
|
|
13
|
-
if (!isFullscreen)
|
|
40
|
+
if (!isFullscreen) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
14
43
|
|
|
44
|
+
// Entering fullscreen: blur active element and inert all sibling branches
|
|
15
45
|
document.activeElement?.blur()
|
|
16
46
|
|
|
17
47
|
let el = containerEl
|
|
18
48
|
while (el?.parentNode && el !== boundaryEl && el !== document.body) {
|
|
19
49
|
const parent = el.parentNode
|
|
20
50
|
for (const sibling of parent.children) {
|
|
21
|
-
if (
|
|
22
|
-
sibling !== el &&
|
|
23
|
-
(!containerEl || !containerEl.contains(sibling)) &&
|
|
24
|
-
sibling.matches(':not([aria-hidden]):not([data-fm-inert])') &&
|
|
25
|
-
boundaryEl.contains(sibling)
|
|
26
|
-
) {
|
|
51
|
+
if (shouldMakeInert(sibling, el, containerEl, boundaryEl)) {
|
|
27
52
|
sibling.setAttribute('aria-hidden', 'true')
|
|
28
|
-
sibling.
|
|
53
|
+
sibling.dataset.fmInert = ''
|
|
29
54
|
}
|
|
30
55
|
}
|
|
31
56
|
el = parent
|
package/webpack.dev.mjs
CHANGED
|
@@ -66,9 +66,8 @@ export default {
|
|
|
66
66
|
OS_NAMES_URL: JSON.stringify(process.env.OS_NAMES_URL),
|
|
67
67
|
OS_NEAREST_URL: JSON.stringify(process.env.OS_NEAREST_URL),
|
|
68
68
|
// Data services
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
PARCEL_SERVICE_URL: JSON.stringify(process.env.PARCEL_SERVICE_URL)
|
|
69
|
+
FARMING_TILE_SERVICE_URL: JSON.stringify(process.env.FARMING_TILE_SERVICE_URL),
|
|
70
|
+
FARMING_API_URL: JSON.stringify(process.env.FARMING_API_URL)
|
|
72
71
|
}
|
|
73
72
|
})
|
|
74
73
|
],
|
|
@@ -77,7 +76,7 @@ export default {
|
|
|
77
76
|
{
|
|
78
77
|
test: /\.jsx?$/,
|
|
79
78
|
loader: 'babel-loader',
|
|
80
|
-
exclude: /node_modules
|
|
79
|
+
exclude: /node_modules/
|
|
81
80
|
},{
|
|
82
81
|
test: /\.s[ac]ss$/i,
|
|
83
82
|
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { polygon, multiPolygon, lineString, multiLineString, point, multiPoint } from '@turf/helpers'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Convert a GeoJSON Feature or geometry-like object into a Turf geometry.
|
|
5
|
-
*
|
|
6
|
-
* @param {Object} featureOrGeom - Either a Feature with a `.geometry` property or a raw GeoJSON geometry object.
|
|
7
|
-
* @returns {Object} Turf geometry (Polygon, LineString, Point, etc.)
|
|
8
|
-
*
|
|
9
|
-
* @throws Will throw if the geometry type is not supported.
|
|
10
|
-
*/
|
|
11
|
-
export function toTurfGeometry(featureOrGeom) {
|
|
12
|
-
const geom = featureOrGeom.geometry || featureOrGeom
|
|
13
|
-
|
|
14
|
-
switch (geom.type) {
|
|
15
|
-
case 'Polygon':
|
|
16
|
-
return polygon(geom.coordinates);
|
|
17
|
-
case 'MultiPolygon':
|
|
18
|
-
return multiPolygon(geom.coordinates);
|
|
19
|
-
case 'LineString':
|
|
20
|
-
return lineString(geom.coordinates);
|
|
21
|
-
case 'MultiLineString':
|
|
22
|
-
return multiLineString(geom.coordinates);
|
|
23
|
-
case 'Point':
|
|
24
|
-
return point(geom.coordinates);
|
|
25
|
-
case 'MultiPoint':
|
|
26
|
-
return multiPoint(geom.coordinates);
|
|
27
|
-
default:
|
|
28
|
-
throw new Error(`Unsupported geometry type: ${geom.type}`)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { toTurfGeometry } from './turfHelpers.js'
|
|
2
|
-
import { polygon, multiPolygon, lineString, multiLineString, point, multiPoint } from '@turf/helpers'
|
|
3
|
-
|
|
4
|
-
describe('toTurfGeometry', () => {
|
|
5
|
-
const geomTypes = [
|
|
6
|
-
{ type: 'Polygon', coords: [[[0,0],[1,0],[1,1],[0,0]]], fn: polygon },
|
|
7
|
-
{ type: 'MultiPolygon', coords: [[[[0,0],[1,0],[1,1],[0,0]]]], fn: multiPolygon },
|
|
8
|
-
{ type: 'LineString', coords: [[0,0],[1,1]], fn: lineString },
|
|
9
|
-
{ type: 'MultiLineString', coords: [[[0,0],[1,1]]], fn: multiLineString },
|
|
10
|
-
{ type: 'Point', coords: [0,0], fn: point },
|
|
11
|
-
{ type: 'MultiPoint', coords: [[0,0],[1,1]], fn: multiPoint }
|
|
12
|
-
]
|
|
13
|
-
|
|
14
|
-
geomTypes.forEach(({ type, coords, fn }) => {
|
|
15
|
-
it(`converts ${type} Feature`, () => {
|
|
16
|
-
const feature = { type: 'Feature', geometry: { type, coordinates: coords } }
|
|
17
|
-
expect(toTurfGeometry(feature)).toEqual(fn(coords))
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it(`converts raw ${type} geometry`, () => {
|
|
21
|
-
const geom = { type, coordinates: coords }
|
|
22
|
-
expect(toTurfGeometry(geom)).toEqual(fn(coords))
|
|
23
|
-
})
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('throws on unsupported geometry', () => {
|
|
27
|
-
expect(() => toTurfGeometry({ type: 'Feature', geometry: { type: 'Circle', coordinates: [] } }))
|
|
28
|
-
.toThrow('Unsupported geometry type: Circle')
|
|
29
|
-
})
|
|
30
|
-
})
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// src/plugins/search/utils/fetch.js
|
|
2
|
-
export async function fetchDataset(url, options = {}, transformRequest) {
|
|
3
|
-
let request = new Request(url, options)
|
|
4
|
-
|
|
5
|
-
if (transformRequest) {
|
|
6
|
-
try {
|
|
7
|
-
request = await transformRequest(request)
|
|
8
|
-
} catch (err) {
|
|
9
|
-
console.error('Error transforming request:', err)
|
|
10
|
-
return null
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const response = await fetch(request)
|
|
15
|
-
|
|
16
|
-
if (!response.ok) {
|
|
17
|
-
console.error('Fetch error', response.status)
|
|
18
|
-
return null
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const json = await response.json()
|
|
22
|
-
return json
|
|
23
|
-
}
|