@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
|
@@ -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
|
+
|