@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,87 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react'
|
|
2
|
+
import { useMarkerCursor } from './useMarkerCursor.js'
|
|
3
|
+
|
|
4
|
+
const HIT_BOUNDS = { left: 10, top: 10, right: 50, bottom: 50 }
|
|
5
|
+
const CURSOR_INSIDE = { clientX: 20, clientY: 20 }
|
|
6
|
+
const CURSOR_OUTSIDE = { clientX: 100, clientY: 100 }
|
|
7
|
+
|
|
8
|
+
const makeMarker = (overrides = {}) => ({ id: 'marker-1', isVisible: true, symbol: 'pin', ...overrides })
|
|
9
|
+
|
|
10
|
+
const makeMarkers = (marker, bounds) => {
|
|
11
|
+
const markerRefs = new Map()
|
|
12
|
+
if (bounds) markerRefs.set(marker.id, { getBoundingClientRect: () => bounds })
|
|
13
|
+
return { items: [marker], markerRefs }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const fireMove = (vp, { clientX, clientY }) => act(() => {
|
|
17
|
+
vp.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX, clientY }))
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
let viewport
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
viewport = document.createElement('div')
|
|
24
|
+
viewport.className = 'im-c-viewport'
|
|
25
|
+
document.body.appendChild(viewport)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(() => { viewport.remove() })
|
|
29
|
+
|
|
30
|
+
describe('useMarkerCursor — inactive guards', () => {
|
|
31
|
+
it('does not track cursor when interactActive is false', () => {
|
|
32
|
+
const markers = makeMarkers(makeMarker(), HIT_BOUNDS)
|
|
33
|
+
renderHook(() => useMarkerCursor(markers, false, { current: viewport }))
|
|
34
|
+
fireMove(viewport, CURSOR_INSIDE)
|
|
35
|
+
expect(viewport.style.cursor).toBe('')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('does not track cursor when viewport ref is null', () => {
|
|
39
|
+
const markers = makeMarkers(makeMarker(), HIT_BOUNDS)
|
|
40
|
+
renderHook(() => useMarkerCursor(markers, true, { current: null }))
|
|
41
|
+
fireMove(viewport, CURSOR_INSIDE)
|
|
42
|
+
expect(viewport.style.cursor).toBe('')
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('useMarkerCursor — cursor tracking', () => {
|
|
47
|
+
it('sets cursor to pointer when mousemove lands inside a marker', () => {
|
|
48
|
+
const markers = makeMarkers(makeMarker(), HIT_BOUNDS)
|
|
49
|
+
renderHook(() => useMarkerCursor(markers, true, { current: viewport }))
|
|
50
|
+
fireMove(viewport, CURSOR_INSIDE)
|
|
51
|
+
expect(viewport.style.cursor).toBe('pointer')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('cursor stays empty when mousemove is outside all markers', () => {
|
|
55
|
+
const markers = makeMarkers(makeMarker(), HIT_BOUNDS)
|
|
56
|
+
renderHook(() => useMarkerCursor(markers, true, { current: viewport }))
|
|
57
|
+
fireMove(viewport, CURSOR_OUTSIDE)
|
|
58
|
+
expect(viewport.style.cursor).toBe('')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('cursor stays empty when marker has no ref element', () => {
|
|
62
|
+
const markers = makeMarkers(makeMarker(), null)
|
|
63
|
+
renderHook(() => useMarkerCursor(markers, true, { current: viewport }))
|
|
64
|
+
fireMove(viewport, CURSOR_INSIDE)
|
|
65
|
+
expect(viewport.style.cursor).toBe('')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('skips standalone label markers in the cursor hit test', () => {
|
|
69
|
+
const marker = makeMarker({ label: 'lbl', symbol: null, symbolSvgContent: null })
|
|
70
|
+
const markers = makeMarkers(marker, HIT_BOUNDS)
|
|
71
|
+
renderHook(() => useMarkerCursor(markers, true, { current: viewport }))
|
|
72
|
+
fireMove(viewport, CURSOR_INSIDE)
|
|
73
|
+
expect(viewport.style.cursor).toBe('')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('clears cursor when interactActive becomes false', () => {
|
|
77
|
+
const markers = makeMarkers(makeMarker(), HIT_BOUNDS)
|
|
78
|
+
const { rerender } = renderHook(
|
|
79
|
+
({ active }) => useMarkerCursor(markers, active, { current: viewport }),
|
|
80
|
+
{ initialProps: { active: true } }
|
|
81
|
+
)
|
|
82
|
+
fireMove(viewport, CURSOR_INSIDE)
|
|
83
|
+
expect(viewport.style.cursor).toBe('pointer')
|
|
84
|
+
act(() => rerender({ active: false }))
|
|
85
|
+
expect(viewport.style.cursor).toBe('')
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -50,33 +50,43 @@ const updateMarkerPositions = (items, markerRefs, mapProvider, mapSize, isMapRea
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* Registers event bus listeners for adding and removing markers via
|
|
54
|
-
* APP_ADD_MARKER and APP_REMOVE_MARKER events.
|
|
53
|
+
* Registers event bus listeners for adding, updating and removing markers via
|
|
54
|
+
* APP_ADD_MARKER, APP_UPDATE_MARKER and APP_REMOVE_MARKER events.
|
|
55
55
|
*
|
|
56
56
|
* @param {Object} eventBus - Application event bus
|
|
57
|
-
* @param {Object} markers - Markers API object with `add` and `remove` methods
|
|
57
|
+
* @param {Object} markers - Markers API object with `add`, `update` and `remove` methods
|
|
58
58
|
*/
|
|
59
|
-
const useMarkerEventListeners = (eventBus,
|
|
59
|
+
const useMarkerEventListeners = (eventBus, markersRef) => {
|
|
60
60
|
useEffect(() => {
|
|
61
61
|
const handleAddMarker = (payload = {}) => {
|
|
62
62
|
if (!payload?.id || !payload?.coords) {
|
|
63
63
|
return
|
|
64
64
|
}
|
|
65
65
|
const { id, coords, options } = payload
|
|
66
|
-
|
|
66
|
+
markersRef.current.add(id, coords, options)
|
|
67
67
|
}
|
|
68
68
|
eventBus.on(events.APP_ADD_MARKER, handleAddMarker)
|
|
69
69
|
|
|
70
|
+
const handleUpdateMarker = (payload = {}) => {
|
|
71
|
+
if (!payload?.id) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
const { id, options } = payload
|
|
75
|
+
markersRef.current.update(id, options)
|
|
76
|
+
}
|
|
77
|
+
eventBus.on(events.APP_UPDATE_MARKER, handleUpdateMarker)
|
|
78
|
+
|
|
70
79
|
const handleRemoveMarker = (id) => {
|
|
71
80
|
if (!id) {
|
|
72
81
|
return
|
|
73
82
|
}
|
|
74
|
-
|
|
83
|
+
markersRef.current.remove(id)
|
|
75
84
|
}
|
|
76
85
|
eventBus.on(events.APP_REMOVE_MARKER, handleRemoveMarker)
|
|
77
86
|
|
|
78
87
|
return () => {
|
|
79
88
|
eventBus.off(events.APP_ADD_MARKER, handleAddMarker)
|
|
89
|
+
eventBus.off(events.APP_UPDATE_MARKER, handleUpdateMarker)
|
|
80
90
|
eventBus.off(events.APP_REMOVE_MARKER, handleRemoveMarker)
|
|
81
91
|
}
|
|
82
92
|
}, [])
|
|
@@ -84,8 +94,8 @@ const useMarkerEventListeners = (eventBus, markers) => {
|
|
|
84
94
|
|
|
85
95
|
/**
|
|
86
96
|
* Hook that provides the markers API and ref callback for positioning marker
|
|
87
|
-
* elements on the map. Attaches `add`, `remove`, and `getMarker` methods
|
|
88
|
-
* markers store object and keeps marker positions in sync with map render and
|
|
97
|
+
* elements on the map. Attaches `add`, `update`, `remove`, and `getMarker` methods
|
|
98
|
+
* to the markers store object and keeps marker positions in sync with map render and
|
|
89
99
|
* resize events.
|
|
90
100
|
*
|
|
91
101
|
* @returns {{ markers: Object, markerRef: Function }}
|
|
@@ -95,6 +105,7 @@ export const useMarkers = () => {
|
|
|
95
105
|
const { eventBus } = useService()
|
|
96
106
|
const { markers, dispatch, mapSize, isMapReady } = useMap()
|
|
97
107
|
const markerRefs = useRef(new Map())
|
|
108
|
+
const markersRef = useRef(markers)
|
|
98
109
|
|
|
99
110
|
// Attach add, remove, and getMarker methods to the markers store object.
|
|
100
111
|
// useLayoutEffect ensures these are assigned before paint so rapid clicks can't
|
|
@@ -104,6 +115,7 @@ export const useMarkers = () => {
|
|
|
104
115
|
return
|
|
105
116
|
}
|
|
106
117
|
|
|
118
|
+
markersRef.current = markers
|
|
107
119
|
markers.markerRefs = markerRefs.current
|
|
108
120
|
|
|
109
121
|
markers.add = (id, coords, options) => {
|
|
@@ -111,6 +123,17 @@ export const useMarkers = () => {
|
|
|
111
123
|
dispatch({ type: 'UPSERT_LOCATION_MARKER', payload: { id, coords, ...options, x, y, isVisible: true } })
|
|
112
124
|
}
|
|
113
125
|
|
|
126
|
+
markers.update = (id, options = {}) => {
|
|
127
|
+
const existing = markers.items.find(m => m.id === id)
|
|
128
|
+
if (!existing) {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
const { coords, ...rest } = options
|
|
132
|
+
const resolvedCoords = coords ?? existing.coords
|
|
133
|
+
const { x, y } = projectCoords(resolvedCoords, mapProvider, mapSize, isMapReady)
|
|
134
|
+
dispatch({ type: 'UPSERT_LOCATION_MARKER', payload: { id, coords: resolvedCoords, ...rest, x, y } })
|
|
135
|
+
}
|
|
136
|
+
|
|
114
137
|
markers.remove = (id) => {
|
|
115
138
|
dispatch({ type: 'REMOVE_LOCATION_MARKER', payload: id })
|
|
116
139
|
}
|
|
@@ -150,7 +173,7 @@ export const useMarkers = () => {
|
|
|
150
173
|
updateMarkerPositions(markers.items, markerRefs.current, mapProvider, mapSize, isMapReady)
|
|
151
174
|
}, [mapSize, markers.items, mapProvider, isMapReady])
|
|
152
175
|
|
|
153
|
-
useMarkerEventListeners(eventBus,
|
|
176
|
+
useMarkerEventListeners(eventBus, markersRef)
|
|
154
177
|
|
|
155
178
|
return { markers, markerRef }
|
|
156
179
|
}
|
|
@@ -15,6 +15,8 @@ jest.mock('../../services/eventBus.js', () => ({
|
|
|
15
15
|
}))
|
|
16
16
|
jest.mock('../../config/appConfig.js', () => ({ scaleFactor: { small: 1, medium: 2, large: 3 } }))
|
|
17
17
|
|
|
18
|
+
// ─── projectCoords ────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
18
20
|
describe('projectCoords', () => {
|
|
19
21
|
const mockProvider = { mapToScreen: jest.fn(() => ({ x: 100, y: 200 })) }
|
|
20
22
|
|
|
@@ -29,41 +31,44 @@ describe('projectCoords', () => {
|
|
|
29
31
|
})
|
|
30
32
|
})
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
mockMapProvider
|
|
37
|
-
mockDispatch
|
|
38
|
-
mockMarkers
|
|
39
|
-
mockElement
|
|
40
|
-
mockEventBus
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
isMapReady: true
|
|
52
|
-
})
|
|
34
|
+
// ─── useMarkers shared setup ──────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const makeCtx = () => {
|
|
37
|
+
const ctx = {
|
|
38
|
+
mockMapProvider: { mapToScreen: jest.fn(() => ({ x: 100, y: 200 })) },
|
|
39
|
+
mockDispatch: jest.fn(),
|
|
40
|
+
mockMarkers: { items: [] },
|
|
41
|
+
mockElement: { style: {} },
|
|
42
|
+
mockEventBus: { on: jest.fn(), off: jest.fn(), emit: jest.fn() }
|
|
43
|
+
}
|
|
44
|
+
eventBus.on = jest.fn()
|
|
45
|
+
eventBus.off = jest.fn()
|
|
46
|
+
useConfig.mockReturnValue({ mapProvider: ctx.mockMapProvider })
|
|
47
|
+
useService.mockReturnValue({ eventBus: ctx.mockEventBus })
|
|
48
|
+
useMap.mockReturnValue({
|
|
49
|
+
markers: ctx.mockMarkers,
|
|
50
|
+
dispatch: ctx.mockDispatch,
|
|
51
|
+
mapSize: 'medium',
|
|
52
|
+
isMapReady: true
|
|
53
53
|
})
|
|
54
|
+
return ctx
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── useMarkers — API setup ───────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
describe('useMarkers — API setup', () => {
|
|
60
|
+
let ctx
|
|
61
|
+
beforeEach(() => { ctx = makeCtx() })
|
|
54
62
|
|
|
55
63
|
it('returns markers and markerRef', () => {
|
|
56
64
|
const { result } = renderHook(() => useMarkers())
|
|
57
|
-
expect(result.current).toMatchObject({ markers: mockMarkers, markerRef: expect.any(Function) })
|
|
65
|
+
expect(result.current).toMatchObject({ markers: ctx.mockMarkers, markerRef: expect.any(Function) })
|
|
58
66
|
})
|
|
59
67
|
|
|
60
|
-
it('returns early when mapProvider is null
|
|
68
|
+
it('returns early when mapProvider is null', () => {
|
|
61
69
|
useConfig.mockReturnValue({ mapProvider: null })
|
|
62
|
-
mockMarkers.items = [{ id: 'm1', label: 'Test' }]
|
|
63
|
-
|
|
70
|
+
ctx.mockMarkers.items = [{ id: 'm1', label: 'Test' }]
|
|
64
71
|
const { result } = renderHook(() => useMarkers())
|
|
65
|
-
|
|
66
|
-
// The markers object should not have the API methods attached
|
|
67
72
|
expect(result.current.markers.add).toBeUndefined()
|
|
68
73
|
expect(result.current.markers.remove).toBeUndefined()
|
|
69
74
|
expect(result.current.markers.getMarker).toBeUndefined()
|
|
@@ -71,122 +76,186 @@ describe('useMarkers', () => {
|
|
|
71
76
|
})
|
|
72
77
|
|
|
73
78
|
it('getMarker returns correct marker', () => {
|
|
74
|
-
mockMarkers.items = [{ id: 'm1', label: 'First' }, { id: 'm2', label: 'Second' }]
|
|
79
|
+
ctx.mockMarkers.items = [{ id: 'm1', label: 'First' }, { id: 'm2', label: 'Second' }]
|
|
75
80
|
const { result } = renderHook(() => useMarkers())
|
|
76
|
-
|
|
77
|
-
// Found
|
|
78
81
|
expect(result.current.markers.getMarker('m2')).toEqual({ id: 'm2', label: 'Second' })
|
|
79
|
-
// Not found
|
|
80
82
|
expect(result.current.markers.getMarker('nonexistent')).toBeUndefined()
|
|
81
83
|
})
|
|
84
|
+
})
|
|
82
85
|
|
|
83
|
-
|
|
84
|
-
const { result } = renderHook(() => useMarkers())
|
|
85
|
-
const ref = result.current.markerRef('m1')
|
|
86
|
+
// ─── useMarkers — markerRef and positioning ───────────────────────────────────
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
describe('useMarkers — markerRef and positioning', () => {
|
|
89
|
+
let ctx
|
|
90
|
+
beforeEach(() => { ctx = makeCtx() })
|
|
89
91
|
|
|
90
|
-
|
|
92
|
+
it('markerRef stores and removes DOM refs', () => {
|
|
93
|
+
const { result } = renderHook(() => useMarkers())
|
|
94
|
+
const ref = result.current.markerRef('m1')
|
|
95
|
+
act(() => ref(ctx.mockElement))
|
|
96
|
+
expect(result.current.markers.markerRefs.get('m1')).toBe(ctx.mockElement)
|
|
97
|
+
act(() => ref(null))
|
|
91
98
|
expect(result.current.markers.markerRefs.has('m1')).toBe(false)
|
|
92
99
|
})
|
|
93
100
|
|
|
94
|
-
it('updateMarkers skips missing coords or ref
|
|
95
|
-
mockMarkers.items = [
|
|
101
|
+
it('updateMarkers skips markers with missing coords or no ref', () => {
|
|
102
|
+
ctx.mockMarkers.items = [
|
|
96
103
|
{ id: 'm1', coords: { lat: 1, lng: 1 } },
|
|
97
|
-
{ id: 'm2', coords: null },
|
|
98
|
-
{ id: 'm3', coords: { lat: 0, lng: 0 } }
|
|
104
|
+
{ id: 'm2', coords: null },
|
|
105
|
+
{ id: 'm3', coords: { lat: 0, lng: 0 } }
|
|
99
106
|
]
|
|
100
107
|
const { result } = renderHook(() => useMarkers())
|
|
101
|
-
act(() => result.current.markerRef('m1')(mockElement))
|
|
102
|
-
|
|
103
|
-
const renderCallback = mockEventBus.on.mock.calls.find(call => call[0] === 'map:render')[1]
|
|
108
|
+
act(() => result.current.markerRef('m1')(ctx.mockElement))
|
|
109
|
+
const renderCallback = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'map:render')[1]
|
|
104
110
|
act(() => renderCallback())
|
|
105
|
-
expect(mockElement.style.transform).toBe('translate(200px, 400px)')
|
|
111
|
+
expect(ctx.mockElement.style.transform).toBe('translate(200px, 400px)')
|
|
106
112
|
})
|
|
107
113
|
|
|
108
|
-
it('skips map:render when not ready or no provider
|
|
109
|
-
useMap.mockReturnValue({ markers: mockMarkers, dispatch: mockDispatch, mapSize: 'medium', isMapReady: false })
|
|
114
|
+
it('skips map:render callback when not ready or no provider', () => {
|
|
115
|
+
useMap.mockReturnValue({ markers: ctx.mockMarkers, dispatch: ctx.mockDispatch, mapSize: 'medium', isMapReady: false })
|
|
110
116
|
const { result } = renderHook(() => useMarkers())
|
|
111
|
-
act(() => result.current.markerRef('m1')(mockElement))
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
act(() => result.current.markerRef('m1')(ctx.mockElement))
|
|
118
|
+
const renderCallback = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'map:render')?.[1]
|
|
119
|
+
if (renderCallback) act(() => renderCallback())
|
|
120
|
+
expect(ctx.mockElement.style.transform).toBeUndefined()
|
|
115
121
|
})
|
|
116
122
|
|
|
117
123
|
it('updates positions on mapSize change', () => {
|
|
118
|
-
mockMarkers.items = [{ id: 'm1', coords: { lat: 1, lng: 1 } }]
|
|
124
|
+
ctx.mockMarkers.items = [{ id: 'm1', coords: { lat: 1, lng: 1 } }]
|
|
119
125
|
const { result, rerender } = renderHook(() => useMarkers())
|
|
120
|
-
act(() => result.current.markerRef('m1')(mockElement))
|
|
121
|
-
|
|
122
|
-
useMap.mockReturnValue({
|
|
123
|
-
markers: mockMarkers,
|
|
124
|
-
dispatch: mockDispatch,
|
|
125
|
-
mapSize: 'large',
|
|
126
|
-
isMapReady: true
|
|
127
|
-
})
|
|
126
|
+
act(() => result.current.markerRef('m1')(ctx.mockElement))
|
|
127
|
+
useMap.mockReturnValue({ markers: ctx.mockMarkers, dispatch: ctx.mockDispatch, mapSize: 'large', isMapReady: true })
|
|
128
128
|
rerender()
|
|
129
|
-
expect(mockElement.style.transform).toBe('translate(300px, 600px)')
|
|
129
|
+
expect(ctx.mockElement.style.transform).toBe('translate(300px, 600px)')
|
|
130
130
|
})
|
|
131
|
+
})
|
|
131
132
|
|
|
132
|
-
|
|
133
|
-
renderHook(() => useMarkers())
|
|
134
|
-
const addPayload = { id: 'm1', coords: { lat: 1, lng: 1 }, options: { label: 'Test' } }
|
|
133
|
+
// ─── useMarkers — add/update/remove event handlers ───────────────────────────
|
|
135
134
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
describe('useMarkers — add/update/remove event handlers', () => {
|
|
136
|
+
let ctx
|
|
137
|
+
beforeEach(() => { ctx = makeCtx() })
|
|
138
|
+
|
|
139
|
+
it('handles app:addmarker — dispatches with projected coords', () => {
|
|
140
|
+
renderHook(() => useMarkers())
|
|
141
|
+
const handleAddMarker = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'app:addmarker')[1]
|
|
142
|
+
act(() => handleAddMarker({ id: 'm1', coords: { lat: 1, lng: 1 }, options: { label: 'Test' } }))
|
|
143
|
+
expect(ctx.mockDispatch).toHaveBeenCalledWith({
|
|
139
144
|
type: 'UPSERT_LOCATION_MARKER',
|
|
140
145
|
payload: { id: 'm1', coords: { lat: 1, lng: 1 }, label: 'Test', x: 200, y: 400, isVisible: true }
|
|
141
146
|
})
|
|
142
147
|
})
|
|
143
148
|
|
|
144
|
-
it('
|
|
149
|
+
it('handles app:addmarker guard — ignores missing id or coords', () => {
|
|
145
150
|
renderHook(() => useMarkers())
|
|
146
|
-
const handleAddMarker = mockEventBus.on.mock.calls.find(call => call[0] === 'app:addmarker')[1]
|
|
147
|
-
|
|
151
|
+
const handleAddMarker = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'app:addmarker')[1]
|
|
148
152
|
act(() => handleAddMarker(undefined))
|
|
149
153
|
act(() => handleAddMarker(null))
|
|
150
|
-
act(() => handleAddMarker({}))
|
|
151
|
-
act(() => handleAddMarker({ id: 'm1' }))
|
|
152
|
-
expect(mockDispatch).not.toHaveBeenCalled()
|
|
154
|
+
act(() => handleAddMarker({}))
|
|
155
|
+
act(() => handleAddMarker({ id: 'm1' }))
|
|
156
|
+
expect(ctx.mockDispatch).not.toHaveBeenCalled()
|
|
153
157
|
})
|
|
154
158
|
|
|
155
|
-
it('handles app:
|
|
159
|
+
it('handles app:updatemarker guard — ignores missing id (lines 71-73)', () => {
|
|
160
|
+
ctx.mockMarkers.items = [{ id: 'm1', coords: { lat: 1, lng: 1 } }]
|
|
156
161
|
renderHook(() => useMarkers())
|
|
157
|
-
const
|
|
162
|
+
const handleUpdateMarker = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'app:updatemarker')[1]
|
|
163
|
+
act(() => handleUpdateMarker(undefined))
|
|
164
|
+
act(() => handleUpdateMarker(null))
|
|
165
|
+
act(() => handleUpdateMarker({}))
|
|
166
|
+
expect(ctx.mockDispatch).not.toHaveBeenCalled()
|
|
167
|
+
})
|
|
158
168
|
|
|
159
|
-
|
|
160
|
-
|
|
169
|
+
it('handles app:updatemarker — dispatches update for valid id (lines 74-75)', () => {
|
|
170
|
+
ctx.mockMarkers.items = [{ id: 'm1', coords: { lat: 1, lng: 1 } }]
|
|
171
|
+
renderHook(() => useMarkers())
|
|
172
|
+
const handleUpdateMarker = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'app:updatemarker')[1]
|
|
173
|
+
act(() => handleUpdateMarker({ id: 'm1', options: { showLabel: true } }))
|
|
174
|
+
expect(ctx.mockDispatch).toHaveBeenCalledWith({
|
|
175
|
+
type: 'UPSERT_LOCATION_MARKER',
|
|
176
|
+
payload: { id: 'm1', coords: { lat: 1, lng: 1 }, showLabel: true, x: 200, y: 400 }
|
|
177
|
+
})
|
|
178
|
+
})
|
|
161
179
|
|
|
162
|
-
|
|
180
|
+
it('handles app:removemarker — dispatches and guards null/undefined', () => {
|
|
181
|
+
renderHook(() => useMarkers())
|
|
182
|
+
const handleRemoveMarker = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'app:removemarker')[1]
|
|
183
|
+
act(() => handleRemoveMarker('m1'))
|
|
184
|
+
expect(ctx.mockDispatch).toHaveBeenCalledWith({ type: 'REMOVE_LOCATION_MARKER', payload: 'm1' })
|
|
185
|
+
act(() => handleRemoveMarker(undefined))
|
|
163
186
|
act(() => handleRemoveMarker(null))
|
|
164
|
-
expect(mockDispatch).toHaveBeenCalledTimes(1)
|
|
187
|
+
expect(ctx.mockDispatch).toHaveBeenCalledTimes(1)
|
|
165
188
|
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// ─── useMarkers — markers.update ─────────────────────────────────────────────
|
|
166
192
|
|
|
167
|
-
|
|
193
|
+
describe('useMarkers — markers.update', () => {
|
|
194
|
+
let ctx
|
|
195
|
+
beforeEach(() => { ctx = makeCtx() })
|
|
196
|
+
|
|
197
|
+
it('dispatches using existing coords when none provided (lines 127-134)', () => {
|
|
198
|
+
ctx.mockMarkers.items = [{ id: 'm1', coords: { lat: 1, lng: 1 }, label: 'Old' }]
|
|
168
199
|
const { result } = renderHook(() => useMarkers())
|
|
200
|
+
act(() => result.current.markers.update('m1', { showLabel: true }))
|
|
201
|
+
expect(ctx.mockDispatch).toHaveBeenCalledWith({
|
|
202
|
+
type: 'UPSERT_LOCATION_MARKER',
|
|
203
|
+
payload: { id: 'm1', coords: { lat: 1, lng: 1 }, showLabel: true, x: 200, y: 400 }
|
|
204
|
+
})
|
|
205
|
+
})
|
|
169
206
|
|
|
170
|
-
|
|
171
|
-
|
|
207
|
+
it('is a no-op when marker id is not found (lines 128-130)', () => {
|
|
208
|
+
ctx.mockMarkers.items = []
|
|
209
|
+
const { result } = renderHook(() => useMarkers())
|
|
210
|
+
act(() => result.current.markers.update('nonexistent', { showLabel: true }))
|
|
211
|
+
expect(ctx.mockDispatch).not.toHaveBeenCalled()
|
|
212
|
+
})
|
|
172
213
|
|
|
173
|
-
|
|
174
|
-
|
|
214
|
+
it('uses provided coords over existing (lines 131-133)', () => {
|
|
215
|
+
ctx.mockMarkers.items = [{ id: 'm1', coords: { lat: 1, lng: 1 } }]
|
|
216
|
+
const { result } = renderHook(() => useMarkers())
|
|
217
|
+
const newCoords = { lat: 5, lng: 5 }
|
|
218
|
+
act(() => result.current.markers.update('m1', { coords: newCoords, showLabel: true }))
|
|
219
|
+
expect(ctx.mockDispatch).toHaveBeenCalledWith({
|
|
220
|
+
type: 'UPSERT_LOCATION_MARKER',
|
|
221
|
+
payload: { id: 'm1', coords: newCoords, showLabel: true, x: 200, y: 400 }
|
|
222
|
+
})
|
|
223
|
+
})
|
|
175
224
|
|
|
176
|
-
|
|
225
|
+
it('uses default empty options when none provided (line 126)', () => {
|
|
226
|
+
ctx.mockMarkers.items = [{ id: 'm1', coords: { lat: 1, lng: 1 } }]
|
|
227
|
+
const { result } = renderHook(() => useMarkers())
|
|
228
|
+
act(() => result.current.markers.update('m1'))
|
|
229
|
+
expect(ctx.mockDispatch).toHaveBeenCalledWith({
|
|
230
|
+
type: 'UPSERT_LOCATION_MARKER',
|
|
231
|
+
payload: { id: 'm1', coords: { lat: 1, lng: 1 }, x: 200, y: 400 }
|
|
232
|
+
})
|
|
177
233
|
})
|
|
234
|
+
})
|
|
178
235
|
|
|
179
|
-
|
|
180
|
-
const { unmount } = renderHook(() => useMarkers())
|
|
236
|
+
// ─── useMarkers — cleanup ─────────────────────────────────────────────────────
|
|
181
237
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
238
|
+
describe('useMarkers — cleanup', () => {
|
|
239
|
+
let ctx
|
|
240
|
+
beforeEach(() => { ctx = makeCtx() })
|
|
185
241
|
|
|
186
|
-
|
|
242
|
+
it('cleans up map:render listener when markerRef is called with null', () => {
|
|
243
|
+
const { result } = renderHook(() => useMarkers())
|
|
244
|
+
let cleanup
|
|
245
|
+
act(() => { cleanup = result.current.markerRef('m1')(ctx.mockElement) })
|
|
246
|
+
const updateCallback = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'map:render')[1]
|
|
247
|
+
act(() => { if (cleanup) cleanup() })
|
|
248
|
+
expect(ctx.mockEventBus.off).toHaveBeenCalledWith('map:render', updateCallback)
|
|
249
|
+
})
|
|
187
250
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
251
|
+
it('cleans up add/update/remove eventBus listeners on unmount', () => {
|
|
252
|
+
const { unmount } = renderHook(() => useMarkers())
|
|
253
|
+
const addCallback = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'app:addmarker')[1]
|
|
254
|
+
const updateCallback = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'app:updatemarker')[1]
|
|
255
|
+
const removeCallback = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'app:removemarker')[1]
|
|
256
|
+
unmount()
|
|
257
|
+
expect(ctx.mockEventBus.off).toHaveBeenCalledWith('app:addmarker', addCallback)
|
|
258
|
+
expect(ctx.mockEventBus.off).toHaveBeenCalledWith('app:updatemarker', updateCallback)
|
|
259
|
+
expect(ctx.mockEventBus.off).toHaveBeenCalledWith('app:removemarker', removeCallback)
|
|
191
260
|
})
|
|
192
261
|
})
|
|
@@ -256,9 +256,10 @@ describe('useModalPanelBehaviour', () => {
|
|
|
256
256
|
})
|
|
257
257
|
|
|
258
258
|
it('handles null focus targets gracefully', () => {
|
|
259
|
+
refs.panel.current.focus = jest.fn()
|
|
259
260
|
render(<TestComponent />)
|
|
260
261
|
dispatchFocusIn(null)
|
|
261
|
-
expect(
|
|
262
|
+
expect(refs.panel.current.focus).not.toHaveBeenCalled()
|
|
262
263
|
})
|
|
263
264
|
})
|
|
264
265
|
|
|
@@ -10,6 +10,7 @@ import { Attributions } from '../components/Attributions/Attributions'
|
|
|
10
10
|
import { layoutSlots } from '../renderer/slots'
|
|
11
11
|
import { SlotRenderer } from '../renderer/SlotRenderer'
|
|
12
12
|
import { HtmlElementHost } from '../renderer/HtmlElementHost'
|
|
13
|
+
import { Hints } from '../components/Hints/Hints.jsx'
|
|
13
14
|
import { getMapThemeVars } from '../../config/mapTheme.js'
|
|
14
15
|
|
|
15
16
|
// eslint-disable-next-line camelcase, react/jsx-pascal-case
|
|
@@ -110,6 +111,7 @@ export const Layout = () => {
|
|
|
110
111
|
</div>
|
|
111
112
|
</div>
|
|
112
113
|
</div>
|
|
114
|
+
<Hints />
|
|
113
115
|
<HtmlElementHost />
|
|
114
116
|
</div>
|
|
115
117
|
)
|
|
@@ -21,6 +21,9 @@ jest.mock('../components/Attributions/Attributions', () => ({
|
|
|
21
21
|
jest.mock('../renderer/SlotRenderer', () => ({
|
|
22
22
|
SlotRenderer: jest.fn(({ slot }) => <div data-testid={`slot-${slot.toLowerCase()}`} />)
|
|
23
23
|
}))
|
|
24
|
+
jest.mock('../components/Hints/Hints', () => ({
|
|
25
|
+
Hints: jest.fn(() => null)
|
|
26
|
+
}))
|
|
24
27
|
|
|
25
28
|
// Mock hooks
|
|
26
29
|
jest.mock('../store/configContext', () => ({ useConfig: jest.fn() }))
|
|
@@ -10,8 +10,9 @@ const pluginShortcutIds = new Set()
|
|
|
10
10
|
let providerSupportedIds = new Set()
|
|
11
11
|
|
|
12
12
|
export const registerKeyboardShortcut = ({ shortcut }) => {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
if (pluginShortcutIds.has(shortcut.id)) {
|
|
14
|
+
pluginShortcutHelp[pluginShortcutHelp.findIndex(s => s.id === shortcut.id)] = shortcut
|
|
15
|
+
} else {
|
|
15
16
|
pluginShortcutIds.add(shortcut.id)
|
|
16
17
|
pluginShortcutHelp.push(shortcut)
|
|
17
18
|
}
|
|
@@ -29,17 +29,15 @@ describe('keyboardShortcutRegistry', () => {
|
|
|
29
29
|
expect(shortcuts).toContain(shortcut)
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
test('registerKeyboardShortcut
|
|
32
|
+
test('registerKeyboardShortcut updates an existing shortcut when re-registered with the same id', () => {
|
|
33
33
|
const shortcut = { id: 'duplicate', description: 'First' }
|
|
34
|
-
const
|
|
35
|
-
// Register the first one
|
|
34
|
+
const updatedShortcut = { id: 'duplicate', description: 'Second' }
|
|
36
35
|
registerKeyboardShortcut({ shortcut })
|
|
37
|
-
|
|
38
|
-
registerKeyboardShortcut({ shortcut: duplicateShortcut })
|
|
36
|
+
registerKeyboardShortcut({ shortcut: updatedShortcut })
|
|
39
37
|
const shortcuts = getKeyboardShortcuts()
|
|
40
|
-
|
|
41
|
-
expect(shortcuts).toContain(shortcut)
|
|
42
|
-
expect(shortcuts).
|
|
38
|
+
expect(shortcuts).toContain(updatedShortcut)
|
|
39
|
+
expect(shortcuts).not.toContain(shortcut)
|
|
40
|
+
expect(shortcuts.filter(s => s.id === 'duplicate')).toHaveLength(1)
|
|
43
41
|
})
|
|
44
42
|
|
|
45
43
|
test('setProviderSupportedShortcuts should filter core shortcuts', () => {
|
|
@@ -52,10 +52,14 @@ function getMatchingButtons ({ appState, buttonConfig, slot, evaluateProp }) {
|
|
|
52
52
|
function createButtonClickHandler (btn, appState, evaluateProp) {
|
|
53
53
|
const [, config] = btn
|
|
54
54
|
const isPanelOpen = !!(config.panelId && appState.openPanels[config.panelId])
|
|
55
|
+
const isToggle = config.isPressed !== undefined || !!config.pressedWhen
|
|
55
56
|
|
|
56
57
|
return (e) => {
|
|
57
58
|
if (typeof config.onClick === 'function') {
|
|
58
59
|
config.onClick(e, evaluateProp(ctx => ctx, config.pluginId))
|
|
60
|
+
if (!isToggle && !config.keepFocus) {
|
|
61
|
+
requestAnimationFrame(() => appState.layoutRefs.viewportRef.current?.focus())
|
|
62
|
+
}
|
|
59
63
|
return
|
|
60
64
|
}
|
|
61
65
|
|
|
@@ -65,7 +69,7 @@ function createButtonClickHandler (btn, appState, evaluateProp) {
|
|
|
65
69
|
type: isPanelOpen ? 'CLOSE_PANEL' : 'OPEN_PANEL',
|
|
66
70
|
payload: isPanelOpen
|
|
67
71
|
? config.panelId
|
|
68
|
-
: { panelId: config.panelId, props: { triggeringElement } }
|
|
72
|
+
: { panelId: config.panelId, props: { triggeringElement }, ...(config.keepFocus && { focusOnOpen: false }) }
|
|
69
73
|
})
|
|
70
74
|
}
|
|
71
75
|
}
|