@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
|
@@ -10,7 +10,7 @@ import { useMediaQueryDispatch } from '../hooks/useMediaQueryDispatch.js'
|
|
|
10
10
|
export const AppContext = createContext(null)
|
|
11
11
|
|
|
12
12
|
export const AppProvider = ({ options, children }) => {
|
|
13
|
-
const { pluginRegistry, buttonRegistry, panelRegistry, controlRegistry, keyboardShortcutRegistry, eventBus, breakpointDetector } = options
|
|
13
|
+
const { pluginRegistry, buttonRegistry, panelRegistry, controlRegistry, keyboardShortcutRegistry, spatialListRegistry, eventBus, breakpointDetector } = options
|
|
14
14
|
|
|
15
15
|
const layoutRefs = {
|
|
16
16
|
appContainerRef: useRef(null),
|
|
@@ -96,7 +96,8 @@ export const AppProvider = ({ options, children }) => {
|
|
|
96
96
|
buttonRegistry,
|
|
97
97
|
panelRegistry,
|
|
98
98
|
controlRegistry,
|
|
99
|
-
keyboardShortcutRegistry
|
|
99
|
+
keyboardShortcutRegistry,
|
|
100
|
+
spatialListRegistry
|
|
100
101
|
}), [state, dispatch])
|
|
101
102
|
|
|
102
103
|
return (
|
|
@@ -10,7 +10,7 @@ import { patternRegistry } from '../../services/patternRegistry.js'
|
|
|
10
10
|
|
|
11
11
|
export const ServiceContext = createContext(null)
|
|
12
12
|
|
|
13
|
-
export const ServiceProvider = ({ eventBus, children }) => {
|
|
13
|
+
export const ServiceProvider = ({ eventBus, pluginRegistry, children }) => {
|
|
14
14
|
const { id, handleExitClick, symbolDefaults: constructorSymbolDefaults } = useConfig()
|
|
15
15
|
const mapStatusRef = useRef(null)
|
|
16
16
|
const announce = useMemo(() => createAnnouncer(mapStatusRef), [])
|
|
@@ -26,8 +26,10 @@ export const ServiceProvider = ({ eventBus, children }) => {
|
|
|
26
26
|
mapStatusRef,
|
|
27
27
|
closeApp: () => closeApp(id, handleExitClick, eventBus),
|
|
28
28
|
symbolRegistry,
|
|
29
|
-
patternRegistry
|
|
30
|
-
|
|
29
|
+
patternRegistry,
|
|
30
|
+
// See pluginRegistry.js's getPlugin() for what this does and doesn't guarantee.
|
|
31
|
+
getPlugin: (pluginId) => pluginRegistry?.getPlugin(pluginId)
|
|
32
|
+
}), [announce, hints, pluginRegistry])
|
|
31
33
|
|
|
32
34
|
return (
|
|
33
35
|
<ServiceContext.Provider value={services}>
|
|
@@ -34,7 +34,10 @@ jest.mock('../store/configContext.js', () => ({
|
|
|
34
34
|
|
|
35
35
|
describe('ServiceProvider', () => {
|
|
36
36
|
const mockEventBus = { on: jest.fn(), off: jest.fn(), emit: jest.fn() }
|
|
37
|
-
const
|
|
37
|
+
const mockPluginRegistry = { getPlugin: jest.fn(() => ({ newPolygon: jest.fn() })) }
|
|
38
|
+
const wrapper = ({ children }) => (
|
|
39
|
+
<ServiceProvider eventBus={mockEventBus} pluginRegistry={mockPluginRegistry}>{children}</ServiceProvider>
|
|
40
|
+
)
|
|
38
41
|
|
|
39
42
|
test('provides announce, reverseGeocode, eventBus, and closeApp via context', () => {
|
|
40
43
|
const { result } = renderHook(() => React.useContext(ServiceContext), { wrapper })
|
|
@@ -68,6 +71,22 @@ describe('ServiceProvider', () => {
|
|
|
68
71
|
expect(mockShow).toHaveBeenCalledWith('<b>test</b>', { duration: 2000 })
|
|
69
72
|
})
|
|
70
73
|
|
|
74
|
+
test('getPlugin delegates to pluginRegistry.getPlugin', () => {
|
|
75
|
+
const { result } = renderHook(() => React.useContext(ServiceContext), { wrapper })
|
|
76
|
+
|
|
77
|
+
const plugin = result.current.getPlugin('draw')
|
|
78
|
+
|
|
79
|
+
expect(mockPluginRegistry.getPlugin).toHaveBeenCalledWith('draw')
|
|
80
|
+
expect(plugin).toEqual({ newPolygon: expect.any(Function) })
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('getPlugin returns undefined when no pluginRegistry is provided', () => {
|
|
84
|
+
const noRegistryWrapper = ({ children }) => <ServiceProvider eventBus={mockEventBus}>{children}</ServiceProvider>
|
|
85
|
+
const { result } = renderHook(() => React.useContext(ServiceContext), { wrapper: noRegistryWrapper })
|
|
86
|
+
|
|
87
|
+
expect(result.current.getPlugin('draw')).toBeUndefined()
|
|
88
|
+
})
|
|
89
|
+
|
|
71
90
|
test('closeApp calls closeApp service with id and handleExitClick', () => {
|
|
72
91
|
const mockHandleExitClick = jest.fn()
|
|
73
92
|
const { useConfig } = require('../store/configContext.js')
|
|
@@ -461,12 +461,23 @@ export default class InteractiveMap {
|
|
|
461
461
|
* Focus is moved to the panel by default. Set `focus: false` in options to
|
|
462
462
|
* suppress this — useful when showing a panel and you want focus to remain on the button.
|
|
463
463
|
*
|
|
464
|
+
* When the panel is later closed, focus returns to `triggeringElement` if one is given or
|
|
465
|
+
* can be inferred, falling back to the map viewport otherwise. If `triggeringElement` isn't
|
|
466
|
+
* passed explicitly, whatever currently has real DOM focus is captured automatically (e.g. a
|
|
467
|
+
* selected item in the map features list) — this only needs to be passed explicitly when the
|
|
468
|
+
* panel is opened from something that doesn't hold focus at the time (e.g a timer, or a
|
|
469
|
+
* genuine mouse click, which doesn't reliably focus its target in every browser).
|
|
470
|
+
*
|
|
464
471
|
* @param {string} id - Panel identifier to show.
|
|
465
472
|
* @param {object} [options]
|
|
466
473
|
* @param {boolean} [options.focus=true] - Whether to move focus to the panel.
|
|
474
|
+
* @param {HTMLElement} [options.triggeringElement] - Element to return focus to on close.
|
|
475
|
+
* Defaults to the currently focused element, if any.
|
|
467
476
|
*/
|
|
468
|
-
showPanel (id, { focus = true } = {}) {
|
|
469
|
-
|
|
477
|
+
showPanel (id, { focus = true, triggeringElement } = {}) {
|
|
478
|
+
const resolvedTriggeringElement = triggeringElement ??
|
|
479
|
+
(document.activeElement && document.activeElement !== document.body ? document.activeElement : undefined)
|
|
480
|
+
this.eventBus.emit(events.APP_SHOW_PANEL, { id, focus, triggeringElement: resolvedTriggeringElement })
|
|
470
481
|
}
|
|
471
482
|
|
|
472
483
|
/**
|
|
@@ -580,6 +580,39 @@ describe('InteractiveMap — Public API Methods', () => {
|
|
|
580
580
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:togglebuttonstate', { id: 'btn-1', prop: 'disabled', value: true })
|
|
581
581
|
})
|
|
582
582
|
|
|
583
|
+
describe('showPanel — triggeringElement', () => {
|
|
584
|
+
afterEach(() => { document.body.innerHTML = '' })
|
|
585
|
+
|
|
586
|
+
it('passes an explicit triggeringElement through unchanged', () => {
|
|
587
|
+
const el = document.createElement('button')
|
|
588
|
+
map.showPanel('panel1', { triggeringElement: el })
|
|
589
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:showpanel', { id: 'panel1', focus: true, triggeringElement: el })
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
it('auto-captures the currently focused element when none is given', () => {
|
|
593
|
+
const el = document.createElement('button')
|
|
594
|
+
document.body.appendChild(el)
|
|
595
|
+
el.focus()
|
|
596
|
+
map.showPanel('panel1')
|
|
597
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:showpanel', { id: 'panel1', focus: true, triggeringElement: el })
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
it('does not fall back to document.body as a triggeringElement', () => {
|
|
601
|
+
document.body.focus?.() // no-op in jsdom, but document.activeElement defaults to body regardless
|
|
602
|
+
map.showPanel('panel1')
|
|
603
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:showpanel', { id: 'panel1', focus: true, triggeringElement: undefined })
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
it('an explicit triggeringElement takes priority over the currently focused element', () => {
|
|
607
|
+
const focused = document.createElement('button')
|
|
608
|
+
document.body.appendChild(focused)
|
|
609
|
+
focused.focus()
|
|
610
|
+
const explicit = document.createElement('button')
|
|
611
|
+
map.showPanel('panel1', { triggeringElement: explicit })
|
|
612
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:showpanel', { id: 'panel1', focus: true, triggeringElement: explicit })
|
|
613
|
+
})
|
|
614
|
+
})
|
|
615
|
+
|
|
583
616
|
it('setContinueEnabled enables and disables the journey continue button', () => {
|
|
584
617
|
map.setContinueEnabled(true)
|
|
585
618
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:togglebuttonstate', { id: 'journeyContinue', prop: 'disabled', value: false })
|
package/src/config/appConfig.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { KeyboardHelp } from '../App/components/KeyboardHelp/KeyboardHelp.jsx'
|
|
2
|
-
import {
|
|
2
|
+
import { MapControls } from '../App/components/MapControls/MapControls.jsx'
|
|
3
3
|
|
|
4
4
|
const keyboardBasePanelSlots = {
|
|
5
5
|
slot: 'middle',
|
|
@@ -13,7 +13,7 @@ const buttonSlots = {
|
|
|
13
13
|
showLabel: false
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const
|
|
16
|
+
const mapControlsSlot = {
|
|
17
17
|
slot: 'right-bottom'
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -53,7 +53,7 @@ export const defaultAppConfig = {
|
|
|
53
53
|
desktop: journeyContinueSlots
|
|
54
54
|
}, {
|
|
55
55
|
id: 'exit',
|
|
56
|
-
label: '
|
|
56
|
+
label: 'Close map view',
|
|
57
57
|
iconId: 'close',
|
|
58
58
|
onClick: (_e, { services }) => services.closeApp(),
|
|
59
59
|
excludeWhen: ({ appConfig, appState }) => !appConfig.hasExitButton || !appState.isFullscreen,
|
|
@@ -79,7 +79,7 @@ export const defaultAppConfig = {
|
|
|
79
79
|
iconId: 'plus',
|
|
80
80
|
keepFocus: true,
|
|
81
81
|
onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomIn(appConfig.zoomDelta),
|
|
82
|
-
excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appConfig.
|
|
82
|
+
excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appConfig.enableMapControls || appState.interfaceType === 'touch',
|
|
83
83
|
enableWhen: ({ mapState }) => !mapState.isAtMaxZoom,
|
|
84
84
|
mobile: buttonSlots,
|
|
85
85
|
tablet: buttonSlots,
|
|
@@ -91,23 +91,23 @@ export const defaultAppConfig = {
|
|
|
91
91
|
iconId: 'minus',
|
|
92
92
|
keepFocus: true,
|
|
93
93
|
onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomOut(appConfig.zoomDelta),
|
|
94
|
-
excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appConfig.
|
|
94
|
+
excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appConfig.enableMapControls || appState.interfaceType === 'touch',
|
|
95
95
|
enableWhen: ({ mapState }) => !mapState.isAtMinZoom,
|
|
96
96
|
mobile: buttonSlots,
|
|
97
97
|
tablet: buttonSlots,
|
|
98
98
|
desktop: buttonSlots
|
|
99
99
|
}, {
|
|
100
|
-
id: '
|
|
101
|
-
label: '
|
|
100
|
+
id: 'mapControls',
|
|
101
|
+
label: 'Map controls',
|
|
102
102
|
iconId: 'move',
|
|
103
103
|
keepFocus: true,
|
|
104
104
|
isExpanded: false,
|
|
105
|
-
ariaControls: ({ appConfig }) => `${appConfig.id}-
|
|
105
|
+
ariaControls: ({ appConfig }) => `${appConfig.id}-map-controls-content`,
|
|
106
106
|
onClick: (_e, { appState }) => appState.dispatch({
|
|
107
107
|
type: 'TOGGLE_BUTTON_EXPANDED',
|
|
108
|
-
payload: { id: '
|
|
108
|
+
payload: { id: 'mapControls', isExpanded: !appState.expandedButtons.has('mapControls') }
|
|
109
109
|
}),
|
|
110
|
-
excludeWhen: ({ appConfig }) => !appConfig.
|
|
110
|
+
excludeWhen: ({ appConfig }) => !appConfig.enableMapControls,
|
|
111
111
|
mobile: buttonSlots,
|
|
112
112
|
tablet: buttonSlots,
|
|
113
113
|
desktop: buttonSlots
|
|
@@ -131,13 +131,13 @@ export const defaultAppConfig = {
|
|
|
131
131
|
}],
|
|
132
132
|
|
|
133
133
|
controls: [{
|
|
134
|
-
id: '
|
|
134
|
+
id: 'mapControls',
|
|
135
135
|
label: 'Move and zoom',
|
|
136
|
-
excludeWhen: ({ appConfig }) => !appConfig.
|
|
137
|
-
mobile:
|
|
138
|
-
tablet:
|
|
139
|
-
desktop:
|
|
140
|
-
render:
|
|
136
|
+
excludeWhen: ({ appConfig }) => !appConfig.enableMapControls,
|
|
137
|
+
mobile: mapControlsSlot,
|
|
138
|
+
tablet: mapControlsSlot,
|
|
139
|
+
desktop: mapControlsSlot,
|
|
140
|
+
render: MapControls
|
|
141
141
|
}],
|
|
142
142
|
|
|
143
143
|
icons: [{
|
|
@@ -157,7 +157,7 @@ export const defaultAppConfig = {
|
|
|
157
157
|
svgContent: '<path d="m6 9 6 6 6-6"/>'
|
|
158
158
|
}, {
|
|
159
159
|
id: 'move',
|
|
160
|
-
svgContent: '<path d="
|
|
160
|
+
svgContent: '<path d="M11.146 15.854a1.207 1.207 0 0 1 1.708 0l1.56 1.56A2 2 0 0 1 15 18.828V21a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1v-2.172a2 2 0 0 1 .586-1.414z"/><path d="M18.828 15a2 2 0 0 1-1.414-.586l-1.56-1.56a1.207 1.207 0 0 1 0-1.708l1.56-1.56A2 2 0 0 1 18.828 9H21a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1z"/><path d="M6.586 14.414A2 2 0 0 1 5.172 15H3a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2.172a2 2 0 0 1 1.414.586l1.56 1.56a1.207 1.207 0 0 1 0 1.708z"/><path d="M9 3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2.172a2 2 0 0 1-.586 1.414l-1.56 1.56a1.207 1.207 0 0 1-1.708 0l-1.56-1.56A2 2 0 0 1 9 5.172z"/>'
|
|
161
161
|
}, {
|
|
162
162
|
id: 'precision',
|
|
163
163
|
svgContent: '<circle cx="12" cy="12" r="10"/><line x1="22" x2="18" y1="12" y2="12"/><line x1="6" x2="2" y1="12" y2="12"/><line x1="12" x2="12" y1="6" y2="2"/><line x1="12" x2="12" y1="22" y2="18"/>'
|
|
@@ -19,7 +19,7 @@ describe('defaultAppConfig', () => {
|
|
|
19
19
|
const journeyContinueBtn = buttons.find(b => b.id === 'journeyContinue')
|
|
20
20
|
const zoomInBtn = buttons.find(b => b.id === 'zoomIn')
|
|
21
21
|
const zoomOutBtn = buttons.find(b => b.id === 'zoomOut')
|
|
22
|
-
const
|
|
22
|
+
const mapControlsBtn = buttons.find(b => b.id === 'mapControls')
|
|
23
23
|
|
|
24
24
|
// --- UI RENDER TESTS ---
|
|
25
25
|
it('renders KeyboardHelp panel', () => {
|
|
@@ -144,21 +144,21 @@ describe('defaultAppConfig', () => {
|
|
|
144
144
|
appState: { interfaceType: 'mouse' }
|
|
145
145
|
})).toBe(true)
|
|
146
146
|
|
|
147
|
-
// Branch B: Second part of OR is true (
|
|
147
|
+
// Branch B: Second part of OR is true (enableMapControls)
|
|
148
148
|
expect(btn.excludeWhen({
|
|
149
|
-
appConfig: { enableZoomControls: true,
|
|
149
|
+
appConfig: { enableZoomControls: true, enableMapControls: true },
|
|
150
150
|
appState: { interfaceType: 'mouse' }
|
|
151
151
|
})).toBe(true)
|
|
152
152
|
|
|
153
153
|
// Branch C: Third part of OR is true (interfaceType === 'touch')
|
|
154
154
|
expect(btn.excludeWhen({
|
|
155
|
-
appConfig: { enableZoomControls: true,
|
|
155
|
+
appConfig: { enableZoomControls: true, enableMapControls: false },
|
|
156
156
|
appState: { interfaceType: 'touch' }
|
|
157
157
|
})).toBe(true)
|
|
158
158
|
|
|
159
159
|
// Branch D: All parts are false (Result: false)
|
|
160
160
|
expect(btn.excludeWhen({
|
|
161
|
-
appConfig: { enableZoomControls: true,
|
|
161
|
+
appConfig: { enableZoomControls: true, enableMapControls: false },
|
|
162
162
|
appState: { interfaceType: 'mouse' }
|
|
163
163
|
})).toBe(false)
|
|
164
164
|
})
|
|
@@ -183,37 +183,37 @@ describe('defaultAppConfig', () => {
|
|
|
183
183
|
})
|
|
184
184
|
|
|
185
185
|
// --- MOVE CONTROLS TOGGLE BUTTON ---
|
|
186
|
-
it('covers all branches of
|
|
187
|
-
expect(
|
|
188
|
-
expect(
|
|
186
|
+
it('covers all branches of mapControls excludeWhen', () => {
|
|
187
|
+
expect(mapControlsBtn.excludeWhen({ appConfig: { enableMapControls: false } })).toBe(true)
|
|
188
|
+
expect(mapControlsBtn.excludeWhen({ appConfig: { enableMapControls: true } })).toBe(false)
|
|
189
189
|
})
|
|
190
190
|
|
|
191
|
-
it('
|
|
192
|
-
expect(
|
|
191
|
+
it('mapControls ariaControls resolves the control element id', () => {
|
|
192
|
+
expect(mapControlsBtn.ariaControls({ appConfig: { id: 'im' } })).toBe('im-map-controls-content')
|
|
193
193
|
})
|
|
194
194
|
|
|
195
|
-
it('
|
|
195
|
+
it('mapControls onClick toggles TOGGLE_BUTTON_EXPANDED based on current state', () => {
|
|
196
196
|
const dispatch = jest.fn()
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
mapControlsBtn.onClick({}, { appState: { dispatch, expandedButtons: new Set() } })
|
|
199
199
|
expect(dispatch).toHaveBeenCalledWith({
|
|
200
200
|
type: 'TOGGLE_BUTTON_EXPANDED',
|
|
201
|
-
payload: { id: '
|
|
201
|
+
payload: { id: 'mapControls', isExpanded: true }
|
|
202
202
|
})
|
|
203
203
|
|
|
204
204
|
dispatch.mockClear()
|
|
205
|
-
|
|
205
|
+
mapControlsBtn.onClick({}, { appState: { dispatch, expandedButtons: new Set(['mapControls']) } })
|
|
206
206
|
expect(dispatch).toHaveBeenCalledWith({
|
|
207
207
|
type: 'TOGGLE_BUTTON_EXPANDED',
|
|
208
|
-
payload: { id: '
|
|
208
|
+
payload: { id: 'mapControls', isExpanded: false }
|
|
209
209
|
})
|
|
210
210
|
})
|
|
211
211
|
|
|
212
212
|
// --- MOVE CONTROLS ---
|
|
213
|
-
it('covers all branches of
|
|
214
|
-
const control = defaultAppConfig.controls.find(c => c.id === '
|
|
215
|
-
expect(control.excludeWhen({ appConfig: {
|
|
216
|
-
expect(control.excludeWhen({ appConfig: {
|
|
213
|
+
it('covers all branches of mapControls control excludeWhen', () => {
|
|
214
|
+
const control = defaultAppConfig.controls.find(c => c.id === 'mapControls')
|
|
215
|
+
expect(control.excludeWhen({ appConfig: { enableMapControls: false } })).toBe(true)
|
|
216
|
+
expect(control.excludeWhen({ appConfig: { enableMapControls: true } })).toBe(false)
|
|
217
217
|
})
|
|
218
218
|
|
|
219
219
|
// --- SUPPLEMENTARY CONFIGS ---
|
package/src/config/defaults.js
CHANGED
|
@@ -16,7 +16,7 @@ const defaults = {
|
|
|
16
16
|
containerHeight: '600px',
|
|
17
17
|
deviceNotSupportedText: 'Your device is not supported. A map is available with a more up-to-date browser or device.',
|
|
18
18
|
enableFullscreen: false,
|
|
19
|
-
|
|
19
|
+
enableMapControls: true,
|
|
20
20
|
enableZoomControls: true,
|
|
21
21
|
genericErrorText: 'There was a problem loading the map. Please try again later.',
|
|
22
22
|
hasExitButton: false,
|
|
@@ -32,7 +32,7 @@ const defaults = {
|
|
|
32
32
|
urlPosition: 'sync',
|
|
33
33
|
maxMobileWidth: 640,
|
|
34
34
|
minDesktopWidth: 835,
|
|
35
|
-
|
|
35
|
+
mapControlsHintText: 'A "Move and zoom controls" button is also available, opening further controls for moving and zooming.',
|
|
36
36
|
nudgePanDelta: 5,
|
|
37
37
|
nudgeZoomDelta: 0.1,
|
|
38
38
|
panDelta: 100,
|
package/src/config/events.js
CHANGED
|
@@ -145,14 +145,14 @@ export const EVENTS = {
|
|
|
145
145
|
MAP_SET_STYLE: 'map:setstyle',
|
|
146
146
|
/** @internal Set map size. Payload: { width, height } */
|
|
147
147
|
MAP_SET_SIZE: 'map:setsize',
|
|
148
|
-
/** @internal Set the accessible
|
|
149
|
-
|
|
150
|
-
/** @internal Suppress/restore the accessible
|
|
151
|
-
|
|
152
|
-
/** @internal Set the active
|
|
153
|
-
|
|
154
|
-
/** @internal Select the active
|
|
155
|
-
|
|
148
|
+
/** @internal Set the accessible spatial list's items. Payload: { items: { id: string, label: string }[], multiselectable: boolean, label?: string } */
|
|
149
|
+
MAP_SET_SPATIAL_LIST: 'map:setspatiallist',
|
|
150
|
+
/** @internal Suppress/restore the accessible spatial list. Payload: { suppressed: boolean } */
|
|
151
|
+
MAP_SET_SPATIAL_LIST_SUPPRESSED: 'map:setspatiallistsuppressed',
|
|
152
|
+
/** @internal Set the active item in the accessible spatial list (roving tabindex position). Payload: { id: string | null } */
|
|
153
|
+
MAP_SET_ACTIVE_ITEM: 'map:setactiveitem',
|
|
154
|
+
/** @internal Select the active spatial list item as the real selection (Enter key). Payload: none */
|
|
155
|
+
MAP_SELECT_ITEM: 'map:selectitem',
|
|
156
156
|
/** @internal Set pixel ratio. Payload: pixelRatio */
|
|
157
157
|
MAP_SET_PIXEL_RATIO: 'map:setpixelratio',
|
|
158
158
|
/** @internal Fit the map to a bounding box. Payload: [west, south, east, north] */
|
package/src/scss/main.scss
CHANGED
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
@use '../App/components/Hints/Hints.module';
|
|
30
30
|
@use '../App/components/CrossHair/CrossHair.module';
|
|
31
31
|
@use '../App/components/Markers/Markers.module';
|
|
32
|
-
@use '../App/components/Viewport/
|
|
32
|
+
@use '../App/components/Viewport/SpatialList.module';
|
|
33
33
|
@use '../App/components/Actions/Actions.module';
|
|
34
|
-
@use '../App/components/
|
|
34
|
+
@use '../App/components/MapControls/MapControls.module';
|
|
35
35
|
|
|
36
36
|
// Components that dont have a corresponding React Component
|
|
37
37
|
@use 'components';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// ===================================================
|
|
2
|
+
// Tool: govuk-content-fallback-colors
|
|
3
|
+
// ===================================================
|
|
4
|
+
// Resets GOV.UK Frontend's own text-colour classes back onto our theme tokens, since they set
|
|
5
|
+
// colour explicitly rather than inheriting it. !important wins against a third-party stylesheet
|
|
6
|
+
// whose specificity and load order we don't control. Deliberately-coloured components (tags,
|
|
7
|
+
// error/warning text, buttons, .govuk-link--inverse/--text-colour/--muted) are left untouched so
|
|
8
|
+
// their built-in contrast pairing isn't broken.
|
|
9
|
+
// stylelint-disable declaration-no-important -- resetting a third-party stylesheet's own colours, see comment above
|
|
10
|
+
@mixin govuk-content-fallback-colors {
|
|
11
|
+
:is(
|
|
12
|
+
.govuk-body, .govuk-body-l, .govuk-body-m, .govuk-body-s, .govuk-body-xs,
|
|
13
|
+
.govuk-heading-xl, .govuk-heading-l, .govuk-heading-m, .govuk-heading-s,
|
|
14
|
+
.govuk-list, .govuk-label, .govuk-fieldset__legend,
|
|
15
|
+
.govuk-table, .govuk-table__cell, .govuk-table__header
|
|
16
|
+
) {
|
|
17
|
+
color: var(--foreground-color) !important;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// GOV.UK Frontend renders these as its muted grey, matching our --secondary-text-color.
|
|
21
|
+
:is(.govuk-hint, .govuk-caption-xl, .govuk-caption-l, .govuk-caption-m, .govuk-caption-s) {
|
|
22
|
+
color: var(--secondary-text-color) !important;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.govuk-link:is(:link, :visited) {
|
|
26
|
+
color: var(--content-link-color) !important;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// stylelint-enable declaration-no-important
|
package/src/test-utils.js
CHANGED
|
@@ -37,6 +37,14 @@ export const createMockRegistries = (overrides = {}) => {
|
|
|
37
37
|
registerKeyboardShortcut: jest.fn(),
|
|
38
38
|
setProviderSupportedShortcuts: jest.fn(),
|
|
39
39
|
getKeyboardShortcuts: jest.fn(() => [])
|
|
40
|
+
},
|
|
41
|
+
spatialListRegistry: {
|
|
42
|
+
registerItemProvider: jest.fn(),
|
|
43
|
+
unregisterItemProvider: jest.fn(),
|
|
44
|
+
claimExclusive: jest.fn(),
|
|
45
|
+
releaseExclusive: jest.fn(),
|
|
46
|
+
notifyItemsChanged: jest.fn(),
|
|
47
|
+
clear: jest.fn()
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
}
|
package/src/types.js
CHANGED
|
@@ -306,10 +306,10 @@
|
|
|
306
306
|
* Get the dimensions of the visible map area as a formatted string (e.g., '400m by 750m').
|
|
307
307
|
*
|
|
308
308
|
* @property {(from: [number, number], to: [number, number]) => string} getCardinalMove
|
|
309
|
-
* Get cardinal direction and distance between two coordinates ([lng, lat] or [easting, northing] depending on the crs of the map provider). Returns a formatted string (e.g., 'north
|
|
309
|
+
* Get cardinal direction and distance between two coordinates ([lng, lat] or [easting, northing] depending on the crs of the map provider). Returns a formatted string (e.g., 'north 400 metres' or 'south 400 metres, west 750 metres').
|
|
310
310
|
*
|
|
311
311
|
* @property {() => number} getResolution
|
|
312
|
-
* Get map resolution in
|
|
312
|
+
* Get map resolution in metres per pixel.
|
|
313
313
|
*
|
|
314
314
|
* @property {(coords: [number, number]) => { x: number, y: number }} mapToScreen
|
|
315
315
|
* Convert map coordinates ([lng, lat] or [easting, northing] depending on the crs of the map provider) to screen pixel position (x from left edge, y from top edge of viewport).
|
|
@@ -629,6 +629,43 @@
|
|
|
629
629
|
* Raw SVG content (without the outer SVG tag).
|
|
630
630
|
*/
|
|
631
631
|
|
|
632
|
+
/**
|
|
633
|
+
* Describes a single row in the keyboard shortcuts help panel (opened via Shift+?).
|
|
634
|
+
* Plugins register these via `PluginManifest.keyboardShortcuts`; core (built-in)
|
|
635
|
+
* shortcuts share this same shape internally.
|
|
636
|
+
*
|
|
637
|
+
* @typedef {Object} KeyboardShortcutDefinition
|
|
638
|
+
*
|
|
639
|
+
* @property {string} command
|
|
640
|
+
* HTML string describing the key combination, rendered as markup — wrap keys in
|
|
641
|
+
* `<kbd>` tags, e.g. `'<kbd>Shift</kbd> + <kbd>K</kbd>'`. If it uses the Alt key,
|
|
642
|
+
* label it per platform (macOS calls it Option, not Alt) — see the shared `isMac()`
|
|
643
|
+
* helper and its use in the app's own core shortcuts.
|
|
644
|
+
*
|
|
645
|
+
* @property {'viewport' | 'listbox' | 'global'} [context='viewport']
|
|
646
|
+
* Which help-panel context this shortcut applies to. Used only to choose the panel's
|
|
647
|
+
* default open tab — it doesn't affect whether the row is shown.
|
|
648
|
+
*
|
|
649
|
+
* @property {string} [group='Navigate']
|
|
650
|
+
* Tab label the shortcut is grouped under in the help panel. Shortcuts sharing a group
|
|
651
|
+
* (case/whitespace-insensitive) appear together under one tab; if every visible shortcut
|
|
652
|
+
* shares one group, the panel renders as a flat list with no tabs.
|
|
653
|
+
*
|
|
654
|
+
* @property {string} id
|
|
655
|
+
* Unique shortcut identifier.
|
|
656
|
+
*
|
|
657
|
+
* @property {string[]} [requiredConfig]
|
|
658
|
+
* App config keys that must all be truthy for the shortcut to appear in the help panel.
|
|
659
|
+
*
|
|
660
|
+
* @property {string} title
|
|
661
|
+
* Accessible title shown in the help panel.
|
|
662
|
+
*
|
|
663
|
+
* @property {boolean} [visuallyHidden=false]
|
|
664
|
+
* When true, the row is hidden from sighted users but stays in the DOM and remains
|
|
665
|
+
* discoverable by assistive technology. Use for a shortcut with no visual affordance to
|
|
666
|
+
* discover it by otherwise — e.g. one whose only effect is a screen reader announcement.
|
|
667
|
+
*/
|
|
668
|
+
|
|
632
669
|
/**
|
|
633
670
|
* Manifest defining a plugin's buttons, panels, controls, API methods, and state.
|
|
634
671
|
*
|
|
@@ -649,6 +686,9 @@
|
|
|
649
686
|
* @property {ComponentType} [InitComponent]
|
|
650
687
|
* Initialization component.
|
|
651
688
|
*
|
|
689
|
+
* @property {KeyboardShortcutDefinition[]} [keyboardShortcuts]
|
|
690
|
+
* Keyboard shortcut definitions shown in the keyboard shortcuts help panel.
|
|
691
|
+
*
|
|
652
692
|
* @property {PanelDefinition[]} [panels]
|
|
653
693
|
* Panel definitions.
|
|
654
694
|
*
|
|
@@ -725,16 +765,18 @@
|
|
|
725
765
|
* @property {boolean} [enableFullscreen=false]
|
|
726
766
|
* Whether a toggle fullscreen button is displayed.
|
|
727
767
|
*
|
|
728
|
-
* @property {boolean} [
|
|
729
|
-
* Whether the
|
|
730
|
-
* zoom, and
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
768
|
+
* @property {boolean} [enableMapControls=true]
|
|
769
|
+
* Whether the map controls are displayed — a button that reveals on-screen move,
|
|
770
|
+
* zoom, and precision buttons, plus a target point, typically used to place or
|
|
771
|
+
* select features on the map without a drag gesture. This gives a non-dragging way
|
|
772
|
+
* to operate the map, for anyone who can't perform a drag/pinch gesture (e.g.
|
|
773
|
+
* switch access users) and for voice interfaces such as Voice Control, which can
|
|
774
|
+
* trigger a button click but not a drag. When enabled, enableZoomControls' buttons
|
|
775
|
+
* are hidden to avoid duplication.
|
|
734
776
|
*
|
|
735
777
|
* @property {boolean} [enableZoomControls=true]
|
|
736
778
|
* Whether zoom control buttons are displayed. Not displayed when the interface
|
|
737
|
-
* type is 'touch', or when
|
|
779
|
+
* type is 'touch', or when enableMapControls is enabled.
|
|
738
780
|
*
|
|
739
781
|
* @property {[number, number, number, number]} [extent]
|
|
740
782
|
* Initial extent [minX, minY, maxX, maxY]. Equivalent to bounds; use whichever matches your map provider's terminology.
|
|
@@ -766,8 +808,8 @@
|
|
|
766
808
|
* @property {string} [mapLabel='Interactive map']
|
|
767
809
|
* Accessible label for the map, announced by screen readers. Also prefixed onto mapHintText, so give each map on a page a distinct label.
|
|
768
810
|
*
|
|
769
|
-
* @property {string} [
|
|
770
|
-
* Visually hidden text describing the
|
|
811
|
+
* @property {string} [mapControlsHintText]
|
|
812
|
+
* Visually hidden text describing the map controls button, appended to keyboardHintText when enableMapControls is true.
|
|
771
813
|
*
|
|
772
814
|
* @property {MapProviderDescriptor} [mapProvider]
|
|
773
815
|
* A factory function that returns a map provider instance (e.g. maplibreProvider()).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { spatialNavigate } from './spatialNavigate.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Item-list-shaped counterpart to spatialNavigate's raw pixel-array contract: given a list of
|
|
5
|
+
* items each carrying screen coordinates, finds the id of whichever one is spatially nearest
|
|
6
|
+
* to the current item in the given direction. Used to move roving-tabindex focus in a listbox
|
|
7
|
+
* spatially (Alt+Arrow) rather than sequentially (plain Arrow/Home/End) — the same items list
|
|
8
|
+
* already built for sequential navigation works here unchanged, whether it holds map features
|
|
9
|
+
* or (in future) something else entirely, as long as each item carries x/y.
|
|
10
|
+
*
|
|
11
|
+
* Items missing x/y are skipped — they have no screen position to navigate by. If none of the
|
|
12
|
+
* items have coordinates, or the list is empty, currentId is returned unchanged.
|
|
13
|
+
*
|
|
14
|
+
* @param {Array<{id: string, x?: number, y?: number}>} items
|
|
15
|
+
* @param {string|null} currentId
|
|
16
|
+
* @param {'ArrowUp'|'ArrowDown'|'ArrowLeft'|'ArrowRight'} direction
|
|
17
|
+
* @returns {string|null} The id to move to — currentId unchanged if nothing qualifies.
|
|
18
|
+
*/
|
|
19
|
+
export function findNearestItemInDirection (items, currentId, direction) {
|
|
20
|
+
const positioned = items.filter(item => item.x != null && item.y != null)
|
|
21
|
+
if (!positioned.length) {
|
|
22
|
+
return currentId
|
|
23
|
+
}
|
|
24
|
+
const pixels = positioned.map(item => [item.x, item.y])
|
|
25
|
+
const currentIndex = positioned.findIndex(item => item.id === currentId)
|
|
26
|
+
const start = currentIndex === -1 ? pixels[0] : pixels[currentIndex]
|
|
27
|
+
const nextIndex = spatialNavigate(start, pixels, direction)
|
|
28
|
+
return positioned[nextIndex]?.id ?? currentId
|
|
29
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { findNearestItemInDirection } from './findNearestItemInDirection.js'
|
|
2
|
+
|
|
3
|
+
describe('findNearestItemInDirection', () => {
|
|
4
|
+
const items = [
|
|
5
|
+
{ id: 'center', x: 0, y: 0 },
|
|
6
|
+
{ id: 'up', x: 0, y: -10 },
|
|
7
|
+
{ id: 'down', x: 0, y: 10 },
|
|
8
|
+
{ id: 'left', x: -10, y: 0 },
|
|
9
|
+
{ id: 'right', x: 10, y: 0 }
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
test.each([
|
|
13
|
+
['ArrowUp', 'up'],
|
|
14
|
+
['ArrowDown', 'down'],
|
|
15
|
+
['ArrowLeft', 'left'],
|
|
16
|
+
['ArrowRight', 'right']
|
|
17
|
+
])('finds the nearest item to the %s of the current one', (direction, expectedId) => {
|
|
18
|
+
expect(findNearestItemInDirection(items, 'center', direction)).toBe(expectedId)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('returns the current id unchanged when nothing qualifies in that direction', () => {
|
|
22
|
+
const single = [{ id: 'only', x: 0, y: 0 }]
|
|
23
|
+
expect(findNearestItemInDirection(single, 'only', 'ArrowUp')).toBe('only')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('returns currentId unchanged when the item list is empty', () => {
|
|
27
|
+
expect(findNearestItemInDirection([], 'anything', 'ArrowUp')).toBe('anything')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('skips items with no screen coordinates', () => {
|
|
31
|
+
const withUnpositioned = [
|
|
32
|
+
{ id: 'center', x: 0, y: 0 },
|
|
33
|
+
{ id: 'no-coords' },
|
|
34
|
+
{ id: 'up', x: 0, y: -10 }
|
|
35
|
+
]
|
|
36
|
+
expect(findNearestItemInDirection(withUnpositioned, 'center', 'ArrowUp')).toBe('up')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('returns currentId unchanged when no item has coordinates at all', () => {
|
|
40
|
+
const noneWithCoords = [{ id: 'a' }, { id: 'b' }]
|
|
41
|
+
expect(findNearestItemInDirection(noneWithCoords, 'a', 'ArrowUp')).toBe('a')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('treats an unresolvable currentId as starting from the first positioned item', () => {
|
|
45
|
+
expect(findNearestItemInDirection(items, 'not-in-list', 'ArrowRight')).toBe('right')
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// spatialNavigate's own contract guarantees a valid index in practice (see its own tests), so
|
|
50
|
+
// this can't be reached through findNearestItemInDirection's real dependency — mocked here,
|
|
51
|
+
// matching labels.test.js's identical fallback test for navigateToNextLabel, purely to prove
|
|
52
|
+
// the defensive `?? currentId` itself degrades gracefully rather than crashing if that
|
|
53
|
+
// guarantee were ever weakened.
|
|
54
|
+
describe('findNearestItemInDirection — defensive fallback', () => {
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
jest.dontMock('./spatialNavigate.js')
|
|
57
|
+
jest.resetModules()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('falls back to currentId if spatialNavigate ever returns an out-of-range index', () => {
|
|
61
|
+
jest.resetModules()
|
|
62
|
+
jest.doMock('./spatialNavigate.js', () => ({ spatialNavigate: () => -1 }))
|
|
63
|
+
const { findNearestItemInDirection: fn } = require('./findNearestItemInDirection.js')
|
|
64
|
+
const items = [{ id: 'center', x: 0, y: 0 }, { id: 'up', x: 0, y: -10 }]
|
|
65
|
+
expect(fn(items, 'center', 'ArrowUp')).toBe('center')
|
|
66
|
+
})
|
|
67
|
+
})
|