@defra/interactive-map 0.0.21-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/esm/index.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 +65 -4
- 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/datasets.md +39 -1
- package/docs/plugins/interact.md +4 -11
- package/docusaurus.config.cjs +2 -1
- package/package.json +10 -12
- package/plugins/beta/datasets/dist/adapters/maplibre/esm/im-datasets-ml-adapter.js +1 -0
- 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 +2 -0
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-plugin.js +2 -0
- package/plugins/beta/datasets/dist/umd/maplibre/index.js +2 -0
- package/plugins/beta/datasets/src/adapters/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/index.maplibre.umd.js +23 -0
- 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 +7 -1
- package/providers/maplibre/src/index.test.js +15 -2
- 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/rollup.esm.mjs +1 -1
- 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/MapController.jsx +1 -1
- 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/useMapURLSync.js +3 -3
- package/src/App/hooks/useMapURLSync.test.js +20 -3
- 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 +13 -1
- package/src/InteractiveMap/InteractiveMap.test.js +6 -4
- package/src/InteractiveMap/behaviourController.js +1 -2
- package/src/InteractiveMap/buttonManager.js +1 -3
- package/src/InteractiveMap/buttonManager.test.js +16 -8
- package/src/InteractiveMap/domStateManager.js +2 -3
- package/src/InteractiveMap/historyManager.js +4 -3
- package/src/config/appConfig.js +3 -1
- package/src/config/appConfig.test.js +4 -0
- package/src/config/defaults.js +3 -2
- package/src/config/events.js +8 -0
- package/src/config/mapTheme.js +8 -4
- package/src/config/mergeConfig.js +8 -2
- package/src/config/mergeConfig.test.js +19 -0
- 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 +93 -16
- 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/mapStateSync.js +2 -2
- package/src/utils/mapStateSync.test.js +18 -0
- 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 +4 -0
- package/webpack.umd.mjs +9 -6
- package/plugins/beta/datasets/adapters/maplibre/dist/esm/im-datasets-ml-adapter.js +0 -1
- package/plugins/beta/datasets/adapters/maplibre/dist/umd/im-datasets-ml-adapter.js +0 -2
- package/plugins/beta/datasets/adapters/maplibre/dist/umd/index.js +0 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +0 -2
- package/plugins/beta/datasets/dist/umd/index.js +0 -2
- package/plugins/interact/dist/css/index.css +0 -1
- package/plugins/interact/src/interact.scss +0 -24
- /package/plugins/beta/datasets/{adapters/maplibre/dist → dist/adapters/maplibre}/esm/index.js +0 -0
- /package/plugins/beta/datasets/{adapters/maplibre/dist/umd/im-datasets-ml-adapter.js.LICENSE.txt → dist/umd/maplibre/im-datasets-maplibre-adapter.js.LICENSE.txt} +0 -0
- /package/plugins/beta/datasets/dist/umd/{im-datasets-plugin.js.LICENSE.txt → maplibre/im-datasets-plugin.js.LICENSE.txt} +0 -0
- /package/plugins/beta/datasets/dist/umd/{index.js.LICENSE.txt → maplibre/index.js.LICENSE.txt} +0 -0
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]
|
|
@@ -262,6 +310,19 @@
|
|
|
262
310
|
* Coordinate reference system.
|
|
263
311
|
*/
|
|
264
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Configuration options for the MapLibre provider.
|
|
315
|
+
*
|
|
316
|
+
* @typedef {Object} MaplibreProviderConfig
|
|
317
|
+
*
|
|
318
|
+
* @property {string} [workerUrl]
|
|
319
|
+
* URL to a separately hosted MapLibre worker file (`maplibre-gl-csp-worker.js`).
|
|
320
|
+
* Required when running under a Content Security Policy that blocks `blob:` worker
|
|
321
|
+
* URLs. The file ships with maplibre-gl and must be served from your own origin.
|
|
322
|
+
* When set, MapLibre loads the worker from this URL instead of generating an
|
|
323
|
+
* inline blob, so your CSP only needs `worker-src 'self'`.
|
|
324
|
+
*/
|
|
325
|
+
|
|
265
326
|
/**
|
|
266
327
|
* Descriptor for lazy-loading a map provider.
|
|
267
328
|
*
|
|
@@ -316,7 +377,7 @@
|
|
|
316
377
|
* Alt text for logo.
|
|
317
378
|
*
|
|
318
379
|
* @property {'light' | 'dark'} [mapColorScheme]
|
|
319
|
-
* 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`
|
|
320
381
|
* when not explicitly provided, and signals to map overlay components which tonal range to use.
|
|
321
382
|
* `'light'` (default): dark overlays on a light basemap. `'dark'`: light overlays on a dark or aerial basemap.
|
|
322
383
|
*
|
|
@@ -329,10 +390,14 @@
|
|
|
329
390
|
* Injected as the `--map-overlay-halo-color` CSS custom property.
|
|
330
391
|
*
|
|
331
392
|
* @property {string} [selectedColor]
|
|
332
|
-
* Theme colour for
|
|
393
|
+
* Theme colour for committed selection — used by map overlay components to indicate a selected feature.
|
|
333
394
|
* Falls back to `#0b0c0c` (light) or `#ffffff` (dark).
|
|
334
395
|
* Injected as the `--map-overlay-selected-color` CSS custom property.
|
|
335
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
|
+
*
|
|
336
401
|
* @property {string} [foregroundColor]
|
|
337
402
|
* Foreground colour for elements rendered on top of the map (e.g. text or iconography in overlay components).
|
|
338
403
|
* Falls back to `#0b0c0c` (light) or `#ffffff` (dark).
|
|
@@ -354,7 +419,7 @@
|
|
|
354
419
|
* @property {string} [symbolSvgContent]
|
|
355
420
|
* Default inner SVG path content. When set, overrides `symbol`.
|
|
356
421
|
*
|
|
357
|
-
* @property {string} [viewBox='0 0
|
|
422
|
+
* @property {string} [viewBox='0 0 44 44']
|
|
358
423
|
* Default SVG viewBox.
|
|
359
424
|
*
|
|
360
425
|
* @property {[number, number]} [anchor=[0.5, 0.5]]
|
|
@@ -366,15 +431,10 @@
|
|
|
366
431
|
* @property {string | Record<string, string>} [foregroundColor='#ffffff']
|
|
367
432
|
* Default foreground fill colour.
|
|
368
433
|
*
|
|
369
|
-
* @property {string} [haloWidth='1']
|
|
370
|
-
* Default halo stroke width in SVG units.
|
|
371
|
-
*
|
|
372
434
|
* @property {string} [graphic]
|
|
373
435
|
* Default SVG `d` attribute value for the foreground graphic path. Each built-in symbol sets
|
|
374
436
|
* its own default (a small dot). Override to swap the graphic across all markers globally.
|
|
375
437
|
*
|
|
376
|
-
* @property {string} [selectedWidth='6']
|
|
377
|
-
* Selection ring stroke width in SVG units. App-wide only — ignored at symbol registration and marker creation level.
|
|
378
438
|
*/
|
|
379
439
|
|
|
380
440
|
/**
|
|
@@ -407,8 +467,8 @@
|
|
|
407
467
|
* When set, `symbol` is ignored.
|
|
408
468
|
*
|
|
409
469
|
* @property {string} [viewBox]
|
|
410
|
-
* SVG viewBox attribute for the symbol, e.g. `'0 0
|
|
411
|
-
* 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'`.
|
|
412
472
|
*
|
|
413
473
|
* @property {[number, number]} [anchor]
|
|
414
474
|
* Anchor point as a normalised [x, y] pair where [0, 0] is top-left and [1, 1] is bottom-right.
|
|
@@ -421,9 +481,6 @@
|
|
|
421
481
|
* @property {string | Record<string, string>} [foregroundColor]
|
|
422
482
|
* Foreground fill colour of the symbol (e.g. the inner dot on a pin).
|
|
423
483
|
*
|
|
424
|
-
* @property {string} [haloWidth]
|
|
425
|
-
* Stroke width of the halo in SVG units. Defaults to `'1'`.
|
|
426
|
-
*
|
|
427
484
|
* @property {string} [graphic]
|
|
428
485
|
* SVG `d` attribute value for the foreground graphic path. Replaces the foreground shape of the
|
|
429
486
|
* symbol while keeping its background, halo and selection ring intact. Each built-in symbol
|
|
@@ -434,6 +491,14 @@
|
|
|
434
491
|
* import { graphics } from './config/symbolConfig.js'
|
|
435
492
|
* markers.add('id', coords, { symbol: 'pin', graphic: graphics.cross })
|
|
436
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
|
+
*
|
|
437
502
|
*/
|
|
438
503
|
|
|
439
504
|
/**
|
|
@@ -444,6 +509,11 @@
|
|
|
444
509
|
* @property {PanelBreakpointConfig} desktop
|
|
445
510
|
* Desktop breakpoint configuration.
|
|
446
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
|
+
*
|
|
447
517
|
* @property {string} [html]
|
|
448
518
|
* HTML content.
|
|
449
519
|
*
|
|
@@ -621,8 +691,15 @@
|
|
|
621
691
|
* @property {MapStyleConfig} [mapStyle]
|
|
622
692
|
* Map style configuration.
|
|
623
693
|
*
|
|
624
|
-
* @property {string} [
|
|
625
|
-
* URL query parameter
|
|
694
|
+
* @property {string} [mapViewQueryParam='mv']
|
|
695
|
+
* URL query parameter used to control fullscreen/hybrid/buttonFirst state.
|
|
696
|
+
* Override if the default value clashes with an existing parameter on your page.
|
|
697
|
+
*
|
|
698
|
+
* @property {'sync' | 'readOnly' | 'none'} [urlPosition='sync']
|
|
699
|
+
* Controls how map center and zoom interact with the page URL.
|
|
700
|
+
* - `'sync'` — reads on load and writes on pan/zoom (default)
|
|
701
|
+
* - `'readOnly'` — seeds the initial view from the URL but never writes back
|
|
702
|
+
* - `'none'` — ignores the URL entirely; use when you don't want the user's pan/zoom to be persisted or shared
|
|
626
703
|
*
|
|
627
704
|
* @property {MarkerConfig[]} [markers]
|
|
628
705
|
* Initial markers to display on the map.
|
|
@@ -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
|
|
|
@@ -72,8 +72,8 @@ const setMapStateInURL = (id, state, currentHref = window.location.href) => {
|
|
|
72
72
|
* @param {string} [search] - URL search string. Defaults to `window.location.search`.
|
|
73
73
|
* @returns {{ center: [number, number], zoom: number } | { bounds: any }}
|
|
74
74
|
*/
|
|
75
|
-
const getInitialMapState = ({ id, center, zoom, bounds }, search = window.location.search) => {
|
|
76
|
-
const savedState = getMapStateFromURL(id, search)
|
|
75
|
+
const getInitialMapState = ({ id, center, zoom, bounds, urlPosition = 'sync' }, search = window.location.search) => {
|
|
76
|
+
const savedState = urlPosition === 'none' ? null : getMapStateFromURL(id, search)
|
|
77
77
|
if (savedState) {
|
|
78
78
|
return {
|
|
79
79
|
center: savedState.center,
|
|
@@ -30,6 +30,24 @@ describe('mapStateSync utilities', () => {
|
|
|
30
30
|
const state = getInitialMapState({ id: 'map1', bounds }, '')
|
|
31
31
|
expect(state.bounds).toEqual(bounds)
|
|
32
32
|
})
|
|
33
|
+
|
|
34
|
+
it('ignores URL params when urlPosition is none', () => {
|
|
35
|
+
const state = getInitialMapState(
|
|
36
|
+
{ id: 'map1', center: [1, 2], zoom: 3, urlPosition: 'none' },
|
|
37
|
+
'?map1:center=10,20&map1:zoom=5'
|
|
38
|
+
)
|
|
39
|
+
expect(state.center).toEqual([1, 2])
|
|
40
|
+
expect(state.zoom).toBe(3)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('reads URL params when urlPosition is readOnly', () => {
|
|
44
|
+
const state = getInitialMapState(
|
|
45
|
+
{ id: 'map1', center: [1, 2], zoom: 3, urlPosition: 'readOnly' },
|
|
46
|
+
'?map1:center=10,20&map1:zoom=5'
|
|
47
|
+
)
|
|
48
|
+
expect(state.center).toEqual([10, 20])
|
|
49
|
+
expect(state.zoom).toBe(5)
|
|
50
|
+
})
|
|
33
51
|
})
|
|
34
52
|
|
|
35
53
|
describe('setMapStateInURL', () => {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { getValueForStyle } from '../utils/getValueForStyle.js'
|
|
2
|
-
|
|
3
1
|
// Border path rendered behind the pattern content in Key panel symbols (20×20 coordinate space).
|
|
4
2
|
export const KEY_BORDER_PATH = '<path d="M19 2.862v14.275c0 1.028-.835 1.862-1.862 1.862H2.863c-1.028 0-1.862-.835-1.862-1.862V2.862C1.001 1.834 1.836 1 2.863 1h14.275C18.166 1 19 1.835 19 2.862z" fill="{{backgroundColor}}" stroke="{{foregroundColor}}" stroke-width="2"/>'
|
|
3
|
+
// Minimum oversampling — keeps 16×16 physical pixels as the floor so patterns remain crisp.
|
|
4
|
+
const PATTERN_MIN_PIXEL_RATIO = 2
|
|
5
|
+
|
|
6
|
+
export const getEffectivePixelRatio = (pixelRatio) => Math.max(PATTERN_MIN_PIXEL_RATIO, pixelRatio * 2)
|
|
5
7
|
|
|
6
8
|
export const hashString = (str) => {
|
|
7
9
|
let hash = 0
|
|
@@ -32,68 +34,3 @@ export const injectColors = (content, foregroundColor, backgroundColor) =>
|
|
|
32
34
|
* @returns {boolean}
|
|
33
35
|
*/
|
|
34
36
|
export const hasPattern = (dataset) => !!(dataset.fillPattern || dataset.fillPatternSvgContent)
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Returns the raw (un-coloured) inner SVG content for a dataset's pattern.
|
|
38
|
-
* Precedence: inline fillPatternSvgContent → named fillPattern from registry.
|
|
39
|
-
*
|
|
40
|
-
* @param {Object} dataset
|
|
41
|
-
* @param {Object} patternRegistry
|
|
42
|
-
* @returns {string|null}
|
|
43
|
-
*/
|
|
44
|
-
export const getPatternInnerContent = (dataset, patternRegistry) => {
|
|
45
|
-
if (dataset.fillPatternSvgContent) {
|
|
46
|
-
return dataset.fillPatternSvgContent
|
|
47
|
-
}
|
|
48
|
-
if (dataset.fillPattern) {
|
|
49
|
-
return patternRegistry?.get(dataset.fillPattern)?.svgContent ?? null
|
|
50
|
-
}
|
|
51
|
-
return null
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Returns a deterministic image ID for a pattern + resolved colour + pixel ratio combination.
|
|
56
|
-
*
|
|
57
|
-
* @param {Object} dataset
|
|
58
|
-
* @param {string} mapStyleId
|
|
59
|
-
* @param {Object} patternRegistry
|
|
60
|
-
* @param {number} [pixelRatio=1]
|
|
61
|
-
* @returns {string|null}
|
|
62
|
-
*/
|
|
63
|
-
// Minimum oversampling — keeps 16×16 physical pixels as the floor so patterns remain crisp.
|
|
64
|
-
export const PATTERN_MIN_PIXEL_RATIO = 2
|
|
65
|
-
|
|
66
|
-
export const getPatternImageId = (dataset, mapStyleId, patternRegistry, pixelRatio = 1) => {
|
|
67
|
-
const innerContent = getPatternInnerContent(dataset, patternRegistry)
|
|
68
|
-
if (!innerContent) {
|
|
69
|
-
return null
|
|
70
|
-
}
|
|
71
|
-
const fg = getValueForStyle(dataset.fillPatternForegroundColor, mapStyleId) || 'black'
|
|
72
|
-
const bg = getValueForStyle(dataset.fillPatternBackgroundColor, mapStyleId) || 'transparent'
|
|
73
|
-
const effectiveRatio = Math.max(PATTERN_MIN_PIXEL_RATIO, pixelRatio)
|
|
74
|
-
return `pattern-${hashString(innerContent + fg + bg)}-${effectiveRatio}x`
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Returns colour-injected SVG path content for use in Key panel pattern symbols.
|
|
79
|
-
* Returns { border, content } where border is the rounded-rect outline and content
|
|
80
|
-
* is the pattern fill. Returns null if the dataset has no pattern.
|
|
81
|
-
*
|
|
82
|
-
* @param {Object} dataset
|
|
83
|
-
* @param {string} mapStyleId
|
|
84
|
-
* @param {Object} patternRegistry
|
|
85
|
-
* @returns {{ border: string, content: string }|null}
|
|
86
|
-
*/
|
|
87
|
-
export const getKeyPatternPaths = (dataset, mapStyleId, patternRegistry) => {
|
|
88
|
-
const innerContent = getPatternInnerContent(dataset, patternRegistry)
|
|
89
|
-
if (!innerContent) {
|
|
90
|
-
return null
|
|
91
|
-
}
|
|
92
|
-
const fg = getValueForStyle(dataset.fillPatternForegroundColor, mapStyleId) || 'black'
|
|
93
|
-
const bg = getValueForStyle(dataset.fillPatternBackgroundColor, mapStyleId) || 'transparent'
|
|
94
|
-
const borderStroke = getValueForStyle(dataset.stroke, mapStyleId) || fg
|
|
95
|
-
return {
|
|
96
|
-
border: injectColors(KEY_BORDER_PATH, borderStroke, bg),
|
|
97
|
-
content: injectColors(innerContent, fg, bg)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
hashString,
|
|
3
3
|
injectColors,
|
|
4
|
-
hasPattern
|
|
5
|
-
getPatternInnerContent,
|
|
6
|
-
getPatternImageId,
|
|
7
|
-
getKeyPatternPaths,
|
|
8
|
-
KEY_BORDER_PATH
|
|
4
|
+
hasPattern
|
|
9
5
|
} from './patternUtils.js'
|
|
10
6
|
|
|
11
|
-
const mockRegistry = {
|
|
12
|
-
get: (id) => id === 'dot' ? { id: 'dot', svgContent: '<path d="M4 4" fill="{{foregroundColor}}"/>' } : undefined
|
|
13
|
-
}
|
|
14
|
-
|
|
15
7
|
describe('hashString', () => {
|
|
16
8
|
test('returns a non-empty string', () => {
|
|
17
9
|
expect(typeof hashString('hello')).toBe('string')
|
|
@@ -61,117 +53,3 @@ describe('hasPattern', () => {
|
|
|
61
53
|
expect(hasPattern({ fill: 'red' })).toBe(false)
|
|
62
54
|
})
|
|
63
55
|
})
|
|
64
|
-
|
|
65
|
-
describe('getPatternInnerContent', () => {
|
|
66
|
-
test('returns fillPatternSvgContent when set (inline SVG takes precedence)', () => {
|
|
67
|
-
const dataset = { fillPatternSvgContent: '<path d="custom"/>', fillPattern: 'dot' }
|
|
68
|
-
expect(getPatternInnerContent(dataset, mockRegistry)).toBe('<path d="custom"/>')
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test('returns svgContent from registry for a named fillPattern', () => {
|
|
72
|
-
const dataset = { fillPattern: 'dot' }
|
|
73
|
-
expect(getPatternInnerContent(dataset, mockRegistry)).toBe('<path d="M4 4" fill="{{foregroundColor}}"/>')
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
test('returns null for an unregistered fillPattern name', () => {
|
|
77
|
-
const dataset = { fillPattern: 'unknown-pattern' }
|
|
78
|
-
expect(getPatternInnerContent(dataset, mockRegistry)).toBeNull()
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
test('returns null when no pattern is configured', () => {
|
|
82
|
-
expect(getPatternInnerContent({}, mockRegistry)).toBeNull()
|
|
83
|
-
})
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
describe('getPatternImageId', () => {
|
|
87
|
-
test('returns a deterministic string id', () => {
|
|
88
|
-
const dataset = { fillPattern: 'dot', fillPatternForegroundColor: 'red', fillPatternBackgroundColor: 'blue' }
|
|
89
|
-
const id = getPatternImageId(dataset, 'style-a', mockRegistry)
|
|
90
|
-
expect(typeof id).toBe('string')
|
|
91
|
-
expect(id).toMatch(/^pattern-/)
|
|
92
|
-
expect(id).toBe(getPatternImageId(dataset, 'style-a', mockRegistry))
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
test('returns null when no pattern content is found', () => {
|
|
96
|
-
expect(getPatternImageId({ fillPattern: 'unknown' }, 'style-a', mockRegistry)).toBeNull()
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
test('produces different ids for different colours', () => {
|
|
100
|
-
const base = { fillPattern: 'dot' }
|
|
101
|
-
const idA = getPatternImageId({ ...base, fillPatternForegroundColor: 'red' }, 'style-a', mockRegistry)
|
|
102
|
-
const idB = getPatternImageId({ ...base, fillPatternForegroundColor: 'blue' }, 'style-a', mockRegistry)
|
|
103
|
-
expect(idA).not.toBe(idB)
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
test('floors effective ratio at PATTERN_MIN_PIXEL_RATIO so low-DPI ids match 2x', () => {
|
|
107
|
-
const dataset = { fillPattern: 'dot' }
|
|
108
|
-
const id1x = getPatternImageId(dataset, 'style-a', mockRegistry, 1)
|
|
109
|
-
const id2x = getPatternImageId(dataset, 'style-a', mockRegistry, 2)
|
|
110
|
-
expect(id1x).toBe(id2x)
|
|
111
|
-
expect(id1x).toMatch(/-2x$/)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
test('produces different ids for pixelRatios above the floor', () => {
|
|
115
|
-
const dataset = { fillPattern: 'dot' }
|
|
116
|
-
const id2x = getPatternImageId(dataset, 'style-a', mockRegistry, 2)
|
|
117
|
-
const id3x = getPatternImageId(dataset, 'style-a', mockRegistry, 3)
|
|
118
|
-
expect(id2x).not.toBe(id3x)
|
|
119
|
-
expect(id2x).toMatch(/-2x$/)
|
|
120
|
-
expect(id3x).toMatch(/-3x$/)
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
test('falls back to "black" foreground and "transparent" background when colours are absent', () => {
|
|
124
|
-
const id = getPatternImageId({ fillPattern: 'dot' }, 'style-a', mockRegistry)
|
|
125
|
-
const idExplicit = getPatternImageId(
|
|
126
|
-
{ fillPattern: 'dot', fillPatternForegroundColor: 'black', fillPatternBackgroundColor: 'transparent' },
|
|
127
|
-
'style-a',
|
|
128
|
-
mockRegistry
|
|
129
|
-
)
|
|
130
|
-
expect(id).toBe(idExplicit)
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
describe('getKeyPatternPaths', () => {
|
|
135
|
-
test('returns border and content strings with colours injected', () => {
|
|
136
|
-
const dataset = {
|
|
137
|
-
fillPattern: 'dot',
|
|
138
|
-
fillPatternForegroundColor: 'red',
|
|
139
|
-
fillPatternBackgroundColor: 'white',
|
|
140
|
-
stroke: 'black'
|
|
141
|
-
}
|
|
142
|
-
const result = getKeyPatternPaths(dataset, 'style-a', mockRegistry)
|
|
143
|
-
expect(result).not.toBeNull()
|
|
144
|
-
expect(result.border).toContain('black') // stroke colour
|
|
145
|
-
expect(result.border).toContain('white') // background colour
|
|
146
|
-
expect(result.content).toContain('red') // foreground colour
|
|
147
|
-
expect(result.border).not.toContain('{{foregroundColor}}')
|
|
148
|
-
expect(result.content).not.toContain('{{foregroundColor}}')
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
test('returns null when no pattern content is found', () => {
|
|
152
|
-
expect(getKeyPatternPaths({ fillPattern: 'unknown' }, 'style-a', mockRegistry)).toBeNull()
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
test('falls back to "black" fg and "transparent" bg when colour properties are absent', () => {
|
|
156
|
-
const result = getKeyPatternPaths({ fillPattern: 'dot' }, 'style-a', mockRegistry)
|
|
157
|
-
expect(result).not.toBeNull()
|
|
158
|
-
expect(result.content).toContain('black')
|
|
159
|
-
expect(result.border).toContain('transparent')
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
test('border stroke falls back to foreground colour when stroke is absent', () => {
|
|
163
|
-
const result = getKeyPatternPaths(
|
|
164
|
-
{ fillPattern: 'dot', fillPatternForegroundColor: 'green' },
|
|
165
|
-
'style-a',
|
|
166
|
-
mockRegistry
|
|
167
|
-
)
|
|
168
|
-
expect(result).not.toBeNull()
|
|
169
|
-
// borderStroke falls back to fg ('green'), so the border uses green for both stroke and background
|
|
170
|
-
expect(result.border).toContain('green')
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
test('KEY_BORDER_PATH contains foregroundColor and backgroundColor tokens', () => {
|
|
174
|
-
expect(KEY_BORDER_PATH).toContain('{{foregroundColor}}')
|
|
175
|
-
expect(KEY_BORDER_PATH).toContain('{{backgroundColor}}')
|
|
176
|
-
})
|
|
177
|
-
})
|
package/src/utils/symbolUtils.js
CHANGED
|
@@ -14,23 +14,21 @@ const SYMBOL_STYLE_PROPS = new Set([
|
|
|
14
14
|
export const hasSymbol = (dataset) => !!(dataset.symbol || dataset.symbolSvgContent)
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* Returns true if a marker item should render as a standalone label with no symbol.
|
|
18
|
+
* Requires label content and at least one of symbol/symbolSvgContent to be explicitly null.
|
|
19
|
+
* Undefined values fall through to render-time default symbol resolution.
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* @param {Object} dataset
|
|
23
|
-
* @param {Object} symbolRegistry
|
|
24
|
-
* @returns {Object|undefined}
|
|
21
|
+
* @param {Object} marker
|
|
22
|
+
* @returns {boolean}
|
|
25
23
|
*/
|
|
26
|
-
export const
|
|
27
|
-
if (
|
|
28
|
-
return
|
|
24
|
+
export const isStandaloneLabel = (marker) => {
|
|
25
|
+
if (!marker.label) {
|
|
26
|
+
return false
|
|
29
27
|
}
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
28
|
+
if (marker.symbol || marker.symbolSvgContent) {
|
|
29
|
+
return false
|
|
32
30
|
}
|
|
33
|
-
return
|
|
31
|
+
return marker.symbol === null || marker.symbolSvgContent === null
|
|
34
32
|
}
|
|
35
33
|
|
|
36
34
|
/**
|