@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
|
@@ -38,7 +38,8 @@ describe('mapButtons module', () => {
|
|
|
38
38
|
pressedButtons: new Set(),
|
|
39
39
|
expandedButtons: new Set(),
|
|
40
40
|
buttonConfig: {},
|
|
41
|
-
panelConfig: {}
|
|
41
|
+
panelConfig: {},
|
|
42
|
+
layoutRefs: { viewportRef: { current: { focus: jest.fn() } } }
|
|
42
43
|
}
|
|
43
44
|
appState.buttonConfig = ({})
|
|
44
45
|
getPanelConfig.mockReturnValue({})
|
|
@@ -227,6 +228,41 @@ describe('mapButtons module', () => {
|
|
|
227
228
|
// dispatch should NOT be called because panelId is missing
|
|
228
229
|
expect(appState.dispatch).not.toHaveBeenCalled()
|
|
229
230
|
})
|
|
231
|
+
|
|
232
|
+
it('focuses viewport via requestAnimationFrame after onClick when not toggle and no keepFocus', () => {
|
|
233
|
+
const rafSpy = jest.spyOn(global, 'requestAnimationFrame').mockImplementation(cb => { cb(); return 1 })
|
|
234
|
+
const viewportFocusSpy = jest.spyOn(appState.layoutRefs.viewportRef.current, 'focus')
|
|
235
|
+
const onClick = jest.fn()
|
|
236
|
+
render({ ...baseBtn, onClick }).props.onClick({})
|
|
237
|
+
expect(viewportFocusSpy).toHaveBeenCalled()
|
|
238
|
+
rafSpy.mockRestore()
|
|
239
|
+
viewportFocusSpy.mockRestore()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('does not call requestAnimationFrame when keepFocus is true', () => {
|
|
243
|
+
const rafSpy = jest.spyOn(global, 'requestAnimationFrame')
|
|
244
|
+
const onClick = jest.fn()
|
|
245
|
+
render({ ...baseBtn, onClick, keepFocus: true }).props.onClick({})
|
|
246
|
+
expect(rafSpy).not.toHaveBeenCalled()
|
|
247
|
+
rafSpy.mockRestore()
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('does not call requestAnimationFrame when button is a toggle (pressedWhen set)', () => {
|
|
251
|
+
const rafSpy = jest.spyOn(global, 'requestAnimationFrame')
|
|
252
|
+
const onClick = jest.fn()
|
|
253
|
+
render({ ...baseBtn, onClick, pressedWhen: jest.fn() }).props.onClick({})
|
|
254
|
+
expect(rafSpy).not.toHaveBeenCalled()
|
|
255
|
+
rafSpy.mockRestore()
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('includes focusOnOpen: false in OPEN_PANEL payload when keepFocus is true', () => {
|
|
259
|
+
const mockButtonEl = document.createElement('button')
|
|
260
|
+
render({ ...baseBtn, panelId: 'p1', keepFocus: true }).props.onClick({ currentTarget: mockButtonEl })
|
|
261
|
+
expect(appState.dispatch).toHaveBeenCalledWith({
|
|
262
|
+
type: 'OPEN_PANEL',
|
|
263
|
+
payload: { panelId: 'p1', props: { triggeringElement: mockButtonEl }, focusOnOpen: false }
|
|
264
|
+
})
|
|
265
|
+
})
|
|
230
266
|
})
|
|
231
267
|
|
|
232
268
|
// -------------------------
|
|
@@ -82,6 +82,9 @@ describe('AppProvider', () => {
|
|
|
82
82
|
breakpointCb('sm')
|
|
83
83
|
interfaceCb('mobile')
|
|
84
84
|
})
|
|
85
|
+
|
|
86
|
+
expect(mockBreakpointDetector.subscribe).toHaveBeenCalledWith(expect.any(Function))
|
|
87
|
+
expect(detectInterface.subscribeToInterfaceChanges).toHaveBeenCalledWith(expect.any(Function))
|
|
85
88
|
})
|
|
86
89
|
|
|
87
90
|
test('handles eventBus setmode and revertmode', () => {
|
|
@@ -94,6 +97,8 @@ describe('AppProvider', () => {
|
|
|
94
97
|
mockOptions.eventBus.off('app:setmode', capturedSetMode)
|
|
95
98
|
mockOptions.eventBus.off('app:revertmode', capturedRevertMode)
|
|
96
99
|
})
|
|
100
|
+
expect(mockOptions.eventBus.on).toHaveBeenCalledWith('app:setmode', capturedSetMode)
|
|
101
|
+
expect(mockOptions.eventBus.on).toHaveBeenCalledWith('app:revertmode', capturedRevertMode)
|
|
97
102
|
})
|
|
98
103
|
|
|
99
104
|
test('provides state, dispatch, and layoutRefs via context', () => {
|
|
@@ -186,9 +186,14 @@ describe('MapProvider', () => {
|
|
|
186
186
|
})
|
|
187
187
|
|
|
188
188
|
test('handles dispatch actions correctly', () => {
|
|
189
|
+
let contextValue
|
|
190
|
+
const Child = () => {
|
|
191
|
+
contextValue = React.useContext(MapContext)
|
|
192
|
+
return null
|
|
193
|
+
}
|
|
189
194
|
render(
|
|
190
195
|
<MapProvider options={{ id: 'map1', mapSize: '100x100', eventBus: mockEventBus }}>
|
|
191
|
-
<
|
|
196
|
+
<Child />
|
|
192
197
|
</MapProvider>
|
|
193
198
|
)
|
|
194
199
|
|
|
@@ -197,5 +202,7 @@ describe('MapProvider', () => {
|
|
|
197
202
|
capturedHandlers['map:setstyle']({ id: 'style2' })
|
|
198
203
|
capturedHandlers['map:setsize']('400x400')
|
|
199
204
|
})
|
|
205
|
+
|
|
206
|
+
expect(contextValue.isMapReady).toBe(true)
|
|
200
207
|
})
|
|
201
208
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/App/store/ServiceProvider.jsx
|
|
2
2
|
import React, { createContext, useMemo, useRef } from 'react'
|
|
3
3
|
import { createAnnouncer } from '../../services/announcer.js'
|
|
4
|
+
import { createHints } from '../../services/hints.js'
|
|
4
5
|
import { reverseGeocode } from '../../services/reverseGeocode.js'
|
|
5
6
|
import { useConfig } from '../store/configContext.js'
|
|
6
7
|
import { closeApp } from '../../services/closeApp.js'
|
|
@@ -13,18 +14,20 @@ export const ServiceProvider = ({ eventBus, children }) => {
|
|
|
13
14
|
const { id, handleExitClick, symbolDefaults: constructorSymbolDefaults } = useConfig()
|
|
14
15
|
const mapStatusRef = useRef(null)
|
|
15
16
|
const announce = useMemo(() => createAnnouncer(mapStatusRef), [])
|
|
17
|
+
const hints = useMemo(() => createHints(announce), [announce])
|
|
16
18
|
|
|
17
19
|
symbolRegistry.setDefaults(constructorSymbolDefaults || {})
|
|
18
20
|
|
|
19
21
|
const services = useMemo(() => ({
|
|
20
22
|
announce,
|
|
23
|
+
hints,
|
|
21
24
|
reverseGeocode: (zoom, center) => reverseGeocode(zoom, center),
|
|
22
25
|
eventBus,
|
|
23
26
|
mapStatusRef,
|
|
24
27
|
closeApp: () => closeApp(id, handleExitClick, eventBus),
|
|
25
28
|
symbolRegistry,
|
|
26
29
|
patternRegistry
|
|
27
|
-
}), [announce])
|
|
30
|
+
}), [announce, hints])
|
|
28
31
|
|
|
29
32
|
return (
|
|
30
33
|
<ServiceContext.Provider value={services}>
|
|
@@ -11,6 +11,11 @@ jest.mock('../../services/announcer.js', () => ({
|
|
|
11
11
|
createAnnouncer: jest.fn(() => jest.fn())
|
|
12
12
|
}))
|
|
13
13
|
|
|
14
|
+
const mockShow = jest.fn()
|
|
15
|
+
jest.mock('../../services/hints.js', () => ({
|
|
16
|
+
createHints: jest.fn(() => ({ show: mockShow, dismiss: jest.fn(), subscribe: jest.fn() }))
|
|
17
|
+
}))
|
|
18
|
+
|
|
14
19
|
jest.mock('../../services/reverseGeocode.js', () => ({
|
|
15
20
|
reverseGeocode: jest.fn(() => 'mockedReverseGeocode')
|
|
16
21
|
}))
|
|
@@ -57,6 +62,12 @@ describe('ServiceProvider', () => {
|
|
|
57
62
|
expect(result.current).toBeTruthy()
|
|
58
63
|
})
|
|
59
64
|
|
|
65
|
+
test('hints.show() delegates to createHints show', () => {
|
|
66
|
+
const { result } = renderHook(() => React.useContext(ServiceContext), { wrapper })
|
|
67
|
+
result.current.hints.show('<b>test</b>', { duration: 2000 })
|
|
68
|
+
expect(mockShow).toHaveBeenCalledWith('<b>test</b>', { duration: 2000 })
|
|
69
|
+
})
|
|
70
|
+
|
|
60
71
|
test('closeApp calls closeApp service with id and handleExitClick', () => {
|
|
61
72
|
const mockHandleExitClick = jest.fn()
|
|
62
73
|
const { useConfig } = require('../store/configContext.js')
|
|
@@ -23,7 +23,7 @@ function buildOpenPanels (state, panelId, breakpoint, props, focusOnOpen) {
|
|
|
23
23
|
return {
|
|
24
24
|
...(isExclusiveNonModal ? {} : filteredPanels),
|
|
25
25
|
...(isModal ? state.openPanels : {}),
|
|
26
|
-
[panelId]: { props, ...(focusOnOpen && { focusOnOpen
|
|
26
|
+
[panelId]: { props, ...(focusOnOpen !== undefined && { focusOnOpen }) }
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -158,6 +158,9 @@ const setPluginsEvaluated = (state) =>
|
|
|
158
158
|
const clearPluginsEvaluated = (state) =>
|
|
159
159
|
state.arePluginsEvaluated ? { ...state, arePluginsEvaluated: false } : state
|
|
160
160
|
|
|
161
|
+
const setListboxActive = (state) =>
|
|
162
|
+
state.listboxIsActive ? state : { ...state, listboxIsActive: true }
|
|
163
|
+
|
|
161
164
|
const setSafeZoneInset = (state, { safeZoneInset, syncMapPadding = true }) => {
|
|
162
165
|
return shallowEqual(state.safeZoneInset, safeZoneInset)
|
|
163
166
|
? state
|
|
@@ -373,6 +376,7 @@ export const actionsMap = {
|
|
|
373
376
|
SET_MODE: setMode,
|
|
374
377
|
PLUGINS_EVALUATED: setPluginsEvaluated,
|
|
375
378
|
CLEAR_PLUGINS_EVALUATED: clearPluginsEvaluated,
|
|
379
|
+
SET_LISTBOX_ACTIVE: setListboxActive,
|
|
376
380
|
SET_SAFE_ZONE_INSET: setSafeZoneInset,
|
|
377
381
|
REVERT_MODE: revertMode,
|
|
378
382
|
OPEN_PANEL: openPanel,
|
|
@@ -425,4 +425,17 @@ describe('actionsMap full coverage', () => {
|
|
|
425
425
|
expect(res.openPanels).toHaveProperty('p1')
|
|
426
426
|
})
|
|
427
427
|
})
|
|
428
|
+
|
|
429
|
+
describe('SET_LISTBOX_ACTIVE', () => {
|
|
430
|
+
test('sets listboxIsActive to true when currently false', () => {
|
|
431
|
+
const res = actionsMap.SET_LISTBOX_ACTIVE({ ...state, listboxIsActive: false })
|
|
432
|
+
expect(res.listboxIsActive).toBe(true)
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
test('returns same state reference when listboxIsActive is already true', () => {
|
|
436
|
+
const activeState = { ...state, listboxIsActive: true }
|
|
437
|
+
const res = actionsMap.SET_LISTBOX_ACTIVE(activeState)
|
|
438
|
+
expect(res).toBe(activeState)
|
|
439
|
+
})
|
|
440
|
+
})
|
|
428
441
|
})
|
|
@@ -36,7 +36,8 @@ const updateCrossHair = (state, payload) => {
|
|
|
36
36
|
|
|
37
37
|
const upsertMarker = (state, payload) => {
|
|
38
38
|
const map = new Map(state.markers.items.map(item => [item.id, item]))
|
|
39
|
-
map.
|
|
39
|
+
const existing = map.get(payload.id) || {}
|
|
40
|
+
map.set(payload.id, { ...existing, ...payload })
|
|
40
41
|
const newItems = Array.from(map.values())
|
|
41
42
|
|
|
42
43
|
return {
|
|
@@ -336,6 +336,18 @@ export default class InteractiveMap {
|
|
|
336
336
|
this.eventBus.emit(events.APP_ADD_MARKER, { id, coords, options })
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Update an existing marker's options without replacing it. No-op if the marker id is not found.
|
|
341
|
+
* Merges the provided options into the existing marker — unspecified properties are preserved.
|
|
342
|
+
* Pass `coords` inside options to move the marker to new coordinates.
|
|
343
|
+
*
|
|
344
|
+
* @param {string} id - Marker identifier to update.
|
|
345
|
+
* @param {MarkerOptions & { coords?: [number, number] }} [options] - Properties to merge into the marker.
|
|
346
|
+
*/
|
|
347
|
+
updateMarker (id, options) {
|
|
348
|
+
this.eventBus.emit(events.APP_UPDATE_MARKER, { id, options })
|
|
349
|
+
}
|
|
350
|
+
|
|
339
351
|
/**
|
|
340
352
|
* Remove a marker from the map.
|
|
341
353
|
*
|
|
@@ -586,6 +586,7 @@ describe('InteractiveMap Public API Methods', () => {
|
|
|
586
586
|
map.off('testEvent', cb)
|
|
587
587
|
map.emit('customEvent', 123)
|
|
588
588
|
map.addMarker('marker-1', coords, options)
|
|
589
|
+
map.updateMarker('marker-1', options)
|
|
589
590
|
map.removeMarker('marker-1')
|
|
590
591
|
map.setMode('test-mode')
|
|
591
592
|
|
|
@@ -593,6 +594,7 @@ describe('InteractiveMap Public API Methods', () => {
|
|
|
593
594
|
expect(map.eventBus.off).toHaveBeenCalledWith('testEvent', cb)
|
|
594
595
|
expect(map.eventBus.emit).toHaveBeenCalledWith('customEvent', 123)
|
|
595
596
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:addmarker', { id: 'marker-1', coords, options })
|
|
597
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:updatemarker', { id: 'marker-1', options })
|
|
596
598
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:removemarker', 'marker-1')
|
|
597
599
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:setmode', 'test-mode')
|
|
598
600
|
})
|
package/src/config/appConfig.js
CHANGED
|
@@ -45,6 +45,7 @@ export const defaultAppConfig = {
|
|
|
45
45
|
group: { name: 'zoom', label: 'Zoom controls', order: 0 },
|
|
46
46
|
label: 'Zoom in',
|
|
47
47
|
iconId: 'plus',
|
|
48
|
+
keepFocus: true,
|
|
48
49
|
onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomIn(appConfig.zoomDelta),
|
|
49
50
|
excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appState.interfaceType === 'touch',
|
|
50
51
|
enableWhen: ({ mapState }) => !mapState.isAtMaxZoom,
|
|
@@ -56,6 +57,7 @@ export const defaultAppConfig = {
|
|
|
56
57
|
group: { name: 'zoom', label: 'Zoom controls', order: 0 },
|
|
57
58
|
label: 'Zoom out',
|
|
58
59
|
iconId: 'minus',
|
|
60
|
+
keepFocus: true,
|
|
59
61
|
onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomOut(appConfig.zoomDelta),
|
|
60
62
|
excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appState.interfaceType === 'touch',
|
|
61
63
|
enableWhen: ({ mapState }) => !mapState.isAtMinZoom,
|
|
@@ -78,7 +80,7 @@ export const defaultAppConfig = {
|
|
|
78
80
|
...keyboardBasePanelSlots,
|
|
79
81
|
width: '500px'
|
|
80
82
|
},
|
|
81
|
-
render: () => <KeyboardHelp />
|
|
83
|
+
render: (props) => <KeyboardHelp context={props?.context} />
|
|
82
84
|
}],
|
|
83
85
|
|
|
84
86
|
icons: [{
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { render } from '@testing-library/react'
|
|
2
2
|
import { defaultAppConfig, defaultButtonConfig, scaleFactor } from './appConfig'
|
|
3
3
|
|
|
4
|
+
jest.mock('../App/store/appContext.js', () => ({
|
|
5
|
+
useApp: () => ({ listboxIsActive: false })
|
|
6
|
+
}))
|
|
7
|
+
|
|
4
8
|
describe('defaultAppConfig', () => {
|
|
5
9
|
const appState = {
|
|
6
10
|
layoutRefs: { appContainerRef: { current: document.createElement('div') } },
|
package/src/config/defaults.js
CHANGED
|
@@ -21,7 +21,7 @@ const defaults = {
|
|
|
21
21
|
hasExitButton: false,
|
|
22
22
|
hybridWidth: null, // Defaults to maxMobileWidth if not set
|
|
23
23
|
keyboardHintText: '<span class="im-u-visually-hidden">Press </span><kbd>Alt</kbd> + <kbd>K</kbd> <span class="im-u-visually-hidden">to view </span>keyboard shortcuts',
|
|
24
|
-
mapLabel: 'Interactive map',
|
|
24
|
+
mapLabel: 'Interactive map application',
|
|
25
25
|
mapProvider: null,
|
|
26
26
|
mapSize: 'small',
|
|
27
27
|
manageHistoryState: true,
|
package/src/config/events.js
CHANGED
|
@@ -25,6 +25,8 @@ export const EVENTS = {
|
|
|
25
25
|
|
|
26
26
|
/** @internal Add a marker. Payload: { id, coords, options } */
|
|
27
27
|
APP_ADD_MARKER: 'app:addmarker',
|
|
28
|
+
/** @internal Update an existing marker's options. No-op if id not found. Payload: { id, options } */
|
|
29
|
+
APP_UPDATE_MARKER: 'app:updatemarker',
|
|
28
30
|
/** @internal Remove a marker. Payload: id */
|
|
29
31
|
APP_REMOVE_MARKER: 'app:removemarker',
|
|
30
32
|
/** @internal Set application mode. Payload: mode */
|
|
@@ -121,6 +123,12 @@ export const EVENTS = {
|
|
|
121
123
|
MAP_SET_STYLE: 'map:setstyle',
|
|
122
124
|
/** @internal Set map size. Payload: { width, height } */
|
|
123
125
|
MAP_SET_SIZE: 'map:setsize',
|
|
126
|
+
/** @internal Set the accessible features list. Payload: { items: { id: string, label: string }[] } */
|
|
127
|
+
MAP_SET_FEATURES: 'map:setfeatures',
|
|
128
|
+
/** @internal Set the active feature in the accessible features list. Payload: { id: string | null } */
|
|
129
|
+
MAP_SET_ACTIVE_FEATURE: 'map:setactivefeature',
|
|
130
|
+
/** @internal Select the active listbox feature as the real selection (Enter key). Payload: none */
|
|
131
|
+
MAP_SELECT_FEATURE: 'map:selectfeature',
|
|
124
132
|
/** @internal Set pixel ratio. Payload: pixelRatio */
|
|
125
133
|
MAP_SET_PIXEL_RATIO: 'map:setpixelratio',
|
|
126
134
|
/** @internal Fit the map to a bounding box. Payload: [west, south, east, north] */
|
package/src/config/mapTheme.js
CHANGED
|
@@ -4,18 +4,20 @@
|
|
|
4
4
|
* used both by the symbol SVG renderer (JS canvas) and injected as
|
|
5
5
|
* CSS custom properties onto the app container for CSS-rendered elements.
|
|
6
6
|
*
|
|
7
|
-
* Per-style overrides take precedence: set `haloColor`, `selectedColor`, or
|
|
7
|
+
* Per-style overrides take precedence: set `haloColor`, `selectedColor`, `activeColor`, or
|
|
8
8
|
* `foregroundColor` directly on a `MapStyleConfig` to override these defaults.
|
|
9
9
|
*/
|
|
10
|
-
export const
|
|
10
|
+
export const THEME_COLORS = {
|
|
11
11
|
light: {
|
|
12
12
|
haloColor: '#ffffff',
|
|
13
13
|
selectedColor: '#0b0c0c',
|
|
14
|
+
activeColor: '#ffdd00',
|
|
14
15
|
foregroundColor: '#0b0c0c'
|
|
15
16
|
},
|
|
16
17
|
dark: {
|
|
17
18
|
haloColor: '#0b0c0c',
|
|
18
19
|
selectedColor: '#ffffff',
|
|
20
|
+
activeColor: '#ffdd00',
|
|
19
21
|
foregroundColor: '#ffffff'
|
|
20
22
|
}
|
|
21
23
|
}
|
|
@@ -30,10 +32,11 @@ export const SCHEME_COLORS = {
|
|
|
30
32
|
* @returns {{ haloColor: string, selectedColor: string, foregroundColor: string }}
|
|
31
33
|
*/
|
|
32
34
|
export const resolveMapTheme = (mapStyle) => {
|
|
33
|
-
const scheme =
|
|
35
|
+
const scheme = THEME_COLORS[mapStyle?.mapColorScheme] ?? THEME_COLORS.light
|
|
34
36
|
return {
|
|
35
37
|
haloColor: mapStyle?.haloColor ?? scheme.haloColor,
|
|
36
38
|
selectedColor: mapStyle?.selectedColor ?? scheme.selectedColor,
|
|
39
|
+
activeColor: mapStyle?.activeColor ?? scheme.activeColor,
|
|
37
40
|
foregroundColor: mapStyle?.foregroundColor ?? scheme.foregroundColor
|
|
38
41
|
}
|
|
39
42
|
}
|
|
@@ -47,10 +50,11 @@ export const resolveMapTheme = (mapStyle) => {
|
|
|
47
50
|
* @returns {Object} CSS custom property object
|
|
48
51
|
*/
|
|
49
52
|
export const getMapThemeVars = (mapStyle) => {
|
|
50
|
-
const { haloColor, selectedColor, foregroundColor } = resolveMapTheme(mapStyle)
|
|
53
|
+
const { haloColor, selectedColor, activeColor, foregroundColor } = resolveMapTheme(mapStyle)
|
|
51
54
|
return {
|
|
52
55
|
'--map-overlay-halo-color': haloColor,
|
|
53
56
|
'--map-overlay-selected-color': selectedColor,
|
|
57
|
+
'--map-overlay-active-color': activeColor,
|
|
54
58
|
'--map-overlay-foreground-color': foregroundColor
|
|
55
59
|
}
|
|
56
60
|
}
|
|
@@ -7,11 +7,18 @@
|
|
|
7
7
|
export const symbolDefaults = {
|
|
8
8
|
symbol: 'pin',
|
|
9
9
|
backgroundColor: '#ca3535',
|
|
10
|
-
foregroundColor: '#ffffff'
|
|
11
|
-
haloWidth: '1',
|
|
12
|
-
selectedWidth: '6'
|
|
10
|
+
foregroundColor: '#ffffff'
|
|
13
11
|
}
|
|
14
12
|
|
|
13
|
+
/** Stroke width for the halo (background shape outline) in SVG units */
|
|
14
|
+
export const HALO_STROKE_WIDTH = 2
|
|
15
|
+
|
|
16
|
+
/** Stroke width for the selected state — symbol rings and feature highlight lines */
|
|
17
|
+
export const SELECTED_STROKE_WIDTH = 3
|
|
18
|
+
|
|
19
|
+
/** Stroke width for the active (keyboard cursor) state — double selected, extends 1× each side */
|
|
20
|
+
export const ACTIVE_STROKE_WIDTH = SELECTED_STROKE_WIDTH * 2
|
|
21
|
+
|
|
15
22
|
/**
|
|
16
23
|
* Built-in graphic path data strings for use with the `graphic` token.
|
|
17
24
|
*
|
|
@@ -46,35 +53,35 @@ export const graphics = {
|
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
// ─── Built-in symbol definitions ─────────────────────────────────────────────
|
|
49
|
-
// Each symbol uses a
|
|
56
|
+
// Each symbol uses a 44×44 viewBox. SVG templates use {{token}} placeholders
|
|
50
57
|
// resolved at render time by the symbolRegistry.
|
|
51
58
|
|
|
52
59
|
export const pin = {
|
|
53
60
|
id: 'pin',
|
|
54
|
-
viewBox: '0 0
|
|
61
|
+
viewBox: '0 0 44 44',
|
|
55
62
|
anchor: [0.5, 0.9], // NOSONAR
|
|
56
63
|
graphic: graphics.dot,
|
|
57
|
-
svg: `<path d="
|
|
58
|
-
<path d="
|
|
59
|
-
<g transform="translate(
|
|
64
|
+
svg: `<path d="M22 40.999c-3.621 0-8.306-5.864-10.258-8.3C9.02 29.302 6 23.66 6 19.002a16.01 16.01 0 0 1 16-16 16.01 16.01 0 0 1 16 16c0 4.658-3.02 10.3-5.742 13.697-1.952 2.437-6.637 8.3-10.258 8.3z" fill="{{selectedColor}}" stroke="{{activeColor}}" stroke-width="6" paint-order="stroke fill"/>
|
|
65
|
+
<path d="M22 7.001a12.01 12.01 0 0 1 12 12c0 7.623-10.377 17.998-12 17.998S10 26.624 10 19.001a12.01 12.01 0 0 1 12-12z" fill="{{backgroundColor}}" stroke="{{haloColor}}" stroke-width="2" paint-order="stroke fill"/>
|
|
66
|
+
<g transform="translate(22, 19) scale(0.8) translate(-8, -8)"><path d="{{graphic}}" fill="{{foregroundColor}}"/></g>`
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
export const circle = {
|
|
63
70
|
id: 'circle',
|
|
64
|
-
viewBox: '0 0
|
|
71
|
+
viewBox: '0 0 44 44',
|
|
65
72
|
anchor: [0.5, 0.5],
|
|
66
73
|
graphic: graphics.dot,
|
|
67
|
-
svg: `<
|
|
68
|
-
<
|
|
69
|
-
<g transform="translate(
|
|
74
|
+
svg: `<circle cx="22" cy="22" r="16" fill="{{selectedColor}}" stroke="{{activeColor}}" stroke-width="6" paint-order="stroke fill"/>
|
|
75
|
+
<circle cx="22" cy="22" r="12" fill="{{backgroundColor}}" stroke="{{haloColor}}" stroke-width="2" paint-order="stroke fill"/>
|
|
76
|
+
<g transform="translate(22, 22) scale(0.8) translate(-8, -8)"><path d="{{graphic}}" fill="{{foregroundColor}}"/></g>`
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
export const square = {
|
|
73
80
|
id: 'square',
|
|
74
|
-
viewBox: '0 0
|
|
81
|
+
viewBox: '0 0 44 44',
|
|
75
82
|
anchor: [0.5, 0.5],
|
|
76
83
|
graphic: graphics.dot,
|
|
77
|
-
svg: `<path d="
|
|
78
|
-
<path d="
|
|
79
|
-
<g transform="translate(
|
|
84
|
+
svg: `<path d="M13 6h18c3.863 0 7 3.137 7 7v18c0 3.863-3.137 7-7 7H13c-3.863 0-7-3.137-7-7V13c0-3.863 3.137-7 7-7" fill="{{selectedColor}}" stroke="{{activeColor}}" stroke-width="6" paint-order="stroke fill"/>
|
|
85
|
+
<path d="M13 34a3 3 0 0 1-3-3V13a3 3 0 0 1 3-3h18a3 3 0 0 1 3 3v18a3 3 0 0 1-3 3H13z" fill-rule="nonzero" fill="{{backgroundColor}}" stroke="{{haloColor}}" stroke-width="2" paint-order="stroke fill"/>
|
|
86
|
+
<g transform="translate(22, 22) scale(0.8) translate(-8, -8)"><path d="{{graphic}}" fill="{{foregroundColor}}"/></g>`
|
|
80
87
|
}
|
package/src/scss/main.scss
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
// Components: specific UI components. Using 'CSS in Component' pattern
|
|
19
19
|
@use '../App/components/KeyboardHelp/KeyboardHelp.module';
|
|
20
|
+
@use '../App/components/Tabs/Tabs.module';
|
|
20
21
|
@use '../App/components/MapButton/MapButton.module';
|
|
21
22
|
@use '../App/components/Panel/Panel.module';
|
|
22
23
|
@use '../App/components/Icon/Icon.module';
|
|
@@ -25,8 +26,10 @@
|
|
|
25
26
|
@use '../App/components/PopupMenu/PopupMenu.module';
|
|
26
27
|
@use '../App/components/Attributions/Attributions.module';
|
|
27
28
|
@use '../App/components/Logo/Logo.module';
|
|
29
|
+
@use '../App/components/Hints/Hints.module';
|
|
28
30
|
@use '../App/components/CrossHair/CrossHair.module';
|
|
29
31
|
@use '../App/components/Markers/Markers.module';
|
|
32
|
+
@use '../App/components/Viewport/Features.module';
|
|
30
33
|
@use '../App/components/Actions/Actions.module';
|
|
31
34
|
|
|
32
35
|
// Components that dont have a corresponding React Component
|
|
@@ -24,21 +24,19 @@ describe('createAnnouncer', () => {
|
|
|
24
24
|
|
|
25
25
|
it('does nothing if mapStatusRef.current is null', () => {
|
|
26
26
|
const announceWithNull = createAnnouncer({ current: null })
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
expect(() => {
|
|
28
|
+
announceWithNull('Hello')
|
|
29
|
+
jest.advanceTimersByTime(1000)
|
|
30
|
+
}).not.toThrow()
|
|
30
31
|
})
|
|
31
32
|
|
|
32
33
|
it('returns early if mapStatusRef.current becomes null during setTimeout', () => {
|
|
33
34
|
announce('Test message', 'plugin')
|
|
34
35
|
expect(mapStatusRef.current.textContent).toBe('') // cleared immediately
|
|
35
36
|
|
|
36
|
-
// Set current to null before setTimeout fires
|
|
37
37
|
mapStatusRef.current = null
|
|
38
38
|
|
|
39
|
-
jest.advanceTimersByTime(100)
|
|
40
|
-
// Should not throw, just return early
|
|
41
|
-
expect(true).toBe(true)
|
|
39
|
+
expect(() => jest.advanceTimersByTime(100)).not.toThrow()
|
|
42
40
|
})
|
|
43
41
|
|
|
44
42
|
it('sets textContent for a plugin message immediately', () => {
|
package/src/services/eventBus.js
CHANGED
|
@@ -31,6 +31,21 @@ class EventBus {
|
|
|
31
31
|
return this
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Register a one-time handler that removes itself after the first call.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} eventName
|
|
38
|
+
* @param {Function} handler
|
|
39
|
+
* @returns {this}
|
|
40
|
+
*/
|
|
41
|
+
once (eventName, handler) {
|
|
42
|
+
const wrapper = (...args) => {
|
|
43
|
+
this.off(eventName, wrapper)
|
|
44
|
+
handler(...args)
|
|
45
|
+
}
|
|
46
|
+
return this.on(eventName, wrapper)
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
/**
|
|
35
50
|
* Remove a handler for an event. Omit `handler` to remove all handlers.
|
|
36
51
|
*
|
|
@@ -82,6 +82,17 @@ describe('EventBus singleton', () => {
|
|
|
82
82
|
console.error.mockRestore()
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
+
it('once fires the handler exactly once', () => {
|
|
86
|
+
const handler = jest.fn()
|
|
87
|
+
eventBus.once('single', handler)
|
|
88
|
+
|
|
89
|
+
eventBus.emit('single', 'a')
|
|
90
|
+
eventBus.emit('single', 'b')
|
|
91
|
+
|
|
92
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
93
|
+
expect(handler).toHaveBeenCalledWith('a')
|
|
94
|
+
})
|
|
95
|
+
|
|
85
96
|
it('destroys all events', () => {
|
|
86
97
|
const handler = jest.fn()
|
|
87
98
|
eventBus.on('destroyMe', handler)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const stripHtml = (html) => html.replace(/<[^<>]*>/g, '')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates the hints service. Manages a single active toast hint shown in the
|
|
5
|
+
* Hints container. Calling show() replaces any current hint and
|
|
6
|
+
* restarts the dismiss timer. Internally calls announce() so screen readers
|
|
7
|
+
* receive the message through the live region without callers needing to pair
|
|
8
|
+
* the two calls manually.
|
|
9
|
+
*/
|
|
10
|
+
export function createHints (announce) {
|
|
11
|
+
const subscribers = new Set()
|
|
12
|
+
let current = null
|
|
13
|
+
let timer = null
|
|
14
|
+
|
|
15
|
+
const notify = () => subscribers.forEach(fn => fn(current))
|
|
16
|
+
|
|
17
|
+
const clearTimer = () => {
|
|
18
|
+
if (timer) {
|
|
19
|
+
clearTimeout(timer)
|
|
20
|
+
timer = null
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const dismiss = () => {
|
|
25
|
+
clearTimer()
|
|
26
|
+
current = null
|
|
27
|
+
notify()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const show = (html, options = {}) => {
|
|
31
|
+
const { duration = 4000, announce: announceText } = options
|
|
32
|
+
clearTimer()
|
|
33
|
+
current = { html }
|
|
34
|
+
notify()
|
|
35
|
+
announce(announceText ?? stripHtml(html), 'plugin')
|
|
36
|
+
if (duration > 0) {
|
|
37
|
+
timer = setTimeout(dismiss, duration)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const subscribe = (fn) => {
|
|
42
|
+
subscribers.add(fn)
|
|
43
|
+
return () => subscribers.delete(fn)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { show, dismiss, subscribe }
|
|
47
|
+
}
|