@defra/interactive-map 0.0.43-fmp-experimental → 0.0.45-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/templates/add-polygons.njk +11 -7
- package/assets/templates/add-symbols.njk +5 -1
- package/assets/templates/select-feature.njk +72 -15
- 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/context.md +24 -0
- package/docs/api/keyboard-shortcut-definition.md +102 -0
- package/docs/api.md +18 -8
- package/docs/building-a-plugin.md +1 -0
- package/docs/examples/add-marker-with-panel.mdx +4 -0
- package/docs/examples/add-polygons.mdx +16 -12
- package/docs/examples/add-symbols.mdx +4 -0
- package/docs/examples/basic-map.mdx +4 -0
- package/docs/examples/button-map.mdx +4 -0
- package/docs/examples/draw-tools.mdx +4 -0
- package/docs/examples/place-marker.mdx +4 -0
- package/docs/examples/search-control.mdx +4 -0
- package/docs/examples/select-feature.mdx +4 -0
- package/docs/examples/style-switcher.mdx +36 -44
- package/docs/examples/toggle-marker-label.mdx +4 -0
- package/docs/plugins/draw.md +2 -2
- package/docs/plugins/plugin-manifest.md +9 -0
- package/package.json +4 -2
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/api/newPolygon.js +1 -0
- package/plugins/beta/draw-es/src/events.js +17 -8
- package/plugins/beta/draw-es/src/reducer.js +8 -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/draw-ol/dist/css/index.css +1 -13
- package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
- package/plugins/beta/frame/dist/css/index.css +1 -11
- package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/src/Frame.jsx +28 -2
- package/plugins/beta/map-styles/dist/css/index.css +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/scale-bar/dist/css/index.css +1 -8
- package/plugins/datasets/dist/css/5648.index.css +1 -5
- package/plugins/datasets/dist/css/index.css +1 -1
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/src/components/LayersMenu/Layers.module.scss +1 -0
- package/plugins/datasets/src/initialise/DatasetsInit.jsx +2 -2
- package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +6 -5
- package/plugins/datasets/src/registry/dataset.js +8 -12
- package/plugins/datasets/src/registry/dataset.test.js +17 -0
- package/plugins/datasets/src/registry/datasetRegistry.js +4 -5
- package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
- package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/esm/index.js +1 -1
- package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/umd/index.js +1 -1
- package/plugins/draw/src/DrawInit.jsx +41 -26
- package/plugins/draw/src/DrawInit.test.jsx +55 -12
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +113 -114
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +82 -9
- package/plugins/draw/src/adapters/maplibre/drawEvents.js +3 -1
- package/plugins/draw/src/adapters/maplibre/modes/createDrawMode.test.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/__helpers__/harness.js +18 -9
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.js +9 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +11 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +23 -2
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +53 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.js +29 -19
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +17 -2
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/undoHandlers.test.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +35 -29
- package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +24 -6
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +22 -20
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +2 -2
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +3 -3
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.js +31 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/geometryHelpers.test.js +8 -1
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +38 -30
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +20 -3
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +5 -5
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +2 -2
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +2 -2
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +3 -3
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -7
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +4 -4
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +14 -14
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.js +12 -32
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexQueries.test.js +10 -10
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +48 -22
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +74 -6
- package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +2 -2
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +6 -0
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +9 -0
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +15 -2
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +27 -1
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +1 -1
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +40 -17
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +55 -1
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.js +13 -7
- package/plugins/draw/src/adapters/openlayers/draw/vertexPlacement.test.js +7 -15
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +36 -4
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +41 -1
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +35 -25
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +40 -2
- package/plugins/draw/src/adapters/openlayers/edit/nudge.js +2 -2
- package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +2 -2
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +40 -54
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +32 -13
- package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +1 -1
- package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +17 -1
- package/plugins/draw/src/events.js +4 -4
- package/plugins/draw/src/events.test.js +1 -1
- package/plugins/draw/src/hooks/useSpatialList.js +180 -0
- package/plugins/draw/src/hooks/useSpatialList.test.js +339 -0
- package/plugins/draw/src/manifest.js +4 -1
- package/plugins/draw/src/manifest.test.js +19 -7
- package/plugins/draw/src/utils/spatial.js +1 -31
- package/plugins/draw/src/utils/spatial.test.js +1 -24
- 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 +2 -2
- package/plugins/interact/src/InteractInit.test.js +3 -3
- package/plugins/interact/src/events.js +11 -2
- package/plugins/interact/src/events.test.js +32 -0
- package/plugins/interact/src/hooks/useAttachEvents.js +20 -0
- package/plugins/interact/src/hooks/useAttachEvents.test.js +44 -1
- package/plugins/interact/src/hooks/useCrossHairVisibility.js +9 -4
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +7 -7
- package/plugins/interact/src/hooks/useSpatialList.js +251 -0
- package/plugins/interact/src/hooks/{useMapItemList.test.js → useSpatialList.test.js} +216 -70
- package/plugins/interact/src/manifest.js +12 -1
- package/plugins/interact/src/manifest.test.js +20 -0
- package/plugins/interact/src/utils/spatial.js +24 -1
- package/plugins/interact/src/utils/spatial.test.js +26 -1
- package/plugins/map-key/dist/css/index.css +1 -106
- package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -1
- package/plugins/map-key/dist/umd/im-map-key-plugin.js +1 -1
- package/plugins/map-key/dist/umd/index.js +1 -1
- package/plugins/map-key/src/components/Key/Key.jsx +3 -4
- package/plugins/map-key/src/components/Key/Key.module.scss +10 -8
- package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +2 -2
- package/plugins/map-key/src/components/Key/KeyItem.jsx +2 -2
- package/plugins/map-key/src/components/Key/KeySvg.jsx +9 -7
- package/plugins/map-key/src/components/Key/KeySvg.test.jsx +4 -4
- package/plugins/map-key/src/components/Key/MapKey.jsx +5 -3
- package/plugins/map-key/src/manifest.js +1 -1
- package/plugins/map-key/src/utils/groupStyles.js +7 -0
- package/plugins/map-key/src/utils/mergeKeyGroupItems.js +12 -0
- package/plugins/map-key/src/utils/mergeKeyGroupItems.test.js +53 -0
- package/plugins/menu/dist/css/index.css +1 -80
- package/plugins/menu/dist/esm/im-menu-plugin.js +1 -1
- package/plugins/menu/dist/umd/im-menu-plugin.js +1 -1
- package/plugins/menu/dist/umd/index.js +1 -1
- package/plugins/menu/src/initialise/MenuInit.jsx +15 -3
- package/plugins/menu/src/initialise/MenuInit.test.jsx +25 -0
- package/plugins/menu/src/initialise/createMenu.js +3 -1
- package/plugins/menu/src/initialise/createMenu.test.js +20 -5
- package/plugins/menu/src/initialise/updateUrl.js +44 -0
- package/plugins/menu/src/initialise/updateUrl.test.js +91 -0
- package/plugins/menu/src/reducers/menuStateReducer.js +19 -1
- package/plugins/menu/src/reducers/menuStateReducer.test.js +53 -20
- package/plugins/menu/src/reducers/pluginState.js +1 -6
- package/plugins/menu/src/reducers/pluginState.test.js +0 -25
- 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/Form/Form.jsx +3 -3
- package/plugins/search/src/components/Suggestions/Suggestions.jsx +2 -2
- package/plugins/search/src/components/Suggestions/Suggestions.test.jsx +1 -1
- package/plugins/search/src/manifest.js +5 -9
- package/plugins/search/src/manifest.test.js +5 -0
- package/providers/beta/esri/dist/css/index.css +1 -34
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/utils/spatial.js +15 -13
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/src/openlayersProvider.js +3 -3
- package/providers/beta/openlayers/src/openlayersProvider.test.js +6 -3
- package/providers/beta/openlayers/src/utils/queryFeatures.js +72 -0
- package/providers/beta/openlayers/src/utils/queryFeatures.test.js +122 -1
- package/providers/beta/openlayers/src/utils/spatial.js +16 -14
- package/providers/beta/openlayers/src/utils/spatial.test.js +10 -6
- package/providers/maplibre/dist/esm/im-maplibre-provider.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/defaults.test.js +26 -0
- package/providers/maplibre/src/maplibreProvider.js +1 -1
- package/providers/maplibre/src/utils/labels.js +2 -2
- package/providers/maplibre/src/utils/labels.test.js +5 -5
- package/providers/maplibre/src/utils/spatial.js +19 -74
- package/providers/maplibre/src/utils/spatial.test.js +8 -32
- package/rollup.esm.mjs +2 -1
- package/scripts/publish-package.sh +36 -0
- package/src/App/App.jsx +1 -1
- package/src/App/App.test.jsx +6 -1
- package/src/App/components/CrossHair/CrossHair.jsx +25 -11
- package/src/App/components/CrossHair/CrossHair.module.scss +37 -12
- package/src/App/components/CrossHair/CrossHair.test.jsx +40 -11
- package/src/App/components/Hints/Hints.jsx +3 -3
- package/src/App/components/Hints/Hints.test.jsx +3 -3
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +4 -1
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +17 -0
- package/src/App/components/MapButton/MapButton.jsx +19 -7
- package/src/App/components/MapButton/MapButton.module.scss +1 -2
- package/src/App/components/MapButton/MapButton.test.jsx +42 -4
- package/src/App/components/{MoveControls/MoveControls.jsx → MapControls/MapControls.jsx} +10 -10
- package/src/App/components/{MoveControls/MoveControls.module.scss → MapControls/MapControls.module.scss} +25 -25
- package/src/App/components/{MoveControls/MoveControls.test.jsx → MapControls/MapControls.test.jsx} +48 -48
- package/src/App/components/Markers/Markers.jsx +2 -2
- package/src/App/components/Markers/Markers.test.jsx +3 -3
- package/src/App/components/Markers/SymbolMarker.jsx +2 -3
- package/src/App/components/Markers/SymbolMarker.test.jsx +8 -9
- package/src/App/components/Panel/Panel.jsx +19 -20
- package/src/App/components/Panel/Panel.module.scss +9 -0
- package/src/App/components/Panel/Panel.test.jsx +69 -4
- package/src/App/components/PopupMenu/PopupMenu.jsx +7 -3
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +71 -0
- package/src/App/components/PopupMenu/usePopupMenu.js +10 -9
- package/src/App/components/Viewport/MapStatus.jsx +1 -1
- package/src/App/components/Viewport/SpatialList.jsx +47 -0
- package/src/App/components/Viewport/{Features.module.scss → SpatialList.module.scss} +6 -6
- package/src/App/components/Viewport/SpatialList.test.jsx +285 -0
- package/src/App/components/Viewport/Viewport.jsx +21 -11
- package/src/App/components/Viewport/Viewport.test.jsx +18 -2
- package/src/App/controls/keyboardMappings.js +7 -2
- package/src/App/controls/keyboardMappings.test.js +21 -0
- package/src/App/controls/keyboardShortcuts.js +16 -2
- package/src/App/controls/keyboardShortcuts.test.js +21 -0
- package/src/App/hooks/useCrossHairAPI.js +29 -1
- package/src/App/hooks/useCrossHairAPI.test.js +17 -1
- package/src/App/hooks/useHintsAPI.js +5 -5
- package/src/App/hooks/useInterfaceAPI.js +2 -1
- package/src/App/hooks/useInterfaceAPI.test.js +9 -0
- package/src/App/hooks/useKeyboardShortcuts.js +66 -35
- package/src/App/hooks/useKeyboardShortcuts.test.js +52 -0
- package/src/App/hooks/useModalPanelBehaviour.js +48 -34
- package/src/App/hooks/useModalPanelBehaviour.test.js +59 -0
- package/src/App/hooks/useSpatialListFocus.js +296 -0
- package/src/App/hooks/useSpatialListFocus.test.js +957 -0
- package/src/App/hooks/useSpatialListItems.js +51 -0
- package/src/App/hooks/useSpatialListItems.test.js +217 -0
- package/src/App/initialiseApp.js +6 -3
- package/src/App/initialiseApp.test.js +11 -0
- package/src/App/layout/Layout.jsx +4 -2
- package/src/App/layout/Layout.test.jsx +21 -0
- package/src/App/layout/layout.module.scss +7 -1
- package/src/App/registry/keyboardShortcutRegistry.js +2 -2
- package/src/App/registry/keyboardShortcutRegistry.test.js +16 -0
- package/src/App/registry/pluginRegistry.js +13 -0
- package/src/App/registry/pluginRegistry.test.js +15 -0
- package/src/App/registry/spatialListRegistry.js +104 -0
- package/src/App/registry/spatialListRegistry.test.js +218 -0
- package/src/App/renderer/HtmlElementHost.jsx +12 -32
- package/src/App/renderer/mapButtons.js +4 -0
- package/src/App/renderer/mapButtons.test.js +20 -0
- package/src/App/renderer/mapPanels.js +33 -53
- package/src/App/renderer/mapPanels.test.js +29 -4
- package/src/App/renderer/slotHelpers.js +33 -0
- package/src/App/renderer/slotHelpers.test.js +79 -2
- package/src/App/store/AppProvider.jsx +3 -2
- package/src/App/store/ServiceProvider.jsx +5 -3
- package/src/App/store/ServiceProvider.test.jsx +20 -1
- package/src/InteractiveMap/InteractiveMap.js +13 -2
- package/src/InteractiveMap/InteractiveMap.test.js +33 -0
- package/src/config/appConfig.js +17 -17
- package/src/config/appConfig.test.js +19 -19
- package/src/config/defaults.js +2 -2
- package/src/config/events.js +8 -8
- package/src/scss/main.scss +2 -2
- package/src/scss/tools/_govuk-content.scss +29 -0
- package/src/scss/tools/_index.scss +2 -1
- package/src/test-utils.js +8 -0
- package/src/types.js +53 -11
- package/src/utils/findNearestItemInDirection.js +29 -0
- package/src/utils/findNearestItemInDirection.test.js +67 -0
- package/src/utils/getPanelElementId.js +4 -0
- package/src/utils/getPanelElementId.test.js +11 -0
- package/src/utils/getPanelRole.js +22 -0
- package/src/utils/getPanelRole.test.js +56 -0
- package/src/utils/globalAltShortcuts.js +12 -0
- package/src/utils/globalAltShortcuts.test.js +27 -0
- package/src/utils/spatialNavigate.js +49 -0
- package/src/utils/spatialNavigate.test.js +42 -0
- package/webpack.umd.mjs +6 -1
- package/plugins/interact/src/hooks/useMapItemList.js +0 -187
- package/src/App/components/Viewport/Features.jsx +0 -35
- package/src/App/components/Viewport/Features.test.jsx +0 -124
- package/src/App/hooks/useFeatureFocus.js +0 -199
- package/src/App/hooks/useFeatureFocus.test.js +0 -635
- package/src/App/hooks/useFeatureItems.js +0 -39
- package/src/App/hooks/useFeatureItems.test.js +0 -155
|
@@ -7,7 +7,7 @@ import turfBbox from '@turf/bbox'
|
|
|
7
7
|
// -----------------------------------------------------------------------------
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Calculate distance in
|
|
10
|
+
* Calculate distance in metres between two [lng, lat] coordinates
|
|
11
11
|
*/
|
|
12
12
|
const getDistance = (from, to) => {
|
|
13
13
|
const [lng1, lat1] = from
|
|
@@ -16,22 +16,24 @@ const getDistance = (from, to) => {
|
|
|
16
16
|
const fromLatLon = new LatLon(lat1, lng1)
|
|
17
17
|
const toLatLon = new LatLon(lat2, lng2)
|
|
18
18
|
|
|
19
|
-
return fromLatLon.distanceTo(toLatLon) //
|
|
19
|
+
return fromLatLon.distanceTo(toLatLon) // metres
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Format dimension,
|
|
23
|
+
* Format dimension, metres if less than 0.5 miles, otherwise miles
|
|
24
24
|
*/
|
|
25
|
-
const formatDimension = (
|
|
25
|
+
const formatDimension = (metres) => {
|
|
26
26
|
const WHOLE_MILE_THRESHOLD = 10
|
|
27
27
|
const MILE_THRESHOLD = 0.5
|
|
28
|
-
const
|
|
28
|
+
const METRES_PER_MILE = 1609.344
|
|
29
29
|
|
|
30
|
-
const miles =
|
|
30
|
+
const miles = metres / METRES_PER_MILE
|
|
31
31
|
|
|
32
32
|
// Check if we are under the half-mile threshold
|
|
33
33
|
if (miles < MILE_THRESHOLD) {
|
|
34
|
-
|
|
34
|
+
const roundedMetres = Math.round(metres)
|
|
35
|
+
const units = roundedMetres === 1 ? 'metre' : 'metres'
|
|
36
|
+
return `${roundedMetres} ${units}`
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
if (miles < WHOLE_MILE_THRESHOLD) {
|
|
@@ -50,7 +52,7 @@ const formatDimension = (meters) => {
|
|
|
50
52
|
|
|
51
53
|
/**
|
|
52
54
|
* bounds: [[west, south], [east, north]]
|
|
53
|
-
* Returns: "
|
|
55
|
+
* Returns: "400 metres by 1.4 miles"
|
|
54
56
|
*/
|
|
55
57
|
const getAreaDimensions = (bounds) => {
|
|
56
58
|
let west, south, east, north
|
|
@@ -69,12 +71,12 @@ const getAreaDimensions = (bounds) => {
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
// Width: west <-> east at the southern latitude
|
|
72
|
-
const
|
|
74
|
+
const widthMetres = getDistance([west, south], [east, south])
|
|
73
75
|
// Height: south <-> north at the western longitude
|
|
74
|
-
const
|
|
76
|
+
const heightMetres = getDistance([west, south], [west, north])
|
|
75
77
|
|
|
76
|
-
const widthLabel = formatDimension(
|
|
77
|
-
const heightLabel = formatDimension(
|
|
78
|
+
const widthLabel = formatDimension(widthMetres)
|
|
79
|
+
const heightLabel = formatDimension(heightMetres)
|
|
78
80
|
|
|
79
81
|
return `${heightLabel} by ${widthLabel}`
|
|
80
82
|
}
|
|
@@ -82,7 +84,7 @@ const getAreaDimensions = (bounds) => {
|
|
|
82
84
|
/**
|
|
83
85
|
* Generate a cardinal direction move description.
|
|
84
86
|
* Only non-zero moves are announced.
|
|
85
|
-
* Example: "north
|
|
87
|
+
* Example: "north 400 metres", "east 750 metres", or "south 400 metres, west 750 metres"
|
|
86
88
|
*/
|
|
87
89
|
const getCardinalMove = (from, to) => {
|
|
88
90
|
const [lng1, lat1] = from
|
|
@@ -94,74 +96,18 @@ const getCardinalMove = (from, to) => {
|
|
|
94
96
|
const moves = []
|
|
95
97
|
|
|
96
98
|
if (Math.abs(dLat) > 0.0001) { // threshold to ignore tiny movement
|
|
97
|
-
const
|
|
98
|
-
moves.push(`${dLat > 0 ? 'north' : 'south'} ${formatDimension(
|
|
99
|
+
const metres = Math.round(getDistance([lng1, lat1], [lng1, lat2]))
|
|
100
|
+
moves.push(`${dLat > 0 ? 'north' : 'south'} ${formatDimension(metres)}`)
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
if (Math.abs(dLng) > 0.0001) {
|
|
102
|
-
const
|
|
103
|
-
moves.push(`${dLng > 0 ? 'east' : 'west'} ${formatDimension(
|
|
104
|
+
const metres = Math.round(getDistance([lng1, lat1], [lng2, lat1]))
|
|
105
|
+
moves.push(`${dLng > 0 ? 'east' : 'west'} ${formatDimension(metres)}`)
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
return moves.join(', ')
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
/**
|
|
110
|
-
* Find the index of the nearest pixel in a given cardinal direction.
|
|
111
|
-
*
|
|
112
|
-
* The function:
|
|
113
|
-
* - Filters candidate points that lie in the specified direction
|
|
114
|
-
* (up, down, left, right) relative to a start pixel.
|
|
115
|
-
* - Selects the nearest valid candidate using Euclidean distance.
|
|
116
|
-
* - Falls back to returning the start pixel index if no candidate exists.
|
|
117
|
-
*
|
|
118
|
-
* Example:
|
|
119
|
-
* spatialNavigate('up', [100, 200], [[100, 100], [150, 250], [90, 180]])
|
|
120
|
-
* → returns the index of [100, 100]
|
|
121
|
-
*
|
|
122
|
-
* @param {'ArrowUp'|'ArrowDown'|'ArrowLeft'|'ArrowRight'} direction - The direction to search.
|
|
123
|
-
* @param {[number, number]} start - The starting pixel coordinate [x, y].
|
|
124
|
-
* @param {Array<[number, number]>} pixels - Array of pixel coordinates.
|
|
125
|
-
* @returns {number} Index of the closest pixel in the given direction.
|
|
126
|
-
*/
|
|
127
|
-
const isInDirection = (direction, dx, dy) => {
|
|
128
|
-
switch (direction) {
|
|
129
|
-
case 'ArrowUp': return dy < 0 && Math.abs(dy) >= Math.abs(dx)
|
|
130
|
-
case 'ArrowDown': return dy > 0 && Math.abs(dy) >= Math.abs(dx)
|
|
131
|
-
case 'ArrowLeft': return dx < 0 && Math.abs(dx) > Math.abs(dy)
|
|
132
|
-
case 'ArrowRight': return dx > 0 && Math.abs(dx) > Math.abs(dy)
|
|
133
|
-
default: return false
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const spatialNavigate = (direction, start, pixels) => {
|
|
138
|
-
const [sx, sy] = start
|
|
139
|
-
|
|
140
|
-
// Direction filters
|
|
141
|
-
const candidates = pixels.filter(([x, y]) =>
|
|
142
|
-
(x !== sx || y !== sy) && isInDirection(direction, x - sx, y - sy)
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
if (!candidates.length) {
|
|
146
|
-
return pixels.findIndex(p => p[0] === sx && p[1] === sy)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Choose the closest by Euclidean distance
|
|
150
|
-
let closestIndex = -1
|
|
151
|
-
let minDist = Infinity
|
|
152
|
-
candidates.forEach(c => {
|
|
153
|
-
const dx = c[0] - sx
|
|
154
|
-
const dy = c[1] - sy
|
|
155
|
-
const dist = dx * dx + dy * dy // squared distance is enough
|
|
156
|
-
if (dist < minDist) {
|
|
157
|
-
minDist = dist
|
|
158
|
-
closestIndex = pixels.indexOf(c)
|
|
159
|
-
}
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
return closestIndex
|
|
163
|
-
}
|
|
164
|
-
|
|
165
111
|
const getResolution = (center, zoom) => {
|
|
166
112
|
const EARTH_CIRCUMFERENCE = 40075016.686
|
|
167
113
|
const TILE_SIZE = 512
|
|
@@ -239,7 +185,6 @@ export {
|
|
|
239
185
|
getCardinalMove,
|
|
240
186
|
getBboxFromGeoJSON,
|
|
241
187
|
isGeometryObscured,
|
|
242
|
-
spatialNavigate,
|
|
243
188
|
getResolution,
|
|
244
189
|
getPaddedBounds,
|
|
245
190
|
formatDimension
|
|
@@ -12,18 +12,21 @@ jest.mock('@turf/bbox', () => jest.fn(() => [-1, 50, 1, 52]))
|
|
|
12
12
|
describe('spatial utils', () => {
|
|
13
13
|
test('formatDimension hits all branches', () => {
|
|
14
14
|
// < 0.5 miles
|
|
15
|
-
expect(spatial.formatDimension(500)).toMatch(/
|
|
15
|
+
expect(spatial.formatDimension(500)).toMatch(/metres?$/)
|
|
16
|
+
|
|
17
|
+
// Singular metre (exactly 1) - hits roundedMetres === 1 branch
|
|
18
|
+
expect(spatial.formatDimension(1)).toBe('1 metre')
|
|
16
19
|
|
|
17
20
|
// Singular mile (exactly 1.0) - hits value === 1 branch
|
|
18
21
|
expect(spatial.formatDimension(1609.344)).toBe('1 mile')
|
|
19
22
|
|
|
20
23
|
// 5 miles
|
|
21
|
-
const
|
|
22
|
-
expect(spatial.formatDimension(
|
|
24
|
+
const metresSmallMiles = 5 * 1609.344
|
|
25
|
+
expect(spatial.formatDimension(metresSmallMiles)).toMatch(/^5\s*miles$/)
|
|
23
26
|
|
|
24
27
|
// >= WHOLE_MILE_THRESHOLD
|
|
25
|
-
const
|
|
26
|
-
expect(spatial.formatDimension(
|
|
28
|
+
const metresLarge = 15 * 1609.344
|
|
29
|
+
expect(spatial.formatDimension(metresLarge)).toBe('15 miles')
|
|
27
30
|
})
|
|
28
31
|
|
|
29
32
|
test('array bounds triggers all branches', () => {
|
|
@@ -50,33 +53,6 @@ describe('spatial utils', () => {
|
|
|
50
53
|
expect(spatial.getCardinalMove([0, 0], [0.00001, 0.00001])).toBe('')
|
|
51
54
|
})
|
|
52
55
|
|
|
53
|
-
test('spatialNavigate all directions and fallback', () => {
|
|
54
|
-
const pixels = [[0, 0], [0, -1], [1, 0], [0, 1], [-1, 0]]
|
|
55
|
-
expect(spatial.spatialNavigate('ArrowUp', [0, 0], pixels)).toBe(1)
|
|
56
|
-
expect(spatial.spatialNavigate('ArrowDown', [0, 0], pixels)).toBe(3)
|
|
57
|
-
expect(spatial.spatialNavigate('ArrowLeft', [0, 0], pixels)).toBe(4)
|
|
58
|
-
expect(spatial.spatialNavigate('ArrowRight', [0, 0], pixels)).toBe(2)
|
|
59
|
-
expect(spatial.spatialNavigate('InvalidDir', [0, 0], pixels)).toBe(0)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test('spatialNavigate finds closer candidates (hits dist < minDist)', () => {
|
|
63
|
-
const start = [0, 0]
|
|
64
|
-
const pixels = [[0, 0], [10, 0], [2, 0]]
|
|
65
|
-
expect(spatial.spatialNavigate('ArrowRight', start, pixels)).toBe(2)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
test('spatialNavigate skips farther candidate (dist >= minDist false branch)', () => {
|
|
69
|
-
// Closer candidate first → second candidate fails dist < minDist
|
|
70
|
-
const pixels = [[0, 0], [2, 0], [10, 0]]
|
|
71
|
-
expect(spatial.spatialNavigate('ArrowRight', [0, 0], pixels)).toBe(1)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
test('spatialNavigate diagonal with dx>dy', () => {
|
|
75
|
-
const start = [0, 0]
|
|
76
|
-
const pixels = [[0, 0], [3, 1], [1, 0]] // dx>dy
|
|
77
|
-
expect(spatial.spatialNavigate('ArrowRight', start, pixels)).toBe(2)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
56
|
test('getResolution returns positive value', () => {
|
|
81
57
|
expect(spatial.getResolution({ lat: 0 }, 1)).toBeGreaterThan(0)
|
|
82
58
|
})
|
package/rollup.esm.mjs
CHANGED
|
@@ -166,7 +166,8 @@ const createESMConfig = (entryPath, outDir, isCore = false, manualChunks = null,
|
|
|
166
166
|
// (rootDir) → "css/index.css" — no ".." in the emitted fileName.
|
|
167
167
|
postcss({
|
|
168
168
|
extract: path.resolve(cssDir, 'index.css'),
|
|
169
|
-
use: ['sass']
|
|
169
|
+
use: ['sass'],
|
|
170
|
+
minimize: true
|
|
170
171
|
}),
|
|
171
172
|
|
|
172
173
|
terser(),
|
|
@@ -5,6 +5,12 @@ set -e
|
|
|
5
5
|
VERSION_PATTERN="^v([0-9]{1,}.[0-9]{1,}.[0-9]{1,})(-[0-9A-Za-z-].*)?$"
|
|
6
6
|
PRE_RELEASE_PATTERN="(-[0-9A-Za-z-].*)$"
|
|
7
7
|
|
|
8
|
+
# Prerelease labels (case-insensitive) that share the "pre-release" npm dist-tag.
|
|
9
|
+
# Any other label (e.g. an experimental feature slug) gets isolated to its own
|
|
10
|
+
# dist-tag instead — see determine_release_tag(). Add to this list if another
|
|
11
|
+
# shared-channel convention (e.g. "rc") is adopted.
|
|
12
|
+
SHARED_PRERELEASE_LABELS=("alpha" "beta")
|
|
13
|
+
|
|
8
14
|
validate_arguments() {
|
|
9
15
|
if [ -z "$TAG_NAME" ]; then
|
|
10
16
|
echo "ERROR: TAG_NAME is required"
|
|
@@ -52,7 +58,37 @@ validate_version_bump() {
|
|
|
52
58
|
echo "✓ Version check passed"
|
|
53
59
|
}
|
|
54
60
|
|
|
61
|
+
is_shared_prerelease_label() {
|
|
62
|
+
local candidate_lower
|
|
63
|
+
candidate_lower=$(echo "$1" | tr '[:upper:]' '[:lower:]')
|
|
64
|
+
|
|
65
|
+
local shared_label
|
|
66
|
+
for shared_label in "${SHARED_PRERELEASE_LABELS[@]}"; do
|
|
67
|
+
if [[ "$candidate_lower" == "$shared_label" ]]; then
|
|
68
|
+
return 0
|
|
69
|
+
fi
|
|
70
|
+
done
|
|
71
|
+
return 1
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
determine_release_tag() {
|
|
75
|
+
# Extract the first prerelease identifier: semver splits identifiers on '.',
|
|
76
|
+
# so this stops at the first dot if there is one ("-fmp.2" -> "fmp"), or runs
|
|
77
|
+
# to the end of the string if there isn't ("-fmp-2" -> "fmp-2", since hyphens
|
|
78
|
+
# are legal inside a single identifier). Anything not in
|
|
79
|
+
# SHARED_PRERELEASE_LABELS (matched case-insensitively) gets isolated to its
|
|
80
|
+
# own npm dist-tag either way — this is what prevents a mistyped experimental
|
|
81
|
+
# tag from silently landing in the shared "pre-release" channel, regardless
|
|
82
|
+
# of whether a dot or hyphen was used before the build number, or how the
|
|
83
|
+
# label happens to be capitalised.
|
|
84
|
+
if [[ "$TAG_NAME" =~ -([^.]+) ]]; then
|
|
85
|
+
local label="${BASH_REMATCH[1]}"
|
|
86
|
+
if ! is_shared_prerelease_label "$label"; then
|
|
87
|
+
echo "$label"
|
|
88
|
+
return
|
|
89
|
+
fi
|
|
90
|
+
fi
|
|
91
|
+
|
|
56
92
|
if [[ "$IS_PRE_RELEASE" == "true" ]] || [[ "$TAG_NAME" =~ $PRE_RELEASE_PATTERN ]]; then
|
|
57
93
|
echo "pre-release"
|
|
58
94
|
else
|
package/src/App/App.jsx
CHANGED
|
@@ -19,7 +19,7 @@ export const App = (props) => {
|
|
|
19
19
|
return (
|
|
20
20
|
<AppProvider options={props}>
|
|
21
21
|
<MapProvider options={props}>
|
|
22
|
-
<ServiceProvider eventBus={props.eventBus}>
|
|
22
|
+
<ServiceProvider eventBus={props.eventBus} pluginRegistry={props.pluginRegistry}>
|
|
23
23
|
<PluginProvider>
|
|
24
24
|
<PluginInits />
|
|
25
25
|
<Layout />
|
package/src/App/App.test.jsx
CHANGED
|
@@ -70,7 +70,8 @@ describe('App', () => {
|
|
|
70
70
|
})
|
|
71
71
|
|
|
72
72
|
test('renders component hierarchy with props', () => {
|
|
73
|
-
const
|
|
73
|
+
const mockPluginRegistry = { getPlugin: jest.fn() }
|
|
74
|
+
const props = { mapProvider: 'test', extraProp: 'value', eventBus: mockEventBus, pluginRegistry: mockPluginRegistry }
|
|
74
75
|
|
|
75
76
|
render(<App {...props} />)
|
|
76
77
|
|
|
@@ -98,6 +99,10 @@ describe('App', () => {
|
|
|
98
99
|
// Verify eventBus exists
|
|
99
100
|
expect(appProviderProps.options.eventBus).toBe(mockEventBus)
|
|
100
101
|
expect(mapProviderProps.options.eventBus).toBe(mockEventBus)
|
|
102
|
+
|
|
103
|
+
// Verify pluginRegistry reaches ServiceProvider
|
|
104
|
+
const serviceProviderProps = ServiceProvider.mock.calls[0][0]
|
|
105
|
+
expect(serviceProviderProps.pluginRegistry).toBe(mockPluginRegistry)
|
|
101
106
|
})
|
|
102
107
|
|
|
103
108
|
test('calls removeLoadingState and emits APP_READY on mount', () => {
|
|
@@ -16,24 +16,38 @@ export const CrossHair = () => {
|
|
|
16
16
|
const { isVisible, isPinnedToMap, state } = crossHair
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
|
-
<
|
|
20
|
-
|
|
19
|
+
<button
|
|
20
|
+
tabIndex='-1'
|
|
21
21
|
ref={crossHairRef}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
id={`${id}-cross-hair`}
|
|
23
|
+
className='im-c-cross-hair-button'
|
|
24
|
+
// "Target" — short for Voice Control to say ("Click Target"). A plain attribute, not a
|
|
25
|
+
// hidden child <span>, so there's no separate text node for Chrome's AX tree to expose.
|
|
26
|
+
aria-label='Target'
|
|
27
|
+
// This button should never take real DOM focus — a screen reader's virtual cursor and
|
|
28
|
+
// Voice Control can both reach it without that, and real focus landing here would break
|
|
29
|
+
// keyboard drawing (which requires focus to stay on the viewport container).
|
|
30
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
31
|
+
onClick={() => crossHair.activate?.()}
|
|
28
32
|
style={{
|
|
29
33
|
position: 'absolute',
|
|
30
34
|
left: isPinnedToMap ? 0 : '50%',
|
|
31
35
|
top: isPinnedToMap ? 0 : '50%',
|
|
32
|
-
pointerEvents: 'none',
|
|
33
36
|
display: isVisible ? 'block' : 'none'
|
|
34
37
|
}}
|
|
35
38
|
>
|
|
36
|
-
<path
|
|
37
|
-
|
|
39
|
+
{/* aria-hidden: without it, WebKit exposes the <path> as its own accessibility node, and
|
|
40
|
+
Voice Control activates that instead of bubbling a click up to this button. */}
|
|
41
|
+
<svg
|
|
42
|
+
width='38'
|
|
43
|
+
height='38'
|
|
44
|
+
viewBox='0 0 38 38'
|
|
45
|
+
fillRule='evenodd'
|
|
46
|
+
fill='currentColor'
|
|
47
|
+
aria-hidden='true'
|
|
48
|
+
>
|
|
49
|
+
<path d={paths[state || 'active']} />
|
|
50
|
+
</svg>
|
|
51
|
+
</button>
|
|
38
52
|
)
|
|
39
53
|
}
|
|
@@ -1,14 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// A real <button> (tabIndex="-1") so it's a discoverable target for AT (Voice Control, a screen
|
|
2
|
+
// reader's virtual cursor) without being a Tab stop or ever taking real DOM focus.
|
|
3
|
+
.im-c-cross-hair-button {
|
|
4
|
+
// Its ancestor (.im-c-viewport__safezone) is pointer-events:none, so this has to opt back in
|
|
5
|
+
// itself or clicks here pass straight through to the map canvas underneath.
|
|
6
|
+
pointer-events: auto;
|
|
7
|
+
|
|
8
|
+
// Reset native chrome to zero — position/margin below assumes the content-box starts exactly
|
|
9
|
+
// at the border-box edge.
|
|
10
|
+
appearance: none;
|
|
11
|
+
border: 0;
|
|
12
|
+
background: none;
|
|
13
|
+
padding: 0;
|
|
14
|
+
font: inherit;
|
|
15
|
+
cursor: inherit;
|
|
16
|
+
|
|
17
|
+
// Explicit size plus the negative margin centre the button's own geometry (not just its SVG
|
|
18
|
+
// child's) on wherever CrossHair.jsx's inline top/left positions it — without it, an AT
|
|
19
|
+
// overlay's badge would land at the anchor point's corner, not on the icon.
|
|
20
|
+
width: 38px;
|
|
21
|
+
height: 38px;
|
|
6
22
|
margin: -19px 0 0 -19px;
|
|
7
|
-
}
|
|
8
23
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
outline: 3px solid transparent;
|
|
25
|
+
|
|
26
|
+
svg {
|
|
27
|
+
display: block;
|
|
28
|
+
// Purely decorative (aria-hidden in CrossHair.jsx) — without this it's still its own
|
|
29
|
+
// hit-testable target, so a click could resolve to the icon instead of the button.
|
|
30
|
+
pointer-events: none;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
path {
|
|
34
|
+
stroke-width: 3;
|
|
35
|
+
stroke: var(--map-overlay-halo-color);
|
|
36
|
+
fill: var(--map-overlay-foreground-color);
|
|
37
|
+
paint-order: stroke fill;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { render } from '@testing-library/react'
|
|
2
|
+
import { render, fireEvent } from '@testing-library/react'
|
|
3
3
|
import { CrossHair } from './CrossHair.jsx'
|
|
4
4
|
import { useCrossHair } from '../../hooks/useCrossHairAPI.js'
|
|
5
5
|
|
|
@@ -9,6 +9,8 @@ jest.mock('../../store/configContext', () => ({ useConfig: jest.fn(() => ({ id:
|
|
|
9
9
|
describe('CrossHair', () => {
|
|
10
10
|
const crossHairRef = React.createRef()
|
|
11
11
|
|
|
12
|
+
// Positioning/visibility/sizing all live on the outer <button> (tabIndex="-1", discoverable
|
|
13
|
+
// by AT like Voice Control without being a Tab stop); the SVG itself is a plain, unstyled child.
|
|
12
14
|
const renderWith = (overrides = {}) => {
|
|
13
15
|
useCrossHair.mockReturnValue({
|
|
14
16
|
crossHairRef,
|
|
@@ -19,43 +21,70 @@ describe('CrossHair', () => {
|
|
|
19
21
|
...overrides
|
|
20
22
|
}
|
|
21
23
|
})
|
|
22
|
-
|
|
24
|
+
const { container } = render(<CrossHair />)
|
|
25
|
+
return { button: container.querySelector('button'), svg: container.querySelector('svg') }
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
it('renders visible active marker when pinned', () => {
|
|
26
|
-
const svg = renderWith()
|
|
29
|
+
const { button, svg } = renderWith()
|
|
27
30
|
const path = svg.querySelector('path')
|
|
28
31
|
|
|
29
|
-
expect(
|
|
30
|
-
expect(
|
|
32
|
+
expect(button).toHaveClass('im-c-cross-hair-button')
|
|
33
|
+
expect(button).toHaveStyle({
|
|
31
34
|
position: 'absolute',
|
|
32
35
|
left: 0,
|
|
33
36
|
top: 0,
|
|
34
|
-
pointerEvents: 'none',
|
|
35
37
|
display: 'block'
|
|
36
38
|
})
|
|
37
39
|
expect(path.getAttribute('d')).toContain('M5.035 20H1v-2h4.035C5.525') // active path
|
|
38
40
|
})
|
|
39
41
|
|
|
40
42
|
it('positions marker at center when not pinned', () => {
|
|
41
|
-
const
|
|
43
|
+
const { button } = renderWith({ isPinnedToMap: false })
|
|
42
44
|
|
|
43
|
-
expect(
|
|
45
|
+
expect(button).toHaveStyle({
|
|
44
46
|
left: '50%',
|
|
45
47
|
top: '50%'
|
|
46
48
|
})
|
|
47
49
|
})
|
|
48
50
|
|
|
51
|
+
// Fully removed from the layout/accessibility tree when hidden, not just faded — so Voice
|
|
52
|
+
// Control's Show Names never puts a "Target" label over nothing. Its real route back is
|
|
53
|
+
// opening MapControls (a normal, always-visible button), not re-discovering this element.
|
|
49
54
|
it('hides marker when not visible', () => {
|
|
50
|
-
const
|
|
51
|
-
expect(
|
|
55
|
+
const { button } = renderWith({ isVisible: false })
|
|
56
|
+
expect(button).toHaveStyle({ display: 'none' })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('invokes crossHair.activate() on click, for Voice Control / mouse / touch activation', () => {
|
|
60
|
+
const activate = jest.fn()
|
|
61
|
+
const { button } = renderWith({ activate })
|
|
62
|
+
fireEvent.click(button)
|
|
63
|
+
expect(activate).toHaveBeenCalledTimes(1)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('does not throw when clicked with no activate() assigned', () => {
|
|
67
|
+
const { button } = renderWith()
|
|
68
|
+
expect(() => fireEvent.click(button)).not.toThrow()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('prevents the default mousedown so a real click never gives it focus (or the resulting outline)', () => {
|
|
72
|
+
const { button } = renderWith()
|
|
73
|
+
const event = fireEvent.mouseDown(button)
|
|
74
|
+
expect(event).toBe(false) // fireEvent returns false when preventDefault() was called
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('is not a Tab stop, but is a real button for coordinate-based AT (e.g. Voice Control) to find', () => {
|
|
78
|
+
const { button } = renderWith()
|
|
79
|
+
expect(button.tabIndex).toBe(-1)
|
|
80
|
+
expect(button.tagName).toBe('BUTTON')
|
|
52
81
|
})
|
|
53
82
|
|
|
54
83
|
it.each([
|
|
55
84
|
['inactive', 'M5.035 20H1v-2h4.035a13.98'],
|
|
56
85
|
[undefined, 'M5.035 20H1v-2h4.035C5.525'] // fallback to active
|
|
57
86
|
])('renders correct path for state=%s', (state, expected) => {
|
|
58
|
-
const svg = renderWith({ state })
|
|
87
|
+
const { svg } = renderWith({ state })
|
|
59
88
|
expect(svg.querySelector('path').getAttribute('d')).toContain(expected)
|
|
60
89
|
})
|
|
61
90
|
})
|
|
@@ -17,7 +17,7 @@ import { htmlToPlainText } from '../../../utils/htmlToPlainText.js'
|
|
|
17
17
|
// eslint-disable-next-line camelcase, react/jsx-pascal-case
|
|
18
18
|
// sonarjs/disable-next-line function-name
|
|
19
19
|
export const Hints = () => {
|
|
20
|
-
const { id, keyboardHintText,
|
|
20
|
+
const { id, keyboardHintText, enableMapControls, mapControlsHintText } = useConfig()
|
|
21
21
|
const { hints } = useService()
|
|
22
22
|
const { layoutRefs } = useApp()
|
|
23
23
|
const [activeHint, setActiveHint] = useState(null)
|
|
@@ -33,8 +33,8 @@ export const Hints = () => {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Composed at render time so overriding one text doesn't drop the other.
|
|
36
|
-
const keyboardDescriptionHtml =
|
|
37
|
-
? `${keyboardHintText} ${
|
|
36
|
+
const keyboardDescriptionHtml = enableMapControls
|
|
37
|
+
? `${keyboardHintText} ${mapControlsHintText}`
|
|
38
38
|
: keyboardHintText
|
|
39
39
|
|
|
40
40
|
return createPortal(
|
|
@@ -52,13 +52,13 @@ describe('Hints — rendering', () => {
|
|
|
52
52
|
expect(desc.innerHTML).toBe('Shift + ?')
|
|
53
53
|
})
|
|
54
54
|
|
|
55
|
-
it('appends the
|
|
55
|
+
it('appends the map-controls hint text when enableMapControls is true', () => {
|
|
56
56
|
const mainEl = setup()
|
|
57
57
|
useConfig.mockReturnValue({
|
|
58
58
|
id: 'test-map',
|
|
59
59
|
keyboardHintText: '<kbd>Shift</kbd> + <kbd>?</kbd>',
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
enableMapControls: true,
|
|
61
|
+
mapControlsHintText: '<kbd>Arrow keys</kbd> to move'
|
|
62
62
|
})
|
|
63
63
|
render(<Hints />)
|
|
64
64
|
const desc = mainEl.querySelector('#test-map-keyboard-desc')
|
|
@@ -10,7 +10,10 @@ const DEFAULT_GROUP = 'Navigate'
|
|
|
10
10
|
const ShortcutList = ({ items }) => (
|
|
11
11
|
<dl className='im-c-keyboard-help__list'>
|
|
12
12
|
{items.map((item) => (
|
|
13
|
-
<div
|
|
13
|
+
<div
|
|
14
|
+
key={item.id}
|
|
15
|
+
className={`im-c-keyboard-help__item${item.visuallyHidden ? ' im-u-visually-hidden' : ''}`}
|
|
16
|
+
>
|
|
14
17
|
<dt className='im-c-keyboard-help__title'>{item.title}</dt>
|
|
15
18
|
<dd className='im-c-keyboard-help__description' dangerouslySetInnerHTML={{ __html: item.command }} />
|
|
16
19
|
</div>
|
|
@@ -68,6 +68,23 @@ describe('KeyboardHelp — flat list', () => {
|
|
|
68
68
|
render(<KeyboardHelp />)
|
|
69
69
|
expect(screen.getByText('↑')).toBeInTheDocument()
|
|
70
70
|
})
|
|
71
|
+
|
|
72
|
+
it('visually hides a shortcut flagged visuallyHidden but keeps it in the DOM for assistive tech', () => {
|
|
73
|
+
getKeyboardShortcuts.mockReturnValue([
|
|
74
|
+
...VIEWPORT_SHORTCUTS,
|
|
75
|
+
{ id: 'sr', context: 'viewport', title: 'Get info', command: '<kbd>Alt</kbd> + <kbd>I</kbd>', visuallyHidden: true }
|
|
76
|
+
])
|
|
77
|
+
render(<KeyboardHelp />)
|
|
78
|
+
const item = screen.getByText('Get info').closest('.im-c-keyboard-help__item')
|
|
79
|
+
expect(item).toHaveClass('im-u-visually-hidden')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('does not visually hide a regular shortcut', () => {
|
|
83
|
+
getKeyboardShortcuts.mockReturnValue(VIEWPORT_SHORTCUTS)
|
|
84
|
+
render(<KeyboardHelp />)
|
|
85
|
+
const item = screen.getByText('Move').closest('.im-c-keyboard-help__item')
|
|
86
|
+
expect(item).not.toHaveClass('im-u-visually-hidden')
|
|
87
|
+
})
|
|
71
88
|
})
|
|
72
89
|
|
|
73
90
|
// ─── tab UI (multiple groups) ─────────────────────────────────────────────────
|