@defra/interactive-map 0.0.22-alpha → 0.0.23-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/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/button-definition.md +39 -0
- package/docs/api/context.md +4 -4
- package/docs/api/map-style-config.md +11 -2
- package/docs/api/marker-config.md +38 -7
- package/docs/api/panel-definition.md +7 -3
- package/docs/api/symbol-config.md +10 -26
- package/docs/api/symbol-registry.md +28 -13
- package/docs/api.md +24 -2
- package/docs/assets/css/{docusaurus.css → docusaurus.scss} +18 -12
- package/docs/assets/images/toggle-marker-label.jpg +0 -0
- package/docs/examples/index.mdx +12 -0
- package/docs/examples/toggle-marker-label.mdx +89 -0
- package/docs/plugins/interact.md +4 -11
- package/docusaurus.config.cjs +2 -1
- package/package.json +8 -8
- package/plugins/beta/datasets/dist/adapters/maplibre/esm/im-datasets-ml-adapter.js +1 -1
- package/plugins/beta/datasets/dist/css/index.css +2 -2
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-maplibre-adapter.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/index.js +1 -1
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +5 -5
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +24 -29
- package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +2 -2
- package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +3 -3
- package/plugins/beta/datasets/src/components/KeySvg.jsx +2 -2
- package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +1 -2
- package/plugins/beta/datasets/src/components/svgProperties.js +2 -2
- package/plugins/beta/datasets/src/panels/Key.module.scss +2 -2
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +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/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/index.js +1 -1
- package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/src/manifest.js +1 -0
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/index.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +23 -55
- package/plugins/interact/src/InteractInit.test.js +58 -80
- package/plugins/interact/src/defaults.js +1 -3
- package/plugins/interact/src/hooks/useAttachEvents.js +46 -0
- package/plugins/interact/src/hooks/useAttachEvents.test.js +65 -0
- package/plugins/interact/src/hooks/useCrossHairVisibility.js +47 -0
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +54 -0
- package/plugins/interact/src/hooks/useHighlightSync.js +30 -8
- package/plugins/interact/src/hooks/useHighlightSync.test.js +127 -80
- package/plugins/interact/src/hooks/useInteractionHandlers.js +18 -17
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +22 -61
- package/plugins/interact/src/hooks/useMapItemList.js +187 -0
- package/plugins/interact/src/hooks/useMapItemList.test.js +584 -0
- package/plugins/interact/src/index.js +0 -2
- package/plugins/interact/src/index.test.js +0 -2
- package/plugins/interact/src/manifest.js +37 -6
- package/plugins/interact/src/reducer.js +8 -4
- package/plugins/interact/src/reducer.test.js +18 -3
- package/plugins/interact/src/utils/buildStylesMap.js +21 -10
- package/plugins/interact/src/utils/buildStylesMap.test.js +29 -29
- package/plugins/interact/src/utils/spatial.js +0 -15
- package/plugins/interact/src/utils/spatial.test.js +1 -13
- 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/components/CloseButton/CloseButton.module.scss +2 -0
- package/plugins/search/src/components/OpenButton/OpenButton.jsx +1 -1
- package/plugins/search/src/components/OpenButton/OpenButton.module.scss +3 -0
- package/plugins/search/src/search.scss +1 -0
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/esriProvider.js +4 -0
- 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-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/index.js +1 -1
- package/providers/maplibre/src/index.test.js +9 -14
- package/providers/maplibre/src/mapEvents.js +11 -5
- package/providers/maplibre/src/mapEvents.test.js +14 -3
- package/providers/maplibre/src/maplibreProvider.js +18 -11
- package/providers/maplibre/src/maplibreProvider.test.js +43 -36
- package/providers/maplibre/src/utils/highlightFeatures.js +102 -57
- package/providers/maplibre/src/utils/highlightFeatures.test.js +162 -51
- package/providers/maplibre/src/utils/maplibreFixes.js +10 -0
- package/providers/maplibre/src/utils/maplibreFixes.test.js +31 -0
- package/providers/maplibre/src/utils/patternImages.js +10 -12
- package/providers/maplibre/src/utils/patternImages.test.js +145 -167
- package/providers/maplibre/src/utils/rasteriseToImageData.js +1 -4
- package/providers/maplibre/src/utils/rasteriseToImageData.test.js +23 -24
- package/providers/maplibre/src/utils/symbolImages.js +23 -86
- package/providers/maplibre/src/utils/symbolImages.test.js +60 -105
- package/src/App/components/Actions/Actions.module.scss +1 -0
- package/src/App/components/Actions/Actions.test.jsx +17 -4
- package/src/App/components/Hints/Hints.jsx +52 -0
- package/src/App/components/Hints/Hints.module.scss +37 -0
- package/src/App/components/Hints/Hints.test.jsx +104 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +57 -19
- package/src/App/components/KeyboardHelp/KeyboardHelp.module.scss +10 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +140 -31
- package/src/App/components/Markers/LabelMarker.jsx +14 -0
- package/src/App/components/Markers/LabelMarker.test.jsx +36 -0
- package/src/App/components/Markers/Markers.jsx +44 -75
- package/src/App/components/Markers/Markers.module.scss +58 -0
- package/src/App/components/Markers/Markers.test.jsx +90 -113
- package/src/App/components/Markers/SymbolLabelMarker.jsx +43 -0
- package/src/App/components/Markers/SymbolLabelMarker.test.jsx +56 -0
- package/src/App/components/Markers/SymbolMarker.jsx +31 -0
- package/src/App/components/Markers/SymbolMarker.test.jsx +72 -0
- package/src/App/components/Panel/Panel.jsx +1 -1
- package/src/App/components/Panel/Panel.module.scss +1 -0
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +117 -1
- package/src/App/components/PopupMenu/usePopupMenu.js +132 -85
- package/src/App/components/Tabs/Tabs.jsx +68 -0
- package/src/App/components/Tabs/Tabs.module.scss +57 -0
- package/src/App/components/Tabs/Tabs.test.jsx +121 -0
- package/src/App/components/Tooltip/Tooltip.module.scss +1 -1
- package/src/App/components/Viewport/Features.jsx +35 -0
- package/src/App/components/Viewport/Features.module.scss +31 -0
- package/src/App/components/Viewport/Features.test.jsx +124 -0
- package/src/App/components/Viewport/MapStatus.jsx +0 -4
- package/src/App/components/Viewport/Viewport.jsx +35 -29
- package/src/App/components/Viewport/Viewport.module.scss +7 -21
- package/src/App/components/Viewport/Viewport.test.jsx +108 -65
- package/src/App/controls/keyboardActions.js +9 -3
- package/src/App/controls/keyboardActions.test.js +122 -87
- package/src/App/controls/keyboardShortcuts.js +1 -14
- package/src/App/hooks/useFeatureFocus.js +199 -0
- package/src/App/hooks/useFeatureFocus.test.js +635 -0
- package/src/App/hooks/useFeatureItems.js +30 -0
- package/src/App/hooks/useFeatureItems.test.js +97 -0
- package/src/App/hooks/useFocusVisible.js +4 -7
- package/src/App/hooks/useFocusVisible.test.js +6 -6
- package/src/App/hooks/useKeyboardHint.js +9 -34
- package/src/App/hooks/useKeyboardHint.test.js +34 -84
- package/src/App/hooks/useKeyboardShortcuts.js +8 -5
- package/src/App/hooks/useKeyboardShortcuts.test.js +16 -12
- package/src/App/hooks/useLayoutMeasurements.js +10 -4
- package/src/App/hooks/useLayoutMeasurements.test.js +30 -0
- package/src/App/hooks/useMarkerCursor.js +33 -0
- package/src/App/hooks/useMarkerCursor.test.js +87 -0
- package/src/App/hooks/useMarkersAPI.js +32 -9
- package/src/App/hooks/useMarkersAPI.test.js +162 -94
- package/src/App/layout/Layout.jsx +2 -0
- package/src/App/layout/Layout.test.jsx +3 -0
- package/src/App/registry/keyboardShortcutRegistry.js +3 -2
- package/src/App/registry/keyboardShortcutRegistry.test.js +6 -8
- package/src/App/renderer/mapButtons.js +5 -1
- package/src/App/renderer/mapButtons.test.js +37 -1
- package/src/App/store/ServiceProvider.jsx +4 -1
- package/src/App/store/ServiceProvider.test.jsx +11 -0
- package/src/App/store/appActionsMap.js +5 -1
- package/src/App/store/appActionsMap.test.js +13 -0
- package/src/App/store/appReducer.js +1 -0
- package/src/App/store/mapActionsMap.js +2 -1
- package/src/InteractiveMap/InteractiveMap.js +12 -0
- package/src/InteractiveMap/InteractiveMap.test.js +2 -0
- package/src/config/appConfig.js +3 -1
- package/src/config/appConfig.test.js +4 -0
- package/src/config/defaults.js +1 -1
- package/src/config/events.js +8 -0
- package/src/config/mapTheme.js +8 -4
- package/src/config/symbolConfig.js +23 -16
- package/src/scss/main.scss +3 -0
- package/src/scss/tools/_border-focus.scss +0 -1
- package/src/services/eventBus.js +15 -0
- package/src/services/eventBus.test.js +11 -0
- package/src/services/hints.js +47 -0
- package/src/services/hints.test.js +110 -0
- package/src/services/patternRegistry.js +69 -1
- package/src/services/patternRegistry.test.js +112 -1
- package/src/services/reverseGeocode.js +4 -0
- package/src/services/reverseGeocode.test.js +11 -1
- package/src/services/symbolRegistry.js +152 -17
- package/src/services/symbolRegistry.test.js +141 -31
- package/src/types.js +71 -14
- package/src/utils/constrainKeyboardFocus.js +5 -5
- package/src/utils/detectInterfaceType.js +27 -5
- package/src/utils/detectInterfaceType.test.js +29 -3
- package/src/utils/patternUtils.js +4 -67
- package/src/utils/patternUtils.test.js +1 -123
- package/src/utils/symbolUtils.js +11 -13
- package/src/utils/symbolUtils.test.js +18 -26
- package/plugins/interact/dist/css/index.css +0 -1
- package/plugins/interact/src/interact.scss +0 -24
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react'
|
|
2
|
+
import { useMapItemList } from './useMapItemList.js'
|
|
3
|
+
|
|
4
|
+
const MARKER_LABEL = 'Marker One'
|
|
5
|
+
|
|
6
|
+
const MOVE_END = 'map:moveend'
|
|
7
|
+
const DATA_CHANGE = 'map:datachange'
|
|
8
|
+
const SET_FEATURES = 'map:setfeatures'
|
|
9
|
+
const SET_ACTIVE = 'map:setactivefeature'
|
|
10
|
+
const CONFIRM = 'map:selectfeature'
|
|
11
|
+
|
|
12
|
+
const makeEventBus = () => {
|
|
13
|
+
const listeners = {}
|
|
14
|
+
return {
|
|
15
|
+
on: jest.fn((e, fn) => { listeners[e] = fn }),
|
|
16
|
+
off: jest.fn(),
|
|
17
|
+
emit: jest.fn((e, payload) => listeners[e]?.(payload))
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const makeMarkerEl = ({ inViewport = true } = {}) => {
|
|
22
|
+
const el = document.createElement('div')
|
|
23
|
+
const container = document.createElement('div')
|
|
24
|
+
container.className = 'im-c-viewport__markers'
|
|
25
|
+
container.appendChild(el)
|
|
26
|
+
document.body.appendChild(container)
|
|
27
|
+
|
|
28
|
+
const containerRect = { left: 0, top: 0, right: 500, bottom: 500 }
|
|
29
|
+
const markerRect = inViewport
|
|
30
|
+
? { left: 100, top: 100, right: 150, bottom: 150 }
|
|
31
|
+
: { left: 600, top: 600, right: 650, bottom: 650 }
|
|
32
|
+
|
|
33
|
+
jest.spyOn(container, 'getBoundingClientRect').mockReturnValue(containerRect)
|
|
34
|
+
jest.spyOn(el, 'getBoundingClientRect').mockReturnValue(markerRect)
|
|
35
|
+
|
|
36
|
+
return { el, container }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const makeMarkers = (overrides = []) => {
|
|
40
|
+
const markerRefs = new Map()
|
|
41
|
+
return { items: overrides, markerRefs }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const makeMapProvider = (features = []) => ({
|
|
45
|
+
getVisibleFeatures: jest.fn(() => features)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const setup = ({ interactionModes = [], markers, layers = [], mapProvider, eventBus, dispatch, multiSelect = false } = {}) => {
|
|
49
|
+
const eb = eventBus ?? makeEventBus()
|
|
50
|
+
const mp = mapProvider ?? makeMapProvider()
|
|
51
|
+
const dp = dispatch ?? jest.fn()
|
|
52
|
+
const { result, unmount } = renderHook(() => useMapItemList({
|
|
53
|
+
mapState: { markers: markers ?? makeMarkers() },
|
|
54
|
+
pluginState: { interactionModes, layers, dispatch: dp, multiSelect },
|
|
55
|
+
services: { eventBus: eb },
|
|
56
|
+
mapProvider: mp
|
|
57
|
+
}))
|
|
58
|
+
return { eb, mp, dp, result, unmount }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── useMapItemList — lifecycle ──────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
describe('useMapItemList — lifecycle', () => {
|
|
64
|
+
it('subscribes to map:moveend on mount', () => {
|
|
65
|
+
const { eb } = setup()
|
|
66
|
+
expect(eb.on).toHaveBeenCalledWith(MOVE_END, expect.any(Function))
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('subscribes to map:datachange on mount', () => {
|
|
70
|
+
const { eb } = setup()
|
|
71
|
+
expect(eb.on).toHaveBeenCalledWith(DATA_CHANGE, expect.any(Function))
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('unsubscribes from map:moveend on unmount', () => {
|
|
75
|
+
const { eb, unmount } = setup()
|
|
76
|
+
unmount()
|
|
77
|
+
expect(eb.off).toHaveBeenCalledWith(MOVE_END, expect.any(Function))
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('unsubscribes from map:datachange on unmount', () => {
|
|
81
|
+
const { eb, unmount } = setup()
|
|
82
|
+
unmount()
|
|
83
|
+
expect(eb.off).toHaveBeenCalledWith(DATA_CHANGE, expect.any(Function))
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// ─── useMapItemList — initial population ─────────────────────────────────
|
|
88
|
+
|
|
89
|
+
describe('useMapItemList — initial population', () => {
|
|
90
|
+
afterEach(() => { document.body.innerHTML = '' })
|
|
91
|
+
|
|
92
|
+
it('emits visible markers immediately on mount without waiting for moveend', () => {
|
|
93
|
+
const { el, container } = makeMarkerEl({ inViewport: true })
|
|
94
|
+
const markers = makeMarkers([{ id: 'm1', label: MARKER_LABEL, symbol: 'pin', isVisible: true }])
|
|
95
|
+
markers.markerRefs.set('m1', el)
|
|
96
|
+
|
|
97
|
+
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
98
|
+
|
|
99
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
100
|
+
items: [{ id: 'm1', label: MARKER_LABEL }], multiselectable: false
|
|
101
|
+
})
|
|
102
|
+
container.remove()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('emits empty items immediately when no markers are in viewport', () => {
|
|
106
|
+
const { eb } = setup({ interactionModes: ['selectMarker'] })
|
|
107
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// ─── useMapItemList — datachange trigger ─────────────────────────────────
|
|
112
|
+
|
|
113
|
+
describe('useMapItemList — datachange trigger', () => {
|
|
114
|
+
afterEach(() => { document.body.innerHTML = '' })
|
|
115
|
+
|
|
116
|
+
it('re-emits items on map:datachange the same as map:moveend', () => {
|
|
117
|
+
const { el, container } = makeMarkerEl({ inViewport: true })
|
|
118
|
+
const markers = makeMarkers([{ id: 'm1', label: MARKER_LABEL, symbol: 'pin', isVisible: true }])
|
|
119
|
+
markers.markerRefs.set('m1', el)
|
|
120
|
+
|
|
121
|
+
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
122
|
+
act(() => eb.emit(DATA_CHANGE, {}))
|
|
123
|
+
|
|
124
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
125
|
+
items: [{ id: 'm1', label: MARKER_LABEL }], multiselectable: false
|
|
126
|
+
})
|
|
127
|
+
container.remove()
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// ─── useMapItemList — selectMarker mode ───────────────────────────────────
|
|
132
|
+
|
|
133
|
+
describe('useMapItemList — selectMarker mode', () => {
|
|
134
|
+
afterEach(() => { document.body.innerHTML = '' })
|
|
135
|
+
|
|
136
|
+
it('emits visible markers as items on moveend', () => {
|
|
137
|
+
const { el, container } = makeMarkerEl({ inViewport: true })
|
|
138
|
+
const markers = makeMarkers([{ id: 'm1', label: MARKER_LABEL, symbol: 'pin', isVisible: true }])
|
|
139
|
+
markers.markerRefs.set('m1', el)
|
|
140
|
+
|
|
141
|
+
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
142
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
143
|
+
|
|
144
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
145
|
+
items: [{ id: 'm1', label: MARKER_LABEL }], multiselectable: false
|
|
146
|
+
})
|
|
147
|
+
container.remove()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('excludes markers whose element has no .im-c-viewport__markers ancestor', () => {
|
|
151
|
+
const el = document.createElement('div')
|
|
152
|
+
document.body.appendChild(el)
|
|
153
|
+
const markers = makeMarkers([{ id: 'm1', label: 'Orphan', symbol: 'pin', isVisible: true }])
|
|
154
|
+
markers.markerRefs.set('m1', el)
|
|
155
|
+
|
|
156
|
+
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
157
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
158
|
+
|
|
159
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
160
|
+
el.remove()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('excludes markers outside the viewport', () => {
|
|
164
|
+
const { el, container } = makeMarkerEl({ inViewport: false })
|
|
165
|
+
const markers = makeMarkers([{ id: 'm1', label: 'Offscreen', symbol: 'pin', isVisible: true }])
|
|
166
|
+
markers.markerRefs.set('m1', el)
|
|
167
|
+
|
|
168
|
+
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
169
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
170
|
+
|
|
171
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
172
|
+
container.remove()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('excludes standalone label markers', () => {
|
|
176
|
+
const { el, container } = makeMarkerEl({ inViewport: true })
|
|
177
|
+
const markers = makeMarkers([{ id: 'm1', label: 'Label', symbol: null, isVisible: true }])
|
|
178
|
+
markers.markerRefs.set('m1', el)
|
|
179
|
+
|
|
180
|
+
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
181
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
182
|
+
|
|
183
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
184
|
+
container.remove()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('excludes markers without a label', () => {
|
|
188
|
+
const { el, container } = makeMarkerEl({ inViewport: true })
|
|
189
|
+
const markers = makeMarkers([{ id: 'm1', symbol: 'pin', isVisible: true }])
|
|
190
|
+
markers.markerRefs.set('m1', el)
|
|
191
|
+
|
|
192
|
+
const { eb } = setup({ interactionModes: ['selectMarker'], markers })
|
|
193
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
194
|
+
|
|
195
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
196
|
+
container.remove()
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// ─── useMapItemList — selectFeature mode: label resolution ───────────────
|
|
201
|
+
|
|
202
|
+
describe('useMapItemList — selectFeature mode: label resolution', () => {
|
|
203
|
+
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
204
|
+
|
|
205
|
+
it('emits layer features as items on moveend', () => {
|
|
206
|
+
const features = [
|
|
207
|
+
{ layer: { id: 'roads' }, properties: { road_id: '1', road_name: 'High Street' } }
|
|
208
|
+
]
|
|
209
|
+
const { eb, mp } = setup({
|
|
210
|
+
interactionModes: ['selectFeature'],
|
|
211
|
+
layers,
|
|
212
|
+
mapProvider: makeMapProvider(features)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
216
|
+
|
|
217
|
+
expect(mp.getVisibleFeatures).toHaveBeenCalledWith(['roads'])
|
|
218
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
219
|
+
items: [{ id: '1', label: 'High Street' }], multiselectable: false
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('falls back to idProperty value when labelProperty is absent on feature', () => {
|
|
224
|
+
const features = [{ layer: { id: 'roads' }, properties: { road_id: '2' } }]
|
|
225
|
+
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: makeMapProvider(features) })
|
|
226
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
227
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
228
|
+
items: [{ id: '2', label: '2' }], multiselectable: false
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('excludes features from layers with no labelProperty configured', () => {
|
|
233
|
+
const features = [{ layer: { id: 'roads' }, properties: { road_id: '3' } }]
|
|
234
|
+
const { eb } = setup({
|
|
235
|
+
interactionModes: ['selectFeature'],
|
|
236
|
+
layers: [{ layerId: 'roads', idProperty: 'road_id' }],
|
|
237
|
+
mapProvider: makeMapProvider(features)
|
|
238
|
+
})
|
|
239
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
240
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
241
|
+
items: [], multiselectable: false
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('uses feature.id when idProperty is not present in feature properties', () => {
|
|
246
|
+
const features = [{ layer: { id: 'roads' }, id: 42, properties: { road_name: 'Oak Ave' } }]
|
|
247
|
+
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: makeMapProvider(features) })
|
|
248
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
249
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
250
|
+
items: [{ id: '42', label: 'Oak Ave' }], multiselectable: false
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('skips feature when both idProperty and feature.id are absent', () => {
|
|
255
|
+
const features = [{ layer: { id: 'roads' }, properties: { road_name: 'Lost Lane' } }]
|
|
256
|
+
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: makeMapProvider(features) })
|
|
257
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
258
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('falls back to stringId label when feature has no properties object', () => {
|
|
262
|
+
// Line 45: feature.properties is undefined → ?. short-circuits → ?? stringId used
|
|
263
|
+
const features = [{ layer: { id: 'roads' }, id: 99 }]
|
|
264
|
+
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: makeMapProvider(features) })
|
|
265
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
266
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
267
|
+
items: [{ id: '99', label: '99' }], multiselectable: false
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('deduplicates features with the same id', () => {
|
|
272
|
+
const features = [
|
|
273
|
+
{ layer: { id: 'roads' }, properties: { road_id: '1', road_name: 'High Street' } },
|
|
274
|
+
{ layer: { id: 'roads' }, properties: { road_id: '1', road_name: 'Duplicate' } }
|
|
275
|
+
]
|
|
276
|
+
const { eb } = setup({
|
|
277
|
+
interactionModes: ['selectFeature'],
|
|
278
|
+
layers: [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }],
|
|
279
|
+
mapProvider: makeMapProvider(features)
|
|
280
|
+
})
|
|
281
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
282
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
283
|
+
items: [{ id: '1', label: 'High Street' }], multiselectable: false
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
// ─── useMapItemList — selectFeature mode: guards ─────────────────────────
|
|
289
|
+
|
|
290
|
+
describe('useMapItemList — selectFeature mode: guards', () => {
|
|
291
|
+
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
292
|
+
|
|
293
|
+
it('skips features with no matching layer config', () => {
|
|
294
|
+
const features = [{ layer: { id: 'unknown-layer' }, properties: { road_id: '4' } }]
|
|
295
|
+
const { eb } = setup({ interactionModes: ['selectFeature'], layers, mapProvider: makeMapProvider(features) })
|
|
296
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
297
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('emits empty items when layers array is empty', () => {
|
|
301
|
+
const { eb } = setup({ interactionModes: ['selectFeature'], layers: [] })
|
|
302
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
303
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('includes multiselectable: true in payload when multiSelect is enabled', () => {
|
|
307
|
+
const features = [{ layer: { id: 'roads' }, properties: { road_id: '1', road_name: 'High St' } }]
|
|
308
|
+
const { eb } = setup({
|
|
309
|
+
interactionModes: ['selectFeature'],
|
|
310
|
+
layers,
|
|
311
|
+
mapProvider: makeMapProvider(features),
|
|
312
|
+
multiSelect: true
|
|
313
|
+
})
|
|
314
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
315
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
316
|
+
items: [{ id: '1', label: 'High St' }], multiselectable: true
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
// ─── useMapItemList — combined modes ─────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
describe('useMapItemList — combined modes', () => {
|
|
324
|
+
afterEach(() => { document.body.innerHTML = '' })
|
|
325
|
+
|
|
326
|
+
it('emits both markers and features when both modes are active', () => {
|
|
327
|
+
const { el, container } = makeMarkerEl({ inViewport: true })
|
|
328
|
+
const markers = makeMarkers([{ id: 'm1', label: 'A Marker', symbol: 'pin', isVisible: true }])
|
|
329
|
+
markers.markerRefs.set('m1', el)
|
|
330
|
+
|
|
331
|
+
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
332
|
+
const features = [{ layer: { id: 'roads' }, properties: { road_id: '1', road_name: 'Main Rd' } }]
|
|
333
|
+
|
|
334
|
+
const { eb } = setup({
|
|
335
|
+
interactionModes: ['selectMarker', 'selectFeature'],
|
|
336
|
+
markers,
|
|
337
|
+
layers,
|
|
338
|
+
mapProvider: makeMapProvider(features)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
342
|
+
|
|
343
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, {
|
|
344
|
+
items: [{ id: 'm1', label: 'A Marker' }, { id: '1', label: 'Main Rd' }], multiselectable: false
|
|
345
|
+
})
|
|
346
|
+
container.remove()
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('emits empty items when no interaction modes are active', () => {
|
|
350
|
+
const { eb } = setup({ interactionModes: [] })
|
|
351
|
+
act(() => eb.emit(MOVE_END, {}))
|
|
352
|
+
expect(eb.emit).toHaveBeenCalledWith(SET_FEATURES, { items: [], multiselectable: false })
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
// ─── useMapItemList — map:setactivefeature listener ──────────────────────
|
|
357
|
+
|
|
358
|
+
describe('useMapItemList — map:setactivefeature listener', () => {
|
|
359
|
+
afterEach(() => { document.body.innerHTML = '' })
|
|
360
|
+
|
|
361
|
+
it('subscribes to map:setactivefeature on mount and unsubscribes on unmount', () => {
|
|
362
|
+
const { eb, unmount } = setup()
|
|
363
|
+
expect(eb.on).toHaveBeenCalledWith(SET_ACTIVE, expect.any(Function)) // NOSONAR
|
|
364
|
+
unmount()
|
|
365
|
+
expect(eb.off).toHaveBeenCalledWith(SET_ACTIVE, expect.any(Function)) // NOSONAR
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('dispatches SET_LISTBOX_ACTIVE null when id is null', () => {
|
|
369
|
+
const { eb, dp } = setup({ interactionModes: ['selectMarker'] })
|
|
370
|
+
act(() => eb.emit(SET_ACTIVE, { id: null })) // NOSONAR
|
|
371
|
+
expect(dp).toHaveBeenCalledWith({ type: 'SET_LISTBOX_ACTIVE', payload: null })
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('dispatches SET_LISTBOX_ACTIVE null when id matches a marker (ring handled by Markers.jsx)', () => {
|
|
375
|
+
const markers = makeMarkers([{ id: 'm1', label: 'Marker', symbol: 'pin', isVisible: true }])
|
|
376
|
+
const { eb, dp } = setup({ interactionModes: ['selectMarker'], markers })
|
|
377
|
+
act(() => eb.emit(SET_ACTIVE, { id: 'm1' })) // NOSONAR
|
|
378
|
+
expect(dp).toHaveBeenCalledWith({ type: 'SET_LISTBOX_ACTIVE', payload: null })
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('dispatches SET_LISTBOX_ACTIVE with feature payload when id matches a feature', () => {
|
|
382
|
+
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
383
|
+
const features = [
|
|
384
|
+
{ layer: { id: 'roads' }, properties: { road_id: '1', road_name: 'High St' }, geometry: { type: 'Point' } }
|
|
385
|
+
]
|
|
386
|
+
const { eb, dp } = setup({
|
|
387
|
+
interactionModes: ['selectFeature'],
|
|
388
|
+
layers,
|
|
389
|
+
mapProvider: makeMapProvider(features)
|
|
390
|
+
})
|
|
391
|
+
act(() => eb.emit(SET_ACTIVE, { id: '1' })) // NOSONAR
|
|
392
|
+
expect(dp).toHaveBeenCalledWith({
|
|
393
|
+
type: 'SET_LISTBOX_ACTIVE',
|
|
394
|
+
payload: {
|
|
395
|
+
featureId: '1',
|
|
396
|
+
layerId: 'roads',
|
|
397
|
+
idProperty: 'road_id',
|
|
398
|
+
geometry: { type: 'Point' }
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('preserves raw numeric featureId in SET_LISTBOX_ACTIVE payload (MapLibre filter type-strictness)', () => {
|
|
404
|
+
const layers = [{ layerId: 'hedges', idProperty: 'id' }]
|
|
405
|
+
const features = [
|
|
406
|
+
{ layer: { id: 'hedges' }, properties: { id: 27665979 }, geometry: { type: 'LineString' } }
|
|
407
|
+
]
|
|
408
|
+
const { eb, dp } = setup({
|
|
409
|
+
interactionModes: ['selectFeature'],
|
|
410
|
+
layers,
|
|
411
|
+
mapProvider: makeMapProvider(features)
|
|
412
|
+
})
|
|
413
|
+
act(() => eb.emit(SET_ACTIVE, { id: '27665979' }))
|
|
414
|
+
expect(dp).toHaveBeenCalledWith({
|
|
415
|
+
type: 'SET_LISTBOX_ACTIVE',
|
|
416
|
+
payload: {
|
|
417
|
+
featureId: 27665979,
|
|
418
|
+
layerId: 'hedges',
|
|
419
|
+
idProperty: 'id',
|
|
420
|
+
geometry: { type: 'LineString' }
|
|
421
|
+
}
|
|
422
|
+
})
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('does not dispatch when feature id is not found in visible features', () => {
|
|
426
|
+
const layers = [{ layerId: 'roads', idProperty: 'road_id' }]
|
|
427
|
+
const { eb, dp } = setup({
|
|
428
|
+
interactionModes: ['selectFeature'],
|
|
429
|
+
layers,
|
|
430
|
+
mapProvider: makeMapProvider([])
|
|
431
|
+
})
|
|
432
|
+
act(() => eb.emit(SET_ACTIVE, { id: 'missing' })) // NOSONAR
|
|
433
|
+
expect(dp).not.toHaveBeenCalled()
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it('skips features from unknown layers when searching by id', () => {
|
|
437
|
+
// Line 69: getFeatureId returns null for unknown layer → rawId != null is false
|
|
438
|
+
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
439
|
+
const features = [{ layer: { id: 'unknown' }, properties: { some_id: '1' }, geometry: { type: 'Point' } }]
|
|
440
|
+
const { eb, dp } = setup({
|
|
441
|
+
interactionModes: ['selectFeature'],
|
|
442
|
+
layers,
|
|
443
|
+
mapProvider: makeMapProvider(features)
|
|
444
|
+
})
|
|
445
|
+
act(() => eb.emit(SET_ACTIVE, { id: '1' }))
|
|
446
|
+
expect(dp).not.toHaveBeenCalled()
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it('does not search features when interactionModes excludes selectFeature', () => {
|
|
450
|
+
// Line 114: interactionModes.includes('selectFeature') → false → block skipped
|
|
451
|
+
const features = [{ layer: { id: 'roads' }, properties: { road_id: '1' }, geometry: { type: 'Point' } }]
|
|
452
|
+
const { eb, dp } = setup({
|
|
453
|
+
interactionModes: ['selectMarker'],
|
|
454
|
+
layers: [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }],
|
|
455
|
+
mapProvider: makeMapProvider(features)
|
|
456
|
+
})
|
|
457
|
+
act(() => eb.emit(SET_ACTIVE, { id: '1' }))
|
|
458
|
+
expect(dp).not.toHaveBeenCalled()
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
it('does not search features when layers is empty', () => {
|
|
462
|
+
// Line 114: layers.length > 0 → false → block skipped
|
|
463
|
+
const { eb, dp } = setup({ interactionModes: ['selectFeature'], layers: [] })
|
|
464
|
+
act(() => eb.emit(SET_ACTIVE, { id: '1' }))
|
|
465
|
+
expect(dp).not.toHaveBeenCalled()
|
|
466
|
+
})
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
// ─── useMapItemList — confirm: lifecycle and guards ──────────────────────
|
|
470
|
+
|
|
471
|
+
describe('useMapItemList — confirm: lifecycle and guards', () => {
|
|
472
|
+
it('subscribes to map:confirmfeature on mount and unsubscribes on unmount', () => {
|
|
473
|
+
const { eb, unmount } = setup()
|
|
474
|
+
expect(eb.on).toHaveBeenCalledWith(CONFIRM, expect.any(Function))
|
|
475
|
+
unmount()
|
|
476
|
+
expect(eb.off).toHaveBeenCalledWith(CONFIRM, expect.any(Function))
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
it('does nothing when no item is active', () => {
|
|
480
|
+
const { eb, dp } = setup()
|
|
481
|
+
act(() => eb.emit(CONFIRM))
|
|
482
|
+
expect(dp).not.toHaveBeenCalled()
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
it('keeps the active item after confirm so repeated confirms dispatch again', () => {
|
|
486
|
+
const markers = makeMarkers([{ id: 'm1', label: 'Marker', symbol: 'pin', isVisible: true }])
|
|
487
|
+
const { eb, dp } = setup({ interactionModes: ['selectMarker'], markers })
|
|
488
|
+
act(() => eb.emit(SET_ACTIVE, { id: 'm1' }))
|
|
489
|
+
act(() => eb.emit(CONFIRM))
|
|
490
|
+
dp.mockClear()
|
|
491
|
+
act(() => eb.emit(CONFIRM))
|
|
492
|
+
expect(dp).toHaveBeenCalledWith({ type: 'TOGGLE_SELECTED_MARKERS', payload: { markerId: 'm1', multiSelect: false } })
|
|
493
|
+
})
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
// ─── useMapItemList — confirm: dispatches ────────────────────────────────
|
|
497
|
+
|
|
498
|
+
describe('useMapItemList — confirm: marker dispatches', () => {
|
|
499
|
+
afterEach(() => { document.body.innerHTML = '' })
|
|
500
|
+
|
|
501
|
+
it('dispatches TOGGLE_SELECTED_MARKERS after activating a marker', () => {
|
|
502
|
+
const markers = makeMarkers([{ id: 'm1', label: 'Marker', symbol: 'pin', isVisible: true }])
|
|
503
|
+
const { eb, dp } = setup({ interactionModes: ['selectMarker'], markers })
|
|
504
|
+
act(() => eb.emit(SET_ACTIVE, { id: 'm1' }))
|
|
505
|
+
act(() => eb.emit(CONFIRM))
|
|
506
|
+
expect(dp).toHaveBeenCalledWith({
|
|
507
|
+
type: 'TOGGLE_SELECTED_MARKERS',
|
|
508
|
+
payload: { markerId: 'm1', multiSelect: false }
|
|
509
|
+
})
|
|
510
|
+
})
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
describe('useMapItemList — confirm: feature dispatches', () => {
|
|
514
|
+
it('dispatches TOGGLE_SELECTED_FEATURES after activating a feature', () => {
|
|
515
|
+
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
516
|
+
const features = [
|
|
517
|
+
{ layer: { id: 'roads' }, properties: { road_id: '1', road_name: 'High St' }, geometry: { type: 'Point' } }
|
|
518
|
+
]
|
|
519
|
+
const { eb, dp } = setup({
|
|
520
|
+
interactionModes: ['selectFeature'],
|
|
521
|
+
layers,
|
|
522
|
+
mapProvider: makeMapProvider(features)
|
|
523
|
+
})
|
|
524
|
+
act(() => eb.emit(SET_ACTIVE, { id: '1' }))
|
|
525
|
+
act(() => eb.emit(CONFIRM))
|
|
526
|
+
expect(dp).toHaveBeenCalledWith({
|
|
527
|
+
type: 'TOGGLE_SELECTED_FEATURES',
|
|
528
|
+
payload: {
|
|
529
|
+
featureId: '1',
|
|
530
|
+
multiSelect: false,
|
|
531
|
+
replaceAll: true,
|
|
532
|
+
layerId: 'roads',
|
|
533
|
+
idProperty: 'road_id',
|
|
534
|
+
properties: { road_id: '1', road_name: 'High St' },
|
|
535
|
+
geometry: { type: 'Point' }
|
|
536
|
+
}
|
|
537
|
+
})
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
it('dispatches with multiSelect: true and replaceAll: false when plugin multiSelect is enabled', () => {
|
|
541
|
+
const layers = [{ layerId: 'roads', idProperty: 'road_id', labelProperty: 'road_name' }]
|
|
542
|
+
const features = [
|
|
543
|
+
{ layer: { id: 'roads' }, properties: { road_id: '1', road_name: 'High St' }, geometry: { type: 'Point' } }
|
|
544
|
+
]
|
|
545
|
+
const { eb, dp } = setup({
|
|
546
|
+
interactionModes: ['selectFeature'],
|
|
547
|
+
layers,
|
|
548
|
+
mapProvider: makeMapProvider(features),
|
|
549
|
+
multiSelect: true
|
|
550
|
+
})
|
|
551
|
+
act(() => eb.emit(SET_ACTIVE, { id: '1' }))
|
|
552
|
+
act(() => eb.emit(CONFIRM))
|
|
553
|
+
expect(dp).toHaveBeenCalledWith({
|
|
554
|
+
type: 'TOGGLE_SELECTED_FEATURES',
|
|
555
|
+
payload: expect.objectContaining({ multiSelect: true, replaceAll: false })
|
|
556
|
+
})
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
it('preserves raw numeric featureId in TOGGLE_SELECTED_FEATURES payload', () => {
|
|
560
|
+
const layers = [{ layerId: 'hedges', idProperty: 'id' }]
|
|
561
|
+
const features = [
|
|
562
|
+
{ layer: { id: 'hedges' }, properties: { id: 27665979 }, geometry: { type: 'LineString' } }
|
|
563
|
+
]
|
|
564
|
+
const { eb, dp } = setup({
|
|
565
|
+
interactionModes: ['selectFeature'],
|
|
566
|
+
layers,
|
|
567
|
+
mapProvider: makeMapProvider(features)
|
|
568
|
+
})
|
|
569
|
+
act(() => eb.emit(SET_ACTIVE, { id: '27665979' }))
|
|
570
|
+
act(() => eb.emit(CONFIRM))
|
|
571
|
+
expect(dp).toHaveBeenCalledWith({
|
|
572
|
+
type: 'TOGGLE_SELECTED_FEATURES',
|
|
573
|
+
payload: {
|
|
574
|
+
featureId: 27665979,
|
|
575
|
+
multiSelect: false,
|
|
576
|
+
replaceAll: true,
|
|
577
|
+
layerId: 'hedges',
|
|
578
|
+
idProperty: 'id',
|
|
579
|
+
properties: { id: 27665979 },
|
|
580
|
+
geometry: { type: 'LineString' }
|
|
581
|
+
}
|
|
582
|
+
})
|
|
583
|
+
})
|
|
584
|
+
})
|
|
@@ -10,6 +10,8 @@ import { unselectFeature } from './api/unselectFeature.js'
|
|
|
10
10
|
import { selectMarker } from './api/selectMarker.js'
|
|
11
11
|
import { unselectMarker } from './api/unselectMarker.js'
|
|
12
12
|
|
|
13
|
+
const SELECT_FEATURES_GROUP = 'Select features'
|
|
14
|
+
|
|
13
15
|
export const manifest = {
|
|
14
16
|
InitComponent: InteractInit,
|
|
15
17
|
|
|
@@ -72,12 +74,41 @@ export const manifest = {
|
|
|
72
74
|
}
|
|
73
75
|
}],
|
|
74
76
|
|
|
75
|
-
keyboardShortcuts: [
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
keyboardShortcuts: [
|
|
78
|
+
{
|
|
79
|
+
id: 'clickAtTarget',
|
|
80
|
+
title: 'Click at target',
|
|
81
|
+
command: '<kbd>Enter</kbd>'
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'selectFeatures',
|
|
85
|
+
group: SELECT_FEATURES_GROUP,
|
|
86
|
+
context: 'listbox',
|
|
87
|
+
title: 'Select features',
|
|
88
|
+
command: '<kbd>Tab</kbd>'
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'navigateFeatures',
|
|
92
|
+
group: SELECT_FEATURES_GROUP,
|
|
93
|
+
context: 'listbox',
|
|
94
|
+
title: 'Navigate features',
|
|
95
|
+
command: '<kbd>↑</kbd> or <kbd>↓</kbd>'
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'selectFeature',
|
|
99
|
+
group: SELECT_FEATURES_GROUP,
|
|
100
|
+
context: 'listbox',
|
|
101
|
+
title: 'Select a feature',
|
|
102
|
+
command: '<kbd>Enter</kbd> or <kbd>Space</kbd>'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'dismissHintOrReturn',
|
|
106
|
+
group: SELECT_FEATURES_GROUP,
|
|
107
|
+
context: 'listbox',
|
|
108
|
+
title: 'Dismiss hint / return to map',
|
|
109
|
+
command: '<kbd>Escape</kbd>'
|
|
110
|
+
}
|
|
111
|
+
],
|
|
81
112
|
|
|
82
113
|
icons: [{
|
|
83
114
|
id: 'select',
|