@furystack/shades 13.2.2 → 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.
Files changed (146) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/esm/component-factory.spec.d.ts.map +1 -1
  3. package/esm/component-factory.spec.js +2 -2
  4. package/esm/component-factory.spec.js.map +1 -1
  5. package/esm/components/define-nested-routes.d.ts +27 -0
  6. package/esm/components/define-nested-routes.d.ts.map +1 -0
  7. package/esm/components/define-nested-routes.js +26 -0
  8. package/esm/components/define-nested-routes.js.map +1 -0
  9. package/esm/components/define-nested-routes.spec.d.ts +2 -0
  10. package/esm/components/define-nested-routes.spec.d.ts.map +1 -0
  11. package/esm/components/define-nested-routes.spec.js +81 -0
  12. package/esm/components/define-nested-routes.spec.js.map +1 -0
  13. package/esm/components/index.d.ts +3 -3
  14. package/esm/components/index.d.ts.map +1 -1
  15. package/esm/components/index.js +3 -3
  16. package/esm/components/index.js.map +1 -1
  17. package/esm/components/nested-hooks.d.ts +62 -0
  18. package/esm/components/nested-hooks.d.ts.map +1 -0
  19. package/esm/components/nested-hooks.js +73 -0
  20. package/esm/components/nested-hooks.js.map +1 -0
  21. package/esm/components/nested-hooks.spec.d.ts +2 -0
  22. package/esm/components/nested-hooks.spec.d.ts.map +1 -0
  23. package/esm/components/nested-hooks.spec.js +211 -0
  24. package/esm/components/nested-hooks.spec.js.map +1 -0
  25. package/esm/components/nested-navigate.d.ts +52 -21
  26. package/esm/components/nested-navigate.d.ts.map +1 -1
  27. package/esm/components/nested-navigate.js +32 -17
  28. package/esm/components/nested-navigate.js.map +1 -1
  29. package/esm/components/nested-navigate.spec.js +109 -19
  30. package/esm/components/nested-navigate.spec.js.map +1 -1
  31. package/esm/components/nested-replace.d.ts +36 -0
  32. package/esm/components/nested-replace.d.ts.map +1 -0
  33. package/esm/components/nested-replace.js +38 -0
  34. package/esm/components/nested-replace.js.map +1 -0
  35. package/esm/components/nested-replace.spec.d.ts +2 -0
  36. package/esm/components/nested-replace.spec.d.ts.map +1 -0
  37. package/esm/components/nested-replace.spec.js +59 -0
  38. package/esm/components/nested-replace.spec.js.map +1 -0
  39. package/esm/components/nested-route-link.d.ts +36 -26
  40. package/esm/components/nested-route-link.d.ts.map +1 -1
  41. package/esm/components/nested-route-link.js +18 -20
  42. package/esm/components/nested-route-link.js.map +1 -1
  43. package/esm/components/nested-route-link.spec.js +67 -17
  44. package/esm/components/nested-route-link.spec.js.map +1 -1
  45. package/esm/components/nested-route-types.d.ts +79 -2
  46. package/esm/components/nested-route-types.d.ts.map +1 -1
  47. package/esm/components/nested-router.d.ts +77 -7
  48. package/esm/components/nested-router.d.ts.map +1 -1
  49. package/esm/components/nested-router.js +139 -13
  50. package/esm/components/nested-router.js.map +1 -1
  51. package/esm/components/nested-router.spec.js +283 -52
  52. package/esm/components/nested-router.spec.js.map +1 -1
  53. package/esm/services/location-service.d.ts +39 -42
  54. package/esm/services/location-service.d.ts.map +1 -1
  55. package/esm/services/location-service.js +115 -134
  56. package/esm/services/location-service.js.map +1 -1
  57. package/esm/services/location-service.spec.js +59 -24
  58. package/esm/services/location-service.spec.js.map +1 -1
  59. package/esm/services/route-match-service.d.ts +3 -2
  60. package/esm/services/route-match-service.d.ts.map +1 -1
  61. package/esm/services/route-match-service.js +13 -22
  62. package/esm/services/route-match-service.js.map +1 -1
  63. package/esm/services/route-match-service.spec.js +42 -49
  64. package/esm/services/route-match-service.spec.js.map +1 -1
  65. package/esm/services/route-meta-utils.d.ts +28 -6
  66. package/esm/services/route-meta-utils.d.ts.map +1 -1
  67. package/esm/services/route-meta-utils.js +18 -4
  68. package/esm/services/route-meta-utils.js.map +1 -1
  69. package/esm/services/route-meta-utils.spec.js +60 -5
  70. package/esm/services/route-meta-utils.spec.js.map +1 -1
  71. package/esm/services/screen-service.d.ts +13 -72
  72. package/esm/services/screen-service.d.ts.map +1 -1
  73. package/esm/services/screen-service.js +45 -137
  74. package/esm/services/screen-service.js.map +1 -1
  75. package/esm/services/screen-service.spec.js +19 -18
  76. package/esm/services/screen-service.spec.js.map +1 -1
  77. package/esm/services/spatial-navigation-service.d.ts +17 -39
  78. package/esm/services/spatial-navigation-service.d.ts.map +1 -1
  79. package/esm/services/spatial-navigation-service.js +280 -313
  80. package/esm/services/spatial-navigation-service.js.map +1 -1
  81. package/esm/services/spatial-navigation-service.spec.js +163 -158
  82. package/esm/services/spatial-navigation-service.spec.js.map +1 -1
  83. package/esm/shade.d.ts +1 -1
  84. package/esm/shade.d.ts.map +1 -1
  85. package/esm/shade.js +10 -7
  86. package/esm/shade.js.map +1 -1
  87. package/esm/vnode.d.ts +1 -1
  88. package/esm/vnode.js +1 -1
  89. package/package.json +5 -4
  90. package/src/component-factory.spec.tsx +3 -2
  91. package/src/components/define-nested-routes.spec.ts +91 -0
  92. package/src/components/define-nested-routes.ts +27 -0
  93. package/src/components/index.ts +3 -3
  94. package/src/components/nested-hooks.spec.ts +254 -0
  95. package/src/components/nested-hooks.ts +109 -0
  96. package/src/components/nested-navigate.spec.ts +136 -32
  97. package/src/components/nested-navigate.ts +75 -26
  98. package/src/components/nested-replace.spec.ts +89 -0
  99. package/src/components/nested-replace.ts +46 -0
  100. package/src/components/nested-route-link.spec.tsx +90 -22
  101. package/src/components/nested-route-link.tsx +54 -29
  102. package/src/components/nested-route-types.ts +82 -2
  103. package/src/components/nested-router.spec.tsx +334 -51
  104. package/src/components/nested-router.tsx +184 -24
  105. package/src/services/location-service.spec.ts +68 -25
  106. package/src/services/location-service.tsx +156 -122
  107. package/src/services/route-match-service.spec.ts +41 -28
  108. package/src/services/route-match-service.ts +17 -8
  109. package/src/services/route-meta-utils.spec.ts +78 -10
  110. package/src/services/route-meta-utils.ts +44 -15
  111. package/src/services/screen-service.spec.ts +19 -18
  112. package/src/services/screen-service.ts +65 -110
  113. package/src/services/spatial-navigation-service.spec.ts +165 -160
  114. package/src/services/spatial-navigation-service.ts +299 -287
  115. package/src/shade.ts +11 -8
  116. package/src/vnode.ts +1 -1
  117. package/esm/components/link-to-route.d.ts +0 -10
  118. package/esm/components/link-to-route.d.ts.map +0 -1
  119. package/esm/components/link-to-route.js +0 -16
  120. package/esm/components/link-to-route.js.map +0 -1
  121. package/esm/components/link-to-route.spec.d.ts +0 -2
  122. package/esm/components/link-to-route.spec.d.ts.map +0 -1
  123. package/esm/components/link-to-route.spec.js +0 -30
  124. package/esm/components/link-to-route.spec.js.map +0 -1
  125. package/esm/components/route-link.d.ts +0 -10
  126. package/esm/components/route-link.d.ts.map +0 -1
  127. package/esm/components/route-link.js +0 -25
  128. package/esm/components/route-link.js.map +0 -1
  129. package/esm/components/route-link.spec.d.ts +0 -2
  130. package/esm/components/route-link.spec.d.ts.map +0 -1
  131. package/esm/components/route-link.spec.js +0 -34
  132. package/esm/components/route-link.spec.js.map +0 -1
  133. package/esm/components/router.d.ts +0 -44
  134. package/esm/components/router.d.ts.map +0 -1
  135. package/esm/components/router.js +0 -66
  136. package/esm/components/router.js.map +0 -1
  137. package/esm/components/router.spec.d.ts +0 -2
  138. package/esm/components/router.spec.d.ts.map +0 -1
  139. package/esm/components/router.spec.js +0 -161
  140. package/esm/components/router.spec.js.map +0 -1
  141. package/src/components/link-to-route.spec.tsx +0 -45
  142. package/src/components/link-to-route.tsx +0 -26
  143. package/src/components/route-link.spec.tsx +0 -44
  144. package/src/components/route-link.tsx +0 -29
  145. package/src/components/router.spec.tsx +0 -217
  146. package/src/components/router.tsx +0 -103
