@furystack/shades 13.0.0 → 13.1.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 +52 -0
- package/esm/models/render-options.d.ts +5 -2
- package/esm/models/render-options.d.ts.map +1 -1
- package/esm/services/index.d.ts +1 -0
- package/esm/services/index.d.ts.map +1 -1
- package/esm/services/index.js +1 -0
- package/esm/services/index.js.map +1 -1
- package/esm/services/spatial-navigation-service.d.ts +88 -0
- package/esm/services/spatial-navigation-service.d.ts.map +1 -0
- package/esm/services/spatial-navigation-service.js +523 -0
- package/esm/services/spatial-navigation-service.js.map +1 -0
- package/esm/services/spatial-navigation-service.spec.d.ts +2 -0
- package/esm/services/spatial-navigation-service.spec.d.ts.map +1 -0
- package/esm/services/spatial-navigation-service.spec.js +1133 -0
- package/esm/services/spatial-navigation-service.spec.js.map +1 -0
- package/package.json +2 -2
- package/src/models/render-options.ts +5 -2
- package/src/services/index.ts +1 -0
- package/src/services/spatial-navigation-service.spec.ts +1396 -0
- package/src/services/spatial-navigation-service.ts +597 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [13.1.0] - 2026-03-10
|
|
4
|
+
|
|
5
|
+
### ✨ Features
|
|
6
|
+
|
|
7
|
+
### Spatial Navigation Service
|
|
8
|
+
|
|
9
|
+
Added `SpatialNavigationService` for D-pad / arrow-key spatial navigation across interactive elements. The service intercepts arrow key events and moves focus spatially based on element geometry, supporting section boundaries via `data-nav-section` attributes and optional cross-section navigation.
|
|
10
|
+
|
|
11
|
+
**Key capabilities:**
|
|
12
|
+
|
|
13
|
+
- Arrow key focus movement based on Euclidean distance between element centers
|
|
14
|
+
- Section-scoped navigation with `data-nav-section` DOM attributes
|
|
15
|
+
- Cross-section navigation with focus memory (restores last-focused element per section)
|
|
16
|
+
- Input passthrough — arrow keys work normally inside text inputs, textareas, selects, and contenteditable elements
|
|
17
|
+
- Configurable `Backspace` → `history.back()` and `Escape` → parent section behaviors
|
|
18
|
+
- Runtime enable/disable via `enabled` observable
|
|
19
|
+
- `configureSpatialNavigation()` helper to set options before the singleton is first resolved
|
|
20
|
+
|
|
21
|
+
**Usage:**
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { SpatialNavigationService, configureSpatialNavigation } from '@furystack/shades'
|
|
25
|
+
|
|
26
|
+
// Configure before first use
|
|
27
|
+
configureSpatialNavigation(injector, {
|
|
28
|
+
initiallyEnabled: true,
|
|
29
|
+
crossSectionNavigation: true,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Or resolve directly with defaults
|
|
33
|
+
const spatialNav = injector.getInstance(SpatialNavigationService)
|
|
34
|
+
|
|
35
|
+
// Toggle at runtime
|
|
36
|
+
spatialNav.enabled.setValue(false)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### `useDisposable` deps parameter
|
|
40
|
+
|
|
41
|
+
Added optional `deps` parameter to `useDisposable` — when provided, the resource is re-created (and the old one disposed) whenever the serialized deps value changes.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
useDisposable('my-resource', () => createResource(value), [value])
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 🧪 Tests
|
|
48
|
+
|
|
49
|
+
- Added tests for `SpatialNavigationService` covering directional movement, section boundaries, cross-section navigation, input passthrough, focus memory, and disposal
|
|
50
|
+
|
|
51
|
+
### ⬆️ Dependencies
|
|
52
|
+
|
|
53
|
+
- Updated `@furystack/core` dependency to the new major version
|
|
54
|
+
|
|
3
55
|
## [13.0.0] - 2026-03-07
|
|
4
56
|
|
|
5
57
|
### ⬆️ Dependencies
|
|
@@ -77,12 +77,15 @@ export type RenderOptions<TProps, TElementBase extends HTMLElement = HTMLElement
|
|
|
77
77
|
*/
|
|
78
78
|
useRef: <T extends Element = HTMLElement>(key: string) => RefObject<T>;
|
|
79
79
|
/**
|
|
80
|
-
* Creates and disposes a resource after the component has been detached from the DOM
|
|
80
|
+
* Creates and disposes a resource after the component has been detached from the DOM.
|
|
81
|
+
* When `deps` is provided, the resource is re-created (and the old one disposed) whenever
|
|
82
|
+
* the serialized deps value changes.
|
|
81
83
|
* @param key The key for caching the disposable resource
|
|
82
84
|
* @param factory A factory method for creating the disposable resource
|
|
85
|
+
* @param deps Optional dependency array — when deps change, the old resource is disposed and a new one is created
|
|
83
86
|
* @returns The Disposable instance
|
|
84
87
|
*/
|
|
85
|
-
useDisposable: <T extends Disposable | AsyncDisposable>(key: string, factory: () => T) => T;
|
|
88
|
+
useDisposable: <T extends Disposable | AsyncDisposable>(key: string, factory: () => T, deps?: readonly unknown[]) => T;
|
|
86
89
|
/**
|
|
87
90
|
* Creates a state object from an existing observable value.
|
|
88
91
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-options.d.ts","sourceRoot":"","sources":["../../src/models/render-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE1D;;;;;GAKG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,OAAO,GAAG,WAAW,IAAI;IACvD,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAA;CAC3B,CAAA;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,CAAC,MAAM,EAAE,YAAY,SAAS,WAAW,GAAG,WAAW,IAAI;IAClF,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC,YAAY,CAAC,CAAA;IACrD,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,QAAQ,CAAA;IAClB,QAAQ,CAAC,EAAE,YAAY,CAAA;IACvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,KAAK,IAAI,CAAA;IAE/F;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,EAAE,CAAC,CAAC,SAAS,OAAO,GAAG,WAAW,EAAE,GAAG,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,CAAA;IAEtE
|
|
1
|
+
{"version":3,"file":"render-options.d.ts","sourceRoot":"","sources":["../../src/models/render-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE1D;;;;;GAKG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,OAAO,GAAG,WAAW,IAAI;IACvD,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAA;CAC3B,CAAA;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,CAAC,MAAM,EAAE,YAAY,SAAS,WAAW,GAAG,WAAW,IAAI;IAClF,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC,YAAY,CAAC,CAAA;IACrD,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,QAAQ,CAAA;IAClB,QAAQ,CAAC,EAAE,YAAY,CAAA;IACvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACH,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,KAAK,IAAI,CAAA;IAE/F;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,EAAE,CAAC,CAAC,SAAS,OAAO,GAAG,WAAW,EAAE,GAAG,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,CAAA;IAEtE;;;;;;;;OAQG;IACH,aAAa,EAAE,CAAC,CAAC,SAAS,UAAU,GAAG,eAAe,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,EAAE,SAAS,OAAO,EAAE,KAAK,CAAC,CAAA;IAEtH;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,aAAa,EAAE,CAAC,CAAC,EACf,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,eAAe,CAAC,CAAC,CAAC,EAC9B,OAAO,CAAC,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAAG;QAAE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAA;KAAE,KACrE,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC,CAAA;IAEhD;;;;;OAKG;IACH,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC,CAAA;IAE1F;;;;;OAKG;IACH,cAAc,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC,CAAA;IAEhG;;;;;;OAMG;IACH,cAAc,EAAE,CAAC,CAAC,EAChB,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,EACf,WAAW,CAAC,EAAE,OAAO,KAClB,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAC,CAAA;CACjD,CAAA"}
|
package/esm/services/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAA;AACrC,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAA;AACrC,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA;AACnC,cAAc,iCAAiC,CAAA"}
|
package/esm/services/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAA;AACrC,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAA;AACrC,cAAc,0BAA0B,CAAA;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA;AACnC,cAAc,iCAAiC,CAAA"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type Injector } from '@furystack/inject';
|
|
2
|
+
import { ObservableValue } from '@furystack/utils';
|
|
3
|
+
/**
|
|
4
|
+
* Direction for spatial navigation movement.
|
|
5
|
+
*/
|
|
6
|
+
export type SpatialDirection = 'up' | 'down' | 'left' | 'right';
|
|
7
|
+
/**
|
|
8
|
+
* Configuration options for the SpatialNavigationService.
|
|
9
|
+
*/
|
|
10
|
+
export type SpatialNavigationOptions = {
|
|
11
|
+
/** Whether spatial navigation is enabled on startup. Default: true */
|
|
12
|
+
initiallyEnabled?: boolean;
|
|
13
|
+
/** Whether to allow cross-section navigation. Default: true */
|
|
14
|
+
crossSectionNavigation?: boolean;
|
|
15
|
+
/** Custom focusable selector override */
|
|
16
|
+
focusableSelector?: string;
|
|
17
|
+
/** Whether Backspace triggers history.back(). Default: false */
|
|
18
|
+
backspaceGoesBack?: boolean;
|
|
19
|
+
/** Whether Escape moves focus to parent section. Default: false */
|
|
20
|
+
escapeGoesToParentSection?: boolean;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Service for D-pad / arrow-key spatial navigation across interactive elements.
|
|
24
|
+
*
|
|
25
|
+
* Intercepts arrow key events and moves focus spatially based on element geometry.
|
|
26
|
+
* Supports section boundaries via `data-nav-section` attributes and optional
|
|
27
|
+
* cross-section navigation.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* // Opt in to spatial navigation
|
|
32
|
+
* const spatialNav = injector.getInstance(SpatialNavigationService)
|
|
33
|
+
*
|
|
34
|
+
* // Disable during video playback
|
|
35
|
+
* spatialNav.enabled.setValue(false)
|
|
36
|
+
*
|
|
37
|
+
* // Re-enable
|
|
38
|
+
* spatialNav.enabled.setValue(true)
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare class SpatialNavigationService implements Disposable {
|
|
42
|
+
/** Toggle spatial navigation on/off at runtime */
|
|
43
|
+
readonly enabled: ObservableValue<boolean>;
|
|
44
|
+
/** The currently active section name (from data-nav-section), or null if none */
|
|
45
|
+
readonly activeSection: ObservableValue<string | null>;
|
|
46
|
+
/** Remembered last-focused element per section for focus restoration */
|
|
47
|
+
private readonly focusMemory;
|
|
48
|
+
private readonly focusTrapStack;
|
|
49
|
+
private readonly focusableSelector;
|
|
50
|
+
private readonly crossSectionNavigation;
|
|
51
|
+
private readonly backspaceGoesBack;
|
|
52
|
+
private readonly escapeGoesToParentSection;
|
|
53
|
+
constructor(options?: SpatialNavigationOptions);
|
|
54
|
+
[Symbol.dispose](): void;
|
|
55
|
+
/**
|
|
56
|
+
* Push a focus trap onto the stack. While the trap is active, cross-section
|
|
57
|
+
* navigation is blocked and `activeSection` is locked to `sectionName`.
|
|
58
|
+
* Supports nesting — only the topmost trap is enforced.
|
|
59
|
+
*/
|
|
60
|
+
pushFocusTrap(sectionName: string): void;
|
|
61
|
+
/**
|
|
62
|
+
* Remove a focus trap from the stack. If other traps remain, the topmost
|
|
63
|
+
* one becomes active. Otherwise `activeSection` reverts to `previousSection`.
|
|
64
|
+
*/
|
|
65
|
+
popFocusTrap(sectionName: string, previousSection?: string | null): void;
|
|
66
|
+
private get activeTrap();
|
|
67
|
+
/** Programmatically move focus in a direction */
|
|
68
|
+
moveFocus(direction: SpatialDirection): void;
|
|
69
|
+
/** Programmatically activate (click) the currently focused element */
|
|
70
|
+
activateFocused(): void;
|
|
71
|
+
private handleKeyDown;
|
|
72
|
+
private focusFirstElement;
|
|
73
|
+
private findContainingSection;
|
|
74
|
+
private isVisibleInScrollContainers;
|
|
75
|
+
private getFocusableCandidates;
|
|
76
|
+
private findNearestInDirection;
|
|
77
|
+
private navigateCrossSection;
|
|
78
|
+
private storeFocusMemory;
|
|
79
|
+
private moveToParentSection;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Configures spatial navigation options before the service is first instantiated.
|
|
83
|
+
* Must be called **before** `SpatialNavigationService` is first resolved from the injector.
|
|
84
|
+
* @param injector The root injector
|
|
85
|
+
* @param options Configuration options for spatial navigation
|
|
86
|
+
*/
|
|
87
|
+
export declare const configureSpatialNavigation: (injector: Injector, options: SpatialNavigationOptions) => void;
|
|
88
|
+
//# sourceMappingURL=spatial-navigation-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spatial-navigation-service.d.ts","sourceRoot":"","sources":["../../src/services/spatial-navigation-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAElD;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AAE/D;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,+DAA+D;IAC/D,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,yCAAyC;IACzC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,gEAAgE;IAChE,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,mEAAmE;IACnE,yBAAyB,CAAC,EAAE,OAAO,CAAA;CACpC,CAAA;AAoND;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBACa,wBAAyB,YAAW,UAAU;IACzD,kDAAkD;IAClD,SAAgB,OAAO,EAAE,eAAe,CAAC,OAAO,CAAC,CAAA;IAEjD,iFAAiF;IACjF,SAAgB,aAAa,iCAA2C;IAExE,wEAAwE;IACxE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsC;IAElE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAe;IAE9C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAQ;IAC1C,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAS;IAChD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAS;gBAEvC,OAAO,GAAE,wBAA6B;IAU3C,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;IAQ/B;;;;OAIG;IACI,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAK/C;;;OAGG;IACI,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAS/E,OAAO,KAAK,UAAU,GAErB;IAED,iDAAiD;IAC1C,SAAS,CAAC,SAAS,EAAE,gBAAgB,GAAG,IAAI;IAuCnD,sEAAsE;IAC/D,eAAe,IAAI,IAAI;IAO9B,OAAO,CAAC,aAAa,CAmDpB;IAED,OAAO,CAAC,iBAAiB;IA8BzB,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,2BAA2B;IAuBnC,OAAO,CAAC,sBAAsB;IAe9B,OAAO,CAAC,sBAAsB;IAwB9B,OAAO,CAAC,oBAAoB;IA8C5B,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,mBAAmB;CAgB5B;AAED;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,GAAI,UAAU,QAAQ,EAAE,SAAS,wBAAwB,KAAG,IAOlG,CAAA"}
|