@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
|
@@ -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
|
// -------------------------
|
|
@@ -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
|
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
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createHints } from './hints.js'
|
|
2
|
+
|
|
3
|
+
let announce
|
|
4
|
+
let hints
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
jest.useFakeTimers()
|
|
8
|
+
announce = jest.fn()
|
|
9
|
+
hints = createHints(announce)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
jest.useRealTimers()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// ─── show() ──────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
describe('show', () => {
|
|
19
|
+
it('notifies subscribers with the html payload', () => {
|
|
20
|
+
const sub = jest.fn()
|
|
21
|
+
hints.subscribe(sub)
|
|
22
|
+
hints.show('<kbd>Enter</kbd> to select')
|
|
23
|
+
expect(sub).toHaveBeenCalledWith({ html: '<kbd>Enter</kbd> to select' })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('calls announce with stripped html', () => {
|
|
27
|
+
hints.show('<kbd>Enter</kbd> to select')
|
|
28
|
+
expect(announce).toHaveBeenCalledWith('Enter to select', 'plugin')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('calls announce with custom text when announce option is provided', () => {
|
|
32
|
+
hints.show('<kbd>Alt+K</kbd> help', { announce: 'Press Alt+K for keyboard controls' })
|
|
33
|
+
expect(announce).toHaveBeenCalledWith('Press Alt+K for keyboard controls', 'plugin')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('replaces an existing hint and resets the timer', () => {
|
|
37
|
+
const sub = jest.fn()
|
|
38
|
+
hints.subscribe(sub)
|
|
39
|
+
hints.show('first', { duration: 2000 })
|
|
40
|
+
hints.show('second', { duration: 2000 })
|
|
41
|
+
jest.advanceTimersByTime(2000)
|
|
42
|
+
expect(sub).toHaveBeenLastCalledWith(null)
|
|
43
|
+
// Only one dismiss fired (the second hint's timer)
|
|
44
|
+
expect(sub).toHaveBeenCalledTimes(3) // show1, show2, dismiss
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('auto-dismisses after duration', () => {
|
|
48
|
+
const sub = jest.fn()
|
|
49
|
+
hints.subscribe(sub)
|
|
50
|
+
hints.show('hello', { duration: 3000 })
|
|
51
|
+
jest.advanceTimersByTime(3000)
|
|
52
|
+
expect(sub).toHaveBeenLastCalledWith(null)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('does not auto-dismiss when duration is 0', () => {
|
|
56
|
+
const sub = jest.fn()
|
|
57
|
+
hints.subscribe(sub)
|
|
58
|
+
hints.show('persistent', { duration: 0 })
|
|
59
|
+
jest.advanceTimersByTime(60000)
|
|
60
|
+
expect(sub).not.toHaveBeenCalledWith(null)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// ─── dismiss() ───────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
describe('dismiss', () => {
|
|
67
|
+
it('clears the active hint and notifies', () => {
|
|
68
|
+
const sub = jest.fn()
|
|
69
|
+
hints.subscribe(sub)
|
|
70
|
+
hints.show('hello')
|
|
71
|
+
hints.dismiss()
|
|
72
|
+
expect(sub).toHaveBeenLastCalledWith(null)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('cancels the auto-dismiss timer', () => {
|
|
76
|
+
const sub = jest.fn()
|
|
77
|
+
hints.subscribe(sub)
|
|
78
|
+
hints.show('hello', { duration: 3000 })
|
|
79
|
+
hints.dismiss()
|
|
80
|
+
sub.mockClear()
|
|
81
|
+
jest.advanceTimersByTime(3000)
|
|
82
|
+
expect(sub).not.toHaveBeenCalled()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('is safe to call when no hint is active', () => {
|
|
86
|
+
expect(() => hints.dismiss()).not.toThrow()
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// ─── subscribe / unsubscribe ──────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
describe('subscribe', () => {
|
|
93
|
+
it('returns an unsubscribe function', () => {
|
|
94
|
+
const sub = jest.fn()
|
|
95
|
+
const unsub = hints.subscribe(sub)
|
|
96
|
+
unsub()
|
|
97
|
+
hints.show('hello')
|
|
98
|
+
expect(sub).not.toHaveBeenCalled()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('notifies multiple subscribers', () => {
|
|
102
|
+
const sub1 = jest.fn()
|
|
103
|
+
const sub2 = jest.fn()
|
|
104
|
+
hints.subscribe(sub1)
|
|
105
|
+
hints.subscribe(sub2)
|
|
106
|
+
hints.show('hello')
|
|
107
|
+
expect(sub1).toHaveBeenCalled()
|
|
108
|
+
expect(sub2).toHaveBeenCalled()
|
|
109
|
+
})
|
|
110
|
+
})
|