@defra/interactive-map 0.0.22-alpha → 0.0.23-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/button-definition.md +39 -0
- package/docs/api/context.md +4 -4
- package/docs/api/map-style-config.md +11 -2
- package/docs/api/marker-config.md +38 -7
- package/docs/api/panel-definition.md +7 -3
- package/docs/api/symbol-config.md +10 -26
- package/docs/api/symbol-registry.md +28 -13
- package/docs/api.md +24 -2
- package/docs/assets/css/{docusaurus.css → docusaurus.scss} +18 -12
- package/docs/assets/images/toggle-marker-label.jpg +0 -0
- package/docs/examples/index.mdx +12 -0
- package/docs/examples/toggle-marker-label.mdx +89 -0
- package/docs/plugins/interact.md +4 -11
- package/docusaurus.config.cjs +2 -1
- package/package.json +8 -8
- package/plugins/beta/datasets/dist/adapters/maplibre/esm/im-datasets-ml-adapter.js +1 -1
- package/plugins/beta/datasets/dist/css/index.css +2 -2
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-maplibre-adapter.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/index.js +1 -1
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +5 -5
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +24 -29
- package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +2 -2
- package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +3 -3
- package/plugins/beta/datasets/src/components/KeySvg.jsx +2 -2
- package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +1 -2
- package/plugins/beta/datasets/src/components/svgProperties.js +2 -2
- package/plugins/beta/datasets/src/panels/Key.module.scss +2 -2
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/index.js +1 -1
- package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/src/manifest.js +1 -0
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/index.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +23 -55
- package/plugins/interact/src/InteractInit.test.js +58 -80
- package/plugins/interact/src/defaults.js +1 -3
- package/plugins/interact/src/hooks/useAttachEvents.js +46 -0
- package/plugins/interact/src/hooks/useAttachEvents.test.js +65 -0
- package/plugins/interact/src/hooks/useCrossHairVisibility.js +47 -0
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +54 -0
- package/plugins/interact/src/hooks/useHighlightSync.js +30 -8
- package/plugins/interact/src/hooks/useHighlightSync.test.js +127 -80
- package/plugins/interact/src/hooks/useInteractionHandlers.js +18 -17
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +22 -61
- package/plugins/interact/src/hooks/useMapItemList.js +187 -0
- package/plugins/interact/src/hooks/useMapItemList.test.js +584 -0
- package/plugins/interact/src/index.js +0 -2
- package/plugins/interact/src/index.test.js +0 -2
- package/plugins/interact/src/manifest.js +37 -6
- package/plugins/interact/src/reducer.js +8 -4
- package/plugins/interact/src/reducer.test.js +18 -3
- package/plugins/interact/src/utils/buildStylesMap.js +21 -10
- package/plugins/interact/src/utils/buildStylesMap.test.js +29 -29
- package/plugins/interact/src/utils/spatial.js +0 -15
- package/plugins/interact/src/utils/spatial.test.js +1 -13
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/src/components/CloseButton/CloseButton.module.scss +2 -0
- package/plugins/search/src/components/OpenButton/OpenButton.jsx +1 -1
- package/plugins/search/src/components/OpenButton/OpenButton.module.scss +3 -0
- package/plugins/search/src/search.scss +1 -0
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/esriProvider.js +4 -0
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/esm/index.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/index.js +1 -1
- package/providers/maplibre/src/index.test.js +9 -14
- package/providers/maplibre/src/mapEvents.js +11 -5
- package/providers/maplibre/src/mapEvents.test.js +14 -3
- package/providers/maplibre/src/maplibreProvider.js +18 -11
- package/providers/maplibre/src/maplibreProvider.test.js +43 -36
- package/providers/maplibre/src/utils/highlightFeatures.js +102 -57
- package/providers/maplibre/src/utils/highlightFeatures.test.js +162 -51
- package/providers/maplibre/src/utils/maplibreFixes.js +10 -0
- package/providers/maplibre/src/utils/maplibreFixes.test.js +31 -0
- package/providers/maplibre/src/utils/patternImages.js +10 -12
- package/providers/maplibre/src/utils/patternImages.test.js +145 -167
- package/providers/maplibre/src/utils/rasteriseToImageData.js +1 -4
- package/providers/maplibre/src/utils/rasteriseToImageData.test.js +23 -24
- package/providers/maplibre/src/utils/symbolImages.js +23 -86
- package/providers/maplibre/src/utils/symbolImages.test.js +60 -105
- package/src/App/components/Actions/Actions.module.scss +1 -0
- package/src/App/components/Actions/Actions.test.jsx +17 -4
- package/src/App/components/Hints/Hints.jsx +52 -0
- package/src/App/components/Hints/Hints.module.scss +37 -0
- package/src/App/components/Hints/Hints.test.jsx +104 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +57 -19
- package/src/App/components/KeyboardHelp/KeyboardHelp.module.scss +10 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +140 -31
- package/src/App/components/Markers/LabelMarker.jsx +14 -0
- package/src/App/components/Markers/LabelMarker.test.jsx +36 -0
- package/src/App/components/Markers/Markers.jsx +44 -75
- package/src/App/components/Markers/Markers.module.scss +58 -0
- package/src/App/components/Markers/Markers.test.jsx +90 -113
- package/src/App/components/Markers/SymbolLabelMarker.jsx +43 -0
- package/src/App/components/Markers/SymbolLabelMarker.test.jsx +56 -0
- package/src/App/components/Markers/SymbolMarker.jsx +31 -0
- package/src/App/components/Markers/SymbolMarker.test.jsx +72 -0
- package/src/App/components/Panel/Panel.jsx +1 -1
- package/src/App/components/Panel/Panel.module.scss +1 -0
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +117 -1
- package/src/App/components/PopupMenu/usePopupMenu.js +132 -85
- package/src/App/components/Tabs/Tabs.jsx +68 -0
- package/src/App/components/Tabs/Tabs.module.scss +57 -0
- package/src/App/components/Tabs/Tabs.test.jsx +121 -0
- package/src/App/components/Tooltip/Tooltip.module.scss +1 -1
- package/src/App/components/Viewport/Features.jsx +35 -0
- package/src/App/components/Viewport/Features.module.scss +31 -0
- package/src/App/components/Viewport/Features.test.jsx +124 -0
- package/src/App/components/Viewport/MapStatus.jsx +0 -4
- package/src/App/components/Viewport/Viewport.jsx +35 -29
- package/src/App/components/Viewport/Viewport.module.scss +7 -21
- package/src/App/components/Viewport/Viewport.test.jsx +108 -65
- package/src/App/controls/keyboardActions.js +9 -3
- package/src/App/controls/keyboardActions.test.js +122 -87
- package/src/App/controls/keyboardShortcuts.js +1 -14
- package/src/App/hooks/useFeatureFocus.js +199 -0
- package/src/App/hooks/useFeatureFocus.test.js +635 -0
- package/src/App/hooks/useFeatureItems.js +30 -0
- package/src/App/hooks/useFeatureItems.test.js +97 -0
- package/src/App/hooks/useFocusVisible.js +4 -7
- package/src/App/hooks/useFocusVisible.test.js +6 -6
- package/src/App/hooks/useKeyboardHint.js +9 -34
- package/src/App/hooks/useKeyboardHint.test.js +34 -84
- package/src/App/hooks/useKeyboardShortcuts.js +8 -5
- package/src/App/hooks/useKeyboardShortcuts.test.js +16 -12
- package/src/App/hooks/useLayoutMeasurements.js +10 -4
- package/src/App/hooks/useLayoutMeasurements.test.js +30 -0
- package/src/App/hooks/useMarkerCursor.js +33 -0
- package/src/App/hooks/useMarkerCursor.test.js +87 -0
- package/src/App/hooks/useMarkersAPI.js +32 -9
- package/src/App/hooks/useMarkersAPI.test.js +162 -94
- package/src/App/layout/Layout.jsx +2 -0
- package/src/App/layout/Layout.test.jsx +3 -0
- package/src/App/registry/keyboardShortcutRegistry.js +3 -2
- package/src/App/registry/keyboardShortcutRegistry.test.js +6 -8
- package/src/App/renderer/mapButtons.js +5 -1
- package/src/App/renderer/mapButtons.test.js +37 -1
- package/src/App/store/ServiceProvider.jsx +4 -1
- package/src/App/store/ServiceProvider.test.jsx +11 -0
- package/src/App/store/appActionsMap.js +5 -1
- package/src/App/store/appActionsMap.test.js +13 -0
- package/src/App/store/appReducer.js +1 -0
- package/src/App/store/mapActionsMap.js +2 -1
- package/src/InteractiveMap/InteractiveMap.js +12 -0
- package/src/InteractiveMap/InteractiveMap.test.js +2 -0
- package/src/config/appConfig.js +3 -1
- package/src/config/appConfig.test.js +4 -0
- package/src/config/defaults.js +1 -1
- package/src/config/events.js +8 -0
- package/src/config/mapTheme.js +8 -4
- package/src/config/symbolConfig.js +23 -16
- package/src/scss/main.scss +3 -0
- package/src/scss/tools/_border-focus.scss +0 -1
- package/src/services/eventBus.js +15 -0
- package/src/services/eventBus.test.js +11 -0
- package/src/services/hints.js +47 -0
- package/src/services/hints.test.js +110 -0
- package/src/services/patternRegistry.js +69 -1
- package/src/services/patternRegistry.test.js +112 -1
- package/src/services/reverseGeocode.js +4 -0
- package/src/services/reverseGeocode.test.js +11 -1
- package/src/services/symbolRegistry.js +152 -17
- package/src/services/symbolRegistry.test.js +141 -31
- package/src/types.js +71 -14
- package/src/utils/constrainKeyboardFocus.js +5 -5
- package/src/utils/detectInterfaceType.js +27 -5
- package/src/utils/detectInterfaceType.test.js +29 -3
- package/src/utils/patternUtils.js +4 -67
- package/src/utils/patternUtils.test.js +1 -123
- package/src/utils/symbolUtils.js +11 -13
- package/src/utils/symbolUtils.test.js +18 -26
- package/plugins/interact/dist/css/index.css +0 -1
- package/plugins/interact/src/interact.scss +0 -24
|
@@ -0,0 +1,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,185 @@ 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
|
-
if (renderCallback) act(() => renderCallback()) // line 60: early return
|
|
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())
|
|
115
120
|
})
|
|
116
121
|
|
|
117
122
|
it('updates positions on mapSize change', () => {
|
|
118
|
-
mockMarkers.items = [{ id: 'm1', coords: { lat: 1, lng: 1 } }]
|
|
123
|
+
ctx.mockMarkers.items = [{ id: 'm1', coords: { lat: 1, lng: 1 } }]
|
|
119
124
|
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
|
-
})
|
|
125
|
+
act(() => result.current.markerRef('m1')(ctx.mockElement))
|
|
126
|
+
useMap.mockReturnValue({ markers: ctx.mockMarkers, dispatch: ctx.mockDispatch, mapSize: 'large', isMapReady: true })
|
|
128
127
|
rerender()
|
|
129
|
-
expect(mockElement.style.transform).toBe('translate(300px, 600px)')
|
|
128
|
+
expect(ctx.mockElement.style.transform).toBe('translate(300px, 600px)')
|
|
130
129
|
})
|
|
130
|
+
})
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
renderHook(() => useMarkers())
|
|
134
|
-
const addPayload = { id: 'm1', coords: { lat: 1, lng: 1 }, options: { label: 'Test' } }
|
|
132
|
+
// ─── useMarkers — add/update/remove event handlers ───────────────────────────
|
|
135
133
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
describe('useMarkers — add/update/remove event handlers', () => {
|
|
135
|
+
let ctx
|
|
136
|
+
beforeEach(() => { ctx = makeCtx() })
|
|
137
|
+
|
|
138
|
+
it('handles app:addmarker — dispatches with projected coords', () => {
|
|
139
|
+
renderHook(() => useMarkers())
|
|
140
|
+
const handleAddMarker = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'app:addmarker')[1]
|
|
141
|
+
act(() => handleAddMarker({ id: 'm1', coords: { lat: 1, lng: 1 }, options: { label: 'Test' } }))
|
|
142
|
+
expect(ctx.mockDispatch).toHaveBeenCalledWith({
|
|
139
143
|
type: 'UPSERT_LOCATION_MARKER',
|
|
140
144
|
payload: { id: 'm1', coords: { lat: 1, lng: 1 }, label: 'Test', x: 200, y: 400, isVisible: true }
|
|
141
145
|
})
|
|
142
146
|
})
|
|
143
147
|
|
|
144
|
-
it('
|
|
148
|
+
it('handles app:addmarker guard — ignores missing id or coords', () => {
|
|
145
149
|
renderHook(() => useMarkers())
|
|
146
|
-
const handleAddMarker = mockEventBus.on.mock.calls.find(call => call[0] === 'app:addmarker')[1]
|
|
147
|
-
|
|
150
|
+
const handleAddMarker = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'app:addmarker')[1]
|
|
148
151
|
act(() => handleAddMarker(undefined))
|
|
149
152
|
act(() => handleAddMarker(null))
|
|
150
|
-
act(() => handleAddMarker({}))
|
|
151
|
-
act(() => handleAddMarker({ id: 'm1' }))
|
|
152
|
-
expect(mockDispatch).not.toHaveBeenCalled()
|
|
153
|
+
act(() => handleAddMarker({}))
|
|
154
|
+
act(() => handleAddMarker({ id: 'm1' }))
|
|
155
|
+
expect(ctx.mockDispatch).not.toHaveBeenCalled()
|
|
153
156
|
})
|
|
154
157
|
|
|
155
|
-
it('handles app:
|
|
158
|
+
it('handles app:updatemarker guard — ignores missing id (lines 71-73)', () => {
|
|
159
|
+
ctx.mockMarkers.items = [{ id: 'm1', coords: { lat: 1, lng: 1 } }]
|
|
156
160
|
renderHook(() => useMarkers())
|
|
157
|
-
const
|
|
161
|
+
const handleUpdateMarker = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'app:updatemarker')[1]
|
|
162
|
+
act(() => handleUpdateMarker(undefined))
|
|
163
|
+
act(() => handleUpdateMarker(null))
|
|
164
|
+
act(() => handleUpdateMarker({}))
|
|
165
|
+
expect(ctx.mockDispatch).not.toHaveBeenCalled()
|
|
166
|
+
})
|
|
158
167
|
|
|
159
|
-
|
|
160
|
-
|
|
168
|
+
it('handles app:updatemarker — dispatches update for valid id (lines 74-75)', () => {
|
|
169
|
+
ctx.mockMarkers.items = [{ id: 'm1', coords: { lat: 1, lng: 1 } }]
|
|
170
|
+
renderHook(() => useMarkers())
|
|
171
|
+
const handleUpdateMarker = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'app:updatemarker')[1]
|
|
172
|
+
act(() => handleUpdateMarker({ id: 'm1', options: { showLabel: true } }))
|
|
173
|
+
expect(ctx.mockDispatch).toHaveBeenCalledWith({
|
|
174
|
+
type: 'UPSERT_LOCATION_MARKER',
|
|
175
|
+
payload: { id: 'm1', coords: { lat: 1, lng: 1 }, showLabel: true, x: 200, y: 400 }
|
|
176
|
+
})
|
|
177
|
+
})
|
|
161
178
|
|
|
162
|
-
|
|
179
|
+
it('handles app:removemarker — dispatches and guards null/undefined', () => {
|
|
180
|
+
renderHook(() => useMarkers())
|
|
181
|
+
const handleRemoveMarker = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'app:removemarker')[1]
|
|
182
|
+
act(() => handleRemoveMarker('m1'))
|
|
183
|
+
expect(ctx.mockDispatch).toHaveBeenCalledWith({ type: 'REMOVE_LOCATION_MARKER', payload: 'm1' })
|
|
184
|
+
act(() => handleRemoveMarker(undefined))
|
|
163
185
|
act(() => handleRemoveMarker(null))
|
|
164
|
-
expect(mockDispatch).toHaveBeenCalledTimes(1)
|
|
186
|
+
expect(ctx.mockDispatch).toHaveBeenCalledTimes(1)
|
|
165
187
|
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// ─── useMarkers — markers.update ─────────────────────────────────────────────
|
|
166
191
|
|
|
167
|
-
|
|
192
|
+
describe('useMarkers — markers.update', () => {
|
|
193
|
+
let ctx
|
|
194
|
+
beforeEach(() => { ctx = makeCtx() })
|
|
195
|
+
|
|
196
|
+
it('dispatches using existing coords when none provided (lines 127-134)', () => {
|
|
197
|
+
ctx.mockMarkers.items = [{ id: 'm1', coords: { lat: 1, lng: 1 }, label: 'Old' }]
|
|
168
198
|
const { result } = renderHook(() => useMarkers())
|
|
199
|
+
act(() => result.current.markers.update('m1', { showLabel: true }))
|
|
200
|
+
expect(ctx.mockDispatch).toHaveBeenCalledWith({
|
|
201
|
+
type: 'UPSERT_LOCATION_MARKER',
|
|
202
|
+
payload: { id: 'm1', coords: { lat: 1, lng: 1 }, showLabel: true, x: 200, y: 400 }
|
|
203
|
+
})
|
|
204
|
+
})
|
|
169
205
|
|
|
170
|
-
|
|
171
|
-
|
|
206
|
+
it('is a no-op when marker id is not found (lines 128-130)', () => {
|
|
207
|
+
ctx.mockMarkers.items = []
|
|
208
|
+
const { result } = renderHook(() => useMarkers())
|
|
209
|
+
act(() => result.current.markers.update('nonexistent', { showLabel: true }))
|
|
210
|
+
expect(ctx.mockDispatch).not.toHaveBeenCalled()
|
|
211
|
+
})
|
|
172
212
|
|
|
173
|
-
|
|
174
|
-
|
|
213
|
+
it('uses provided coords over existing (lines 131-133)', () => {
|
|
214
|
+
ctx.mockMarkers.items = [{ id: 'm1', coords: { lat: 1, lng: 1 } }]
|
|
215
|
+
const { result } = renderHook(() => useMarkers())
|
|
216
|
+
const newCoords = { lat: 5, lng: 5 }
|
|
217
|
+
act(() => result.current.markers.update('m1', { coords: newCoords, showLabel: true }))
|
|
218
|
+
expect(ctx.mockDispatch).toHaveBeenCalledWith({
|
|
219
|
+
type: 'UPSERT_LOCATION_MARKER',
|
|
220
|
+
payload: { id: 'm1', coords: newCoords, showLabel: true, x: 200, y: 400 }
|
|
221
|
+
})
|
|
222
|
+
})
|
|
175
223
|
|
|
176
|
-
|
|
224
|
+
it('uses default empty options when none provided (line 126)', () => {
|
|
225
|
+
ctx.mockMarkers.items = [{ id: 'm1', coords: { lat: 1, lng: 1 } }]
|
|
226
|
+
const { result } = renderHook(() => useMarkers())
|
|
227
|
+
act(() => result.current.markers.update('m1'))
|
|
228
|
+
expect(ctx.mockDispatch).toHaveBeenCalledWith({
|
|
229
|
+
type: 'UPSERT_LOCATION_MARKER',
|
|
230
|
+
payload: { id: 'm1', coords: { lat: 1, lng: 1 }, x: 200, y: 400 }
|
|
231
|
+
})
|
|
177
232
|
})
|
|
233
|
+
})
|
|
178
234
|
|
|
179
|
-
|
|
180
|
-
const { unmount } = renderHook(() => useMarkers())
|
|
235
|
+
// ─── useMarkers — cleanup ─────────────────────────────────────────────────────
|
|
181
236
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
237
|
+
describe('useMarkers — cleanup', () => {
|
|
238
|
+
let ctx
|
|
239
|
+
beforeEach(() => { ctx = makeCtx() })
|
|
185
240
|
|
|
186
|
-
|
|
241
|
+
it('cleans up map:render listener when markerRef is called with null', () => {
|
|
242
|
+
const { result } = renderHook(() => useMarkers())
|
|
243
|
+
let cleanup
|
|
244
|
+
act(() => { cleanup = result.current.markerRef('m1')(ctx.mockElement) })
|
|
245
|
+
const updateCallback = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'map:render')[1]
|
|
246
|
+
act(() => { if (cleanup) cleanup() })
|
|
247
|
+
expect(ctx.mockEventBus.off).toHaveBeenCalledWith('map:render', updateCallback)
|
|
248
|
+
})
|
|
187
249
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
250
|
+
it('cleans up add/update/remove eventBus listeners on unmount', () => {
|
|
251
|
+
const { unmount } = renderHook(() => useMarkers())
|
|
252
|
+
const addCallback = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'app:addmarker')[1]
|
|
253
|
+
const updateCallback = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'app:updatemarker')[1]
|
|
254
|
+
const removeCallback = ctx.mockEventBus.on.mock.calls.find(call => call[0] === 'app:removemarker')[1]
|
|
255
|
+
unmount()
|
|
256
|
+
expect(ctx.mockEventBus.off).toHaveBeenCalledWith('app:addmarker', addCallback)
|
|
257
|
+
expect(ctx.mockEventBus.off).toHaveBeenCalledWith('app:updatemarker', updateCallback)
|
|
258
|
+
expect(ctx.mockEventBus.off).toHaveBeenCalledWith('app:removemarker', removeCallback)
|
|
191
259
|
})
|
|
192
260
|
})
|
|
@@ -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
|
}
|