@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
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// src/components/KeyboardHelp.test.jsx
|
|
2
2
|
import React from 'react'
|
|
3
|
-
import { render, screen,
|
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
4
4
|
import { KeyboardHelp } from './KeyboardHelp'
|
|
5
5
|
import { getKeyboardShortcuts } from '../../registry/keyboardShortcutRegistry.js'
|
|
6
6
|
import { useConfig } from '../../store/configContext'
|
|
7
|
+
import { useApp } from '../../store/appContext.js'
|
|
7
8
|
|
|
8
9
|
jest.mock('../../registry/keyboardShortcutRegistry.js', () => ({
|
|
9
10
|
getKeyboardShortcuts: jest.fn()
|
|
@@ -13,51 +14,159 @@ jest.mock('../../store/configContext', () => ({
|
|
|
13
14
|
useConfig: jest.fn()
|
|
14
15
|
}))
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
jest.mock('../../store/appContext.js', () => ({
|
|
18
|
+
useApp: jest.fn()
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
jest.mock('../../hooks/useResizeObserver.js', () => ({ useResizeObserver: jest.fn() }))
|
|
22
|
+
|
|
23
|
+
const SELECT_FEATURES_GROUP = 'Select features'
|
|
24
|
+
const DEFAULT_GROUP = 'Navigate'
|
|
25
|
+
|
|
26
|
+
// Ungrouped — mirrors real core shortcuts (no group, plain context)
|
|
27
|
+
const VIEWPORT_SHORTCUTS = [
|
|
28
|
+
{ id: '1', context: 'viewport', title: 'Move', command: '<kbd>↑</kbd>' },
|
|
29
|
+
{ id: '2', context: 'viewport', title: 'Zoom', command: '<kbd>+</kbd>' }
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const LISTBOX_SHORTCUTS = [
|
|
33
|
+
{ id: '3', group: SELECT_FEATURES_GROUP, context: 'listbox', title: 'Navigate', command: '<kbd>↓</kbd>' },
|
|
34
|
+
{ id: '4', group: SELECT_FEATURES_GROUP, context: 'listbox', title: 'Select', command: '<kbd>Enter</kbd>' }
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
const GLOBAL_SHORTCUTS = [
|
|
38
|
+
{ id: '5', context: 'global', title: 'Help', command: '<kbd>Alt</kbd>' }
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
useConfig.mockReturnValue({})
|
|
43
|
+
useApp.mockReturnValue({ listboxIsActive: false })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
jest.clearAllMocks()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// ─── flat list (single group) ─────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
describe('KeyboardHelp — flat list', () => {
|
|
53
|
+
it('renders a flat list with no tabs when all shortcuts share one group', () => {
|
|
54
|
+
getKeyboardShortcuts.mockReturnValue(VIEWPORT_SHORTCUTS)
|
|
55
|
+
render(<KeyboardHelp />)
|
|
56
|
+
expect(document.querySelector('.im-c-keyboard-help')).toBeInTheDocument()
|
|
57
|
+
expect(screen.queryByRole('tablist')).not.toBeInTheDocument()
|
|
58
|
+
expect(screen.getByText('Move')).toBeInTheDocument()
|
|
59
|
+
expect(screen.getByText('Zoom')).toBeInTheDocument()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('renders a flat list with no tabs when there are no shortcuts', () => {
|
|
63
|
+
getKeyboardShortcuts.mockReturnValue([])
|
|
64
|
+
render(<KeyboardHelp />)
|
|
65
|
+
expect(document.querySelector('.im-c-keyboard-help')).toBeInTheDocument()
|
|
66
|
+
expect(screen.queryByRole('tablist')).not.toBeInTheDocument()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('renders command HTML', () => {
|
|
70
|
+
getKeyboardShortcuts.mockReturnValue(VIEWPORT_SHORTCUTS)
|
|
71
|
+
render(<KeyboardHelp />)
|
|
72
|
+
expect(screen.getByText('↑')).toBeInTheDocument()
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// ─── tab UI (multiple groups) ─────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
describe('KeyboardHelp — tabs', () => {
|
|
79
|
+
const allShortcuts = [...GLOBAL_SHORTCUTS, ...VIEWPORT_SHORTCUTS, ...LISTBOX_SHORTCUTS]
|
|
80
|
+
|
|
17
81
|
beforeEach(() => {
|
|
18
|
-
|
|
82
|
+
useApp.mockReturnValue({ listboxIsActive: true })
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('renders a tab for each group when listboxIsActive is true', () => {
|
|
86
|
+
getKeyboardShortcuts.mockReturnValue(allShortcuts)
|
|
87
|
+
render(<KeyboardHelp />)
|
|
88
|
+
expect(screen.getByRole('tablist')).toBeInTheDocument()
|
|
89
|
+
expect(screen.getByRole('tab', { name: DEFAULT_GROUP })).toBeInTheDocument()
|
|
90
|
+
expect(screen.getByRole('tab', { name: SELECT_FEATURES_GROUP })).toBeInTheDocument()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('defaults to first viewport-context tab when context is viewport', () => {
|
|
94
|
+
getKeyboardShortcuts.mockReturnValue(allShortcuts)
|
|
95
|
+
render(<KeyboardHelp context='viewport' />)
|
|
96
|
+
expect(screen.getByRole('tab', { name: DEFAULT_GROUP })).toHaveAttribute('aria-selected', 'true')
|
|
97
|
+
expect(screen.getByText('Move')).toBeInTheDocument()
|
|
19
98
|
})
|
|
20
99
|
|
|
21
|
-
|
|
22
|
-
|
|
100
|
+
it('defaults to first listbox-context tab when context is listbox', () => {
|
|
101
|
+
getKeyboardShortcuts.mockReturnValue(allShortcuts)
|
|
102
|
+
render(<KeyboardHelp context='listbox' />)
|
|
103
|
+
expect(screen.getByRole('tab', { name: SELECT_FEATURES_GROUP })).toHaveAttribute('aria-selected', 'true')
|
|
104
|
+
expect(screen.getByText('Select')).toBeInTheDocument()
|
|
23
105
|
})
|
|
24
106
|
|
|
25
|
-
it('
|
|
107
|
+
it('falls back to first global-context tab when no exact match exists', () => {
|
|
108
|
+
getKeyboardShortcuts.mockReturnValue([...GLOBAL_SHORTCUTS, ...LISTBOX_SHORTCUTS])
|
|
109
|
+
render(<KeyboardHelp context='viewport' />)
|
|
110
|
+
expect(screen.getByRole('tab', { name: DEFAULT_GROUP })).toHaveAttribute('aria-selected', 'true')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('falls back to first tab when no exact or global-context match exists', () => {
|
|
26
114
|
getKeyboardShortcuts.mockReturnValue([
|
|
27
|
-
{ id: '
|
|
28
|
-
{ id: '
|
|
29
|
-
{ id: '3', group: 'Editing', title: 'Copy', command: '<kbd>Ctrl+C</kbd>' }
|
|
115
|
+
{ id: 'a', group: 'Group A', context: 'listbox', title: 'A', command: '<kbd>A</kbd>' },
|
|
116
|
+
{ id: 'b', group: 'Group B', context: 'listbox', title: 'B', command: '<kbd>B</kbd>' }
|
|
30
117
|
])
|
|
118
|
+
render(<KeyboardHelp context='viewport' />)
|
|
119
|
+
expect(screen.getByRole('tab', { name: 'Group A' })).toHaveAttribute('aria-selected', 'true')
|
|
120
|
+
})
|
|
31
121
|
|
|
32
|
-
|
|
122
|
+
it('shows only the active tab panel content', () => {
|
|
123
|
+
getKeyboardShortcuts.mockReturnValue(allShortcuts)
|
|
124
|
+
render(<KeyboardHelp context='viewport' />)
|
|
125
|
+
expect(screen.getByText('Move')).toBeInTheDocument()
|
|
126
|
+
expect(screen.queryByText('Select')).not.toBeInTheDocument()
|
|
127
|
+
})
|
|
33
128
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
129
|
+
it('switches tab and content on click', () => {
|
|
130
|
+
getKeyboardShortcuts.mockReturnValue(allShortcuts)
|
|
131
|
+
render(<KeyboardHelp context='viewport' />)
|
|
132
|
+
fireEvent.click(screen.getByRole('tab', { name: SELECT_FEATURES_GROUP }))
|
|
133
|
+
expect(screen.getByRole('tab', { name: SELECT_FEATURES_GROUP })).toHaveAttribute('aria-selected', 'true')
|
|
134
|
+
expect(screen.getByText('Select')).toBeInTheDocument()
|
|
135
|
+
expect(screen.queryByText('Move')).not.toBeInTheDocument()
|
|
136
|
+
})
|
|
37
137
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
138
|
+
it('ungrouped shortcuts default to Navigate group', () => {
|
|
139
|
+
getKeyboardShortcuts.mockReturnValue([
|
|
140
|
+
{ id: 'x', title: 'No group', command: '<kbd>X</kbd>' },
|
|
141
|
+
...LISTBOX_SHORTCUTS
|
|
142
|
+
])
|
|
143
|
+
render(<KeyboardHelp />)
|
|
144
|
+
fireEvent.click(screen.getByRole('tab', { name: DEFAULT_GROUP }))
|
|
145
|
+
expect(screen.getByText('No group')).toBeInTheDocument()
|
|
146
|
+
})
|
|
147
|
+
})
|
|
43
148
|
|
|
44
|
-
|
|
45
|
-
const editGroup = screen.getByText('Copy').closest('.im-c-keyboard-help__group')
|
|
46
|
-
expect(editGroup).toBeInTheDocument()
|
|
149
|
+
// ─── listboxIsActive filtering ────────────────────────────────────────────────
|
|
47
150
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
expect(screen.getByText('/')).toBeInTheDocument()
|
|
51
|
-
expect(screen.getByText('Ctrl+C')).toBeInTheDocument()
|
|
52
|
-
})
|
|
151
|
+
describe('KeyboardHelp — listboxIsActive filtering', () => {
|
|
152
|
+
const allShortcuts = [...GLOBAL_SHORTCUTS, ...VIEWPORT_SHORTCUTS, ...LISTBOX_SHORTCUTS]
|
|
53
153
|
|
|
54
|
-
it('
|
|
55
|
-
getKeyboardShortcuts.mockReturnValue(
|
|
154
|
+
it('hides the Select features tab when listboxIsActive is false', () => {
|
|
155
|
+
getKeyboardShortcuts.mockReturnValue(allShortcuts)
|
|
156
|
+
render(<KeyboardHelp />)
|
|
157
|
+
expect(screen.queryByRole('tab', { name: SELECT_FEATURES_GROUP })).not.toBeInTheDocument()
|
|
158
|
+
})
|
|
56
159
|
|
|
160
|
+
it('still shows ungrouped shortcuts when listboxIsActive is false', () => {
|
|
161
|
+
getKeyboardShortcuts.mockReturnValue(allShortcuts)
|
|
57
162
|
render(<KeyboardHelp />)
|
|
163
|
+
expect(screen.getByText('Move')).toBeInTheDocument()
|
|
164
|
+
})
|
|
58
165
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
166
|
+
it('renders as a flat list when only ungrouped shortcuts remain after filtering', () => {
|
|
167
|
+
getKeyboardShortcuts.mockReturnValue([...VIEWPORT_SHORTCUTS, ...LISTBOX_SHORTCUTS])
|
|
168
|
+
render(<KeyboardHelp />)
|
|
169
|
+
expect(screen.queryByRole('tablist')).not.toBeInTheDocument()
|
|
170
|
+
expect(screen.getByText('Move')).toBeInTheDocument()
|
|
62
171
|
})
|
|
63
172
|
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const LabelMarker = ({ marker, mapId, markerRef }) => ( // NOSONAR: project does not use PropTypes
|
|
2
|
+
<div
|
|
3
|
+
ref={markerRef(marker.id)}
|
|
4
|
+
id={`${mapId}-marker-${marker.id}`}
|
|
5
|
+
className='im-c-marker-wrapper im-c-marker-wrapper--label'
|
|
6
|
+
style={{ display: marker.isVisible ? 'block' : 'none' }}
|
|
7
|
+
>
|
|
8
|
+
<div role='note' className='im-c-marker__label im-c-marker__label--standalone'>
|
|
9
|
+
{marker.label}
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
export default LabelMarker
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { render } from '@testing-library/react'
|
|
2
|
+
import LabelMarker from './LabelMarker.jsx'
|
|
3
|
+
|
|
4
|
+
const makeMarker = (overrides = {}) => ({ id: 'lbl-1', isVisible: true, label: 'Test Label', ...overrides })
|
|
5
|
+
const markerRef = () => () => {}
|
|
6
|
+
|
|
7
|
+
describe('LabelMarker', () => {
|
|
8
|
+
it('renders the label text', () => {
|
|
9
|
+
const { getByText } = render(<LabelMarker marker={makeMarker()} mapId='map' markerRef={markerRef} />)
|
|
10
|
+
expect(getByText('Test Label')).toBeTruthy()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('has wrapper id of mapId-marker-markerId', () => {
|
|
14
|
+
const { container } = render(<LabelMarker marker={makeMarker()} mapId='my-map' markerRef={markerRef} />)
|
|
15
|
+
expect(container.querySelector('.im-c-marker-wrapper').getAttribute('id')).toBe('my-map-marker-lbl-1')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('has wrapper class im-c-marker-wrapper--label', () => {
|
|
19
|
+
const { container } = render(<LabelMarker marker={makeMarker()} mapId='map' markerRef={markerRef} />)
|
|
20
|
+
expect(container.querySelector('.im-c-marker-wrapper')).toHaveClass('im-c-marker-wrapper--label')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('label element has role note and standalone class', () => {
|
|
24
|
+
const { container } = render(<LabelMarker marker={makeMarker()} mapId='map' markerRef={markerRef} />)
|
|
25
|
+
const label = container.querySelector('.im-c-marker__label--standalone')
|
|
26
|
+
expect(label.getAttribute('role')).toBe('note')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it.each([
|
|
30
|
+
[true, 'block'],
|
|
31
|
+
[false, 'none']
|
|
32
|
+
])('display is %s when isVisible=%s', (isVisible, display) => {
|
|
33
|
+
const { container } = render(<LabelMarker marker={makeMarker({ isVisible })} mapId='map' markerRef={markerRef} />)
|
|
34
|
+
expect(container.querySelector('.im-c-marker-wrapper')).toHaveStyle({ display })
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -1,59 +1,51 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { useMarkers } from '../../hooks/useMarkersAPI.js'
|
|
3
|
+
import { useMarkerCursor } from '../../hooks/useMarkerCursor.js'
|
|
3
4
|
import { useConfig } from '../../store/configContext.js'
|
|
4
5
|
import { useMap } from '../../store/mapContext.js'
|
|
5
6
|
import { useService } from '../../store/serviceContext.js'
|
|
6
|
-
import { stringToKebab } from '../../../utils/stringToKebab.js'
|
|
7
7
|
import { scaleFactor } from '../../../config/appConfig.js'
|
|
8
|
+
import { isStandaloneLabel } from '../../../utils/symbolUtils.js'
|
|
9
|
+
import { EVENTS } from '../../../config/events.js'
|
|
10
|
+
import LabelMarker from './LabelMarker.jsx'
|
|
11
|
+
import SymbolLabelMarker from './SymbolLabelMarker.jsx'
|
|
12
|
+
import SymbolMarker from './SymbolMarker.jsx'
|
|
8
13
|
|
|
9
14
|
// Marker properties handled internally — excluded from style value resolution
|
|
10
|
-
const INTERNAL_KEYS = new Set(['id', 'coords', 'x', 'y', 'isVisible', 'symbol', 'symbolSvgContent', 'viewBox', 'anchor', 'selectedColor', '
|
|
15
|
+
const INTERNAL_KEYS = new Set(['id', 'coords', 'x', 'y', 'isVisible', 'symbol', 'symbolSvgContent', 'viewBox', 'anchor', 'selectedColor', 'label', 'showLabel'])
|
|
11
16
|
|
|
12
17
|
const resolveSymbolDef = (marker, defaults, symbolRegistry) => {
|
|
13
18
|
const svgContent = marker.symbolSvgContent || defaults.symbolSvgContent
|
|
14
|
-
// Inline symbolSvgContent takes precedence over a registered symbol,
|
|
15
|
-
// cascading through marker → constructor defaults
|
|
16
19
|
return svgContent
|
|
17
20
|
? { svg: svgContent }
|
|
18
21
|
: symbolRegistry.get(marker.symbol || defaults.symbol)
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
const resolveViewBox = (marker, defaults, symbolDef) =>
|
|
22
|
-
marker.viewBox || defaults.viewBox || symbolDef?.viewBox || '0 0
|
|
25
|
+
marker.viewBox || defaults.viewBox || symbolDef?.viewBox || '0 0 44 44'
|
|
23
26
|
|
|
24
27
|
const resolveAnchor = (marker, defaults, symbolDef) =>
|
|
25
28
|
marker.anchor ?? defaults.anchor ?? symbolDef?.anchor ?? [0.5, 0.5]
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const { left, top, right, bottom } = el.getBoundingClientRect()
|
|
47
|
-
return e.clientX >= left && e.clientX <= right && e.clientY >= top && e.clientY <= bottom
|
|
48
|
-
})
|
|
49
|
-
viewport.style.cursor = hit ? 'pointer' : ''
|
|
50
|
-
}
|
|
51
|
-
viewport.addEventListener('mousemove', onMove)
|
|
52
|
-
return () => {
|
|
53
|
-
viewport.removeEventListener('mousemove', onMove)
|
|
54
|
-
viewport.style.cursor = ''
|
|
55
|
-
}
|
|
56
|
-
}, [markers, interactActive, viewportRef])
|
|
30
|
+
const resolveSymbolProps = (marker, defaults, symbolRegistry, mapStyle, mapSize, isSelected, isActive) => {
|
|
31
|
+
const symbolDef = resolveSymbolDef(marker, defaults, symbolRegistry)
|
|
32
|
+
const styleValues = Object.fromEntries(
|
|
33
|
+
Object.entries(marker).filter(([k]) => !INTERNAL_KEYS.has(k))
|
|
34
|
+
)
|
|
35
|
+
let resolvedSvg
|
|
36
|
+
if (isActive) {
|
|
37
|
+
resolvedSvg = symbolRegistry.resolveActive(symbolDef, styleValues, mapStyle)
|
|
38
|
+
} else if (isSelected) {
|
|
39
|
+
resolvedSvg = symbolRegistry.resolveSelected(symbolDef, styleValues, mapStyle)
|
|
40
|
+
} else {
|
|
41
|
+
resolvedSvg = symbolRegistry.resolve(symbolDef, styleValues, mapStyle)
|
|
42
|
+
}
|
|
43
|
+
const viewBox = resolveViewBox(marker, defaults, symbolDef)
|
|
44
|
+
const [,, svgWidth, svgHeight] = viewBox.split(' ').map(Number)
|
|
45
|
+
const anchor = resolveAnchor(marker, defaults, symbolDef)
|
|
46
|
+
const shapeId = marker.symbol || defaults.symbol
|
|
47
|
+
const scale = scaleFactor[mapSize] ?? 1
|
|
48
|
+
return { resolvedSvg, viewBox, anchor, shapeId, scaledWidth: svgWidth * scale, scaledHeight: svgHeight * scale }
|
|
57
49
|
}
|
|
58
50
|
|
|
59
51
|
// eslint-disable-next-line camelcase, react/jsx-pascal-case
|
|
@@ -66,16 +58,20 @@ export const Markers = () => {
|
|
|
66
58
|
|
|
67
59
|
const [canSelectMarker, setCanSelectMarker] = useState(false)
|
|
68
60
|
const [selectedMarkers, setSelectedMarkers] = useState([])
|
|
61
|
+
const [activeMarkerId, setActiveMarkerId] = useState(null)
|
|
69
62
|
const viewportRef = useRef(null)
|
|
70
63
|
|
|
71
64
|
useEffect(() => {
|
|
72
65
|
const handleActive = ({ active, interactionModes = [] }) => setCanSelectMarker(active && interactionModes.includes('selectMarker'))
|
|
73
66
|
const handleSelectionChange = ({ selectedMarkers: next = [] }) => setSelectedMarkers(next)
|
|
67
|
+
const handleSetActive = ({ id: markerId }) => setActiveMarkerId(markerId)
|
|
74
68
|
eventBus.on('interact:active', handleActive)
|
|
75
69
|
eventBus.on('interact:selectionchange', handleSelectionChange)
|
|
70
|
+
eventBus.on(EVENTS.MAP_SET_ACTIVE_FEATURE, handleSetActive)
|
|
76
71
|
return () => {
|
|
77
72
|
eventBus.off('interact:active', handleActive)
|
|
78
73
|
eventBus.off('interact:selectionchange', handleSelectionChange)
|
|
74
|
+
eventBus.off(EVENTS.MAP_SET_ACTIVE_FEATURE, handleSetActive)
|
|
79
75
|
}
|
|
80
76
|
}, [eventBus])
|
|
81
77
|
|
|
@@ -95,47 +91,20 @@ export const Markers = () => {
|
|
|
95
91
|
return (
|
|
96
92
|
<>
|
|
97
93
|
{markers.items.map(marker => {
|
|
98
|
-
const symbolDef = resolveSymbolDef(marker, defaults, symbolRegistry)
|
|
99
|
-
// selectedColor comes from mapStyle — not per-marker; selectedWidth stays in cascade
|
|
100
|
-
const styleValues = Object.fromEntries(
|
|
101
|
-
Object.entries(marker).filter(([k]) => !INTERNAL_KEYS.has(k))
|
|
102
|
-
)
|
|
103
94
|
const isSelected = selectedMarkers.includes(marker.id)
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return
|
|
117
|
-
<svg
|
|
118
|
-
key={marker.id}
|
|
119
|
-
ref={markerRef(marker.id)}
|
|
120
|
-
id={`${id}-marker-${marker.id}`}
|
|
121
|
-
className={[
|
|
122
|
-
'im-c-marker',
|
|
123
|
-
`im-c-marker--${stringToKebab(shapeId)}`,
|
|
124
|
-
isSelected && 'im-c-marker--selected'
|
|
125
|
-
].filter(Boolean).join(' ')}
|
|
126
|
-
width={scaledWidth}
|
|
127
|
-
height={scaledHeight}
|
|
128
|
-
viewBox={viewBox}
|
|
129
|
-
overflow='visible'
|
|
130
|
-
style={{
|
|
131
|
-
display: marker.isVisible ? 'block' : 'none',
|
|
132
|
-
marginLeft: `${-anchor[0] * scaledWidth}px`,
|
|
133
|
-
marginTop: `${-anchor[1] * scaledHeight}px`
|
|
134
|
-
}}
|
|
135
|
-
>
|
|
136
|
-
<g dangerouslySetInnerHTML={{ __html: resolvedSvg }} />
|
|
137
|
-
</svg>
|
|
138
|
-
)
|
|
95
|
+
const isActive = marker.id === activeMarkerId
|
|
96
|
+
|
|
97
|
+
if (isStandaloneLabel(marker)) {
|
|
98
|
+
return <LabelMarker key={marker.id} marker={marker} mapId={id} markerRef={markerRef} />
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const symbolProps = resolveSymbolProps(marker, defaults, symbolRegistry, mapStyle, mapSize, isSelected, isActive)
|
|
102
|
+
|
|
103
|
+
if (marker.showLabel && marker.label) {
|
|
104
|
+
return <SymbolLabelMarker key={marker.id} marker={marker} mapId={id} markerRef={markerRef} isSelected={isSelected} symbolProps={symbolProps} />
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return <SymbolMarker key={marker.id} marker={marker} mapId={id} markerRef={markerRef} isSelected={isSelected} symbolProps={symbolProps} />
|
|
139
108
|
})}
|
|
140
109
|
</>
|
|
141
110
|
)
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
// ===================================================
|
|
4
4
|
|
|
5
5
|
// 1. Base styles
|
|
6
|
+
|
|
7
|
+
// Wrapper for all markers — absolutely positioned at origin so marker children
|
|
8
|
+
// position correctly and parentElement.getBoundingClientRect() matches the viewport origin
|
|
9
|
+
.im-c-markers {
|
|
10
|
+
position: absolute;
|
|
11
|
+
top: 0;
|
|
12
|
+
left: 0;
|
|
13
|
+
pointer-events: none;
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
.im-c-marker {
|
|
7
17
|
position: absolute;
|
|
8
18
|
left: 0;
|
|
@@ -10,3 +20,51 @@
|
|
|
10
20
|
pointer-events: none;
|
|
11
21
|
}
|
|
12
22
|
|
|
23
|
+
// Wrapper used when a marker has an html label, or for standalone labels.
|
|
24
|
+
// Takes the same absolute positioning as .im-c-marker.
|
|
25
|
+
.im-c-marker-wrapper {
|
|
26
|
+
position: absolute;
|
|
27
|
+
left: 0;
|
|
28
|
+
top: 0;
|
|
29
|
+
pointer-events: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// SVG inside a wrapper — fills wrapper dimensions, no independent positioning
|
|
33
|
+
.im-c-marker__svg {
|
|
34
|
+
display: block;
|
|
35
|
+
pointer-events: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Label rendered above a marker symbol
|
|
39
|
+
.im-c-marker__label {
|
|
40
|
+
position: absolute;
|
|
41
|
+
bottom: 100%;
|
|
42
|
+
left: 50%;
|
|
43
|
+
transform: translateX(-50%);
|
|
44
|
+
white-space: nowrap;
|
|
45
|
+
pointer-events: none;
|
|
46
|
+
|
|
47
|
+
color: var(--tooltip-foreground-color);
|
|
48
|
+
background-color: var(--tooltip-background-color);
|
|
49
|
+
border-radius: var(--tooltip-border-radius);
|
|
50
|
+
padding: var(--tooltip-padding);
|
|
51
|
+
font-size: var(--tooltip-font-size);
|
|
52
|
+
line-height: 1.2;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Standalone label — no symbol, coordinate is where the arrow points.
|
|
56
|
+
// Label body sits above (x, y); arrow points down to (x, y).
|
|
57
|
+
.im-c-marker__label--standalone {
|
|
58
|
+
bottom: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.im-c-marker__label--standalone::after {
|
|
62
|
+
content: '';
|
|
63
|
+
position: absolute;
|
|
64
|
+
top: calc(100% - 1px);
|
|
65
|
+
left: 50%;
|
|
66
|
+
transform: translateX(-50%);
|
|
67
|
+
border: calc(var(--tooltip-arrow-size) - 2px) solid transparent;
|
|
68
|
+
border-top-color: var(--tooltip-background-color);
|
|
69
|
+
}
|
|
70
|
+
|