@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.
- package/CHANGELOG.md +157 -0
- package/esm/component-factory.spec.d.ts.map +1 -1
- package/esm/component-factory.spec.js +2 -2
- package/esm/component-factory.spec.js.map +1 -1
- package/esm/components/define-nested-routes.d.ts +27 -0
- package/esm/components/define-nested-routes.d.ts.map +1 -0
- package/esm/components/define-nested-routes.js +26 -0
- package/esm/components/define-nested-routes.js.map +1 -0
- package/esm/components/define-nested-routes.spec.d.ts +2 -0
- package/esm/components/define-nested-routes.spec.d.ts.map +1 -0
- package/esm/components/define-nested-routes.spec.js +81 -0
- package/esm/components/define-nested-routes.spec.js.map +1 -0
- package/esm/components/index.d.ts +3 -3
- package/esm/components/index.d.ts.map +1 -1
- package/esm/components/index.js +3 -3
- package/esm/components/index.js.map +1 -1
- package/esm/components/nested-hooks.d.ts +62 -0
- package/esm/components/nested-hooks.d.ts.map +1 -0
- package/esm/components/nested-hooks.js +73 -0
- package/esm/components/nested-hooks.js.map +1 -0
- package/esm/components/nested-hooks.spec.d.ts +2 -0
- package/esm/components/nested-hooks.spec.d.ts.map +1 -0
- package/esm/components/nested-hooks.spec.js +211 -0
- package/esm/components/nested-hooks.spec.js.map +1 -0
- package/esm/components/nested-navigate.d.ts +52 -21
- package/esm/components/nested-navigate.d.ts.map +1 -1
- package/esm/components/nested-navigate.js +32 -17
- package/esm/components/nested-navigate.js.map +1 -1
- package/esm/components/nested-navigate.spec.js +109 -19
- package/esm/components/nested-navigate.spec.js.map +1 -1
- package/esm/components/nested-replace.d.ts +36 -0
- package/esm/components/nested-replace.d.ts.map +1 -0
- package/esm/components/nested-replace.js +38 -0
- package/esm/components/nested-replace.js.map +1 -0
- package/esm/components/nested-replace.spec.d.ts +2 -0
- package/esm/components/nested-replace.spec.d.ts.map +1 -0
- package/esm/components/nested-replace.spec.js +59 -0
- package/esm/components/nested-replace.spec.js.map +1 -0
- package/esm/components/nested-route-link.d.ts +36 -26
- package/esm/components/nested-route-link.d.ts.map +1 -1
- package/esm/components/nested-route-link.js +18 -20
- package/esm/components/nested-route-link.js.map +1 -1
- package/esm/components/nested-route-link.spec.js +67 -17
- package/esm/components/nested-route-link.spec.js.map +1 -1
- package/esm/components/nested-route-types.d.ts +79 -2
- package/esm/components/nested-route-types.d.ts.map +1 -1
- package/esm/components/nested-router.d.ts +77 -7
- package/esm/components/nested-router.d.ts.map +1 -1
- package/esm/components/nested-router.js +139 -13
- package/esm/components/nested-router.js.map +1 -1
- package/esm/components/nested-router.spec.js +283 -52
- package/esm/components/nested-router.spec.js.map +1 -1
- package/esm/services/location-service.d.ts +39 -42
- package/esm/services/location-service.d.ts.map +1 -1
- package/esm/services/location-service.js +115 -134
- package/esm/services/location-service.js.map +1 -1
- package/esm/services/location-service.spec.js +59 -24
- package/esm/services/location-service.spec.js.map +1 -1
- package/esm/services/route-match-service.d.ts +3 -2
- package/esm/services/route-match-service.d.ts.map +1 -1
- package/esm/services/route-match-service.js +13 -22
- package/esm/services/route-match-service.js.map +1 -1
- package/esm/services/route-match-service.spec.js +42 -49
- package/esm/services/route-match-service.spec.js.map +1 -1
- package/esm/services/route-meta-utils.d.ts +28 -6
- package/esm/services/route-meta-utils.d.ts.map +1 -1
- package/esm/services/route-meta-utils.js +18 -4
- package/esm/services/route-meta-utils.js.map +1 -1
- package/esm/services/route-meta-utils.spec.js +60 -5
- package/esm/services/route-meta-utils.spec.js.map +1 -1
- package/esm/services/screen-service.d.ts +13 -72
- package/esm/services/screen-service.d.ts.map +1 -1
- package/esm/services/screen-service.js +45 -137
- package/esm/services/screen-service.js.map +1 -1
- package/esm/services/screen-service.spec.js +19 -18
- package/esm/services/screen-service.spec.js.map +1 -1
- package/esm/services/spatial-navigation-service.d.ts +17 -39
- package/esm/services/spatial-navigation-service.d.ts.map +1 -1
- package/esm/services/spatial-navigation-service.js +280 -313
- package/esm/services/spatial-navigation-service.js.map +1 -1
- package/esm/services/spatial-navigation-service.spec.js +163 -158
- package/esm/services/spatial-navigation-service.spec.js.map +1 -1
- package/esm/shade.d.ts +1 -1
- package/esm/shade.d.ts.map +1 -1
- package/esm/shade.js +10 -7
- package/esm/shade.js.map +1 -1
- package/esm/vnode.d.ts +1 -1
- package/esm/vnode.js +1 -1
- package/package.json +5 -4
- package/src/component-factory.spec.tsx +3 -2
- package/src/components/define-nested-routes.spec.ts +91 -0
- package/src/components/define-nested-routes.ts +27 -0
- package/src/components/index.ts +3 -3
- package/src/components/nested-hooks.spec.ts +254 -0
- package/src/components/nested-hooks.ts +109 -0
- package/src/components/nested-navigate.spec.ts +136 -32
- package/src/components/nested-navigate.ts +75 -26
- package/src/components/nested-replace.spec.ts +89 -0
- package/src/components/nested-replace.ts +46 -0
- package/src/components/nested-route-link.spec.tsx +90 -22
- package/src/components/nested-route-link.tsx +54 -29
- package/src/components/nested-route-types.ts +82 -2
- package/src/components/nested-router.spec.tsx +334 -51
- package/src/components/nested-router.tsx +184 -24
- package/src/services/location-service.spec.ts +68 -25
- package/src/services/location-service.tsx +156 -122
- package/src/services/route-match-service.spec.ts +41 -28
- package/src/services/route-match-service.ts +17 -8
- package/src/services/route-meta-utils.spec.ts +78 -10
- package/src/services/route-meta-utils.ts +44 -15
- package/src/services/screen-service.spec.ts +19 -18
- package/src/services/screen-service.ts +65 -110
- package/src/services/spatial-navigation-service.spec.ts +165 -160
- package/src/services/spatial-navigation-service.ts +299 -287
- package/src/shade.ts +11 -8
- package/src/vnode.ts +1 -1
- package/esm/components/link-to-route.d.ts +0 -10
- package/esm/components/link-to-route.d.ts.map +0 -1
- package/esm/components/link-to-route.js +0 -16
- package/esm/components/link-to-route.js.map +0 -1
- package/esm/components/link-to-route.spec.d.ts +0 -2
- package/esm/components/link-to-route.spec.d.ts.map +0 -1
- package/esm/components/link-to-route.spec.js +0 -30
- package/esm/components/link-to-route.spec.js.map +0 -1
- package/esm/components/route-link.d.ts +0 -10
- package/esm/components/route-link.d.ts.map +0 -1
- package/esm/components/route-link.js +0 -25
- package/esm/components/route-link.js.map +0 -1
- package/esm/components/route-link.spec.d.ts +0 -2
- package/esm/components/route-link.spec.d.ts.map +0 -1
- package/esm/components/route-link.spec.js +0 -34
- package/esm/components/route-link.spec.js.map +0 -1
- package/esm/components/router.d.ts +0 -44
- package/esm/components/router.d.ts.map +0 -1
- package/esm/components/router.js +0 -66
- package/esm/components/router.js.map +0 -1
- package/esm/components/router.spec.d.ts +0 -2
- package/esm/components/router.spec.d.ts.map +0 -1
- package/esm/components/router.spec.js +0 -161
- package/esm/components/router.spec.js.map +0 -1
- package/src/components/link-to-route.spec.tsx +0 -45
- package/src/components/link-to-route.tsx +0 -26
- package/src/components/route-link.spec.tsx +0 -44
- package/src/components/route-link.tsx +0 -29
- package/src/components/router.spec.tsx +0 -217
- package/src/components/router.tsx +0 -103
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
-
};
|
|
7
|
-
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
-
};
|
|
10
|
-
import { Injectable } from '@furystack/inject';
|
|
1
|
+
import { defineService } from '@furystack/inject';
|
|
11
2
|
import { ObservableValue } from '@furystack/utils';
|
|
12
3
|
const DEFAULT_FOCUSABLE_SELECTOR = [
|
|
13
4
|
'[tabindex]:not([tabindex="-1"])',
|
|
@@ -193,331 +184,307 @@ const shouldPassthroughArrowKeys = (element, key) => {
|
|
|
193
184
|
return false;
|
|
194
185
|
};
|
|
195
186
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* Intercepts arrow key events and moves focus spatially based on element geometry.
|
|
199
|
-
* Supports section boundaries via `data-nav-section` attributes and optional
|
|
200
|
-
* cross-section navigation.
|
|
201
|
-
*
|
|
202
|
-
* @example
|
|
203
|
-
* ```typescript
|
|
204
|
-
* // Opt in to spatial navigation
|
|
205
|
-
* const spatialNav = injector.getInstance(SpatialNavigationService)
|
|
206
|
-
*
|
|
207
|
-
* // Disable during video playback
|
|
208
|
-
* spatialNav.enabled.setValue(false)
|
|
209
|
-
*
|
|
210
|
-
* // Re-enable
|
|
211
|
-
* spatialNav.enabled.setValue(true)
|
|
212
|
-
* ```
|
|
187
|
+
* Configuration token for {@link SpatialNavigationService}. Override via
|
|
188
|
+
* {@link configureSpatialNavigation} before the service is first resolved.
|
|
213
189
|
*/
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
let target = this.findNearestInDirection(currentRect, candidates, direction);
|
|
279
|
-
if (!target) {
|
|
280
|
-
const relaxedCandidates = this.getFocusableCandidates(searchRoot, activeElement, {
|
|
281
|
-
skipScrollVisibility: true,
|
|
190
|
+
export const SpatialNavigationSettings = defineService({
|
|
191
|
+
name: '@furystack/shades/SpatialNavigationSettings',
|
|
192
|
+
lifetime: 'singleton',
|
|
193
|
+
factory: () => ({}),
|
|
194
|
+
});
|
|
195
|
+
export const SpatialNavigationService = defineService({
|
|
196
|
+
name: '@furystack/shades/SpatialNavigationService',
|
|
197
|
+
lifetime: 'singleton',
|
|
198
|
+
factory: ({ inject, onDispose }) => {
|
|
199
|
+
const options = inject(SpatialNavigationSettings);
|
|
200
|
+
const enabled = new ObservableValue(options.initiallyEnabled ?? true);
|
|
201
|
+
const activeSection = new ObservableValue(null);
|
|
202
|
+
const focusMemory = new Map();
|
|
203
|
+
const focusTrapStack = [];
|
|
204
|
+
const focusableSelector = options.focusableSelector ?? DEFAULT_FOCUSABLE_SELECTOR;
|
|
205
|
+
const crossSectionNavigation = options.crossSectionNavigation ?? true;
|
|
206
|
+
const backspaceGoesBack = options.backspaceGoesBack ?? false;
|
|
207
|
+
const escapeGoesToParentSection = options.escapeGoesToParentSection ?? false;
|
|
208
|
+
const getActiveTrap = () => focusTrapStack[focusTrapStack.length - 1] ?? null;
|
|
209
|
+
const pushFocusTrap = (sectionName) => {
|
|
210
|
+
focusTrapStack.push(sectionName);
|
|
211
|
+
activeSection.setValue(sectionName);
|
|
212
|
+
};
|
|
213
|
+
const popFocusTrap = (sectionName, previousSection) => {
|
|
214
|
+
const idx = focusTrapStack.lastIndexOf(sectionName);
|
|
215
|
+
if (idx !== -1) {
|
|
216
|
+
focusTrapStack.splice(idx, 1);
|
|
217
|
+
}
|
|
218
|
+
const top = focusTrapStack[focusTrapStack.length - 1];
|
|
219
|
+
activeSection.setValue(top ?? previousSection ?? null);
|
|
220
|
+
};
|
|
221
|
+
const findContainingSection = (element) => element.closest('[data-nav-section]');
|
|
222
|
+
const isVisibleInScrollContainers = (el, rect) => {
|
|
223
|
+
const centerX = rect.left + rect.width / 2;
|
|
224
|
+
const centerY = rect.top + rect.height / 2;
|
|
225
|
+
const hasOverflow = (val) => val !== '' && val !== 'visible';
|
|
226
|
+
let ancestor = el.parentElement;
|
|
227
|
+
while (ancestor) {
|
|
228
|
+
const style = getComputedStyle(ancestor);
|
|
229
|
+
if (hasOverflow(style.overflow) || hasOverflow(style.overflowX) || hasOverflow(style.overflowY)) {
|
|
230
|
+
const containerRect = ancestor.getBoundingClientRect();
|
|
231
|
+
if (centerX < containerRect.left ||
|
|
232
|
+
centerX > containerRect.right ||
|
|
233
|
+
centerY < containerRect.top ||
|
|
234
|
+
centerY > containerRect.bottom) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
ancestor = ancestor.parentElement;
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
};
|
|
242
|
+
const getFocusableCandidates = (root, exclude, candidateOptions) => {
|
|
243
|
+
return Array.from(root.querySelectorAll(focusableSelector)).filter((el) => {
|
|
244
|
+
if (el === exclude)
|
|
245
|
+
return false;
|
|
246
|
+
if (!el.hasAttribute('data-spatial-nav-target') && el.closest('[data-spatial-nav-target]'))
|
|
247
|
+
return false;
|
|
248
|
+
const rect = el.getBoundingClientRect();
|
|
249
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
250
|
+
return false;
|
|
251
|
+
if (candidateOptions?.skipScrollVisibility)
|
|
252
|
+
return true;
|
|
253
|
+
return isVisibleInScrollContainers(el, rect);
|
|
282
254
|
});
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
/** Programmatically activate (click) the currently focused element */
|
|
298
|
-
activateFocused() {
|
|
299
|
-
const { activeElement } = document;
|
|
300
|
-
if (activeElement && activeElement !== document.body) {
|
|
301
|
-
;
|
|
302
|
-
activeElement.click();
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
handleKeyDown = (ev) => {
|
|
306
|
-
if (!this.enabled.getValue())
|
|
307
|
-
return;
|
|
308
|
-
if (ev.defaultPrevented)
|
|
309
|
-
return;
|
|
310
|
-
const { activeElement } = document;
|
|
311
|
-
if (activeElement && isInsidePassthrough(activeElement))
|
|
312
|
-
return;
|
|
313
|
-
if (activeElement && shouldPassthroughArrowKeys(activeElement, ev.key)) {
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
switch (ev.key) {
|
|
317
|
-
case 'ArrowUp':
|
|
318
|
-
ev.preventDefault();
|
|
319
|
-
this.moveFocus('up');
|
|
320
|
-
break;
|
|
321
|
-
case 'ArrowDown':
|
|
322
|
-
ev.preventDefault();
|
|
323
|
-
this.moveFocus('down');
|
|
324
|
-
break;
|
|
325
|
-
case 'ArrowLeft':
|
|
326
|
-
ev.preventDefault();
|
|
327
|
-
this.moveFocus('left');
|
|
328
|
-
break;
|
|
329
|
-
case 'ArrowRight':
|
|
330
|
-
ev.preventDefault();
|
|
331
|
-
this.moveFocus('right');
|
|
332
|
-
break;
|
|
333
|
-
case 'Enter':
|
|
334
|
-
if (activeElement && isTextInput(activeElement))
|
|
335
|
-
break;
|
|
336
|
-
ev.preventDefault();
|
|
337
|
-
this.activateFocused();
|
|
338
|
-
break;
|
|
339
|
-
case 'Backspace':
|
|
340
|
-
if (this.backspaceGoesBack && !(activeElement && isTextInput(activeElement))) {
|
|
341
|
-
ev.preventDefault();
|
|
342
|
-
history.back();
|
|
255
|
+
};
|
|
256
|
+
const findNearestInDirection = (currentRect, candidates, direction) => {
|
|
257
|
+
const currentCenter = getElementCenter(currentRect);
|
|
258
|
+
let nearest = null;
|
|
259
|
+
let nearestDistance = Infinity;
|
|
260
|
+
for (const candidate of candidates) {
|
|
261
|
+
const candidateRect = candidate.getBoundingClientRect();
|
|
262
|
+
if (!isInDirection(currentRect, candidateRect, direction))
|
|
263
|
+
continue;
|
|
264
|
+
const candidateCenter = getElementCenter(candidateRect);
|
|
265
|
+
const distance = spatialDistance(currentCenter, candidateCenter, direction);
|
|
266
|
+
if (distance < nearestDistance) {
|
|
267
|
+
nearestDistance = distance;
|
|
268
|
+
nearest = candidate;
|
|
343
269
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
270
|
+
}
|
|
271
|
+
return nearest;
|
|
272
|
+
};
|
|
273
|
+
const storeFocusMemory = (sectionName, element) => {
|
|
274
|
+
if (sectionName) {
|
|
275
|
+
focusMemory.set(sectionName, new WeakRef(element));
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const navigateCrossSection = (activeElement, currentSection, direction) => {
|
|
279
|
+
const currentSectionName = currentSection.getAttribute('data-nav-section');
|
|
280
|
+
const allFocusable = Array.from(document.querySelectorAll(focusableSelector)).filter((el) => {
|
|
281
|
+
if (el === activeElement)
|
|
282
|
+
return false;
|
|
283
|
+
if (currentSection.contains(el))
|
|
284
|
+
return false;
|
|
285
|
+
if (!el.hasAttribute('data-spatial-nav-target') && el.closest('[data-spatial-nav-target]'))
|
|
286
|
+
return false;
|
|
287
|
+
const rect = el.getBoundingClientRect();
|
|
288
|
+
return rect.width > 0 && rect.height > 0 && isVisibleInScrollContainers(el, rect);
|
|
289
|
+
});
|
|
290
|
+
const currentRect = activeElement.getBoundingClientRect();
|
|
291
|
+
const nearest = findNearestInDirection(currentRect, allFocusable, direction);
|
|
292
|
+
if (nearest) {
|
|
293
|
+
storeFocusMemory(currentSectionName, activeElement);
|
|
294
|
+
const targetSection = findContainingSection(nearest);
|
|
295
|
+
const targetSectionName = targetSection?.getAttribute('data-nav-section') ?? null;
|
|
296
|
+
const remembered = targetSectionName ? focusMemory.get(targetSectionName)?.deref() : null;
|
|
297
|
+
if (remembered &&
|
|
298
|
+
remembered !== activeElement &&
|
|
299
|
+
targetSection?.contains(remembered) &&
|
|
300
|
+
remembered.matches(focusableSelector)) {
|
|
301
|
+
const rememberedRect = remembered.getBoundingClientRect();
|
|
302
|
+
if (rememberedRect.width > 0 &&
|
|
303
|
+
rememberedRect.height > 0 &&
|
|
304
|
+
isVisibleInScrollContainers(remembered, rememberedRect)) {
|
|
305
|
+
;
|
|
306
|
+
remembered.focus();
|
|
307
|
+
remembered.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
308
|
+
activeSection.setValue(targetSectionName);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
350
311
|
}
|
|
351
|
-
|
|
352
|
-
|
|
312
|
+
nearest.focus();
|
|
313
|
+
nearest.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
314
|
+
activeSection.setValue(targetSectionName);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const focusFirstElement = () => {
|
|
318
|
+
const trap = getActiveTrap();
|
|
319
|
+
if (trap) {
|
|
320
|
+
const trapSection = document.querySelector(`[data-nav-section="${escapeCssString(trap)}"]`);
|
|
321
|
+
if (trapSection) {
|
|
322
|
+
const firstFocusable = trapSection.querySelector(focusableSelector);
|
|
323
|
+
if (firstFocusable) {
|
|
324
|
+
;
|
|
325
|
+
firstFocusable.focus();
|
|
326
|
+
activeSection.setValue(trap);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
353
329
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
};
|
|
359
|
-
focusFirstElement() {
|
|
360
|
-
const trap = this.activeTrap;
|
|
361
|
-
if (trap) {
|
|
362
|
-
const trapSection = document.querySelector(`[data-nav-section="${escapeCssString(trap)}"]`);
|
|
363
|
-
if (trapSection) {
|
|
364
|
-
const firstFocusable = trapSection.querySelector(this.focusableSelector);
|
|
330
|
+
}
|
|
331
|
+
const sections = document.querySelectorAll('[data-nav-section]');
|
|
332
|
+
if (sections.length > 0) {
|
|
333
|
+
const firstFocusable = sections[0].querySelector(focusableSelector);
|
|
365
334
|
if (firstFocusable) {
|
|
366
335
|
;
|
|
367
336
|
firstFocusable.focus();
|
|
368
|
-
|
|
337
|
+
activeSection.setValue(sections[0].getAttribute('data-nav-section'));
|
|
369
338
|
return;
|
|
370
339
|
}
|
|
371
340
|
}
|
|
372
|
-
|
|
373
|
-
const sections = document.querySelectorAll('[data-nav-section]');
|
|
374
|
-
if (sections.length > 0) {
|
|
375
|
-
const firstFocusable = sections[0].querySelector(this.focusableSelector);
|
|
341
|
+
const firstFocusable = document.querySelector(focusableSelector);
|
|
376
342
|
if (firstFocusable) {
|
|
377
343
|
;
|
|
378
344
|
firstFocusable.focus();
|
|
379
|
-
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const moveFocus = (direction) => {
|
|
348
|
+
const activeElement = document.activeElement;
|
|
349
|
+
if (!activeElement || activeElement === document.body) {
|
|
350
|
+
focusFirstElement();
|
|
380
351
|
return;
|
|
381
352
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const centerY = rect.top + rect.height / 2;
|
|
395
|
-
const hasOverflow = (val) => val !== '' && val !== 'visible';
|
|
396
|
-
let ancestor = el.parentElement;
|
|
397
|
-
while (ancestor) {
|
|
398
|
-
const style = getComputedStyle(ancestor);
|
|
399
|
-
if (hasOverflow(style.overflow) || hasOverflow(style.overflowX) || hasOverflow(style.overflowY)) {
|
|
400
|
-
const containerRect = ancestor.getBoundingClientRect();
|
|
401
|
-
if (centerX < containerRect.left ||
|
|
402
|
-
centerX > containerRect.right ||
|
|
403
|
-
centerY < containerRect.top ||
|
|
404
|
-
centerY > containerRect.bottom) {
|
|
405
|
-
return false;
|
|
406
|
-
}
|
|
353
|
+
const currentSection = findContainingSection(activeElement);
|
|
354
|
+
const currentSectionName = currentSection?.getAttribute('data-nav-section') ?? null;
|
|
355
|
+
activeSection.setValue(currentSectionName);
|
|
356
|
+
const searchRoot = currentSection ?? document;
|
|
357
|
+
const candidates = getFocusableCandidates(searchRoot, activeElement);
|
|
358
|
+
const currentRect = activeElement.getBoundingClientRect();
|
|
359
|
+
let target = findNearestInDirection(currentRect, candidates, direction);
|
|
360
|
+
if (!target) {
|
|
361
|
+
const relaxedCandidates = getFocusableCandidates(searchRoot, activeElement, {
|
|
362
|
+
skipScrollVisibility: true,
|
|
363
|
+
});
|
|
364
|
+
target = findNearestInDirection(currentRect, relaxedCandidates, direction);
|
|
407
365
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
return false;
|
|
416
|
-
if (!el.hasAttribute('data-spatial-nav-target') && el.closest('[data-spatial-nav-target]'))
|
|
417
|
-
return false;
|
|
418
|
-
const rect = el.getBoundingClientRect();
|
|
419
|
-
if (rect.width <= 0 || rect.height <= 0)
|
|
420
|
-
return false;
|
|
421
|
-
if (options?.skipScrollVisibility)
|
|
422
|
-
return true;
|
|
423
|
-
return this.isVisibleInScrollContainers(el, rect);
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
findNearestInDirection(currentRect, candidates, direction) {
|
|
427
|
-
const currentCenter = getElementCenter(currentRect);
|
|
428
|
-
let nearest = null;
|
|
429
|
-
let nearestDistance = Infinity;
|
|
430
|
-
for (const candidate of candidates) {
|
|
431
|
-
const candidateRect = candidate.getBoundingClientRect();
|
|
432
|
-
if (!isInDirection(currentRect, candidateRect, direction))
|
|
433
|
-
continue;
|
|
434
|
-
const candidateCenter = getElementCenter(candidateRect);
|
|
435
|
-
const distance = spatialDistance(currentCenter, candidateCenter, direction);
|
|
436
|
-
if (distance < nearestDistance) {
|
|
437
|
-
nearestDistance = distance;
|
|
438
|
-
nearest = candidate;
|
|
366
|
+
if (target) {
|
|
367
|
+
storeFocusMemory(currentSectionName, activeElement);
|
|
368
|
+
target.focus();
|
|
369
|
+
target.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
370
|
+
const targetSection = findContainingSection(target);
|
|
371
|
+
activeSection.setValue(targetSection?.getAttribute('data-nav-section') ?? null);
|
|
372
|
+
return;
|
|
439
373
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
navigateCrossSection(activeElement, currentSection, direction) {
|
|
444
|
-
const currentSectionName = currentSection.getAttribute('data-nav-section');
|
|
445
|
-
const allFocusable = Array.from(document.querySelectorAll(this.focusableSelector)).filter((el) => {
|
|
446
|
-
if (el === activeElement)
|
|
447
|
-
return false;
|
|
448
|
-
if (currentSection.contains(el))
|
|
449
|
-
return false;
|
|
450
|
-
if (!el.hasAttribute('data-spatial-nav-target') && el.closest('[data-spatial-nav-target]'))
|
|
451
|
-
return false;
|
|
452
|
-
const rect = el.getBoundingClientRect();
|
|
453
|
-
return rect.width > 0 && rect.height > 0 && this.isVisibleInScrollContainers(el, rect);
|
|
454
|
-
});
|
|
455
|
-
const currentRect = activeElement.getBoundingClientRect();
|
|
456
|
-
const nearest = this.findNearestInDirection(currentRect, allFocusable, direction);
|
|
457
|
-
if (nearest) {
|
|
458
|
-
this.storeFocusMemory(currentSectionName, activeElement);
|
|
459
|
-
const targetSection = this.findContainingSection(nearest);
|
|
460
|
-
const targetSectionName = targetSection?.getAttribute('data-nav-section') ?? null;
|
|
461
|
-
const remembered = targetSectionName ? this.focusMemory.get(targetSectionName)?.deref() : null;
|
|
462
|
-
if (remembered &&
|
|
463
|
-
remembered !== activeElement &&
|
|
464
|
-
targetSection?.contains(remembered) &&
|
|
465
|
-
remembered.matches(this.focusableSelector)) {
|
|
466
|
-
const rememberedRect = remembered.getBoundingClientRect();
|
|
467
|
-
if (rememberedRect.width > 0 &&
|
|
468
|
-
rememberedRect.height > 0 &&
|
|
469
|
-
this.isVisibleInScrollContainers(remembered, rememberedRect)) {
|
|
470
|
-
;
|
|
471
|
-
remembered.focus();
|
|
472
|
-
remembered.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
473
|
-
this.activeSection.setValue(targetSectionName);
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
374
|
+
if (crossSectionNavigation && currentSection && !getActiveTrap()) {
|
|
375
|
+
navigateCrossSection(activeElement, currentSection, direction);
|
|
476
376
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
377
|
+
};
|
|
378
|
+
const activateFocused = () => {
|
|
379
|
+
const { activeElement } = document;
|
|
380
|
+
if (activeElement && activeElement !== document.body) {
|
|
381
|
+
;
|
|
382
|
+
activeElement.click();
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
const moveToParentSection = () => {
|
|
386
|
+
const { activeElement } = document;
|
|
387
|
+
if (!activeElement || activeElement === document.body)
|
|
388
|
+
return;
|
|
389
|
+
const currentSection = findContainingSection(activeElement);
|
|
390
|
+
if (!currentSection)
|
|
391
|
+
return;
|
|
392
|
+
const parentSection = currentSection.parentElement?.closest('[data-nav-section]');
|
|
393
|
+
if (!parentSection)
|
|
394
|
+
return;
|
|
395
|
+
const firstFocusable = parentSection.querySelector(focusableSelector);
|
|
396
|
+
if (firstFocusable) {
|
|
397
|
+
;
|
|
398
|
+
firstFocusable.focus();
|
|
399
|
+
activeSection.setValue(parentSection.getAttribute('data-nav-section'));
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
const handleKeyDown = (ev) => {
|
|
403
|
+
if (!enabled.getValue())
|
|
404
|
+
return;
|
|
405
|
+
if (ev.defaultPrevented)
|
|
406
|
+
return;
|
|
407
|
+
const { activeElement } = document;
|
|
408
|
+
if (activeElement && isInsidePassthrough(activeElement))
|
|
409
|
+
return;
|
|
410
|
+
if (activeElement && shouldPassthroughArrowKeys(activeElement, ev.key)) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
switch (ev.key) {
|
|
414
|
+
case 'ArrowUp':
|
|
415
|
+
ev.preventDefault();
|
|
416
|
+
moveFocus('up');
|
|
417
|
+
break;
|
|
418
|
+
case 'ArrowDown':
|
|
419
|
+
ev.preventDefault();
|
|
420
|
+
moveFocus('down');
|
|
421
|
+
break;
|
|
422
|
+
case 'ArrowLeft':
|
|
423
|
+
ev.preventDefault();
|
|
424
|
+
moveFocus('left');
|
|
425
|
+
break;
|
|
426
|
+
case 'ArrowRight':
|
|
427
|
+
ev.preventDefault();
|
|
428
|
+
moveFocus('right');
|
|
429
|
+
break;
|
|
430
|
+
case 'Enter':
|
|
431
|
+
if (activeElement && isTextInput(activeElement))
|
|
432
|
+
break;
|
|
433
|
+
ev.preventDefault();
|
|
434
|
+
activateFocused();
|
|
435
|
+
break;
|
|
436
|
+
case 'Backspace':
|
|
437
|
+
if (backspaceGoesBack && !(activeElement && isTextInput(activeElement))) {
|
|
438
|
+
ev.preventDefault();
|
|
439
|
+
history.back();
|
|
440
|
+
}
|
|
441
|
+
break;
|
|
442
|
+
case 'Escape':
|
|
443
|
+
if (activeElement && activeElement !== document.body && shouldEscapeBlurElement(activeElement)) {
|
|
444
|
+
ev.preventDefault();
|
|
445
|
+
activeElement.blur();
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
if (escapeGoesToParentSection) {
|
|
449
|
+
moveToParentSection();
|
|
450
|
+
}
|
|
451
|
+
break;
|
|
452
|
+
default:
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
457
|
+
onDispose(() => {
|
|
458
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
459
|
+
// eslint-disable-next-line furystack/prefer-using-wrapper -- Disposal is deferred to the injector's onDispose hook.
|
|
460
|
+
enabled[Symbol.dispose]();
|
|
461
|
+
// eslint-disable-next-line furystack/prefer-using-wrapper -- Disposal is deferred to the injector's onDispose hook.
|
|
462
|
+
activeSection[Symbol.dispose]();
|
|
463
|
+
focusMemory.clear();
|
|
464
|
+
focusTrapStack.length = 0;
|
|
465
|
+
});
|
|
466
|
+
return {
|
|
467
|
+
enabled,
|
|
468
|
+
activeSection,
|
|
469
|
+
pushFocusTrap,
|
|
470
|
+
popFocusTrap,
|
|
471
|
+
moveFocus,
|
|
472
|
+
activateFocused,
|
|
473
|
+
};
|
|
474
|
+
},
|
|
475
|
+
});
|
|
510
476
|
/**
|
|
511
477
|
* Configures spatial navigation options before the service is first instantiated.
|
|
512
|
-
*
|
|
513
|
-
* @
|
|
514
|
-
*
|
|
478
|
+
* Rebinds {@link SpatialNavigationSettings} and invalidates the cached
|
|
479
|
+
* {@link SpatialNavigationService}. Must be called **before** the service is
|
|
480
|
+
* first resolved — calling afterwards drops the cached instance without
|
|
481
|
+
* disposing it (listeners leak until the injector is disposed).
|
|
482
|
+
* @param injector The root injector.
|
|
483
|
+
* @param options Configuration options for spatial navigation.
|
|
515
484
|
*/
|
|
516
485
|
export const configureSpatialNavigation = (injector, options) => {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const service = new SpatialNavigationService(options);
|
|
521
|
-
injector.setExplicitInstance(service, SpatialNavigationService);
|
|
486
|
+
injector.bind(SpatialNavigationSettings, () => options);
|
|
487
|
+
injector.invalidate(SpatialNavigationSettings);
|
|
488
|
+
injector.invalidate(SpatialNavigationService);
|
|
522
489
|
};
|
|
523
490
|
//# sourceMappingURL=spatial-navigation-service.js.map
|