@@ -1,4 +1,4 @@
1
- import { Injector } from '@furystack/inject';
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(new Injector(), async (i) => {
39
- const s = i.getInstance(SpatialNavigationService);
40
- expect(s).toBeInstanceOf(SpatialNavigationService);
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(new Injector(), async (i) => {
45
- const s = i.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
51
- const s = i.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
159
- const s = i.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
782
- i.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
792
- i.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
1040
- const s = i.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
1047
- const s = i.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
1057
- const s = i.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
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.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
1107
- const s = i.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
1112
- const s = i.getInstance(SpatialNavigationService);
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(new Injector(), async (i) => {
1120
+ await usingAsync(createInjector(), async (i) => {
1120
1121
  configureSpatialNavigation(i, { initiallyEnabled: false });
1121
- const s = i.getInstance(SpatialNavigationService);
1122
+ const s = i.get(SpatialNavigationService);
1122
1123
  expect(s.enabled.getValue()).toBe(false);
1123
1124
  });
1124
1125
  });
1125
- it('Should throw if called after service is instantiated', async () => {
1126
- await usingAsync(new Injector(), async (i) => {
1127
- i.getInstance(SpatialNavigationService);
1128
- expect(() => configureSpatialNavigation(i, {})).toThrow('configureSpatialNavigation must be called before the SpatialNavigationService is instantiated');
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
  });