@furystack/shades 14.0.0 → 15.0.0
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/CHANGELOG.md +13 -0
- package/esm/component-factory.spec.d.ts.map +1 -1
- package/esm/component-factory.spec.js +2 -2
- package/esm/component-factory.spec.js.map +1 -1
- package/esm/components/nested-hooks.js +2 -2
- package/esm/components/nested-hooks.js.map +1 -1
- package/esm/components/nested-hooks.spec.js +14 -14
- package/esm/components/nested-hooks.spec.js.map +1 -1
- package/esm/components/nested-navigate.js +1 -1
- package/esm/components/nested-navigate.js.map +1 -1
- package/esm/components/nested-navigate.spec.js +16 -16
- package/esm/components/nested-navigate.spec.js.map +1 -1
- package/esm/components/nested-replace.js +1 -1
- package/esm/components/nested-replace.js.map +1 -1
- package/esm/components/nested-replace.spec.js +6 -6
- package/esm/components/nested-replace.spec.js.map +1 -1
- package/esm/components/nested-route-link.js +1 -1
- package/esm/components/nested-route-link.js.map +1 -1
- package/esm/components/nested-route-link.spec.js +8 -8
- package/esm/components/nested-route-link.spec.js.map +1 -1
- package/esm/components/nested-router.js +3 -3
- package/esm/components/nested-router.js.map +1 -1
- package/esm/components/nested-router.spec.js +24 -24
- package/esm/components/nested-router.spec.js.map +1 -1
- package/esm/services/location-service.d.ts +39 -42
- package/esm/services/location-service.d.ts.map +1 -1
- package/esm/services/location-service.js +115 -134
- package/esm/services/location-service.js.map +1 -1
- package/esm/services/location-service.spec.js +32 -30
- package/esm/services/location-service.spec.js.map +1 -1
- package/esm/services/route-match-service.d.ts +3 -2
- package/esm/services/route-match-service.d.ts.map +1 -1
- package/esm/services/route-match-service.js +13 -22
- package/esm/services/route-match-service.js.map +1 -1
- package/esm/services/route-match-service.spec.js +40 -51
- package/esm/services/route-match-service.spec.js.map +1 -1
- package/esm/services/route-meta-utils.spec.js +3 -3
- package/esm/services/route-meta-utils.spec.js.map +1 -1
- package/esm/services/screen-service.d.ts +13 -72
- package/esm/services/screen-service.d.ts.map +1 -1
- package/esm/services/screen-service.js +45 -137
- package/esm/services/screen-service.js.map +1 -1
- package/esm/services/screen-service.spec.js +19 -18
- package/esm/services/screen-service.spec.js.map +1 -1
- package/esm/services/spatial-navigation-service.d.ts +17 -39
- package/esm/services/spatial-navigation-service.d.ts.map +1 -1
- package/esm/services/spatial-navigation-service.js +280 -313
- package/esm/services/spatial-navigation-service.js.map +1 -1
- package/esm/services/spatial-navigation-service.spec.js +163 -158
- package/esm/services/spatial-navigation-service.spec.js.map +1 -1
- package/esm/shade.d.ts +1 -1
- package/esm/shade.d.ts.map +1 -1
- package/esm/shade.js +10 -7
- package/esm/shade.js.map +1 -1
- package/package.json +5 -4
- package/src/component-factory.spec.tsx +3 -2
- package/src/components/nested-hooks.spec.ts +14 -14
- package/src/components/nested-hooks.ts +2 -2
- package/src/components/nested-navigate.spec.ts +17 -16
- package/src/components/nested-navigate.ts +1 -1
- package/src/components/nested-replace.spec.ts +7 -6
- package/src/components/nested-replace.ts +1 -1
- package/src/components/nested-route-link.spec.tsx +8 -8
- package/src/components/nested-route-link.tsx +1 -1
- package/src/components/nested-router.spec.tsx +24 -24
- package/src/components/nested-router.tsx +3 -3
- package/src/services/location-service.spec.ts +35 -31
- package/src/services/location-service.tsx +156 -122
- package/src/services/route-match-service.spec.ts +41 -32
- package/src/services/route-match-service.ts +17 -8
- package/src/services/route-meta-utils.spec.ts +3 -3
- package/src/services/screen-service.spec.ts +19 -18
- package/src/services/screen-service.ts +65 -110
- package/src/services/spatial-navigation-service.spec.ts +165 -160
- package/src/services/spatial-navigation-service.ts +299 -287
- package/src/shade.ts +11 -8
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createInjector } from '@furystack/inject';
|
|
2
2
|
import { usingAsync } from '@furystack/utils';
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { SpatialNavigationService, configureSpatialNavigation } from './spatial-navigation-service.js';
|
|
@@ -35,98 +35,99 @@ describe('SpatialNavigationService', () => {
|
|
|
35
35
|
vi.restoreAllMocks();
|
|
36
36
|
});
|
|
37
37
|
it('Should be constructed via injector', async () => {
|
|
38
|
-
await usingAsync(
|
|
39
|
-
const s = i.
|
|
40
|
-
expect(s).
|
|
38
|
+
await usingAsync(createInjector(), async (i) => {
|
|
39
|
+
const s = i.get(SpatialNavigationService);
|
|
40
|
+
expect(s).toBeDefined();
|
|
41
|
+
expect(typeof s.moveFocus).toBe('function');
|
|
41
42
|
});
|
|
42
43
|
});
|
|
43
44
|
it('Should be enabled by default', async () => {
|
|
44
|
-
await usingAsync(
|
|
45
|
-
const s = i.
|
|
45
|
+
await usingAsync(createInjector(), async (i) => {
|
|
46
|
+
const s = i.get(SpatialNavigationService);
|
|
46
47
|
expect(s.enabled.getValue()).toBe(true);
|
|
47
48
|
});
|
|
48
49
|
});
|
|
49
50
|
it('Should have null activeSection initially', async () => {
|
|
50
|
-
await usingAsync(
|
|
51
|
-
const s = i.
|
|
51
|
+
await usingAsync(createInjector(), async (i) => {
|
|
52
|
+
const s = i.get(SpatialNavigationService);
|
|
52
53
|
expect(s.activeSection.getValue()).toBeNull();
|
|
53
54
|
});
|
|
54
55
|
});
|
|
55
56
|
describe('focus movement', () => {
|
|
56
57
|
it('Should move focus to the right', async () => {
|
|
57
|
-
await usingAsync(
|
|
58
|
+
await usingAsync(createInjector(), async (i) => {
|
|
58
59
|
const left = createButton('left', { left: 0, top: 0, width: 50, height: 50 });
|
|
59
60
|
const right = createButton('right', { left: 100, top: 0, width: 50, height: 50 });
|
|
60
61
|
document.body.append(left, right);
|
|
61
62
|
left.focus();
|
|
62
63
|
expect(document.activeElement).toBe(left);
|
|
63
|
-
const s = i.
|
|
64
|
+
const s = i.get(SpatialNavigationService);
|
|
64
65
|
s.moveFocus('right');
|
|
65
66
|
expect(document.activeElement).toBe(right);
|
|
66
67
|
});
|
|
67
68
|
});
|
|
68
69
|
it('Should move focus to the left', async () => {
|
|
69
|
-
await usingAsync(
|
|
70
|
+
await usingAsync(createInjector(), async (i) => {
|
|
70
71
|
const left = createButton('left', { left: 0, top: 0, width: 50, height: 50 });
|
|
71
72
|
const right = createButton('right', { left: 100, top: 0, width: 50, height: 50 });
|
|
72
73
|
document.body.append(left, right);
|
|
73
74
|
right.focus();
|
|
74
|
-
const s = i.
|
|
75
|
+
const s = i.get(SpatialNavigationService);
|
|
75
76
|
s.moveFocus('left');
|
|
76
77
|
expect(document.activeElement).toBe(left);
|
|
77
78
|
});
|
|
78
79
|
});
|
|
79
80
|
it('Should move focus down', async () => {
|
|
80
|
-
await usingAsync(
|
|
81
|
+
await usingAsync(createInjector(), async (i) => {
|
|
81
82
|
const top = createButton('top', { left: 0, top: 0, width: 50, height: 50 });
|
|
82
83
|
const bottom = createButton('bottom', { left: 0, top: 100, width: 50, height: 50 });
|
|
83
84
|
document.body.append(top, bottom);
|
|
84
85
|
top.focus();
|
|
85
|
-
const s = i.
|
|
86
|
+
const s = i.get(SpatialNavigationService);
|
|
86
87
|
s.moveFocus('down');
|
|
87
88
|
expect(document.activeElement).toBe(bottom);
|
|
88
89
|
});
|
|
89
90
|
});
|
|
90
91
|
it('Should move focus up', async () => {
|
|
91
|
-
await usingAsync(
|
|
92
|
+
await usingAsync(createInjector(), async (i) => {
|
|
92
93
|
const top = createButton('top', { left: 0, top: 0, width: 50, height: 50 });
|
|
93
94
|
const bottom = createButton('bottom', { left: 0, top: 100, width: 50, height: 50 });
|
|
94
95
|
document.body.append(top, bottom);
|
|
95
96
|
bottom.focus();
|
|
96
|
-
const s = i.
|
|
97
|
+
const s = i.get(SpatialNavigationService);
|
|
97
98
|
s.moveFocus('up');
|
|
98
99
|
expect(document.activeElement).toBe(top);
|
|
99
100
|
});
|
|
100
101
|
});
|
|
101
102
|
it('Should select nearest element by Euclidean distance', async () => {
|
|
102
|
-
await usingAsync(
|
|
103
|
+
await usingAsync(createInjector(), async (i) => {
|
|
103
104
|
const origin = createButton('origin', { left: 0, top: 0, width: 50, height: 50 });
|
|
104
105
|
const near = createButton('near', { left: 100, top: 10, width: 50, height: 50 });
|
|
105
106
|
const far = createButton('far', { left: 300, top: 10, width: 50, height: 50 });
|
|
106
107
|
document.body.append(origin, near, far);
|
|
107
108
|
origin.focus();
|
|
108
|
-
const s = i.
|
|
109
|
+
const s = i.get(SpatialNavigationService);
|
|
109
110
|
s.moveFocus('right');
|
|
110
111
|
expect(document.activeElement).toBe(near);
|
|
111
112
|
});
|
|
112
113
|
});
|
|
113
114
|
it('Should be a no-op when no candidate exists in the direction', async () => {
|
|
114
|
-
await usingAsync(
|
|
115
|
+
await usingAsync(createInjector(), async (i) => {
|
|
115
116
|
const only = createButton('only', { left: 0, top: 0, width: 50, height: 50 });
|
|
116
117
|
document.body.append(only);
|
|
117
118
|
only.focus();
|
|
118
|
-
const s = i.
|
|
119
|
+
const s = i.get(SpatialNavigationService);
|
|
119
120
|
s.moveFocus('right');
|
|
120
121
|
expect(document.activeElement).toBe(only);
|
|
121
122
|
});
|
|
122
123
|
});
|
|
123
124
|
it('Should call scrollIntoView on the target element', async () => {
|
|
124
|
-
await usingAsync(
|
|
125
|
+
await usingAsync(createInjector(), async (i) => {
|
|
125
126
|
const left = createButton('left', { left: 0, top: 0, width: 50, height: 50 });
|
|
126
127
|
const right = createButton('right', { left: 100, top: 0, width: 50, height: 50 });
|
|
127
128
|
document.body.append(left, right);
|
|
128
129
|
left.focus();
|
|
129
|
-
const s = i.
|
|
130
|
+
const s = i.get(SpatialNavigationService);
|
|
130
131
|
s.moveFocus('right');
|
|
131
132
|
expect(right.scrollIntoView).toHaveBeenCalledWith({ block: 'nearest', inline: 'nearest' });
|
|
132
133
|
});
|
|
@@ -134,29 +135,29 @@ describe('SpatialNavigationService', () => {
|
|
|
134
135
|
});
|
|
135
136
|
describe('initial focus', () => {
|
|
136
137
|
it('Should focus first element when no element is focused', async () => {
|
|
137
|
-
await usingAsync(
|
|
138
|
+
await usingAsync(createInjector(), async (i) => {
|
|
138
139
|
const btn = createButton('first', { left: 0, top: 0, width: 50, height: 50 });
|
|
139
140
|
document.body.append(btn);
|
|
140
|
-
const s = i.
|
|
141
|
+
const s = i.get(SpatialNavigationService);
|
|
141
142
|
s.moveFocus('down');
|
|
142
143
|
expect(document.activeElement).toBe(btn);
|
|
143
144
|
});
|
|
144
145
|
});
|
|
145
146
|
it('Should focus first element in first section when no element is focused', async () => {
|
|
146
|
-
await usingAsync(
|
|
147
|
+
await usingAsync(createInjector(), async (i) => {
|
|
147
148
|
const section = document.createElement('div');
|
|
148
149
|
section.setAttribute('data-nav-section', 'main');
|
|
149
150
|
const btn = createButton('first', { left: 0, top: 0, width: 50, height: 50 });
|
|
150
151
|
section.append(btn);
|
|
151
152
|
document.body.append(section);
|
|
152
|
-
const s = i.
|
|
153
|
+
const s = i.get(SpatialNavigationService);
|
|
153
154
|
s.moveFocus('down');
|
|
154
155
|
expect(document.activeElement).toBe(btn);
|
|
155
156
|
});
|
|
156
157
|
});
|
|
157
158
|
it('Should be a no-op when there are no focusable elements', async () => {
|
|
158
|
-
await usingAsync(
|
|
159
|
-
const s = i.
|
|
159
|
+
await usingAsync(createInjector(), async (i) => {
|
|
160
|
+
const s = i.get(SpatialNavigationService);
|
|
160
161
|
s.moveFocus('down');
|
|
161
162
|
expect(document.activeElement).toBe(document.body);
|
|
162
163
|
});
|
|
@@ -164,47 +165,47 @@ describe('SpatialNavigationService', () => {
|
|
|
164
165
|
});
|
|
165
166
|
describe('keydown event handling', () => {
|
|
166
167
|
it('Should move focus on arrow key press', async () => {
|
|
167
|
-
await usingAsync(
|
|
168
|
+
await usingAsync(createInjector(), async (i) => {
|
|
168
169
|
const left = createButton('left', { left: 0, top: 0, width: 50, height: 50 });
|
|
169
170
|
const right = createButton('right', { left: 100, top: 0, width: 50, height: 50 });
|
|
170
171
|
document.body.append(left, right);
|
|
171
172
|
left.focus();
|
|
172
|
-
i.
|
|
173
|
+
i.get(SpatialNavigationService);
|
|
173
174
|
pressKey('ArrowRight');
|
|
174
175
|
expect(document.activeElement).toBe(right);
|
|
175
176
|
});
|
|
176
177
|
});
|
|
177
178
|
it('Should activate focused element on Enter', async () => {
|
|
178
|
-
await usingAsync(
|
|
179
|
+
await usingAsync(createInjector(), async (i) => {
|
|
179
180
|
const btn = createButton('btn', { left: 0, top: 0, width: 50, height: 50 });
|
|
180
181
|
const clickHandler = vi.fn();
|
|
181
182
|
btn.addEventListener('click', clickHandler);
|
|
182
183
|
document.body.append(btn);
|
|
183
184
|
btn.focus();
|
|
184
|
-
i.
|
|
185
|
+
i.get(SpatialNavigationService);
|
|
185
186
|
pressKey('Enter');
|
|
186
187
|
expect(clickHandler).toHaveBeenCalledTimes(1);
|
|
187
188
|
});
|
|
188
189
|
});
|
|
189
190
|
it('Should not handle events when disabled', async () => {
|
|
190
|
-
await usingAsync(
|
|
191
|
+
await usingAsync(createInjector(), async (i) => {
|
|
191
192
|
const left = createButton('left', { left: 0, top: 0, width: 50, height: 50 });
|
|
192
193
|
const right = createButton('right', { left: 100, top: 0, width: 50, height: 50 });
|
|
193
194
|
document.body.append(left, right);
|
|
194
195
|
left.focus();
|
|
195
|
-
const s = i.
|
|
196
|
+
const s = i.get(SpatialNavigationService);
|
|
196
197
|
s.enabled.setValue(false);
|
|
197
198
|
pressKey('ArrowRight');
|
|
198
199
|
expect(document.activeElement).toBe(left);
|
|
199
200
|
});
|
|
200
201
|
});
|
|
201
202
|
it('Should skip events that are already defaultPrevented', async () => {
|
|
202
|
-
await usingAsync(
|
|
203
|
+
await usingAsync(createInjector(), async (i) => {
|
|
203
204
|
const left = createButton('left', { left: 0, top: 0, width: 50, height: 50 });
|
|
204
205
|
const right = createButton('right', { left: 100, top: 0, width: 50, height: 50 });
|
|
205
206
|
document.body.append(left, right);
|
|
206
207
|
left.focus();
|
|
207
|
-
i.
|
|
208
|
+
i.get(SpatialNavigationService);
|
|
208
209
|
const preventer = (ev) => ev.preventDefault();
|
|
209
210
|
window.addEventListener('keydown', preventer, { capture: true });
|
|
210
211
|
try {
|
|
@@ -219,7 +220,7 @@ describe('SpatialNavigationService', () => {
|
|
|
219
220
|
});
|
|
220
221
|
describe('input passthrough', () => {
|
|
221
222
|
it('Should not intercept arrow keys on text input when cursor is mid-text', async () => {
|
|
222
|
-
await usingAsync(
|
|
223
|
+
await usingAsync(createInjector(), async (i) => {
|
|
223
224
|
const input = document.createElement('input');
|
|
224
225
|
input.type = 'text';
|
|
225
226
|
input.value = 'hello';
|
|
@@ -228,13 +229,13 @@ describe('SpatialNavigationService', () => {
|
|
|
228
229
|
document.body.append(input, btn);
|
|
229
230
|
input.focus();
|
|
230
231
|
input.setSelectionRange(2, 2);
|
|
231
|
-
i.
|
|
232
|
+
i.get(SpatialNavigationService);
|
|
232
233
|
pressKey('ArrowRight');
|
|
233
234
|
expect(document.activeElement).toBe(input);
|
|
234
235
|
});
|
|
235
236
|
});
|
|
236
237
|
it('Should escape text input with ArrowRight when cursor is at end', async () => {
|
|
237
|
-
await usingAsync(
|
|
238
|
+
await usingAsync(createInjector(), async (i) => {
|
|
238
239
|
const input = document.createElement('input');
|
|
239
240
|
input.type = 'text';
|
|
240
241
|
input.value = 'hello';
|
|
@@ -244,13 +245,13 @@ describe('SpatialNavigationService', () => {
|
|
|
244
245
|
document.body.append(input, btn);
|
|
245
246
|
input.focus();
|
|
246
247
|
input.setSelectionRange(5, 5);
|
|
247
|
-
i.
|
|
248
|
+
i.get(SpatialNavigationService);
|
|
248
249
|
pressKey('ArrowRight');
|
|
249
250
|
expect(document.activeElement).toBe(btn);
|
|
250
251
|
});
|
|
251
252
|
});
|
|
252
253
|
it('Should escape text input with ArrowLeft when cursor is at start', async () => {
|
|
253
|
-
await usingAsync(
|
|
254
|
+
await usingAsync(createInjector(), async (i) => {
|
|
254
255
|
const input = document.createElement('input');
|
|
255
256
|
input.type = 'text';
|
|
256
257
|
input.value = 'hello';
|
|
@@ -260,13 +261,13 @@ describe('SpatialNavigationService', () => {
|
|
|
260
261
|
document.body.append(btn, input);
|
|
261
262
|
input.focus();
|
|
262
263
|
input.setSelectionRange(0, 0);
|
|
263
|
-
i.
|
|
264
|
+
i.get(SpatialNavigationService);
|
|
264
265
|
pressKey('ArrowLeft');
|
|
265
266
|
expect(document.activeElement).toBe(btn);
|
|
266
267
|
});
|
|
267
268
|
});
|
|
268
269
|
it('Should not intercept arrow keys on textarea when cursor is mid-text', async () => {
|
|
269
|
-
await usingAsync(
|
|
270
|
+
await usingAsync(createInjector(), async (i) => {
|
|
270
271
|
const textarea = document.createElement('textarea');
|
|
271
272
|
textarea.value = 'line1\nline2';
|
|
272
273
|
mockRect(textarea, { left: 0, top: 0, width: 200, height: 100 });
|
|
@@ -274,25 +275,25 @@ describe('SpatialNavigationService', () => {
|
|
|
274
275
|
document.body.append(textarea, btn);
|
|
275
276
|
textarea.focus();
|
|
276
277
|
textarea.setSelectionRange(3, 3);
|
|
277
|
-
i.
|
|
278
|
+
i.get(SpatialNavigationService);
|
|
278
279
|
pressKey('ArrowDown');
|
|
279
280
|
expect(document.activeElement).toBe(textarea);
|
|
280
281
|
});
|
|
281
282
|
});
|
|
282
283
|
it('Should not intercept arrow keys on select', async () => {
|
|
283
|
-
await usingAsync(
|
|
284
|
+
await usingAsync(createInjector(), async (i) => {
|
|
284
285
|
const select = document.createElement('select');
|
|
285
286
|
mockRect(select, { left: 0, top: 0, width: 200, height: 30 });
|
|
286
287
|
const btn = createButton('btn', { left: 0, top: 100, width: 50, height: 50 });
|
|
287
288
|
document.body.append(select, btn);
|
|
288
289
|
select.focus();
|
|
289
|
-
i.
|
|
290
|
+
i.get(SpatialNavigationService);
|
|
290
291
|
pressKey('ArrowDown');
|
|
291
292
|
expect(document.activeElement).toBe(select);
|
|
292
293
|
});
|
|
293
294
|
});
|
|
294
295
|
it('Should not intercept arrow keys on contenteditable', async () => {
|
|
295
|
-
await usingAsync(
|
|
296
|
+
await usingAsync(createInjector(), async (i) => {
|
|
296
297
|
const div = document.createElement('div');
|
|
297
298
|
div.contentEditable = 'true';
|
|
298
299
|
div.tabIndex = 0;
|
|
@@ -300,13 +301,13 @@ describe('SpatialNavigationService', () => {
|
|
|
300
301
|
const btn = createButton('btn', { left: 300, top: 0, width: 50, height: 50 });
|
|
301
302
|
document.body.append(div, btn);
|
|
302
303
|
div.focus();
|
|
303
|
-
i.
|
|
304
|
+
i.get(SpatialNavigationService);
|
|
304
305
|
pressKey('ArrowRight');
|
|
305
306
|
expect(document.activeElement).toBe(div);
|
|
306
307
|
});
|
|
307
308
|
});
|
|
308
309
|
it('Should not intercept arrow keys on children of contenteditable', async () => {
|
|
309
|
-
await usingAsync(
|
|
310
|
+
await usingAsync(createInjector(), async (i) => {
|
|
310
311
|
const div = document.createElement('div');
|
|
311
312
|
div.contentEditable = 'true';
|
|
312
313
|
div.tabIndex = 0;
|
|
@@ -319,13 +320,13 @@ describe('SpatialNavigationService', () => {
|
|
|
319
320
|
const btn = createButton('btn', { left: 300, top: 0, width: 50, height: 50 });
|
|
320
321
|
document.body.append(div, btn);
|
|
321
322
|
span.focus();
|
|
322
|
-
i.
|
|
323
|
+
i.get(SpatialNavigationService);
|
|
323
324
|
pressKey('ArrowRight');
|
|
324
325
|
expect(document.activeElement).toBe(span);
|
|
325
326
|
});
|
|
326
327
|
});
|
|
327
328
|
it('Should not intercept Enter on children of contenteditable', async () => {
|
|
328
|
-
await usingAsync(
|
|
329
|
+
await usingAsync(createInjector(), async (i) => {
|
|
329
330
|
const div = document.createElement('div');
|
|
330
331
|
div.contentEditable = 'true';
|
|
331
332
|
div.tabIndex = 0;
|
|
@@ -338,13 +339,13 @@ describe('SpatialNavigationService', () => {
|
|
|
338
339
|
span.addEventListener('click', clickHandler);
|
|
339
340
|
document.body.append(div);
|
|
340
341
|
span.focus();
|
|
341
|
-
i.
|
|
342
|
+
i.get(SpatialNavigationService);
|
|
342
343
|
pressKey('Enter');
|
|
343
344
|
expect(clickHandler).not.toHaveBeenCalled();
|
|
344
345
|
});
|
|
345
346
|
});
|
|
346
347
|
it('Should intercept arrow keys on button-type input', async () => {
|
|
347
|
-
await usingAsync(
|
|
348
|
+
await usingAsync(createInjector(), async (i) => {
|
|
348
349
|
const input = document.createElement('input');
|
|
349
350
|
input.type = 'button';
|
|
350
351
|
mockRect(input, { left: 0, top: 0, width: 50, height: 30 });
|
|
@@ -352,20 +353,20 @@ describe('SpatialNavigationService', () => {
|
|
|
352
353
|
const btn = createButton('btn', { left: 100, top: 0, width: 50, height: 50 });
|
|
353
354
|
document.body.append(input, btn);
|
|
354
355
|
input.focus();
|
|
355
|
-
i.
|
|
356
|
+
i.get(SpatialNavigationService);
|
|
356
357
|
pressKey('ArrowRight');
|
|
357
358
|
expect(document.activeElement).toBe(btn);
|
|
358
359
|
});
|
|
359
360
|
});
|
|
360
361
|
it('Should not intercept Left/Right on range input (slider adjustment)', async () => {
|
|
361
|
-
await usingAsync(
|
|
362
|
+
await usingAsync(createInjector(), async (i) => {
|
|
362
363
|
const input = document.createElement('input');
|
|
363
364
|
input.type = 'range';
|
|
364
365
|
mockRect(input, { left: 0, top: 0, width: 200, height: 30 });
|
|
365
366
|
const btn = createButton('btn', { left: 0, top: 100, width: 50, height: 50 });
|
|
366
367
|
document.body.append(input, btn);
|
|
367
368
|
input.focus();
|
|
368
|
-
i.
|
|
369
|
+
i.get(SpatialNavigationService);
|
|
369
370
|
pressKey('ArrowLeft');
|
|
370
371
|
expect(document.activeElement).toBe(input);
|
|
371
372
|
pressKey('ArrowRight');
|
|
@@ -373,7 +374,7 @@ describe('SpatialNavigationService', () => {
|
|
|
373
374
|
});
|
|
374
375
|
});
|
|
375
376
|
it('Should intercept Up/Down on range input for spatial navigation', async () => {
|
|
376
|
-
await usingAsync(
|
|
377
|
+
await usingAsync(createInjector(), async (i) => {
|
|
377
378
|
const input = document.createElement('input');
|
|
378
379
|
input.type = 'range';
|
|
379
380
|
mockRect(input, { left: 0, top: 0, width: 200, height: 30 });
|
|
@@ -381,13 +382,13 @@ describe('SpatialNavigationService', () => {
|
|
|
381
382
|
const btn = createButton('btn', { left: 0, top: 100, width: 50, height: 50 });
|
|
382
383
|
document.body.append(input, btn);
|
|
383
384
|
input.focus();
|
|
384
|
-
i.
|
|
385
|
+
i.get(SpatialNavigationService);
|
|
385
386
|
pressKey('ArrowDown');
|
|
386
387
|
expect(document.activeElement).toBe(btn);
|
|
387
388
|
});
|
|
388
389
|
});
|
|
389
390
|
it('Should intercept arrow keys on color input', async () => {
|
|
390
|
-
await usingAsync(
|
|
391
|
+
await usingAsync(createInjector(), async (i) => {
|
|
391
392
|
const input = document.createElement('input');
|
|
392
393
|
input.type = 'color';
|
|
393
394
|
mockRect(input, { left: 0, top: 0, width: 50, height: 50 });
|
|
@@ -395,13 +396,13 @@ describe('SpatialNavigationService', () => {
|
|
|
395
396
|
const btn = createButton('btn', { left: 100, top: 0, width: 50, height: 50 });
|
|
396
397
|
document.body.append(input, btn);
|
|
397
398
|
input.focus();
|
|
398
|
-
i.
|
|
399
|
+
i.get(SpatialNavigationService);
|
|
399
400
|
pressKey('ArrowRight');
|
|
400
401
|
expect(document.activeElement).toBe(btn);
|
|
401
402
|
});
|
|
402
403
|
});
|
|
403
404
|
it('Should intercept arrow keys on file input', async () => {
|
|
404
|
-
await usingAsync(
|
|
405
|
+
await usingAsync(createInjector(), async (i) => {
|
|
405
406
|
const input = document.createElement('input');
|
|
406
407
|
input.type = 'file';
|
|
407
408
|
mockRect(input, { left: 0, top: 0, width: 200, height: 30 });
|
|
@@ -409,13 +410,13 @@ describe('SpatialNavigationService', () => {
|
|
|
409
410
|
const btn = createButton('btn', { left: 300, top: 0, width: 50, height: 50 });
|
|
410
411
|
document.body.append(input, btn);
|
|
411
412
|
input.focus();
|
|
412
|
-
i.
|
|
413
|
+
i.get(SpatialNavigationService);
|
|
413
414
|
pressKey('ArrowRight');
|
|
414
415
|
expect(document.activeElement).toBe(btn);
|
|
415
416
|
});
|
|
416
417
|
});
|
|
417
418
|
it('Should not intercept arrow keys on radio input', async () => {
|
|
418
|
-
await usingAsync(
|
|
419
|
+
await usingAsync(createInjector(), async (i) => {
|
|
419
420
|
const radio = document.createElement('input');
|
|
420
421
|
radio.type = 'radio';
|
|
421
422
|
radio.name = 'group';
|
|
@@ -423,7 +424,7 @@ describe('SpatialNavigationService', () => {
|
|
|
423
424
|
const btn = createButton('btn', { left: 100, top: 0, width: 50, height: 50 });
|
|
424
425
|
document.body.append(radio, btn);
|
|
425
426
|
radio.focus();
|
|
426
|
-
i.
|
|
427
|
+
i.get(SpatialNavigationService);
|
|
427
428
|
pressKey('ArrowDown');
|
|
428
429
|
expect(document.activeElement).toBe(radio);
|
|
429
430
|
pressKey('ArrowRight');
|
|
@@ -431,7 +432,7 @@ describe('SpatialNavigationService', () => {
|
|
|
431
432
|
});
|
|
432
433
|
});
|
|
433
434
|
it('Should not intercept Up/Down on number input (increment/decrement)', async () => {
|
|
434
|
-
await usingAsync(
|
|
435
|
+
await usingAsync(createInjector(), async (i) => {
|
|
435
436
|
const input = document.createElement('input');
|
|
436
437
|
input.type = 'number';
|
|
437
438
|
input.value = '5';
|
|
@@ -439,7 +440,7 @@ describe('SpatialNavigationService', () => {
|
|
|
439
440
|
const btn = createButton('btn', { left: 0, top: 100, width: 50, height: 50 });
|
|
440
441
|
document.body.append(input, btn);
|
|
441
442
|
input.focus();
|
|
442
|
-
i.
|
|
443
|
+
i.get(SpatialNavigationService);
|
|
443
444
|
pressKey('ArrowUp');
|
|
444
445
|
expect(document.activeElement).toBe(input);
|
|
445
446
|
pressKey('ArrowDown');
|
|
@@ -447,7 +448,7 @@ describe('SpatialNavigationService', () => {
|
|
|
447
448
|
});
|
|
448
449
|
});
|
|
449
450
|
it('Should intercept Left/Right on number input for spatial navigation', async () => {
|
|
450
|
-
await usingAsync(
|
|
451
|
+
await usingAsync(createInjector(), async (i) => {
|
|
451
452
|
const input = document.createElement('input');
|
|
452
453
|
input.type = 'number';
|
|
453
454
|
input.value = '5';
|
|
@@ -457,20 +458,20 @@ describe('SpatialNavigationService', () => {
|
|
|
457
458
|
const btnRight = createButton('btn-right', { left: 400, top: 0, width: 50, height: 30 });
|
|
458
459
|
document.body.append(btnLeft, input, btnRight);
|
|
459
460
|
input.focus();
|
|
460
|
-
i.
|
|
461
|
+
i.get(SpatialNavigationService);
|
|
461
462
|
pressKey('ArrowRight');
|
|
462
463
|
expect(document.activeElement).toBe(btnRight);
|
|
463
464
|
});
|
|
464
465
|
});
|
|
465
466
|
it('Should not intercept arrow keys on date input', async () => {
|
|
466
|
-
await usingAsync(
|
|
467
|
+
await usingAsync(createInjector(), async (i) => {
|
|
467
468
|
const input = document.createElement('input');
|
|
468
469
|
input.type = 'date';
|
|
469
470
|
mockRect(input, { left: 0, top: 0, width: 200, height: 30 });
|
|
470
471
|
const btn = createButton('btn', { left: 300, top: 0, width: 50, height: 50 });
|
|
471
472
|
document.body.append(input, btn);
|
|
472
473
|
input.focus();
|
|
473
|
-
i.
|
|
474
|
+
i.get(SpatialNavigationService);
|
|
474
475
|
pressKey('ArrowRight');
|
|
475
476
|
expect(document.activeElement).toBe(input);
|
|
476
477
|
pressKey('ArrowUp');
|
|
@@ -478,14 +479,14 @@ describe('SpatialNavigationService', () => {
|
|
|
478
479
|
});
|
|
479
480
|
});
|
|
480
481
|
it('Should not intercept arrow keys on time input', async () => {
|
|
481
|
-
await usingAsync(
|
|
482
|
+
await usingAsync(createInjector(), async (i) => {
|
|
482
483
|
const input = document.createElement('input');
|
|
483
484
|
input.type = 'time';
|
|
484
485
|
mockRect(input, { left: 0, top: 0, width: 200, height: 30 });
|
|
485
486
|
const btn = createButton('btn', { left: 300, top: 0, width: 50, height: 50 });
|
|
486
487
|
document.body.append(input, btn);
|
|
487
488
|
input.focus();
|
|
488
|
-
i.
|
|
489
|
+
i.get(SpatialNavigationService);
|
|
489
490
|
pressKey('ArrowDown');
|
|
490
491
|
expect(document.activeElement).toBe(input);
|
|
491
492
|
pressKey('ArrowLeft');
|
|
@@ -493,7 +494,7 @@ describe('SpatialNavigationService', () => {
|
|
|
493
494
|
});
|
|
494
495
|
});
|
|
495
496
|
it('Should escape empty text input on any arrow key', async () => {
|
|
496
|
-
await usingAsync(
|
|
497
|
+
await usingAsync(createInjector(), async (i) => {
|
|
497
498
|
const input = document.createElement('input');
|
|
498
499
|
input.type = 'text';
|
|
499
500
|
mockRect(input, { left: 0, top: 0, width: 200, height: 30 });
|
|
@@ -501,13 +502,13 @@ describe('SpatialNavigationService', () => {
|
|
|
501
502
|
const btn = createButton('btn', { left: 300, top: 0, width: 50, height: 50 });
|
|
502
503
|
document.body.append(input, btn);
|
|
503
504
|
input.focus();
|
|
504
|
-
i.
|
|
505
|
+
i.get(SpatialNavigationService);
|
|
505
506
|
pressKey('ArrowRight');
|
|
506
507
|
expect(document.activeElement).toBe(btn);
|
|
507
508
|
});
|
|
508
509
|
});
|
|
509
510
|
it('Should not intercept Enter on text input', async () => {
|
|
510
|
-
await usingAsync(
|
|
511
|
+
await usingAsync(createInjector(), async (i) => {
|
|
511
512
|
const input = document.createElement('input');
|
|
512
513
|
input.type = 'text';
|
|
513
514
|
mockRect(input, { left: 0, top: 0, width: 200, height: 30 });
|
|
@@ -515,13 +516,13 @@ describe('SpatialNavigationService', () => {
|
|
|
515
516
|
input.addEventListener('click', clickHandler);
|
|
516
517
|
document.body.append(input);
|
|
517
518
|
input.focus();
|
|
518
|
-
i.
|
|
519
|
+
i.get(SpatialNavigationService);
|
|
519
520
|
pressKey('Enter');
|
|
520
521
|
expect(clickHandler).not.toHaveBeenCalled();
|
|
521
522
|
});
|
|
522
523
|
});
|
|
523
524
|
it('Should not intercept Enter on textarea', async () => {
|
|
524
|
-
await usingAsync(
|
|
525
|
+
await usingAsync(createInjector(), async (i) => {
|
|
525
526
|
const textarea = document.createElement('textarea');
|
|
526
527
|
textarea.value = 'hello';
|
|
527
528
|
mockRect(textarea, { left: 0, top: 0, width: 200, height: 100 });
|
|
@@ -529,13 +530,13 @@ describe('SpatialNavigationService', () => {
|
|
|
529
530
|
textarea.addEventListener('click', clickHandler);
|
|
530
531
|
document.body.append(textarea);
|
|
531
532
|
textarea.focus();
|
|
532
|
-
i.
|
|
533
|
+
i.get(SpatialNavigationService);
|
|
533
534
|
pressKey('Enter');
|
|
534
535
|
expect(clickHandler).not.toHaveBeenCalled();
|
|
535
536
|
});
|
|
536
537
|
});
|
|
537
538
|
it('Should activate Enter on button-type input', async () => {
|
|
538
|
-
await usingAsync(
|
|
539
|
+
await usingAsync(createInjector(), async (i) => {
|
|
539
540
|
const input = document.createElement('input');
|
|
540
541
|
input.type = 'button';
|
|
541
542
|
mockRect(input, { left: 0, top: 0, width: 50, height: 30 });
|
|
@@ -543,7 +544,7 @@ describe('SpatialNavigationService', () => {
|
|
|
543
544
|
input.addEventListener('click', clickHandler);
|
|
544
545
|
document.body.append(input);
|
|
545
546
|
input.focus();
|
|
546
|
-
i.
|
|
547
|
+
i.get(SpatialNavigationService);
|
|
547
548
|
pressKey('Enter');
|
|
548
549
|
expect(clickHandler).toHaveBeenCalledTimes(1);
|
|
549
550
|
});
|
|
@@ -551,7 +552,7 @@ describe('SpatialNavigationService', () => {
|
|
|
551
552
|
});
|
|
552
553
|
describe('data-spatial-nav-passthrough', () => {
|
|
553
554
|
it('Should not intercept arrow keys inside a passthrough container', async () => {
|
|
554
|
-
await usingAsync(
|
|
555
|
+
await usingAsync(createInjector(), async (i) => {
|
|
555
556
|
const container = document.createElement('div');
|
|
556
557
|
container.setAttribute('data-spatial-nav-passthrough', '');
|
|
557
558
|
const inner = document.createElement('input');
|
|
@@ -561,13 +562,13 @@ describe('SpatialNavigationService', () => {
|
|
|
561
562
|
const btn = createButton('btn', { left: 100, top: 0, width: 50, height: 50 });
|
|
562
563
|
document.body.append(container, btn);
|
|
563
564
|
inner.focus();
|
|
564
|
-
i.
|
|
565
|
+
i.get(SpatialNavigationService);
|
|
565
566
|
pressKey('ArrowRight');
|
|
566
567
|
expect(document.activeElement).toBe(inner);
|
|
567
568
|
});
|
|
568
569
|
});
|
|
569
570
|
it('Should not intercept Enter inside a passthrough container', async () => {
|
|
570
|
-
await usingAsync(
|
|
571
|
+
await usingAsync(createInjector(), async (i) => {
|
|
571
572
|
const container = document.createElement('div');
|
|
572
573
|
container.setAttribute('data-spatial-nav-passthrough', '');
|
|
573
574
|
const inner = createButton('inner', { left: 0, top: 0, width: 50, height: 50 });
|
|
@@ -576,13 +577,13 @@ describe('SpatialNavigationService', () => {
|
|
|
576
577
|
container.append(inner);
|
|
577
578
|
document.body.append(container);
|
|
578
579
|
inner.focus();
|
|
579
|
-
i.
|
|
580
|
+
i.get(SpatialNavigationService);
|
|
580
581
|
pressKey('Enter');
|
|
581
582
|
expect(clickHandler).not.toHaveBeenCalled();
|
|
582
583
|
});
|
|
583
584
|
});
|
|
584
585
|
it('Should still intercept keys outside a passthrough container', async () => {
|
|
585
|
-
await usingAsync(
|
|
586
|
+
await usingAsync(createInjector(), async (i) => {
|
|
586
587
|
const container = document.createElement('div');
|
|
587
588
|
container.setAttribute('data-spatial-nav-passthrough', '');
|
|
588
589
|
const inner = document.createElement('input');
|
|
@@ -593,7 +594,7 @@ describe('SpatialNavigationService', () => {
|
|
|
593
594
|
const right = createButton('right', { left: 100, top: 0, width: 50, height: 50 });
|
|
594
595
|
document.body.append(left, right, container);
|
|
595
596
|
left.focus();
|
|
596
|
-
i.
|
|
597
|
+
i.get(SpatialNavigationService);
|
|
597
598
|
pressKey('ArrowRight');
|
|
598
599
|
expect(document.activeElement).toBe(right);
|
|
599
600
|
});
|
|
@@ -601,7 +602,7 @@ describe('SpatialNavigationService', () => {
|
|
|
601
602
|
});
|
|
602
603
|
describe('section navigation', () => {
|
|
603
604
|
it('Should scope navigation within the active section', async () => {
|
|
604
|
-
await usingAsync(
|
|
605
|
+
await usingAsync(createInjector(), async (i) => {
|
|
605
606
|
const section1 = document.createElement('div');
|
|
606
607
|
section1.setAttribute('data-nav-section', 'sidebar');
|
|
607
608
|
mockRect(section1, { left: 0, top: 0, width: 200, height: 400 });
|
|
@@ -615,20 +616,20 @@ describe('SpatialNavigationService', () => {
|
|
|
615
616
|
section2.append(btn2, btn3);
|
|
616
617
|
document.body.append(section1, section2);
|
|
617
618
|
btn2.focus();
|
|
618
|
-
const s = i.
|
|
619
|
+
const s = i.get(SpatialNavigationService);
|
|
619
620
|
s.moveFocus('down');
|
|
620
621
|
expect(document.activeElement).toBe(btn3);
|
|
621
622
|
});
|
|
622
623
|
});
|
|
623
624
|
it('Should update activeSection when focus moves', async () => {
|
|
624
|
-
await usingAsync(
|
|
625
|
+
await usingAsync(createInjector(), async (i) => {
|
|
625
626
|
const section = document.createElement('div');
|
|
626
627
|
section.setAttribute('data-nav-section', 'main');
|
|
627
628
|
const btn = createButton('btn', { left: 0, top: 0, width: 50, height: 50 });
|
|
628
629
|
section.append(btn);
|
|
629
630
|
document.body.append(section);
|
|
630
631
|
btn.focus();
|
|
631
|
-
const s = i.
|
|
632
|
+
const s = i.get(SpatialNavigationService);
|
|
632
633
|
s.moveFocus('down');
|
|
633
634
|
expect(s.activeSection.getValue()).toBe('main');
|
|
634
635
|
});
|
|
@@ -636,7 +637,7 @@ describe('SpatialNavigationService', () => {
|
|
|
636
637
|
});
|
|
637
638
|
describe('cross-section navigation', () => {
|
|
638
639
|
it('Should navigate to adjacent section when no candidate in current section', async () => {
|
|
639
|
-
await usingAsync(
|
|
640
|
+
await usingAsync(createInjector(), async (i) => {
|
|
640
641
|
const section1 = document.createElement('div');
|
|
641
642
|
section1.setAttribute('data-nav-section', 'left');
|
|
642
643
|
mockRect(section1, { left: 0, top: 0, width: 200, height: 400 });
|
|
@@ -650,14 +651,14 @@ describe('SpatialNavigationService', () => {
|
|
|
650
651
|
section2.append(btn2);
|
|
651
652
|
document.body.append(section1, section2);
|
|
652
653
|
btn1.focus();
|
|
653
|
-
const s = i.
|
|
654
|
+
const s = i.get(SpatialNavigationService);
|
|
654
655
|
s.moveFocus('right');
|
|
655
656
|
expect(document.activeElement).toBe(btn2);
|
|
656
657
|
expect(s.activeSection.getValue()).toBe('right');
|
|
657
658
|
});
|
|
658
659
|
});
|
|
659
660
|
it('Should not navigate cross-section when disabled', async () => {
|
|
660
|
-
await usingAsync(
|
|
661
|
+
await usingAsync(createInjector(), async (i) => {
|
|
661
662
|
configureSpatialNavigation(i, { crossSectionNavigation: false });
|
|
662
663
|
const section1 = document.createElement('div');
|
|
663
664
|
section1.setAttribute('data-nav-section', 'left');
|
|
@@ -671,7 +672,7 @@ describe('SpatialNavigationService', () => {
|
|
|
671
672
|
section2.append(btn2);
|
|
672
673
|
document.body.append(section1, section2);
|
|
673
674
|
btn1.focus();
|
|
674
|
-
const s = i.
|
|
675
|
+
const s = i.get(SpatialNavigationService);
|
|
675
676
|
s.moveFocus('right');
|
|
676
677
|
expect(document.activeElement).toBe(btn1);
|
|
677
678
|
});
|
|
@@ -679,7 +680,7 @@ describe('SpatialNavigationService', () => {
|
|
|
679
680
|
});
|
|
680
681
|
describe('focus memory', () => {
|
|
681
682
|
it('Should remember and restore focus when returning to a section', async () => {
|
|
682
|
-
await usingAsync(
|
|
683
|
+
await usingAsync(createInjector(), async (i) => {
|
|
683
684
|
const section1 = document.createElement('div');
|
|
684
685
|
section1.setAttribute('data-nav-section', 'left');
|
|
685
686
|
mockRect(section1, { left: 0, top: 0, width: 200, height: 400 });
|
|
@@ -694,7 +695,7 @@ describe('SpatialNavigationService', () => {
|
|
|
694
695
|
section2.append(btn2);
|
|
695
696
|
document.body.append(section1, section2);
|
|
696
697
|
btn1b.focus();
|
|
697
|
-
const s = i.
|
|
698
|
+
const s = i.get(SpatialNavigationService);
|
|
698
699
|
// Navigate away from section1
|
|
699
700
|
s.moveFocus('right');
|
|
700
701
|
expect(document.activeElement).toBe(btn2);
|
|
@@ -705,7 +706,7 @@ describe('SpatialNavigationService', () => {
|
|
|
705
706
|
});
|
|
706
707
|
});
|
|
707
708
|
it('Should fall back to nearest element when remembered element is removed', async () => {
|
|
708
|
-
await usingAsync(
|
|
709
|
+
await usingAsync(createInjector(), async (i) => {
|
|
709
710
|
const section1 = document.createElement('div');
|
|
710
711
|
section1.setAttribute('data-nav-section', 'left');
|
|
711
712
|
mockRect(section1, { left: 0, top: 0, width: 200, height: 400 });
|
|
@@ -722,7 +723,7 @@ describe('SpatialNavigationService', () => {
|
|
|
722
723
|
document.body.append(section1, section2);
|
|
723
724
|
// Focus btn2a, navigate to section1
|
|
724
725
|
btn2a.focus();
|
|
725
|
-
const s = i.
|
|
726
|
+
const s = i.get(SpatialNavigationService);
|
|
726
727
|
s.moveFocus('left');
|
|
727
728
|
// Remove btn2a from DOM
|
|
728
729
|
btn2a.remove();
|
|
@@ -732,7 +733,7 @@ describe('SpatialNavigationService', () => {
|
|
|
732
733
|
});
|
|
733
734
|
});
|
|
734
735
|
it('Should fall back to nearest element when remembered element becomes disabled', async () => {
|
|
735
|
-
await usingAsync(
|
|
736
|
+
await usingAsync(createInjector(), async (i) => {
|
|
736
737
|
const section1 = document.createElement('div');
|
|
737
738
|
section1.setAttribute('data-nav-section', 'left');
|
|
738
739
|
mockRect(section1, { left: 0, top: 0, width: 200, height: 400 });
|
|
@@ -748,7 +749,7 @@ describe('SpatialNavigationService', () => {
|
|
|
748
749
|
section2.append(btn2a, btn2b);
|
|
749
750
|
document.body.append(section1, section2);
|
|
750
751
|
btn2a.focus();
|
|
751
|
-
const s = i.
|
|
752
|
+
const s = i.get(SpatialNavigationService);
|
|
752
753
|
s.moveFocus('left');
|
|
753
754
|
// Disable btn2a after navigating away
|
|
754
755
|
btn2a.disabled = true;
|
|
@@ -760,12 +761,12 @@ describe('SpatialNavigationService', () => {
|
|
|
760
761
|
});
|
|
761
762
|
describe('enabled toggle', () => {
|
|
762
763
|
it('Should stop handling keys when disabled', async () => {
|
|
763
|
-
await usingAsync(
|
|
764
|
+
await usingAsync(createInjector(), async (i) => {
|
|
764
765
|
const left = createButton('left', { left: 0, top: 0, width: 50, height: 50 });
|
|
765
766
|
const right = createButton('right', { left: 100, top: 0, width: 50, height: 50 });
|
|
766
767
|
document.body.append(left, right);
|
|
767
768
|
left.focus();
|
|
768
|
-
const s = i.
|
|
769
|
+
const s = i.get(SpatialNavigationService);
|
|
769
770
|
s.enabled.setValue(false);
|
|
770
771
|
pressKey('ArrowRight');
|
|
771
772
|
expect(document.activeElement).toBe(left);
|
|
@@ -778,8 +779,8 @@ describe('SpatialNavigationService', () => {
|
|
|
778
779
|
describe('disposal', () => {
|
|
779
780
|
it('Should remove keydown listener on dispose', async () => {
|
|
780
781
|
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
|
|
781
|
-
await usingAsync(
|
|
782
|
-
i.
|
|
782
|
+
await usingAsync(createInjector(), async (i) => {
|
|
783
|
+
i.get(SpatialNavigationService);
|
|
783
784
|
});
|
|
784
785
|
expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function));
|
|
785
786
|
});
|
|
@@ -788,8 +789,8 @@ describe('SpatialNavigationService', () => {
|
|
|
788
789
|
const right = createButton('right', { left: 100, top: 0, width: 50, height: 50 });
|
|
789
790
|
document.body.append(left, right);
|
|
790
791
|
left.focus();
|
|
791
|
-
await usingAsync(
|
|
792
|
-
i.
|
|
792
|
+
await usingAsync(createInjector(), async (i) => {
|
|
793
|
+
i.get(SpatialNavigationService);
|
|
793
794
|
});
|
|
794
795
|
pressKey('ArrowRight');
|
|
795
796
|
expect(document.activeElement).toBe(left);
|
|
@@ -797,30 +798,30 @@ describe('SpatialNavigationService', () => {
|
|
|
797
798
|
});
|
|
798
799
|
describe('backspace and escape', () => {
|
|
799
800
|
it('Should call history.back() on Backspace when configured', async () => {
|
|
800
|
-
await usingAsync(
|
|
801
|
+
await usingAsync(createInjector(), async (i) => {
|
|
801
802
|
configureSpatialNavigation(i, { backspaceGoesBack: true });
|
|
802
803
|
const btn = createButton('btn', { left: 0, top: 0, width: 50, height: 50 });
|
|
803
804
|
document.body.append(btn);
|
|
804
805
|
btn.focus();
|
|
805
806
|
const backSpy = vi.spyOn(history, 'back').mockImplementation(() => { });
|
|
806
|
-
i.
|
|
807
|
+
i.get(SpatialNavigationService);
|
|
807
808
|
pressKey('Backspace');
|
|
808
809
|
expect(backSpy).toHaveBeenCalledTimes(1);
|
|
809
810
|
});
|
|
810
811
|
});
|
|
811
812
|
it('Should not call history.back() on Backspace by default', async () => {
|
|
812
|
-
await usingAsync(
|
|
813
|
+
await usingAsync(createInjector(), async (i) => {
|
|
813
814
|
const btn = createButton('btn', { left: 0, top: 0, width: 50, height: 50 });
|
|
814
815
|
document.body.append(btn);
|
|
815
816
|
btn.focus();
|
|
816
817
|
const backSpy = vi.spyOn(history, 'back').mockImplementation(() => { });
|
|
817
|
-
i.
|
|
818
|
+
i.get(SpatialNavigationService);
|
|
818
819
|
pressKey('Backspace');
|
|
819
820
|
expect(backSpy).not.toHaveBeenCalled();
|
|
820
821
|
});
|
|
821
822
|
});
|
|
822
823
|
it('Should not call history.back() on Backspace when focused on a text input', async () => {
|
|
823
|
-
await usingAsync(
|
|
824
|
+
await usingAsync(createInjector(), async (i) => {
|
|
824
825
|
configureSpatialNavigation(i, { backspaceGoesBack: true });
|
|
825
826
|
const input = document.createElement('input');
|
|
826
827
|
input.type = 'text';
|
|
@@ -829,13 +830,13 @@ describe('SpatialNavigationService', () => {
|
|
|
829
830
|
document.body.append(input);
|
|
830
831
|
input.focus();
|
|
831
832
|
const backSpy = vi.spyOn(history, 'back').mockImplementation(() => { });
|
|
832
|
-
i.
|
|
833
|
+
i.get(SpatialNavigationService);
|
|
833
834
|
pressKey('Backspace');
|
|
834
835
|
expect(backSpy).not.toHaveBeenCalled();
|
|
835
836
|
});
|
|
836
837
|
});
|
|
837
838
|
it('Should not call history.back() on Backspace when focused on a textarea', async () => {
|
|
838
|
-
await usingAsync(
|
|
839
|
+
await usingAsync(createInjector(), async (i) => {
|
|
839
840
|
configureSpatialNavigation(i, { backspaceGoesBack: true });
|
|
840
841
|
const textarea = document.createElement('textarea');
|
|
841
842
|
textarea.value = 'some text';
|
|
@@ -843,13 +844,13 @@ describe('SpatialNavigationService', () => {
|
|
|
843
844
|
document.body.append(textarea);
|
|
844
845
|
textarea.focus();
|
|
845
846
|
const backSpy = vi.spyOn(history, 'back').mockImplementation(() => { });
|
|
846
|
-
i.
|
|
847
|
+
i.get(SpatialNavigationService);
|
|
847
848
|
pressKey('Backspace');
|
|
848
849
|
expect(backSpy).not.toHaveBeenCalled();
|
|
849
850
|
});
|
|
850
851
|
});
|
|
851
852
|
it('Should move to parent section on Escape when configured', async () => {
|
|
852
|
-
await usingAsync(
|
|
853
|
+
await usingAsync(createInjector(), async (i) => {
|
|
853
854
|
configureSpatialNavigation(i, { escapeGoesToParentSection: true });
|
|
854
855
|
const outer = document.createElement('div');
|
|
855
856
|
outer.setAttribute('data-nav-section', 'outer');
|
|
@@ -861,26 +862,26 @@ describe('SpatialNavigationService', () => {
|
|
|
861
862
|
outer.append(outerBtn, inner);
|
|
862
863
|
document.body.append(outer);
|
|
863
864
|
innerBtn.focus();
|
|
864
|
-
i.
|
|
865
|
+
i.get(SpatialNavigationService);
|
|
865
866
|
pressKey('Escape');
|
|
866
867
|
expect(document.activeElement).toBe(outerBtn);
|
|
867
868
|
});
|
|
868
869
|
});
|
|
869
870
|
it('Should blur a range input on Escape', async () => {
|
|
870
|
-
await usingAsync(
|
|
871
|
+
await usingAsync(createInjector(), async (i) => {
|
|
871
872
|
const input = document.createElement('input');
|
|
872
873
|
input.type = 'range';
|
|
873
874
|
mockRect(input, { left: 0, top: 0, width: 200, height: 30 });
|
|
874
875
|
document.body.append(input);
|
|
875
876
|
input.focus();
|
|
876
877
|
expect(document.activeElement).toBe(input);
|
|
877
|
-
i.
|
|
878
|
+
i.get(SpatialNavigationService);
|
|
878
879
|
pressKey('Escape');
|
|
879
880
|
expect(document.activeElement).toBe(document.body);
|
|
880
881
|
});
|
|
881
882
|
});
|
|
882
883
|
it('Should blur a radio input on Escape', async () => {
|
|
883
|
-
await usingAsync(
|
|
884
|
+
await usingAsync(createInjector(), async (i) => {
|
|
884
885
|
const radio = document.createElement('input');
|
|
885
886
|
radio.type = 'radio';
|
|
886
887
|
radio.name = 'group';
|
|
@@ -888,39 +889,39 @@ describe('SpatialNavigationService', () => {
|
|
|
888
889
|
document.body.append(radio);
|
|
889
890
|
radio.focus();
|
|
890
891
|
expect(document.activeElement).toBe(radio);
|
|
891
|
-
i.
|
|
892
|
+
i.get(SpatialNavigationService);
|
|
892
893
|
pressKey('Escape');
|
|
893
894
|
expect(document.activeElement).toBe(document.body);
|
|
894
895
|
});
|
|
895
896
|
});
|
|
896
897
|
it('Should blur a date input on Escape', async () => {
|
|
897
|
-
await usingAsync(
|
|
898
|
+
await usingAsync(createInjector(), async (i) => {
|
|
898
899
|
const input = document.createElement('input');
|
|
899
900
|
input.type = 'date';
|
|
900
901
|
mockRect(input, { left: 0, top: 0, width: 200, height: 30 });
|
|
901
902
|
document.body.append(input);
|
|
902
903
|
input.focus();
|
|
903
904
|
expect(document.activeElement).toBe(input);
|
|
904
|
-
i.
|
|
905
|
+
i.get(SpatialNavigationService);
|
|
905
906
|
pressKey('Escape');
|
|
906
907
|
expect(document.activeElement).toBe(document.body);
|
|
907
908
|
});
|
|
908
909
|
});
|
|
909
910
|
it('Should blur a time input on Escape', async () => {
|
|
910
|
-
await usingAsync(
|
|
911
|
+
await usingAsync(createInjector(), async (i) => {
|
|
911
912
|
const input = document.createElement('input');
|
|
912
913
|
input.type = 'time';
|
|
913
914
|
mockRect(input, { left: 0, top: 0, width: 200, height: 30 });
|
|
914
915
|
document.body.append(input);
|
|
915
916
|
input.focus();
|
|
916
917
|
expect(document.activeElement).toBe(input);
|
|
917
|
-
i.
|
|
918
|
+
i.get(SpatialNavigationService);
|
|
918
919
|
pressKey('Escape');
|
|
919
920
|
expect(document.activeElement).toBe(document.body);
|
|
920
921
|
});
|
|
921
922
|
});
|
|
922
923
|
it('Should blur a number input on Escape', async () => {
|
|
923
|
-
await usingAsync(
|
|
924
|
+
await usingAsync(createInjector(), async (i) => {
|
|
924
925
|
const input = document.createElement('input');
|
|
925
926
|
input.type = 'number';
|
|
926
927
|
input.value = '5';
|
|
@@ -928,13 +929,13 @@ describe('SpatialNavigationService', () => {
|
|
|
928
929
|
document.body.append(input);
|
|
929
930
|
input.focus();
|
|
930
931
|
expect(document.activeElement).toBe(input);
|
|
931
|
-
i.
|
|
932
|
+
i.get(SpatialNavigationService);
|
|
932
933
|
pressKey('Escape');
|
|
933
934
|
expect(document.activeElement).toBe(document.body);
|
|
934
935
|
});
|
|
935
936
|
});
|
|
936
937
|
it('Should blur a text input on Escape', async () => {
|
|
937
|
-
await usingAsync(
|
|
938
|
+
await usingAsync(createInjector(), async (i) => {
|
|
938
939
|
const input = document.createElement('input');
|
|
939
940
|
input.type = 'text';
|
|
940
941
|
input.value = 'hello';
|
|
@@ -942,31 +943,31 @@ describe('SpatialNavigationService', () => {
|
|
|
942
943
|
document.body.append(input);
|
|
943
944
|
input.focus();
|
|
944
945
|
expect(document.activeElement).toBe(input);
|
|
945
|
-
i.
|
|
946
|
+
i.get(SpatialNavigationService);
|
|
946
947
|
pressKey('Escape');
|
|
947
948
|
expect(document.activeElement).toBe(document.body);
|
|
948
949
|
});
|
|
949
950
|
});
|
|
950
951
|
it('Should blur a textarea on Escape', async () => {
|
|
951
|
-
await usingAsync(
|
|
952
|
+
await usingAsync(createInjector(), async (i) => {
|
|
952
953
|
const textarea = document.createElement('textarea');
|
|
953
954
|
textarea.value = 'hello';
|
|
954
955
|
mockRect(textarea, { left: 0, top: 0, width: 200, height: 100 });
|
|
955
956
|
document.body.append(textarea);
|
|
956
957
|
textarea.focus();
|
|
957
958
|
expect(document.activeElement).toBe(textarea);
|
|
958
|
-
i.
|
|
959
|
+
i.get(SpatialNavigationService);
|
|
959
960
|
pressKey('Escape');
|
|
960
961
|
expect(document.activeElement).toBe(document.body);
|
|
961
962
|
});
|
|
962
963
|
});
|
|
963
964
|
it('Should not blur a button on Escape', async () => {
|
|
964
|
-
await usingAsync(
|
|
965
|
+
await usingAsync(createInjector(), async (i) => {
|
|
965
966
|
const btn = createButton('btn', { left: 0, top: 0, width: 50, height: 50 });
|
|
966
967
|
document.body.append(btn);
|
|
967
968
|
btn.focus();
|
|
968
969
|
expect(document.activeElement).toBe(btn);
|
|
969
|
-
i.
|
|
970
|
+
i.get(SpatialNavigationService);
|
|
970
971
|
pressKey('Escape');
|
|
971
972
|
expect(document.activeElement).toBe(btn);
|
|
972
973
|
});
|
|
@@ -974,7 +975,7 @@ describe('SpatialNavigationService', () => {
|
|
|
974
975
|
});
|
|
975
976
|
describe('data-spatial-nav-target', () => {
|
|
976
977
|
it('Should treat elements with data-spatial-nav-target as focusable candidates', async () => {
|
|
977
|
-
await usingAsync(
|
|
978
|
+
await usingAsync(createInjector(), async (i) => {
|
|
978
979
|
const origin = createButton('origin', { left: 0, top: 0, width: 50, height: 50 });
|
|
979
980
|
const target = document.createElement('div');
|
|
980
981
|
target.setAttribute('data-spatial-nav-target', '');
|
|
@@ -984,7 +985,7 @@ describe('SpatialNavigationService', () => {
|
|
|
984
985
|
target.scrollIntoView = vi.fn();
|
|
985
986
|
document.body.append(origin, target);
|
|
986
987
|
origin.focus();
|
|
987
|
-
const s = i.
|
|
988
|
+
const s = i.get(SpatialNavigationService);
|
|
988
989
|
s.moveFocus('right');
|
|
989
990
|
expect(document.activeElement).toBe(target);
|
|
990
991
|
});
|
|
@@ -992,7 +993,7 @@ describe('SpatialNavigationService', () => {
|
|
|
992
993
|
});
|
|
993
994
|
describe('overflow-aware visibility', () => {
|
|
994
995
|
it('Should skip candidates whose center is outside their overflow container', async () => {
|
|
995
|
-
await usingAsync(
|
|
996
|
+
await usingAsync(createInjector(), async (i) => {
|
|
996
997
|
const container = document.createElement('div');
|
|
997
998
|
Object.defineProperty(container, 'computedStyleMap', { value: () => new Map() });
|
|
998
999
|
vi.spyOn(window, 'getComputedStyle').mockImplementation((el) => {
|
|
@@ -1008,7 +1009,7 @@ describe('SpatialNavigationService', () => {
|
|
|
1008
1009
|
const origin = createButton('origin', { left: 0, top: 100, width: 50, height: 50 });
|
|
1009
1010
|
document.body.append(container, origin);
|
|
1010
1011
|
origin.focus();
|
|
1011
|
-
const s = i.
|
|
1012
|
+
const s = i.get(SpatialNavigationService);
|
|
1012
1013
|
s.moveFocus('up');
|
|
1013
1014
|
expect(document.activeElement).toBe(visible);
|
|
1014
1015
|
});
|
|
@@ -1016,7 +1017,7 @@ describe('SpatialNavigationService', () => {
|
|
|
1016
1017
|
});
|
|
1017
1018
|
describe('focus trap', () => {
|
|
1018
1019
|
it('Should block cross-section navigation when a trap is active', async () => {
|
|
1019
|
-
await usingAsync(
|
|
1020
|
+
await usingAsync(createInjector(), async (i) => {
|
|
1020
1021
|
const section1 = document.createElement('div');
|
|
1021
1022
|
section1.setAttribute('data-nav-section', 'modal');
|
|
1022
1023
|
mockRect(section1, { left: 0, top: 0, width: 400, height: 400 });
|
|
@@ -1029,22 +1030,22 @@ describe('SpatialNavigationService', () => {
|
|
|
1029
1030
|
section2.append(btn2);
|
|
1030
1031
|
document.body.append(section1, section2);
|
|
1031
1032
|
btn1.focus();
|
|
1032
|
-
const s = i.
|
|
1033
|
+
const s = i.get(SpatialNavigationService);
|
|
1033
1034
|
s.pushFocusTrap('modal');
|
|
1034
1035
|
s.moveFocus('right');
|
|
1035
1036
|
expect(document.activeElement).toBe(btn1);
|
|
1036
1037
|
});
|
|
1037
1038
|
});
|
|
1038
1039
|
it('Should set activeSection when pushing a trap', async () => {
|
|
1039
|
-
await usingAsync(
|
|
1040
|
-
const s = i.
|
|
1040
|
+
await usingAsync(createInjector(), async (i) => {
|
|
1041
|
+
const s = i.get(SpatialNavigationService);
|
|
1041
1042
|
s.pushFocusTrap('dialog');
|
|
1042
1043
|
expect(s.activeSection.getValue()).toBe('dialog');
|
|
1043
1044
|
});
|
|
1044
1045
|
});
|
|
1045
1046
|
it('Should restore previous section on pop', async () => {
|
|
1046
|
-
await usingAsync(
|
|
1047
|
-
const s = i.
|
|
1047
|
+
await usingAsync(createInjector(), async (i) => {
|
|
1048
|
+
const s = i.get(SpatialNavigationService);
|
|
1048
1049
|
s.activeSection.setValue('main');
|
|
1049
1050
|
s.pushFocusTrap('dialog');
|
|
1050
1051
|
expect(s.activeSection.getValue()).toBe('dialog');
|
|
@@ -1053,8 +1054,8 @@ describe('SpatialNavigationService', () => {
|
|
|
1053
1054
|
});
|
|
1054
1055
|
});
|
|
1055
1056
|
it('Should support nested traps — topmost wins', async () => {
|
|
1056
|
-
await usingAsync(
|
|
1057
|
-
const s = i.
|
|
1057
|
+
await usingAsync(createInjector(), async (i) => {
|
|
1058
|
+
const s = i.get(SpatialNavigationService);
|
|
1058
1059
|
s.pushFocusTrap('outer-dialog');
|
|
1059
1060
|
s.pushFocusTrap('inner-dialog');
|
|
1060
1061
|
expect(s.activeSection.getValue()).toBe('inner-dialog');
|
|
@@ -1065,7 +1066,7 @@ describe('SpatialNavigationService', () => {
|
|
|
1065
1066
|
});
|
|
1066
1067
|
});
|
|
1067
1068
|
it('Should allow within-section navigation when trap is active', async () => {
|
|
1068
|
-
await usingAsync(
|
|
1069
|
+
await usingAsync(createInjector(), async (i) => {
|
|
1069
1070
|
const section = document.createElement('div');
|
|
1070
1071
|
section.setAttribute('data-nav-section', 'modal');
|
|
1071
1072
|
mockRect(section, { left: 0, top: 0, width: 400, height: 400 });
|
|
@@ -1074,14 +1075,14 @@ describe('SpatialNavigationService', () => {
|
|
|
1074
1075
|
section.append(btn1, btn2);
|
|
1075
1076
|
document.body.append(section);
|
|
1076
1077
|
btn1.focus();
|
|
1077
|
-
const s = i.
|
|
1078
|
+
const s = i.get(SpatialNavigationService);
|
|
1078
1079
|
s.pushFocusTrap('modal');
|
|
1079
1080
|
s.moveFocus('down');
|
|
1080
1081
|
expect(document.activeElement).toBe(btn2);
|
|
1081
1082
|
});
|
|
1082
1083
|
});
|
|
1083
1084
|
it('Should focus within trapped section when activeElement is document.body', async () => {
|
|
1084
|
-
await usingAsync(
|
|
1085
|
+
await usingAsync(createInjector(), async (i) => {
|
|
1085
1086
|
const section1 = document.createElement('div');
|
|
1086
1087
|
section1.setAttribute('data-nav-section', 'sidebar');
|
|
1087
1088
|
mockRect(section1, { left: 0, top: 0, width: 200, height: 400 });
|
|
@@ -1095,7 +1096,7 @@ describe('SpatialNavigationService', () => {
|
|
|
1095
1096
|
document.body.append(section1, section2);
|
|
1096
1097
|
document.activeElement?.blur();
|
|
1097
1098
|
expect(document.activeElement).toBe(document.body);
|
|
1098
|
-
const s = i.
|
|
1099
|
+
const s = i.get(SpatialNavigationService);
|
|
1099
1100
|
s.pushFocusTrap('modal');
|
|
1100
1101
|
s.moveFocus('down');
|
|
1101
1102
|
expect(document.activeElement).toBe(modalBtn);
|
|
@@ -1103,29 +1104,33 @@ describe('SpatialNavigationService', () => {
|
|
|
1103
1104
|
});
|
|
1104
1105
|
});
|
|
1105
1106
|
it('Should clear trap stack on disposal', async () => {
|
|
1106
|
-
await usingAsync(
|
|
1107
|
-
const s = i.
|
|
1107
|
+
await usingAsync(createInjector(), async (i) => {
|
|
1108
|
+
const s = i.get(SpatialNavigationService);
|
|
1108
1109
|
s.pushFocusTrap('dialog');
|
|
1109
1110
|
});
|
|
1110
1111
|
// After disposal, creating a new instance should have no traps
|
|
1111
|
-
await usingAsync(
|
|
1112
|
-
const s = i.
|
|
1112
|
+
await usingAsync(createInjector(), async (i) => {
|
|
1113
|
+
const s = i.get(SpatialNavigationService);
|
|
1113
1114
|
expect(s.activeSection.getValue()).toBeNull();
|
|
1114
1115
|
});
|
|
1115
1116
|
});
|
|
1116
1117
|
});
|
|
1117
1118
|
describe('configureSpatialNavigation', () => {
|
|
1118
1119
|
it('Should configure the service with custom options', async () => {
|
|
1119
|
-
await usingAsync(
|
|
1120
|
+
await usingAsync(createInjector(), async (i) => {
|
|
1120
1121
|
configureSpatialNavigation(i, { initiallyEnabled: false });
|
|
1121
|
-
const s = i.
|
|
1122
|
+
const s = i.get(SpatialNavigationService);
|
|
1122
1123
|
expect(s.enabled.getValue()).toBe(false);
|
|
1123
1124
|
});
|
|
1124
1125
|
});
|
|
1125
|
-
it('Should
|
|
1126
|
-
await usingAsync(
|
|
1127
|
-
i.
|
|
1128
|
-
expect(()
|
|
1126
|
+
it('Should rebind options when called after the service is instantiated', async () => {
|
|
1127
|
+
await usingAsync(createInjector(), async (i) => {
|
|
1128
|
+
const first = i.get(SpatialNavigationService);
|
|
1129
|
+
expect(first.enabled.getValue()).toBe(true);
|
|
1130
|
+
configureSpatialNavigation(i, { initiallyEnabled: false });
|
|
1131
|
+
const second = i.get(SpatialNavigationService);
|
|
1132
|
+
expect(second).not.toBe(first);
|
|
1133
|
+
expect(second.enabled.getValue()).toBe(false);
|
|
1129
1134
|
});
|
|
1130
1135
|
});
|
|
1131
1136
|
});
|