@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,12 +1,16 @@
|
|
|
1
1
|
import { symbolRegistry } from './symbolRegistry.js'
|
|
2
|
-
import { symbolDefaults } from '../config/symbolConfig.js'
|
|
3
|
-
import {
|
|
2
|
+
import { symbolDefaults, pin } from '../config/symbolConfig.js'
|
|
3
|
+
import { THEME_COLORS } from '../config/mapTheme.js'
|
|
4
4
|
import { getValueForStyle } from '../utils/getValueForStyle.js'
|
|
5
5
|
|
|
6
6
|
const STYLE_ID = 'test'
|
|
7
7
|
const mapStyle = { id: STYLE_ID }
|
|
8
|
+
const COLOR_OVERRIDE = '#ff0000'
|
|
9
|
+
const FILL_OVERRIDE = `fill="${COLOR_OVERRIDE}"`
|
|
8
10
|
|
|
9
11
|
beforeEach(() => {
|
|
12
|
+
symbolRegistry.clear()
|
|
13
|
+
symbolRegistry.initialise()
|
|
10
14
|
symbolRegistry.setDefaults({})
|
|
11
15
|
})
|
|
12
16
|
|
|
@@ -58,20 +62,20 @@ describe('symbolRegistry — setDefaults / getDefaults', () => {
|
|
|
58
62
|
})
|
|
59
63
|
|
|
60
64
|
it('constructor defaults override hardcoded defaults', () => {
|
|
61
|
-
symbolRegistry.setDefaults({ backgroundColor:
|
|
65
|
+
symbolRegistry.setDefaults({ backgroundColor: COLOR_OVERRIDE, symbol: 'circle' })
|
|
62
66
|
const defaults = symbolRegistry.getDefaults()
|
|
63
|
-
expect(defaults.backgroundColor).toBe(
|
|
67
|
+
expect(defaults.backgroundColor).toBe(COLOR_OVERRIDE)
|
|
64
68
|
expect(defaults.symbol).toBe('circle')
|
|
65
69
|
})
|
|
66
70
|
|
|
67
71
|
it('constructor defaults do not affect unset properties', () => {
|
|
68
|
-
symbolRegistry.setDefaults({ backgroundColor:
|
|
72
|
+
symbolRegistry.setDefaults({ backgroundColor: COLOR_OVERRIDE })
|
|
69
73
|
const defaults = symbolRegistry.getDefaults()
|
|
70
74
|
expect(defaults.foregroundColor).toBe(symbolDefaults.foregroundColor)
|
|
71
75
|
})
|
|
72
76
|
|
|
73
77
|
it('setDefaults with null or undefined resets to hardcoded defaults', () => {
|
|
74
|
-
symbolRegistry.setDefaults({ backgroundColor:
|
|
78
|
+
symbolRegistry.setDefaults({ backgroundColor: COLOR_OVERRIDE })
|
|
75
79
|
symbolRegistry.setDefaults(null)
|
|
76
80
|
expect(symbolRegistry.getDefaults().backgroundColor).toBe(symbolDefaults.backgroundColor)
|
|
77
81
|
})
|
|
@@ -81,24 +85,23 @@ describe('symbolRegistry — resolve', () => {
|
|
|
81
85
|
const BACKGROUND_SVG = '<path fill="{{backgroundColor}}"/>'
|
|
82
86
|
const symbolDef = {
|
|
83
87
|
id: 'test',
|
|
84
|
-
svg: '<path fill="{{backgroundColor}}" stroke="{{haloColor}}"
|
|
88
|
+
svg: '<path fill="{{backgroundColor}}" stroke="{{haloColor}}"/><path fill="{{foregroundColor}}" stroke="{{selectedColor}}"/><path stroke="{{activeColor}}"/>'
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
it('injects default token values when no overrides given', () => {
|
|
88
92
|
const resolved = symbolRegistry.resolve(symbolDef, {}, mapStyle)
|
|
89
93
|
expect(resolved).toContain(`fill="${getValueForStyle(symbolDefaults.backgroundColor, STYLE_ID)}"`)
|
|
90
94
|
expect(resolved).toContain(`fill="${getValueForStyle(symbolDefaults.foregroundColor, STYLE_ID)}"`)
|
|
91
|
-
expect(resolved).toContain(`stroke-width="${symbolDefaults.haloWidth}"`)
|
|
92
95
|
})
|
|
93
96
|
|
|
94
|
-
it('always produces empty selectedColor
|
|
97
|
+
it('always produces empty selectedColor and activeColor tokens — rings are hidden', () => {
|
|
95
98
|
const resolved = symbolRegistry.resolve(symbolDef, {}, mapStyle)
|
|
96
|
-
expect(resolved).toContain('stroke=""')
|
|
99
|
+
expect(resolved).toContain('stroke="none"')
|
|
97
100
|
})
|
|
98
101
|
|
|
99
102
|
it('uses light scheme haloColor when mapStyle has no haloColor', () => {
|
|
100
103
|
const resolved = symbolRegistry.resolve(symbolDef, {}, mapStyle)
|
|
101
|
-
expect(resolved).toContain(`stroke="${
|
|
104
|
+
expect(resolved).toContain(`stroke="${THEME_COLORS.light.haloColor}"`)
|
|
102
105
|
})
|
|
103
106
|
|
|
104
107
|
it('uses mapStyle.haloColor when provided', () => {
|
|
@@ -107,8 +110,8 @@ describe('symbolRegistry — resolve', () => {
|
|
|
107
110
|
})
|
|
108
111
|
|
|
109
112
|
it('overrides default backgroundColor with a plain string', () => {
|
|
110
|
-
const resolved = symbolRegistry.resolve(symbolDef, { backgroundColor:
|
|
111
|
-
expect(resolved).toContain(
|
|
113
|
+
const resolved = symbolRegistry.resolve(symbolDef, { backgroundColor: COLOR_OVERRIDE }, mapStyle)
|
|
114
|
+
expect(resolved).toContain(FILL_OVERRIDE)
|
|
112
115
|
})
|
|
113
116
|
|
|
114
117
|
it('overrides default with a style-keyed color', () => {
|
|
@@ -163,48 +166,70 @@ describe('symbolRegistry — resolve', () => {
|
|
|
163
166
|
})
|
|
164
167
|
})
|
|
165
168
|
|
|
166
|
-
describe('symbolRegistry —
|
|
169
|
+
describe('symbolRegistry — resolveActive (keyboard cursor state)', () => {
|
|
167
170
|
const symbolDef = {
|
|
168
|
-
id: 'test-
|
|
169
|
-
svg: '<path
|
|
171
|
+
id: 'test-active',
|
|
172
|
+
svg: '<path fill="{{selectedColor}}" stroke="{{activeColor}}" stroke-width="6"/><path fill="{{backgroundColor}}"/>'
|
|
170
173
|
}
|
|
171
174
|
|
|
172
|
-
it('
|
|
173
|
-
const resolved = symbolRegistry.
|
|
174
|
-
expect(resolved).toContain(`
|
|
175
|
+
it('renders selectedColor from scheme when mapStyle has no selectedColor', () => {
|
|
176
|
+
const resolved = symbolRegistry.resolveActive(symbolDef, {}, mapStyle)
|
|
177
|
+
expect(resolved).toContain(`fill="${THEME_COLORS.light.selectedColor}"`)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('renders activeColor from scheme when mapStyle has no activeColor', () => {
|
|
181
|
+
const resolved = symbolRegistry.resolveActive(symbolDef, {}, mapStyle)
|
|
182
|
+
expect(resolved).toContain(`stroke="${THEME_COLORS.light.activeColor}"`)
|
|
175
183
|
})
|
|
176
184
|
|
|
177
185
|
it('uses mapStyle.selectedColor when provided', () => {
|
|
178
|
-
const resolved = symbolRegistry.
|
|
179
|
-
expect(resolved).toContain(
|
|
186
|
+
const resolved = symbolRegistry.resolveActive(symbolDef, {}, { id: STYLE_ID, selectedColor: COLOR_OVERRIDE })
|
|
187
|
+
expect(resolved).toContain(FILL_OVERRIDE)
|
|
180
188
|
})
|
|
181
189
|
|
|
182
|
-
it('uses
|
|
183
|
-
const resolved = symbolRegistry.
|
|
184
|
-
expect(resolved).toContain(
|
|
190
|
+
it('uses mapStyle.activeColor when provided', () => {
|
|
191
|
+
const resolved = symbolRegistry.resolveActive(symbolDef, {}, { id: STYLE_ID, activeColor: '#00ff00' })
|
|
192
|
+
expect(resolved).toContain('stroke="#00ff00"')
|
|
185
193
|
})
|
|
186
194
|
|
|
187
195
|
it('handles null styleColors — uses cascade defaults', () => {
|
|
188
|
-
const resolved = symbolRegistry.
|
|
189
|
-
expect(resolved).toContain(`
|
|
196
|
+
const resolved = symbolRegistry.resolveActive(symbolDef, null, mapStyle)
|
|
197
|
+
expect(resolved).toContain(`fill="${THEME_COLORS.light.selectedColor}"`)
|
|
190
198
|
})
|
|
191
199
|
|
|
192
200
|
it('returns empty string for null symbolDef', () => {
|
|
193
|
-
expect(symbolRegistry.
|
|
201
|
+
expect(symbolRegistry.resolveActive(null, {}, mapStyle)).toBe('')
|
|
194
202
|
})
|
|
195
203
|
|
|
196
204
|
it('symbol-level selectedColor is ignored — mapStyle wins', () => {
|
|
197
205
|
const defWithSelected = { ...symbolDef, selectedColor: '#00ff00' }
|
|
198
|
-
const resolved = symbolRegistry.
|
|
199
|
-
expect(resolved).toContain(
|
|
206
|
+
const resolved = symbolRegistry.resolveActive(defWithSelected, {}, { id: STYLE_ID, selectedColor: COLOR_OVERRIDE })
|
|
207
|
+
expect(resolved).toContain(FILL_OVERRIDE)
|
|
200
208
|
})
|
|
201
209
|
|
|
202
210
|
it('still resolves other tokens correctly', () => {
|
|
203
|
-
const resolved = symbolRegistry.
|
|
211
|
+
const resolved = symbolRegistry.resolveActive(symbolDef, { backgroundColor: '#d4351c' }, mapStyle)
|
|
204
212
|
expect(resolved).toContain('fill="#d4351c"')
|
|
205
213
|
})
|
|
206
214
|
})
|
|
207
215
|
|
|
216
|
+
describe('symbolRegistry — resolveSelected (committed selection state)', () => {
|
|
217
|
+
const symbolDef = {
|
|
218
|
+
id: 'test-selected',
|
|
219
|
+
svg: '<path fill="{{selectedColor}}"/>'
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
it('returns empty string for null symbolDef', () => {
|
|
223
|
+
expect(symbolRegistry.resolveSelected(null, {}, mapStyle)).toBe('')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('handles null styleColors — uses cascade defaults', () => {
|
|
227
|
+
const resolved = symbolRegistry.resolveSelected(symbolDef, null, mapStyle)
|
|
228
|
+
expect(typeof resolved).toBe('string')
|
|
229
|
+
expect(resolved.length).toBeGreaterThan(0)
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
208
233
|
describe('symbolRegistry — graphic token', () => {
|
|
209
234
|
const graphicDef = {
|
|
210
235
|
id: 'test-graphic',
|
|
@@ -257,6 +282,91 @@ describe('symbolRegistry — graphic token', () => {
|
|
|
257
282
|
const pin = symbolRegistry.get('pin')
|
|
258
283
|
const resolved = symbolRegistry.resolve(pin, {}, mapStyle)
|
|
259
284
|
expect(resolved).toContain(`d="${pin.graphic}"`)
|
|
260
|
-
expect(resolved).toContain('translate(
|
|
285
|
+
expect(resolved).toContain('translate(22, 19) scale(0.8) translate(-8, -8)')
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
// ─── getSymbolDef ─────────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
describe('getSymbolDef', () => {
|
|
292
|
+
it('returns undefined when dataset has no symbol', () => {
|
|
293
|
+
expect(symbolRegistry.getSymbolDef({})).toBeUndefined()
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('looks up string symbol id in the registry', () => {
|
|
297
|
+
expect(symbolRegistry.getSymbolDef({ symbol: 'pin' })).toBe(pin)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('returns undefined for an unregistered string symbol', () => {
|
|
301
|
+
expect(symbolRegistry.getSymbolDef({ symbol: 'missing' })).toBeUndefined()
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('returns inline def from symbolSvgContent with svg key', () => {
|
|
305
|
+
const dataset = { symbolSvgContent: '<circle/>', symbolViewBox: '0 0 10 10' }
|
|
306
|
+
const result = symbolRegistry.getSymbolDef(dataset)
|
|
307
|
+
expect(result.svg).toBe('<circle/>')
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it('symbolSvgContent takes precedence over symbol id', () => {
|
|
311
|
+
const result = symbolRegistry.getSymbolDef({ symbol: 'pin', symbolSvgContent: '<circle/>' })
|
|
312
|
+
expect(result.svg).toBe('<circle/>')
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
// ─── getSymbolImageId ─────────────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
describe('getSymbolImageId', () => {
|
|
319
|
+
it('returns null when dataset has no symbol', () => {
|
|
320
|
+
expect(symbolRegistry.getSymbolImageId({}, mapStyle)).toBeNull()
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('returns null for an unregistered symbol id', () => {
|
|
324
|
+
expect(symbolRegistry.getSymbolImageId({ symbol: 'does-not-exist' }, mapStyle)).toBeNull()
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('returns a string prefixed symbol- for normal state', () => {
|
|
328
|
+
const id = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle)
|
|
329
|
+
expect(typeof id).toBe('string')
|
|
330
|
+
expect(id).toMatch(/^symbol-[a-z0-9]+-\d+(\.\d+)?x$/)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('returns a string prefixed symbol-act- for active state', () => {
|
|
334
|
+
const id = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle, true)
|
|
335
|
+
expect(typeof id).toBe('string')
|
|
336
|
+
expect(id).toMatch(/^symbol-act-[a-z0-9]+-\d+(\.\d+)?x$/)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('normal and active ids differ for the same dataset', () => {
|
|
340
|
+
const normalId = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle, false)
|
|
341
|
+
const activeId = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle, true)
|
|
342
|
+
expect(normalId).not.toBe(activeId)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('same dataset and style always produces the same id', () => {
|
|
346
|
+
const id1 = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle)
|
|
347
|
+
const id2 = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle)
|
|
348
|
+
expect(id1).toBe(id2)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('different symbols produce different ids', () => {
|
|
352
|
+
const pinId = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle)
|
|
353
|
+
const circleId = symbolRegistry.getSymbolImageId({ symbol: 'circle' }, mapStyle)
|
|
354
|
+
expect(pinId).not.toBe(circleId)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('different backgrounds produce different ids', () => {
|
|
358
|
+
const redId = symbolRegistry.getSymbolImageId({ symbol: 'pin', symbolBackgroundColor: '#ff0000' }, mapStyle)
|
|
359
|
+
const blueId = symbolRegistry.getSymbolImageId({ symbol: 'pin', symbolBackgroundColor: '#0000ff' }, mapStyle)
|
|
360
|
+
expect(redId).not.toBe(blueId)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('resolves inline symbolSvgContent', () => {
|
|
364
|
+
const dataset = {
|
|
365
|
+
symbolSvgContent: '<circle cx="19" cy="19" r="12" fill="{{backgroundColor}}"/>',
|
|
366
|
+
symbolViewBox: '0 0 38 38',
|
|
367
|
+
symbolAnchor: [0.5, 0.5]
|
|
368
|
+
}
|
|
369
|
+
const id = symbolRegistry.getSymbolImageId(dataset, mapStyle)
|
|
370
|
+
expect(id).toMatch(/^symbol-[a-z0-9]+-\d+(\.\d+)?x$/)
|
|
261
371
|
})
|
|
262
372
|
})
|
package/src/types.js
CHANGED
|
@@ -91,6 +91,41 @@
|
|
|
91
91
|
* Core services (announce, reverseGeocode, closeApp, etc.).
|
|
92
92
|
*/
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Defines an item in a button's popup menu.
|
|
96
|
+
*
|
|
97
|
+
* @typedef {Object} MenuItemDefinition
|
|
98
|
+
*
|
|
99
|
+
* @property {string} id
|
|
100
|
+
* Unique item identifier. Used to control state via toggleButtonState().
|
|
101
|
+
*
|
|
102
|
+
* @property {string} label
|
|
103
|
+
* Display text for the item.
|
|
104
|
+
*
|
|
105
|
+
* @property {string} [iconId]
|
|
106
|
+
* Icon identifier from the icon registry.
|
|
107
|
+
*
|
|
108
|
+
* @property {string} [iconSvgContent]
|
|
109
|
+
* Raw SVG content for the item icon. The outer SVG tag should be excluded.
|
|
110
|
+
*
|
|
111
|
+
* @property {boolean} [isPressed]
|
|
112
|
+
* Initial checked state. When set, the item renders as menuitemcheckbox.
|
|
113
|
+
*
|
|
114
|
+
* @property {boolean} [keepFocus=false]
|
|
115
|
+
* When true, focus returns to the menu's trigger button after selection instead of
|
|
116
|
+
* moving to the panel (if panelId is set) or the map viewport.
|
|
117
|
+
*
|
|
118
|
+
* @property {(e: MouseEvent) => void} [onClick]
|
|
119
|
+
* Click handler. Receives the native event. Not called when panelId is set.
|
|
120
|
+
*
|
|
121
|
+
* @property {string} [panelId]
|
|
122
|
+
* Associated panel identifier. When set, selecting the item opens the panel and moves
|
|
123
|
+
* focus to it. onClick is not called.
|
|
124
|
+
*
|
|
125
|
+
* @property {(context: PluginContext) => boolean} [pressedWhen]
|
|
126
|
+
* Reactive callback to determine if the item should appear checked. Plugin buttons only.
|
|
127
|
+
*/
|
|
128
|
+
|
|
94
129
|
/**
|
|
95
130
|
* Defines a button that can be rendered in the UI at various breakpoints.
|
|
96
131
|
*
|
|
@@ -133,12 +168,21 @@
|
|
|
133
168
|
* @property {string | (() => string)} label
|
|
134
169
|
* Accesible label. Text or a function returning the text. Used for the label or tooltip if 'showLabel' is false.
|
|
135
170
|
*
|
|
171
|
+
* @property {MenuItemDefinition[]} [menuItems]
|
|
172
|
+
* Items for the button's popup menu. When provided, the button acts as a menu trigger.
|
|
173
|
+
*
|
|
136
174
|
* @property {ButtonBreakpointConfig} mobile
|
|
137
175
|
* Mobile breakpoint configuration.
|
|
138
176
|
*
|
|
139
177
|
* @property {(event: MouseEvent, context: PluginContext) => void} [onClick]
|
|
140
178
|
* Click handler for the button.
|
|
141
179
|
*
|
|
180
|
+
* @property {boolean} [keepFocus=false]
|
|
181
|
+
* When true, focus remains on this button after activation instead of moving to the panel or viewport.
|
|
182
|
+
* Use for repeated incremental actions (e.g. zoom) where the user may activate the button
|
|
183
|
+
* multiple times. Toggle buttons (isPressed/pressedWhen) always keep focus regardless of this flag.
|
|
184
|
+
* When combined with panelId, the panel opens but focus stays on the button.
|
|
185
|
+
*
|
|
142
186
|
* @property {string} [panelId]
|
|
143
187
|
* Associated panel identifier to toggle open when clicked.
|
|
144
188
|
*
|
|
@@ -221,6 +265,10 @@
|
|
|
221
265
|
* @property {(point: { x: number, y: number }) => any[]} getFeaturesAtPoint
|
|
222
266
|
* Query rendered features at a screen pixel position (x from left edge, y from top edge of viewport).
|
|
223
267
|
*
|
|
268
|
+
* @property {(layerIds: string[]) => any[]} getVisibleFeatures
|
|
269
|
+
* Return all GeoJSON features currently visible in the viewport on the given layers,
|
|
270
|
+
* respecting layer visibility and any active filters. Returns [] if unsupported.
|
|
271
|
+
*
|
|
224
272
|
* @property {() => string} getAreaDimensions
|
|
225
273
|
* Get the dimensions of the visible map area as a formatted string (e.g., '400m by 750m').
|
|
226
274
|
*
|
|
@@ -236,7 +284,7 @@
|
|
|
236
284
|
* @property {(point: { x: number, y: number }) => [number, number]} screenToMap
|
|
237
285
|
* Convert screen pixel position (x from left edge, y from top edge of viewport) to map coordinates ([lng, lat] or [easting, northing] depending on the crs of the map provider).
|
|
238
286
|
*
|
|
239
|
-
* @property {(selectedFeatures: any[], stylesMap: any) => any} [updateHighlightedFeatures]
|
|
287
|
+
* @property {(selectedFeatures: any[], activeFeatures: any[], stylesMap: any) => any} [updateHighlightedFeatures]
|
|
240
288
|
* @experimental Update highlighted features on the map.
|
|
241
289
|
*
|
|
242
290
|
* @property {(direction: string) => any} [highlightNextLabel]
|
|
@@ -329,7 +377,7 @@
|
|
|
329
377
|
* Alt text for logo.
|
|
330
378
|
*
|
|
331
379
|
* @property {'light' | 'dark'} [mapColorScheme]
|
|
332
|
-
* Map colour scheme. Sets the default values of `haloColor`, `selectedColor`, and `foregroundColor`
|
|
380
|
+
* Map colour scheme. Sets the default values of `haloColor`, `selectedColor`, `activeColor`, and `foregroundColor`
|
|
333
381
|
* when not explicitly provided, and signals to map overlay components which tonal range to use.
|
|
334
382
|
* `'light'` (default): dark overlays on a light basemap. `'dark'`: light overlays on a dark or aerial basemap.
|
|
335
383
|
*
|
|
@@ -342,10 +390,14 @@
|
|
|
342
390
|
* Injected as the `--map-overlay-halo-color` CSS custom property.
|
|
343
391
|
*
|
|
344
392
|
* @property {string} [selectedColor]
|
|
345
|
-
* Theme colour for
|
|
393
|
+
* Theme colour for committed selection — used by map overlay components to indicate a selected feature.
|
|
346
394
|
* Falls back to `#0b0c0c` (light) or `#ffffff` (dark).
|
|
347
395
|
* Injected as the `--map-overlay-selected-color` CSS custom property.
|
|
348
396
|
*
|
|
397
|
+
* @property {string} [activeColor]
|
|
398
|
+
* Theme colour for the active (keyboard cursor) state — the focus ring shown when navigating features
|
|
399
|
+
* with the keyboard. Falls back to `#ffdd00` for both light and dark schemes.
|
|
400
|
+
*
|
|
349
401
|
* @property {string} [foregroundColor]
|
|
350
402
|
* Foreground colour for elements rendered on top of the map (e.g. text or iconography in overlay components).
|
|
351
403
|
* Falls back to `#0b0c0c` (light) or `#ffffff` (dark).
|
|
@@ -367,7 +419,7 @@
|
|
|
367
419
|
* @property {string} [symbolSvgContent]
|
|
368
420
|
* Default inner SVG path content. When set, overrides `symbol`.
|
|
369
421
|
*
|
|
370
|
-
* @property {string} [viewBox='0 0
|
|
422
|
+
* @property {string} [viewBox='0 0 44 44']
|
|
371
423
|
* Default SVG viewBox.
|
|
372
424
|
*
|
|
373
425
|
* @property {[number, number]} [anchor=[0.5, 0.5]]
|
|
@@ -379,15 +431,10 @@
|
|
|
379
431
|
* @property {string | Record<string, string>} [foregroundColor='#ffffff']
|
|
380
432
|
* Default foreground fill colour.
|
|
381
433
|
*
|
|
382
|
-
* @property {string} [haloWidth='1']
|
|
383
|
-
* Default halo stroke width in SVG units.
|
|
384
|
-
*
|
|
385
434
|
* @property {string} [graphic]
|
|
386
435
|
* Default SVG `d` attribute value for the foreground graphic path. Each built-in symbol sets
|
|
387
436
|
* its own default (a small dot). Override to swap the graphic across all markers globally.
|
|
388
437
|
*
|
|
389
|
-
* @property {string} [selectedWidth='6']
|
|
390
|
-
* Selection ring stroke width in SVG units. App-wide only — ignored at symbol registration and marker creation level.
|
|
391
438
|
*/
|
|
392
439
|
|
|
393
440
|
/**
|
|
@@ -420,8 +467,8 @@
|
|
|
420
467
|
* When set, `symbol` is ignored.
|
|
421
468
|
*
|
|
422
469
|
* @property {string} [viewBox]
|
|
423
|
-
* SVG viewBox attribute for the symbol, e.g. `'0 0
|
|
424
|
-
* Defaults to the registered symbol's viewBox, or `'0 0
|
|
470
|
+
* SVG viewBox attribute for the symbol, e.g. `'0 0 44 44'`.
|
|
471
|
+
* Defaults to the registered symbol's viewBox, or `'0 0 44 44'`.
|
|
425
472
|
*
|
|
426
473
|
* @property {[number, number]} [anchor]
|
|
427
474
|
* Anchor point as a normalised [x, y] pair where [0, 0] is top-left and [1, 1] is bottom-right.
|
|
@@ -434,9 +481,6 @@
|
|
|
434
481
|
* @property {string | Record<string, string>} [foregroundColor]
|
|
435
482
|
* Foreground fill colour of the symbol (e.g. the inner dot on a pin).
|
|
436
483
|
*
|
|
437
|
-
* @property {string} [haloWidth]
|
|
438
|
-
* Stroke width of the halo in SVG units. Defaults to `'1'`.
|
|
439
|
-
*
|
|
440
484
|
* @property {string} [graphic]
|
|
441
485
|
* SVG `d` attribute value for the foreground graphic path. Replaces the foreground shape of the
|
|
442
486
|
* symbol while keeping its background, halo and selection ring intact. Each built-in symbol
|
|
@@ -447,6 +491,14 @@
|
|
|
447
491
|
* import { graphics } from './config/symbolConfig.js'
|
|
448
492
|
* markers.add('id', coords, { symbol: 'pin', graphic: graphics.cross })
|
|
449
493
|
*
|
|
494
|
+
* @property {string} [label]
|
|
495
|
+
* Plain-text label for this marker. Used as the accessible name on the marker SVG, and rendered
|
|
496
|
+
* as a visible label bubble when `showLabel` is `true` or when `symbol` is `null` (standalone label).
|
|
497
|
+
*
|
|
498
|
+
* @property {boolean} [showLabel=false]
|
|
499
|
+
* Whether to render a visible label bubble above the marker symbol. Defaults to `false`.
|
|
500
|
+
* Set `symbol` to `null` instead to create a standalone label with no symbol.
|
|
501
|
+
*
|
|
450
502
|
*/
|
|
451
503
|
|
|
452
504
|
/**
|
|
@@ -457,6 +509,11 @@
|
|
|
457
509
|
* @property {PanelBreakpointConfig} desktop
|
|
458
510
|
* Desktop breakpoint configuration.
|
|
459
511
|
*
|
|
512
|
+
* @property {boolean} [focus=true]
|
|
513
|
+
* Whether to move focus to the panel when it opens. Set to false to prevent the panel from
|
|
514
|
+
* receiving focus — useful for panels present on page load or panels that should not interrupt
|
|
515
|
+
* the user's current flow. Modal panels always receive focus regardless of this setting.
|
|
516
|
+
*
|
|
460
517
|
* @property {string} [html]
|
|
461
518
|
* HTML content.
|
|
462
519
|
*
|
|
@@ -4,11 +4,11 @@ export function constrainKeyboardFocus (containerEl, e) {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
const selectors = [
|
|
7
|
-
'a[href]:not([disabled])',
|
|
8
|
-
'button:not([disabled])',
|
|
9
|
-
'textarea:not([disabled])',
|
|
10
|
-
'input:not([disabled])',
|
|
11
|
-
'select:not([disabled])',
|
|
7
|
+
'a[href]:not([disabled]):not([tabindex="-1"])',
|
|
8
|
+
'button:not([disabled]):not([tabindex="-1"])',
|
|
9
|
+
'textarea:not([disabled]):not([tabindex="-1"])',
|
|
10
|
+
'input:not([disabled]):not([tabindex="-1"])',
|
|
11
|
+
'select:not([disabled]):not([tabindex="-1"])',
|
|
12
12
|
'*[tabindex="0"]:not([disabled])'
|
|
13
13
|
]
|
|
14
14
|
let focusableEls = Array.from(containerEl.querySelectorAll(selectors.join(',')))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
let lastInterfaceType = window.matchMedia('(pointer: coarse)').matches ? 'touch' : 'unknown'
|
|
2
2
|
const interfaceTypeListeners = new Set()
|
|
3
|
+
const interfaceTypeImmediateListeners = new Set()
|
|
3
4
|
|
|
4
5
|
// -----------------------------------------------------------------------------
|
|
5
6
|
// Internal (not exported)
|
|
@@ -20,9 +21,8 @@ function normalizePointerType (pointerType) {
|
|
|
20
21
|
function notifyListeners (newType) {
|
|
21
22
|
if (lastInterfaceType !== newType) {
|
|
22
23
|
lastInterfaceType = newType
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
})
|
|
24
|
+
interfaceTypeImmediateListeners.forEach(listener => listener(newType))
|
|
25
|
+
interfaceTypeListeners.forEach(listener => listener(newType))
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -44,7 +44,18 @@ function createInterfaceDetector () {
|
|
|
44
44
|
|
|
45
45
|
const handlePointer = event => {
|
|
46
46
|
const type = normalizePointerType(event.pointerType)
|
|
47
|
-
|
|
47
|
+
if (type === lastInterfaceType) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
// Update synchronously so getInterfaceType() returns the new value immediately —
|
|
51
|
+
// this prevents focusin handlers from seeing the stale 'keyboard' type during a
|
|
52
|
+
// pointer-triggered focus move. Listeners (React state) are still notified async
|
|
53
|
+
// to avoid layout thrashing during the click event.
|
|
54
|
+
lastInterfaceType = type
|
|
55
|
+
interfaceTypeImmediateListeners.forEach(listener => listener(type))
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
interfaceTypeListeners.forEach(listener => listener(type))
|
|
58
|
+
}, REACT_CLICK_DELAY)
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
const handleKeyDown = e => {
|
|
@@ -81,8 +92,19 @@ function subscribeToInterfaceChanges (onInterfaceTypeChange) {
|
|
|
81
92
|
}
|
|
82
93
|
}
|
|
83
94
|
|
|
95
|
+
// Fires synchronously within the same event cycle — use for direct DOM updates
|
|
96
|
+
// that need to be in sync with the pointer event (no 150ms React delay).
|
|
97
|
+
function subscribeToInterfaceChangesImmediate (onInterfaceTypeChange) {
|
|
98
|
+
interfaceTypeImmediateListeners.add(onInterfaceTypeChange)
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
interfaceTypeImmediateListeners.delete(onInterfaceTypeChange)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
84
105
|
export {
|
|
85
106
|
createInterfaceDetector,
|
|
86
107
|
getInterfaceType,
|
|
87
|
-
subscribeToInterfaceChanges
|
|
108
|
+
subscribeToInterfaceChanges,
|
|
109
|
+
subscribeToInterfaceChangesImmediate
|
|
88
110
|
}
|
|
@@ -54,7 +54,7 @@ const triggerDomEvent = (event, payload) => {
|
|
|
54
54
|
|
|
55
55
|
// --- TEST SUITE ---
|
|
56
56
|
describe('Interface Detector Utility Module', () => {
|
|
57
|
-
let createInterfaceDetector, getInterfaceType, subscribeToInterfaceChanges
|
|
57
|
+
let createInterfaceDetector, getInterfaceType, subscribeToInterfaceChanges, subscribeToInterfaceChangesImmediate
|
|
58
58
|
let cleanup
|
|
59
59
|
|
|
60
60
|
beforeEach(async () => {
|
|
@@ -72,6 +72,7 @@ describe('Interface Detector Utility Module', () => {
|
|
|
72
72
|
createInterfaceDetector = importedModule.createInterfaceDetector
|
|
73
73
|
getInterfaceType = importedModule.getInterfaceType
|
|
74
74
|
subscribeToInterfaceChanges = importedModule.subscribeToInterfaceChanges
|
|
75
|
+
subscribeToInterfaceChangesImmediate = importedModule.subscribeToInterfaceChangesImmediate
|
|
75
76
|
|
|
76
77
|
Object.keys(eventListeners).forEach(key => delete eventListeners[key])
|
|
77
78
|
Object.keys(mediaListeners).forEach(key => delete mediaListeners[key])
|
|
@@ -99,18 +100,21 @@ describe('Interface Detector Utility Module', () => {
|
|
|
99
100
|
|
|
100
101
|
// --- Path 1a: pointerdown 'touch' ---
|
|
101
102
|
triggerDomEvent('pointerdown', { pointerType: 'touch' })
|
|
103
|
+
expect(getInterfaceType()).toBe('touch') // synchronous update
|
|
104
|
+
expect(handler).not.toHaveBeenCalled() // listener still pending
|
|
102
105
|
jest.advanceTimersByTime(150)
|
|
103
|
-
expect(getInterfaceType()).toBe('touch')
|
|
104
106
|
expect(handler).toHaveBeenCalledWith('touch')
|
|
105
107
|
handler.mockClear()
|
|
106
108
|
|
|
107
109
|
// --- Path 1b: pointerdown 'pen' ---
|
|
108
110
|
triggerDomEvent('pointerdown', { pointerType: 'pen' })
|
|
109
111
|
jest.advanceTimersByTime(150)
|
|
110
|
-
expect(handler).not.toHaveBeenCalled()
|
|
112
|
+
expect(handler).not.toHaveBeenCalled() // 'pen' normalises to 'touch' — no change
|
|
111
113
|
|
|
112
114
|
// --- Path 1c: pointerdown 'mouse' ---
|
|
113
115
|
triggerDomEvent('pointerdown', { pointerType: 'mouse' })
|
|
116
|
+
expect(getInterfaceType()).toBe('mouse') // synchronous update
|
|
117
|
+
expect(handler).not.toHaveBeenCalled() // listener still pending
|
|
114
118
|
jest.advanceTimersByTime(150)
|
|
115
119
|
expect(getInterfaceType()).toBe('mouse')
|
|
116
120
|
expect(handler).toHaveBeenCalledWith('mouse')
|
|
@@ -129,6 +133,10 @@ describe('Interface Detector Utility Module', () => {
|
|
|
129
133
|
expect(handler).toHaveBeenCalledWith('keyboard')
|
|
130
134
|
handler.mockClear()
|
|
131
135
|
|
|
136
|
+
// Tab again when already keyboard — notifyListeners no-op branch
|
|
137
|
+
triggerDomEvent('keydown', { key: 'Tab' })
|
|
138
|
+
expect(handler).not.toHaveBeenCalled()
|
|
139
|
+
|
|
132
140
|
// keydown with non-Tab
|
|
133
141
|
triggerDomEvent('keydown', { key: 'a' })
|
|
134
142
|
expect(handler).not.toHaveBeenCalled()
|
|
@@ -152,6 +160,24 @@ describe('Interface Detector Utility Module', () => {
|
|
|
152
160
|
expect(handler).not.toHaveBeenCalled()
|
|
153
161
|
})
|
|
154
162
|
|
|
163
|
+
it('subscribeToInterfaceChangesImmediate fires synchronously and unsubscribes', () => {
|
|
164
|
+
const handler = jest.fn()
|
|
165
|
+
const unsubscribe = subscribeToInterfaceChangesImmediate(handler)
|
|
166
|
+
cleanup = createInterfaceDetector()
|
|
167
|
+
|
|
168
|
+
triggerDomEvent('pointerdown', { pointerType: 'touch' })
|
|
169
|
+
expect(handler).toHaveBeenCalledWith('touch') // fires before timer advance
|
|
170
|
+
|
|
171
|
+
// Tab keydown goes through notifyListeners — exercises immediate forEach callback
|
|
172
|
+
triggerDomEvent('keydown', { key: 'Tab' })
|
|
173
|
+
expect(handler).toHaveBeenCalledWith('keyboard')
|
|
174
|
+
expect(handler).toHaveBeenCalledTimes(2)
|
|
175
|
+
|
|
176
|
+
unsubscribe()
|
|
177
|
+
triggerDomEvent('pointerdown', { pointerType: 'mouse' })
|
|
178
|
+
expect(handler).toHaveBeenCalledTimes(2) // no longer called after unsubscribe
|
|
179
|
+
})
|
|
180
|
+
|
|
155
181
|
it('should return "touch" when matchMedia initially matches coarse pointer', async () => {
|
|
156
182
|
jest.resetModules()
|
|
157
183
|
|