@defra/interactive-map 0.0.43-alpha → 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/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.md +16 -8
- package/docs/plugins/draw.md +1 -1
- package/package.json +1 -1
- 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/draw-ol/dist/esm/im-draw-ol-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/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/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 +31 -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 +8 -2
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +35 -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 +31 -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 +32 -57
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +14 -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/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 +5 -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 +4 -4
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +7 -7
- package/plugins/interact/src/hooks/{useMapItemList.js → useSpatialList.js} +87 -52
- package/plugins/interact/src/hooks/{useMapItemList.test.js → useSpatialList.test.js} +152 -71
- package/plugins/interact/src/manifest.js +12 -1
- package/plugins/interact/src/manifest.test.js +20 -0
- package/plugins/interact/src/utils/spatial.js +1 -1
- package/providers/beta/openlayers/src/utils/queryFeatures.js +1 -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/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/src/App/components/CrossHair/CrossHair.jsx +10 -4
- package/src/App/components/CrossHair/CrossHair.module.scss +16 -12
- package/src/App/components/CrossHair/CrossHair.test.jsx +9 -4
- package/src/App/components/Hints/Hints.jsx +3 -3
- package/src/App/components/Hints/Hints.test.jsx +3 -3
- package/src/App/components/MapButton/MapButton.module.scss +1 -2
- 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.test.jsx +2 -2
- 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} +3 -3
- package/src/App/components/Viewport/{Features.test.jsx → SpatialList.test.jsx} +60 -43
- package/src/App/components/Viewport/Viewport.jsx +20 -10
- package/src/App/components/Viewport/Viewport.test.jsx +18 -2
- package/src/App/hooks/useCrossHairAPI.js +5 -5
- package/src/App/hooks/useCrossHairAPI.test.js +5 -5
- package/src/App/hooks/useHintsAPI.js +5 -5
- package/src/App/hooks/useSpatialListFocus.js +296 -0
- package/src/App/hooks/{useFeatureFocus.test.js → useSpatialListFocus.test.js} +318 -163
- 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/registry/spatialListRegistry.js +104 -0
- package/src/App/registry/spatialListRegistry.test.js +218 -0
- package/src/App/store/AppProvider.jsx +3 -2
- package/src/config/appConfig.js +14 -14
- 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 +11 -9
- package/src/utils/findNearestItemInDirection.js +29 -0
- package/src/utils/findNearestItemInDirection.test.js +67 -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/src/App/components/Viewport/Features.jsx +0 -49
- package/src/App/hooks/useFeatureFocus.js +0 -257
- package/src/App/hooks/useFeatureItems.js +0 -39
- package/src/App/hooks/useFeatureItems.test.js +0 -155
|
@@ -10,7 +10,7 @@ import { useMediaQueryDispatch } from '../hooks/useMediaQueryDispatch.js'
|
|
|
10
10
|
export const AppContext = createContext(null)
|
|
11
11
|
|
|
12
12
|
export const AppProvider = ({ options, children }) => {
|
|
13
|
-
const { pluginRegistry, buttonRegistry, panelRegistry, controlRegistry, keyboardShortcutRegistry, eventBus, breakpointDetector } = options
|
|
13
|
+
const { pluginRegistry, buttonRegistry, panelRegistry, controlRegistry, keyboardShortcutRegistry, spatialListRegistry, eventBus, breakpointDetector } = options
|
|
14
14
|
|
|
15
15
|
const layoutRefs = {
|
|
16
16
|
appContainerRef: useRef(null),
|
|
@@ -96,7 +96,8 @@ export const AppProvider = ({ options, children }) => {
|
|
|
96
96
|
buttonRegistry,
|
|
97
97
|
panelRegistry,
|
|
98
98
|
controlRegistry,
|
|
99
|
-
keyboardShortcutRegistry
|
|
99
|
+
keyboardShortcutRegistry,
|
|
100
|
+
spatialListRegistry
|
|
100
101
|
}), [state, dispatch])
|
|
101
102
|
|
|
102
103
|
return (
|
package/src/config/appConfig.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { KeyboardHelp } from '../App/components/KeyboardHelp/KeyboardHelp.jsx'
|
|
2
|
-
import {
|
|
2
|
+
import { MapControls } from '../App/components/MapControls/MapControls.jsx'
|
|
3
3
|
|
|
4
4
|
const keyboardBasePanelSlots = {
|
|
5
5
|
slot: 'middle',
|
|
@@ -13,7 +13,7 @@ const buttonSlots = {
|
|
|
13
13
|
showLabel: false
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const
|
|
16
|
+
const mapControlsSlot = {
|
|
17
17
|
slot: 'right-bottom'
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -79,7 +79,7 @@ export const defaultAppConfig = {
|
|
|
79
79
|
iconId: 'plus',
|
|
80
80
|
keepFocus: true,
|
|
81
81
|
onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomIn(appConfig.zoomDelta),
|
|
82
|
-
excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appConfig.
|
|
82
|
+
excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appConfig.enableMapControls || appState.interfaceType === 'touch',
|
|
83
83
|
enableWhen: ({ mapState }) => !mapState.isAtMaxZoom,
|
|
84
84
|
mobile: buttonSlots,
|
|
85
85
|
tablet: buttonSlots,
|
|
@@ -91,23 +91,23 @@ export const defaultAppConfig = {
|
|
|
91
91
|
iconId: 'minus',
|
|
92
92
|
keepFocus: true,
|
|
93
93
|
onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomOut(appConfig.zoomDelta),
|
|
94
|
-
excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appConfig.
|
|
94
|
+
excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appConfig.enableMapControls || appState.interfaceType === 'touch',
|
|
95
95
|
enableWhen: ({ mapState }) => !mapState.isAtMinZoom,
|
|
96
96
|
mobile: buttonSlots,
|
|
97
97
|
tablet: buttonSlots,
|
|
98
98
|
desktop: buttonSlots
|
|
99
99
|
}, {
|
|
100
|
-
id: '
|
|
100
|
+
id: 'mapControls',
|
|
101
101
|
label: 'Map controls',
|
|
102
102
|
iconId: 'move',
|
|
103
103
|
keepFocus: true,
|
|
104
104
|
isExpanded: false,
|
|
105
|
-
ariaControls: ({ appConfig }) => `${appConfig.id}-
|
|
105
|
+
ariaControls: ({ appConfig }) => `${appConfig.id}-map-controls-content`,
|
|
106
106
|
onClick: (_e, { appState }) => appState.dispatch({
|
|
107
107
|
type: 'TOGGLE_BUTTON_EXPANDED',
|
|
108
|
-
payload: { id: '
|
|
108
|
+
payload: { id: 'mapControls', isExpanded: !appState.expandedButtons.has('mapControls') }
|
|
109
109
|
}),
|
|
110
|
-
excludeWhen: ({ appConfig }) => !appConfig.
|
|
110
|
+
excludeWhen: ({ appConfig }) => !appConfig.enableMapControls,
|
|
111
111
|
mobile: buttonSlots,
|
|
112
112
|
tablet: buttonSlots,
|
|
113
113
|
desktop: buttonSlots
|
|
@@ -131,13 +131,13 @@ export const defaultAppConfig = {
|
|
|
131
131
|
}],
|
|
132
132
|
|
|
133
133
|
controls: [{
|
|
134
|
-
id: '
|
|
134
|
+
id: 'mapControls',
|
|
135
135
|
label: 'Move and zoom',
|
|
136
|
-
excludeWhen: ({ appConfig }) => !appConfig.
|
|
137
|
-
mobile:
|
|
138
|
-
tablet:
|
|
139
|
-
desktop:
|
|
140
|
-
render:
|
|
136
|
+
excludeWhen: ({ appConfig }) => !appConfig.enableMapControls,
|
|
137
|
+
mobile: mapControlsSlot,
|
|
138
|
+
tablet: mapControlsSlot,
|
|
139
|
+
desktop: mapControlsSlot,
|
|
140
|
+
render: MapControls
|
|
141
141
|
}],
|
|
142
142
|
|
|
143
143
|
icons: [{
|
|
@@ -19,7 +19,7 @@ describe('defaultAppConfig', () => {
|
|
|
19
19
|
const journeyContinueBtn = buttons.find(b => b.id === 'journeyContinue')
|
|
20
20
|
const zoomInBtn = buttons.find(b => b.id === 'zoomIn')
|
|
21
21
|
const zoomOutBtn = buttons.find(b => b.id === 'zoomOut')
|
|
22
|
-
const
|
|
22
|
+
const mapControlsBtn = buttons.find(b => b.id === 'mapControls')
|
|
23
23
|
|
|
24
24
|
// --- UI RENDER TESTS ---
|
|
25
25
|
it('renders KeyboardHelp panel', () => {
|
|
@@ -144,21 +144,21 @@ describe('defaultAppConfig', () => {
|
|
|
144
144
|
appState: { interfaceType: 'mouse' }
|
|
145
145
|
})).toBe(true)
|
|
146
146
|
|
|
147
|
-
// Branch B: Second part of OR is true (
|
|
147
|
+
// Branch B: Second part of OR is true (enableMapControls)
|
|
148
148
|
expect(btn.excludeWhen({
|
|
149
|
-
appConfig: { enableZoomControls: true,
|
|
149
|
+
appConfig: { enableZoomControls: true, enableMapControls: true },
|
|
150
150
|
appState: { interfaceType: 'mouse' }
|
|
151
151
|
})).toBe(true)
|
|
152
152
|
|
|
153
153
|
// Branch C: Third part of OR is true (interfaceType === 'touch')
|
|
154
154
|
expect(btn.excludeWhen({
|
|
155
|
-
appConfig: { enableZoomControls: true,
|
|
155
|
+
appConfig: { enableZoomControls: true, enableMapControls: false },
|
|
156
156
|
appState: { interfaceType: 'touch' }
|
|
157
157
|
})).toBe(true)
|
|
158
158
|
|
|
159
159
|
// Branch D: All parts are false (Result: false)
|
|
160
160
|
expect(btn.excludeWhen({
|
|
161
|
-
appConfig: { enableZoomControls: true,
|
|
161
|
+
appConfig: { enableZoomControls: true, enableMapControls: false },
|
|
162
162
|
appState: { interfaceType: 'mouse' }
|
|
163
163
|
})).toBe(false)
|
|
164
164
|
})
|
|
@@ -183,37 +183,37 @@ describe('defaultAppConfig', () => {
|
|
|
183
183
|
})
|
|
184
184
|
|
|
185
185
|
// --- MOVE CONTROLS TOGGLE BUTTON ---
|
|
186
|
-
it('covers all branches of
|
|
187
|
-
expect(
|
|
188
|
-
expect(
|
|
186
|
+
it('covers all branches of mapControls excludeWhen', () => {
|
|
187
|
+
expect(mapControlsBtn.excludeWhen({ appConfig: { enableMapControls: false } })).toBe(true)
|
|
188
|
+
expect(mapControlsBtn.excludeWhen({ appConfig: { enableMapControls: true } })).toBe(false)
|
|
189
189
|
})
|
|
190
190
|
|
|
191
|
-
it('
|
|
192
|
-
expect(
|
|
191
|
+
it('mapControls ariaControls resolves the control element id', () => {
|
|
192
|
+
expect(mapControlsBtn.ariaControls({ appConfig: { id: 'im' } })).toBe('im-map-controls-content')
|
|
193
193
|
})
|
|
194
194
|
|
|
195
|
-
it('
|
|
195
|
+
it('mapControls onClick toggles TOGGLE_BUTTON_EXPANDED based on current state', () => {
|
|
196
196
|
const dispatch = jest.fn()
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
mapControlsBtn.onClick({}, { appState: { dispatch, expandedButtons: new Set() } })
|
|
199
199
|
expect(dispatch).toHaveBeenCalledWith({
|
|
200
200
|
type: 'TOGGLE_BUTTON_EXPANDED',
|
|
201
|
-
payload: { id: '
|
|
201
|
+
payload: { id: 'mapControls', isExpanded: true }
|
|
202
202
|
})
|
|
203
203
|
|
|
204
204
|
dispatch.mockClear()
|
|
205
|
-
|
|
205
|
+
mapControlsBtn.onClick({}, { appState: { dispatch, expandedButtons: new Set(['mapControls']) } })
|
|
206
206
|
expect(dispatch).toHaveBeenCalledWith({
|
|
207
207
|
type: 'TOGGLE_BUTTON_EXPANDED',
|
|
208
|
-
payload: { id: '
|
|
208
|
+
payload: { id: 'mapControls', isExpanded: false }
|
|
209
209
|
})
|
|
210
210
|
})
|
|
211
211
|
|
|
212
212
|
// --- MOVE CONTROLS ---
|
|
213
|
-
it('covers all branches of
|
|
214
|
-
const control = defaultAppConfig.controls.find(c => c.id === '
|
|
215
|
-
expect(control.excludeWhen({ appConfig: {
|
|
216
|
-
expect(control.excludeWhen({ appConfig: {
|
|
213
|
+
it('covers all branches of mapControls control excludeWhen', () => {
|
|
214
|
+
const control = defaultAppConfig.controls.find(c => c.id === 'mapControls')
|
|
215
|
+
expect(control.excludeWhen({ appConfig: { enableMapControls: false } })).toBe(true)
|
|
216
|
+
expect(control.excludeWhen({ appConfig: { enableMapControls: true } })).toBe(false)
|
|
217
217
|
})
|
|
218
218
|
|
|
219
219
|
// --- SUPPLEMENTARY CONFIGS ---
|
package/src/config/defaults.js
CHANGED
|
@@ -16,7 +16,7 @@ const defaults = {
|
|
|
16
16
|
containerHeight: '600px',
|
|
17
17
|
deviceNotSupportedText: 'Your device is not supported. A map is available with a more up-to-date browser or device.',
|
|
18
18
|
enableFullscreen: false,
|
|
19
|
-
|
|
19
|
+
enableMapControls: true,
|
|
20
20
|
enableZoomControls: true,
|
|
21
21
|
genericErrorText: 'There was a problem loading the map. Please try again later.',
|
|
22
22
|
hasExitButton: false,
|
|
@@ -32,7 +32,7 @@ const defaults = {
|
|
|
32
32
|
urlPosition: 'sync',
|
|
33
33
|
maxMobileWidth: 640,
|
|
34
34
|
minDesktopWidth: 835,
|
|
35
|
-
|
|
35
|
+
mapControlsHintText: 'A "Move and zoom controls" button is also available, opening further controls for moving and zooming.',
|
|
36
36
|
nudgePanDelta: 5,
|
|
37
37
|
nudgeZoomDelta: 0.1,
|
|
38
38
|
panDelta: 100,
|
package/src/config/events.js
CHANGED
|
@@ -145,14 +145,14 @@ export const EVENTS = {
|
|
|
145
145
|
MAP_SET_STYLE: 'map:setstyle',
|
|
146
146
|
/** @internal Set map size. Payload: { width, height } */
|
|
147
147
|
MAP_SET_SIZE: 'map:setsize',
|
|
148
|
-
/** @internal Set the accessible
|
|
149
|
-
|
|
150
|
-
/** @internal Suppress/restore the accessible
|
|
151
|
-
|
|
152
|
-
/** @internal Set the active
|
|
153
|
-
|
|
154
|
-
/** @internal Select the active
|
|
155
|
-
|
|
148
|
+
/** @internal Set the accessible spatial list's items. Payload: { items: { id: string, label: string }[], multiselectable: boolean, label?: string } */
|
|
149
|
+
MAP_SET_SPATIAL_LIST: 'map:setspatiallist',
|
|
150
|
+
/** @internal Suppress/restore the accessible spatial list. Payload: { suppressed: boolean } */
|
|
151
|
+
MAP_SET_SPATIAL_LIST_SUPPRESSED: 'map:setspatiallistsuppressed',
|
|
152
|
+
/** @internal Set the active item in the accessible spatial list (roving tabindex position). Payload: { id: string | null } */
|
|
153
|
+
MAP_SET_ACTIVE_ITEM: 'map:setactiveitem',
|
|
154
|
+
/** @internal Select the active spatial list item as the real selection (Enter key). Payload: none */
|
|
155
|
+
MAP_SELECT_ITEM: 'map:selectitem',
|
|
156
156
|
/** @internal Set pixel ratio. Payload: pixelRatio */
|
|
157
157
|
MAP_SET_PIXEL_RATIO: 'map:setpixelratio',
|
|
158
158
|
/** @internal Fit the map to a bounding box. Payload: [west, south, east, north] */
|
package/src/scss/main.scss
CHANGED
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
@use '../App/components/Hints/Hints.module';
|
|
30
30
|
@use '../App/components/CrossHair/CrossHair.module';
|
|
31
31
|
@use '../App/components/Markers/Markers.module';
|
|
32
|
-
@use '../App/components/Viewport/
|
|
32
|
+
@use '../App/components/Viewport/SpatialList.module';
|
|
33
33
|
@use '../App/components/Actions/Actions.module';
|
|
34
|
-
@use '../App/components/
|
|
34
|
+
@use '../App/components/MapControls/MapControls.module';
|
|
35
35
|
|
|
36
36
|
// Components that dont have a corresponding React Component
|
|
37
37
|
@use 'components';
|
package/src/test-utils.js
CHANGED
|
@@ -37,6 +37,14 @@ export const createMockRegistries = (overrides = {}) => {
|
|
|
37
37
|
registerKeyboardShortcut: jest.fn(),
|
|
38
38
|
setProviderSupportedShortcuts: jest.fn(),
|
|
39
39
|
getKeyboardShortcuts: jest.fn(() => [])
|
|
40
|
+
},
|
|
41
|
+
spatialListRegistry: {
|
|
42
|
+
registerItemProvider: jest.fn(),
|
|
43
|
+
unregisterItemProvider: jest.fn(),
|
|
44
|
+
claimExclusive: jest.fn(),
|
|
45
|
+
releaseExclusive: jest.fn(),
|
|
46
|
+
notifyItemsChanged: jest.fn(),
|
|
47
|
+
clear: jest.fn()
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
}
|
package/src/types.js
CHANGED
|
@@ -765,16 +765,18 @@
|
|
|
765
765
|
* @property {boolean} [enableFullscreen=false]
|
|
766
766
|
* Whether a toggle fullscreen button is displayed.
|
|
767
767
|
*
|
|
768
|
-
* @property {boolean} [
|
|
769
|
-
* Whether the
|
|
770
|
-
* zoom, and
|
|
771
|
-
*
|
|
772
|
-
*
|
|
773
|
-
*
|
|
768
|
+
* @property {boolean} [enableMapControls=true]
|
|
769
|
+
* Whether the map controls are displayed — a button that reveals on-screen move,
|
|
770
|
+
* zoom, and precision buttons, plus a target point, typically used to place or
|
|
771
|
+
* select features on the map without a drag gesture. This gives a non-dragging way
|
|
772
|
+
* to operate the map, for anyone who can't perform a drag/pinch gesture (e.g.
|
|
773
|
+
* switch access users) and for voice interfaces such as Voice Control, which can
|
|
774
|
+
* trigger a button click but not a drag. When enabled, enableZoomControls' buttons
|
|
775
|
+
* are hidden to avoid duplication.
|
|
774
776
|
*
|
|
775
777
|
* @property {boolean} [enableZoomControls=true]
|
|
776
778
|
* Whether zoom control buttons are displayed. Not displayed when the interface
|
|
777
|
-
* type is 'touch', or when
|
|
779
|
+
* type is 'touch', or when enableMapControls is enabled.
|
|
778
780
|
*
|
|
779
781
|
* @property {[number, number, number, number]} [extent]
|
|
780
782
|
* Initial extent [minX, minY, maxX, maxY]. Equivalent to bounds; use whichever matches your map provider's terminology.
|
|
@@ -806,8 +808,8 @@
|
|
|
806
808
|
* @property {string} [mapLabel='Interactive map']
|
|
807
809
|
* Accessible label for the map, announced by screen readers. Also prefixed onto mapHintText, so give each map on a page a distinct label.
|
|
808
810
|
*
|
|
809
|
-
* @property {string} [
|
|
810
|
-
* Visually hidden text describing the
|
|
811
|
+
* @property {string} [mapControlsHintText]
|
|
812
|
+
* Visually hidden text describing the map controls button, appended to keyboardHintText when enableMapControls is true.
|
|
811
813
|
*
|
|
812
814
|
* @property {MapProviderDescriptor} [mapProvider]
|
|
813
815
|
* A factory function that returns a map provider instance (e.g. maplibreProvider()).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { spatialNavigate } from './spatialNavigate.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Item-list-shaped counterpart to spatialNavigate's raw pixel-array contract: given a list of
|
|
5
|
+
* items each carrying screen coordinates, finds the id of whichever one is spatially nearest
|
|
6
|
+
* to the current item in the given direction. Used to move roving-tabindex focus in a listbox
|
|
7
|
+
* spatially (Alt+Arrow) rather than sequentially (plain Arrow/Home/End) — the same items list
|
|
8
|
+
* already built for sequential navigation works here unchanged, whether it holds map features
|
|
9
|
+
* or (in future) something else entirely, as long as each item carries x/y.
|
|
10
|
+
*
|
|
11
|
+
* Items missing x/y are skipped — they have no screen position to navigate by. If none of the
|
|
12
|
+
* items have coordinates, or the list is empty, currentId is returned unchanged.
|
|
13
|
+
*
|
|
14
|
+
* @param {Array<{id: string, x?: number, y?: number}>} items
|
|
15
|
+
* @param {string|null} currentId
|
|
16
|
+
* @param {'ArrowUp'|'ArrowDown'|'ArrowLeft'|'ArrowRight'} direction
|
|
17
|
+
* @returns {string|null} The id to move to — currentId unchanged if nothing qualifies.
|
|
18
|
+
*/
|
|
19
|
+
export function findNearestItemInDirection (items, currentId, direction) {
|
|
20
|
+
const positioned = items.filter(item => item.x != null && item.y != null)
|
|
21
|
+
if (!positioned.length) {
|
|
22
|
+
return currentId
|
|
23
|
+
}
|
|
24
|
+
const pixels = positioned.map(item => [item.x, item.y])
|
|
25
|
+
const currentIndex = positioned.findIndex(item => item.id === currentId)
|
|
26
|
+
const start = currentIndex === -1 ? pixels[0] : pixels[currentIndex]
|
|
27
|
+
const nextIndex = spatialNavigate(start, pixels, direction)
|
|
28
|
+
return positioned[nextIndex]?.id ?? currentId
|
|
29
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { findNearestItemInDirection } from './findNearestItemInDirection.js'
|
|
2
|
+
|
|
3
|
+
describe('findNearestItemInDirection', () => {
|
|
4
|
+
const items = [
|
|
5
|
+
{ id: 'center', x: 0, y: 0 },
|
|
6
|
+
{ id: 'up', x: 0, y: -10 },
|
|
7
|
+
{ id: 'down', x: 0, y: 10 },
|
|
8
|
+
{ id: 'left', x: -10, y: 0 },
|
|
9
|
+
{ id: 'right', x: 10, y: 0 }
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
test.each([
|
|
13
|
+
['ArrowUp', 'up'],
|
|
14
|
+
['ArrowDown', 'down'],
|
|
15
|
+
['ArrowLeft', 'left'],
|
|
16
|
+
['ArrowRight', 'right']
|
|
17
|
+
])('finds the nearest item to the %s of the current one', (direction, expectedId) => {
|
|
18
|
+
expect(findNearestItemInDirection(items, 'center', direction)).toBe(expectedId)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('returns the current id unchanged when nothing qualifies in that direction', () => {
|
|
22
|
+
const single = [{ id: 'only', x: 0, y: 0 }]
|
|
23
|
+
expect(findNearestItemInDirection(single, 'only', 'ArrowUp')).toBe('only')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('returns currentId unchanged when the item list is empty', () => {
|
|
27
|
+
expect(findNearestItemInDirection([], 'anything', 'ArrowUp')).toBe('anything')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('skips items with no screen coordinates', () => {
|
|
31
|
+
const withUnpositioned = [
|
|
32
|
+
{ id: 'center', x: 0, y: 0 },
|
|
33
|
+
{ id: 'no-coords' },
|
|
34
|
+
{ id: 'up', x: 0, y: -10 }
|
|
35
|
+
]
|
|
36
|
+
expect(findNearestItemInDirection(withUnpositioned, 'center', 'ArrowUp')).toBe('up')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('returns currentId unchanged when no item has coordinates at all', () => {
|
|
40
|
+
const noneWithCoords = [{ id: 'a' }, { id: 'b' }]
|
|
41
|
+
expect(findNearestItemInDirection(noneWithCoords, 'a', 'ArrowUp')).toBe('a')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('treats an unresolvable currentId as starting from the first positioned item', () => {
|
|
45
|
+
expect(findNearestItemInDirection(items, 'not-in-list', 'ArrowRight')).toBe('right')
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// spatialNavigate's own contract guarantees a valid index in practice (see its own tests), so
|
|
50
|
+
// this can't be reached through findNearestItemInDirection's real dependency — mocked here,
|
|
51
|
+
// matching labels.test.js's identical fallback test for navigateToNextLabel, purely to prove
|
|
52
|
+
// the defensive `?? currentId` itself degrades gracefully rather than crashing if that
|
|
53
|
+
// guarantee were ever weakened.
|
|
54
|
+
describe('findNearestItemInDirection — defensive fallback', () => {
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
jest.dontMock('./spatialNavigate.js')
|
|
57
|
+
jest.resetModules()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('falls back to currentId if spatialNavigate ever returns an out-of-range index', () => {
|
|
61
|
+
jest.resetModules()
|
|
62
|
+
jest.doMock('./spatialNavigate.js', () => ({ spatialNavigate: () => -1 }))
|
|
63
|
+
const { findNearestItemInDirection: fn } = require('./findNearestItemInDirection.js')
|
|
64
|
+
const items = [{ id: 'center', x: 0, y: 0 }, { id: 'up', x: 0, y: -10 }]
|
|
65
|
+
expect(fn(items, 'center', 'ArrowUp')).toBe('center')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Alt+<key> keyup combos useKeyboardShortcuts.js binds globally (see keyboardMappings.js's
|
|
2
|
+
// `keyup` section) — a locally-focused context using the same combo must shadow it, or both
|
|
3
|
+
// fire. Single source of truth so a new global mapping is a visible prompt to check for that.
|
|
4
|
+
export const GLOBAL_ALT_KEYS = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Enter'])
|
|
5
|
+
|
|
6
|
+
// Call unconditionally from a local keyup handler while its scope is active — it only acts
|
|
7
|
+
// when the key is actually shadowed, so it's safe to call regardless of which key was pressed.
|
|
8
|
+
export const stopIfGlobalAltKey = (event) => {
|
|
9
|
+
if (event.altKey && GLOBAL_ALT_KEYS.has(event.key)) {
|
|
10
|
+
event.stopPropagation()
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { GLOBAL_ALT_KEYS, stopIfGlobalAltKey } from './globalAltShortcuts.js'
|
|
2
|
+
|
|
3
|
+
describe('GLOBAL_ALT_KEYS', () => {
|
|
4
|
+
test('lists the arrow keys and Enter — the combos keyboardMappings.js binds globally under Alt', () => {
|
|
5
|
+
expect([...GLOBAL_ALT_KEYS].sort()).toEqual(['ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'Enter'].sort())
|
|
6
|
+
})
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
describe('stopIfGlobalAltKey', () => {
|
|
10
|
+
test.each(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Enter'])('stops propagation for Alt+%s', (key) => {
|
|
11
|
+
const event = { altKey: true, key, stopPropagation: jest.fn() }
|
|
12
|
+
stopIfGlobalAltKey(event)
|
|
13
|
+
expect(event.stopPropagation).toHaveBeenCalled()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('leaves the same keys alone without Alt held', () => {
|
|
17
|
+
const event = { altKey: false, key: 'ArrowUp', stopPropagation: jest.fn() }
|
|
18
|
+
stopIfGlobalAltKey(event)
|
|
19
|
+
expect(event.stopPropagation).not.toHaveBeenCalled()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('leaves an Alt+<other key> alone — nothing global uses this combo', () => {
|
|
23
|
+
const event = { altKey: true, key: 'i', stopPropagation: jest.fn() }
|
|
24
|
+
stopIfGlobalAltKey(event)
|
|
25
|
+
expect(event.stopPropagation).not.toHaveBeenCalled()
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Given a starting screen pixel and a set of candidate pixels, finds the index of the
|
|
3
|
+
* candidate that's nearest to `start` in the given cardinal direction — the shared primitive
|
|
4
|
+
* behind every directional "move to the next spatially-adjacent thing" interaction in this
|
|
5
|
+
* codebase (map label navigation, draw/edit vertex navigation, and the map features listbox).
|
|
6
|
+
*
|
|
7
|
+
* Candidates are first filtered to a ~90° cone centred on the pressed direction (whichever
|
|
8
|
+
* axis dominates the offset decides up/down vs left/right), then the nearest of those by
|
|
9
|
+
* Euclidean distance wins. If nothing falls in that cone, `start`'s own index in `pixels` is
|
|
10
|
+
* returned (a no-op move) rather than jumping somewhere the user didn't ask for.
|
|
11
|
+
*
|
|
12
|
+
* Previously duplicated (with subtly different tie-breaking/equality logic) between
|
|
13
|
+
* providers/maplibre/src/utils/spatial.js and plugins/draw/src/utils/spatial.js —
|
|
14
|
+
* consolidated here as the one canonical implementation both now import.
|
|
15
|
+
*
|
|
16
|
+
* @param {[number, number]} start - Current pixel [x, y].
|
|
17
|
+
* @param {Array<[number, number]>} pixels - All candidate pixels, including `start` itself.
|
|
18
|
+
* @param {'ArrowUp'|'ArrowDown'|'ArrowLeft'|'ArrowRight'|undefined} direction - undefined matches every candidate (nearest overall, any direction).
|
|
19
|
+
* @returns {number} Index into `pixels` of the chosen candidate.
|
|
20
|
+
*/
|
|
21
|
+
export const spatialNavigate = (start, pixels, direction) => {
|
|
22
|
+
const isSamePixel = (a, b) => a[0] === b[0] && a[1] === b[1]
|
|
23
|
+
|
|
24
|
+
const quadrant = pixels.filter((p) => {
|
|
25
|
+
const offsetX = Math.abs(p[0] - start[0])
|
|
26
|
+
const offsetY = Math.abs(p[1] - start[1])
|
|
27
|
+
let isQuadrant = false
|
|
28
|
+
if (direction === 'ArrowUp') {
|
|
29
|
+
isQuadrant = p[1] <= start[1] && offsetY >= offsetX
|
|
30
|
+
} else if (direction === 'ArrowDown') {
|
|
31
|
+
isQuadrant = p[1] > start[1] && offsetY >= offsetX
|
|
32
|
+
} else if (direction === 'ArrowLeft') {
|
|
33
|
+
isQuadrant = p[0] <= start[0] && offsetY < offsetX
|
|
34
|
+
} else if (direction === 'ArrowRight') {
|
|
35
|
+
isQuadrant = p[0] > start[0] && offsetY < offsetX
|
|
36
|
+
} else {
|
|
37
|
+
isQuadrant = true
|
|
38
|
+
}
|
|
39
|
+
return isQuadrant && !isSamePixel(p, start)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
if (!quadrant.length) {
|
|
43
|
+
quadrant.push(start)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const distance = (p) => Math.hypot(start[0] - p[0], start[1] - p[1])
|
|
47
|
+
const closest = quadrant.reduce((best, p) => (distance(p) < distance(best) ? p : best), quadrant[0])
|
|
48
|
+
return pixels.findIndex((p) => isSamePixel(p, closest))
|
|
49
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { spatialNavigate } from './spatialNavigate.js'
|
|
2
|
+
|
|
3
|
+
describe('spatialNavigate', () => {
|
|
4
|
+
const start = [0, 0]
|
|
5
|
+
const pixels = [[0, 0], [0, -10], [0, 10], [-10, 0], [10, 0]]
|
|
6
|
+
|
|
7
|
+
test.each([
|
|
8
|
+
['ArrowUp', 1],
|
|
9
|
+
['ArrowDown', 2],
|
|
10
|
+
['ArrowLeft', 3],
|
|
11
|
+
['ArrowRight', 4]
|
|
12
|
+
])('finds the nearest pixel for %s', (direction, expectedIndex) => {
|
|
13
|
+
expect(spatialNavigate(start, pixels, direction)).toBe(expectedIndex)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('considers all pixels for an unrecognised direction', () => {
|
|
17
|
+
expect(spatialNavigate(start, pixels, 'Tab')).toBe(1)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('considers all pixels when no direction is given (nearest overall)', () => {
|
|
21
|
+
expect(spatialNavigate(start, pixels, undefined)).toBe(1)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('falls back to the start point when no pixel is in the quadrant', () => {
|
|
25
|
+
expect(spatialNavigate(start, [[0, 0]], 'ArrowUp')).toBe(0)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('picks the closer of two candidates in the same direction', () => {
|
|
29
|
+
const pixels = [[0, 0], [10, 0], [2, 0]]
|
|
30
|
+
expect(spatialNavigate(start, pixels, 'ArrowRight')).toBe(2)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('keeps the first-seen closer candidate rather than a later farther one', () => {
|
|
34
|
+
const pixels = [[0, 0], [2, 0], [10, 0]]
|
|
35
|
+
expect(spatialNavigate(start, pixels, 'ArrowRight')).toBe(1)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('a diagonal candidate with dx > dy counts as ArrowRight, not ArrowUp/Down', () => {
|
|
39
|
+
const pixels = [[0, 0], [3, 1], [1, 0]]
|
|
40
|
+
expect(spatialNavigate(start, pixels, 'ArrowRight')).toBe(2)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import React, { forwardRef } from 'react'
|
|
2
|
-
import { useConfig } from '../../store/configContext.js'
|
|
3
|
-
|
|
4
|
-
export const Features = forwardRef(({ activeFeatureId, tabbableId, selectedIds = [], multiselectable = false, items = [], onFocus, onBlur, onSelectItem }, ref) => {
|
|
5
|
-
const { id } = useConfig()
|
|
6
|
-
const hasItems = items.length > 0
|
|
7
|
-
// Roving tabindex: exactly one option is a Tab stop at a time (the focused item while the
|
|
8
|
-
// list has real focus, otherwise the resting entry position) — see useFeatureFocus.js.
|
|
9
|
-
const currentId = activeFeatureId ?? tabbableId
|
|
10
|
-
return (
|
|
11
|
-
<ul // NOSONAR: role='listbox' is correct for custom composite widget; native <select> cannot host SVG marker elements
|
|
12
|
-
id={`${id}-features`}
|
|
13
|
-
ref={ref}
|
|
14
|
-
role='listbox' // NOSONAR
|
|
15
|
-
aria-hidden={hasItems ? undefined : true}
|
|
16
|
-
aria-label='Map features'
|
|
17
|
-
aria-describedby={`${id}-keyboard-desc`}
|
|
18
|
-
aria-multiselectable={multiselectable || undefined}
|
|
19
|
-
className='im-c-features'
|
|
20
|
-
onFocus={onFocus}
|
|
21
|
-
onBlur={onBlur}
|
|
22
|
-
>
|
|
23
|
-
{items.map(item => (
|
|
24
|
-
<li // NOSONAR: role='option' overrides implicit listitem; this is the correct ARIA listbox child pattern.
|
|
25
|
-
// tabIndex/onClick make each option a real, independently focusable and activatable
|
|
26
|
-
// control — required so touch screen readers and voice control can reach and operate
|
|
27
|
-
// it directly, not just app-managed keyboard-arrow navigation.
|
|
28
|
-
key={item.id} id={`${id}-feature-${item.id}`} role='option' // NOSONAR
|
|
29
|
-
data-id={item.id}
|
|
30
|
-
tabIndex={item.id === currentId ? 0 : -1}
|
|
31
|
-
aria-selected={selectedIds.includes(item.id)}
|
|
32
|
-
// Positions the (visually clipped) option over its on-map feature — item.x/item.y are
|
|
33
|
-
// screen coordinates from the interact plugin (see useMapItemList.js). Without this,
|
|
34
|
-
// every option collapses to the CSS static-position default (~top-left of the
|
|
35
|
-
// viewport), which is invisible to sighted users but misaligns coordinate-based AT
|
|
36
|
-
// overlays, e.g. macOS Voice Control's "Show Numbers", which numbers items at their
|
|
37
|
-
// real position on screen. Items without a resolved position (not yet supported for
|
|
38
|
-
// dataset/polygon features) fall back to that same default.
|
|
39
|
-
style={item.x != null ? { left: item.x, top: item.y } : undefined}
|
|
40
|
-
onClick={() => onSelectItem?.(item.id)}
|
|
41
|
-
>
|
|
42
|
-
{item.label}
|
|
43
|
-
</li>
|
|
44
|
-
))}
|
|
45
|
-
</ul>
|
|
46
|
-
)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
Features.displayName = 'Features'
|