@defra/interactive-map 0.0.4-alpha → 0.0.5-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 +95 -0
- package/plugins/interact/src/utils/spatial.test.js +93 -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 +3 -0
- package/src/App/store/AppProvider.test.jsx +40 -0
- package/src/App/store/PluginProvider.jsx +7 -5
- package/src/App/store/appActionsMap.js +25 -1
- package/src/App/store/appActionsMap.test.js +21 -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/_dimensions.scss +6 -2
- 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 +4 -4
- 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
|
@@ -5,22 +5,101 @@ 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
|
-
//
|
|
8
|
+
// Vertical offset to align the marker tip with the coordinate point
|
|
9
|
+
const MARKER_ANCHOR_OFFSET_Y = 19
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Projects geographic coordinates to screen pixel position, scaled for the
|
|
13
|
+
* current map size and offset so the marker tip aligns with the point.
|
|
14
|
+
*
|
|
15
|
+
* @param {Array<number>} coords - [lng, lat] geographic coordinates
|
|
16
|
+
* @param {Object} mapProvider - Map provider instance with `mapToScreen` method
|
|
17
|
+
* @param {string} mapSize - Current map size key (e.g. 'small', 'medium', 'large')
|
|
18
|
+
* @param {boolean} isMapReady - Whether the map has finished initialising
|
|
19
|
+
* @returns {{ x: number, y: number }} Screen position in pixels
|
|
20
|
+
*/
|
|
9
21
|
export const projectCoords = (coords, mapProvider, mapSize, isMapReady) => {
|
|
10
22
|
if (!mapProvider || !isMapReady) {
|
|
11
23
|
return { x: 0, y: 0 }
|
|
12
24
|
}
|
|
13
25
|
const { x, y } = mapProvider.mapToScreen(coords)
|
|
14
|
-
return {
|
|
26
|
+
return {
|
|
27
|
+
x: x * scaleFactor[mapSize],
|
|
28
|
+
y: y * scaleFactor[mapSize] - MARKER_ANCHOR_OFFSET_Y
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reprojects and repositions all marker DOM elements to their current screen
|
|
34
|
+
* coordinates. Called on map render and resize events.
|
|
35
|
+
*
|
|
36
|
+
* @param {Array<Object>} items - Marker state items from the store
|
|
37
|
+
* @param {Map<string, HTMLElement>} markerRefs - Map of marker id to DOM element
|
|
38
|
+
* @param {Object} mapProvider - Map provider instance
|
|
39
|
+
* @param {string} mapSize - Current map size key
|
|
40
|
+
* @param {boolean} isMapReady - Whether the map has finished initialising
|
|
41
|
+
*/
|
|
42
|
+
const updateMarkerPositions = (items, markerRefs, mapProvider, mapSize, isMapReady) => {
|
|
43
|
+
items.forEach(marker => {
|
|
44
|
+
const ref = markerRefs.get(marker.id)
|
|
45
|
+
if (!ref || !marker.coords) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { x, y } = projectCoords(marker.coords, mapProvider, mapSize, isMapReady)
|
|
50
|
+
ref.style.transform = `translate(${x}px, ${y}px)`
|
|
51
|
+
ref.style.display = 'block'
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Registers event bus listeners for adding and removing markers via
|
|
57
|
+
* APP_ADD_MARKER and APP_REMOVE_MARKER events.
|
|
58
|
+
*
|
|
59
|
+
* @param {Object} eventBus - Application event bus
|
|
60
|
+
* @param {Object} markers - Markers API object with `add` and `remove` methods
|
|
61
|
+
*/
|
|
62
|
+
const useMarkerEventListeners = (eventBus, markers) => {
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const handleAddMarker = (payload = {}) => {
|
|
65
|
+
if (!payload?.id || !payload?.coords) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
const { id, coords, options } = payload
|
|
69
|
+
markers.add(id, coords, options)
|
|
70
|
+
}
|
|
71
|
+
eventBus.on(events.APP_ADD_MARKER, handleAddMarker)
|
|
72
|
+
|
|
73
|
+
const handleRemoveMarker = (id) => {
|
|
74
|
+
if (!id) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
markers.remove(id)
|
|
78
|
+
}
|
|
79
|
+
eventBus.on(events.APP_REMOVE_MARKER, handleRemoveMarker)
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
eventBus.off(events.APP_ADD_MARKER, handleAddMarker)
|
|
83
|
+
eventBus.off(events.APP_REMOVE_MARKER, handleRemoveMarker)
|
|
84
|
+
}
|
|
85
|
+
}, [])
|
|
15
86
|
}
|
|
16
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Hook that provides the markers API and ref callback for positioning marker
|
|
90
|
+
* elements on the map. Attaches `add`, `remove`, and `getMarker` methods to the
|
|
91
|
+
* markers store object and keeps marker positions in sync with map render and
|
|
92
|
+
* resize events.
|
|
93
|
+
*
|
|
94
|
+
* @returns {{ markers: Object, markerRef: Function }}
|
|
95
|
+
*/
|
|
17
96
|
export const useMarkers = () => {
|
|
18
97
|
const { mapProvider } = useConfig()
|
|
19
98
|
const { eventBus } = useService()
|
|
20
99
|
const { markers, dispatch, mapSize, isMapReady } = useMap()
|
|
21
100
|
const markerRefs = useRef(new Map())
|
|
22
101
|
|
|
23
|
-
//
|
|
102
|
+
// Attach add, remove, and getMarker methods to the markers store object
|
|
24
103
|
useEffect(() => {
|
|
25
104
|
if (!mapProvider) {
|
|
26
105
|
return
|
|
@@ -42,11 +121,11 @@ export const useMarkers = () => {
|
|
|
42
121
|
}
|
|
43
122
|
}, [mapProvider, markers, dispatch, mapSize])
|
|
44
123
|
|
|
45
|
-
//
|
|
124
|
+
// Ref callback: stores marker DOM refs and subscribes to MAP_RENDER for repositioning
|
|
46
125
|
const markerRef = useCallback((id) => (el) => {
|
|
47
126
|
if (!el) {
|
|
48
127
|
markerRefs.current.delete(id)
|
|
49
|
-
return
|
|
128
|
+
return undefined
|
|
50
129
|
}
|
|
51
130
|
markerRefs.current.set(id, el)
|
|
52
131
|
|
|
@@ -54,17 +133,7 @@ export const useMarkers = () => {
|
|
|
54
133
|
if (!isMapReady || !mapProvider) {
|
|
55
134
|
return
|
|
56
135
|
}
|
|
57
|
-
|
|
58
|
-
markers.items.forEach(marker => {
|
|
59
|
-
const ref = markerRefs.current.get(marker.id)
|
|
60
|
-
if (!ref || !marker.coords) {
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const { x, y } = projectCoords(marker.coords, mapProvider, mapSize, isMapReady)
|
|
65
|
-
ref.style.transform = `translate(${x}px, ${y}px)`
|
|
66
|
-
ref.style.display = 'block'
|
|
67
|
-
})
|
|
136
|
+
updateMarkerPositions(markers.items, markerRefs.current, mapProvider, mapSize, isMapReady)
|
|
68
137
|
}
|
|
69
138
|
eventBus.on(events.MAP_RENDER, updateMarkers)
|
|
70
139
|
|
|
@@ -73,47 +142,16 @@ export const useMarkers = () => {
|
|
|
73
142
|
}
|
|
74
143
|
}, [markers, mapProvider, isMapReady, mapSize])
|
|
75
144
|
|
|
76
|
-
//
|
|
145
|
+
// Reproject all markers when the map size changes
|
|
77
146
|
useEffect(() => {
|
|
78
147
|
if (!isMapReady || !mapProvider) {
|
|
79
148
|
return
|
|
80
149
|
}
|
|
81
150
|
|
|
82
|
-
markers.items.
|
|
83
|
-
const ref = markerRefs.current.get(marker.id)
|
|
84
|
-
if (!ref || !marker.coords) {
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const { x, y } = projectCoords(marker.coords, mapProvider, mapSize, isMapReady)
|
|
89
|
-
ref.style.transform = `translate(${x}px, ${y}px)`
|
|
90
|
-
})
|
|
151
|
+
updateMarkerPositions(markers.items, markerRefs.current, mapProvider, mapSize, isMapReady)
|
|
91
152
|
}, [mapSize, markers.items, mapProvider, isMapReady])
|
|
92
153
|
|
|
93
|
-
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
const handleAddMarker = (payload = {}) => {
|
|
96
|
-
if (!payload || !payload.id || !payload.coords) {
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
const { id, coords, options } = payload
|
|
100
|
-
markers.add(id, coords, options)
|
|
101
|
-
}
|
|
102
|
-
eventBus.on(events.APP_ADD_MARKER, handleAddMarker)
|
|
103
|
-
|
|
104
|
-
const handleRemoveMarker = (id) => {
|
|
105
|
-
if (!id) {
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
markers.remove(id)
|
|
109
|
-
}
|
|
110
|
-
eventBus.on(events.APP_REMOVE_MARKER, handleRemoveMarker)
|
|
111
|
-
|
|
112
|
-
return () => {
|
|
113
|
-
eventBus.off(events.APP_ADD_MARKER, handleAddMarker)
|
|
114
|
-
eventBus.off(events.APP_REMOVE_MARKER, handleRemoveMarker)
|
|
115
|
-
}
|
|
116
|
-
}, [])
|
|
154
|
+
useMarkerEventListeners(eventBus, markers)
|
|
117
155
|
|
|
118
156
|
return { markers, markerRef }
|
|
119
157
|
}
|
|
@@ -115,4 +115,52 @@ describe('useMediaQueryDispatch', () => {
|
|
|
115
115
|
payload: { preferredColorScheme: 'light', prefersReducedMotion: true }
|
|
116
116
|
})
|
|
117
117
|
})
|
|
118
|
+
|
|
119
|
+
it('sets up hybrid media query and dispatches on change', () => {
|
|
120
|
+
const options = {
|
|
121
|
+
behaviour: 'hybrid',
|
|
122
|
+
hybridWidth: 500,
|
|
123
|
+
maxMobileWidth: 768,
|
|
124
|
+
appColorScheme: 'light',
|
|
125
|
+
autoColorScheme: true
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
renderHook(() => useMediaQueryDispatch(dispatch, options))
|
|
129
|
+
|
|
130
|
+
// Verify matchMedia was called with the hybrid threshold
|
|
131
|
+
expect(window.matchMedia).toHaveBeenCalledWith('(max-width: 500px)')
|
|
132
|
+
|
|
133
|
+
// Verify addEventListener was called for the 2 standard queries + 1 hybrid query
|
|
134
|
+
expect(mockMediaQuery.addEventListener).toHaveBeenCalledTimes(3)
|
|
135
|
+
|
|
136
|
+
// Find the hybrid change handler (the last one registered)
|
|
137
|
+
const hybridHandler = mockMediaQuery.addEventListener.mock.calls[2][1]
|
|
138
|
+
|
|
139
|
+
// Simulate a media query match event
|
|
140
|
+
act(() => {
|
|
141
|
+
hybridHandler({ matches: true })
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
expect(dispatch).toHaveBeenCalledWith({
|
|
145
|
+
type: 'SET_HYBRID_FULLSCREEN',
|
|
146
|
+
payload: true
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('falls back to maxMobileWidth for hybrid threshold when hybridWidth is missing', () => {
|
|
151
|
+
const options = {
|
|
152
|
+
behaviour: 'hybrid',
|
|
153
|
+
// hybridWidth is undefined here
|
|
154
|
+
maxMobileWidth: 800,
|
|
155
|
+
appColorScheme: 'light',
|
|
156
|
+
autoColorScheme: true
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
renderHook(() => useMediaQueryDispatch(dispatch, options))
|
|
160
|
+
|
|
161
|
+
// This covers the right side of the ?? operator
|
|
162
|
+
expect(window.matchMedia).toHaveBeenCalledWith('(max-width: 800px)')
|
|
163
|
+
|
|
164
|
+
expect(mockMediaQuery.addEventListener).toHaveBeenCalledTimes(3)
|
|
165
|
+
})
|
|
118
166
|
})
|
|
@@ -3,18 +3,10 @@ import { useResizeObserver } from './useResizeObserver.js'
|
|
|
3
3
|
import { constrainKeyboardFocus } from '../../utils/constrainKeyboardFocus.js'
|
|
4
4
|
import { toggleInertElements } from '../../utils/toggleInertElements.js'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
mainRef,
|
|
8
|
-
panelRef,
|
|
9
|
-
isModal,
|
|
10
|
-
rootEl,
|
|
11
|
-
buttonContainerEl,
|
|
12
|
-
handleClose
|
|
13
|
-
}) {
|
|
14
|
-
// === Escape and Tab key handling === //
|
|
6
|
+
const useModalKeyHandler = (panelRef, isModal, handleClose) => {
|
|
15
7
|
useEffect(() => {
|
|
16
8
|
if (!isModal) {
|
|
17
|
-
return
|
|
9
|
+
return undefined
|
|
18
10
|
}
|
|
19
11
|
|
|
20
12
|
const handleKeyDown = (e) => {
|
|
@@ -36,6 +28,47 @@ export function useModalPanelBehaviour ({
|
|
|
36
28
|
current?.removeEventListener('keydown', handleKeyDown)
|
|
37
29
|
}
|
|
38
30
|
}, [isModal, panelRef, handleClose])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const useFocusRedirect = (isModal, panelRef, rootEl) => {
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!isModal) {
|
|
36
|
+
return undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const handleFocusIn = (e) => {
|
|
40
|
+
const focusedEl = e.target
|
|
41
|
+
const panelEl = panelRef.current
|
|
42
|
+
|
|
43
|
+
if (!focusedEl || !panelEl || !rootEl) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const isInsideApp = rootEl.contains(focusedEl)
|
|
48
|
+
const isInsidePanel = panelEl.contains(focusedEl)
|
|
49
|
+
|
|
50
|
+
if (isInsideApp && !isInsidePanel) {
|
|
51
|
+
panelEl.focus()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
document.addEventListener('focusin', handleFocusIn)
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
document.removeEventListener('focusin', handleFocusIn)
|
|
59
|
+
}
|
|
60
|
+
}, [isModal, panelRef, rootEl])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useModalPanelBehaviour ({
|
|
64
|
+
mainRef,
|
|
65
|
+
panelRef,
|
|
66
|
+
isModal,
|
|
67
|
+
rootEl,
|
|
68
|
+
buttonContainerEl,
|
|
69
|
+
handleClose
|
|
70
|
+
}) {
|
|
71
|
+
useModalKeyHandler(panelRef, isModal, handleClose)
|
|
39
72
|
|
|
40
73
|
// === Set absolute offset positions and recalculate on mainRef resize === //
|
|
41
74
|
const root = document.documentElement
|
|
@@ -55,7 +88,7 @@ export function useModalPanelBehaviour ({
|
|
|
55
88
|
// === Click on modal backdrop to close === //
|
|
56
89
|
useEffect(() => {
|
|
57
90
|
if (!isModal) {
|
|
58
|
-
return
|
|
91
|
+
return undefined
|
|
59
92
|
}
|
|
60
93
|
|
|
61
94
|
const handleClick = (e) => {
|
|
@@ -74,7 +107,7 @@ export function useModalPanelBehaviour ({
|
|
|
74
107
|
// === Inert everything outside the panel but within the app === //
|
|
75
108
|
useEffect(() => {
|
|
76
109
|
if (!isModal || !panelRef.current || !rootEl) {
|
|
77
|
-
return
|
|
110
|
+
return undefined
|
|
78
111
|
}
|
|
79
112
|
|
|
80
113
|
toggleInertElements({
|
|
@@ -92,32 +125,5 @@ export function useModalPanelBehaviour ({
|
|
|
92
125
|
}
|
|
93
126
|
}, [isModal, panelRef, rootEl])
|
|
94
127
|
|
|
95
|
-
|
|
96
|
-
useEffect(() => {
|
|
97
|
-
if (!isModal) {
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const handleFocusIn = (e) => {
|
|
102
|
-
const focusedEl = e.target
|
|
103
|
-
const panelEl = panelRef.current
|
|
104
|
-
|
|
105
|
-
if (!focusedEl || !panelEl || !rootEl) {
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const isInsideApp = rootEl.contains(focusedEl)
|
|
110
|
-
const isInsidePanel = panelEl.contains(focusedEl)
|
|
111
|
-
|
|
112
|
-
if (isInsideApp && !isInsidePanel) {
|
|
113
|
-
panelEl.focus()
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
document.addEventListener('focusin', handleFocusIn)
|
|
118
|
-
|
|
119
|
-
return () => {
|
|
120
|
-
document.removeEventListener('focusin', handleFocusIn)
|
|
121
|
-
}
|
|
122
|
-
}, [isModal, panelRef, rootEl])
|
|
128
|
+
useFocusRedirect(isModal, panelRef, rootEl)
|
|
123
129
|
}
|
|
@@ -9,10 +9,10 @@ export function useResizeObserver (targetRefs, callback) {
|
|
|
9
9
|
const elements = refs.map(r => r?.current).filter(Boolean)
|
|
10
10
|
|
|
11
11
|
if (!elements.length || !callback) {
|
|
12
|
-
return
|
|
12
|
+
return undefined
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const observer = new ResizeObserver(entries => {
|
|
15
|
+
const observer = new window.ResizeObserver(entries => {
|
|
16
16
|
for (const entry of entries) {
|
|
17
17
|
const { width, height } = entry.contentRect
|
|
18
18
|
const prev = prevSizes.current.get(entry.target) || {}
|
package/src/App/initialiseApp.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRoot } from 'react-dom/client'
|
|
2
2
|
import { EVENTS as events } from '../config/events.js'
|
|
3
|
-
import {
|
|
3
|
+
import { defaultAppConfig } from '../config/appConfig.js'
|
|
4
4
|
import { createButtonRegistry } from './registry/buttonRegistry.js'
|
|
5
5
|
import { createPanelRegistry } from './registry/panelRegistry.js'
|
|
6
6
|
import { createControlRegistry } from './registry/controlRegistry.js'
|
|
@@ -13,6 +13,47 @@ const rootMap = new WeakMap()
|
|
|
13
13
|
const mapProviderMap = new WeakMap()
|
|
14
14
|
const registryMap = new WeakMap()
|
|
15
15
|
|
|
16
|
+
const getOrCreateRegistries = (rootElement) => {
|
|
17
|
+
let registries = registryMap.get(rootElement)
|
|
18
|
+
if (!registries) {
|
|
19
|
+
const buttonRegistry = createButtonRegistry()
|
|
20
|
+
const panelRegistry = createPanelRegistry()
|
|
21
|
+
const controlRegistry = createControlRegistry()
|
|
22
|
+
const pluginRegistry = createPluginRegistry({
|
|
23
|
+
registerButton: buttonRegistry.registerButton,
|
|
24
|
+
registerPanel: panelRegistry.registerPanel,
|
|
25
|
+
registerControl: controlRegistry.registerControl
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
registries = { buttonRegistry, panelRegistry, controlRegistry, pluginRegistry }
|
|
29
|
+
registryMap.set(rootElement, registries)
|
|
30
|
+
}
|
|
31
|
+
return registries
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const loadPlugins = async (plugins, registerPlugin) => {
|
|
35
|
+
for (const plugin of plugins) {
|
|
36
|
+
if (typeof plugin.load === 'function') {
|
|
37
|
+
const module = await plugin.load()
|
|
38
|
+
const { id: pluginId, load, manifest: overrideManifest, ...config } = plugin
|
|
39
|
+
const { InitComponent, api, reducer, ...baseManifest } = module
|
|
40
|
+
|
|
41
|
+
// Merge runtime overrides with module manifest
|
|
42
|
+
const manifest = mergeManifests(baseManifest, overrideManifest)
|
|
43
|
+
|
|
44
|
+
registerPlugin({
|
|
45
|
+
id: pluginId,
|
|
46
|
+
InitComponent,
|
|
47
|
+
api,
|
|
48
|
+
reducer,
|
|
49
|
+
config,
|
|
50
|
+
manifest,
|
|
51
|
+
_originalPlugin: plugin
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
16
57
|
export async function initialiseApp (rootElement, {
|
|
17
58
|
MapProvider: MapProviderClass,
|
|
18
59
|
mapProviderConfig,
|
|
@@ -35,22 +76,7 @@ export async function initialiseApp (rootElement, {
|
|
|
35
76
|
}
|
|
36
77
|
|
|
37
78
|
// Reuse or create registries (persist across app open/close cycles)
|
|
38
|
-
|
|
39
|
-
if (!registries) {
|
|
40
|
-
const buttonRegistry = createButtonRegistry()
|
|
41
|
-
const panelRegistry = createPanelRegistry()
|
|
42
|
-
const controlRegistry = createControlRegistry()
|
|
43
|
-
const pluginRegistry = createPluginRegistry({
|
|
44
|
-
registerButton: buttonRegistry.registerButton,
|
|
45
|
-
registerPanel: panelRegistry.registerPanel,
|
|
46
|
-
registerControl: controlRegistry.registerControl
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
registries = { buttonRegistry, panelRegistry, controlRegistry, pluginRegistry }
|
|
50
|
-
registryMap.set(rootElement, registries)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const { buttonRegistry, panelRegistry, controlRegistry, pluginRegistry } = registries
|
|
79
|
+
const { buttonRegistry, panelRegistry, controlRegistry, pluginRegistry } = getOrCreateRegistries(rootElement)
|
|
54
80
|
const { registerPlugin } = pluginRegistry
|
|
55
81
|
|
|
56
82
|
// Clear previous plugins (but keep runtime additions)
|
|
@@ -59,7 +85,7 @@ export async function initialiseApp (rootElement, {
|
|
|
59
85
|
// Register default appConfig as a plugin
|
|
60
86
|
registerPlugin({
|
|
61
87
|
id: 'appConfig',
|
|
62
|
-
manifest:
|
|
88
|
+
manifest: defaultAppConfig
|
|
63
89
|
})
|
|
64
90
|
|
|
65
91
|
// Create root if not already present
|
|
@@ -87,26 +113,7 @@ export async function initialiseApp (rootElement, {
|
|
|
87
113
|
}
|
|
88
114
|
|
|
89
115
|
// Load plugins
|
|
90
|
-
|
|
91
|
-
if (typeof plugin.load === 'function') {
|
|
92
|
-
const module = await plugin.load()
|
|
93
|
-
const { id: pluginId, load, manifest: overrideManifest, ...config } = plugin
|
|
94
|
-
const { InitComponent, api, reducer, ...baseManifest } = module
|
|
95
|
-
|
|
96
|
-
// Merge runtime overrides with module manifest
|
|
97
|
-
const manifest = mergeManifests(baseManifest, overrideManifest)
|
|
98
|
-
|
|
99
|
-
registerPlugin({
|
|
100
|
-
id: pluginId,
|
|
101
|
-
InitComponent,
|
|
102
|
-
api,
|
|
103
|
-
reducer,
|
|
104
|
-
config,
|
|
105
|
-
manifest,
|
|
106
|
-
_originalPlugin: plugin
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
}
|
|
116
|
+
await loadPlugins(plugins, registerPlugin)
|
|
110
117
|
|
|
111
118
|
root.render(<App
|
|
112
119
|
{...restProps}
|
|
@@ -9,6 +9,7 @@ import { Logo } from '../components/Logo/Logo'
|
|
|
9
9
|
import { Attributions } from '../components/Attributions/Attributions'
|
|
10
10
|
import { layoutSlots } from '../renderer/slots'
|
|
11
11
|
import { SlotRenderer } from '../renderer/SlotRenderer'
|
|
12
|
+
import { HtmlElementHost } from '../renderer/HtmlElementHost'
|
|
12
13
|
|
|
13
14
|
// eslint-disable-next-line camelcase, react/jsx-pascal-case
|
|
14
15
|
// sonarjs/disable-next-line function-name
|
|
@@ -36,7 +37,7 @@ export const Layout = () => {
|
|
|
36
37
|
ref={layoutRefs.appContainerRef}
|
|
37
38
|
>
|
|
38
39
|
<Viewport keyboardHintPortalRef={layoutRefs.topRef} />
|
|
39
|
-
<div className={`im-o-app__overlay${
|
|
40
|
+
<div className={`im-o-app__overlay${isLayoutReady ? '' : ' im-o-app__overlay--not-ready'}`}>
|
|
40
41
|
<div className='im-o-app__side' ref={layoutRefs.sideRef}>
|
|
41
42
|
<SlotRenderer slot={layoutSlots.SIDE} />
|
|
42
43
|
</div>
|
|
@@ -73,7 +74,7 @@ export const Layout = () => {
|
|
|
73
74
|
<SlotRenderer slot={layoutSlots.RIGHT_BOTTOM} />
|
|
74
75
|
</div>
|
|
75
76
|
</div>
|
|
76
|
-
<div className='im-o-app__middle'>
|
|
77
|
+
<div className='im-o-app__middle' ref={layoutRefs.middleRef}>
|
|
77
78
|
<SlotRenderer slot={layoutSlots.MIDDLE} />
|
|
78
79
|
</div>
|
|
79
80
|
<div className='im-o-app__footer' ref={layoutRefs.footerRef}>
|
|
@@ -87,7 +88,7 @@ export const Layout = () => {
|
|
|
87
88
|
</div>
|
|
88
89
|
</div>
|
|
89
90
|
</div>
|
|
90
|
-
<div className='im-o-app__bottom'>
|
|
91
|
+
<div className='im-o-app__bottom' ref={layoutRefs.bottomRef}>
|
|
91
92
|
<SlotRenderer slot={layoutSlots.BOTTOM} />
|
|
92
93
|
</div>
|
|
93
94
|
<div className='im-o-app__actions' ref={layoutRefs.actionsRef}>
|
|
@@ -95,7 +96,8 @@ export const Layout = () => {
|
|
|
95
96
|
</div>
|
|
96
97
|
</div>
|
|
97
98
|
</div>
|
|
98
|
-
<
|
|
99
|
+
<HtmlElementHost />
|
|
100
|
+
<div className='im-o-app__modal' ref={layoutRefs.modalRef}>
|
|
99
101
|
<SlotRenderer slot={layoutSlots.MODAL} />
|
|
100
102
|
<div className='im-o-app__modal-backdrop' />
|
|
101
103
|
</div>
|
|
@@ -268,7 +268,10 @@
|
|
|
268
268
|
// Panel overrides
|
|
269
269
|
.im-c-panel {
|
|
270
270
|
position: absolute;
|
|
271
|
-
|
|
271
|
+
top: 0;
|
|
272
|
+
right: 0;
|
|
273
|
+
bottom: auto;
|
|
274
|
+
left: 0;
|
|
272
275
|
z-index: 1000;
|
|
273
276
|
}
|
|
274
277
|
|
|
@@ -318,15 +321,22 @@
|
|
|
318
321
|
}
|
|
319
322
|
|
|
320
323
|
[class*="im-c-panel--"][class*="-button"] { // Adjacent to button
|
|
321
|
-
|
|
324
|
+
top: var(--modal-inset);
|
|
325
|
+
right: var(--modal-inset);
|
|
326
|
+
bottom: var(--modal-inset);
|
|
327
|
+
left: var(--modal-inset);
|
|
322
328
|
}
|
|
323
329
|
}
|
|
324
330
|
|
|
325
|
-
// Mobile and tablet
|
|
331
|
+
// Mobile and tablet
|
|
326
332
|
.im-o-app--mobile, .im-o-app--tablet {
|
|
327
333
|
.im-o-app__modal .im-c-panel--banner {
|
|
328
334
|
margin: var(--primary-gap);
|
|
329
335
|
}
|
|
336
|
+
|
|
337
|
+
.im-o-app__banner {
|
|
338
|
+
z-index: 9;
|
|
339
|
+
}
|
|
330
340
|
}
|
|
331
341
|
|
|
332
342
|
// Desktop modal banner margin
|
|
@@ -347,7 +357,6 @@
|
|
|
347
357
|
.im-o-app__right,
|
|
348
358
|
.im-o-app__right-bottom {
|
|
349
359
|
opacity: 0;
|
|
350
|
-
// visibility: hidden; Breaks focus order in Preact
|
|
351
360
|
}
|
|
352
361
|
}
|
|
353
362
|
|
|
@@ -357,13 +366,6 @@
|
|
|
357
366
|
opacity: 0;
|
|
358
367
|
}
|
|
359
368
|
|
|
360
|
-
// Banner on mobile and tablet
|
|
361
|
-
.im-o-app--mobile, .im-o-app--tablet {
|
|
362
|
-
.im-o-app__banner {
|
|
363
|
-
z-index: 9;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
369
|
// Banner on desktop
|
|
368
370
|
.im-o-app--desktop .im-c-panel--banner {
|
|
369
371
|
margin-left: auto;
|
|
@@ -404,12 +406,6 @@
|
|
|
404
406
|
clip-path: inset(-20px 0 0 0);
|
|
405
407
|
}
|
|
406
408
|
|
|
407
|
-
// Bottom inset on large devices
|
|
408
|
-
// .im-o-app--tablet .im-o-app__bottom,
|
|
409
|
-
// .im-o-app--desktop .im-o-app__bottom {
|
|
410
|
-
// margin-top: var(--primary-gap);
|
|
411
|
-
// }
|
|
412
|
-
|
|
413
409
|
// 4. State styles
|
|
414
410
|
|
|
415
411
|
// 5. Responsive tweaks
|
|
@@ -21,8 +21,18 @@ export const setProviderSupportedShortcuts = (ids = []) => {
|
|
|
21
21
|
providerSupportedIds = new Set(ids)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export const getKeyboardShortcuts = () => {
|
|
25
|
-
const filteredCore = coreShortcuts.filter(s =>
|
|
24
|
+
export const getKeyboardShortcuts = (appConfig = {}) => {
|
|
25
|
+
const filteredCore = coreShortcuts.filter(s => {
|
|
26
|
+
// Must be supported by provider
|
|
27
|
+
if (!providerSupportedIds.has(s.id)) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
// Check requiredConfig - all specified config values must be truthy
|
|
31
|
+
if (s.requiredConfig) {
|
|
32
|
+
return s.requiredConfig.every(key => appConfig[key])
|
|
33
|
+
}
|
|
34
|
+
return true
|
|
35
|
+
})
|
|
26
36
|
|
|
27
37
|
return [
|
|
28
38
|
...filteredCore, // supported core shortcuts
|
|
@@ -70,4 +70,32 @@ describe('keyboardShortcutRegistry', () => {
|
|
|
70
70
|
const shortcuts = getKeyboardShortcuts()
|
|
71
71
|
expect(shortcuts).toEqual([])
|
|
72
72
|
})
|
|
73
|
+
|
|
74
|
+
test('getKeyboardShortcuts filters by requiredConfig when appConfig provided', () => {
|
|
75
|
+
jest.resetModules()
|
|
76
|
+
jest.doMock('../controls/keyboardShortcuts.js', () => ({
|
|
77
|
+
coreShortcuts: [
|
|
78
|
+
{ id: 'always', description: 'Always shown' },
|
|
79
|
+
{ id: 'conditional', description: 'Conditional', requiredConfig: ['featureEnabled'] }
|
|
80
|
+
]
|
|
81
|
+
}))
|
|
82
|
+
const module = require('./keyboardShortcutRegistry.js')
|
|
83
|
+
module.setProviderSupportedShortcuts(['always', 'conditional'])
|
|
84
|
+
|
|
85
|
+
// Without config, conditional shortcut is excluded
|
|
86
|
+
expect(module.getKeyboardShortcuts()).toEqual([
|
|
87
|
+
{ id: 'always', description: 'Always shown' }
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
// With config false, conditional shortcut is excluded
|
|
91
|
+
expect(module.getKeyboardShortcuts({ featureEnabled: false })).toEqual([
|
|
92
|
+
{ id: 'always', description: 'Always shown' }
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
// With config true, conditional shortcut is included
|
|
96
|
+
expect(module.getKeyboardShortcuts({ featureEnabled: true })).toEqual([
|
|
97
|
+
{ id: 'always', description: 'Always shown' },
|
|
98
|
+
{ id: 'conditional', description: 'Conditional', requiredConfig: ['featureEnabled'] }
|
|
99
|
+
])
|
|
100
|
+
})
|
|
73
101
|
})
|
|
@@ -30,7 +30,7 @@ export const addPanel = (currentConfig, id, config) => {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export const removePanel = (currentConfig, id) => {
|
|
33
|
-
const { [id]: _, ...rest } = currentConfig
|
|
33
|
+
const { [id]: _, ...rest } = currentConfig // NOSONAR - _ is required to destructure out the key
|
|
34
34
|
return rest
|
|
35
35
|
}
|
|
36
36
|
|