@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { renderHook, act } from '@testing-library/react'
|
|
2
|
-
import {
|
|
2
|
+
import { useSpatialListFocus } from './useSpatialListFocus.js'
|
|
3
3
|
|
|
4
4
|
const ITEMS = [
|
|
5
5
|
{ id: 'a', label: 'Feature A' },
|
|
@@ -7,17 +7,17 @@ const ITEMS = [
|
|
|
7
7
|
{ id: 'c', label: 'Feature C' }
|
|
8
8
|
]
|
|
9
9
|
|
|
10
|
-
const SET_ACTIVE = 'map:
|
|
11
|
-
const SELECT = 'map:
|
|
10
|
+
const SET_ACTIVE = 'map:setactiveitem' // NOSONAR
|
|
11
|
+
const SELECT = 'map:selectitem'
|
|
12
12
|
const SELECTION_CHANGE = 'interact:selectionchange'
|
|
13
13
|
|
|
14
14
|
const makeEventBus = () => {
|
|
15
15
|
const listeners = {}
|
|
16
16
|
return {
|
|
17
|
-
on: jest.fn((
|
|
17
|
+
on: jest.fn((eventName, fn) => { listeners[eventName] = fn }),
|
|
18
18
|
off: jest.fn(),
|
|
19
19
|
emit: jest.fn(),
|
|
20
|
-
trigger: (
|
|
20
|
+
trigger: (eventName, payload) => listeners[eventName]?.(payload)
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -26,7 +26,7 @@ const makeRefs = ({ viewportFocus } = {}) => {
|
|
|
26
26
|
viewportEl.focus = viewportFocus ?? jest.fn()
|
|
27
27
|
return {
|
|
28
28
|
viewportRef: { current: viewportEl },
|
|
29
|
-
|
|
29
|
+
spatialListRef: { current: document.createElement('ul') },
|
|
30
30
|
hints: { subscribe: jest.fn(() => jest.fn()), dismiss: jest.fn() }
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -40,83 +40,83 @@ const fireKey = (el, key) => {
|
|
|
40
40
|
return event
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// ───
|
|
43
|
+
// ─── useSpatialListFocus — initial state ─────────────────────────────────────────
|
|
44
44
|
|
|
45
|
-
describe('
|
|
46
|
-
it('
|
|
47
|
-
const { result } = renderHook(() =>
|
|
48
|
-
expect(result.current.
|
|
45
|
+
describe('useSpatialListFocus — initial state', () => {
|
|
46
|
+
it('activeItemId starts as null', () => {
|
|
47
|
+
const { result } = renderHook(() => useSpatialListFocus(makeRefs()))
|
|
48
|
+
expect(result.current.activeItemId).toBeNull()
|
|
49
49
|
})
|
|
50
50
|
|
|
51
51
|
it('exposes onFocus function', () => {
|
|
52
|
-
const { result } = renderHook(() =>
|
|
52
|
+
const { result } = renderHook(() => useSpatialListFocus(makeRefs()))
|
|
53
53
|
expect(typeof result.current.onFocus).toBe('function')
|
|
54
54
|
})
|
|
55
55
|
|
|
56
56
|
it('exposes onBlur function', () => {
|
|
57
|
-
const { result } = renderHook(() =>
|
|
57
|
+
const { result } = renderHook(() => useSpatialListFocus(makeRefs()))
|
|
58
58
|
expect(typeof result.current.onBlur).toBe('function')
|
|
59
59
|
})
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
// ───
|
|
62
|
+
// ─── useSpatialListFocus — onFocus ────────────────────────────────────────────────
|
|
63
63
|
|
|
64
|
-
describe('
|
|
65
|
-
it('sets
|
|
66
|
-
const { result } = renderHook(() =>
|
|
64
|
+
describe('useSpatialListFocus — onFocus', () => {
|
|
65
|
+
it('sets activeItemId to first item on first focus (no previous, no selected)', () => {
|
|
66
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items: ITEMS }))
|
|
67
67
|
act(() => result.current.onFocus())
|
|
68
|
-
expect(result.current.
|
|
68
|
+
expect(result.current.activeItemId).toBe('a')
|
|
69
69
|
})
|
|
70
70
|
|
|
71
71
|
it('does nothing when items is empty', () => {
|
|
72
|
-
const { result } = renderHook(() =>
|
|
72
|
+
const { result } = renderHook(() => useSpatialListFocus(makeRefs()))
|
|
73
73
|
act(() => result.current.onFocus())
|
|
74
|
-
expect(result.current.
|
|
74
|
+
expect(result.current.activeItemId).toBeNull()
|
|
75
75
|
})
|
|
76
76
|
|
|
77
77
|
it('sets active to first selected item (highest priority)', () => {
|
|
78
78
|
const eb = makeEventBus()
|
|
79
|
-
const { result } = renderHook(() =>
|
|
79
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items: ITEMS, eventBus: eb }))
|
|
80
80
|
act(() => eb.trigger(SELECTION_CHANGE, { selectedFeatures: [{ featureId: 'b', layerId: 'roads' }] }))
|
|
81
81
|
act(() => result.current.onFocus())
|
|
82
|
-
expect(result.current.
|
|
82
|
+
expect(result.current.activeItemId).toBe('b')
|
|
83
83
|
})
|
|
84
84
|
|
|
85
85
|
it('selected item takes priority over last active position', () => {
|
|
86
86
|
const eb = makeEventBus()
|
|
87
87
|
const refs = makeRefs()
|
|
88
|
-
const el = refs.
|
|
88
|
+
const el = refs.spatialListRef.current
|
|
89
89
|
document.body.appendChild(el)
|
|
90
|
-
const { result, unmount } = renderHook(() =>
|
|
90
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS, eventBus: eb }))
|
|
91
91
|
act(() => result.current.onFocus())
|
|
92
92
|
fireKey(el, 'ArrowDown') // last active = 'b'
|
|
93
93
|
act(() => result.current.onBlur())
|
|
94
94
|
act(() => eb.trigger(SELECTION_CHANGE, { selectedFeatures: [{ featureId: 'c', layerId: 'roads' }] }))
|
|
95
95
|
act(() => result.current.onFocus())
|
|
96
|
-
expect(result.current.
|
|
96
|
+
expect(result.current.activeItemId).toBe('c') // selected beats last-active
|
|
97
97
|
unmount(); el.remove()
|
|
98
98
|
})
|
|
99
99
|
|
|
100
100
|
it('restores last active position when nothing is selected', () => {
|
|
101
101
|
const refs = makeRefs()
|
|
102
|
-
const el = refs.
|
|
102
|
+
const el = refs.spatialListRef.current
|
|
103
103
|
document.body.appendChild(el)
|
|
104
|
-
const { result, unmount } = renderHook(() =>
|
|
104
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
105
105
|
act(() => result.current.onFocus())
|
|
106
106
|
fireKey(el, 'ArrowDown') // last active = 'b'
|
|
107
107
|
act(() => result.current.onBlur())
|
|
108
108
|
act(() => result.current.onFocus())
|
|
109
|
-
expect(result.current.
|
|
109
|
+
expect(result.current.activeItemId).toBe('b')
|
|
110
110
|
unmount(); el.remove()
|
|
111
111
|
})
|
|
112
112
|
|
|
113
113
|
it('falls back to first item when previous active is no longer in the list', () => {
|
|
114
114
|
const eb = makeEventBus()
|
|
115
115
|
const refs = makeRefs()
|
|
116
|
-
const el = refs.
|
|
116
|
+
const el = refs.spatialListRef.current
|
|
117
117
|
document.body.appendChild(el)
|
|
118
118
|
const { result, rerender, unmount } = renderHook(
|
|
119
|
-
({ items }) =>
|
|
119
|
+
({ items }) => useSpatialListFocus({ ...refs, items, eventBus: eb }),
|
|
120
120
|
{ initialProps: { items: ITEMS } }
|
|
121
121
|
)
|
|
122
122
|
act(() => result.current.onFocus())
|
|
@@ -125,25 +125,52 @@ describe('useFeatureFocus — onFocus', () => {
|
|
|
125
125
|
const NEW_ITEMS = [{ id: 'x', label: 'X' }, { id: 'y', label: 'Y' }]
|
|
126
126
|
rerender({ items: NEW_ITEMS })
|
|
127
127
|
act(() => result.current.onFocus())
|
|
128
|
-
expect(result.current.
|
|
128
|
+
expect(result.current.activeItemId).toBe('x')
|
|
129
129
|
unmount(); el.remove()
|
|
130
130
|
})
|
|
131
|
+
|
|
132
|
+
it('prefers the item nearest the map center over the structurally-first one', () => {
|
|
133
|
+
const items = [
|
|
134
|
+
{ id: 'a', label: 'A', x: 0, y: 0 },
|
|
135
|
+
{ id: 'b', label: 'B', x: 100, y: 100 },
|
|
136
|
+
{ id: 'c', label: 'C', x: 500, y: 500 }
|
|
137
|
+
]
|
|
138
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items, centerScreenPoint: { x: 90, y: 90 } }))
|
|
139
|
+
act(() => result.current.onFocus())
|
|
140
|
+
expect(result.current.activeItemId).toBe('b')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('falls back to the first item when no centerScreenPoint is given, even with positioned items', () => {
|
|
144
|
+
const items = [
|
|
145
|
+
{ id: 'a', label: 'A', x: 0, y: 0 },
|
|
146
|
+
{ id: 'b', label: 'B', x: 100, y: 100 }
|
|
147
|
+
]
|
|
148
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items }))
|
|
149
|
+
act(() => result.current.onFocus())
|
|
150
|
+
expect(result.current.activeItemId).toBe('a')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('falls back to the first item when centerScreenPoint is given but no item carries a screen position', () => {
|
|
154
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items: ITEMS, centerScreenPoint: { x: 90, y: 90 } }))
|
|
155
|
+
act(() => result.current.onFocus())
|
|
156
|
+
expect(result.current.activeItemId).toBe('a')
|
|
157
|
+
})
|
|
131
158
|
})
|
|
132
159
|
|
|
133
|
-
// ───
|
|
160
|
+
// ─── useSpatialListFocus — onBlur ─────────────────────────────────────────────────
|
|
134
161
|
|
|
135
|
-
describe('
|
|
136
|
-
it('clears
|
|
137
|
-
const { result } = renderHook(() =>
|
|
162
|
+
describe('useSpatialListFocus — onBlur', () => {
|
|
163
|
+
it('clears activeItemId when focus leaves the listbox', () => {
|
|
164
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items: ITEMS }))
|
|
138
165
|
act(() => result.current.onFocus())
|
|
139
|
-
expect(result.current.
|
|
166
|
+
expect(result.current.activeItemId).toBe('a')
|
|
140
167
|
act(() => result.current.onBlur())
|
|
141
|
-
expect(result.current.
|
|
168
|
+
expect(result.current.activeItemId).toBeNull()
|
|
142
169
|
})
|
|
143
170
|
|
|
144
171
|
it('emits map:setactivefeature with null on blur', () => {
|
|
145
172
|
const eb = makeEventBus()
|
|
146
|
-
const { result } = renderHook(() =>
|
|
173
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items: ITEMS, eventBus: eb }))
|
|
147
174
|
act(() => result.current.onFocus())
|
|
148
175
|
eb.emit.mockClear()
|
|
149
176
|
act(() => result.current.onBlur())
|
|
@@ -151,7 +178,7 @@ describe('useFeatureFocus — onBlur', () => {
|
|
|
151
178
|
})
|
|
152
179
|
|
|
153
180
|
it('does not throw when eventBus is not provided', () => {
|
|
154
|
-
const { result } = renderHook(() =>
|
|
181
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items: ITEMS }))
|
|
155
182
|
expect(() => act(() => result.current.onBlur())).not.toThrow()
|
|
156
183
|
})
|
|
157
184
|
|
|
@@ -159,63 +186,63 @@ describe('useFeatureFocus — onBlur', () => {
|
|
|
159
186
|
// driven by an internal isInternalFocusMoveRef, set only while focusOption()'s own .focus()
|
|
160
187
|
// call is synchronously in flight — not observable by calling onFocus/onBlur directly here,
|
|
161
188
|
// since that requires a real DOM focus transition bubbling through React. See the
|
|
162
|
-
// "
|
|
189
|
+
// "SpatialList + useSpatialListFocus — roving tabindex real focus events" suite in SpatialList.test.jsx,
|
|
163
190
|
// which renders the real markup and is the only level this can be faithfully exercised at
|
|
164
191
|
// (and is where the regression this guards against was actually found).
|
|
165
192
|
})
|
|
166
193
|
|
|
167
|
-
// ───
|
|
194
|
+
// ─── useSpatialListFocus — keydown listener lifecycle ────────────────────────────
|
|
168
195
|
|
|
169
|
-
describe('
|
|
170
|
-
it('does not attach listener when
|
|
196
|
+
describe('useSpatialListFocus — keydown listener lifecycle', () => {
|
|
197
|
+
it('does not attach listener when spatialListRef.current is null', () => {
|
|
171
198
|
const refs = makeRefs()
|
|
172
|
-
refs.
|
|
199
|
+
refs.spatialListRef = { current: null }
|
|
173
200
|
const spy = jest.spyOn(document.body, 'addEventListener')
|
|
174
|
-
renderHook(() =>
|
|
201
|
+
renderHook(() => useSpatialListFocus(refs))
|
|
175
202
|
expect(spy).not.toHaveBeenCalledWith('keydown', expect.any(Function))
|
|
176
203
|
spy.mockRestore()
|
|
177
204
|
})
|
|
178
205
|
|
|
179
206
|
it('attaches and removes keydown listener on the features element', () => {
|
|
180
207
|
const refs = makeRefs()
|
|
181
|
-
const el = refs.
|
|
208
|
+
const el = refs.spatialListRef.current
|
|
182
209
|
const addSpy = jest.spyOn(el, 'addEventListener')
|
|
183
210
|
const removeSpy = jest.spyOn(el, 'removeEventListener')
|
|
184
|
-
const { unmount } = renderHook(() =>
|
|
211
|
+
const { unmount } = renderHook(() => useSpatialListFocus(refs))
|
|
185
212
|
expect(addSpy).toHaveBeenCalledWith('keydown', expect.any(Function))
|
|
186
213
|
unmount()
|
|
187
214
|
expect(removeSpy).toHaveBeenCalledWith('keydown', expect.any(Function))
|
|
188
215
|
})
|
|
189
216
|
})
|
|
190
217
|
|
|
191
|
-
// ───
|
|
218
|
+
// ─── useSpatialListFocus — unhandled keys ────────────────────────────────────────
|
|
192
219
|
|
|
193
|
-
describe('
|
|
220
|
+
describe('useSpatialListFocus — unhandled keys', () => {
|
|
194
221
|
it('does nothing for keys that are not Escape or Arrow', () => {
|
|
195
222
|
const viewportFocus = jest.fn()
|
|
196
223
|
const refs = makeRefs({ viewportFocus })
|
|
197
|
-
const el = refs.
|
|
224
|
+
const el = refs.spatialListRef.current
|
|
198
225
|
document.body.appendChild(el)
|
|
199
|
-
const { result } = renderHook(() =>
|
|
226
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
200
227
|
fireKey(el, 'Tab')
|
|
201
|
-
expect(result.current.
|
|
228
|
+
expect(result.current.activeItemId).toBeNull()
|
|
202
229
|
expect(viewportFocus).not.toHaveBeenCalled()
|
|
203
230
|
el.remove()
|
|
204
231
|
})
|
|
205
232
|
})
|
|
206
233
|
|
|
207
|
-
// ───
|
|
234
|
+
// ─── useSpatialListFocus — Escape key ────────────────────────────────────────────
|
|
208
235
|
|
|
209
|
-
describe('
|
|
236
|
+
describe('useSpatialListFocus — Escape key', () => {
|
|
210
237
|
it('dismisses hint on Escape when hint is visible', () => {
|
|
211
238
|
const refs = makeRefs()
|
|
212
239
|
refs.hints.subscribe.mockImplementation((fn) => {
|
|
213
240
|
fn({ html: 'test' })
|
|
214
241
|
return jest.fn()
|
|
215
242
|
})
|
|
216
|
-
const el = refs.
|
|
243
|
+
const el = refs.spatialListRef.current
|
|
217
244
|
document.body.appendChild(el)
|
|
218
|
-
const { result } = renderHook(() =>
|
|
245
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
219
246
|
act(() => result.current.onFocus())
|
|
220
247
|
fireKey(el, 'Escape')
|
|
221
248
|
expect(refs.hints.dismiss).toHaveBeenCalled()
|
|
@@ -226,9 +253,9 @@ describe('useFeatureFocus — Escape key', () => {
|
|
|
226
253
|
it('focuses the viewport on Escape when no hint is visible', () => {
|
|
227
254
|
const viewportFocus = jest.fn()
|
|
228
255
|
const refs = makeRefs({ viewportFocus })
|
|
229
|
-
const el = refs.
|
|
256
|
+
const el = refs.spatialListRef.current
|
|
230
257
|
document.body.appendChild(el)
|
|
231
|
-
const { result } = renderHook(() =>
|
|
258
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
232
259
|
act(() => result.current.onFocus())
|
|
233
260
|
fireKey(el, 'Escape')
|
|
234
261
|
expect(refs.hints.dismiss).not.toHaveBeenCalled()
|
|
@@ -237,19 +264,19 @@ describe('useFeatureFocus — Escape key', () => {
|
|
|
237
264
|
})
|
|
238
265
|
|
|
239
266
|
it('does not throw when viewportRef.current is null', () => {
|
|
240
|
-
const refs = { viewportRef: { current: null },
|
|
241
|
-
const el = refs.
|
|
267
|
+
const refs = { viewportRef: { current: null }, spatialListRef: { current: document.createElement('ul') }, hints: { subscribe: jest.fn(() => jest.fn()), dismiss: jest.fn() } }
|
|
268
|
+
const el = refs.spatialListRef.current
|
|
242
269
|
document.body.appendChild(el)
|
|
243
|
-
renderHook(() =>
|
|
270
|
+
renderHook(() => useSpatialListFocus(refs))
|
|
244
271
|
expect(() => fireKey(el, 'Escape')).not.toThrow()
|
|
245
272
|
el.remove()
|
|
246
273
|
})
|
|
247
274
|
|
|
248
275
|
it('stops propagation on Escape so the viewport keyboard handler does not also handle it', () => {
|
|
249
276
|
const refs = makeRefs()
|
|
250
|
-
const el = refs.
|
|
277
|
+
const el = refs.spatialListRef.current
|
|
251
278
|
document.body.appendChild(el)
|
|
252
|
-
renderHook(() =>
|
|
279
|
+
renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
253
280
|
const event = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })
|
|
254
281
|
const stopSpy = jest.spyOn(event, 'stopPropagation')
|
|
255
282
|
act(() => el.dispatchEvent(event))
|
|
@@ -258,21 +285,21 @@ describe('useFeatureFocus — Escape key', () => {
|
|
|
258
285
|
})
|
|
259
286
|
})
|
|
260
287
|
|
|
261
|
-
// ───
|
|
288
|
+
// ─── useSpatialListFocus — ArrowDown navigation ───────────────────────────────────
|
|
262
289
|
|
|
263
|
-
describe('
|
|
290
|
+
describe('useSpatialListFocus — ArrowDown navigation', () => {
|
|
264
291
|
const setup = () => {
|
|
265
292
|
const refs = makeRefs()
|
|
266
|
-
const el = refs.
|
|
293
|
+
const el = refs.spatialListRef.current
|
|
267
294
|
document.body.appendChild(el)
|
|
268
|
-
const { result, unmount } = renderHook(() =>
|
|
295
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
269
296
|
return { result, el, unmount }
|
|
270
297
|
}
|
|
271
298
|
|
|
272
299
|
it('selects first item when no item is active', () => {
|
|
273
300
|
const { result, el, unmount } = setup()
|
|
274
301
|
fireKey(el, 'ArrowDown')
|
|
275
|
-
expect(result.current.
|
|
302
|
+
expect(result.current.activeItemId).toBe('a')
|
|
276
303
|
unmount(); el.remove()
|
|
277
304
|
})
|
|
278
305
|
|
|
@@ -280,30 +307,30 @@ describe('useFeatureFocus — ArrowDown navigation', () => {
|
|
|
280
307
|
const { result, el, unmount } = setup()
|
|
281
308
|
act(() => result.current.onFocus())
|
|
282
309
|
fireKey(el, 'ArrowDown')
|
|
283
|
-
expect(result.current.
|
|
310
|
+
expect(result.current.activeItemId).toBe('b')
|
|
284
311
|
unmount(); el.remove()
|
|
285
312
|
})
|
|
286
313
|
|
|
287
314
|
it('clamps at last item', () => {
|
|
288
315
|
const refs = makeRefs()
|
|
289
|
-
const el = refs.
|
|
316
|
+
const el = refs.spatialListRef.current
|
|
290
317
|
document.body.appendChild(el)
|
|
291
|
-
const { result, unmount } = renderHook(() =>
|
|
318
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
292
319
|
act(() => result.current.onFocus())
|
|
293
320
|
fireKey(el, 'ArrowDown')
|
|
294
321
|
fireKey(el, 'ArrowDown')
|
|
295
322
|
fireKey(el, 'ArrowDown')
|
|
296
|
-
expect(result.current.
|
|
323
|
+
expect(result.current.activeItemId).toBe('c')
|
|
297
324
|
unmount(); el.remove()
|
|
298
325
|
})
|
|
299
326
|
|
|
300
327
|
it('does nothing when items is empty', () => {
|
|
301
328
|
const refs = makeRefs()
|
|
302
|
-
const el = refs.
|
|
329
|
+
const el = refs.spatialListRef.current
|
|
303
330
|
document.body.appendChild(el)
|
|
304
|
-
const { result, unmount } = renderHook(() =>
|
|
331
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus(refs))
|
|
305
332
|
fireKey(el, 'ArrowDown')
|
|
306
|
-
expect(result.current.
|
|
333
|
+
expect(result.current.activeItemId).toBeNull()
|
|
307
334
|
unmount(); el.remove()
|
|
308
335
|
})
|
|
309
336
|
|
|
@@ -318,52 +345,171 @@ describe('useFeatureFocus — ArrowDown navigation', () => {
|
|
|
318
345
|
})
|
|
319
346
|
})
|
|
320
347
|
|
|
321
|
-
// ───
|
|
348
|
+
// ─── useSpatialListFocus — ArrowUp navigation ────────────────────────────────────
|
|
322
349
|
|
|
323
|
-
describe('
|
|
350
|
+
describe('useSpatialListFocus — ArrowUp navigation', () => {
|
|
324
351
|
it('selects last item when no item is active', () => {
|
|
325
352
|
const refs = makeRefs()
|
|
326
|
-
const el = refs.
|
|
353
|
+
const el = refs.spatialListRef.current
|
|
327
354
|
document.body.appendChild(el)
|
|
328
|
-
const { result, unmount } = renderHook(() =>
|
|
355
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
329
356
|
fireKey(el, 'ArrowUp')
|
|
330
|
-
expect(result.current.
|
|
357
|
+
expect(result.current.activeItemId).toBe('c')
|
|
331
358
|
unmount(); el.remove()
|
|
332
359
|
})
|
|
333
360
|
|
|
334
361
|
it('moves to previous item', () => {
|
|
335
362
|
const refs = makeRefs()
|
|
336
|
-
const el = refs.
|
|
363
|
+
const el = refs.spatialListRef.current
|
|
337
364
|
document.body.appendChild(el)
|
|
338
|
-
const { result, unmount } = renderHook(() =>
|
|
365
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
339
366
|
act(() => result.current.onFocus())
|
|
340
367
|
fireKey(el, 'ArrowDown')
|
|
341
368
|
fireKey(el, 'ArrowUp')
|
|
342
|
-
expect(result.current.
|
|
369
|
+
expect(result.current.activeItemId).toBe('a')
|
|
343
370
|
unmount(); el.remove()
|
|
344
371
|
})
|
|
345
372
|
|
|
346
373
|
it('clamps at first item', () => {
|
|
347
374
|
const refs = makeRefs()
|
|
348
|
-
const el = refs.
|
|
375
|
+
const el = refs.spatialListRef.current
|
|
349
376
|
document.body.appendChild(el)
|
|
350
|
-
const { result, unmount } = renderHook(() =>
|
|
377
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
351
378
|
act(() => result.current.onFocus())
|
|
352
379
|
fireKey(el, 'ArrowUp')
|
|
353
380
|
fireKey(el, 'ArrowUp')
|
|
354
|
-
expect(result.current.
|
|
381
|
+
expect(result.current.activeItemId).toBe('a')
|
|
355
382
|
unmount(); el.remove()
|
|
356
383
|
})
|
|
357
384
|
})
|
|
358
385
|
|
|
359
|
-
// ───
|
|
386
|
+
// ─── useSpatialListFocus — Alt+Arrow spatial navigation ──────────────────────────
|
|
387
|
+
|
|
388
|
+
describe('useSpatialListFocus — Alt+Arrow spatial navigation', () => {
|
|
389
|
+
const POSITIONED_ITEMS = [
|
|
390
|
+
{ id: 'center', label: 'Center', x: 0, y: 0 },
|
|
391
|
+
{ id: 'up', label: 'Up', x: 0, y: -10 },
|
|
392
|
+
{ id: 'right', label: 'Right', x: 10, y: 0 }
|
|
393
|
+
]
|
|
360
394
|
|
|
361
|
-
|
|
395
|
+
const setup = (items = POSITIONED_ITEMS) => {
|
|
396
|
+
const refs = makeRefs()
|
|
397
|
+
const el = refs.spatialListRef.current
|
|
398
|
+
document.body.appendChild(el)
|
|
399
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus({ ...refs, items }))
|
|
400
|
+
return { result, el, unmount }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const fireAltKey = (el, key, type = 'keydown') => {
|
|
404
|
+
let event
|
|
405
|
+
act(() => {
|
|
406
|
+
event = new KeyboardEvent(type, { key, altKey: true, bubbles: true, cancelable: true })
|
|
407
|
+
el.dispatchEvent(event)
|
|
408
|
+
})
|
|
409
|
+
return event
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
it('moves to the item spatially nearest in the pressed direction, not the next item in list order', () => {
|
|
413
|
+
const { result, el, unmount } = setup()
|
|
414
|
+
act(() => result.current.onFocus()) // resolves to 'center', the first item
|
|
415
|
+
fireAltKey(el, 'ArrowRight')
|
|
416
|
+
expect(result.current.activeItemId).toBe('right')
|
|
417
|
+
unmount(); el.remove()
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
it('emits map:setactivefeature for the spatially-found item', () => {
|
|
421
|
+
const refs = makeRefs()
|
|
422
|
+
const el = refs.spatialListRef.current
|
|
423
|
+
document.body.appendChild(el)
|
|
424
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus({ ...refs, items: POSITIONED_ITEMS, eventBus: makeEventBus() }))
|
|
425
|
+
act(() => result.current.onFocus())
|
|
426
|
+
fireAltKey(el, 'ArrowUp')
|
|
427
|
+
expect(result.current.activeItemId).toBe('up')
|
|
428
|
+
unmount(); el.remove()
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it('prevents the browser default (e.g. Alt+Arrow history navigation) on keydown', () => {
|
|
432
|
+
const { result, el, unmount } = setup()
|
|
433
|
+
act(() => result.current.onFocus())
|
|
434
|
+
let event
|
|
435
|
+
act(() => {
|
|
436
|
+
event = new KeyboardEvent('keydown', { key: 'ArrowRight', altKey: true, bubbles: true, cancelable: true })
|
|
437
|
+
})
|
|
438
|
+
const preventSpy = jest.spyOn(event, 'preventDefault')
|
|
439
|
+
act(() => el.dispatchEvent(event))
|
|
440
|
+
expect(preventSpy).toHaveBeenCalled()
|
|
441
|
+
unmount(); el.remove()
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
it('stops the matching keyup from bubbling to the viewport label-navigation binding on a shared ancestor', () => {
|
|
445
|
+
const { result, el, unmount } = setup()
|
|
446
|
+
act(() => result.current.onFocus())
|
|
447
|
+
let event
|
|
448
|
+
act(() => {
|
|
449
|
+
event = new KeyboardEvent('keyup', { key: 'ArrowRight', altKey: true, bubbles: true, cancelable: true })
|
|
450
|
+
})
|
|
451
|
+
const stopSpy = jest.spyOn(event, 'stopPropagation')
|
|
452
|
+
act(() => el.dispatchEvent(event))
|
|
453
|
+
expect(stopSpy).toHaveBeenCalled()
|
|
454
|
+
unmount(); el.remove()
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('leaves a plain (non-Alt) arrow keyup alone — sequential navigation has already handled it on keydown', () => {
|
|
458
|
+
const { result, el, unmount } = setup()
|
|
459
|
+
act(() => result.current.onFocus())
|
|
460
|
+
let event
|
|
461
|
+
act(() => {
|
|
462
|
+
event = new KeyboardEvent('keyup', { key: 'ArrowRight', altKey: false, bubbles: true, cancelable: true })
|
|
463
|
+
})
|
|
464
|
+
const stopSpy = jest.spyOn(event, 'stopPropagation')
|
|
465
|
+
act(() => el.dispatchEvent(event))
|
|
466
|
+
expect(stopSpy).not.toHaveBeenCalled()
|
|
467
|
+
unmount(); el.remove()
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
it('stops an Alt+Enter keyup from bubbling to the viewport center-label binding on a shared ancestor', () => {
|
|
471
|
+
const { result, el, unmount } = setup()
|
|
472
|
+
act(() => result.current.onFocus())
|
|
473
|
+
let event
|
|
474
|
+
act(() => {
|
|
475
|
+
event = new KeyboardEvent('keyup', { key: 'Enter', altKey: true, bubbles: true, cancelable: true })
|
|
476
|
+
})
|
|
477
|
+
const stopSpy = jest.spyOn(event, 'stopPropagation')
|
|
478
|
+
act(() => el.dispatchEvent(event))
|
|
479
|
+
expect(stopSpy).toHaveBeenCalled()
|
|
480
|
+
unmount(); el.remove()
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
it('leaves an Alt+<other> keyup alone — nothing else uses this modifier here', () => {
|
|
484
|
+
const { result, el, unmount } = setup()
|
|
485
|
+
act(() => result.current.onFocus())
|
|
486
|
+
let event
|
|
487
|
+
act(() => {
|
|
488
|
+
event = new KeyboardEvent('keyup', { key: 'i', altKey: true, bubbles: true, cancelable: true })
|
|
489
|
+
})
|
|
490
|
+
const stopSpy = jest.spyOn(event, 'stopPropagation')
|
|
491
|
+
act(() => el.dispatchEvent(event))
|
|
492
|
+
expect(stopSpy).not.toHaveBeenCalled()
|
|
493
|
+
unmount(); el.remove()
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it('is a no-op when no item lies in the pressed direction — stays on the current item', () => {
|
|
497
|
+
const { result, el, unmount } = setup([{ id: 'only', label: 'Only', x: 0, y: 0 }])
|
|
498
|
+
act(() => result.current.onFocus())
|
|
499
|
+
fireAltKey(el, 'ArrowRight')
|
|
500
|
+
expect(result.current.activeItemId).toBe('only')
|
|
501
|
+
unmount(); el.remove()
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
// ─── useSpatialListFocus — Home/End navigation ───────────────────────────────────
|
|
506
|
+
|
|
507
|
+
describe('useSpatialListFocus — Home/End navigation', () => {
|
|
362
508
|
const setup = () => {
|
|
363
509
|
const refs = makeRefs()
|
|
364
|
-
const el = refs.
|
|
510
|
+
const el = refs.spatialListRef.current
|
|
365
511
|
document.body.appendChild(el)
|
|
366
|
-
const { result, unmount } = renderHook(() =>
|
|
512
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
367
513
|
return { result, el, unmount }
|
|
368
514
|
}
|
|
369
515
|
|
|
@@ -372,7 +518,7 @@ describe('useFeatureFocus — Home/End navigation', () => {
|
|
|
372
518
|
act(() => result.current.onFocus())
|
|
373
519
|
fireKey(el, 'ArrowDown')
|
|
374
520
|
fireKey(el, 'Home')
|
|
375
|
-
expect(result.current.
|
|
521
|
+
expect(result.current.activeItemId).toBe('a')
|
|
376
522
|
unmount(); el.remove()
|
|
377
523
|
})
|
|
378
524
|
|
|
@@ -380,18 +526,18 @@ describe('useFeatureFocus — Home/End navigation', () => {
|
|
|
380
526
|
const { result, el, unmount } = setup()
|
|
381
527
|
act(() => result.current.onFocus())
|
|
382
528
|
fireKey(el, 'End')
|
|
383
|
-
expect(result.current.
|
|
529
|
+
expect(result.current.activeItemId).toBe('c')
|
|
384
530
|
unmount(); el.remove()
|
|
385
531
|
})
|
|
386
532
|
|
|
387
533
|
it('does nothing when items is empty', () => {
|
|
388
534
|
const refs = makeRefs()
|
|
389
|
-
const el = refs.
|
|
535
|
+
const el = refs.spatialListRef.current
|
|
390
536
|
document.body.appendChild(el)
|
|
391
|
-
const { result, unmount } = renderHook(() =>
|
|
537
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus(refs))
|
|
392
538
|
fireKey(el, 'Home')
|
|
393
539
|
fireKey(el, 'End')
|
|
394
|
-
expect(result.current.
|
|
540
|
+
expect(result.current.activeItemId).toBeNull()
|
|
395
541
|
unmount(); el.remove()
|
|
396
542
|
})
|
|
397
543
|
|
|
@@ -409,9 +555,9 @@ describe('useFeatureFocus — Home/End navigation', () => {
|
|
|
409
555
|
it('emits map:setactivefeature with the boundary id', () => {
|
|
410
556
|
const eb = makeEventBus()
|
|
411
557
|
const refs = makeRefs()
|
|
412
|
-
const el = refs.
|
|
558
|
+
const el = refs.spatialListRef.current
|
|
413
559
|
document.body.appendChild(el)
|
|
414
|
-
const { result, unmount } = renderHook(() =>
|
|
560
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS, eventBus: eb }))
|
|
415
561
|
act(() => result.current.onFocus())
|
|
416
562
|
eb.emit.mockClear()
|
|
417
563
|
fireKey(el, 'End')
|
|
@@ -420,43 +566,43 @@ describe('useFeatureFocus — Home/End navigation', () => {
|
|
|
420
566
|
})
|
|
421
567
|
})
|
|
422
568
|
|
|
423
|
-
// ───
|
|
569
|
+
// ─── useSpatialListFocus — eventBus integration ───────────────────────────────────
|
|
424
570
|
|
|
425
|
-
describe('
|
|
571
|
+
describe('useSpatialListFocus — eventBus: map:setactivefeature listener', () => {
|
|
426
572
|
it('subscribes to map:setactivefeature on mount and unsubscribes on unmount', () => {
|
|
427
573
|
const eb = makeEventBus()
|
|
428
|
-
const { unmount } = renderHook(() =>
|
|
574
|
+
const { unmount } = renderHook(() => useSpatialListFocus({ ...makeRefs(), eventBus: eb }))
|
|
429
575
|
expect(eb.on).toHaveBeenCalledWith(SET_ACTIVE, expect.any(Function))
|
|
430
576
|
unmount()
|
|
431
577
|
expect(eb.off).toHaveBeenCalledWith(SET_ACTIVE, expect.any(Function))
|
|
432
578
|
})
|
|
433
579
|
|
|
434
|
-
it('updates
|
|
580
|
+
it('updates activeItemId when map:setactivefeature is received', () => {
|
|
435
581
|
const eb = makeEventBus()
|
|
436
|
-
const { result } = renderHook(() =>
|
|
582
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), eventBus: eb }))
|
|
437
583
|
act(() => eb.trigger(SET_ACTIVE, { id: 'a' }))
|
|
438
|
-
expect(result.current.
|
|
584
|
+
expect(result.current.activeItemId).toBe('a')
|
|
439
585
|
})
|
|
440
586
|
|
|
441
|
-
it('clears
|
|
587
|
+
it('clears activeItemId when map:setactivefeature is received with null', () => {
|
|
442
588
|
const eb = makeEventBus()
|
|
443
|
-
const { result } = renderHook(() =>
|
|
589
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items: ITEMS, eventBus: eb }))
|
|
444
590
|
act(() => eb.trigger(SET_ACTIVE, { id: 'a' }))
|
|
445
591
|
act(() => eb.trigger(SET_ACTIVE, { id: null }))
|
|
446
|
-
expect(result.current.
|
|
592
|
+
expect(result.current.activeItemId).toBeNull()
|
|
447
593
|
})
|
|
448
594
|
})
|
|
449
595
|
|
|
450
596
|
const setupWithBus = () => {
|
|
451
597
|
const eb = makeEventBus()
|
|
452
598
|
const refs = makeRefs()
|
|
453
|
-
const el = refs.
|
|
599
|
+
const el = refs.spatialListRef.current
|
|
454
600
|
document.body.appendChild(el)
|
|
455
|
-
const { result, unmount } = renderHook(() =>
|
|
601
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS, eventBus: eb }))
|
|
456
602
|
return { eb, el, result, unmount }
|
|
457
603
|
}
|
|
458
604
|
|
|
459
|
-
describe('
|
|
605
|
+
describe('useSpatialListFocus — eventBus: map:setactivefeature emit', () => {
|
|
460
606
|
afterEach(() => { document.body.innerHTML = '' })
|
|
461
607
|
|
|
462
608
|
it('emits map:setactivefeature with first item id on onFocus', () => {
|
|
@@ -476,9 +622,9 @@ describe('useFeatureFocus — eventBus: map:setactivefeature emit', () => {
|
|
|
476
622
|
|
|
477
623
|
it('does not emit when eventBus is not provided', () => {
|
|
478
624
|
const refs = makeRefs()
|
|
479
|
-
const el = refs.
|
|
625
|
+
const el = refs.spatialListRef.current
|
|
480
626
|
document.body.appendChild(el)
|
|
481
|
-
const { result, unmount } = renderHook(() =>
|
|
627
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
482
628
|
expect(() => {
|
|
483
629
|
act(() => result.current.onFocus())
|
|
484
630
|
fireKey(el, 'ArrowDown')
|
|
@@ -487,12 +633,12 @@ describe('useFeatureFocus — eventBus: map:setactivefeature emit', () => {
|
|
|
487
633
|
})
|
|
488
634
|
})
|
|
489
635
|
|
|
490
|
-
// ───
|
|
636
|
+
// ─── useSpatialListFocus — interact:selectionchange listener ─────────────────────
|
|
491
637
|
|
|
492
|
-
describe('
|
|
638
|
+
describe('useSpatialListFocus — interact:selectionchange listener', () => {
|
|
493
639
|
it('subscribes to interact:selectionchange on mount and unsubscribes on unmount', () => {
|
|
494
640
|
const eb = makeEventBus()
|
|
495
|
-
const { unmount } = renderHook(() =>
|
|
641
|
+
const { unmount } = renderHook(() => useSpatialListFocus({ ...makeRefs(), eventBus: eb }))
|
|
496
642
|
expect(eb.on).toHaveBeenCalledWith(SELECTION_CHANGE, expect.any(Function))
|
|
497
643
|
unmount()
|
|
498
644
|
expect(eb.off).toHaveBeenCalledWith(SELECTION_CHANGE, expect.any(Function))
|
|
@@ -500,51 +646,51 @@ describe('useFeatureFocus — interact:selectionchange listener', () => {
|
|
|
500
646
|
|
|
501
647
|
it('sets selectedIds from selected features', () => {
|
|
502
648
|
const eb = makeEventBus()
|
|
503
|
-
const { result } = renderHook(() =>
|
|
649
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), eventBus: eb }))
|
|
504
650
|
act(() => eb.trigger(SELECTION_CHANGE, { selectedFeatures: [{ featureId: 27665979, layerId: 'hedges' }] }))
|
|
505
651
|
expect(result.current.selectedIds).toEqual(['27665979'])
|
|
506
652
|
})
|
|
507
653
|
|
|
508
654
|
it('sets selectedIds from multiple selected features', () => {
|
|
509
655
|
const eb = makeEventBus()
|
|
510
|
-
const { result } = renderHook(() =>
|
|
656
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), eventBus: eb }))
|
|
511
657
|
act(() => eb.trigger(SELECTION_CHANGE, { selectedFeatures: [{ featureId: 'f1', layerId: 'roads' }, { featureId: 'f2', layerId: 'roads' }] }))
|
|
512
658
|
expect(result.current.selectedIds).toEqual(['f1', 'f2'])
|
|
513
659
|
})
|
|
514
660
|
|
|
515
661
|
it('sets selectedIds from selected markers', () => {
|
|
516
662
|
const eb = makeEventBus()
|
|
517
|
-
const { result } = renderHook(() =>
|
|
663
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), eventBus: eb }))
|
|
518
664
|
act(() => eb.trigger(SELECTION_CHANGE, { selectedMarkers: ['m1', 'm2'] }))
|
|
519
665
|
expect(result.current.selectedIds).toEqual(['m1', 'm2'])
|
|
520
666
|
})
|
|
521
667
|
|
|
522
668
|
it('sets selectedIds from both features and markers when both are selected', () => {
|
|
523
669
|
const eb = makeEventBus()
|
|
524
|
-
const { result } = renderHook(() =>
|
|
670
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), eventBus: eb }))
|
|
525
671
|
act(() => eb.trigger(SELECTION_CHANGE, { selectedFeatures: [{ featureId: 'f1', layerId: 'roads' }], selectedMarkers: ['m1'] }))
|
|
526
672
|
expect(result.current.selectedIds).toEqual(['f1', 'm1'])
|
|
527
673
|
})
|
|
528
674
|
|
|
529
675
|
it('clears selectedIds when selection becomes empty', () => {
|
|
530
676
|
const eb = makeEventBus()
|
|
531
|
-
const { result } = renderHook(() =>
|
|
677
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), eventBus: eb }))
|
|
532
678
|
act(() => eb.trigger(SELECTION_CHANGE, { selectedFeatures: [{ featureId: 'f1', layerId: 'roads' }] }))
|
|
533
679
|
act(() => eb.trigger(SELECTION_CHANGE, { selectedFeatures: [], selectedMarkers: [] }))
|
|
534
680
|
expect(result.current.selectedIds).toEqual([])
|
|
535
681
|
})
|
|
536
682
|
|
|
537
|
-
it('does not move
|
|
683
|
+
it('does not move activeItemId when selectionchange fires (cursor is keyboard-only)', () => {
|
|
538
684
|
const eb = makeEventBus()
|
|
539
|
-
const { result } = renderHook(() =>
|
|
685
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items: ITEMS, eventBus: eb }))
|
|
540
686
|
act(() => eb.trigger(SELECTION_CHANGE, { selectedFeatures: [{ featureId: 'a', layerId: 'roads' }] }))
|
|
541
|
-
expect(result.current.
|
|
687
|
+
expect(result.current.activeItemId).toBeNull()
|
|
542
688
|
})
|
|
543
689
|
})
|
|
544
690
|
|
|
545
|
-
// ───
|
|
691
|
+
// ─── useSpatialListFocus — map interaction returns focus to viewport ──────────────
|
|
546
692
|
|
|
547
|
-
describe('
|
|
693
|
+
describe('useSpatialListFocus — map pointerdown returns focus to viewport', () => {
|
|
548
694
|
const pointerDown = (target) => act(() => {
|
|
549
695
|
target.dispatchEvent(new Event('pointerdown', { bubbles: true, cancelable: true }))
|
|
550
696
|
})
|
|
@@ -555,7 +701,7 @@ describe('useFeatureFocus — map pointerdown returns focus to viewport', () =>
|
|
|
555
701
|
document.body.appendChild(refs.viewportRef.current)
|
|
556
702
|
const mapCanvas = document.createElement('canvas')
|
|
557
703
|
refs.viewportRef.current.appendChild(mapCanvas)
|
|
558
|
-
const { result, unmount } = renderHook(() =>
|
|
704
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
559
705
|
act(() => result.current.onFocus())
|
|
560
706
|
viewportFocus.mockClear()
|
|
561
707
|
pointerDown(mapCanvas)
|
|
@@ -567,10 +713,10 @@ describe('useFeatureFocus — map pointerdown returns focus to viewport', () =>
|
|
|
567
713
|
const viewportFocus = jest.fn()
|
|
568
714
|
const refs = makeRefs({ viewportFocus })
|
|
569
715
|
document.body.appendChild(refs.viewportRef.current)
|
|
570
|
-
refs.viewportRef.current.appendChild(refs.
|
|
716
|
+
refs.viewportRef.current.appendChild(refs.spatialListRef.current)
|
|
571
717
|
const option = document.createElement('li')
|
|
572
|
-
refs.
|
|
573
|
-
const { result, unmount } = renderHook(() =>
|
|
718
|
+
refs.spatialListRef.current.appendChild(option)
|
|
719
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
574
720
|
act(() => result.current.onFocus())
|
|
575
721
|
viewportFocus.mockClear()
|
|
576
722
|
pointerDown(option)
|
|
@@ -584,7 +730,7 @@ describe('useFeatureFocus — map pointerdown returns focus to viewport', () =>
|
|
|
584
730
|
document.body.appendChild(refs.viewportRef.current)
|
|
585
731
|
const mapCanvas = document.createElement('canvas')
|
|
586
732
|
refs.viewportRef.current.appendChild(mapCanvas)
|
|
587
|
-
const { unmount } = renderHook(() =>
|
|
733
|
+
const { unmount } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
588
734
|
pointerDown(mapCanvas)
|
|
589
735
|
expect(viewportFocus).not.toHaveBeenCalled()
|
|
590
736
|
unmount()
|
|
@@ -593,9 +739,9 @@ describe('useFeatureFocus — map pointerdown returns focus to viewport', () =>
|
|
|
593
739
|
afterEach(() => { document.body.innerHTML = '' })
|
|
594
740
|
})
|
|
595
741
|
|
|
596
|
-
// ───
|
|
742
|
+
// ─── useSpatialListFocus — Enter and Space keys ───────────────────────────────────
|
|
597
743
|
|
|
598
|
-
describe('
|
|
744
|
+
describe('useSpatialListFocus — Enter and Space keys', () => {
|
|
599
745
|
afterEach(() => { document.body.innerHTML = '' })
|
|
600
746
|
|
|
601
747
|
it('emits map:selectfeature on Enter', () => {
|
|
@@ -637,17 +783,17 @@ describe('useFeatureFocus — Enter and Space keys', () => {
|
|
|
637
783
|
})
|
|
638
784
|
})
|
|
639
785
|
|
|
640
|
-
// ───
|
|
786
|
+
// ─── useSpatialListFocus — items change while focused ─────────────────────────────
|
|
641
787
|
|
|
642
|
-
describe('
|
|
788
|
+
describe('useSpatialListFocus — items change while focused', () => {
|
|
643
789
|
afterEach(() => { document.body.innerHTML = '' })
|
|
644
790
|
|
|
645
791
|
const setupRerender = (initialItems) => {
|
|
646
792
|
const eb = makeEventBus()
|
|
647
793
|
const refs = makeRefs()
|
|
648
|
-
document.body.appendChild(refs.
|
|
794
|
+
document.body.appendChild(refs.spatialListRef.current)
|
|
649
795
|
const { result, rerender, unmount } = renderHook(
|
|
650
|
-
({ items }) =>
|
|
796
|
+
({ items }) => useSpatialListFocus({ ...refs, items, eventBus: eb }),
|
|
651
797
|
{ initialProps: { items: initialItems } }
|
|
652
798
|
)
|
|
653
799
|
return { eb, result, rerender, unmount }
|
|
@@ -657,7 +803,7 @@ describe('useFeatureFocus — items change while focused', () => {
|
|
|
657
803
|
const { result, rerender, unmount } = setupRerender(ITEMS)
|
|
658
804
|
act(() => result.current.onFocus()) // active = 'a'
|
|
659
805
|
act(() => { rerender({ items: [{ id: 'a', label: 'A updated' }, { id: 'd', label: 'D' }] }) })
|
|
660
|
-
expect(result.current.
|
|
806
|
+
expect(result.current.activeItemId).toBe('a')
|
|
661
807
|
unmount()
|
|
662
808
|
})
|
|
663
809
|
|
|
@@ -666,7 +812,7 @@ describe('useFeatureFocus — items change while focused', () => {
|
|
|
666
812
|
act(() => eb.trigger(SELECTION_CHANGE, { selectedFeatures: [{ featureId: 'c', layerId: 'l' }] }))
|
|
667
813
|
act(() => result.current.onFocus()) // active = 'a' (first, no prev)
|
|
668
814
|
act(() => { rerender({ items: [{ id: 'c', label: 'C' }, { id: 'd', label: 'D' }] }) }) // 'a' gone
|
|
669
|
-
expect(result.current.
|
|
815
|
+
expect(result.current.activeItemId).toBe('c') // first selected
|
|
670
816
|
unmount()
|
|
671
817
|
})
|
|
672
818
|
|
|
@@ -674,7 +820,7 @@ describe('useFeatureFocus — items change while focused', () => {
|
|
|
674
820
|
const { result, rerender, unmount } = setupRerender(ITEMS)
|
|
675
821
|
act(() => result.current.onFocus()) // active = 'a'
|
|
676
822
|
act(() => { rerender({ items: [{ id: 'x', label: 'X' }, { id: 'y', label: 'Y' }] }) })
|
|
677
|
-
expect(result.current.
|
|
823
|
+
expect(result.current.activeItemId).toBe('x')
|
|
678
824
|
unmount()
|
|
679
825
|
})
|
|
680
826
|
|
|
@@ -682,7 +828,7 @@ describe('useFeatureFocus — items change while focused', () => {
|
|
|
682
828
|
const { result, rerender, unmount } = setupRerender(ITEMS)
|
|
683
829
|
// never called onFocus — isFocusedRef stays false
|
|
684
830
|
act(() => { rerender({ items: [{ id: 'x', label: 'X' }] }) })
|
|
685
|
-
expect(result.current.
|
|
831
|
+
expect(result.current.activeItemId).toBeNull()
|
|
686
832
|
unmount()
|
|
687
833
|
})
|
|
688
834
|
|
|
@@ -691,7 +837,7 @@ describe('useFeatureFocus — items change while focused', () => {
|
|
|
691
837
|
act(() => result.current.onFocus())
|
|
692
838
|
eb.emit.mockClear()
|
|
693
839
|
act(() => { rerender({ items: [{ id: 'x', label: 'X' }] }) }) // 'a' gone
|
|
694
|
-
expect(eb.emit).toHaveBeenCalledWith('map:
|
|
840
|
+
expect(eb.emit).toHaveBeenCalledWith('map:setactiveitem', { id: 'x' })
|
|
695
841
|
unmount()
|
|
696
842
|
})
|
|
697
843
|
|
|
@@ -700,22 +846,31 @@ describe('useFeatureFocus — items change while focused', () => {
|
|
|
700
846
|
act(() => result.current.onFocus())
|
|
701
847
|
eb.emit.mockClear()
|
|
702
848
|
act(() => { rerender({ items: [] }) })
|
|
703
|
-
expect(result.current.
|
|
704
|
-
expect(eb.emit).toHaveBeenCalledWith('map:
|
|
849
|
+
expect(result.current.activeItemId).toBeNull()
|
|
850
|
+
expect(eb.emit).toHaveBeenCalledWith('map:setactiveitem', { id: null })
|
|
705
851
|
unmount()
|
|
706
852
|
})
|
|
707
853
|
})
|
|
708
854
|
|
|
709
|
-
// ───
|
|
855
|
+
// ─── useSpatialListFocus — tabbableId (roving tabindex resting position) ─────────
|
|
710
856
|
|
|
711
|
-
describe('
|
|
857
|
+
describe('useSpatialListFocus — tabbableId', () => {
|
|
712
858
|
it('is null when items is empty', () => {
|
|
713
|
-
const { result } = renderHook(() =>
|
|
859
|
+
const { result } = renderHook(() => useSpatialListFocus(makeRefs()))
|
|
714
860
|
expect(result.current.tabbableId).toBeNull()
|
|
715
861
|
})
|
|
716
862
|
|
|
863
|
+
it('defaults to the item nearest the map center before the list has ever been focused', () => {
|
|
864
|
+
const items = [
|
|
865
|
+
{ id: 'a', label: 'A', x: 0, y: 0 },
|
|
866
|
+
{ id: 'b', label: 'B', x: 100, y: 100 }
|
|
867
|
+
]
|
|
868
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items, centerScreenPoint: { x: 90, y: 90 } }))
|
|
869
|
+
expect(result.current.tabbableId).toBe('b')
|
|
870
|
+
})
|
|
871
|
+
|
|
717
872
|
it('defaults to the first item before the list has ever been focused', () => {
|
|
718
|
-
const { result } = renderHook(() =>
|
|
873
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items: ITEMS }))
|
|
719
874
|
expect(result.current.tabbableId).toBe('a')
|
|
720
875
|
})
|
|
721
876
|
|
|
@@ -725,14 +880,14 @@ describe('useFeatureFocus — tabbableId', () => {
|
|
|
725
880
|
// tabIndex to match it on every click.
|
|
726
881
|
it('is not influenced by a selection change before any position is established', () => {
|
|
727
882
|
const eb = makeEventBus()
|
|
728
|
-
const { result } = renderHook(() =>
|
|
883
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items: ITEMS, eventBus: eb }))
|
|
729
884
|
act(() => eb.trigger(SELECTION_CHANGE, { selectedFeatures: [{ featureId: 'b', layerId: 'roads' }] }))
|
|
730
885
|
expect(result.current.tabbableId).toBe('a')
|
|
731
886
|
})
|
|
732
887
|
|
|
733
888
|
it('does not chase a single item toggled selected/deselected purely via mouse clicks, with no keyboard interaction at all', () => {
|
|
734
889
|
const eb = makeEventBus()
|
|
735
|
-
const { result } = renderHook(() =>
|
|
890
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items: ITEMS, eventBus: eb }))
|
|
736
891
|
act(() => eb.trigger(SELECTION_CHANGE, { selectedMarkers: ['a'] }))
|
|
737
892
|
expect(result.current.tabbableId).toBe('a')
|
|
738
893
|
act(() => eb.trigger(SELECTION_CHANGE, { selectedMarkers: ['b'] })) // clicked a different marker
|
|
@@ -744,9 +899,9 @@ describe('useFeatureFocus — tabbableId', () => {
|
|
|
744
899
|
it('stays on the established position when a selection change arrives from elsewhere (e.g. clicking a marker on the map)', () => {
|
|
745
900
|
const eb = makeEventBus()
|
|
746
901
|
const refs = makeRefs()
|
|
747
|
-
const el = refs.
|
|
902
|
+
const el = refs.spatialListRef.current
|
|
748
903
|
document.body.appendChild(el)
|
|
749
|
-
const { result, unmount } = renderHook(() =>
|
|
904
|
+
const { result, unmount } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS, eventBus: eb }))
|
|
750
905
|
act(() => result.current.onFocus()) // establishes position at 'a'
|
|
751
906
|
fireKey(el, 'ArrowDown') // establishes position at 'b'
|
|
752
907
|
act(() => result.current.onBlur())
|
|
@@ -758,20 +913,20 @@ describe('useFeatureFocus — tabbableId', () => {
|
|
|
758
913
|
})
|
|
759
914
|
})
|
|
760
915
|
|
|
761
|
-
// ───
|
|
916
|
+
// ─── useSpatialListFocus — selectItem (click / touch / voice-control activation) ─
|
|
762
917
|
|
|
763
|
-
describe('
|
|
918
|
+
describe('useSpatialListFocus — selectItem', () => {
|
|
764
919
|
afterEach(() => { document.body.innerHTML = '' })
|
|
765
920
|
|
|
766
|
-
it('sets
|
|
767
|
-
const { result } = renderHook(() =>
|
|
921
|
+
it('sets activeItemId to the clicked item', () => {
|
|
922
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items: ITEMS }))
|
|
768
923
|
act(() => result.current.selectItem('b'))
|
|
769
|
-
expect(result.current.
|
|
924
|
+
expect(result.current.activeItemId).toBe('b')
|
|
770
925
|
})
|
|
771
926
|
|
|
772
927
|
it('emits map:setactivefeature then map:selectfeature for the clicked item', () => {
|
|
773
928
|
const eb = makeEventBus()
|
|
774
|
-
const { result } = renderHook(() =>
|
|
929
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items: ITEMS, eventBus: eb }))
|
|
775
930
|
act(() => result.current.selectItem('b'))
|
|
776
931
|
expect(eb.emit).toHaveBeenNthCalledWith(1, SET_ACTIVE, { id: 'b' })
|
|
777
932
|
expect(eb.emit).toHaveBeenNthCalledWith(2, SELECT)
|
|
@@ -782,21 +937,21 @@ describe('useFeatureFocus — selectItem', () => {
|
|
|
782
937
|
const option = document.createElement('li')
|
|
783
938
|
option.dataset.id = 'b'
|
|
784
939
|
option.focus = jest.fn()
|
|
785
|
-
refs.
|
|
786
|
-
const { result } = renderHook(() =>
|
|
940
|
+
refs.spatialListRef.current.appendChild(option)
|
|
941
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
787
942
|
act(() => result.current.selectItem('b'))
|
|
788
943
|
expect(option.focus).toHaveBeenCalledWith({ preventScroll: true })
|
|
789
944
|
})
|
|
790
945
|
|
|
791
946
|
it('does not throw when eventBus is not provided', () => {
|
|
792
|
-
const { result } = renderHook(() =>
|
|
947
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...makeRefs(), items: ITEMS }))
|
|
793
948
|
expect(() => act(() => result.current.selectItem('b'))).not.toThrow()
|
|
794
949
|
})
|
|
795
950
|
|
|
796
|
-
it('does not throw when
|
|
797
|
-
const refs = { ...makeRefs(),
|
|
798
|
-
const { result } = renderHook(() =>
|
|
951
|
+
it('does not throw when spatialListRef.current is null (e.g. unmounted)', () => {
|
|
952
|
+
const refs = { ...makeRefs(), spatialListRef: { current: null } }
|
|
953
|
+
const { result } = renderHook(() => useSpatialListFocus({ ...refs, items: ITEMS }))
|
|
799
954
|
expect(() => act(() => result.current.selectItem('b'))).not.toThrow()
|
|
800
|
-
expect(result.current.
|
|
955
|
+
expect(result.current.activeItemId).toBe('b') // state still updates even though there's no element to focus
|
|
801
956
|
})
|
|
802
957
|
})
|