@furystack/shades 14.0.0 → 15.0.1

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 (125) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/esm/compile-route.d.ts +5 -0
  3. package/esm/compile-route.d.ts.map +1 -1
  4. package/esm/compile-route.js +5 -0
  5. package/esm/compile-route.js.map +1 -1
  6. package/esm/component-factory.spec.d.ts.map +1 -1
  7. package/esm/component-factory.spec.js +2 -2
  8. package/esm/component-factory.spec.js.map +1 -1
  9. package/esm/components/nested-hooks.js +2 -2
  10. package/esm/components/nested-hooks.js.map +1 -1
  11. package/esm/components/nested-hooks.spec.js +14 -14
  12. package/esm/components/nested-hooks.spec.js.map +1 -1
  13. package/esm/components/nested-navigate.js +1 -1
  14. package/esm/components/nested-navigate.js.map +1 -1
  15. package/esm/components/nested-navigate.spec.js +16 -16
  16. package/esm/components/nested-navigate.spec.js.map +1 -1
  17. package/esm/components/nested-replace.js +1 -1
  18. package/esm/components/nested-replace.js.map +1 -1
  19. package/esm/components/nested-replace.spec.js +6 -6
  20. package/esm/components/nested-replace.spec.js.map +1 -1
  21. package/esm/components/nested-route-link.js +1 -1
  22. package/esm/components/nested-route-link.js.map +1 -1
  23. package/esm/components/nested-route-link.spec.js +8 -8
  24. package/esm/components/nested-route-link.spec.js.map +1 -1
  25. package/esm/components/nested-router.js +3 -3
  26. package/esm/components/nested-router.js.map +1 -1
  27. package/esm/components/nested-router.spec.js +24 -24
  28. package/esm/components/nested-router.spec.js.map +1 -1
  29. package/esm/css-generator.d.ts +9 -26
  30. package/esm/css-generator.d.ts.map +1 -1
  31. package/esm/css-generator.js +9 -26
  32. package/esm/css-generator.js.map +1 -1
  33. package/esm/css-generator.spec.js.map +1 -1
  34. package/esm/models/css-object.d.ts +3 -5
  35. package/esm/models/css-object.d.ts.map +1 -1
  36. package/esm/models/render-options.d.ts +16 -20
  37. package/esm/models/render-options.d.ts.map +1 -1
  38. package/esm/models/shade-component.d.ts +6 -4
  39. package/esm/models/shade-component.d.ts.map +1 -1
  40. package/esm/models/shade-component.js +3 -3
  41. package/esm/models/shade-component.js.map +1 -1
  42. package/esm/services/location-service.d.ts +39 -42
  43. package/esm/services/location-service.d.ts.map +1 -1
  44. package/esm/services/location-service.js +115 -134
  45. package/esm/services/location-service.js.map +1 -1
  46. package/esm/services/location-service.spec.js +32 -30
  47. package/esm/services/location-service.spec.js.map +1 -1
  48. package/esm/services/route-match-service.d.ts +3 -2
  49. package/esm/services/route-match-service.d.ts.map +1 -1
  50. package/esm/services/route-match-service.js +13 -22
  51. package/esm/services/route-match-service.js.map +1 -1
  52. package/esm/services/route-match-service.spec.js +40 -51
  53. package/esm/services/route-match-service.spec.js.map +1 -1
  54. package/esm/services/route-meta-utils.js.map +1 -1
  55. package/esm/services/route-meta-utils.spec.js +3 -3
  56. package/esm/services/route-meta-utils.spec.js.map +1 -1
  57. package/esm/services/screen-service.d.ts +13 -72
  58. package/esm/services/screen-service.d.ts.map +1 -1
  59. package/esm/services/screen-service.js +45 -137
  60. package/esm/services/screen-service.js.map +1 -1
  61. package/esm/services/screen-service.spec.js +19 -18
  62. package/esm/services/screen-service.spec.js.map +1 -1
  63. package/esm/services/spatial-navigation-service.d.ts +17 -39
  64. package/esm/services/spatial-navigation-service.d.ts.map +1 -1
  65. package/esm/services/spatial-navigation-service.js +280 -313
  66. package/esm/services/spatial-navigation-service.js.map +1 -1
  67. package/esm/services/spatial-navigation-service.spec.js +163 -158
  68. package/esm/services/spatial-navigation-service.spec.js.map +1 -1
  69. package/esm/shade-component.d.ts +14 -17
  70. package/esm/shade-component.d.ts.map +1 -1
  71. package/esm/shade-component.js +14 -17
  72. package/esm/shade-component.js.map +1 -1
  73. package/esm/shade.d.ts +18 -15
  74. package/esm/shade.d.ts.map +1 -1
  75. package/esm/shade.js +22 -32
  76. package/esm/shade.js.map +1 -1
  77. package/esm/style-manager.d.ts +8 -22
  78. package/esm/style-manager.d.ts.map +1 -1
  79. package/esm/style-manager.js +8 -22
  80. package/esm/style-manager.js.map +1 -1
  81. package/esm/styled-shade.d.ts +4 -7
  82. package/esm/styled-shade.d.ts.map +1 -1
  83. package/esm/styled-shade.js +4 -4
  84. package/esm/styled-shade.js.map +1 -1
  85. package/esm/view-transition.d.ts +10 -22
  86. package/esm/view-transition.d.ts.map +1 -1
  87. package/esm/view-transition.js +10 -22
  88. package/esm/view-transition.js.map +1 -1
  89. package/esm/vnode.js.map +1 -1
  90. package/esm/vnode.spec.js.map +1 -1
  91. package/package.json +9 -8
  92. package/src/compile-route.ts +5 -0
  93. package/src/component-factory.spec.tsx +3 -2
  94. package/src/components/nested-hooks.spec.ts +14 -14
  95. package/src/components/nested-hooks.ts +2 -2
  96. package/src/components/nested-navigate.spec.ts +17 -16
  97. package/src/components/nested-navigate.ts +1 -1
  98. package/src/components/nested-replace.spec.ts +7 -6
  99. package/src/components/nested-replace.ts +1 -1
  100. package/src/components/nested-route-link.spec.tsx +8 -8
  101. package/src/components/nested-route-link.tsx +1 -1
  102. package/src/components/nested-router.spec.tsx +24 -24
  103. package/src/components/nested-router.tsx +5 -5
  104. package/src/css-generator.spec.ts +1 -1
  105. package/src/css-generator.ts +9 -26
  106. package/src/models/css-object.ts +3 -5
  107. package/src/models/render-options.ts +17 -20
  108. package/src/models/shade-component.ts +6 -4
  109. package/src/services/location-service.spec.ts +35 -31
  110. package/src/services/location-service.tsx +156 -122
  111. package/src/services/route-match-service.spec.ts +41 -32
  112. package/src/services/route-match-service.ts +17 -8
  113. package/src/services/route-meta-utils.spec.ts +3 -3
  114. package/src/services/route-meta-utils.ts +1 -1
  115. package/src/services/screen-service.spec.ts +21 -20
  116. package/src/services/screen-service.ts +65 -110
  117. package/src/services/spatial-navigation-service.spec.ts +165 -160
  118. package/src/services/spatial-navigation-service.ts +299 -287
  119. package/src/shade-component.ts +14 -17
  120. package/src/shade.ts +31 -41
  121. package/src/style-manager.ts +9 -22
  122. package/src/styled-shade.ts +5 -8
  123. package/src/view-transition.ts +10 -22
  124. package/src/vnode.spec.ts +3 -3
  125. package/src/vnode.ts +2 -2
@@ -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'
@@ -44,29 +44,30 @@ describe('SpatialNavigationService', () => {
44
44
  })
45
45
 
