@defra/interactive-map 0.0.43-alpha → 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/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.md +16 -8
- package/docs/plugins/draw.md +1 -1
- package/package.json +1 -1
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/index.js +1 -1
- package/plugins/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/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 +31 -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 +8 -2
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +35 -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 +31 -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 +32 -57
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +14 -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/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 +5 -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 +4 -4
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +7 -7
- package/plugins/interact/src/hooks/{useMapItemList.js → useSpatialList.js} +87 -52
- package/plugins/interact/src/hooks/{useMapItemList.test.js → useSpatialList.test.js} +152 -71
- package/plugins/interact/src/manifest.js +12 -1
- package/plugins/interact/src/manifest.test.js +20 -0
- package/plugins/interact/src/utils/spatial.js +1 -1
- package/providers/beta/openlayers/src/utils/queryFeatures.js +1 -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/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/src/App/components/CrossHair/CrossHair.jsx +10 -4
- package/src/App/components/CrossHair/CrossHair.module.scss +16 -12
- package/src/App/components/CrossHair/CrossHair.test.jsx +9 -4
- package/src/App/components/Hints/Hints.jsx +3 -3
- package/src/App/components/Hints/Hints.test.jsx +3 -3
- package/src/App/components/MapButton/MapButton.module.scss +1 -2
- 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.test.jsx +2 -2
- 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} +3 -3
- package/src/App/components/Viewport/{Features.test.jsx → SpatialList.test.jsx} +60 -43
- package/src/App/components/Viewport/Viewport.jsx +20 -10
- package/src/App/components/Viewport/Viewport.test.jsx +18 -2
- package/src/App/hooks/useCrossHairAPI.js +5 -5
- package/src/App/hooks/useCrossHairAPI.test.js +5 -5
- package/src/App/hooks/useHintsAPI.js +5 -5
- package/src/App/hooks/useSpatialListFocus.js +296 -0
- package/src/App/hooks/{useFeatureFocus.test.js → useSpatialListFocus.test.js} +318 -163
- 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/registry/spatialListRegistry.js +104 -0
- package/src/App/registry/spatialListRegistry.test.js +218 -0
- package/src/App/store/AppProvider.jsx +3 -2
- package/src/config/appConfig.js +14 -14
- 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 +11 -9
- package/src/utils/findNearestItemInDirection.js +29 -0
- package/src/utils/findNearestItemInDirection.test.js +67 -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/src/App/components/Viewport/Features.jsx +0 -49
- package/src/App/hooks/useFeatureFocus.js +0 -257
- package/src/App/hooks/useFeatureItems.js +0 -39
- package/src/App/hooks/useFeatureItems.test.js +0 -155
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
navigateToNextLabel, createMapLabelNavigator
|
|
6
6
|
} from './labels.js'
|
|
7
7
|
|
|
8
|
-
import { spatialNavigate } from '
|
|
8
|
+
import { spatialNavigate } from '../../../../src/utils/spatialNavigate.js'
|
|
9
9
|
import { calculateLinearTextSize } from './calculateLinearTextSize.js'
|
|
10
10
|
|
|
11
|
-
jest.mock('
|
|
11
|
+
jest.mock('../../../../src/utils/spatialNavigate.js', () => ({ spatialNavigate: jest.fn() }))
|
|
12
12
|
jest.mock('./calculateLinearTextSize.js', () => ({ calculateLinearTextSize: jest.fn(() => 12) }))
|
|
13
13
|
|
|
14
14
|
describe('labels utils', () => {
|
|
@@ -183,7 +183,7 @@ describe('labels utils', () => {
|
|
|
183
183
|
expect(nav.highlightNextLabel('ArrowRight')).toBeNull()
|
|
184
184
|
|
|
185
185
|
// Zoom handler: no active highlight → no-op
|
|
186
|
-
const zoomHandler = map.on.mock.calls.find(([
|
|
186
|
+
const zoomHandler = map.on.mock.calls.find(([eventName]) => eventName === 'zoom')[1]
|
|
187
187
|
map.setLayoutProperty.mockClear()
|
|
188
188
|
zoomHandler()
|
|
189
189
|
expect(map.setLayoutProperty).not.toHaveBeenCalled()
|
|
@@ -222,10 +222,10 @@ describe('labels utils', () => {
|
|
|
222
222
|
// Fire MAP_SET_STYLE → styledata → idle → setLineCenterPlacement + initLabelSource
|
|
223
223
|
const styleHandler = eventBus.on.mock.calls[0][1]
|
|
224
224
|
styleHandler({ mapColorScheme: 'dark' })
|
|
225
|
-
const styleDataHandler = map.once.mock.calls.find(([
|
|
225
|
+
const styleDataHandler = map.once.mock.calls.find(([eventName]) => eventName === 'styledata')[1]
|
|
226
226
|
styleDataHandler()
|
|
227
227
|
map.setLayoutProperty.mockClear()
|
|
228
|
-
const idleHandler = map.once.mock.calls.find(([
|
|
228
|
+
const idleHandler = map.once.mock.calls.find(([eventName]) => eventName === 'idle')[1]
|
|
229
229
|
idleHandler()
|
|
230
230
|
expect(map.setLayoutProperty).toHaveBeenCalledWith('s1', 'symbol-placement', 'line-center')
|
|
231
231
|
})
|
|
@@ -106,62 +106,6 @@ const getCardinalMove = (from, to) => {
|
|
|
106
106
|
return moves.join(', ')
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/**
|
|
110
|
-
* Find the index of the nearest pixel in a given cardinal direction.
|
|
111
|
-
*
|
|
112
|
-
* The function:
|
|
113
|
-
* - Filters candidate points that lie in the specified direction
|
|
114
|
-
* (up, down, left, right) relative to a start pixel.
|
|
115
|
-
* - Selects the nearest valid candidate using Euclidean distance.
|
|
116
|
-
* - Falls back to returning the start pixel index if no candidate exists.
|
|
117
|
-
*
|
|
118
|
-
* Example:
|
|
119
|
-
* spatialNavigate('up', [100, 200], [[100, 100], [150, 250], [90, 180]])
|
|
120
|
-
* → returns the index of [100, 100]
|
|
121
|
-
*
|
|
122
|
-
* @param {'ArrowUp'|'ArrowDown'|'ArrowLeft'|'ArrowRight'} direction - The direction to search.
|
|
123
|
-
* @param {[number, number]} start - The starting pixel coordinate [x, y].
|
|
124
|
-
* @param {Array<[number, number]>} pixels - Array of pixel coordinates.
|
|
125
|
-
* @returns {number} Index of the closest pixel in the given direction.
|
|
126
|
-
*/
|
|
127
|
-
const isInDirection = (direction, dx, dy) => {
|
|
128
|
-
switch (direction) {
|
|
129
|
-
case 'ArrowUp': return dy < 0 && Math.abs(dy) >= Math.abs(dx)
|
|
130
|
-
case 'ArrowDown': return dy > 0 && Math.abs(dy) >= Math.abs(dx)
|
|
131
|
-
case 'ArrowLeft': return dx < 0 && Math.abs(dx) > Math.abs(dy)
|
|
132
|
-
case 'ArrowRight': return dx > 0 && Math.abs(dx) > Math.abs(dy)
|
|
133
|
-
default: return false
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const spatialNavigate = (direction, start, pixels) => {
|
|
138
|
-
const [sx, sy] = start
|
|
139
|
-
|
|
140
|
-
// Direction filters
|
|
141
|
-
const candidates = pixels.filter(([x, y]) =>
|
|
142
|
-
(x !== sx || y !== sy) && isInDirection(direction, x - sx, y - sy)
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
if (!candidates.length) {
|
|
146
|
-
return pixels.findIndex(p => p[0] === sx && p[1] === sy)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Choose the closest by Euclidean distance
|
|
150
|
-
let closestIndex = -1
|
|
151
|
-
let minDist = Infinity
|
|
152
|
-
candidates.forEach(c => {
|
|
153
|
-
const dx = c[0] - sx
|
|
154
|
-
const dy = c[1] - sy
|
|
155
|
-
const dist = dx * dx + dy * dy // squared distance is enough
|
|
156
|
-
if (dist < minDist) {
|
|
157
|
-
minDist = dist
|
|
158
|
-
closestIndex = pixels.indexOf(c)
|
|
159
|
-
}
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
return closestIndex
|
|
163
|
-
}
|
|
164
|
-
|
|
165
109
|
const getResolution = (center, zoom) => {
|
|
166
110
|
const EARTH_CIRCUMFERENCE = 40075016.686
|
|
167
111
|
const TILE_SIZE = 512
|
|
@@ -239,7 +183,6 @@ export {
|
|
|
239
183
|
getCardinalMove,
|
|
240
184
|
getBboxFromGeoJSON,
|
|
241
185
|
isGeometryObscured,
|
|
242
|
-
spatialNavigate,
|
|
243
186
|
getResolution,
|
|
244
187
|
getPaddedBounds,
|
|
245
188
|
formatDimension
|
|
@@ -50,33 +50,6 @@ describe('spatial utils', () => {
|
|
|
50
50
|
expect(spatial.getCardinalMove([0, 0], [0.00001, 0.00001])).toBe('')
|
|
51
51
|
})
|
|
52
52
|
|
|
53
|
-
test('spatialNavigate all directions and fallback', () => {
|
|
54
|
-
const pixels = [[0, 0], [0, -1], [1, 0], [0, 1], [-1, 0]]
|
|
55
|
-
expect(spatial.spatialNavigate('ArrowUp', [0, 0], pixels)).toBe(1)
|
|
56
|
-
expect(spatial.spatialNavigate('ArrowDown', [0, 0], pixels)).toBe(3)
|
|
57
|
-
expect(spatial.spatialNavigate('ArrowLeft', [0, 0], pixels)).toBe(4)
|
|
58
|
-
expect(spatial.spatialNavigate('ArrowRight', [0, 0], pixels)).toBe(2)
|
|
59
|
-
expect(spatial.spatialNavigate('InvalidDir', [0, 0], pixels)).toBe(0)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test('spatialNavigate finds closer candidates (hits dist < minDist)', () => {
|
|
63
|
-
const start = [0, 0]
|
|
64
|
-
const pixels = [[0, 0], [10, 0], [2, 0]]
|
|
65
|
-
expect(spatial.spatialNavigate('ArrowRight', start, pixels)).toBe(2)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
test('spatialNavigate skips farther candidate (dist >= minDist false branch)', () => {
|
|
69
|
-
// Closer candidate first → second candidate fails dist < minDist
|
|
70
|
-
const pixels = [[0, 0], [2, 0], [10, 0]]
|
|
71
|
-
expect(spatial.spatialNavigate('ArrowRight', [0, 0], pixels)).toBe(1)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
test('spatialNavigate diagonal with dx>dy', () => {
|
|
75
|
-
const start = [0, 0]
|
|
76
|
-
const pixels = [[0, 0], [3, 1], [1, 0]] // dx>dy
|
|
77
|
-
expect(spatial.spatialNavigate('ArrowRight', start, pixels)).toBe(2)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
53
|
test('getResolution returns positive value', () => {
|
|
81
54
|
expect(spatial.getResolution({ lat: 0 }, 1)).toBeGreaterThan(0)
|
|
82
55
|
})
|
|
@@ -21,6 +21,13 @@ export const CrossHair = () => {
|
|
|
21
21
|
ref={crossHairRef}
|
|
22
22
|
id={`${id}-cross-hair`}
|
|
23
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()}
|
|
24
31
|
onClick={() => crossHair.activate?.()}
|
|
25
32
|
style={{
|
|
26
33
|
position: 'absolute',
|
|
@@ -29,19 +36,18 @@ export const CrossHair = () => {
|
|
|
29
36
|
display: isVisible ? 'block' : 'none'
|
|
30
37
|
}}
|
|
31
38
|
>
|
|
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. */}
|
|
32
41
|
<svg
|
|
33
42
|
width='38'
|
|
34
43
|
height='38'
|
|
35
44
|
viewBox='0 0 38 38'
|
|
36
45
|
fillRule='evenodd'
|
|
37
46
|
fill='currentColor'
|
|
47
|
+
aria-hidden='true'
|
|
38
48
|
>
|
|
39
49
|
<path d={paths[state || 'active']} />
|
|
40
50
|
</svg>
|
|
41
|
-
{/* "Target" — shorter and easier for Voice Control users to say than "Cross hair"
|
|
42
|
-
("Click Target"); a general-purpose map is unlikely to already have a feature/marker
|
|
43
|
-
named "Target" that this would collide with. */}
|
|
44
|
-
<span className='im-u-visually-hidden'>Target</span>
|
|
45
51
|
</button>
|
|
46
52
|
)
|
|
47
53
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// see CrossHair.jsx — without being a Tab stop.
|
|
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.
|
|
4
3
|
.im-c-cross-hair-button {
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
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.
|
|
8
10
|
appearance: none;
|
|
9
11
|
border: 0;
|
|
10
12
|
background: none;
|
|
@@ -12,18 +14,20 @@
|
|
|
12
14
|
font: inherit;
|
|
13
15
|
cursor: inherit;
|
|
14
16
|
|
|
15
|
-
// Explicit size
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// based AT overlay (e.g. Voice Control) reads to place its badge — without it, the button's
|
|
19
|
-
// content-box collapses to 0×0 (its only child is position:absolute, contributing nothing to
|
|
20
|
-
// normal-flow sizing), so the badge would land at the anchor point's corner, not on the icon.
|
|
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.
|
|
21
20
|
width: 38px;
|
|
22
21
|
height: 38px;
|
|
23
22
|
margin: -19px 0 0 -19px;
|
|
24
23
|
|
|
24
|
+
outline: 3px solid transparent;
|
|
25
|
+
|
|
25
26
|
svg {
|
|
26
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;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
path {
|
|
@@ -9,9 +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"
|
|
13
|
-
//
|
|
14
|
-
// can find it, at the right geometry); the SVG itself is a plain, unstyled child.
|
|
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.
|
|
15
14
|
const renderWith = (overrides = {}) => {
|
|
16
15
|
useCrossHair.mockReturnValue({
|
|
17
16
|
crossHairRef,
|
|
@@ -51,7 +50,7 @@ describe('CrossHair', () => {
|
|
|
51
50
|
|
|
52
51
|
// Fully removed from the layout/accessibility tree when hidden, not just faded — so Voice
|
|
53
52
|
// Control's Show Names never puts a "Target" label over nothing. Its real route back is
|
|
54
|
-
// opening
|
|
53
|
+
// opening MapControls (a normal, always-visible button), not re-discovering this element.
|
|
55
54
|
it('hides marker when not visible', () => {
|
|
56
55
|
const { button } = renderWith({ isVisible: false })
|
|
57
56
|
expect(button).toHaveStyle({ display: 'none' })
|
|
@@ -69,6 +68,12 @@ describe('CrossHair', () => {
|
|
|
69
68
|
expect(() => fireEvent.click(button)).not.toThrow()
|
|
70
69
|
})
|
|
71
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
|
+
|
|
72
77
|
it('is not a Tab stop, but is a real button for coordinate-based AT (e.g. Voice Control) to find', () => {
|
|
73
78
|
const { button } = renderWith()
|
|
74
79
|
expect(button.tabIndex).toBe(-1)
|
|
@@ -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')
|
|
@@ -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);
|
|
@@ -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>
|
|
@@ -1,63 +1,63 @@
|
|
|
1
1
|
@use '../../../scss/tools/index' as tools;
|
|
2
2
|
|
|
3
3
|
// ===================================================
|
|
4
|
-
// Component:
|
|
4
|
+
// Component: MapControls
|
|
5
5
|
// ===================================================
|
|
6
6
|
|
|
7
7
|
// 1. Base styles
|
|
8
|
-
.im-c-
|
|
8
|
+
.im-c-map-controls {
|
|
9
9
|
display: flex;
|
|
10
10
|
flex-direction: column;
|
|
11
11
|
align-items: center;
|
|
12
12
|
gap: var(--divider-gap);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
.im-c-
|
|
15
|
+
.im-c-map-controls.im-c-map-controls--collapsed {
|
|
16
16
|
display: none;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// 2. Elements
|
|
20
|
-
.im-c-
|
|
20
|
+
.im-c-map-controls__directions {
|
|
21
21
|
display: grid;
|
|
22
22
|
grid-template-columns: repeat(3, var(--button-size));
|
|
23
23
|
grid-template-rows: repeat(3, var(--button-size));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
.im-c-
|
|
26
|
+
.im-c-map-controls__directions .im-c-button-wrapper--pan-up {
|
|
27
27
|
grid-column: 2;
|
|
28
28
|
grid-row: 1;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
.im-c-
|
|
31
|
+
.im-c-map-controls__directions .im-c-button-wrapper--pan-left {
|
|
32
32
|
grid-column: 1;
|
|
33
33
|
grid-row: 2;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
.im-c-
|
|
36
|
+
.im-c-map-controls__directions .im-c-button-wrapper--pan-right {
|
|
37
37
|
grid-column: 3;
|
|
38
38
|
grid-row: 2;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
.im-c-
|
|
41
|
+
.im-c-map-controls__directions .im-c-button-wrapper--pan-down {
|
|
42
42
|
grid-column: 2;
|
|
43
43
|
grid-row: 3;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
.im-c-
|
|
46
|
+
.im-c-map-controls__directions .im-c-button-wrapper--nudge-step-toggle {
|
|
47
47
|
grid-column: 2;
|
|
48
48
|
grid-row: 2;
|
|
49
49
|
z-index: 1;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
.im-c-
|
|
52
|
+
.im-c-map-controls__directions .im-c-map-button--pan-up svg {
|
|
53
53
|
transform: rotate(180deg);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
.im-c-
|
|
56
|
+
.im-c-map-controls__directions .im-c-map-button--pan-left svg {
|
|
57
57
|
transform: rotate(90deg);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
.im-c-
|
|
60
|
+
.im-c-map-controls__directions .im-c-map-button--pan-right svg {
|
|
61
61
|
transform: rotate(-90deg);
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
// express a plus-shaped grid where the centre button touches all four neighbours —
|
|
68
68
|
// clip-path is set directly per button instead, clipping only the side(s) where it
|
|
69
69
|
// touches another button and leaving the exposed side(s) showing the shadow as normal.
|
|
70
|
-
.im-c-
|
|
70
|
+
.im-c-map-controls__directions .im-c-button-wrapper--pan-up .im-c-map-button {
|
|
71
71
|
border-bottom-left-radius: 0;
|
|
72
72
|
border-bottom-right-radius: 0;
|
|
73
73
|
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
.im-c-
|
|
79
|
+
.im-c-map-controls__directions .im-c-button-wrapper--pan-left .im-c-map-button {
|
|
80
80
|
border-top-right-radius: 0;
|
|
81
81
|
border-bottom-right-radius: 0;
|
|
82
82
|
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
.im-c-
|
|
88
|
+
.im-c-map-controls__directions .im-c-button-wrapper--pan-right .im-c-map-button {
|
|
89
89
|
border-top-left-radius: 0;
|
|
90
90
|
border-bottom-left-radius: 0;
|
|
91
91
|
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
.im-c-
|
|
97
|
+
.im-c-map-controls__directions .im-c-button-wrapper--pan-down .im-c-map-button {
|
|
98
98
|
border-top-left-radius: 0;
|
|
99
99
|
border-top-right-radius: 0;
|
|
100
100
|
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
.im-c-
|
|
106
|
+
.im-c-map-controls__directions .im-c-button-wrapper--nudge-step-toggle .im-c-map-button {
|
|
107
107
|
border-radius: 0;
|
|
108
108
|
outline: 1px solid var(--button-hover-color);
|
|
109
109
|
|
|
@@ -114,13 +114,13 @@
|
|
|
114
114
|
|
|
115
115
|
// Same inverted "pressed" treatment already used for actions-slot toggle buttons
|
|
116
116
|
// (e.g. draw's Snap toggle) — reused here so Nudge mode reads consistently active.
|
|
117
|
-
.im-c-
|
|
117
|
+
.im-c-map-controls .im-c-map-button[aria-pressed="true"] {
|
|
118
118
|
color: var(--pressed-button-foreground-color);
|
|
119
119
|
border-color: var(--pressed-button-border-color);
|
|
120
120
|
background-color: var(--pressed-button-background-color);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
// The trigger lives in the right-top slot, outside .im-c-
|
|
123
|
+
// The trigger lives in the right-top slot, outside .im-c-map-controls, so this is
|
|
124
124
|
// its own top-level rule. Same colour treatment as above, but keyed off
|
|
125
125
|
// aria-expanded rather than aria-pressed — the trigger is correctly a disclosure
|
|
126
126
|
// button (it reveals the control below), not a toggle button, so it doesn't carry
|
|
@@ -132,31 +132,31 @@
|
|
|
132
132
|
// hovering the expanded trigger button was a coin-flip decided by cascade order —
|
|
133
133
|
// one cssnano mergeRules reshuffle in production and hover started winning,
|
|
134
134
|
// showing the hover tint instead of the pressed colours. The sibling
|
|
135
|
-
// .im-c-
|
|
135
|
+
// .im-c-map-controls .im-c-map-button[aria-pressed="true"] rule above never had
|
|
136
136
|
// this problem (it's already (0,3,0), safely ahead of hover); this one needed the
|
|
137
137
|
// same headroom.
|
|
138
|
-
.im-c-map-button.im-c-map-button--
|
|
138
|
+
.im-c-map-button.im-c-map-button--map-controls[aria-expanded="true"] {
|
|
139
139
|
color: var(--pressed-button-foreground-color);
|
|
140
140
|
border-color: var(--pressed-button-border-color);
|
|
141
141
|
background-color: var(--pressed-button-background-color);
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
.im-c-
|
|
144
|
+
.im-c-map-controls__zoom {
|
|
145
145
|
display: flex;
|
|
146
146
|
flex-direction: row;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
// A simple two-item linear row, so the standard button-group corner/shadow treatment
|
|
150
150
|
// applies directly (matches .im-o-app__top's first/last-child pattern).
|
|
151
|
-
.im-c-
|
|
151
|
+
.im-c-map-controls__zoom .im-c-button-wrapper--nudge-zoom-in .im-c-map-button {
|
|
152
152
|
@include tools.border-focus-corner-override($corners: 'left');
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
.im-c-
|
|
155
|
+
.im-c-map-controls__zoom .im-c-button-wrapper--nudge-zoom-out .im-c-map-button {
|
|
156
156
|
@include tools.border-focus-corner-override($corners: 'right');
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
// App dark modifiers
|
|
160
|
-
.im-o-app--dark-app .im-c-
|
|
160
|
+
.im-o-app--dark-app .im-c-map-controls__directions .im-c-button-wrapper--nudge-step-toggle .im-c-map-button {
|
|
161
161
|
outline-offset: -1px;
|
|
162
162
|
}
|