@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,6 @@
|
|
|
1
|
-
import { anchorToMaplibre,
|
|
1
|
+
import { anchorToMaplibre, addSymbolsToMap } from './symbolImages.js'
|
|
2
2
|
import { symbolRegistry } from '../../../../src/services/symbolRegistry.js'
|
|
3
3
|
|
|
4
|
-
const STYLE_ID = 'test'
|
|
5
|
-
const mapStyle = { id: STYLE_ID }
|
|
6
|
-
|
|
7
4
|
beforeAll(() => {
|
|
8
5
|
globalThis.URL.createObjectURL = jest.fn(() => 'blob:mock')
|
|
9
6
|
globalThis.URL.revokeObjectURL = jest.fn()
|
|
@@ -87,142 +84,100 @@ describe('anchorToMaplibre', () => {
|
|
|
87
84
|
})
|
|
88
85
|
})
|
|
89
86
|
|
|
90
|
-
// ───
|
|
91
|
-
|
|
92
|
-
describe('getSymbolImageId', () => {
|
|
93
|
-
it('returns null when dataset has no symbol', () => {
|
|
94
|
-
expect(getSymbolImageId({}, mapStyle, symbolRegistry)).toBeNull()
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('returns null for an unregistered symbol id', () => {
|
|
98
|
-
expect(getSymbolImageId({ symbol: 'does-not-exist' }, mapStyle, symbolRegistry)).toBeNull()
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('returns a string prefixed symbol- for normal state', () => {
|
|
102
|
-
const id = getSymbolImageId({ symbol: 'pin' }, mapStyle, symbolRegistry)
|
|
103
|
-
expect(typeof id).toBe('string')
|
|
104
|
-
expect(id).toMatch(/^symbol-[a-z0-9]+-\d+(\.\d+)?x$/)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('returns a string prefixed symbol-sel- for selected state', () => {
|
|
108
|
-
const id = getSymbolImageId({ symbol: 'pin' }, mapStyle, symbolRegistry, true)
|
|
109
|
-
expect(typeof id).toBe('string')
|
|
110
|
-
expect(id).toMatch(/^symbol-sel-[a-z0-9]+-\d+(\.\d+)?x$/)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('normal and selected ids differ for the same dataset', () => {
|
|
114
|
-
const normalId = getSymbolImageId({ symbol: 'pin' }, mapStyle, symbolRegistry, false)
|
|
115
|
-
const selectedId = getSymbolImageId({ symbol: 'pin' }, mapStyle, symbolRegistry, true)
|
|
116
|
-
expect(normalId).not.toBe(selectedId)
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('same dataset and style always produces the same id', () => {
|
|
120
|
-
const id1 = getSymbolImageId({ symbol: 'pin' }, mapStyle, symbolRegistry)
|
|
121
|
-
const id2 = getSymbolImageId({ symbol: 'pin' }, mapStyle, symbolRegistry)
|
|
122
|
-
expect(id1).toBe(id2)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('different symbols produce different ids', () => {
|
|
126
|
-
const pinId = getSymbolImageId({ symbol: 'pin' }, mapStyle, symbolRegistry)
|
|
127
|
-
const circleId = getSymbolImageId({ symbol: 'circle' }, mapStyle, symbolRegistry)
|
|
128
|
-
expect(pinId).not.toBe(circleId)
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
it('different backgrounds produce different ids', () => {
|
|
132
|
-
const redId = getSymbolImageId({ symbol: 'pin', symbolBackgroundColor: '#ff0000' }, mapStyle, symbolRegistry)
|
|
133
|
-
const blueId = getSymbolImageId({ symbol: 'pin', symbolBackgroundColor: '#0000ff' }, mapStyle, symbolRegistry)
|
|
134
|
-
expect(redId).not.toBe(blueId)
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('resolves inline symbolSvgContent', () => {
|
|
138
|
-
const dataset = {
|
|
139
|
-
symbolSvgContent: '<circle cx="19" cy="19" r="12" fill="{{backgroundColor}}"/>',
|
|
140
|
-
symbolViewBox: '0 0 38 38',
|
|
141
|
-
symbolAnchor: [0.5, 0.5]
|
|
142
|
-
}
|
|
143
|
-
const id = getSymbolImageId(dataset, mapStyle, symbolRegistry)
|
|
144
|
-
expect(id).toMatch(/^symbol-[a-z0-9]+-\d+(\.\d+)?x$/)
|
|
145
|
-
})
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
// ─── registerSymbols ──────────────────────────────────────────────────────────
|
|
87
|
+
// ─── addSymbolsToMap ──────────────────────────────────────────────────────────
|
|
149
88
|
|
|
150
89
|
const makeMap = (existingIds = []) => ({
|
|
151
|
-
|
|
90
|
+
_activeSymbolImageMap: {},
|
|
91
|
+
_selectedSymbolImageMap: {},
|
|
152
92
|
hasImage: jest.fn((id) => existingIds.includes(id)),
|
|
153
93
|
addImage: jest.fn()
|
|
154
94
|
})
|
|
95
|
+
const STYLE_ID = 'test'
|
|
96
|
+
const mapStyle = { id: STYLE_ID }
|
|
155
97
|
|
|
156
|
-
describe('
|
|
98
|
+
describe('addSymbolsToMap — registration', () => {
|
|
157
99
|
it('returns early and does not touch map for empty configs', async () => {
|
|
158
100
|
const map = makeMap()
|
|
159
|
-
await
|
|
101
|
+
await addSymbolsToMap(map, [], mapStyle, symbolRegistry)
|
|
160
102
|
expect(map.hasImage).not.toHaveBeenCalled()
|
|
161
103
|
expect(map.addImage).not.toHaveBeenCalled()
|
|
162
104
|
})
|
|
163
105
|
|
|
164
|
-
it('resets
|
|
106
|
+
it('resets _activeSymbolImageMap and _selectedSymbolImageMap before processing', async () => {
|
|
165
107
|
const map = makeMap()
|
|
166
|
-
map.
|
|
167
|
-
|
|
168
|
-
|
|
108
|
+
map._activeSymbolImageMap = { stale: 'entry' }
|
|
109
|
+
map._selectedSymbolImageMap = { stale: 'entry' }
|
|
110
|
+
await addSymbolsToMap(map, [{ symbol: 'pin' }], mapStyle, symbolRegistry)
|
|
111
|
+
expect(map._activeSymbolImageMap).not.toHaveProperty('stale')
|
|
112
|
+
expect(map._selectedSymbolImageMap).not.toHaveProperty('stale')
|
|
169
113
|
})
|
|
170
114
|
|
|
171
|
-
it('calls addImage for normal and selected variants', async () => {
|
|
115
|
+
it('calls addImage for normal, active and selected variants', async () => {
|
|
172
116
|
const map = makeMap()
|
|
173
|
-
await
|
|
174
|
-
expect(map.addImage).toHaveBeenCalledTimes(
|
|
117
|
+
await addSymbolsToMap(map, [{ symbol: 'pin' }], mapStyle, symbolRegistry)
|
|
118
|
+
expect(map.addImage).toHaveBeenCalledTimes(3) // NOSONAR S109 — normal, active, selected
|
|
175
119
|
expect(map.addImage).toHaveBeenCalledWith(expect.stringMatching(/^symbol-[a-z0-9]+-\d+(\.\d+)?x$/), expect.any(Object), { pixelRatio: 2 })
|
|
120
|
+
expect(map.addImage).toHaveBeenCalledWith(expect.stringMatching(/^symbol-act-[a-z0-9]+-\d+(\.\d+)?x$/), expect.any(Object), { pixelRatio: 2 })
|
|
176
121
|
expect(map.addImage).toHaveBeenCalledWith(expect.stringMatching(/^symbol-sel-[a-z0-9]+-\d+(\.\d+)?x$/), expect.any(Object), { pixelRatio: 2 })
|
|
177
122
|
})
|
|
178
123
|
|
|
179
|
-
it('populates
|
|
124
|
+
it('populates _activeSymbolImageMap and _selectedSymbolImageMap with normal → variant id pairs', async () => {
|
|
180
125
|
const map = makeMap()
|
|
181
|
-
await
|
|
182
|
-
const normalId = getSymbolImageId({ symbol: 'pin' }, mapStyle,
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
126
|
+
await addSymbolsToMap(map, [{ symbol: 'pin' }], mapStyle, symbolRegistry)
|
|
127
|
+
const normalId = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle, false)
|
|
128
|
+
const activeId = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle, true)
|
|
129
|
+
const selectedId = map._selectedSymbolImageMap[normalId]
|
|
130
|
+
expect(map._activeSymbolImageMap[normalId]).toBe(activeId)
|
|
131
|
+
expect(selectedId).toMatch(/^symbol-sel-[a-z0-9]+-\d+(\.\d+)?x$/)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('skips addImage when all three variant images are already registered', async () => {
|
|
135
|
+
// Run once to discover the selected image ID (not derivable without rasterising)
|
|
136
|
+
const setupMap = makeMap()
|
|
137
|
+
await addSymbolsToMap(setupMap, [{ symbol: 'circle' }], mapStyle, symbolRegistry)
|
|
138
|
+
const normalId = symbolRegistry.getSymbolImageId({ symbol: 'circle' }, mapStyle, false)
|
|
139
|
+
const activeId = symbolRegistry.getSymbolImageId({ symbol: 'circle' }, mapStyle, true)
|
|
140
|
+
const selectedId = setupMap._selectedSymbolImageMap[normalId]
|
|
141
|
+
|
|
142
|
+
const map = makeMap([normalId, activeId, selectedId])
|
|
143
|
+
await addSymbolsToMap(map, [{ symbol: 'circle' }], mapStyle, symbolRegistry)
|
|
192
144
|
expect(map.addImage).not.toHaveBeenCalled()
|
|
193
145
|
})
|
|
194
146
|
|
|
195
147
|
it('processes multiple configs independently', async () => {
|
|
196
148
|
const map = makeMap()
|
|
197
|
-
await
|
|
198
|
-
expect(map.addImage).toHaveBeenCalledTimes(
|
|
199
|
-
expect(Object.keys(map.
|
|
149
|
+
await addSymbolsToMap(map, [{ symbol: 'pin' }, { symbol: 'circle' }], mapStyle, symbolRegistry)
|
|
150
|
+
expect(map.addImage).toHaveBeenCalledTimes(6) // NOSONAR S109 — 2 configs × 3 variants each
|
|
151
|
+
expect(Object.keys(map._activeSymbolImageMap)).toHaveLength(2)
|
|
152
|
+
expect(Object.keys(map._selectedSymbolImageMap)).toHaveLength(2)
|
|
200
153
|
})
|
|
201
154
|
})
|
|
202
155
|
|
|
203
|
-
describe('
|
|
156
|
+
describe('addSymbolsToMap — null results and caching', () => {
|
|
204
157
|
it('does not call addImage when rasteriseSymbolImage returns null', async () => {
|
|
205
|
-
// getSymbolImageId (called twice — normal +
|
|
158
|
+
// getSymbolImageId (called twice — normal + active) needs a real symbolDef to produce imageIds,
|
|
206
159
|
// but rasteriseSymbolImage must get undefined from getSymbolDef so it returns null.
|
|
207
|
-
// The registry.get call order: [1] getSymbolImageId normal, [2] getSymbolImageId
|
|
208
|
-
// [3] rasteriseSymbolImage normal, [4] rasteriseSymbolImage selected.
|
|
160
|
+
// The registry.get call order: [1] getSymbolImageId normal, [2] getSymbolImageId active,
|
|
161
|
+
// [3] rasteriseSymbolImage normal, [4] rasteriseSymbolImage active, [5] rasteriseSymbolImage selected.
|
|
209
162
|
const pinDef = symbolRegistry.get('pin')
|
|
210
163
|
const getSpy = jest.spyOn(symbolRegistry, 'get')
|
|
211
164
|
.mockReturnValueOnce(pinDef)
|
|
212
165
|
.mockReturnValueOnce(pinDef)
|
|
213
166
|
.mockReturnValueOnce(undefined)
|
|
214
167
|
.mockReturnValueOnce(undefined)
|
|
168
|
+
.mockReturnValueOnce(undefined)
|
|
215
169
|
const map = makeMap()
|
|
216
|
-
await
|
|
170
|
+
await addSymbolsToMap(map, [{ symbol: 'pin' }], mapStyle, symbolRegistry)
|
|
217
171
|
expect(map.addImage).not.toHaveBeenCalled()
|
|
218
172
|
getSpy.mockRestore()
|
|
219
173
|
})
|
|
220
174
|
|
|
221
175
|
it('skips config when symbolDef cannot be resolved', async () => {
|
|
222
176
|
const map = makeMap()
|
|
223
|
-
await
|
|
177
|
+
await addSymbolsToMap(map, [{ symbol: 'no-such-symbol' }], mapStyle, symbolRegistry)
|
|
224
178
|
expect(map.addImage).not.toHaveBeenCalled()
|
|
225
|
-
expect(map.
|
|
179
|
+
expect(map._activeSymbolImageMap).toEqual({})
|
|
180
|
+
expect(map._selectedSymbolImageMap).toEqual({})
|
|
226
181
|
})
|
|
227
182
|
|
|
228
183
|
it('reuses cached imageData when called again with the same pixelRatio', async () => {
|
|
@@ -230,19 +185,19 @@ describe('registerSymbols — null results and caching', () => {
|
|
|
230
185
|
const uniqueRatio = 7
|
|
231
186
|
|
|
232
187
|
const map1 = makeMap()
|
|
233
|
-
const
|
|
234
|
-
await
|
|
235
|
-
const
|
|
236
|
-
// Rasterisation ran —
|
|
237
|
-
expect(
|
|
188
|
+
const getContextCallsBefore = HTMLCanvasElement.prototype.getContext.mock.calls.length
|
|
189
|
+
await addSymbolsToMap(map1, [{ symbol: 'pin' }], mapStyle, symbolRegistry, uniqueRatio)
|
|
190
|
+
const getContextCallsAfterFirst = HTMLCanvasElement.prototype.getContext.mock.calls.length
|
|
191
|
+
// Rasterisation ran — canvas was used
|
|
192
|
+
expect(getContextCallsAfterFirst).toBeGreaterThan(getContextCallsBefore)
|
|
238
193
|
|
|
239
194
|
// Second call with a fresh map (hasImage → false) but same ratio → cache hit
|
|
240
195
|
const map2 = makeMap()
|
|
241
|
-
await
|
|
242
|
-
const
|
|
243
|
-
// No new
|
|
244
|
-
expect(
|
|
196
|
+
await addSymbolsToMap(map2, [{ symbol: 'pin' }], mapStyle, symbolRegistry, uniqueRatio)
|
|
197
|
+
const getContextCallsAfterSecond = HTMLCanvasElement.prototype.getContext.mock.calls.length
|
|
198
|
+
// No new canvas — rasterisation was skipped via cache
|
|
199
|
+
expect(getContextCallsAfterSecond).toBe(getContextCallsAfterFirst)
|
|
245
200
|
// addImage still called because map2 has no pre-registered images
|
|
246
|
-
expect(map2.addImage).toHaveBeenCalledTimes(
|
|
201
|
+
expect(map2.addImage).toHaveBeenCalledTimes(3) // NOSONAR S109 — normal, active, selected
|
|
247
202
|
})
|
|
248
203
|
})
|
package/rollup.esm.mjs
CHANGED
|
@@ -82,7 +82,7 @@ const PREACT_EXTERNALS = [
|
|
|
82
82
|
// dependency so it auto-installs for consumers and is resolved transparently.
|
|
83
83
|
const BABEL_RUNTIME_EXTERNAL = /@babel\/runtime/
|
|
84
84
|
|
|
85
|
-
const createESMConfig = (entryPath, outDir, isCore = false, manualChunks = null) => {
|
|
85
|
+
const createESMConfig = (entryPath, outDir, isCore = false, manualChunks = null, extraExternals = []) => {
|
|
86
86
|
const esmDir = path.resolve(__dirname, outDir)
|
|
87
87
|
// Use the parent dir as output.dir so CSS can be emitted to css/index.css
|
|
88
88
|
// (a sibling subdir) without Rollup 4's ban on ".." in emitted file names.
|
|
@@ -108,7 +108,8 @@ const createESMConfig = (entryPath, outDir, isCore = false, manualChunks = null)
|
|
|
108
108
|
// instance from their own node_modules rather than a 1 MB copy bundled
|
|
109
109
|
// into the provider. (UMD keeps it bundled — no bundler available there.)
|
|
110
110
|
'maplibre-gl',
|
|
111
|
-
/^@arcgis\/core
|
|
111
|
+
/^@arcgis\/core/,
|
|
112
|
+
...extraExternals
|
|
112
113
|
],
|
|
113
114
|
|
|
114
115
|
plugins: [
|
|
@@ -228,6 +229,16 @@ const ALL_BUILDS = [
|
|
|
228
229
|
outDir: 'providers/beta/esri/dist/esm',
|
|
229
230
|
manualChunks: (id) => { if (id.includes('/esriProvider')) return 'im-esri-provider' }
|
|
230
231
|
},
|
|
232
|
+
{
|
|
233
|
+
entryPath: './providers/beta/openlayers/src/index.js',
|
|
234
|
+
outDir: 'providers/beta/openlayers/dist/esm',
|
|
235
|
+
extraExternals: [/^ol\//, 'proj4'],
|
|
236
|
+
manualChunks: (id) => {
|
|
237
|
+
if (id.includes('/openlayersProvider')) {
|
|
238
|
+
return 'im-openlayers-provider'
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
},
|
|
231
242
|
|
|
232
243
|
// Plugins — each lazy-loads ./manifest.js; manualChunks names that split chunk
|
|
233
244
|
{
|
|
@@ -293,5 +304,5 @@ const buildsToRun = BUILD_TARGET
|
|
|
293
304
|
: ALL_BUILDS
|
|
294
305
|
|
|
295
306
|
export default buildsToRun.map(b =>
|
|
296
|
-
createESMConfig(b.entryPath, b.outDir, b.isCore || false, b.manualChunks || null)
|
|
307
|
+
createESMConfig(b.entryPath, b.outDir, b.isCore || false, b.manualChunks || null, b.extraExternals || [])
|
|
297
308
|
)
|
|
@@ -6,6 +6,9 @@ import * as AppContext from '../../store/appContext' // import the module to moc
|
|
|
6
6
|
// Simple child component to handle isHidden
|
|
7
7
|
const TestChild = ({ isHidden, children, ...props }) => <div {...props}>{children}</div>
|
|
8
8
|
|
|
9
|
+
const ACTIONS_SELECTOR = '.im-c-actions'
|
|
10
|
+
const HIDDEN_CLASS = 'im-c-actions--hidden'
|
|
11
|
+
|
|
9
12
|
describe('Actions component', () => {
|
|
10
13
|
const mockUseApp = {
|
|
11
14
|
openPanels: {},
|
|
@@ -53,8 +56,8 @@ describe('Actions component', () => {
|
|
|
53
56
|
<TestChild isHidden data-testid='child2'>Child 2</TestChild>
|
|
54
57
|
</Actions>
|
|
55
58
|
)
|
|
56
|
-
const container = screen.getByTestId('child1').closest(
|
|
57
|
-
expect(container).toHaveClass(
|
|
59
|
+
const container = screen.getByTestId('child1').closest(ACTIONS_SELECTOR)
|
|
60
|
+
expect(container).toHaveClass(HIDDEN_CLASS)
|
|
58
61
|
})
|
|
59
62
|
|
|
60
63
|
it('shows the container when at least one child is visible', () => {
|
|
@@ -64,7 +67,17 @@ describe('Actions component', () => {
|
|
|
64
67
|
<TestChild isHidden data-testid='child2'>Child 2</TestChild>
|
|
65
68
|
</Actions>
|
|
66
69
|
)
|
|
67
|
-
const container = screen.getByTestId('child1').closest(
|
|
68
|
-
expect(container).not.toHaveClass(
|
|
70
|
+
const container = screen.getByTestId('child1').closest(ACTIONS_SELECTOR)
|
|
71
|
+
expect(container).not.toHaveClass(HIDDEN_CLASS)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('hides the container when all visible children have variant touch', () => {
|
|
75
|
+
render(
|
|
76
|
+
<Actions slot='actions'>
|
|
77
|
+
<TestChild isHidden={false} variant='touch' data-testid='child1'>Child 1</TestChild>
|
|
78
|
+
</Actions>
|
|
79
|
+
)
|
|
80
|
+
const container = screen.getByTestId('child1').closest(ACTIONS_SELECTOR)
|
|
81
|
+
expect(container).toHaveClass(HIDDEN_CLASS)
|
|
69
82
|
})
|
|
70
83
|
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import { useConfig } from '../../store/configContext.js'
|
|
4
|
+
import { useService } from '../../store/serviceContext.js'
|
|
5
|
+
import { useApp } from '../../store/appContext.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Renders the active keyboard hint as a toast portaled into im-o-app__main.
|
|
9
|
+
* Positioned above the actions bar using --hint-bottom. All visual
|
|
10
|
+
* hints pass through here; screen reader announcements are handled internally
|
|
11
|
+
* by the hints service so callers only need hints.show().
|
|
12
|
+
*
|
|
13
|
+
* The container element (id="${mapId}-hints") is always in the DOM after mount
|
|
14
|
+
* so aria-describedby references remain valid even when no hint is showing.
|
|
15
|
+
*/
|
|
16
|
+
// eslint-disable-next-line camelcase, react/jsx-pascal-case
|
|
17
|
+
// sonarjs/disable-next-line function-name
|
|
18
|
+
export const Hints = () => {
|
|
19
|
+
const { id, keyboardHintText } = useConfig()
|
|
20
|
+
const { hints } = useService()
|
|
21
|
+
const { layoutRefs } = useApp()
|
|
22
|
+
const [activeHint, setActiveHint] = useState(null)
|
|
23
|
+
const [mounted, setMounted] = useState(false)
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setMounted(true)
|
|
27
|
+
return hints.subscribe(setActiveHint)
|
|
28
|
+
}, [hints])
|
|
29
|
+
|
|
30
|
+
if (!mounted || !layoutRefs.mainRef?.current) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return createPortal(
|
|
35
|
+
<div className='im-o-hints'>
|
|
36
|
+
<div id={`${id}-hints`} className='im-c-hints'>
|
|
37
|
+
{activeHint && (
|
|
38
|
+
<div
|
|
39
|
+
className='im-c-hints__hint'
|
|
40
|
+
dangerouslySetInnerHTML={{ __html: activeHint.html }}
|
|
41
|
+
/>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
<div
|
|
45
|
+
id={`${id}-keyboard-desc`}
|
|
46
|
+
className='im-u-visually-hidden'
|
|
47
|
+
dangerouslySetInnerHTML={{ __html: keyboardHintText }}
|
|
48
|
+
/>
|
|
49
|
+
</div>,
|
|
50
|
+
layoutRefs.mainRef.current
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// ===================================================
|
|
2
|
+
// Object: KeyboardHints (layout wrapper)
|
|
3
|
+
// ===================================================
|
|
4
|
+
|
|
5
|
+
.im-o-hints {
|
|
6
|
+
position: absolute;
|
|
7
|
+
bottom: var(--hint-bottom, var(--primary-gap));
|
|
8
|
+
left: 50%;
|
|
9
|
+
transform: translateX(-50%);
|
|
10
|
+
z-index: 1001;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ===================================================
|
|
14
|
+
// Component: KeyboardHints
|
|
15
|
+
// ===================================================
|
|
16
|
+
|
|
17
|
+
.im-c-hints {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: var(--divider-gap);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.im-c-hints__hint {
|
|
25
|
+
text-wrap: nowrap;
|
|
26
|
+
|
|
27
|
+
color: var(--tooltip-foreground-color);
|
|
28
|
+
background-color: var(--tooltip-background-color);
|
|
29
|
+
border-radius: var(--tooltip-border-radius);
|
|
30
|
+
padding: var(--tooltip-padding);
|
|
31
|
+
font-size: var(--tooltip-font-size);
|
|
32
|
+
line-height: 1.2;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.im-c-hints__hint kbd {
|
|
36
|
+
border: var(--kbd-tooltip-border);
|
|
37
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render, act } from '@testing-library/react'
|
|
3
|
+
import { Hints } from './Hints.jsx'
|
|
4
|
+
import { useConfig } from '../../store/configContext.js'
|
|
5
|
+
import { useService } from '../../store/serviceContext.js'
|
|
6
|
+
import { useApp } from '../../store/appContext.js'
|
|
7
|
+
|
|
8
|
+
jest.mock('../../store/configContext.js', () => ({ useConfig: jest.fn() }))
|
|
9
|
+
jest.mock('../../store/serviceContext.js', () => ({ useService: jest.fn() }))
|
|
10
|
+
jest.mock('../../store/appContext.js', () => ({ useApp: jest.fn() }))
|
|
11
|
+
|
|
12
|
+
const CONTAINER_ID = '#test-map-hints'
|
|
13
|
+
const HINT_CLASS = '.im-c-hints__hint'
|
|
14
|
+
|
|
15
|
+
let capturedSubscriber
|
|
16
|
+
let mockHints
|
|
17
|
+
|
|
18
|
+
const setup = ({ mainEl = document.createElement('div') } = {}) => {
|
|
19
|
+
capturedSubscriber = null
|
|
20
|
+
mockHints = {
|
|
21
|
+
subscribe: jest.fn((fn) => {
|
|
22
|
+
capturedSubscriber = fn
|
|
23
|
+
return () => {}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
useConfig.mockReturnValue({ id: 'test-map', keyboardHintText: '<kbd>Alt</kbd> + <kbd>K</kbd>' })
|
|
27
|
+
useService.mockReturnValue({ hints: mockHints })
|
|
28
|
+
useApp.mockReturnValue({ layoutRefs: { mainRef: { current: mainEl } } })
|
|
29
|
+
if (mainEl) document.body.appendChild(mainEl)
|
|
30
|
+
return mainEl
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
document.body.innerHTML = ''
|
|
35
|
+
jest.clearAllMocks()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// ─── rendering ───────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
describe('Hints — rendering', () => {
|
|
41
|
+
it('renders the container into mainRef after mount', () => {
|
|
42
|
+
const mainEl = setup()
|
|
43
|
+
render(<Hints />)
|
|
44
|
+
expect(mainEl.querySelector(CONTAINER_ID)).toBeTruthy()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('renders a persistent keyboard-desc span with the hint text', () => {
|
|
48
|
+
const mainEl = setup()
|
|
49
|
+
render(<Hints />)
|
|
50
|
+
const desc = mainEl.querySelector('#test-map-keyboard-desc')
|
|
51
|
+
expect(desc).toBeTruthy()
|
|
52
|
+
expect(desc.innerHTML).toBe('<kbd>Alt</kbd> + <kbd>K</kbd>')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('renders no hint content when there is no active hint', () => {
|
|
56
|
+
const mainEl = setup()
|
|
57
|
+
render(<Hints />)
|
|
58
|
+
expect(mainEl.querySelector(HINT_CLASS)).toBeNull()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('renders hint content when subscriber fires with a hint', () => {
|
|
62
|
+
const mainEl = setup()
|
|
63
|
+
render(<Hints />)
|
|
64
|
+
act(() => capturedSubscriber({ html: '<kbd>Enter</kbd> to select' }))
|
|
65
|
+
const hint = mainEl.querySelector(HINT_CLASS)
|
|
66
|
+
expect(hint).toBeTruthy()
|
|
67
|
+
expect(hint.innerHTML).toBe('<kbd>Enter</kbd> to select')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('removes hint content when subscriber fires with null', () => {
|
|
71
|
+
const mainEl = setup()
|
|
72
|
+
render(<Hints />)
|
|
73
|
+
act(() => capturedSubscriber({ html: 'hello' }))
|
|
74
|
+
act(() => capturedSubscriber(null))
|
|
75
|
+
expect(mainEl.querySelector(HINT_CLASS)).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// ─── lifecycle ───────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
describe('Hints — lifecycle', () => {
|
|
82
|
+
it('subscribes to hints on mount', () => {
|
|
83
|
+
setup()
|
|
84
|
+
render(<Hints />)
|
|
85
|
+
expect(mockHints.subscribe).toHaveBeenCalled()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('unsubscribes on unmount', () => {
|
|
89
|
+
const unsub = jest.fn()
|
|
90
|
+
setup()
|
|
91
|
+
mockHints = { subscribe: jest.fn(() => unsub) }
|
|
92
|
+
useService.mockReturnValue({ hints: mockHints })
|
|
93
|
+
const { unmount } = render(<Hints />)
|
|
94
|
+
unmount()
|
|
95
|
+
expect(unsub).toHaveBeenCalled()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('renders nothing when mainRef is null', () => {
|
|
99
|
+
setup({ mainEl: null })
|
|
100
|
+
useApp.mockReturnValue({ layoutRefs: { mainRef: { current: null } } })
|
|
101
|
+
const { container } = render(<Hints />)
|
|
102
|
+
expect(container.innerHTML).toBe('')
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -1,33 +1,71 @@
|
|
|
1
1
|
// src/components/KeyboardHelp.jsx
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { useConfig } from '../../store/configContext'
|
|
4
|
+
import { useApp } from '../../store/appContext.js'
|
|
4
5
|
import { getKeyboardShortcuts } from '../../registry/keyboardShortcutRegistry.js'
|
|
6
|
+
import { Tabs } from '../Tabs/Tabs.jsx'
|
|
7
|
+
|
|
8
|
+
const ShortcutList = ({ items }) => (
|
|
9
|
+
<dl className='im-c-keyboard-help__list'>
|
|
10
|
+
{items.map((item) => (
|
|
11
|
+
<div key={item.id} className='im-c-keyboard-help__item'>
|
|
12
|
+
<dt className='im-c-keyboard-help__title'>{item.title}</dt>
|
|
13
|
+
<dd className='im-c-keyboard-help__description' dangerouslySetInnerHTML={{ __html: item.command }} />
|
|
14
|
+
</div>
|
|
15
|
+
))}
|
|
16
|
+
</dl>
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const buildGroupMap = (shortcuts) => shortcuts.reduce((acc, shortcut) => {
|
|
20
|
+
const group = shortcut.group || 'Navigate'
|
|
21
|
+
if (!acc[group]) {
|
|
22
|
+
acc[group] = []
|
|
23
|
+
}
|
|
24
|
+
acc[group].push(shortcut)
|
|
25
|
+
return acc
|
|
26
|
+
}, {})
|
|
27
|
+
|
|
28
|
+
const getDefaultTab = (groupEntries, context) => {
|
|
29
|
+
const exactMatch = groupEntries.find(([, items]) =>
|
|
30
|
+
items.some(s => (s.context ?? 'viewport') === context)
|
|
31
|
+
)
|
|
32
|
+
if (exactMatch) {
|
|
33
|
+
return exactMatch[0]
|
|
34
|
+
}
|
|
35
|
+
const globalMatch = groupEntries.find(([, items]) =>
|
|
36
|
+
items.some(s => s.context === 'global')
|
|
37
|
+
)
|
|
38
|
+
return globalMatch?.[0] ?? groupEntries[0][0]
|
|
39
|
+
}
|
|
5
40
|
|
|
6
41
|
// eslint-disable-next-line camelcase, react/jsx-pascal-case
|
|
7
42
|
// sonarjs/disable-next-line function-name
|
|
8
|
-
export const KeyboardHelp = () => {
|
|
43
|
+
export const KeyboardHelp = ({ context = 'viewport' }) => { // NOSONAR: project does not use PropTypes
|
|
9
44
|
const appConfig = useConfig()
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
45
|
+
const { listboxIsActive } = useApp()
|
|
46
|
+
const allShortcuts = getKeyboardShortcuts(appConfig)
|
|
47
|
+
const shortcuts = listboxIsActive
|
|
48
|
+
? allShortcuts
|
|
49
|
+
: allShortcuts.filter(s => !s.group && s.context !== 'listbox')
|
|
50
|
+
const groupMap = buildGroupMap(shortcuts)
|
|
51
|
+
const groupEntries = Object.entries(groupMap)
|
|
52
|
+
|
|
53
|
+
if (groupEntries.length <= 1) {
|
|
54
|
+
return (
|
|
55
|
+
<div className='im-c-keyboard-help'>
|
|
56
|
+
<ShortcutList items={shortcuts} />
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const tabs = groupEntries.map(([name, items]) => ({
|
|
62
|
+
name,
|
|
63
|
+
content: <ShortcutList items={items} />
|
|
64
|
+
}))
|
|
15
65
|
|
|
16
66
|
return (
|
|
17
67
|
<div className='im-c-keyboard-help'>
|
|
18
|
-
{
|
|
19
|
-
<div key={groupName} className='im-c-keyboard-help__group'>
|
|
20
|
-
{/* <h3 className='im-c-keyboard-help__title'>{groupName}</h3> */}
|
|
21
|
-
<dl className='im-c-keyboard-help__list'>
|
|
22
|
-
{items.map((item) => (
|
|
23
|
-
<div key={item.id} className='im-c-keyboard-help__item'>
|
|
24
|
-
<dt className='im-c-keyboard-help__title'>{item.title}</dt>
|
|
25
|
-
<dd className='im-c-keyboard-help__description' dangerouslySetInnerHTML={{ __html: item.command }} />
|
|
26
|
-
</div>
|
|
27
|
-
))}
|
|
28
|
-
</dl>
|
|
29
|
-
</div>
|
|
30
|
-
))}
|
|
68
|
+
<Tabs tabs={tabs} defaultTab={getDefaultTab(groupEntries, context)} />
|
|
31
69
|
</div>
|
|
32
70
|
)
|
|
33
71
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Component: KeyboardHelp
|
|
3
3
|
// ===================================================
|
|
4
4
|
|
|
5
|
+
@use '../../../scss/tools/index' as tools;
|
|
6
|
+
|
|
5
7
|
// 1. Base styles
|
|
6
8
|
|
|
7
9
|
// 2. Elements
|
|
@@ -15,6 +17,10 @@
|
|
|
15
17
|
padding: 10px 0;
|
|
16
18
|
width: 100%;
|
|
17
19
|
border-top: 1px solid var(--content-border-color);
|
|
20
|
+
|
|
21
|
+
&:last-child {
|
|
22
|
+
padding-bottom: 0;
|
|
23
|
+
}
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
.im-c-keyboard-help__group:last-child .im-c-keyboard-help__item:last-child {
|
|
@@ -40,3 +46,7 @@
|
|
|
40
46
|
display: block;
|
|
41
47
|
margin-top: 5px;
|
|
42
48
|
}
|
|
49
|
+
|
|
50
|
+
.im-c-tabs__panel .im-c-keyboard-help__item:first-child {
|
|
51
|
+
border-top: 0;
|
|
52
|
+
}
|