46
46
  it('Should be constructed via injector', async () => {
47
- await usingAsync(new Injector(), async (i) => {
48
- const s = i.getInstance(SpatialNavigationService)
49
- expect(s).toBeInstanceOf(SpatialNavigationService)
47
+ await usingAsync(createInjector(), async (i) => {
48
+ const s = i.get(SpatialNavigationService)
49
+ expect(s).toBeDefined()
50
+ expect(typeof s.moveFocus).toBe('function')
50
51
  })
51
52
  })
52
53
 
53
54
  it('Should be enabled by default', async () => {
54
- await usingAsync(new Injector(), async (i) => {
55
- const s = i.getInstance(SpatialNavigationService)
55
+ await usingAsync(createInjector(), async (i) => {
56
+ const s = i.get(SpatialNavigationService)
56
57
  expect(s.enabled.getValue()).toBe(true)
57
58
  })
58
59
  })
59
60
 
60
61
  it('Should have null activeSection initially', async () => {
61
- await usingAsync(new Injector(), async (i) => {
62
- const s = i.getInstance(SpatialNavigationService)
62
+ await usingAsync(createInjector(), async (i) => {
63
+ const s = i.get(SpatialNavigationService)
63
64
  expect(s.activeSection.getValue()).toBeNull()
64
65
  })
65
66
  })
66
67
 
67
68
  describe('focus movement', () => {
68
69
  it('Should move focus to the right', 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)
@@ -74,56 +75,56 @@ describe('SpatialNavigationService', () => {
74
75
  left.focus()
75
76
  expect(document.activeElement).toBe(left)
76
77
 
77
- const s = i.getInstance(SpatialNavigationService)
78
+ const s = i.get(SpatialNavigationService)
78
79
  s.moveFocus('right')
79
80
  expect(document.activeElement).toBe(right)
80
81
  })
81
82
  })
82
83
 
83
84
  it('Should move focus to the left', async () => {
84
- await usingAsync(new Injector(), async (i) => {
85
+ await usingAsync(createInjector(), async (i) => {
85
86
  const left = createButton('left', { left: 0, top: 0, width: 50, height: 50 })
86
87
  const right = createButton('right', { left: 100, top: 0, width: 50, height: 50 })
87
88
  document.body.append(left, right)
88
89
 
89
90
  right.focus()
90
91
 
91
- const s = i.getInstance(SpatialNavigationService)
92
+ const s = i.get(SpatialNavigationService)
92
93
  s.moveFocus('left')
93
94
  expect(document.activeElement).toBe(left)
94
95
  })
95
96
  })
96
97
 
97
98
  it('Should move focus down', async () => {
98
- await usingAsync(new Injector(), async (i) => {
99
+ await usingAsync(createInjector(), async (i) => {
99
100
  const top = createButton('top', { left: 0, top: 0, width: 50, height: 50 })
100
101
  const bottom = createButton('bottom', { left: 0, top: 100, width: 50, height: 50 })
101
102
  document.body.append(top, bottom)
102
103
 
103
104
  top.focus()
104
105
 
105
- const s = i.getInstance(SpatialNavigationService)
106
+ const s = i.get(SpatialNavigationService)
106
107
  s.moveFocus('down')
107
108
  expect(document.activeElement).toBe(bottom)
108
109
  })
109
110
  })
110
111
 
111
112
  it('Should move focus up', async () => {
112
- await usingAsync(new Injector(), async (i) => {
113
+ await usingAsync(createInjector(), async (i) => {
113
114
  const top = createButton('top', { left: 0, top: 0, width: 50, height: 50 })
114
115
  const bottom = createButton('bottom', { left: 0, top: 100, width: 50, height: 50 })
115
116
  document.body.append(top, bottom)
116
117
 
117
118
  bottom.focus()
118
119
 
119
- const s = i.getInstance(SpatialNavigationService)
120
+ const s = i.get(SpatialNavigationService)
120
121
  s.moveFocus('up')
121
122
  expect(document.activeElement).toBe(top)
122
123
  })
123
124
  })
124
125
 
125
126
  it('Should select nearest element by Euclidean distance', async () => {
126
- await usingAsync(new Injector(), async (i) => {
127
+ await usingAsync(createInjector(), async (i) => {
127
128
  const origin = createButton('origin', { left: 0, top: 0, width: 50, height: 50 })
128
129
  const near = createButton('near', { left: 100, top: 10, width: 50, height: 50 })
129
130
  const far = createButton('far', { left: 300, top: 10, width: 50, height: 50 })
@@ -131,34 +132,34 @@ describe('SpatialNavigationService', () => {
131
132
 
132
133
  origin.focus()
133
134
 
134
- const s = i.getInstance(SpatialNavigationService)
135
+ const s = i.get(SpatialNavigationService)
135
136
  s.moveFocus('right')
136
137
  expect(document.activeElement).toBe(near)
137
138
  })
138
139
  })
139
140
 
140
141
  it('Should be a no-op when no candidate exists in the direction', async () => {
141
- await usingAsync(new Injector(), async (i) => {
142
+ await usingAsync(createInjector(), async (i) => {
142
143
  const only = createButton('only', { left: 0, top: 0, width: 50, height: 50 })
143
144
  document.body.append(only)
144
145
 
145
146
  only.focus()
146
147
 
147
- const s = i.getInstance(SpatialNavigationService)
148
+ const s = i.get(SpatialNavigationService)
148
149
  s.moveFocus('right')
149
150
  expect(document.activeElement).toBe(only)
150
151
  })
151
152
  })
152
153
 
153
154
  it('Should call scrollIntoView on the target element', async () => {
154
- await usingAsync(new Injector(), async (i) => {
155
+ await usingAsync(createInjector(), async (i) => {
155
156
  const left = createButton('left', { left: 0, top: 0, width: 50, height: 50 })
156
157
  const right = createButton('right', { left: 100, top: 0, width: 50, height: 50 })
157
158
  document.body.append(left, right)
158
159
 
159
160
  left.focus()
160
161
 
161
- const s = i.getInstance(SpatialNavigationService)
162
+ const s = i.get(SpatialNavigationService)
162
163
  s.moveFocus('right')
163
164
  expect(right.scrollIntoView).toHaveBeenCalledWith({ block: 'nearest', inline: 'nearest' })
164
165
  })
@@ -167,33 +168,33 @@ describe('SpatialNavigationService', () => {
167
168
 
168
169
  describe('initial focus', () => {
169
170
  it('Should focus first element when no element is focused', async () => {
170
- await usingAsync(new Injector(), async (i) => {
171
+ await usingAsync(createInjector(), async (i) => {
171
172
  const btn = createButton('first', { left: 0, top: 0, width: 50, height: 50 })
172
173
  document.body.append(btn)
173
174
 
174
- const s = i.getInstance(SpatialNavigationService)
175
+ const s = i.get(SpatialNavigationService)
175
176
  s.moveFocus('down')
176
177
  expect(document.activeElement).toBe(btn)
177
178
  })
178
179
  })
179
180
 
180
181
  it('Should focus first element in first section when no element is focused', async () => {
181
- await usingAsync(new Injector(), async (i) => {
182
+ await usingAsync(createInjector(), async (i) => {
182
183
  const section = document.createElement('div')
183
184
  section.setAttribute('data-nav-section', 'main')
184
185
  const btn = createButton('first', { left: 0, top: 0, width: 50, height: 50 })
185
186
  section.append(btn)
186
187
  document.body.append(section)
187
188
 
188
- const s = i.getInstance(SpatialNavigationService)
189
+ const s = i.get(SpatialNavigationService)
189
190
  s.moveFocus('down')
190
191
  expect(document.activeElement).toBe(btn)
191
192
  })
192
193
  })
193
194
 
194
195
  it('Should be a no-op when there are no focusable elements', async () => {
195
- await usingAsync(new Injector(), async (i) => {
196
- const s = i.getInstance(SpatialNavigationService)
196
+ await usingAsync(createInjector(), async (i) => {
197
+ const s = i.get(SpatialNavigationService)
197
198
  s.moveFocus('down')
198
199
  expect(document.activeElement).toBe(document.body)
199
200
  })
@@ -202,13 +203,13 @@ describe('SpatialNavigationService', () => {
202
203
 
203
204
  describe('keydown event handling', () => {
204
205
  it('Should move focus on arrow key press', async () => {
205
- await usingAsync(new Injector(), async (i) => {
206
+ await usingAsync(createInjector(), async (i) => {
206
207
  const left = createButton('left', { left: 0, top: 0, width: 50, height: 50 })
207
208
  const right = createButton('right', { left: 100, top: 0, width: 50, height: 50 })
208
209
  document.body.append(left, right)
209
210
 
210
211
  left.focus()
211
- i.getInstance(SpatialNavigationService)
212
+ i.get(SpatialNavigationService)
212
213
 
213
214
  pressKey('ArrowRight')
214
215
  expect(document.activeElement).toBe(right)
@@ -216,14 +217,14 @@ describe('SpatialNavigationService', () => {
216
217
  })
217
218
 
218
219
  it('Should activate focused element on Enter', async () => {
219
- await usingAsync(new Injector(), async (i) => {
220
+ await usingAsync(createInjector(), async (i) => {
220
221
  const btn = createButton('btn', { left: 0, top: 0, width: 50, height: 50 })
221
222
  const clickHandler = vi.fn()
222
223
  btn.addEventListener('click', clickHandler)
223
224
  document.body.append(btn)
224
225
 
225
226
  btn.focus()
226
- i.getInstance(SpatialNavigationService)
227
+ i.get(SpatialNavigationService)
227
228
 
228
229
  pressKey('Enter')
229
230
  expect(clickHandler).toHaveBeenCalledTimes(1)
@@ -231,13 +232,13 @@ describe('SpatialNavigationService', () => {
231
232
  })
232
233
 
233
234
  it('Should not handle events when disabled', async () => {
234
- await usingAsync(new Injector(), async (i) => {
235
+ await usingAsync(createInjector(), async (i) => {
235
236
  const left = createButton('left', { left: 0, top: 0, width: 50, height: 50 })
236
237
  const right = createButton('right', { left: 100, top: 0, width: 50, height: 50 })
237
238
  document.body.append(left, right)
238
239
 
239
240
  left.focus()
240
- const s = i.getInstance(SpatialNavigationService)
241
+ const s = i.get(SpatialNavigationService)
241
242
  s.enabled.setValue(false)
242
243
 
243
244
  pressKey('ArrowRight')
@@ -246,13 +247,13 @@ describe('SpatialNavigationService', () => {
246
247
  })
247
248
 
248
249
  it('Should skip events that are already defaultPrevented', async () => {
249
- await usingAsync(new Injector(), async (i) => {
250
+ await usingAsync(createInjector(), async (i) => {
250
251
  const left = createButton('left', { left: 0, top: 0, width: 50, height: 50 })
251
252
  const right = createButton('right', { left: 100, top: 0, width: 50, height: 50 })
252
253
  document.body.append(left, right)
253
254
 
254
255
  left.focus()
255
- i.getInstance(SpatialNavigationService)
256
+ i.get(SpatialNavigationService)
256
257
 
257
258
  const preventer = (ev: Event) => ev.preventDefault()
258
259
  window.addEventListener('keydown', preventer, { capture: true })
@@ -269,7 +270,7 @@ describe('SpatialNavigationService', () => {
269
270
 
270
271
  describe('input passthrough', () => {
271
272
  it('Should not intercept arrow keys on text input when cursor is mid-text', async () => {
272
- await usingAsync(new Injector(), async (i) => {
273
+ await usingAsync(createInjector(), async (i) => {
273
274
  const input = document.createElement('input')
274
275
  input.type = 'text'
275
276
  input.value = 'hello'
@@ -279,7 +280,7 @@ describe('SpatialNavigationService', () => {
279
280
 
280
281
  input.focus()
281
282
  input.setSelectionRange(2, 2)
282
- i.getInstance(SpatialNavigationService)
283
+ i.get(SpatialNavigationService)
283
284
 
284
285
  pressKey('ArrowRight')
285
286
  expect(document.activeElement).toBe(input)
@@ -287,7 +288,7 @@ describe('SpatialNavigationService', () => {
287
288
  })
288
289
 
289
290
  it('Should escape text input with ArrowRight when cursor is at end', async () => {
290
- await usingAsync(new Injector(), async (i) => {
291
+ await usingAsync(createInjector(), async (i) => {
291
292
  const input = document.createElement('input')
292
293
  input.type = 'text'
293
294
  input.value = 'hello'
@@ -298,7 +299,7 @@ describe('SpatialNavigationService', () => {
298
299
 
299
300
  input.focus()
300
301
  input.setSelectionRange(5, 5)
301
- i.getInstance(SpatialNavigationService)
302
+ i.get(SpatialNavigationService)
302
303
 
303
304
  pressKey('ArrowRight')
304
305
  expect(document.activeElement).toBe(btn)
@@ -306,7 +307,7 @@ describe('SpatialNavigationService', () => {
306
307
  })
307
308
 
308
309
  it('Should escape text input with ArrowLeft when cursor is at start', async () => {
309
- await usingAsync(new Injector(), async (i) => {
310
+ await usingAsync(createInjector(), async (i) => {
310
311
  const input = document.createElement('input')
311
312
  input.type = 'text'
312
313
  input.value = 'hello'
@@ -317,7 +318,7 @@ describe('SpatialNavigationService', () => {
317
318
 
318
319
  input.focus()
319
320
  input.setSelectionRange(0, 0)
320
- i.getInstance(SpatialNavigationService)
321
+ i.get(SpatialNavigationService)
321
322
 
322
323
  pressKey('ArrowLeft')
323
324
  expect(document.activeElement).toBe(btn)
@@ -325,7 +326,7 @@ describe('SpatialNavigationService', () => {
325
326
  })
326
327
 
327
328
  it('Should not intercept arrow keys on textarea when cursor is mid-text', async () => {
328
- await usingAsync(new Injector(), async (i) => {
329
+ await usingAsync(createInjector(), async (i) => {
329
330
  const textarea = document.createElement('textarea')
330
331
  textarea.value = 'line1\nline2'
331
332
  mockRect(textarea, { left: 0, top: 0, width: 200, height: 100 })
@@ -334,7 +335,7 @@ describe('SpatialNavigationService', () => {
334
335
 
335
336
  textarea.focus()
336
337
  textarea.setSelectionRange(3, 3)
337
- i.getInstance(SpatialNavigationService)
338
+ i.get(SpatialNavigationService)
338
339
 
339
340
  pressKey('ArrowDown')
340
341
  expect(document.activeElement).toBe(textarea)
@@ -342,14 +343,14 @@ describe('SpatialNavigationService', () => {
342
343
  })
343
344
 
344
345
  it('Should not intercept arrow keys on select', async () => {
345
- await usingAsync(new Injector(), async (i) => {
346
+ await usingAsync(createInjector(), async (i) => {
346
347
  const select = document.createElement('select')
347
348
  mockRect(select, { left: 0, top: 0, width: 200, height: 30 })
348
349
  const btn = createButton('btn', { left: 0, top: 100, width: 50, height: 50 })
349
350
  document.body.append(select, btn)
350
351
 
351
352
  select.focus()
352
- i.getInstance(SpatialNavigationService)
353
+ i.get(SpatialNavigationService)
353
354
 
354
355
  pressKey('ArrowDown')
355
356
  expect(document.activeElement).toBe(select)
@@ -357,7 +358,7 @@ describe('SpatialNavigationService', () => {
357
358
  })
358
359
 
359
360
  it('Should not intercept arrow keys on contenteditable', async () => {
360
- await usingAsync(new Injector(), async (i) => {
361
+ await usingAsync(createInjector(), async (i) => {
361
362
  const div = document.createElement('div')
362
363
  div.contentEditable = 'true'
363
364
  div.tabIndex = 0
@@ -366,7 +367,7 @@ describe('SpatialNavigationService', () => {
366
367
  document.body.append(div, btn)
367
368
 
368
369
  div.focus()
369
- i.getInstance(SpatialNavigationService)
370
+ i.get(SpatialNavigationService)
370
371
 
371
372
  pressKey('ArrowRight')
372
373
  expect(document.activeElement).toBe(div)
@@ -374,7 +375,7 @@ describe('SpatialNavigationService', () => {
374
375
  })
375
376
 
376
377
  it('Should not intercept arrow keys on children of contenteditable', async () => {
377
- await usingAsync(new Injector(), async (i) => {
378
+ await usingAsync(createInjector(), async (i) => {
378
379
  const div = document.createElement('div')
379
380
  div.contentEditable = 'true'
380
381
  div.tabIndex = 0
@@ -390,7 +391,7 @@ describe('SpatialNavigationService', () => {
390
391
  document.body.append(div, btn)
391
392
 
392
393
  span.focus()
393
- i.getInstance(SpatialNavigationService)
394
+ i.get(SpatialNavigationService)
394
395
 
395
396
  pressKey('ArrowRight')
396
397
  expect(document.activeElement).toBe(span)
@@ -398,7 +399,7 @@ describe('SpatialNavigationService', () => {
398
399
  })
399
400
 
400
401
  it('Should not intercept Enter on children of contenteditable', async () => {
401
- await usingAsync(new Injector(), async (i) => {
402
+ await usingAsync(createInjector(), async (i) => {
402
403
  const div = document.createElement('div')
403
404
  div.contentEditable = 'true'
404
405
  div.tabIndex = 0
@@ -414,7 +415,7 @@ describe('SpatialNavigationService', () => {
414
415
  document.body.append(div)
415
416
 
416
417
  span.focus()
417
- i.getInstance(SpatialNavigationService)
418
+ i.get(SpatialNavigationService)
418
419
 
419
420
  pressKey('Enter')
420
421
  expect(clickHandler).not.toHaveBeenCalled()
@@ -422,7 +423,7 @@ describe('SpatialNavigationService', () => {
422
423
  })
423
424
 
424
425
  it('Should intercept arrow keys on button-type input', async () => {
425
- await usingAsync(new Injector(), async (i) => {
426
+ await usingAsync(createInjector(), async (i) => {
426
427
  const input = document.createElement('input')
427
428
  input.type = 'button'
428
429
  mockRect(input, { left: 0, top: 0, width: 50, height: 30 })
@@ -431,7 +432,7 @@ describe('SpatialNavigationService', () => {
431
432
  document.body.append(input, btn)
432
433
 
433
434
  input.focus()
434
- i.getInstance(SpatialNavigationService)
435
+ i.get(SpatialNavigationService)
435
436
 
436
437
  pressKey('ArrowRight')
437
438
  expect(document.activeElement).toBe(btn)
@@ -439,7 +440,7 @@ describe('SpatialNavigationService', () => {
439
440
  })
440
441
 
441
442
  it('Should not intercept Left/Right on range input (slider adjustment)', async () => {
442
- await usingAsync(new Injector(), async (i) => {
443
+ await usingAsync(createInjector(), async (i) => {
443
444
  const input = document.createElement('input')
444
445
  input.type = 'range'
445
446
  mockRect(input, { left: 0, top: 0, width: 200, height: 30 })
@@ -447,7 +448,7 @@ describe('SpatialNavigationService', () => {
447
448
  document.body.append(input, btn)
448
449
 
449
450
  input.focus()
450
- i.getInstance(SpatialNavigationService)
451
+ i.get(SpatialNavigationService)
451
452
 
452
453
  pressKey('ArrowLeft')
453
454
  expect(document.activeElement).toBe(input)
@@ -458,7 +459,7 @@ describe('SpatialNavigationService', () => {
458
459
  })
459
460
 
460
461
  it('Should intercept Up/Down on range input for spatial navigation', async () => {
461
- await usingAsync(new Injector(), async (i) => {
462
+ await usingAsync(createInjector(), async (i) => {
462
463
  const input = document.createElement('input')
463
464
  input.type = 'range'
464
465
  mockRect(input, { left: 0, top: 0, width: 200, height: 30 })
@@ -467,7 +468,7 @@ describe('SpatialNavigationService', () => {
467
468
  document.body.append(input, btn)
468
469
 
469
470
  input.focus()
470
- i.getInstance(SpatialNavigationService)
471
+ i.get(SpatialNavigationService)
471
472
 
472
473
  pressKey('ArrowDown')
473
474
  expect(document.activeElement).toBe(btn)
@@ -475,7 +476,7 @@ describe('SpatialNavigationService', () => {
475
476
  })
476
477
 
477
478
  it('Should intercept arrow keys on color input', async () => {
478
- await usingAsync(new Injector(), async (i) => {
479
+ await usingAsync(createInjector(), async (i) => {
479
480
  const input = document.createElement('input')
480
481
  input.type = 'color'
481
482
  mockRect(input, { left: 0, top: 0, width: 50, height: 50 })
@@ -484,7 +485,7 @@ describe('SpatialNavigationService', () => {
484
485
  document.body.append(input, btn)
485
486
 
486
487
  input.focus()
487
- i.getInstance(SpatialNavigationService)
488
+ i.get(SpatialNavigationService)
488
489
 
489
490
  pressKey('ArrowRight')
490
491
  expect(document.activeElement).toBe(btn)
@@ -492,7 +493,7 @@ describe('SpatialNavigationService', () => {
492
493
  })
493
494
 
494
495
  it('Should intercept arrow keys on file input', async () => {
495
- await usingAsync(new Injector(), async (i) => {
496
+ await usingAsync(createInjector(), async (i) => {
496
497
  const input = document.createElement('input')
497
498
  input.type = 'file'
498
499
  mockRect(input, { left: 0, top: 0, width: 200, height: 30 })
@@ -501,7 +502,7 @@ describe('SpatialNavigationService', () => {
501
502
  document.body.append(input, btn)
502
503
 
503
504
  input.focus()
504
- i.getInstance(SpatialNavigationService)
505
+ i.get(SpatialNavigationService)
505
506
 
506
507
  pressKey('ArrowRight')
507
508
  expect(document.activeElement).toBe(btn)
@@ -509,7 +510,7 @@ describe('SpatialNavigationService', () => {
509
510
  })
510
511
 
511
512
  it('Should not intercept arrow keys on radio input', async () => {
512
- await usingAsync(new Injector(), async (i) => {
513
+ await usingAsync(createInjector(), async (i) => {
513
514
  const radio = document.createElement('input')
514
515
  radio.type = 'radio'
515
516
  radio.name = 'group'
@@ -518,7 +519,7 @@ describe('SpatialNavigationService', () => {
518
519
  document.body.append(radio, btn)
519
520
 
520
521
  radio.focus()
521
- i.getInstance(SpatialNavigationService)
522
+ i.get(SpatialNavigationService)
522
523
 
523
524
  pressKey('ArrowDown')
524
525
  expect(document.activeElement).toBe(radio)
@@ -529,7 +530,7 @@ describe('SpatialNavigationService', () => {
529
530
  })
530
531
 
531
532
  it('Should not intercept Up/Down on number input (increment/decrement)', async () => {
532
- await usingAsync(new Injector(), async (i) => {
533
+ await usingAsync(createInjector(), async (i) => {
533
534
  const input = document.createElement('input')
534
535
  input.type = 'number'
535
536
  input.value = '5'
@@ -538,7 +539,7 @@ describe('SpatialNavigationService', () => {
538
539
  document.body.append(input, btn)
539
540
 
540
541
  input.focus()
541
- i.getInstance(SpatialNavigationService)
542
+ i.get(SpatialNavigationService)
542
543
 
543
544
  pressKey('ArrowUp')
544
545
  expect(document.activeElement).toBe(input)
@@ -549,7 +550,7 @@ describe('SpatialNavigationService', () => {
549
550
  })
550
551
 
551
552
  it('Should intercept Left/Right on number input for spatial navigation', async () => {
552
- await usingAsync(new Injector(), async (i) => {
553
+ await usingAsync(createInjector(), async (i) => {
553
554
  const input = document.createElement('input')
554
555
  input.type = 'number'
555
556
  input.value = '5'
@@ -560,7 +561,7 @@ describe('SpatialNavigationService', () => {
560
561
  document.body.append(btnLeft, input, btnRight)
561
562
 
562
563
  input.focus()
563
- i.getInstance(SpatialNavigationService)
564
+ i.get(SpatialNavigationService)
564
565
 
565
566
  pressKey('ArrowRight')
566
567
  expect(document.activeElement).toBe(btnRight)
@@ -568,7 +569,7 @@ describe('SpatialNavigationService', () => {
568
569
  })
569
570
 
570
571
  it('Should not intercept arrow keys on date input', async () => {
571
- await usingAsync(new Injector(), async (i) => {
572
+ await usingAsync(createInjector(), async (i) => {
572
573
  const input = document.createElement('input')
573
574
  input.type = 'date'
574
575
  mockRect(input, { left: 0, top: 0, width: 200, height: 30 })
@@ -576,7 +577,7 @@ describe('SpatialNavigationService', () => {
576
577
  document.body.append(input, btn)
577
578
 
578
579
  input.focus()
579
- i.getInstance(SpatialNavigationService)
580
+ i.get(SpatialNavigationService)
580
581
 
581
582
  pressKey('ArrowRight')
582
583
  expect(document.activeElement).toBe(input)
@@ -587,7 +588,7 @@ describe('SpatialNavigationService', () => {
587
588
  })
588
589
 
589
590
  it('Should not intercept arrow keys on time input', async () => {
590
- await usingAsync(new Injector(), async (i) => {
591
+ await usingAsync(createInjector(), async (i) => {
591
592
  const input = document.createElement('input')
592
593
  input.type = 'time'
593
594
  mockRect(input, { left: 0, top: 0, width: 200, height: 30 })
@@ -595,7 +596,7 @@ describe('SpatialNavigationService', () => {
595
596
  document.body.append(input, btn)
596
597
 
597
598
  input.focus()
598
- i.getInstance(SpatialNavigationService)
599
+ i.get(SpatialNavigationService)
599
600
 
600
601
  pressKey('ArrowDown')
601
602
  expect(document.activeElement).toBe(input)
@@ -606,7 +607,7 @@ describe('SpatialNavigationService', () => {
606
607
  })
607
608
 
608
609
  it('Should escape empty text input on any arrow key', async () => {
609
- await usingAsync(new Injector(), async (i) => {
610
+ await usingAsync(createInjector(), async (i) => {
610
611
  const input = document.createElement('input')
611
612
  input.type = 'text'
612
613
  mockRect(input, { left: 0, top: 0, width: 200, height: 30 })
@@ -615,7 +616,7 @@ describe('SpatialNavigationService', () => {
615
616
  document.body.append(input, btn)
616
617
 
617
618
  input.focus()
618
- i.getInstance(SpatialNavigationService)
619
+ i.get(SpatialNavigationService)
619
620
 
620
621
  pressKey('ArrowRight')
621
622
  expect(document.activeElement).toBe(btn)
@@ -623,7 +624,7 @@ describe('SpatialNavigationService', () => {
623
624
  })
624
625
 
625
626
  it('Should not intercept Enter on text input', async () => {
626
- await usingAsync(new Injector(), async (i) => {
627
+ await usingAsync(createInjector(), async (i) => {
627
628
  const input = document.createElement('input')
628
629
  input.type = 'text'
629
630
  mockRect(input, { left: 0, top: 0, width: 200, height: 30 })
@@ -632,7 +633,7 @@ describe('SpatialNavigationService', () => {
632
633
  document.body.append(input)
633
634
 
634
635
  input.focus()
635
- i.getInstance(SpatialNavigationService)
636
+ i.get(SpatialNavigationService)
636
637
 
637
638
  pressKey('Enter')
638
639
  expect(clickHandler).not.toHaveBeenCalled()
@@ -640,7 +641,7 @@ describe('SpatialNavigationService', () => {
640
641
  })
641
642
 
642
643
  it('Should not intercept Enter on textarea', async () => {
643
- await usingAsync(new Injector(), async (i) => {
644
+ await usingAsync(createInjector(), async (i) => {
644
645
  const textarea = document.createElement('textarea')
645
646
  textarea.value = 'hello'
646
647
  mockRect(textarea, { left: 0, top: 0, width: 200, height: 100 })
@@ -649,7 +650,7 @@ describe('SpatialNavigationService', () => {
649
650
  document.body.append(textarea)
650
651
 
651
652
  textarea.focus()
652
- i.getInstance(SpatialNavigationService)
653
+ i.get(SpatialNavigationService)
653
654
 
654
655
  pressKey('Enter')
655
656
  expect(clickHandler).not.toHaveBeenCalled()
@@ -657,7 +658,7 @@ describe('SpatialNavigationService', () => {
657
658
  })
658
659
 
659
660
  it('Should activate Enter on button-type input', async () => {
660
- await usingAsync(new Injector(), async (i) => {
661
+ await usingAsync(createInjector(), async (i) => {
661
662
  const input = document.createElement('input')
662
663
  input.type = 'button'
663
664
  mockRect(input, { left: 0, top: 0, width: 50, height: 30 })
@@ -666,7 +667,7 @@ describe('SpatialNavigationService', () => {
666
667
  document.body.append(input)
667
668
 
668
669
  input.focus()
669
- i.getInstance(SpatialNavigationService)
670
+ i.get(SpatialNavigationService)
670
671
 
671
672
  pressKey('Enter')
672
673
  expect(clickHandler).toHaveBeenCalledTimes(1)
@@ -676,7 +677,7 @@ describe('SpatialNavigationService', () => {
676
677
 
677
678
  describe('data-spatial-nav-passthrough', () => {
678
679
  it('Should not intercept arrow keys inside a passthrough container', async () => {
679
- await usingAsync(new Injector(), async (i) => {
680
+ await usingAsync(createInjector(), async (i) => {
680
681
  const container = document.createElement('div')
681
682
  container.setAttribute('data-spatial-nav-passthrough', '')
682
683
  const inner = document.createElement('input')
@@ -688,7 +689,7 @@ describe('SpatialNavigationService', () => {
688
689
  document.body.append(container, btn)
689
690
 
690
691
  inner.focus()
691
- i.getInstance(SpatialNavigationService)
692
+ i.get(SpatialNavigationService)
692
693
 
693
694
  pressKey('ArrowRight')
694
695
  expect(document.activeElement).toBe(inner)
@@ -696,7 +697,7 @@ describe('SpatialNavigationService', () => {
696
697
  })
697
698
 
698
699
  it('Should not intercept Enter inside a passthrough container', async () => {
699
- await usingAsync(new Injector(), async (i) => {
700
+ await usingAsync(createInjector(), async (i) => {
700
701
  const container = document.createElement('div')
701
702
  container.setAttribute('data-spatial-nav-passthrough', '')
702
703
  const inner = createButton('inner', { left: 0, top: 0, width: 50, height: 50 })
@@ -706,7 +707,7 @@ describe('SpatialNavigationService', () => {
706
707
  document.body.append(container)
707
708
 
708
709
  inner.focus()
709
- i.getInstance(SpatialNavigationService)
710
+ i.get(SpatialNavigationService)
710
711
 
711
712
  pressKey('Enter')
712
713
  expect(clickHandler).not.toHaveBeenCalled()
@@ -714,7 +715,7 @@ describe('SpatialNavigationService', () => {
714
715
  })
715
716
 
716
717
  it('Should still intercept keys outside a passthrough container', async () => {
717
- await usingAsync(new Injector(), async (i) => {
718
+ await usingAsync(createInjector(), async (i) => {
718
719
  const container = document.createElement('div')
719
720
  container.setAttribute('data-spatial-nav-passthrough', '')
720
721
  const inner = document.createElement('input')
@@ -727,7 +728,7 @@ describe('SpatialNavigationService', () => {
727
728
  document.body.append(left, right, container)
728
729
 
729
730
  left.focus()
730
- i.getInstance(SpatialNavigationService)
731
+ i.get(SpatialNavigationService)
731
732
 
732
733
  pressKey('ArrowRight')
733
734
  expect(document.activeElement).toBe(right)
@@ -737,7 +738,7 @@ describe('SpatialNavigationService', () => {
737
738
 
738
739
  describe('section navigation', () => {
739
740
  it('Should scope navigation within the active section', async () => {
740
- await usingAsync(new Injector(), async (i) => {
741
+ await usingAsync(createInjector(), async (i) => {
741
742
  const section1 = document.createElement('div')
742
743
  section1.setAttribute('data-nav-section', 'sidebar')
743
744
  mockRect(section1, { left: 0, top: 0, width: 200, height: 400 })
@@ -755,14 +756,14 @@ describe('SpatialNavigationService', () => {
755
756
 
756
757
  btn2.focus()
757
758
 
758
- const s = i.getInstance(SpatialNavigationService)
759
+ const s = i.get(SpatialNavigationService)
759
760
  s.moveFocus('down')
760
761
  expect(document.activeElement).toBe(btn3)
761
762
  })
762
763
  })
763
764
 
764
765
  it('Should update activeSection when focus moves', async () => {
765
- await usingAsync(new Injector(), async (i) => {
766
+ await usingAsync(createInjector(), async (i) => {
766
767
  const section = document.createElement('div')
767
768
  section.setAttribute('data-nav-section', 'main')
768
769
  const btn = createButton('btn', { left: 0, top: 0, width: 50, height: 50 })
@@ -771,7 +772,7 @@ describe('SpatialNavigationService', () => {
771
772
 
772
773
  btn.focus()
773
774
 
774
- const s = i.getInstance(SpatialNavigationService)
775
+ const s = i.get(SpatialNavigationService)
775
776
  s.moveFocus('down')
776
777
  expect(s.activeSection.getValue()).toBe('main')
777
778
  })
@@ -780,7 +781,7 @@ describe('SpatialNavigationService', () => {
780
781
 
781
782
  describe('cross-section navigation', () => {
782
783
  it('Should navigate to adjacent section when no candidate in current section', async () => {
783
- await usingAsync(new Injector(), async (i) => {
784
+ await usingAsync(createInjector(), async (i) => {
784
785
  const section1 = document.createElement('div')
785
786
  section1.setAttribute('data-nav-section', 'left')
786
787
  mockRect(section1, { left: 0, top: 0, width: 200, height: 400 })
@@ -798,7 +799,7 @@ describe('SpatialNavigationService', () => {
798
799
 
799
800
  btn1.focus()
800
801
 
801
- const s = i.getInstance(SpatialNavigationService)
802
+ const s = i.get(SpatialNavigationService)
802
803
  s.moveFocus('right')
803
804
  expect(document.activeElement).toBe(btn2)
804
805
  expect(s.activeSection.getValue()).toBe('right')
@@ -806,7 +807,7 @@ describe('SpatialNavigationService', () => {
806
807
  })
807
808
 
808
809
  it('Should not navigate cross-section when disabled', async () => {
809
- await usingAsync(new Injector(), async (i) => {
810
+ await usingAsync(createInjector(), async (i) => {
810
811
  configureSpatialNavigation(i, { crossSectionNavigation: false })
811
812
 
812
813
  const section1 = document.createElement('div')
@@ -825,7 +826,7 @@ describe('SpatialNavigationService', () => {
825
826
 
826
827
  btn1.focus()
827
828
 
828
- const s = i.getInstance(SpatialNavigationService)
829
+ const s = i.get(SpatialNavigationService)
829
830
  s.moveFocus('right')
830
831
  expect(document.activeElement).toBe(btn1)
831
832
  })
@@ -834,7 +835,7 @@ describe('SpatialNavigationService', () => {
834
835
 
835
836
  describe('focus memory', () => {
836
837
  it('Should remember and restore focus when returning to a section', async () => {
837
- await usingAsync(new Injector(), async (i) => {
838
+ await usingAsync(createInjector(), async (i) => {
838
839
  const section1 = document.createElement('div')
839
840
  section1.setAttribute('data-nav-section', 'left')
840
841
  mockRect(section1, { left: 0, top: 0, width: 200, height: 400 })
@@ -853,7 +854,7 @@ describe('SpatialNavigationService', () => {
853
854
 
854
855
  btn1b.focus()
855
856
 
856
- const s = i.getInstance(SpatialNavigationService)
857
+ const s = i.get(SpatialNavigationService)
857
858
 
858
859
  // Navigate away from section1
859
860
  s.moveFocus('right')
@@ -867,7 +868,7 @@ describe('SpatialNavigationService', () => {
867
868
  })
868
869
 
869
870
  it('Should fall back to nearest element when remembered element is removed', async () => {
870
- await usingAsync(new Injector(), async (i) => {
871
+ await usingAsync(createInjector(), async (i) => {
871
872
  const section1 = document.createElement('div')
872
873
  section1.setAttribute('data-nav-section', 'left')
873
874
  mockRect(section1, { left: 0, top: 0, width: 200, height: 400 })
@@ -887,7 +888,7 @@ describe('SpatialNavigationService', () => {
887
888
 
888
889
  // Focus btn2a, navigate to section1
889
890
  btn2a.focus()
890
- const s = i.getInstance(SpatialNavigationService)
891
+ const s = i.get(SpatialNavigationService)
891
892
  s.moveFocus('left')
892
893
 
893
894
  // Remove btn2a from DOM
@@ -900,7 +901,7 @@ describe('SpatialNavigationService', () => {
900
901
  })
901
902
 
902
903
  it('Should fall back to nearest element when remembered element becomes disabled', async () => {
903
- await usingAsync(new Injector(), async (i) => {
904
+ await usingAsync(createInjector(), async (i) => {
904
905
  const section1 = document.createElement('div')
905
906
  section1.setAttribute('data-nav-section', 'left')
906
907
  mockRect(section1, { left: 0, top: 0, width: 200, height: 400 })
@@ -919,7 +920,7 @@ describe('SpatialNavigationService', () => {
919
920
  document.body.append(section1, section2)
920
921
 
921
922
  btn2a.focus()
922
- const s = i.getInstance(SpatialNavigationService)
923
+ const s = i.get(SpatialNavigationService)
923
924
  s.moveFocus('left')
924
925
 
925
926
  // Disable btn2a after navigating away
@@ -934,13 +935,13 @@ describe('SpatialNavigationService', () => {
934
935
 
935
936
  describe('enabled toggle', () => {
936
937
  it('Should stop handling keys when disabled', async () => {
937
- await usingAsync(new Injector(), async (i) => {
938
+ await usingAsync(createInjector(), async (i) => {
938
939
  const left = createButton('left', { left: 0, top: 0, width: 50, height: 50 })
939
940
  const right = createButton('right', { left: 100, top: 0, width: 50, height: 50 })
940
941
  document.body.append(left, right)
941
942
 
942
943
  left.focus()
943
- const s = i.getInstance(SpatialNavigationService)
944
+ const s = i.get(SpatialNavigationService)
944
945
 
945
946
  s.enabled.setValue(false)
946
947
  pressKey('ArrowRight')
@@ -957,8 +958,8 @@ describe('SpatialNavigationService', () => {
957
958
  it('Should remove keydown listener on dispose', async () => {
958
959
  const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener')
959
960
 
960
- await usingAsync(new Injector(), async (i) => {
961
- i.getInstance(SpatialNavigationService)
961
+ await usingAsync(createInjector(), async (i) => {
962
+ i.get(SpatialNavigationService)
962
963
  })
963
964
 
964
965
  expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function))
@@ -971,8 +972,8 @@ describe('SpatialNavigationService', () => {
971
972
 
972
973
  left.focus()
973
974
 
974
- await usingAsync(new Injector(), async (i) => {
975
- i.getInstance(SpatialNavigationService)
975
+ await usingAsync(createInjector(), async (i) => {
976
+ i.get(SpatialNavigationService)
976
977
  })
977
978
 
978
979
  pressKey('ArrowRight')
@@ -982,7 +983,7 @@ describe('SpatialNavigationService', () => {
982
983
 
983
984
  describe('backspace and escape', () => {
984
985
  it('Should call history.back() on Backspace when configured', async () => {
985
- await usingAsync(new Injector(), async (i) => {
986
+ await usingAsync(createInjector(), async (i) => {
986
987
  configureSpatialNavigation(i, { backspaceGoesBack: true })
987
988
 
988
989
  const btn = createButton('btn', { left: 0, top: 0, width: 50, height: 50 })
@@ -990,7 +991,7 @@ describe('SpatialNavigationService', () => {
990
991
  btn.focus()
991
992
 
992
993
  const backSpy = vi.spyOn(history, 'back').mockImplementation(() => {})
993
- i.getInstance(SpatialNavigationService)
994
+ i.get(SpatialNavigationService)
994
995
 
995
996
  pressKey('Backspace')
996
997
  expect(backSpy).toHaveBeenCalledTimes(1)
@@ -998,13 +999,13 @@ describe('SpatialNavigationService', () => {
998
999
  })
999
1000
 
1000
1001
  it('Should not call history.back() on Backspace by default', async () => {
1001
- await usingAsync(new Injector(), async (i) => {
1002
+ await usingAsync(createInjector(), async (i) => {
1002
1003
  const btn = createButton('btn', { left: 0, top: 0, width: 50, height: 50 })
1003
1004
  document.body.append(btn)
1004
1005
  btn.focus()
1005
1006
 
1006
1007
  const backSpy = vi.spyOn(history, 'back').mockImplementation(() => {})
1007
- i.getInstance(SpatialNavigationService)
1008
+ i.get(SpatialNavigationService)
1008
1009
 
1009
1010
  pressKey('Backspace')
1010
1011
  expect(backSpy).not.toHaveBeenCalled()
@@ -1012,7 +1013,7 @@ describe('SpatialNavigationService', () => {
1012
1013
  })
1013
1014
 
1014
1015
  it('Should not call history.back() on Backspace when focused on a text input', async () => {
1015
- await usingAsync(new Injector(), async (i) => {
1016
+ await usingAsync(createInjector(), async (i) => {
1016
1017
  configureSpatialNavigation(i, { backspaceGoesBack: true })
1017
1018
 
1018
1019
  const input = document.createElement('input')
@@ -1023,7 +1024,7 @@ describe('SpatialNavigationService', () => {
1023
1024
  input.focus()
1024
1025
 
1025
1026
  const backSpy = vi.spyOn(history, 'back').mockImplementation(() => {})
1026
- i.getInstance(SpatialNavigationService)
1027
+ i.get(SpatialNavigationService)
1027
1028
 
1028
1029
  pressKey('Backspace')
1029
1030
  expect(backSpy).not.toHaveBeenCalled()
@@ -1031,7 +1032,7 @@ describe('SpatialNavigationService', () => {
1031
1032
  })
1032
1033
 
1033
1034
  it('Should not call history.back() on Backspace when focused on a textarea', async () => {
1034
- await usingAsync(new Injector(), async (i) => {
1035
+ await usingAsync(createInjector(), async (i) => {
1035
1036
  configureSpatialNavigation(i, { backspaceGoesBack: true })
1036
1037
 
1037
1038
  const textarea = document.createElement('textarea')
@@ -1041,7 +1042,7 @@ describe('SpatialNavigationService', () => {
1041
1042
  textarea.focus()
1042
1043
 
1043
1044
  const backSpy = vi.spyOn(history, 'back').mockImplementation(() => {})
1044
- i.getInstance(SpatialNavigationService)
1045
+ i.get(SpatialNavigationService)
1045
1046
 
1046
1047
  pressKey('Backspace')
1047
1048
  expect(backSpy).not.toHaveBeenCalled()
@@ -1049,7 +1050,7 @@ describe('SpatialNavigationService', () => {
1049
1050
  })
1050
1051
 
1051
1052
  it('Should move to parent section on Escape when configured', async () => {
1052
- await usingAsync(new Injector(), async (i) => {
1053
+ await usingAsync(createInjector(), async (i) => {
1053
1054
  configureSpatialNavigation(i, { escapeGoesToParentSection: true })
1054
1055
 
1055
1056
  const outer = document.createElement('div')
@@ -1065,7 +1066,7 @@ describe('SpatialNavigationService', () => {
1065
1066
  document.body.append(outer)
1066
1067
 
1067
1068
  innerBtn.focus()
1068
- i.getInstance(SpatialNavigationService)
1069
+ i.get(SpatialNavigationService)
1069
1070
 
1070
1071
  pressKey('Escape')
1071
1072
  expect(document.activeElement).toBe(outerBtn)
@@ -1073,7 +1074,7 @@ describe('SpatialNavigationService', () => {
1073
1074
  })
1074
1075
 
1075
1076
  it('Should blur a range input on Escape', async () => {
1076
- await usingAsync(new Injector(), async (i) => {
1077
+ await usingAsync(createInjector(), async (i) => {
1077
1078
  const input = document.createElement('input')
1078
1079
  input.type = 'range'
1079
1080
  mockRect(input, { left: 0, top: 0, width: 200, height: 30 })
@@ -1082,14 +1083,14 @@ describe('SpatialNavigationService', () => {
1082
1083
  input.focus()
1083
1084
  expect(document.activeElement).toBe(input)
1084
1085
 
1085
- i.getInstance(SpatialNavigationService)
1086
+ i.get(SpatialNavigationService)
1086
1087
  pressKey('Escape')
1087
1088
  expect(document.activeElement).toBe(document.body)
1088
1089
  })
1089
1090
  })
1090
1091
 
1091
1092
  it('Should blur a radio input on Escape', async () => {
1092
- await usingAsync(new Injector(), async (i) => {
1093
+ await usingAsync(createInjector(), async (i) => {
1093
1094
  const radio = document.createElement('input')
1094
1095
  radio.type = 'radio'
1095
1096
  radio.name = 'group'
@@ -1099,14 +1100,14 @@ describe('SpatialNavigationService', () => {
1099
1100
  radio.focus()
1100
1101
  expect(document.activeElement).toBe(radio)
1101
1102
 
1102
- i.getInstance(SpatialNavigationService)
1103
+ i.get(SpatialNavigationService)
1103
1104
  pressKey('Escape')
1104
1105
  expect(document.activeElement).toBe(document.body)
1105
1106
  })
1106
1107
  })
1107
1108
 
1108
1109
  it('Should blur a date input on Escape', async () => {
1109
- await usingAsync(new Injector(), async (i) => {
1110
+ await usingAsync(createInjector(), async (i) => {
1110
1111
  const input = document.createElement('input')
1111
1112
  input.type = 'date'
1112
1113
  mockRect(input, { left: 0, top: 0, width: 200, height: 30 })
@@ -1115,14 +1116,14 @@ describe('SpatialNavigationService', () => {
1115
1116
  input.focus()
1116
1117
  expect(document.activeElement).toBe(input)
1117
1118
 
1118
- i.getInstance(SpatialNavigationService)
1119
+ i.get(SpatialNavigationService)
1119
1120
  pressKey('Escape')
1120
1121
  expect(document.activeElement).toBe(document.body)
1121
1122
  })
1122
1123
  })
1123
1124
 
1124
1125
  it('Should blur a time input on Escape', async () => {
1125
- await usingAsync(new Injector(), async (i) => {
1126
+ await usingAsync(createInjector(), async (i) => {
1126
1127
  const input = document.createElement('input')
1127
1128
  input.type = 'time'
1128
1129
  mockRect(input, { left: 0, top: 0, width: 200, height: 30 })
@@ -1131,14 +1132,14 @@ describe('SpatialNavigationService', () => {
1131
1132
  input.focus()
1132
1133
  expect(document.activeElement).toBe(input)
1133
1134
 
1134
- i.getInstance(SpatialNavigationService)
1135
+ i.get(SpatialNavigationService)
1135
1136
  pressKey('Escape')
1136
1137
  expect(document.activeElement).toBe(document.body)
1137
1138
  })
1138
1139
  })
1139
1140
 
1140
1141
  it('Should blur a number input on Escape', async () => {
1141
- await usingAsync(new Injector(), async (i) => {
1142
+ await usingAsync(createInjector(), async (i) => {
1142
1143
  const input = document.createElement('input')
1143
1144
  input.type = 'number'
1144
1145
  input.value = '5'
@@ -1148,14 +1149,14 @@ describe('SpatialNavigationService', () => {
1148
1149
  input.focus()
1149
1150
  expect(document.activeElement).toBe(input)
1150
1151
 
1151
- i.getInstance(SpatialNavigationService)
1152
+ i.get(SpatialNavigationService)
1152
1153
  pressKey('Escape')
1153
1154
  expect(document.activeElement).toBe(document.body)
1154
1155
  })
1155
1156
  })
1156
1157
 
1157
1158
  it('Should blur a text input on Escape', async () => {
1158
- await usingAsync(new Injector(), async (i) => {
1159
+ await usingAsync(createInjector(), async (i) => {
1159
1160
  const input = document.createElement('input')
1160
1161
  input.type = 'text'
1161
1162
  input.value = 'hello'
@@ -1165,14 +1166,14 @@ describe('SpatialNavigationService', () => {
1165
1166
  input.focus()
1166
1167
  expect(document.activeElement).toBe(input)
1167
1168
 
1168
- i.getInstance(SpatialNavigationService)
1169
+ i.get(SpatialNavigationService)
1169
1170
  pressKey('Escape')
1170
1171
  expect(document.activeElement).toBe(document.body)
1171
1172
  })
1172
1173
  })
1173
1174
 
1174
1175
  it('Should blur a textarea on Escape', async () => {
1175
- await usingAsync(new Injector(), async (i) => {
1176
+ await usingAsync(createInjector(), async (i) => {
1176
1177
  const textarea = document.createElement('textarea')
1177
1178
  textarea.value = 'hello'
1178
1179
  mockRect(textarea, { left: 0, top: 0, width: 200, height: 100 })
@@ -1181,21 +1182,21 @@ describe('SpatialNavigationService', () => {
1181
1182
  textarea.focus()
1182
1183
  expect(document.activeElement).toBe(textarea)
1183
1184
 
1184
- i.getInstance(SpatialNavigationService)
1185
+ i.get(SpatialNavigationService)
1185
1186
  pressKey('Escape')
1186
1187
  expect(document.activeElement).toBe(document.body)
1187
1188
  })
1188
1189
  })
1189
1190
 
1190
1191
  it('Should not blur a button on Escape', async () => {
1191
- await usingAsync(new Injector(), async (i) => {
1192
+ await usingAsync(createInjector(), async (i) => {
1192
1193
  const btn = createButton('btn', { left: 0, top: 0, width: 50, height: 50 })
1193
1194
  document.body.append(btn)
1194
1195
 
1195
1196
  btn.focus()
1196
1197
  expect(document.activeElement).toBe(btn)
1197
1198
 
1198
- i.getInstance(SpatialNavigationService)
1199
+ i.get(SpatialNavigationService)
1199
1200
  pressKey('Escape')
1200
1201
  expect(document.activeElement).toBe(btn)
1201
1202
  })
@@ -1204,7 +1205,7 @@ describe('SpatialNavigationService', () => {
1204
1205
 
1205
1206
  describe('data-spatial-nav-target', () => {
1206
1207
  it('Should treat elements with data-spatial-nav-target as focusable candidates', async () => {
1207
- await usingAsync(new Injector(), async (i) => {
1208
+ await usingAsync(createInjector(), async (i) => {
1208
1209
  const origin = createButton('origin', { left: 0, top: 0, width: 50, height: 50 })
1209
1210
  const target = document.createElement('div')
1210
1211
  target.setAttribute('data-spatial-nav-target', '')
@@ -1216,7 +1217,7 @@ describe('SpatialNavigationService', () => {
1216
1217
  document.body.append(origin, target)
1217
1218
 
1218
1219
  origin.focus()
1219
- const s = i.getInstance(SpatialNavigationService)
1220
+ const s = i.get(SpatialNavigationService)
1220
1221
  s.moveFocus('right')
1221
1222
  expect(document.activeElement).toBe(target)
1222
1223
  })
@@ -1225,7 +1226,7 @@ describe('SpatialNavigationService', () => {
1225
1226
 
1226
1227
  describe('overflow-aware visibility', () => {
1227
1228
  it('Should skip candidates whose center is outside their overflow container', async () => {
1228
- await usingAsync(new Injector(), async (i) => {
1229
+ await usingAsync(createInjector(), async (i) => {
1229
1230
  const container = document.createElement('div')
1230
1231
  Object.defineProperty(container, 'computedStyleMap', { value: () => new Map() })
1231
1232
  vi.spyOn(window, 'getComputedStyle').mockImplementation((el) => {
@@ -1244,7 +1245,7 @@ describe('SpatialNavigationService', () => {
1244
1245
  document.body.append(container, origin)
1245
1246
 
1246
1247
  origin.focus()
1247
- const s = i.getInstance(SpatialNavigationService)
1248
+ const s = i.get(SpatialNavigationService)
1248
1249
  s.moveFocus('up')
1249
1250
  expect(document.activeElement).toBe(visible)
1250
1251
  })
@@ -1253,7 +1254,7 @@ describe('SpatialNavigationService', () => {
1253
1254
 
1254
1255
  describe('focus trap', () => {
1255
1256
  it('Should block cross-section navigation when a trap is active', async () => {
1256
- await usingAsync(new Injector(), async (i) => {
1257
+ await usingAsync(createInjector(), async (i) => {
1257
1258
  const section1 = document.createElement('div')
1258
1259
  section1.setAttribute('data-nav-section', 'modal')
1259
1260
  mockRect(section1, { left: 0, top: 0, width: 400, height: 400 })
@@ -1270,7 +1271,7 @@ describe('SpatialNavigationService', () => {
1270
1271
 
1271
1272
  btn1.focus()
1272
1273
 
1273
- const s = i.getInstance(SpatialNavigationService)
1274
+ const s = i.get(SpatialNavigationService)
1274
1275
  s.pushFocusTrap('modal')
1275
1276
 
1276
1277
  s.moveFocus('right')
@@ -1279,16 +1280,16 @@ describe('SpatialNavigationService', () => {
1279
1280
  })
1280
1281
 
1281
1282
  it('Should set activeSection when pushing a trap', async () => {
1282
- await usingAsync(new Injector(), async (i) => {
1283
- const s = i.getInstance(SpatialNavigationService)
1283
+ await usingAsync(createInjector(), async (i) => {
1284
+ const s = i.get(SpatialNavigationService)
1284
1285
  s.pushFocusTrap('dialog')
1285
1286
  expect(s.activeSection.getValue()).toBe('dialog')
1286
1287
  })
1287
1288
  })
1288
1289
 
1289
1290
  it('Should restore previous section on pop', async () => {
1290
- await usingAsync(new Injector(), async (i) => {
1291
- const s = i.getInstance(SpatialNavigationService)
1291
+ await usingAsync(createInjector(), async (i) => {
1292
+ const s = i.get(SpatialNavigationService)
1292
1293
  s.activeSection.setValue('main')
1293
1294
  s.pushFocusTrap('dialog')
1294
1295
  expect(s.activeSection.getValue()).toBe('dialog')
@@ -1299,8 +1300,8 @@ describe('SpatialNavigationService', () => {
1299
1300
  })
1300
1301
 
1301
1302
  it('Should support nested traps — topmost wins', async () => {
1302
- await usingAsync(new Injector(), async (i) => {
1303
- const s = i.getInstance(SpatialNavigationService)
1303
+ await usingAsync(createInjector(), async (i) => {
1304
+ const s = i.get(SpatialNavigationService)
1304
1305
  s.pushFocusTrap('outer-dialog')
1305
1306
  s.pushFocusTrap('inner-dialog')
1306
1307
  expect(s.activeSection.getValue()).toBe('inner-dialog')
@@ -1314,7 +1315,7 @@ describe('SpatialNavigationService', () => {
1314
1315
  })
1315
1316
 
1316
1317
  it('Should allow within-section navigation when trap is active', async () => {
1317
- await usingAsync(new Injector(), async (i) => {
1318
+ await usingAsync(createInjector(), async (i) => {
1318
1319
  const section = document.createElement('div')
1319
1320
  section.setAttribute('data-nav-section', 'modal')
1320
1321
  mockRect(section, { left: 0, top: 0, width: 400, height: 400 })
@@ -1325,7 +1326,7 @@ describe('SpatialNavigationService', () => {
1325
1326
 
1326
1327
  btn1.focus()
1327
1328
 
1328
- const s = i.getInstance(SpatialNavigationService)
1329
+ const s = i.get(SpatialNavigationService)
1329
1330
  s.pushFocusTrap('modal')
1330
1331
 
1331
1332
  s.moveFocus('down')
@@ -1334,7 +1335,7 @@ describe('SpatialNavigationService', () => {
1334
1335
  })
1335
1336
 
1336
1337
  it('Should focus within trapped section when activeElement is document.body', async () => {
1337
- await usingAsync(new Injector(), async (i) => {
1338
+ await usingAsync(createInjector(), async (i) => {
1338
1339
  const section1 = document.createElement('div')
1339
1340
  section1.setAttribute('data-nav-section', 'sidebar')
1340
1341
  mockRect(section1, { left: 0, top: 0, width: 200, height: 400 })
@@ -1353,7 +1354,7 @@ describe('SpatialNavigationService', () => {
1353
1354
  ;(document.activeElement as HTMLElement)?.blur()
1354
1355
  expect(document.activeElement).toBe(document.body)
1355
1356
 
1356
- const s = i.getInstance(SpatialNavigationService)
1357
+ const s = i.get(SpatialNavigationService)
1357
1358
  s.pushFocusTrap('modal')
1358
1359
 
1359
1360
  s.moveFocus('down')
@@ -1363,13 +1364,13 @@ describe('SpatialNavigationService', () => {
1363
1364
  })
1364
1365
 
1365
1366
  it('Should clear trap stack on disposal', async () => {
1366
- await usingAsync(new Injector(), async (i) => {
1367
- const s = i.getInstance(SpatialNavigationService)
1367
+ await usingAsync(createInjector(), async (i) => {
1368
+ const s = i.get(SpatialNavigationService)
1368
1369
  s.pushFocusTrap('dialog')
1369
1370
  })
1370
1371
  // After disposal, creating a new instance should have no traps
1371
- await usingAsync(new Injector(), async (i) => {
1372
- const s = i.getInstance(SpatialNavigationService)
1372
+ await usingAsync(createInjector(), async (i) => {
1373
+ const s = i.get(SpatialNavigationService)
1373
1374
  expect(s.activeSection.getValue()).toBeNull()
1374
1375
  })
1375
1376
  })
@@ -1377,19 +1378,23 @@ describe('SpatialNavigationService', () => {
1377
1378
 
1378
1379
  describe('configureSpatialNavigation', () => {
1379
1380
  it('Should configure the service with custom options', async () => {
1380
- await usingAsync(new Injector(), async (i) => {
1381
+ await usingAsync(createInjector(), async (i) => {
1381
1382
  configureSpatialNavigation(i, { initiallyEnabled: false })
1382
- const s = i.getInstance(SpatialNavigationService)
1383
+ const s = i.get(SpatialNavigationService)
1383
1384
  expect(s.enabled.getValue()).toBe(false)
1384
1385
  })
1385
1386
  })
1386
1387
 
1387
- it('Should throw if called after service is instantiated', async () => {
1388
- await usingAsync(new Injector(), async (i) => {
1389
- i.getInstance(SpatialNavigationService)
1390
- expect(() => configureSpatialNavigation(i, {})).toThrow(
1391
- 'configureSpatialNavigation must be called before the SpatialNavigationService is instantiated',
1392
- )
1388
+ it('Should rebind options when called after the service is instantiated', async () => {
1389
+ await usingAsync(createInjector(), async (i) => {
1390
+ const first = i.get(SpatialNavigationService)
1391
+ expect(first.enabled.getValue()).toBe(true)
1392
+
1393
+ configureSpatialNavigation(i, { initiallyEnabled: false })
1394
+
1395
+ const second = i.get(SpatialNavigationService)
1396
+ expect(second).not.toBe(first)
1397
+ expect(second.enabled.getValue()).toBe(false)
1393
1398
  })
1394
1399
  })
1395
1400
  })