@defra/interactive-map 0.0.43-fmp-experimental → 0.0.44-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 +5 -1
- package/assets/templates/add-symbols.njk +5 -1
- 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/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/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/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/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/utils/labels.js +2 -2
- package/providers/maplibre/src/utils/labels.test.js +5 -5
- package/providers/maplibre/src/utils/spatial.js +0 -57
- package/providers/maplibre/src/utils/spatial.test.js +0 -27
- 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 +5 -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 +16 -16
- 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/test-utils.js +8 -0
- package/src/types.js +51 -9
- 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
|
@@ -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) ─────────────────────────────────────────────────
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// components/MapButton.jsx
|
|
2
2
|
import React, { useState, useRef, useCallback } from 'react'
|
|
3
3
|
import { stringToKebab } from '../../../utils/stringToKebab'
|
|
4
|
+
import { getPanelElementId } from '../../../utils/getPanelElementId.js'
|
|
4
5
|
import { Tooltip } from '../Tooltip/Tooltip'
|
|
5
6
|
import { Icon } from '../Icon/Icon'
|
|
6
7
|
import { SlotRenderer } from '../../renderer/SlotRenderer'
|
|
@@ -88,6 +89,10 @@ const makePopupKeyUpHandler = (hasMenu, buttonRefs, buttonId, setMenuStartPos, s
|
|
|
88
89
|
const getButtonSlot = (panelId, buttonId) =>
|
|
89
90
|
panelId ? `${stringToKebab(buttonId)}-button` : undefined
|
|
90
91
|
|
|
92
|
+
// aria-haspopup only accepts these role-shaped tokens (plus 'true', unused here) — any other
|
|
93
|
+
// controlled-element role (e.g. a panel's 'complementary'/'region') means no aria-haspopup.
|
|
94
|
+
const HASPOPUP_ROLES = new Set(['menu', 'listbox', 'tree', 'grid', 'dialog'])
|
|
95
|
+
|
|
91
96
|
/**
|
|
92
97
|
* Determines the controlled element (panel or popup menu) for ARIA attributes.
|
|
93
98
|
* @param {Object} options - Configuration options
|
|
@@ -95,14 +100,16 @@ const getButtonSlot = (panelId, buttonId) =>
|
|
|
95
100
|
* @param {string} options.panelId - ID of the controlled panel (if applicable)
|
|
96
101
|
* @param {string} options.buttonId - Unique button identifier
|
|
97
102
|
* @param {boolean} options.hasMenu - Whether the button has a popup menu
|
|
98
|
-
* @
|
|
103
|
+
* @param {string} [options.panelRole] - The controlled panel's own ARIA role (see getPanelRole.js),
|
|
104
|
+
* used for aria-haspopup on the button — undefined when no panel is controlled.
|
|
105
|
+
* @returns {Object|null} Object with id, type ('panel' or 'popup') and role, or null if no controlled element
|
|
99
106
|
*/
|
|
100
|
-
const getControlledElement = ({ idPrefix, panelId, buttonId, hasMenu }) => {
|
|
107
|
+
const getControlledElement = ({ idPrefix, panelId, buttonId, hasMenu, panelRole }) => {
|
|
101
108
|
if (panelId) {
|
|
102
|
-
return { id:
|
|
109
|
+
return { id: getPanelElementId(idPrefix, panelId), type: 'panel', role: panelRole }
|
|
103
110
|
}
|
|
104
111
|
if (hasMenu) {
|
|
105
|
-
return { id: `${idPrefix}-popup-${stringToKebab(buttonId)}`, type: 'popup' }
|
|
112
|
+
return { id: `${idPrefix}-popup-${stringToKebab(buttonId)}`, type: 'popup', role: 'menu' }
|
|
106
113
|
}
|
|
107
114
|
return null
|
|
108
115
|
}
|
|
@@ -168,7 +175,7 @@ const buildButtonProps = ({
|
|
|
168
175
|
'aria-expanded': ariaExpanded,
|
|
169
176
|
'aria-pressed': typeof isPressed === 'boolean' ? isPressed : undefined,
|
|
170
177
|
'aria-controls': controlledElement?.id ?? ariaControls,
|
|
171
|
-
'aria-haspopup': controlledElement?.
|
|
178
|
+
'aria-haspopup': HASPOPUP_ROLES.has(controlledElement?.role) ? controlledElement.role : undefined,
|
|
172
179
|
...(href
|
|
173
180
|
? { href, target: '_blank', onKeyUp: handleKeyUp, role: 'button' }
|
|
174
181
|
: { type: 'button' })
|
|
@@ -191,6 +198,7 @@ const buildButtonProps = ({
|
|
|
191
198
|
* @param {boolean} [props.isExpanded] - Whether content controlled by the button is expanded
|
|
192
199
|
* @param {boolean} [props.isHidden=false] - Whether to hide the button (CSS display: none)
|
|
193
200
|
* @param {boolean} [props.isPanelOpen=false] - Whether the controlled panel is open
|
|
201
|
+
* @param {string} [props.panelRole] - The controlled panel's own ARIA role, used for aria-haspopup
|
|
194
202
|
* @param {string} [props.variant] - CSS variant class for styling (e.g., 'primary')
|
|
195
203
|
* @param {Function} [props.onClick] - Custom click handler
|
|
196
204
|
* @param {string} [props.panelId] - ID of the panel controlled by this button
|
|
@@ -212,6 +220,7 @@ export const MapButton = ({
|
|
|
212
220
|
isExpanded,
|
|
213
221
|
isHidden,
|
|
214
222
|
isPanelOpen,
|
|
223
|
+
panelRole,
|
|
215
224
|
variant,
|
|
216
225
|
onClick,
|
|
217
226
|
panelId,
|
|
@@ -239,7 +248,7 @@ export const MapButton = ({
|
|
|
239
248
|
const hasMenu = menuItems?.length >= 1
|
|
240
249
|
const showIcon = iconId || iconSvgContent || hasMenu
|
|
241
250
|
const buttonSlot = getButtonSlot(panelId, buttonId)
|
|
242
|
-
const controlledElement = getControlledElement({ idPrefix, panelId, buttonId, hasMenu })
|
|
251
|
+
const controlledElement = getControlledElement({ idPrefix, panelId, buttonId, hasMenu, panelRole })
|
|
243
252
|
|
|
244
253
|
/**
|
|
245
254
|
* Handles button click events.
|
|
@@ -301,7 +310,10 @@ export const MapButton = ({
|
|
|
301
310
|
>
|
|
302
311
|
{showLabel ? buttonEl : <Tooltip content={label}>{buttonEl}</Tooltip>}
|
|
303
312
|
{buttonSlot && <SlotRenderer slot={buttonSlot} />}
|
|
304
|
-
{
|
|
313
|
+
{/* Mounted permanently, not just while open, so its aria-controls id stays a stable node. */}
|
|
314
|
+
{controlledElement?.type === 'popup' && (
|
|
315
|
+
<PopupMenu popupMenuId={controlledElement.id} buttonId={buttonId} startPos={menuStartPos} menuRef={menuRef} items={menuItems} setIsOpen={setIsPopupOpen} buttonRect={menuRect} isOpen={isPopupOpen} />
|
|
316
|
+
)}
|
|
305
317
|
</div>
|
|
306
318
|
)
|
|
307
319
|
}
|
|
@@ -30,8 +30,7 @@
|
|
|
30
30
|
justify-content: center;
|
|
31
31
|
align-items: center;
|
|
32
32
|
|
|
33
|
-
padding
|
|
34
|
-
padding-right: var(--button-padding);
|
|
33
|
+
padding: 0 var(--button-padding) 0 var(--button-padding);
|
|
35
34
|
|
|
36
35
|
pointer-events: auto;
|
|
37
36
|
color: var(--foreground-color);
|
|
@@ -23,7 +23,8 @@ jest.mock('../../renderer/SlotRenderer', () => ({
|
|
|
23
23
|
}))
|
|
24
24
|
|
|
25
25
|
jest.mock('../PopupMenu/PopupMenu', () => ({
|
|
26
|
-
|
|
26
|
+
// Mirrors the real component: always mounted, hidden/children gated by isOpen.
|
|
27
|
+
PopupMenu: ({ popupMenuId, isOpen, startPos, items, buttonRect }) => {
|
|
27
28
|
let selectedIndex = -1
|
|
28
29
|
if (startPos === 'first' && items?.length > 0) {
|
|
29
30
|
selectedIndex = 0
|
|
@@ -31,7 +32,11 @@ jest.mock('../PopupMenu/PopupMenu', () => ({
|
|
|
31
32
|
if (startPos === 'last' && items?.length > 0) {
|
|
32
33
|
selectedIndex = items.length - 1
|
|
33
34
|
}
|
|
34
|
-
return
|
|
35
|
+
return (
|
|
36
|
+
<div id={popupMenuId} hidden={!isOpen} data-testid='popup-menu' data-start-pos={String(startPos)} data-selected-index={String(selectedIndex)} data-has-rect={String(!!buttonRect)}>
|
|
37
|
+
{isOpen && items?.map((item, i) => <div key={i} data-testid={`menu-item-${i}`}>{item.label}</div>)}
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
35
40
|
}
|
|
36
41
|
}))
|
|
37
42
|
|
|
@@ -78,9 +83,20 @@ describe('MapButton', () => {
|
|
|
78
83
|
expect(button).toHaveAttribute('aria-expanded', 'false')
|
|
79
84
|
expect(button).not.toHaveAttribute('aria-pressed')
|
|
80
85
|
expect(button).toHaveAttribute('aria-controls', 'prefix-panel-settings')
|
|
86
|
+
expect(button).not.toHaveAttribute('aria-haspopup')
|
|
81
87
|
expect(screen.getByTestId('slot')).toHaveAttribute('data-slot', 'test-button')
|
|
82
88
|
})
|
|
83
89
|
|
|
90
|
+
it('sets aria-haspopup="dialog" when the controlled panel is a dialog', () => {
|
|
91
|
+
renderButton({ panelId: 'Settings', idPrefix: 'prefix', panelRole: 'dialog' })
|
|
92
|
+
expect(getButton()).toHaveAttribute('aria-haspopup', 'dialog')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it.each(['complementary', 'region'])('sets no aria-haspopup when the controlled panel role is %s', (panelRole) => {
|
|
96
|
+
renderButton({ panelId: 'Settings', idPrefix: 'prefix', panelRole })
|
|
97
|
+
expect(getButton()).not.toHaveAttribute('aria-haspopup')
|
|
98
|
+
})
|
|
99
|
+
|
|
84
100
|
it.each([
|
|
85
101
|
[{ isPressed: true }, 'true'],
|
|
86
102
|
[{ isPressed: 'true' }, undefined]
|
|
@@ -118,7 +134,7 @@ describe('MapButton', () => {
|
|
|
118
134
|
renderButton({ isDisabled: true, menuItems: [{ label: 'Item' }], onClick })
|
|
119
135
|
fireEvent.click(getButton())
|
|
120
136
|
expect(onClick).not.toHaveBeenCalled()
|
|
121
|
-
expect(screen.
|
|
137
|
+
expect(screen.getByTestId('popup-menu')).not.toBeVisible()
|
|
122
138
|
})
|
|
123
139
|
|
|
124
140
|
it('stores button refs correctly', () => {
|
|
@@ -138,6 +154,28 @@ describe('MapButton', () => {
|
|
|
138
154
|
expect(button).toHaveAttribute('aria-haspopup', 'menu')
|
|
139
155
|
})
|
|
140
156
|
|
|
157
|
+
it('reserves the aria-controls id as a stable, hidden node before the popup has ever been opened', () => {
|
|
158
|
+
renderButton({ idPrefix: 'prefix', menuItems: [{ label: 'Item' }] })
|
|
159
|
+
const menu = document.getElementById('prefix-popup-test')
|
|
160
|
+
expect(menu).toBeInTheDocument()
|
|
161
|
+
expect(menu).not.toBeVisible()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('keeps the same DOM node (no swap/remount) when opening and closing the popup', () => {
|
|
165
|
+
renderButton({ idPrefix: 'prefix', menuItems: [{ label: 'Item' }] })
|
|
166
|
+
const menuBeforeOpen = document.getElementById('prefix-popup-test')
|
|
167
|
+
|
|
168
|
+
fireEvent.click(getButton())
|
|
169
|
+
const menuWhileOpen = document.getElementById('prefix-popup-test')
|
|
170
|
+
expect(menuWhileOpen).toBe(menuBeforeOpen)
|
|
171
|
+
expect(menuWhileOpen).toBeVisible()
|
|
172
|
+
|
|
173
|
+
fireEvent.click(getButton())
|
|
174
|
+
const menuAfterClose = document.getElementById('prefix-popup-test')
|
|
175
|
+
expect(menuAfterClose).toBe(menuBeforeOpen)
|
|
176
|
+
expect(menuAfterClose).not.toBeVisible()
|
|
177
|
+
})
|
|
178
|
+
|
|
141
179
|
it('toggles popup aria-expanded and startPos', () => {
|
|
142
180
|
renderButton({ menuItems: [{ label: 'Item' }] })
|
|
143
181
|
const button = getButton()
|
|
@@ -224,7 +262,7 @@ describe('MapButton', () => {
|
|
|
224
262
|
it('does nothing for unrelated keys', () => {
|
|
225
263
|
renderButton({ menuItems: [{ label: 'Item' }] })
|
|
226
264
|
fireEvent.keyUp(getButton(), { key: 'Enter' })
|
|
227
|
-
expect(screen.
|
|
265
|
+
expect(screen.getByTestId('popup-menu')).not.toBeVisible()
|
|
228
266
|
})
|
|
229
267
|
|
|
230
268
|
it.each([
|
|
@@ -18,13 +18,13 @@ const ZOOM_ACTIONS = [
|
|
|
18
18
|
{ id: 'nudgeZoomOut', label: 'Zoom out', announceLabel: 'Zoomed out', method: 'zoomOut' }
|
|
19
19
|
]
|
|
20
20
|
|
|
21
|
-
export const
|
|
21
|
+
export const MapControls = () => {
|
|
22
22
|
const { id: appId, mapProvider, panDelta, nudgePanDelta, zoomDelta, nudgeZoomDelta } = useConfig()
|
|
23
23
|
const { dispatch, expandedButtons, nudgeStepSize, interfaceType, layoutRefs } = useApp()
|
|
24
24
|
const { isAtMaxZoom, isAtMinZoom } = useMap()
|
|
25
25
|
const { announce } = useService()
|
|
26
26
|
|
|
27
|
-
const isOpen = expandedButtons.has('
|
|
27
|
+
const isOpen = expandedButtons.has('mapControls')
|
|
28
28
|
const firstDirectionButtonId = `${appId}-pan-up`
|
|
29
29
|
|
|
30
30
|
useEffect(() => {
|
|
@@ -41,7 +41,7 @@ export const MoveControls = () => {
|
|
|
41
41
|
// Registry buttons (declared via a plugin's manifest) auto-return focus to the map
|
|
42
42
|
// viewport after a click (see createButtonClickHandler in mapButtons.js) unless
|
|
43
43
|
// marked keepFocus — which is how e.g. draw's own toolbar buttons never leave
|
|
44
|
-
// keyboard shortcuts (like arrow-key vertex nudging) stranded afterward.
|
|
44
|
+
// keyboard shortcuts (like arrow-key vertex nudging) stranded afterward. MapControls
|
|
45
45
|
// renders its own buttons directly, bypassing that mechanism entirely, so it has to
|
|
46
46
|
// replicate the same return-to-viewport behaviour itself. But unlike a one-off
|
|
47
47
|
// action button, the D-pad is meant to be pressed repeatedly — a keyboard/switch
|
|
@@ -70,7 +70,7 @@ export const MoveControls = () => {
|
|
|
70
70
|
|
|
71
71
|
// Any plugin can claim the D-pad for something other than panning the map (e.g.
|
|
72
72
|
// moving a selected draw vertex) by setting mapProvider.activeMoveTarget — a
|
|
73
|
-
// generic { move(dx, dy, isLargeStep), label? } contract.
|
|
73
|
+
// generic { move(dx, dy, isLargeStep), label? } contract. MapControls doesn't
|
|
74
74
|
// know or care what's claiming it; it just checks for a claimant before
|
|
75
75
|
// defaulting to its own panBy behaviour. Read fresh on every click rather than
|
|
76
76
|
// subscribed to reactively, since it's a plain mapProvider property, not state.
|
|
@@ -107,7 +107,7 @@ export const MoveControls = () => {
|
|
|
107
107
|
// first tabbing all the way back to the map. Handling arrow keys here — while
|
|
108
108
|
// focus is anywhere within this control, not just on a direction button — covers
|
|
109
109
|
// that directly, without needing draw's (or the app's own) keyboard-shortcut
|
|
110
|
-
// guards to special-case
|
|
110
|
+
// guards to special-case MapControls' buttons. Only arrow keys are handled;
|
|
111
111
|
// other shortcuts (delete/escape/undo) still require focus on the viewport,
|
|
112
112
|
// which mouse/touch users already get back automatically after any click here.
|
|
113
113
|
const handleContainerKeyDown = (e) => {
|
|
@@ -120,12 +120,12 @@ export const MoveControls = () => {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
const containerClassName = [
|
|
123
|
-
'im-c-
|
|
124
|
-
!isOpen && 'im-c-
|
|
123
|
+
'im-c-map-controls',
|
|
124
|
+
!isOpen && 'im-c-map-controls--collapsed'
|
|
125
125
|
].filter(Boolean).join(' ')
|
|
126
126
|
|
|
127
127
|
const directionsGroup = (
|
|
128
|
-
<div key='directions' role='group' aria-label='Direction controls' className='im-c-
|
|
128
|
+
<div key='directions' role='group' aria-label='Direction controls' className='im-c-map-controls__directions'>{/* NOSONAR - div with role="group" is correct for a button group */}
|
|
129
129
|
{DIRECTIONS.map(({ id, verb, dx, dy }) => (
|
|
130
130
|
<MapButton
|
|
131
131
|
key={id}
|
|
@@ -154,7 +154,7 @@ export const MoveControls = () => {
|
|
|
154
154
|
)
|
|
155
155
|
|
|
156
156
|
const zoomGroup = (
|
|
157
|
-
<div key='zoom' role='group' aria-label='Zoom controls' className='im-c-
|
|
157
|
+
<div key='zoom' role='group' aria-label='Zoom controls' className='im-c-map-controls__zoom'>{/* NOSONAR - div with role="group" is correct for a button group */}
|
|
158
158
|
{ZOOM_ACTIONS.map(({ id, label, announceLabel, method }) => (
|
|
159
159
|
<MapButton
|
|
160
160
|
key={id}
|
|
@@ -169,7 +169,7 @@ export const MoveControls = () => {
|
|
|
169
169
|
)
|
|
170
170
|
|
|
171
171
|
return (
|
|
172
|
-
<div id={`${appId}-
|
|
172
|
+
<div id={`${appId}-map-controls-content`} className={containerClassName} onKeyDown={handleContainerKeyDown} data-map-keyboard-scope>{/* NOSONAR - only catches arrow-key bubbling from the real, already-focusable button descendants below; the div itself needs no role/tabIndex */}
|
|
173
173
|
{directionsGroup}
|
|
174
174
|
{zoomGroup}
|
|
175
175
|
</div>
|