@defra/interactive-map 0.0.43-fmp-experimental → 0.0.44-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/templates/add-polygons.njk +5 -1
- package/assets/templates/add-symbols.njk +5 -1
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/context.md +24 -0
- package/docs/api/keyboard-shortcut-definition.md +102 -0
- package/docs/api.md +18 -8
- package/docs/building-a-plugin.md +1 -0
- package/docs/plugins/draw.md +2 -2
- package/docs/plugins/plugin-manifest.md +9 -0
- package/package.json +4 -2
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/api/newPolygon.js +1 -0
- package/plugins/beta/draw-es/src/events.js +17 -8
- package/plugins/beta/draw-es/src/reducer.js +8 -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-ol/dist/css/index.css +1 -13
- package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
- package/plugins/beta/frame/dist/css/index.css +1 -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 +28 -2
- package/plugins/beta/map-styles/dist/css/index.css +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/scale-bar/dist/css/index.css +1 -8
- package/plugins/datasets/dist/css/5648.index.css +1 -5
- package/plugins/datasets/dist/css/index.css +1 -1
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/src/components/LayersMenu/Layers.module.scss +1 -0
- package/plugins/datasets/src/initialise/DatasetsInit.jsx +2 -2
- package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +6 -5
- package/plugins/datasets/src/registry/dataset.js +8 -12
- package/plugins/datasets/src/registry/dataset.test.js +17 -0
- package/plugins/datasets/src/registry/datasetRegistry.js +4 -5
- package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
- package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/esm/index.js +1 -1
- package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/umd/index.js +1 -1
- package/plugins/draw/src/DrawInit.jsx +41 -26
- package/plugins/draw/src/DrawInit.test.jsx +55 -12
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +113 -114
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +82 -9
- package/plugins/draw/src/adapters/maplibre/drawEvents.js +3 -1
- package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +18 -9
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +9 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +11 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +23 -2
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +53 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +29 -19
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +17 -2
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +35 -29
- package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +24 -6
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +22 -20
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +2 -2
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +3 -3
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +31 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +8 -1
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +38 -30
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +20 -3
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +5 -5
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +2 -2
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +2 -2
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +3 -3
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -7
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +4 -4
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +14 -14
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +12 -32
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +10 -10
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +48 -22
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +74 -6
- package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +2 -2
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +6 -0
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +9 -0
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +15 -2
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +27 -1
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +1 -1
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +40 -17
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +55 -1
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +13 -7
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +7 -15
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +36 -4
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +41 -1
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +35 -25
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +40 -2
- package/plugins/draw/src/adapters/openlayers/edit/nudge.js +2 -2
- package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +2 -2
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +40 -54
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +32 -13
- package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +1 -1
- package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +17 -1
- package/plugins/draw/src/events.js +4 -4
- package/plugins/draw/src/events.test.js +1 -1
- package/plugins/draw/src/hooks/useSpatialList.js +180 -0
- package/plugins/draw/src/hooks/useSpatialList.test.js +339 -0
- package/plugins/draw/src/manifest.js +4 -1
- package/plugins/draw/src/manifest.test.js +19 -7
- package/plugins/draw/src/utils/spatial.js +1 -31
- package/plugins/draw/src/utils/spatial.test.js +1 -24
- 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 +2 -2
- package/plugins/interact/src/InteractInit.test.js +3 -3
- package/plugins/interact/src/events.js +11 -2
- package/plugins/interact/src/events.test.js +32 -0
- package/plugins/interact/src/hooks/useAttachEvents.js +20 -0
- package/plugins/interact/src/hooks/useAttachEvents.test.js +44 -1
- package/plugins/interact/src/hooks/useCrossHairVisibility.js +9 -4
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +7 -7
- package/plugins/interact/src/hooks/useSpatialList.js +251 -0
- package/plugins/interact/src/hooks/{useMapItemList.test.js → useSpatialList.test.js} +216 -70
- package/plugins/interact/src/manifest.js +12 -1
- package/plugins/interact/src/manifest.test.js +20 -0
- package/plugins/interact/src/utils/spatial.js +24 -1
- package/plugins/interact/src/utils/spatial.test.js +26 -1
- package/plugins/map-key/dist/css/index.css +1 -106
- package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -1
- package/plugins/map-key/dist/umd/im-map-key-plugin.js +1 -1
- package/plugins/map-key/dist/umd/index.js +1 -1
- package/plugins/map-key/src/components/Key/Key.jsx +3 -4
- package/plugins/map-key/src/components/Key/Key.module.scss +10 -8
- package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +2 -2
- package/plugins/map-key/src/components/Key/KeyItem.jsx +2 -2
- package/plugins/map-key/src/components/Key/KeySvg.jsx +9 -7
- package/plugins/map-key/src/components/Key/KeySvg.test.jsx +4 -4
- package/plugins/map-key/src/components/Key/MapKey.jsx +5 -3
- package/plugins/map-key/src/manifest.js +1 -1
- package/plugins/map-key/src/utils/groupStyles.js +7 -0
- package/plugins/map-key/src/utils/mergeKeyGroupItems.js +12 -0
- package/plugins/map-key/src/utils/mergeKeyGroupItems.test.js +53 -0
- package/plugins/menu/dist/css/index.css +1 -80
- package/plugins/menu/dist/esm/im-menu-plugin.js +1 -1
- package/plugins/menu/dist/umd/im-menu-plugin.js +1 -1
- package/plugins/menu/dist/umd/index.js +1 -1
- package/plugins/menu/src/initialise/MenuInit.jsx +15 -3
- package/plugins/menu/src/initialise/MenuInit.test.jsx +25 -0
- package/plugins/menu/src/initialise/createMenu.js +3 -1
- package/plugins/menu/src/initialise/createMenu.test.js +20 -5
- package/plugins/menu/src/initialise/updateUrl.js +44 -0
- package/plugins/menu/src/initialise/updateUrl.test.js +91 -0
- package/plugins/menu/src/reducers/menuStateReducer.js +19 -1
- package/plugins/menu/src/reducers/menuStateReducer.test.js +53 -20
- package/plugins/menu/src/reducers/pluginState.js +1 -6
- package/plugins/menu/src/reducers/pluginState.test.js +0 -25
- 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/src/manifest.js +5 -9
- package/plugins/search/src/manifest.test.js +5 -0
- package/providers/beta/esri/dist/css/index.css +1 -34
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/src/openlayersProvider.js +3 -3
- package/providers/beta/openlayers/src/openlayersProvider.test.js +6 -3
- package/providers/beta/openlayers/src/utils/queryFeatures.js +72 -0
- package/providers/beta/openlayers/src/utils/queryFeatures.test.js +122 -1
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/defaults.test.js +26 -0
- package/providers/maplibre/src/utils/labels.js +2 -2
- package/providers/maplibre/src/utils/labels.test.js +5 -5
- package/providers/maplibre/src/utils/spatial.js +0 -57
- package/providers/maplibre/src/utils/spatial.test.js +0 -27
- package/rollup.esm.mjs +2 -1
- package/scripts/publish-package.sh +36 -0
- package/src/App/App.jsx +1 -1
- package/src/App/App.test.jsx +6 -1
- package/src/App/components/CrossHair/CrossHair.jsx +25 -11
- package/src/App/components/CrossHair/CrossHair.module.scss +37 -12
- package/src/App/components/CrossHair/CrossHair.test.jsx +40 -11
- package/src/App/components/Hints/Hints.jsx +3 -3
- package/src/App/components/Hints/Hints.test.jsx +3 -3
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +4 -1
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +17 -0
- package/src/App/components/MapButton/MapButton.jsx +19 -7
- package/src/App/components/MapButton/MapButton.module.scss +1 -2
- package/src/App/components/MapButton/MapButton.test.jsx +42 -4
- package/src/App/components/{MoveControls/MoveControls.jsx → MapControls/MapControls.jsx} +10 -10
- package/src/App/components/{MoveControls/MoveControls.module.scss → MapControls/MapControls.module.scss} +25 -25
- package/src/App/components/{MoveControls/MoveControls.test.jsx → MapControls/MapControls.test.jsx} +48 -48
- package/src/App/components/Markers/Markers.jsx +2 -2
- package/src/App/components/Markers/Markers.test.jsx +3 -3
- package/src/App/components/Markers/SymbolMarker.jsx +2 -3
- package/src/App/components/Markers/SymbolMarker.test.jsx +8 -9
- package/src/App/components/Panel/Panel.jsx +19 -20
- package/src/App/components/Panel/Panel.module.scss +5 -0
- package/src/App/components/Panel/Panel.test.jsx +69 -4
- package/src/App/components/PopupMenu/PopupMenu.jsx +7 -3
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +71 -0
- package/src/App/components/PopupMenu/usePopupMenu.js +10 -9
- package/src/App/components/Viewport/MapStatus.jsx +1 -1
- package/src/App/components/Viewport/SpatialList.jsx +47 -0
- package/src/App/components/Viewport/{Features.module.scss → SpatialList.module.scss} +6 -6
- package/src/App/components/Viewport/SpatialList.test.jsx +285 -0
- package/src/App/components/Viewport/Viewport.jsx +21 -11
- package/src/App/components/Viewport/Viewport.test.jsx +18 -2
- package/src/App/controls/keyboardMappings.js +7 -2
- package/src/App/controls/keyboardMappings.test.js +21 -0
- package/src/App/controls/keyboardShortcuts.js +16 -2
- package/src/App/controls/keyboardShortcuts.test.js +21 -0
- package/src/App/hooks/useCrossHairAPI.js +29 -1
- package/src/App/hooks/useCrossHairAPI.test.js +17 -1
- package/src/App/hooks/useHintsAPI.js +5 -5
- package/src/App/hooks/useInterfaceAPI.js +2 -1
- package/src/App/hooks/useInterfaceAPI.test.js +9 -0
- package/src/App/hooks/useKeyboardShortcuts.js +66 -35
- package/src/App/hooks/useKeyboardShortcuts.test.js +52 -0
- package/src/App/hooks/useModalPanelBehaviour.js +48 -34
- package/src/App/hooks/useModalPanelBehaviour.test.js +59 -0
- package/src/App/hooks/useSpatialListFocus.js +296 -0
- package/src/App/hooks/useSpatialListFocus.test.js +957 -0
- package/src/App/hooks/useSpatialListItems.js +51 -0
- package/src/App/hooks/useSpatialListItems.test.js +217 -0
- package/src/App/initialiseApp.js +6 -3
- package/src/App/initialiseApp.test.js +11 -0
- package/src/App/layout/Layout.jsx +4 -2
- package/src/App/layout/Layout.test.jsx +21 -0
- package/src/App/layout/layout.module.scss +7 -1
- package/src/App/registry/keyboardShortcutRegistry.js +2 -2
- package/src/App/registry/keyboardShortcutRegistry.test.js +16 -0
- package/src/App/registry/pluginRegistry.js +13 -0
- package/src/App/registry/pluginRegistry.test.js +15 -0
- package/src/App/registry/spatialListRegistry.js +104 -0
- package/src/App/registry/spatialListRegistry.test.js +218 -0
- package/src/App/renderer/HtmlElementHost.jsx +12 -32
- package/src/App/renderer/mapButtons.js +4 -0
- package/src/App/renderer/mapButtons.test.js +20 -0
- package/src/App/renderer/mapPanels.js +33 -53
- package/src/App/renderer/mapPanels.test.js +29 -4
- package/src/App/renderer/slotHelpers.js +33 -0
- package/src/App/renderer/slotHelpers.test.js +79 -2
- package/src/App/store/AppProvider.jsx +3 -2
- package/src/App/store/ServiceProvider.jsx +5 -3
- package/src/App/store/ServiceProvider.test.jsx +20 -1
- package/src/InteractiveMap/InteractiveMap.js +13 -2
- package/src/InteractiveMap/InteractiveMap.test.js +33 -0
- package/src/config/appConfig.js +16 -16
- package/src/config/appConfig.test.js +19 -19
- package/src/config/defaults.js +2 -2
- package/src/config/events.js +8 -8
- package/src/scss/main.scss +2 -2
- package/src/test-utils.js +8 -0
- package/src/types.js +51 -9
- package/src/utils/findNearestItemInDirection.js +29 -0
- package/src/utils/findNearestItemInDirection.test.js +67 -0
- package/src/utils/getPanelElementId.js +4 -0
- package/src/utils/getPanelElementId.test.js +11 -0
- package/src/utils/getPanelRole.js +22 -0
- package/src/utils/getPanelRole.test.js +56 -0
- package/src/utils/globalAltShortcuts.js +12 -0
- package/src/utils/globalAltShortcuts.test.js +27 -0
- package/src/utils/spatialNavigate.js +49 -0
- package/src/utils/spatialNavigate.test.js +42 -0
- package/webpack.umd.mjs +6 -1
- package/plugins/interact/src/hooks/useMapItemList.js +0 -187
- package/src/App/components/Viewport/Features.jsx +0 -35
- package/src/App/components/Viewport/Features.test.jsx +0 -124
- package/src/App/hooks/useFeatureFocus.js +0 -199
- package/src/App/hooks/useFeatureFocus.test.js +0 -635
- package/src/App/hooks/useFeatureItems.js +0 -39
- package/src/App/hooks/useFeatureItems.test.js +0 -155
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import { renderHook, act } from '@testing-library/react'
|
|
2
|
-
import {
|
|
2
|
+
import { useSpatialList } from './useSpatialList.js'
|
|
3
|
+
import { createSpatialListRegistry } from '../../../../src/App/registry/spatialListRegistry.js'
|
|
3
4
|
|
|
4
5
|
const MARKER_LABEL = 'Marker One'
|
|
5
6
|
|
|
6
7
|
const MOVE_END = 'map:moveend'
|
|
7
8
|
const DATA_CHANGE = 'map:datachange'
|
|
8
|
-
const SET_FEATURES = 'map:
|
|
9
|
-
const SET_ACTIVE = 'map:
|
|
10
|
-
const CONFIRM = 'map:
|
|
9
|
+
const SET_FEATURES = 'map:setspatiallist'
|
|
10
|
+
const SET_ACTIVE = 'map:setactiveitem'
|
|
11
|
+
const CONFIRM = 'map:selectitem'
|
|
12
|
+
|
|
13
|
+
// Items now carry more than id/label/x/y (isMarker, and for features: featureId/layerId/
|
|
14
|
+
// idProperty/geometry/properties) — see useSpatialList.js's own comment on why. These two
|
|
15
|
+
// helpers assert only the fields each test actually cares about, rather than retyping the full
|
|
16
|
+
// object everywhere a marker/feature item shows up.
|
|
17
|
+
const markerItem = (overrides) => expect.objectContaining({ isMarker: true, ...overrides })
|
|
18
|
+
const featureItem = (overrides) => expect.objectContaining({ isMarker: false, ...overrides })
|
|
11
19
|
|
|
12
20
|
const makeEventBus = () => {
|
|
13
21
|
const listeners = {}
|
|
14
22
|
return {
|
|
15
|
-
on: jest.fn((
|
|
23
|
+
on: jest.fn((eventName, fn) => { listeners[eventName] = fn }),
|
|
16
24
|
off: jest.fn(),
|
|
17
|
-
emit: jest.fn((
|
|
25
|
+
emit: jest.fn((eventName, payload) => listeners[eventName]?.(payload))
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
28
|
|
|
@@ -42,30 +50,78 @@ const makeMarkers = (overrides = []) => {
|
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
const makeMapProvider = (features = []) => ({
|
|
45
|
-
getVisibleFeatures: jest.fn(() => features)
|
|
53
|
+
getVisibleFeatures: jest.fn(() => features),
|
|
54
|
+
mapToScreen: jest.fn(() => ({ x: 10, y: 20 }))
|
|
46
55
|
})
|
|
47
56
|
|
|
48
|
-
const setup = ({ interactionModes = [], markers, layers = [], mapProvider, eventBus, dispatch, multiSelect = false } = {}) => {
|
|
57
|
+
const setup = ({ interactionModes = [], markers, layers = [], mapProvider, eventBus, dispatch, multiSelect = false, mapSize = 'small' } = {}) => {
|
|
49
58
|
const eb = eventBus ?? makeEventBus()
|
|
50
59
|
const mp = mapProvider ?? makeMapProvider()
|
|
51
60
|
const dp = dispatch ?? jest.fn()
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
// A real registry, not a mock — with only interact registered (additive, the default) it
|
|
62
|
+
// behaves exactly like the old direct-emit code did (same items, same multiselectable), so
|
|
63
|
+
// every existing eb.emit(SET_FEATURES, ...) assertion below still holds unchanged. The
|
|
64
|
+
// registry's own aggregation/exclusive-claim logic is covered separately, in
|
|
65
|
+
// spatialListRegistry.test.js.
|
|
66
|
+
const spatialListRegistry = createSpatialListRegistry({ eventBus: eb })
|
|
67
|
+
const { result, unmount } = renderHook(() => useSpatialList({
|
|
68
|
+
mapState: { markers: markers ?? makeMarkers(), mapSize },
|
|
54
69
|
pluginState: { interactionModes, layers, dispatch: dp, multiSelect },
|
|
55
70
|
services: { eventBus: eb },
|
|
56
|
-
mapProvider: mp
|
|
71
|
+
mapProvider: mp,
|
|
72
|
+
spatialListRegistry
|
|
57
73
|
}))
|
|
58
|
-
return { eb, mp, dp, result, unmount }
|
|
74
|
+
return { eb, mp, dp, result, unmount, spatialListRegistry }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A marker "matching" an id now requires it to actually have made it into the built item list
|
|
78
|
+
// (visible, labelled, in-viewport) — not just exist in markers.items — since useActiveItemHandler
|
|
79
|
+
// resolves purely by looking the id up in that same list. This helper builds one that will.
|
|
80
|
+
const setupVisibleMarker = (overrides = {}) => {
|
|
81
|
+
const { el, container } = makeMarkerEl({ inViewport: true })
|
|
82
|
+
const markers = makeMarkers([{ id: 'm1', label: 'Marker', symbol: 'pin', isVisible: true, ...overrides }])
|
|
83
|
+
markers.markerRefs.set('m1', el)
|
|
84
|
+
return { markers, container }
|
|
59
85
|
}
|
|
60
86
|
|
|
61
|
-
// ───
|
|
87
|
+
// ─── useSpatialList — lifecycle ──────────────────────────────────────────
|
|
62
88
|
|
|
63
|
-
describe('
|
|
89
|
+
describe('useSpatialList — lifecycle', () => {
|
|
64
90
|
it('subscribes to map:moveend on mount', () => {
|
|
65
91
|
const { eb } = setup()
|
|
66
92
|
expect(eb.on).toHaveBeenCalledWith(MOVE_END, expect.any(Function))
|
|
67
93
|
})
|
|
68
94
|
|
|
95
|
+
it('registers an "interact" item provider with spatialListRegistry on mount', () => {
|
|
96
|
+
const eb = makeEventBus()
|
|
97
|
+
const spatialListRegistry = createSpatialListRegistry({ eventBus: eb })
|
|
98
|
+
const registerSpy = jest.spyOn(spatialListRegistry, 'registerItemProvider')
|
|
99
|
+
const { unmount } = renderHook(() => useSpatialList({
|
|
100
|
+
mapState: { markers: makeMarkers(), mapSize: 'small' },
|
|
101
|
+
pluginState: { interactionModes: [], layers: [], dispatch: jest.fn(), multiSelect: false },
|
|
102
|
+
services: { eventBus: eb },
|
|
103
|
+
mapProvider: makeMapProvider(),
|
|
104
|
+
spatialListRegistry
|
|
105
|
+
}))
|
|
106
|
+
expect(registerSpy).toHaveBeenCalledWith('interact', { getItems: expect.any(Function) })
|
|
107
|
+
unmount()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('unregisters the "interact" item provider on unmount', () => {
|
|
111
|
+
const { spatialListRegistry, unmount } = setup()
|
|
112
|
+
const unregisterSpy = jest.spyOn(spatialListRegistry, 'unregisterItemProvider')
|
|
113
|
+
unmount()
|
|
114
|
+
expect(unregisterSpy).toHaveBeenCalledWith('interact')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('does not re-register the provider just because the map moves — it registers once for the hook\'s lifetime', () => {
|
|
118
|
+
const { eb, spatialListRegistry } = setup({ interactionModes: ['selectMarker'] })
|
|
119
|
+
const registerSpy = jest.spyOn(spatialListRegistry, 'registerItemProvider')
|
|
120
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
121
|
+
act(() => eb.emit(DATA_CHANGE, {}))
|
|
122
|
+
expect(registerSpy).not.toHaveBeenCalled()
|
|
123
|
+
})
|
|
124
|
+
|
|
69
125
|
it('subscribes to map:datachange on mount', () => {
|
|
70
126
|
const { eb } = setup()
|
|
71
127
|
expect(eb.on).toHaveBeenCalledWith(DATA_CHANGE, expect.any(Function))
|
|
@@ -84,9 +140,9 @@ describe('useMapItemList — lifecycle', () => {
|
|
|
84
140
|
})
|
|
85
141
|
})
|
|
86
142
|
|
|
87
|
-
// ───
|
|
143
|
+
// ─── useSpatialList — initial population ─────────────────────────────────
|
|
88
144
|
|
|
89
|
-
describe('
|
|
145
|
+
describe('useSpatialList — initial population', () => {
|
|
90
146
|
afterEach(() => { document.body.innerHTML = '' })
|
|
91
147
|
|
|
92
148
|
it('emits visible markers immediately on mount without waiting for moveend', () => {
|
|
@@ -97,20 +153,20 @@ describe('useMapItemList — initial population', () => {
|
|
|
97
153
|
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
98
154
|
|
|
99
155
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
100
|
-
items: [{ id: 'm1', label: MARKER_LABEL }], multiselectable: false
|
|
156
|
+
items: [markerItem({ id: 'm1', label: MARKER_LABEL })], multiselectable: false, label: 'Map features'
|
|
101
157
|
})
|
|
102
158
|
container.remove()
|
|
103
159
|
})
|
|
104
160
|
|
|
105
161
|
it('emits empty items immediately when no markers are in viewport', () => {
|
|
106
162
|
const { eb } = setup({ interactionModes: ['selectMarker'] })
|
|
107
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
163
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
108
164
|
})
|
|
109
165
|
})
|
|
110
166
|
|
|
111
|
-
// ───
|
|
167
|
+
// ─── useSpatialList — datachange trigger ─────────────────────────────────
|
|
112
168
|
|
|
113
|
-
describe('
|
|
169
|
+
describe('useSpatialList — datachange trigger', () => {
|
|
114
170
|
afterEach(() => { document.body.innerHTML = '' })
|
|
115
171
|
|
|
116
172
|
it('re-emits items on map:datachange the same as map:moveend', () => {
|
|
@@ -122,15 +178,15 @@ describe('useMapItemList — datachange trigger', () => {
|
|
|
122
178
|
act(() => eb.emit(DATA_CHANGE, {}))
|
|
123
179
|
|
|
124
180
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
125
|
-
items: [{ id: 'm1', label: MARKER_LABEL }], multiselectable: false
|
|
181
|
+
items: [markerItem({ id: 'm1', label: MARKER_LABEL })], multiselectable: false, label: 'Map features'
|
|
126
182
|
})
|
|
127
183
|
container.remove()
|
|
128
184
|
})
|
|
129
185
|
})
|
|
130
186
|
|
|
131
|
-
// ───
|
|
187
|
+
// ─── useSpatialList — selectMarker mode ───────────────────────────────────
|
|
132
188
|
|
|
133
|
-
describe('
|
|
189
|
+
describe('useSpatialList — selectMarker mode', () => {
|
|
134
190
|
afterEach(() => { document.body.innerHTML = '' })
|
|
135
191
|
|
|
136
192
|
it('emits visible markers as items on moveend', () => {
|
|
@@ -142,7 +198,7 @@ describe('useMapItemList — selectMarker mode', () => {
|
|
|
142
198
|
act(() => eb.emit(MOVE_END, {}))
|
|
143
199
|
|
|
144
200
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
145
|
-
items: [{ id: 'm1', label: MARKER_LABEL }], multiselectable: false
|
|
201
|
+
items: [markerItem({ id: 'm1', label: MARKER_LABEL })], multiselectable: false, label: 'Map features'
|
|
146
202
|
})
|
|
147
203
|
container.remove()
|
|
148
204
|
})
|
|
@@ -156,7 +212,7 @@ describe('useMapItemList — selectMarker mode', () => {
|
|
|
156
212
|
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
157
213
|
act(() => eb.emit(MOVE_END, {}))
|
|
158
214
|
|
|
159
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
215
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
160
216
|
el.remove()
|
|
161
217
|
})
|
|
162
218
|
|
|
@@ -168,7 +224,7 @@ describe('useMapItemList — selectMarker mode', () => {
|
|
|
168
224
|
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
169
225
|
act(() => eb.emit(MOVE_END, {}))
|
|
170
226
|
|
|
171
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
227
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
172
228
|
container.remove()
|
|
173
229
|
})
|
|
174
230
|
|
|
@@ -180,7 +236,7 @@ describe('useMapItemList — selectMarker mode', () => {
|
|
|
180
236
|
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
181
237
|
act(() => eb.emit(MOVE_END, {}))
|
|
182
238
|
|
|
183
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
239
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
184
240
|
container.remove()
|
|
185
241
|
})
|
|
186
242
|
|
|
@@ -192,14 +248,47 @@ describe('useMapItemList — selectMarker mode', () => {
|
|
|
192
248
|
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
193
249
|
act(() => eb.emit(MOVE_END, {}))
|
|
194
250
|
|
|
195
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
251
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
252
|
+
container.remove()
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('includes x/y screen position, scaled for mapSize, when the marker has coords', () => {
|
|
256
|
+
const { el, container } = makeMarkerEl({ inViewport: true })
|
|
257
|
+
const markers = makeMarkers([{ id: 'm1', label: MARKER_LABEL, symbol: 'pin', isVisible: true, coords: [-2.4, 54.5] }])
|
|
258
|
+
markers.markerRefs.set('m1', el)
|
|
259
|
+
const mp = makeMapProvider()
|
|
260
|
+
mp.mapToScreen.mockReturnValue({ x: 100, y: 200 })
|
|
261
|
+
|
|
262
|
+
const { eb } = setup({ interactionModes: ['selectMarker'], markers, mapProvider: mp, mapSize: 'medium' })
|
|
263
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
264
|
+
|
|
265
|
+
expect(mp.mapToScreen).toHaveBeenCalledWith([-2.4, 54.5])
|
|
266
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
267
|
+
items: [markerItem({ id: 'm1', label: MARKER_LABEL, x: 150, y: 300 })], multiselectable: false, label: 'Map features' // scaleFactor.medium = 1.5
|
|
268
|
+
})
|
|
269
|
+
container.remove()
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('omits x/y when the marker has no coords, without calling mapToScreen', () => {
|
|
273
|
+
const { el, container } = makeMarkerEl({ inViewport: true })
|
|
274
|
+
const markers = makeMarkers([{ id: 'm1', label: MARKER_LABEL, symbol: 'pin', isVisible: true }])
|
|
275
|
+
markers.markerRefs.set('m1', el)
|
|
276
|
+
const mp = makeMapProvider()
|
|
277
|
+
|
|
278
|
+
const { eb } = setup({ interactionModes: ['selectMarker'], markers, mapProvider: mp })
|
|
279
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
280
|
+
|
|
281
|
+
expect(mp.mapToScreen).not.toHaveBeenCalled()
|
|
282
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
283
|
+
items: [markerItem({ id: 'm1', label: MARKER_LABEL })], multiselectable: false, label: 'Map features'
|
|
284
|
+
})
|
|
196
285
|
container.remove()
|
|
197
286
|
})
|
|
198
287
|
})
|
|
199
288
|
|
|
200
|
-
// ───
|
|
289
|
+
// ─── useSpatialList — selectFeature mode: label resolution ───────────────
|
|
201
290
|
|
|
202
|
-
describe('
|
|
291
|
+
describe('useSpatialList — selectFeature mode: label resolution', () => {
|
|
203
292
|
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
204
293
|
|
|
205
294
|
it('emits layer features as items on moveend', () => {
|
|
@@ -216,7 +305,9 @@ describe('useMapItemList — selectFeature mode: label resolution', () => {
|
|
|
216
305
|
|
|
217
306
|
expect(mp.getVisibleFeatures).toHaveBeenCalledWith(['roads'])
|
|
218
307
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
219
|
-
items: [{ id: '1', label: 'High Street'
|
|
308
|
+
items: [featureItem({ id: '1', label: 'High Street', featureId: '1', layerId: 'roads', idProperty: 'road_id' })],
|
|
309
|
+
multiselectable: false,
|
|
310
|
+
label: 'Map features'
|
|
220
311
|
})
|
|
221
312
|
})
|
|
222
313
|
|
|
@@ -225,7 +316,7 @@ describe('useMapItemList — selectFeature mode: label resolution', () => {
|
|
|
225
316
|
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: makeMapProvider(features) })
|
|
226
317
|
act(() => eb.emit(MOVE_END, {}))
|
|
227
318
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
228
|
-
items: [{ id: '2', label: '2' }], multiselectable: false
|
|
319
|
+
items: [featureItem({ id: '2', label: '2' })], multiselectable: false, label: 'Map features'
|
|
229
320
|
})
|
|
230
321
|
})
|
|
231
322
|
|
|
@@ -238,7 +329,7 @@ describe('useMapItemList — selectFeature mode: label resolution', () => {
|
|
|
238
329
|
})
|
|
239
330
|
act(() => eb.emit(MOVE_END, {}))
|
|
240
331
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
241
|
-
items: [], multiselectable: false
|
|
332
|
+
items: [], multiselectable: false, label: 'Map features'
|
|
242
333
|
})
|
|
243
334
|
})
|
|
244
335
|
|
|
@@ -247,7 +338,7 @@ describe('useMapItemList — selectFeature mode: label resolution', () => {
|
|
|
247
338
|
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: makeMapProvider(features) })
|
|
248
339
|
act(() => eb.emit(MOVE_END, {}))
|
|
249
340
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
250
|
-
items: [{ id: '42', label: 'Oak Ave' }], multiselectable: false
|
|
341
|
+
items: [featureItem({ id: '42', label: 'Oak Ave' })], multiselectable: false, label: 'Map features'
|
|
251
342
|
})
|
|
252
343
|
})
|
|
253
344
|
|
|
@@ -255,16 +346,16 @@ describe('useMapItemList — selectFeature mode: label resolution', () => {
|
|
|
255
346
|
const features = [{ layer: { id: 'roads' }, properties: { road_name: 'Lost Lane' } }]
|
|
256
347
|
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: makeMapProvider(features) })
|
|
257
348
|
act(() => eb.emit(MOVE_END, {}))
|
|
258
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
349
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
259
350
|
})
|
|
260
351
|
|
|
261
352
|
it('falls back to stringId label when feature has no properties object', () => {
|
|
262
|
-
//
|
|
353
|
+
// feature.properties is undefined → ?. short-circuits → ?? stringId used
|
|
263
354
|
const features = [{ layer: { id: 'roads' }, id: 99 }]
|
|
264
355
|
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: makeMapProvider(features) })
|
|
265
356
|
act(() => eb.emit(MOVE_END, {}))
|
|
266
357
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
267
|
-
items: [{ id: '99', label: '99' }], multiselectable: false
|
|
358
|
+
items: [featureItem({ id: '99', label: '99' })], multiselectable: false, label: 'Map features'
|
|
268
359
|
})
|
|
269
360
|
})
|
|
270
361
|
|
|
@@ -280,27 +371,58 @@ describe('useMapItemList — selectFeature mode: label resolution', () => {
|
|
|
280
371
|
})
|
|
281
372
|
act(() => eb.emit(MOVE_END, {}))
|
|
282
373
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
283
|
-
items: [{ id: '1', label: 'High Street' }], multiselectable: false
|
|
374
|
+
items: [featureItem({ id: '1', label: 'High Street' })], multiselectable: false, label: 'Map features'
|
|
375
|
+
})
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('includes x/y at the bbox centre of the feature geometry, scaled for mapSize', () => {
|
|
379
|
+
const features = [{
|
|
380
|
+
layer: { id: 'roads' },
|
|
381
|
+
properties: { road_id: '1', road_name: 'High Street' },
|
|
382
|
+
geometry: { type: 'Polygon', coordinates: [[[0, 0], [4, 0], [4, 2], [0, 2], [0, 0]]] }
|
|
383
|
+
}]
|
|
384
|
+
const mp = makeMapProvider(features)
|
|
385
|
+
mp.mapToScreen.mockReturnValue({ x: 100, y: 200 })
|
|
386
|
+
|
|
387
|
+
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: mp, mapSize: 'medium' })
|
|
388
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
389
|
+
|
|
390
|
+
expect(mp.mapToScreen).toHaveBeenCalledWith([2, 1]) // bbox centre of the polygon above
|
|
391
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
392
|
+
items: [featureItem({ id: '1', label: 'High Street', x: 150, y: 300 })], multiselectable: false, label: 'Map features' // scaleFactor.medium = 1.5
|
|
393
|
+
})
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('omits x/y when the feature has no geometry, without calling mapToScreen', () => {
|
|
397
|
+
const features = [{ layer: { id: 'roads' }, properties: { road_id: '1', road_name: 'High Street' } }]
|
|
398
|
+
const mp = makeMapProvider(features)
|
|
399
|
+
|
|
400
|
+
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: mp })
|
|
401
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
402
|
+
|
|
403
|
+
expect(mp.mapToScreen).not.toHaveBeenCalled()
|
|
404
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
405
|
+
items: [featureItem({ id: '1', label: 'High Street' })], multiselectable: false, label: 'Map features'
|
|
284
406
|
})
|
|
285
407
|
})
|
|
286
408
|
})
|
|
287
409
|
|
|
288
|
-
// ───
|
|
410
|
+
// ─── useSpatialList — selectFeature mode: guards ─────────────────────────
|
|
289
411
|
|
|
290
|
-
describe('
|
|
412
|
+
describe('useSpatialList — selectFeature mode: guards', () => {
|
|
291
413
|
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
292
414
|
|
|
293
415
|
it('skips features with no matching layer config', () => {
|
|
294
416
|
const features = [{ layer: { id: 'unknown-layer' }, properties: { road_id: '4' } }]
|
|
295
417
|
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: makeMapProvider(features) })
|
|
296
418
|
act(() => eb.emit(MOVE_END, {}))
|
|
297
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
419
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
298
420
|
})
|
|
299
421
|
|
|
300
422
|
it('emits empty items when layers array is empty', () => {
|
|
301
423
|
const { eb } = setup({ interactionModes: ['selectFeature'], layers: [] })
|
|
302
424
|
act(() => eb.emit(MOVE_END, {}))
|
|
303
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
425
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
304
426
|
})
|
|
305
427
|
|
|
306
428
|
it('includes multiselectable: true in payload when multiSelect is enabled', () => {
|
|
@@ -313,14 +435,14 @@ describe('useMapItemList — selectFeature mode: guards', () => {
|
|
|
313
435
|
})
|
|
314
436
|
act(() => eb.emit(MOVE_END, {}))
|
|
315
437
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
316
|
-
items: [{ id: '1', label: 'High St' }], multiselectable: true
|
|
438
|
+
items: [featureItem({ id: '1', label: 'High St' })], multiselectable: true, label: 'Map features'
|
|
317
439
|
})
|
|
318
440
|
})
|
|
319
441
|
})
|
|
320
442
|
|
|
321
|
-
// ───
|
|
443
|
+
// ─── useSpatialList — combined modes ─────────────────────────────────────
|
|
322
444
|
|
|
323
|
-
describe('
|
|
445
|
+
describe('useSpatialList — combined modes', () => {
|
|
324
446
|
afterEach(() => { document.body.innerHTML = '' })
|
|
325
447
|
|
|
326
448
|
it('emits both markers and features when both modes are active', () => {
|
|
@@ -341,7 +463,9 @@ describe('useMapItemList — combined modes', () => {
|
|
|
341
463
|
act(() => eb.emit(MOVE_END, {}))
|
|
342
464
|
|
|
343
465
|
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
344
|
-
items: [{ id: 'm1', label: 'A Marker' }, { id: '1', label: 'Main Rd' }],
|
|
466
|
+
items: [markerItem({ id: 'm1', label: 'A Marker' }), featureItem({ id: '1', label: 'Main Rd' })],
|
|
467
|
+
multiselectable: false,
|
|
468
|
+
label: 'Map features'
|
|
345
469
|
})
|
|
346
470
|
container.remove()
|
|
347
471
|
})
|
|
@@ -349,13 +473,13 @@ describe('useMapItemList — combined modes', () => {
|
|
|
349
473
|
it('emits empty items when no interaction modes are active', () => {
|
|
350
474
|
const { eb } = setup({ interactionModes: [] })
|
|
351
475
|
act(() => eb.emit(MOVE_END, {}))
|
|
352
|
-
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
476
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false, label: 'Map features' })
|
|
353
477
|
})
|
|
354
478
|
})
|
|
355
479
|
|
|
356
|
-
// ───
|
|
480
|
+
// ─── useSpatialList — map:setactivefeature listener ──────────────────────
|
|
357
481
|
|
|
358
|
-
describe('
|
|
482
|
+
describe('useSpatialList — map:setactivefeature listener', () => {
|
|
359
483
|
afterEach(() => { document.body.innerHTML = '' })
|
|
360
484
|
|
|
361
485
|
it('subscribes to map:setactivefeature on mount and unsubscribes on unmount', () => {
|
|
@@ -372,10 +496,11 @@ describe('useMapItemList — map:setactivefeature listener', () => {
|
|
|
372
496
|
})
|
|
373
497
|
|
|
374
498
|
it('dispatches SET_LISTBOX_ACTIVE null when id matches a marker (ring handled by Markers.jsx)', () => {
|
|
375
|
-
const
|
|
499
|
+
const { markers, container } = setupVisibleMarker()
|
|
376
500
|
const { eb, dp } = setup({ interactionModes: ['selectMarker'], markers })
|
|
377
501
|
act(() => eb.emit(SET_ACTIVE, { id: 'm1' })) // NOSONAR
|
|
378
502
|
expect(dp).toHaveBeenCalledWith({ type: 'SET_LISTBOX_ACTIVE', payload: null })
|
|
503
|
+
container.remove()
|
|
379
504
|
})
|
|
380
505
|
|
|
381
506
|
it('dispatches SET_LISTBOX_ACTIVE with feature payload when id matches a feature', () => {
|
|
@@ -401,9 +526,9 @@ describe('useMapItemList — map:setactivefeature listener', () => {
|
|
|
401
526
|
})
|
|
402
527
|
|
|
403
528
|
it('preserves raw numeric featureId in SET_LISTBOX_ACTIVE payload (MapLibre filter type-strictness)', () => {
|
|
404
|
-
const layers = [{ layerId: 'hedges', idProperty: 'id' }]
|
|
529
|
+
const layers = [{ layerId: 'hedges', idProperty: 'id', labelProperty: 'name' }]
|
|
405
530
|
const features = [
|
|
406
|
-
{ layer: { id: 'hedges' }, properties: { id: 27665979 }, geometry: { type: 'LineString' } }
|
|
531
|
+
{ layer: { id: 'hedges' }, properties: { id: 27665979, name: 'Hedge' }, geometry: { type: 'LineString' } }
|
|
407
532
|
]
|
|
408
533
|
const { eb, dp } = setup({
|
|
409
534
|
interactionModes: ['selectFeature'],
|
|
@@ -422,8 +547,8 @@ describe('useMapItemList — map:setactivefeature listener', () => {
|
|
|
422
547
|
})
|
|
423
548
|
})
|
|
424
549
|
|
|
425
|
-
it('does not dispatch when
|
|
426
|
-
const layers = [{ layerId: 'roads', idProperty: 'road_id' }]
|
|
550
|
+
it('does not dispatch when the id was never part of the built item list', () => {
|
|
551
|
+
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
427
552
|
const { eb, dp } = setup({
|
|
428
553
|
interactionModes: ['selectFeature'],
|
|
429
554
|
layers,
|
|
@@ -433,8 +558,7 @@ describe('useMapItemList — map:setactivefeature listener', () => {
|
|
|
433
558
|
expect(dp).not.toHaveBeenCalled()
|
|
434
559
|
})
|
|
435
560
|
|
|
436
|
-
it('
|
|
437
|
-
// Line 69: getFeatureId returns null for unknown layer → rawId != null is false
|
|
561
|
+
it('does not dispatch for a feature from an unknown layer — it was never built into an item', () => {
|
|
438
562
|
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
439
563
|
const features = [{ layer: { id: 'unknown' }, properties: { some_id: '1' }, geometry: { type: 'Point' } }]
|
|
440
564
|
const { eb, dp } = setup({
|
|
@@ -446,8 +570,7 @@ describe('useMapItemList — map:setactivefeature listener', () => {
|
|
|
446
570
|
expect(dp).not.toHaveBeenCalled()
|
|
447
571
|
})
|
|
448
572
|
|
|
449
|
-
it('does not
|
|
450
|
-
// Line 114: interactionModes.includes('selectFeature') → false → block skipped
|
|
573
|
+
it('does not dispatch for a feature id when interactionModes excludes selectFeature — no feature items were built', () => {
|
|
451
574
|
const features = [{ layer: { id: 'roads' }, properties: { road_id: '1' }, geometry: { type: 'Point' } }]
|
|
452
575
|
const { eb, dp } = setup({
|
|
453
576
|
interactionModes: ['selectMarker'],
|
|
@@ -458,17 +581,38 @@ describe('useMapItemList — map:setactivefeature listener', () => {
|
|
|
458
581
|
expect(dp).not.toHaveBeenCalled()
|
|
459
582
|
})
|
|
460
583
|
|
|
461
|
-
it('does not
|
|
462
|
-
// Line 114: layers.length > 0 → false → block skipped
|
|
584
|
+
it('does not dispatch for a feature id when layers is empty — no feature items were built', () => {
|
|
463
585
|
const { eb, dp } = setup({ interactionModes: ['selectFeature'], layers: [] })
|
|
464
586
|
act(() => eb.emit(SET_ACTIVE, { id: '1' }))
|
|
465
587
|
expect(dp).not.toHaveBeenCalled()
|
|
466
588
|
})
|
|
589
|
+
|
|
590
|
+
it('resolves the active item from the already-built list, without re-querying the map engine', () => {
|
|
591
|
+
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
592
|
+
const features = [
|
|
593
|
+
{ layer: { id: 'roads' }, properties: { road_id: '1', road_name: 'High St' }, geometry: { type: 'Point' } },
|
|
594
|
+
{ layer: { id: 'roads' }, properties: { road_id: '2', road_name: 'Low St' }, geometry: { type: 'Point' } }
|
|
595
|
+
]
|
|
596
|
+
const mp = makeMapProvider(features)
|
|
597
|
+
const { eb, dp } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: mp })
|
|
598
|
+
|
|
599
|
+
expect(mp.getVisibleFeatures).toHaveBeenCalledTimes(1) // once, building the list on mount
|
|
600
|
+
|
|
601
|
+
act(() => eb.emit(SET_ACTIVE, { id: '1' }))
|
|
602
|
+
act(() => eb.emit(SET_ACTIVE, { id: '2' }))
|
|
603
|
+
act(() => eb.emit(SET_ACTIVE, { id: '1' }))
|
|
604
|
+
|
|
605
|
+
expect(mp.getVisibleFeatures).toHaveBeenCalledTimes(1) // still just the once
|
|
606
|
+
expect(dp).toHaveBeenLastCalledWith({
|
|
607
|
+
type: 'SET_LISTBOX_ACTIVE',
|
|
608
|
+
payload: { featureId: '1', layerId: 'roads', idProperty: 'road_id', geometry: { type: 'Point' } }
|
|
609
|
+
})
|
|
610
|
+
})
|
|
467
611
|
})
|
|
468
612
|
|
|
469
|
-
// ───
|
|
613
|
+
// ─── useSpatialList — confirm: lifecycle and guards ──────────────────────
|
|
470
614
|
|
|
471
|
-
describe('
|
|
615
|
+
describe('useSpatialList — confirm: lifecycle and guards', () => {
|
|
472
616
|
it('subscribes to map:confirmfeature on mount and unsubscribes on unmount', () => {
|
|
473
617
|
const { eb, unmount } = setup()
|
|
474
618
|
expect(eb.on).toHaveBeenCalledWith(CONFIRM, expect.any(Function))
|
|
@@ -483,23 +627,24 @@ describe('useMapItemList — confirm: lifecycle and guards', () => {
|
|
|
483
627
|
})
|
|
484
628
|
|
|
485
629
|
it('keeps the active item after confirm so repeated confirms dispatch again', () => {
|
|
486
|
-
const
|
|
630
|
+
const { markers, container } = setupVisibleMarker()
|
|
487
631
|
const { eb, dp } = setup({ interactionModes: ['selectMarker'], markers })
|
|
488
632
|
act(() => eb.emit(SET_ACTIVE, { id: 'm1' }))
|
|
489
633
|
act(() => eb.emit(CONFIRM))
|
|
490
634
|
dp.mockClear()
|
|
491
635
|
act(() => eb.emit(CONFIRM))
|
|
492
636
|
expect(dp).toHaveBeenCalledWith({ type: 'TOGGLE_SELECTED_MARKERS', payload: { markerId: 'm1', multiSelect: false } })
|
|
637
|
+
container.remove()
|
|
493
638
|
})
|
|
494
639
|
})
|
|
495
640
|
|
|
496
|
-
// ───
|
|
641
|
+
// ─── useSpatialList — confirm: dispatches ────────────────────────────────
|
|
497
642
|
|
|
498
|
-
describe('
|
|
643
|
+
describe('useSpatialList — confirm: marker dispatches', () => {
|
|
499
644
|
afterEach(() => { document.body.innerHTML = '' })
|
|
500
645
|
|
|
501
646
|
it('dispatches TOGGLE_SELECTED_MARKERS after activating a marker', () => {
|
|
502
|
-
const
|
|
647
|
+
const { markers, container } = setupVisibleMarker()
|
|
503
648
|
const { eb, dp } = setup({ interactionModes: ['selectMarker'], markers })
|
|
504
649
|
act(() => eb.emit(SET_ACTIVE, { id: 'm1' }))
|
|
505
650
|
act(() => eb.emit(CONFIRM))
|
|
@@ -507,10 +652,11 @@ describe('useMapItemList — confirm: marker dispatches', () => {
|
|
|
507
652
|
type: 'TOGGLE_SELECTED_MARKERS',
|
|
508
653
|
payload: { markerId: 'm1', multiSelect: false }
|
|
509
654
|
})
|
|
655
|
+
container.remove()
|
|
510
656
|
})
|
|
511
657
|
})
|
|
512
658
|
|
|
513
|
-
describe('
|
|
659
|
+
describe('useSpatialList — confirm: feature dispatches', () => {
|
|
514
660
|
it('dispatches TOGGLE_SELECTED_FEATURES after activating a feature', () => {
|
|
515
661
|
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
516
662
|
const features = [
|
|
@@ -557,9 +703,9 @@ describe('useMapItemList — confirm: feature dispatches', () => {
|
|
|
557
703
|
})
|
|
558
704
|
|
|
559
705
|
it('preserves raw numeric featureId in TOGGLE_SELECTED_FEATURES payload', () => {
|
|
560
|
-
const layers = [{ layerId: 'hedges', idProperty: 'id' }]
|
|
706
|
+
const layers = [{ layerId: 'hedges', idProperty: 'id', labelProperty: 'name' }]
|
|
561
707
|
const features = [
|
|
562
|
-
{ layer: { id: 'hedges' }, properties: { id: 27665979 }, geometry: { type: 'LineString' } }
|
|
708
|
+
{ layer: { id: 'hedges' }, properties: { id: 27665979, name: 'Hedge' }, geometry: { type: 'LineString' } }
|
|
563
709
|
]
|
|
564
710
|
const { eb, dp } = setup({
|
|
565
711
|
interactionModes: ['selectFeature'],
|
|
@@ -576,7 +722,7 @@ describe('useMapItemList — confirm: feature dispatches', () => {
|
|
|
576
722
|
replaceAll: true,
|
|
577
723
|
layerId: 'hedges',
|
|
578
724
|
idProperty: 'id',
|
|
579
|
-
properties: { id: 27665979 },
|
|
725
|
+
properties: { id: 27665979, name: 'Hedge' },
|
|
580
726
|
geometry: { type: 'LineString' }
|
|
581
727
|
}
|
|
582
728
|
})
|
|
@@ -9,8 +9,12 @@ import { selectFeature } from './api/selectFeature.js'
|
|
|
9
9
|
import { unselectFeature } from './api/unselectFeature.js'
|
|
10
10
|
import { selectMarker } from './api/selectMarker.js'
|
|
11
11
|
import { unselectMarker } from './api/unselectMarker.js'
|
|
12
|
+
import { isMac } from '../../../src/utils/isMac.js'
|
|
12
13
|
|
|
13
14
|
const SELECT_FEATURES_GROUP = 'Select'
|
|
15
|
+
// Matches the map's own label-navigation shortcut (src/App/controls/keyboardShortcuts.js) —
|
|
16
|
+
// same modifier, same Mac-aware label, different subject depending on what has focus.
|
|
17
|
+
const altKeyHtml = isMac() ? '<kbd>Option</kbd>' : '<kbd>Alt</kbd>'
|
|
14
18
|
|
|
15
19
|
export const manifest = {
|
|
16
20
|
InitComponent: InteractInit,
|
|
@@ -56,9 +60,16 @@ export const manifest = {
|
|
|
56
60
|
id: 'navigateFeatures',
|
|
57
61
|
group: SELECT_FEATURES_GROUP,
|
|
58
62
|
context: 'listbox',
|
|
59
|
-
title: '
|
|
63
|
+
title: 'Highlight next feature',
|
|
60
64
|
command: '<kbd>↑</kbd> or <kbd>↓</kbd>'
|
|
61
65
|
},
|
|
66
|
+
{
|
|
67
|
+
id: 'navigateFeaturesSpatially',
|
|
68
|
+
group: SELECT_FEATURES_GROUP,
|
|
69
|
+
context: 'listbox',
|
|
70
|
+
title: 'Highlight nearby feature',
|
|
71
|
+
command: `${altKeyHtml} + <kbd>↑</kbd>, <kbd>↓</kbd>, <kbd>←</kbd> or <kbd>→</kbd>`
|
|
72
|
+
},
|
|
62
73
|
{
|
|
63
74
|
id: 'selectFeature',
|
|
64
75
|
group: SELECT_FEATURES_GROUP,
|
|
@@ -53,3 +53,23 @@ describe('manifest', () => {
|
|
|
53
53
|
})
|
|
54
54
|
})
|
|
55
55
|
})
|
|
56
|
+
|
|
57
|
+
// altKeyHtml is resolved once at module load (see manifest.js), so exercising both branches
|
|
58
|
+
// needs a fresh require() under each platform value — same approach as
|
|
59
|
+
// src/App/controls/keyboardShortcuts.test.js's own isMac coverage.
|
|
60
|
+
describe('manifest — Alt/Option key label', () => {
|
|
61
|
+
beforeEach(() => jest.resetModules())
|
|
62
|
+
|
|
63
|
+
const loadSpatialShortcut = () =>
|
|
64
|
+
require('./manifest.js').manifest.keyboardShortcuts.find(s => s.id === 'navigateFeaturesSpatially')
|
|
65
|
+
|
|
66
|
+
it('uses Option on Mac', () => {
|
|
67
|
+
Object.defineProperty(navigator, 'platform', { value: 'MacIntel', configurable: true })
|
|
68
|
+
expect(loadSpatialShortcut().command).toContain('<kbd>Option</kbd>')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('uses Alt on non-Mac', () => {
|
|
72
|
+
Object.defineProperty(navigator, 'platform', { value: 'Win32', configurable: true })
|
|
73
|
+
expect(loadSpatialShortcut().command).toContain('<kbd>Alt</kbd>')
|
|
74
|
+
})
|
|
75
|
+
})
|