@defra/interactive-map 0.0.22-alpha → 0.0.24-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/images/light-raster-thumb.jpg +0 -0
- package/assets/images/outdoor-raster-thumb.jpg +0 -0
- package/assets/images/road-raster-thumb.jpg +0 -0
- 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/docs/plugins/search.md +70 -0
- package/docusaurus.config.cjs +2 -1
- package/package.json +23 -9
- 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/css/index.css +1 -21
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/src/api/editFeature.js +9 -4
- package/plugins/beta/draw-ml/src/modes/createDrawMode.js +11 -3
- 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 +68 -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/esm/index.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/index.js +1 -1
- package/plugins/search/src/Search.jsx +2 -1
- package/plugins/search/src/components/CloseButton/CloseButton.module.scss +2 -0
- package/plugins/search/src/components/Form/Form.jsx +11 -7
- package/plugins/search/src/components/Form/Form.test.jsx +23 -1
- 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/datasets.js +2 -2
- package/plugins/search/src/datasets.test.js +1 -1
- package/plugins/search/src/defaults.js +7 -1
- package/plugins/search/src/events/fetchSuggestions.js +13 -10
- package/plugins/search/src/events/fetchSuggestions.test.js +14 -1
- package/plugins/search/src/events/formHandlers.js +2 -4
- package/plugins/search/src/events/formHandlers.test.js +3 -3
- package/plugins/search/src/events/inputHandlers.js +1 -1
- package/plugins/search/src/events/inputHandlers.test.js +1 -1
- package/plugins/search/src/events/suggestionHandlers.js +2 -1
- package/plugins/search/src/events/suggestionHandlers.test.js +7 -1
- package/plugins/search/src/index.js +3 -7
- package/plugins/search/src/reducer.js +10 -5
- package/plugins/search/src/reducer.test.js +2 -1
- package/plugins/search/src/search.scss +1 -0
- package/plugins/search/src/utils/parseOsNamesResults.js +3 -3
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/esriProvider.js +4 -0
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -0
- package/providers/beta/openlayers/dist/esm/index.js +1 -0
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +2 -0
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js.LICENSE.txt +1 -0
- package/providers/beta/openlayers/dist/umd/index.js +2 -0
- package/providers/beta/openlayers/dist/umd/index.js.LICENSE.txt +1 -0
- package/providers/beta/openlayers/src/appEvents.js +50 -0
- package/providers/beta/openlayers/src/appEvents.test.js +140 -0
- package/providers/beta/openlayers/src/defaults.js +36 -0
- package/providers/beta/openlayers/src/index.js +21 -0
- package/providers/beta/openlayers/src/mapEvents.js +132 -0
- package/providers/beta/openlayers/src/mapEvents.test.js +197 -0
- package/providers/beta/openlayers/src/openlayersProvider.js +234 -0
- package/providers/beta/openlayers/src/openlayersProvider.test.js +331 -0
- package/providers/beta/openlayers/src/utils/spatial.js +142 -0
- package/providers/beta/openlayers/src/utils/spatial.test.js +117 -0
- package/providers/beta/openlayers/src/utils/zoom.js +26 -0
- package/providers/beta/openlayers/src/utils/zoom.test.js +23 -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 +112 -57
- package/providers/maplibre/src/utils/highlightFeatures.test.js +179 -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 +22 -19
- package/providers/maplibre/src/utils/patternImages.test.js +146 -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/rollup.esm.mjs +14 -3
- 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 +98 -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 -1
- 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 +163 -94
- package/src/App/hooks/useModalPanelBehaviour.test.js +2 -1
- 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/AppProvider.test.jsx +5 -0
- package/src/App/store/MapProvider.test.jsx +8 -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/announcer.test.js +5 -7
- 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/webpack.dev.mjs +6 -1
- package/webpack.umd.mjs +1 -0
- package/plugins/interact/dist/css/index.css +0 -1
- package/plugins/interact/src/interact.scss +0 -24
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react'
|
|
2
|
+
import { EVENTS } from '../../config/events.js'
|
|
3
|
+
|
|
4
|
+
const getNavigatedId = (id, key, items) => {
|
|
5
|
+
if (!items.length) {
|
|
6
|
+
return id
|
|
7
|
+
}
|
|
8
|
+
const idx = items.findIndex(item => item.id === id)
|
|
9
|
+
if (key === 'ArrowDown') {
|
|
10
|
+
return idx === -1 ? items[0].id : items[Math.min(idx + 1, items.length - 1)].id
|
|
11
|
+
}
|
|
12
|
+
return idx === -1 ? items[items.length - 1].id : items[Math.max(idx - 1, 0)].id
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ARIA listbox entry priority: first selected → last active (if still in list) → first item
|
|
16
|
+
const resolveEntryId = (items, lastActiveId, selectedIds) => {
|
|
17
|
+
const firstSelected = items.find(item => selectedIds.includes(item.id))
|
|
18
|
+
if (firstSelected) {
|
|
19
|
+
return firstSelected.id
|
|
20
|
+
}
|
|
21
|
+
if (lastActiveId && items.some(item => item.id === lastActiveId)) {
|
|
22
|
+
return lastActiveId
|
|
23
|
+
}
|
|
24
|
+
return items[0].id
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Keeps local selectedIds in sync with interact:selectionchange, and mirrors
|
|
29
|
+
* MAP_SET_ACTIVE_FEATURE back into React state so aria-activedescendant stays current.
|
|
30
|
+
* Also tracks the last non-null active id so it can be restored on re-focus.
|
|
31
|
+
*/
|
|
32
|
+
function useEventBusListeners ({ eventBus, lastActiveIdRef, setActiveFeatureId, setSelectedIds }) {
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!eventBus) {
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
const handleSetActive = ({ id }) => {
|
|
38
|
+
if (id !== null) {
|
|
39
|
+
lastActiveIdRef.current = id
|
|
40
|
+
}
|
|
41
|
+
setActiveFeatureId(id)
|
|
42
|
+
}
|
|
43
|
+
const handleSelectionChange = ({ selectedFeatures = [], selectedMarkers = [] }) => {
|
|
44
|
+
setSelectedIds([...selectedFeatures.map(f => String(f.featureId)), ...selectedMarkers])
|
|
45
|
+
}
|
|
46
|
+
eventBus.on(EVENTS.MAP_SET_ACTIVE_FEATURE, handleSetActive)
|
|
47
|
+
eventBus.on('interact:selectionchange', handleSelectionChange)
|
|
48
|
+
return () => {
|
|
49
|
+
eventBus.off(EVENTS.MAP_SET_ACTIVE_FEATURE, handleSetActive)
|
|
50
|
+
eventBus.off('interact:selectionchange', handleSelectionChange)
|
|
51
|
+
}
|
|
52
|
+
}, [eventBus])
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Revalidates aria-activedescendant when the item list changes while the listbox is focused.
|
|
57
|
+
* If the current active item is no longer in the list (e.g. panned off screen), picks a new one
|
|
58
|
+
* using ARIA priority order: first selected → last active position → first item.
|
|
59
|
+
* Reads eventBus and selectedIds via refs to avoid spurious re-runs on selection change.
|
|
60
|
+
*/
|
|
61
|
+
function useItemsRevalidation ({ items, eventBus, isFocusedRef, lastActiveIdRef, activeFeatureIdRef, selectedIdsRef, setActiveFeatureId }) {
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!isFocusedRef.current) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
if (!items.length) {
|
|
67
|
+
setActiveFeatureId(null)
|
|
68
|
+
eventBus?.emit(EVENTS.MAP_SET_ACTIVE_FEATURE, { id: null })
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
if (items.some(item => item.id === activeFeatureIdRef.current)) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
const nextId = resolveEntryId(items, lastActiveIdRef.current, selectedIdsRef.current)
|
|
75
|
+
lastActiveIdRef.current = nextId
|
|
76
|
+
setActiveFeatureId(nextId)
|
|
77
|
+
eventBus?.emit(EVENTS.MAP_SET_ACTIVE_FEATURE, { id: nextId })
|
|
78
|
+
}, [items]) // NOSONAR — eventBus/selectedIds consumed via refs to avoid spurious re-runs on selection change
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Attaches a keydown listener to the listbox element for ARIA keyboard navigation:
|
|
83
|
+
* - ArrowUp/ArrowDown — move the active item, emitting MAP_SET_ACTIVE_FEATURE
|
|
84
|
+
* - Enter/Space — confirm selection, emitting MAP_SELECT_FEATURE
|
|
85
|
+
* - Escape — return focus to the map viewport
|
|
86
|
+
*/
|
|
87
|
+
function useKeyboardNavigation ({ featuresRef, viewportRef, items, eventBus, activeFeatureIdRef, lastActiveIdRef, setActiveFeatureId, hints, currentHintRef }) {
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const listboxEl = featuresRef.current
|
|
90
|
+
if (!listboxEl) {
|
|
91
|
+
return undefined
|
|
92
|
+
}
|
|
93
|
+
const handleKeyDown = (event) => {
|
|
94
|
+
if (event.key === 'Escape') {
|
|
95
|
+
event.preventDefault()
|
|
96
|
+
event.stopPropagation()
|
|
97
|
+
if (currentHintRef.current) {
|
|
98
|
+
hints.dismiss()
|
|
99
|
+
} else {
|
|
100
|
+
viewportRef.current?.focus()
|
|
101
|
+
}
|
|
102
|
+
} else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
103
|
+
event.preventDefault()
|
|
104
|
+
event.stopPropagation()
|
|
105
|
+
const newId = getNavigatedId(activeFeatureIdRef.current, event.key, items)
|
|
106
|
+
lastActiveIdRef.current = newId
|
|
107
|
+
setActiveFeatureId(newId)
|
|
108
|
+
eventBus?.emit(EVENTS.MAP_SET_ACTIVE_FEATURE, { id: newId })
|
|
109
|
+
} else if (event.key === 'Enter' || event.key === ' ') {
|
|
110
|
+
event.preventDefault()
|
|
111
|
+
event.stopPropagation()
|
|
112
|
+
eventBus?.emit(EVENTS.MAP_SELECT_FEATURE)
|
|
113
|
+
} else {
|
|
114
|
+
// No action
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
listboxEl.addEventListener('keydown', handleKeyDown)
|
|
118
|
+
return () => { listboxEl.removeEventListener('keydown', handleKeyDown) }
|
|
119
|
+
}, [viewportRef, featuresRef, items, eventBus])
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Clears listbox focus whenever the user interacts with the map via pointer (mouse or touch).
|
|
124
|
+
* Any pointerdown outside the listbox element moves focus to the viewport, removing the
|
|
125
|
+
* visible focus ring from the listbox without dropping focus to an unknown location.
|
|
126
|
+
*/
|
|
127
|
+
function useMapInteractionBlur ({ viewportRef, featuresRef, isFocusedRef }) {
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const el = viewportRef.current
|
|
130
|
+
if (!el) {
|
|
131
|
+
return undefined
|
|
132
|
+
}
|
|
133
|
+
const handlePointerDown = (event) => {
|
|
134
|
+
if (isFocusedRef.current && !featuresRef.current?.contains(event.target)) {
|
|
135
|
+
viewportRef.current?.focus()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
el.addEventListener('pointerdown', handlePointerDown)
|
|
139
|
+
return () => { el.removeEventListener('pointerdown', handlePointerDown) }
|
|
140
|
+
}, [viewportRef, featuresRef])
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Manages ARIA listbox focus state for the keyboard-accessible feature list.
|
|
145
|
+
*
|
|
146
|
+
* On focus, sets aria-activedescendant following ARIA priority order:
|
|
147
|
+
* 1. First selected item (if any)
|
|
148
|
+
* 2. Last active position (if still in the list)
|
|
149
|
+
* 3. First item (fallback)
|
|
150
|
+
*
|
|
151
|
+
* On blur, clears the active descendant. Revalidates automatically when the item list
|
|
152
|
+
* changes (e.g. after a map pan) so the active id never points to a stale item.
|
|
153
|
+
*
|
|
154
|
+
* @param {{ viewportRef: React.RefObject, featuresRef: React.RefObject, items: Array, eventBus: object }} params
|
|
155
|
+
* @returns {{ activeFeatureId: string|null, selectedIds: string[], onFocus: Function, onBlur: Function }}
|
|
156
|
+
*/
|
|
157
|
+
export function useFeatureFocus ({ viewportRef, featuresRef, items = [], eventBus, hints }) {
|
|
158
|
+
const [activeFeatureId, setActiveFeatureId] = useState(null)
|
|
159
|
+
const [selectedIds, setSelectedIds] = useState([])
|
|
160
|
+
|
|
161
|
+
const isFocusedRef = useRef(false)
|
|
162
|
+
const lastActiveIdRef = useRef(null) // preserved across blur; restores position on re-focus
|
|
163
|
+
const activeFeatureIdRef = useRef(null) // always-current for keydown closure
|
|
164
|
+
const selectedIdsRef = useRef([]) // always-current for items-change effect
|
|
165
|
+
const currentHintRef = useRef(null)
|
|
166
|
+
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
return hints.subscribe((hint) => {
|
|
169
|
+
currentHintRef.current = hint
|
|
170
|
+
})
|
|
171
|
+
}, [hints])
|
|
172
|
+
|
|
173
|
+
useEffect(() => { activeFeatureIdRef.current = activeFeatureId }, [activeFeatureId])
|
|
174
|
+
useEffect(() => { selectedIdsRef.current = selectedIds }, [selectedIds])
|
|
175
|
+
|
|
176
|
+
useEventBusListeners({ eventBus, lastActiveIdRef, setActiveFeatureId, setSelectedIds })
|
|
177
|
+
useItemsRevalidation({ items, eventBus, isFocusedRef, lastActiveIdRef, activeFeatureIdRef, selectedIdsRef, setActiveFeatureId })
|
|
178
|
+
useKeyboardNavigation({ featuresRef, viewportRef, items, eventBus, activeFeatureIdRef, lastActiveIdRef, setActiveFeatureId, hints, currentHintRef })
|
|
179
|
+
useMapInteractionBlur({ viewportRef, featuresRef, isFocusedRef })
|
|
180
|
+
|
|
181
|
+
const onFocus = () => {
|
|
182
|
+
isFocusedRef.current = true
|
|
183
|
+
if (!items.length) {
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
const id = resolveEntryId(items, lastActiveIdRef.current, selectedIds)
|
|
187
|
+
lastActiveIdRef.current = id
|
|
188
|
+
setActiveFeatureId(id)
|
|
189
|
+
eventBus?.emit(EVENTS.MAP_SET_ACTIVE_FEATURE, { id })
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const onBlur = () => {
|
|
193
|
+
isFocusedRef.current = false
|
|
194
|
+
setActiveFeatureId(null)
|
|
195
|
+
eventBus?.emit(EVENTS.MAP_SET_ACTIVE_FEATURE, { id: null })
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { activeFeatureId, selectedIds, onFocus, onBlur }
|
|
199
|
+
}
|