@flighthq/interaction 0.1.0 → 0.2.1-next.410.a23f189
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/dist/cursorBackend.d.ts +12 -0
- package/dist/cursorBackend.d.ts.map +1 -0
- package/dist/cursorBackend.js +17 -0
- package/dist/cursorBackend.js.map +1 -0
- package/dist/displayHitTests.d.ts +10 -11
- package/dist/displayHitTests.d.ts.map +1 -1
- package/dist/displayHitTests.js +12 -14
- package/dist/displayHitTests.js.map +1 -1
- package/dist/enableInteractionGuards.d.ts +23 -0
- package/dist/enableInteractionGuards.d.ts.map +1 -0
- package/dist/enableInteractionGuards.js +75 -0
- package/dist/enableInteractionGuards.js.map +1 -0
- package/dist/focusManager.d.ts +46 -0
- package/dist/focusManager.d.ts.map +1 -0
- package/dist/focusManager.js +243 -0
- package/dist/focusManager.js.map +1 -0
- package/dist/hitTests.d.ts +45 -24
- package/dist/hitTests.d.ts.map +1 -1
- package/dist/hitTests.js +164 -47
- package/dist/hitTests.js.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/interactionManager.d.ts +10 -2
- package/dist/interactionManager.d.ts.map +1 -1
- package/dist/interactionManager.js +58 -6
- package/dist/interactionManager.js.map +1 -1
- package/dist/interactionSpatialIndex.d.ts +17 -0
- package/dist/interactionSpatialIndex.d.ts.map +1 -0
- package/dist/interactionSpatialIndex.js +77 -0
- package/dist/interactionSpatialIndex.js.map +1 -0
- package/dist/nodeInteractionState.d.ts +39 -0
- package/dist/nodeInteractionState.d.ts.map +1 -0
- package/dist/nodeInteractionState.js +72 -0
- package/dist/nodeInteractionState.js.map +1 -0
- package/dist/registerBitmapHitTest.d.ts +11 -0
- package/dist/registerBitmapHitTest.d.ts.map +1 -0
- package/dist/registerBitmapHitTest.js +55 -0
- package/dist/registerBitmapHitTest.js.map +1 -0
- package/dist/registerDefaultHitTests.d.ts +2 -0
- package/dist/registerDefaultHitTests.d.ts.map +1 -0
- package/dist/registerDefaultHitTests.js +20 -0
- package/dist/registerDefaultHitTests.js.map +1 -0
- package/dist/registerShapeHitTest.d.ts +10 -0
- package/dist/registerShapeHitTest.d.ts.map +1 -0
- package/dist/registerShapeHitTest.js +32 -0
- package/dist/registerShapeHitTest.js.map +1 -0
- package/dist/registerTextHitTest.d.ts +12 -0
- package/dist/registerTextHitTest.d.ts.map +1 -0
- package/dist/registerTextHitTest.js +32 -0
- package/dist/registerTextHitTest.js.map +1 -0
- package/dist/spriteHitTests.d.ts +3 -3
- package/dist/spriteHitTests.d.ts.map +1 -1
- package/dist/spriteHitTests.js +5 -5
- package/dist/spriteHitTests.js.map +1 -1
- package/package.json +18 -7
- package/src/cursorBackend.test.ts +24 -0
- package/src/displayHitTests.test.ts +39 -47
- package/src/enableInteractionGuards.test.ts +102 -0
- package/src/focusManager.test.ts +307 -0
- package/src/hitTests.test.ts +235 -98
- package/src/interactionManager.test.ts +59 -8
- package/src/interactionSpatialIndex.test.ts +103 -0
- package/src/nodeInteractionState.test.ts +141 -0
- package/src/registerBitmapHitTest.test.ts +22 -0
- package/src/{registerDefaultHitTestPoints.test.ts → registerDefaultHitTests.test.ts} +10 -7
- package/src/registerShapeHitTest.test.ts +27 -0
- package/src/registerTextHitTest.test.ts +54 -0
- package/src/spriteHitTests.test.ts +15 -15
- package/dist/registerDefaultHitTestPoints.d.ts +0 -2
- package/dist/registerDefaultHitTestPoints.d.ts.map +0 -1
- package/dist/registerDefaultHitTestPoints.js +0 -21
- package/dist/registerDefaultHitTestPoints.js.map +0 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CursorBackend } from '@flighthq/types';
|
|
2
|
+
/**
|
|
3
|
+
* Web cursor backend: applies the resolved rollover cursor to a DOM element's `style.cursor`
|
|
4
|
+
* (typically the render canvas). `setCursor(null)` clears back to the element's default. Pass the
|
|
5
|
+
* result to `createInteractionManager` via `cursorBackend`, or assign it to `manager.cursorBackend`.
|
|
6
|
+
*
|
|
7
|
+
* The backend is held per manager rather than globally, so multiple canvases each drive their own
|
|
8
|
+
* cursor zone. Import side-effect-free: this only constructs the backend; nothing is applied until
|
|
9
|
+
* pointer dispatch resolves a rollover.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createWebCursorBackend(element: HTMLElement): CursorBackend;
|
|
12
|
+
//# sourceMappingURL=cursorBackend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursorBackend.d.ts","sourceRoot":"","sources":["../src/cursorBackend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAE7D;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,aAAa,CAM1E"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web cursor backend: applies the resolved rollover cursor to a DOM element's `style.cursor`
|
|
3
|
+
* (typically the render canvas). `setCursor(null)` clears back to the element's default. Pass the
|
|
4
|
+
* result to `createInteractionManager` via `cursorBackend`, or assign it to `manager.cursorBackend`.
|
|
5
|
+
*
|
|
6
|
+
* The backend is held per manager rather than globally, so multiple canvases each drive their own
|
|
7
|
+
* cursor zone. Import side-effect-free: this only constructs the backend; nothing is applied until
|
|
8
|
+
* pointer dispatch resolves a rollover.
|
|
9
|
+
*/
|
|
10
|
+
export function createWebCursorBackend(element) {
|
|
11
|
+
return {
|
|
12
|
+
setCursor(cursor) {
|
|
13
|
+
element.style.cursor = cursor ?? '';
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=cursorBackend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursorBackend.js","sourceRoot":"","sources":["../src/cursorBackend.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAoB;IACzD,OAAO;QACL,SAAS,CAAC,MAAqB;YAC7B,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;QACtC,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import type { NodeAny } from '@flighthq/types';
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function
|
|
4
|
-
export declare function
|
|
5
|
-
export declare function
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function
|
|
8
|
-
export declare function
|
|
9
|
-
export declare function
|
|
10
|
-
export declare function
|
|
11
|
-
export declare function
|
|
12
|
-
export declare function defaultVideoHitTestPointHandler(source: NodeAny, x: number, y: number, _shapeFlag: boolean): boolean;
|
|
2
|
+
export declare function defaultBitmapHitTestHandler(source: NodeAny, x: number, y: number): boolean;
|
|
3
|
+
export declare function defaultDisplayObjectHitTestHandler(_source: NodeAny, _x: number, _y: number): boolean;
|
|
4
|
+
export declare function defaultHtmlViewHitTestHandler(_source: NodeAny, _x: number, _y: number): boolean;
|
|
5
|
+
export declare function defaultMovieClipHitTestHandler(_source: NodeAny, _x: number, _y: number): boolean;
|
|
6
|
+
export declare function defaultRenderViewHitTestHandler(source: NodeAny, x: number, y: number): boolean;
|
|
7
|
+
export declare function defaultRichTextHitTestHandler(source: NodeAny, x: number, y: number): boolean;
|
|
8
|
+
export declare function defaultShapeHitTestHandler(source: NodeAny, x: number, y: number): boolean;
|
|
9
|
+
export declare function defaultTextHitTestHandler(source: NodeAny, x: number, y: number): boolean;
|
|
10
|
+
export declare function defaultTextInputHitTestHandler(source: NodeAny, x: number, y: number): boolean;
|
|
11
|
+
export declare function defaultVideoHitTestHandler(source: NodeAny, x: number, y: number): boolean;
|
|
13
12
|
//# sourceMappingURL=displayHitTests.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"displayHitTests.d.ts","sourceRoot":"","sources":["../src/displayHitTests.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAI/C,wBAAgB,
|
|
1
|
+
{"version":3,"file":"displayHitTests.d.ts","sourceRoot":"","sources":["../src/displayHitTests.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAI/C,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAE1F;AAED,wBAAgB,kCAAkC,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAEpG;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAG/F;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAGhG;AAED,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAE9F;AAED,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAE5F;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAIzF;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAExF;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAE7F;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAEzF"}
|
package/dist/displayHitTests.js
CHANGED
|
@@ -1,38 +1,36 @@
|
|
|
1
1
|
import { hitTestGraphLocalBounds } from './hitTests';
|
|
2
|
-
export function
|
|
2
|
+
export function defaultBitmapHitTestHandler(source, x, y) {
|
|
3
3
|
return hitTestGraphLocalBounds(source, x, y);
|
|
4
4
|
}
|
|
5
|
-
export function
|
|
5
|
+
export function defaultDisplayObjectHitTestHandler(_source, _x, _y) {
|
|
6
6
|
return false;
|
|
7
7
|
}
|
|
8
|
-
export function
|
|
8
|
+
export function defaultHtmlViewHitTestHandler(_source, _x, _y) {
|
|
9
9
|
// HtmlView elements handle pointer events through the browser — not the canvas interaction system.
|
|
10
10
|
return false;
|
|
11
11
|
}
|
|
12
|
-
export function
|
|
12
|
+
export function defaultMovieClipHitTestHandler(_source, _x, _y) {
|
|
13
13
|
// Containers have no self hit area — findGraphHitTarget traverses children separately.
|
|
14
14
|
return false;
|
|
15
15
|
}
|
|
16
|
-
export function
|
|
16
|
+
export function defaultRenderViewHitTestHandler(source, x, y) {
|
|
17
17
|
return hitTestGraphLocalBounds(source, x, y);
|
|
18
18
|
}
|
|
19
|
-
export function
|
|
19
|
+
export function defaultRichTextHitTestHandler(source, x, y) {
|
|
20
20
|
return hitTestGraphLocalBounds(source, x, y);
|
|
21
21
|
}
|
|
22
|
-
export function
|
|
22
|
+
export function defaultShapeHitTestHandler(source, x, y) {
|
|
23
|
+
// Coarse bounds box. Shape-accurate winding is opt-in via `registerShapeHitTest` so the
|
|
24
|
+
// default registrar stays free of `@flighthq/shape` / `@flighthq/path`.
|
|
23
25
|
return hitTestGraphLocalBounds(source, x, y);
|
|
24
26
|
}
|
|
25
|
-
export function
|
|
26
|
-
// Containers have no self hit area — findGraphHitTarget traverses children separately.
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
export function defaultTextHitTestPointHandler(source, x, y, _shapeFlag) {
|
|
27
|
+
export function defaultTextHitTestHandler(source, x, y) {
|
|
30
28
|
return hitTestGraphLocalBounds(source, x, y);
|
|
31
29
|
}
|
|
32
|
-
export function
|
|
30
|
+
export function defaultTextInputHitTestHandler(source, x, y) {
|
|
33
31
|
return hitTestGraphLocalBounds(source, x, y);
|
|
34
32
|
}
|
|
35
|
-
export function
|
|
33
|
+
export function defaultVideoHitTestHandler(source, x, y) {
|
|
36
34
|
return hitTestGraphLocalBounds(source, x, y);
|
|
37
35
|
}
|
|
38
36
|
//# sourceMappingURL=displayHitTests.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"displayHitTests.js","sourceRoot":"","sources":["../src/displayHitTests.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAErD,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"displayHitTests.js","sourceRoot":"","sources":["../src/displayHitTests.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAErD,MAAM,UAAU,2BAA2B,CAAC,MAAe,EAAE,CAAS,EAAE,CAAS;IAC/E,OAAO,uBAAuB,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,kCAAkC,CAAC,OAAgB,EAAE,EAAU,EAAE,EAAU;IACzF,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,6BAA6B,CAAC,OAAgB,EAAE,EAAU,EAAE,EAAU;IACpF,mGAAmG;IACnG,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,8BAA8B,CAAC,OAAgB,EAAE,EAAU,EAAE,EAAU;IACrF,uFAAuF;IACvF,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,+BAA+B,CAAC,MAAe,EAAE,CAAS,EAAE,CAAS;IACnF,OAAO,uBAAuB,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,6BAA6B,CAAC,MAAe,EAAE,CAAS,EAAE,CAAS;IACjF,OAAO,uBAAuB,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,MAAe,EAAE,CAAS,EAAE,CAAS;IAC9E,wFAAwF;IACxF,wEAAwE;IACxE,OAAO,uBAAuB,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,MAAe,EAAE,CAAS,EAAE,CAAS;IAC7E,OAAO,uBAAuB,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,8BAA8B,CAAC,MAAe,EAAE,CAAS,EAAE,CAAS;IAClF,OAAO,uBAAuB,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,MAAe,EAAE,CAAS,EAAE,CAAS;IAC9E,OAAO,uBAAuB,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { NodeAny } from '@flighthq/types';
|
|
2
|
+
/** Plain-data result of `explainInteractionHitEligibility`. */
|
|
3
|
+
export interface InteractionHitEligibility {
|
|
4
|
+
eligible: boolean;
|
|
5
|
+
hasEligibleInSubtree: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Uninstalls the guard installed by `enableInteractionGuards`. */
|
|
8
|
+
export declare function disableInteractionGuards(): void;
|
|
9
|
+
/**
|
|
10
|
+
* Installs the caller-facing interaction guard (opt-in, dev-only). It warns once — through
|
|
11
|
+
* `@flighthq/log` — when a pointer listener is connected to a node whose subtree contains no
|
|
12
|
+
* hit-testable node, i.e. the "I added a click handler but nothing fires" footgun under opt-in
|
|
13
|
+
* eligibility. Because hit testing is opt-in, this catches forgetting `setNodeHitTestEnabled`.
|
|
14
|
+
*
|
|
15
|
+
* Assumes opt-in precedes the connect (the recommended order); a node opted in *after* connecting a
|
|
16
|
+
* listener would warn spuriously. Not importing this module costs production nothing — the message and
|
|
17
|
+
* the `@flighthq/log` dependency live only here.
|
|
18
|
+
*/
|
|
19
|
+
export declare function enableInteractionGuards(): void;
|
|
20
|
+
/** Reports whether a node is a hit candidate and whether any node in its subtree is — the seam a
|
|
21
|
+
* developer queries to understand why events do or do not reach a node. */
|
|
22
|
+
export declare function explainInteractionHitEligibility(node: Readonly<NodeAny>): InteractionHitEligibility;
|
|
23
|
+
//# sourceMappingURL=enableInteractionGuards.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enableInteractionGuards.d.ts","sourceRoot":"","sources":["../src/enableInteractionGuards.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAyB,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAMtE,+DAA+D;AAC/D,MAAM,WAAW,yBAAyB;IAExC,QAAQ,EAAE,OAAO,CAAC;IAElB,oBAAoB,EAAE,OAAO,CAAC;CAC/B;AAED,mEAAmE;AACnE,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAED;2EAC2E;AAC3E,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,yBAAyB,CAKnG"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { logOnce } from '@flighthq/log';
|
|
2
|
+
import { getNodeRuntime } from '@flighthq/node';
|
|
3
|
+
import { LogLevel } from '@flighthq/types';
|
|
4
|
+
import { setInteractionConnectGuard } from './interactionManager';
|
|
5
|
+
import { isNodeFocusable, isNodeHitTestEnabled } from './nodeInteractionState';
|
|
6
|
+
/** Uninstalls the guard installed by `enableInteractionGuards`. */
|
|
7
|
+
export function disableInteractionGuards() {
|
|
8
|
+
setInteractionConnectGuard(null);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Installs the caller-facing interaction guard (opt-in, dev-only). It warns once — through
|
|
12
|
+
* `@flighthq/log` — when a pointer listener is connected to a node whose subtree contains no
|
|
13
|
+
* hit-testable node, i.e. the "I added a click handler but nothing fires" footgun under opt-in
|
|
14
|
+
* eligibility. Because hit testing is opt-in, this catches forgetting `setNodeHitTestEnabled`.
|
|
15
|
+
*
|
|
16
|
+
* Assumes opt-in precedes the connect (the recommended order); a node opted in *after* connecting a
|
|
17
|
+
* listener would warn spuriously. Not importing this module costs production nothing — the message and
|
|
18
|
+
* the `@flighthq/log` dependency live only here.
|
|
19
|
+
*/
|
|
20
|
+
export function enableInteractionGuards() {
|
|
21
|
+
setInteractionConnectGuard(warnOnInertInteractionTarget);
|
|
22
|
+
}
|
|
23
|
+
/** Reports whether a node is a hit candidate and whether any node in its subtree is — the seam a
|
|
24
|
+
* developer queries to understand why events do or do not reach a node. */
|
|
25
|
+
export function explainInteractionHitEligibility(node) {
|
|
26
|
+
return {
|
|
27
|
+
eligible: isNodeHitTestEnabled(node),
|
|
28
|
+
hasEligibleInSubtree: hasEligibleNodeInSubtree(node),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function hasEligibleNodeInSubtree(node) {
|
|
32
|
+
if (isNodeHitTestEnabled(node))
|
|
33
|
+
return true;
|
|
34
|
+
const children = getNodeRuntime(node).children;
|
|
35
|
+
if (children !== null) {
|
|
36
|
+
for (const child of children) {
|
|
37
|
+
if (hasEligibleNodeInSubtree(child))
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function hasFocusableNodeInSubtree(node) {
|
|
44
|
+
if (isNodeFocusable(node))
|
|
45
|
+
return true;
|
|
46
|
+
const children = getNodeRuntime(node).children;
|
|
47
|
+
if (children !== null) {
|
|
48
|
+
for (const child of children) {
|
|
49
|
+
if (hasFocusableNodeInSubtree(child))
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
function warnOnInertInteractionTarget(target, name) {
|
|
56
|
+
// Keyboard signals dispatch to the manager root regardless of eligibility, so they are exempt.
|
|
57
|
+
if (name === 'onKeyDown' || name === 'onKeyUp')
|
|
58
|
+
return;
|
|
59
|
+
// Focus signals dispatch from the focus manager based on `focusable`, not hit eligibility, so they get
|
|
60
|
+
// the symmetric check: warn when nothing in the subtree is a focus stop (forgot setNodeFocusable).
|
|
61
|
+
if (name === 'onFocusIn' || name === 'onFocusOut') {
|
|
62
|
+
if (hasFocusableNodeInSubtree(target))
|
|
63
|
+
return;
|
|
64
|
+
logOnce('interaction:focus-listener-on-non-focusable', LogLevel.Warn, {
|
|
65
|
+
message: `connectInteractionSignal('${name}'): target has no focusable node in its subtree — call setNodeFocusable(node, true) so it can receive focus events`,
|
|
66
|
+
}, 'interaction');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (hasEligibleNodeInSubtree(target))
|
|
70
|
+
return;
|
|
71
|
+
logOnce('interaction:listener-on-inert-node', LogLevel.Warn, {
|
|
72
|
+
message: `connectInteractionSignal('${name}'): target has no hit-testable node in its subtree — call setNodeHitTestEnabled(node, true) so it can receive pointer events`,
|
|
73
|
+
}, 'interaction');
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=enableInteractionGuards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enableInteractionGuards.js","sourceRoot":"","sources":["../src/enableInteractionGuards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE3C,OAAO,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAU/E,mEAAmE;AACnE,MAAM,UAAU,wBAAwB;IACtC,0BAA0B,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB;IACrC,0BAA0B,CAAC,4BAA4B,CAAC,CAAC;AAC3D,CAAC;AAED;2EAC2E;AAC3E,MAAM,UAAU,gCAAgC,CAAC,IAAuB;IACtE,OAAO;QACL,QAAQ,EAAE,oBAAoB,CAAC,IAAI,CAAC;QACpC,oBAAoB,EAAE,wBAAwB,CAAC,IAAI,CAAC;KACrD,CAAC;AACJ,CAAC;AAED,SAAS,wBAAwB,CAAC,IAAuB;IACvD,IAAI,oBAAoB,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC;IAC/C,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,wBAAwB,CAAC,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;QACnD,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,yBAAyB,CAAC,IAAuB;IACxD,IAAI,eAAe,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC;IAC/C,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,yBAAyB,CAAC,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;QACpD,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,4BAA4B,CAAC,MAAe,EAAE,IAA2B;IAChF,+FAA+F;IAC/F,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO;IAEvD,uGAAuG;IACvG,mGAAmG;IACnG,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,YAAY,EAAE,CAAC;QAClD,IAAI,yBAAyB,CAAC,MAAM,CAAC;YAAE,OAAO;QAC9C,OAAO,CACL,6CAA6C,EAC7C,QAAQ,CAAC,IAAI,EACb;YACE,OAAO,EAAE,6BAA6B,IAAI,oHAAoH;SAC/J,EACD,aAAa,CACd,CAAC;QACF,OAAO;IACT,CAAC;IAED,IAAI,wBAAwB,CAAC,MAAM,CAAC;QAAE,OAAO;IAC7C,OAAO,CACL,oCAAoC,EACpC,QAAQ,CAAC,IAAI,EACb;QACE,OAAO,EAAE,6BAA6B,IAAI,8HAA8H;KACzK,EACD,aAAa,CACd,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { FocusDirection, FocusManager, FocusManagerOptions, FocusNavigationInput, FocusNavigationOptions, NodeAny } from '@flighthq/types';
|
|
2
|
+
/** Clears focus: the previously focused node (if any) receives `onFocusOut` and the manager holds none. */
|
|
3
|
+
export declare function clearFocus<N extends NodeAny>(manager: FocusManager<N>): void;
|
|
4
|
+
/**
|
|
5
|
+
* Wires a keyboard source into a focus manager: Tab advances focus, Shift+Tab retreats, and — when
|
|
6
|
+
* `options.arrowKeys` is set — the arrow keys drive directional (spatial) navigation. Returns a disconnect
|
|
7
|
+
* function. The manager owns focus state; this only translates keys into navigation calls, so an app can
|
|
8
|
+
* equally drive `focusNextNode` / `focusNodeInDirection` from its own input handling instead.
|
|
9
|
+
**/
|
|
10
|
+
export declare function connectFocusNavigation<N extends NodeAny>(input: FocusNavigationInput, manager: FocusManager<N>, options?: Readonly<FocusNavigationOptions>): () => void;
|
|
11
|
+
/** Allocates a focus manager for `root`. `wrap` (default `true`) cycles tab navigation past the ends. */
|
|
12
|
+
export declare function createFocusManager<N extends NodeAny>(root: N, options?: Readonly<FocusManagerOptions>): FocusManager<N>;
|
|
13
|
+
/**
|
|
14
|
+
* Moves focus to the next stop in tab order and returns it, or `null` if there is nowhere to go (no
|
|
15
|
+
* focusable nodes, or already at the last stop with `wrap` off). With nothing focused, focuses the first.
|
|
16
|
+
**/
|
|
17
|
+
export declare function focusNextNode<N extends NodeAny>(manager: FocusManager<N>): N | null;
|
|
18
|
+
/**
|
|
19
|
+
* Moves focus to the nearest focus stop in `direction` from the focused node's world-bounds center and
|
|
20
|
+
* returns it, or `null` when nothing is focused yet or no stop lies that way. The D-pad / analog-stick
|
|
21
|
+
* navigation model: candidates are scored by distance along the axis plus a perpendicular-offset penalty,
|
|
22
|
+
* so the most directly-aligned stop wins.
|
|
23
|
+
**/
|
|
24
|
+
export declare function focusNodeInDirection<N extends NodeAny>(manager: FocusManager<N>, direction: FocusDirection): N | null;
|
|
25
|
+
/** Moves focus to the previous stop in tab order; the reverse of `focusNextNode`. */
|
|
26
|
+
export declare function focusPreviousNode<N extends NodeAny>(manager: FocusManager<N>): N | null;
|
|
27
|
+
/** The node currently holding focus, or `null`. */
|
|
28
|
+
export declare function getFocusedNode<N extends NodeAny>(manager: FocusManager<N>): N | null;
|
|
29
|
+
/**
|
|
30
|
+
* Fills `out` (cleared first) with the manager's focus stops in tab order and returns it: every focusable,
|
|
31
|
+
* enabled node in the subtree, with explicit `tabIndex >= 0` stops sorted ascending ahead of the natural
|
|
32
|
+
* tree-order stops (`tabIndex -1`). A disabled node and its whole subtree are skipped. This is the engine
|
|
33
|
+
* primitive the navigation functions walk; call it directly to render a focus ring or inspect tab order.
|
|
34
|
+
**/
|
|
35
|
+
export declare function getFocusOrder<N extends NodeAny>(manager: FocusManager<N>, out?: N[]): N[];
|
|
36
|
+
/** Whether `node` is the manager's currently focused node. */
|
|
37
|
+
export declare function isNodeFocused<N extends NodeAny>(manager: FocusManager<N>, node: N): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Focuses a specific node (or `null` to clear focus), firing `onFocusOut` on the previously focused node
|
|
40
|
+
* and `onFocusIn` on the new one — both bubbling up to `root`, with `relatedTarget` set to the node on the
|
|
41
|
+
* other side of the change. Returns `false` without changing anything if `node` is non-null but not
|
|
42
|
+
* focusable (`setNodeFocusable` was never called); `true` otherwise, including a no-op refocus of the same
|
|
43
|
+
* node. `node` is assumed to live in the managed subtree; ancestry is not validated.
|
|
44
|
+
**/
|
|
45
|
+
export declare function setFocusedNode<N extends NodeAny>(manager: FocusManager<N>, node: N | null): boolean;
|
|
46
|
+
//# sourceMappingURL=focusManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focusManager.d.ts","sourceRoot":"","sources":["../src/focusManager.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,cAAc,EAEd,YAAY,EACZ,mBAAmB,EACnB,oBAAoB,EACpB,sBAAsB,EAEtB,OAAO,EAGR,MAAM,iBAAiB,CAAC;AAKzB,2GAA2G;AAC3G,wBAAgB,UAAU,CAAC,CAAC,SAAS,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,IAAI,CAE5E;AAED;;;;;IAKI;AACJ,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,OAAO,EACtD,KAAK,EAAE,oBAAoB,EAC3B,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,EACxB,OAAO,GAAE,QAAQ,CAAC,sBAAsB,CAAM,GAC7C,MAAM,IAAI,CAcZ;AAED,yGAAyG;AACzG,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,OAAO,EAClD,IAAI,EAAE,CAAC,EACP,OAAO,GAAE,QAAQ,CAAC,mBAAmB,CAAM,GAC1C,YAAY,CAAC,CAAC,CAAC,CAMjB;AAED;;;IAGI;AACJ,wBAAgB,aAAa,CAAC,CAAC,SAAS,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,IAAI,CAEnF;AAED;;;;;IAKI;AACJ,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,cAAc,GAAG,CAAC,GAAG,IAAI,CAqBrH;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,IAAI,CAEvF;AAED,mDAAmD;AACnD,wBAAgB,cAAc,CAAC,CAAC,SAAS,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,IAAI,CAEpF;AAED;;;;;IAKI;AACJ,wBAAgB,aAAa,CAAC,CAAC,SAAS,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,GAAG,GAAE,CAAC,EAAO,GAAG,CAAC,EAAE,CAK7F;AAED,8DAA8D;AAC9D,wBAAgB,aAAa,CAAC,CAAC,SAAS,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,OAAO,CAE3F;AAED;;;;;;IAMI;AACJ,wBAAgB,cAAc,CAAC,CAAC,SAAS,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI,GAAG,OAAO,CASnG"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { getNodeParent, getNodeRuntime, getNodeWorldBoundsRectangle } from '@flighthq/node';
|
|
2
|
+
import { connectSignal, disconnectSignal, emitSignal } from '@flighthq/signals';
|
|
3
|
+
import { getInteractionSignals } from './interactionManager';
|
|
4
|
+
import { getNodeTabIndex, isNodeFocusable } from './nodeInteractionState';
|
|
5
|
+
/** Clears focus: the previously focused node (if any) receives `onFocusOut` and the manager holds none. */
|
|
6
|
+
export function clearFocus(manager) {
|
|
7
|
+
setFocusedNode(manager, null);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Wires a keyboard source into a focus manager: Tab advances focus, Shift+Tab retreats, and — when
|
|
11
|
+
* `options.arrowKeys` is set — the arrow keys drive directional (spatial) navigation. Returns a disconnect
|
|
12
|
+
* function. The manager owns focus state; this only translates keys into navigation calls, so an app can
|
|
13
|
+
* equally drive `focusNextNode` / `focusNodeInDirection` from its own input handling instead.
|
|
14
|
+
**/
|
|
15
|
+
export function connectFocusNavigation(input, manager, options = {}) {
|
|
16
|
+
const arrowKeys = options.arrowKeys ?? false;
|
|
17
|
+
const onKeyDown = (data) => {
|
|
18
|
+
if (data.key === 'Tab') {
|
|
19
|
+
if (data.shiftKey)
|
|
20
|
+
focusPreviousNode(manager);
|
|
21
|
+
else
|
|
22
|
+
focusNextNode(manager);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!arrowKeys)
|
|
26
|
+
return;
|
|
27
|
+
const direction = arrowKeyDirection(data.key);
|
|
28
|
+
if (direction !== null)
|
|
29
|
+
focusNodeInDirection(manager, direction);
|
|
30
|
+
};
|
|
31
|
+
connectSignal(input.onKeyDown, onKeyDown);
|
|
32
|
+
return () => disconnectSignal(input.onKeyDown, onKeyDown);
|
|
33
|
+
}
|
|
34
|
+
/** Allocates a focus manager for `root`. `wrap` (default `true`) cycles tab navigation past the ends. */
|
|
35
|
+
export function createFocusManager(root, options = {}) {
|
|
36
|
+
return {
|
|
37
|
+
focused: null,
|
|
38
|
+
root,
|
|
39
|
+
wrap: options.wrap ?? true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Moves focus to the next stop in tab order and returns it, or `null` if there is nowhere to go (no
|
|
44
|
+
* focusable nodes, or already at the last stop with `wrap` off). With nothing focused, focuses the first.
|
|
45
|
+
**/
|
|
46
|
+
export function focusNextNode(manager) {
|
|
47
|
+
return stepFocus(manager, 1);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Moves focus to the nearest focus stop in `direction` from the focused node's world-bounds center and
|
|
51
|
+
* returns it, or `null` when nothing is focused yet or no stop lies that way. The D-pad / analog-stick
|
|
52
|
+
* navigation model: candidates are scored by distance along the axis plus a perpendicular-offset penalty,
|
|
53
|
+
* so the most directly-aligned stop wins.
|
|
54
|
+
**/
|
|
55
|
+
export function focusNodeInDirection(manager, direction) {
|
|
56
|
+
const current = manager.focused;
|
|
57
|
+
if (current === null)
|
|
58
|
+
return null;
|
|
59
|
+
const order = getFocusOrder(manager, _orderScratch);
|
|
60
|
+
const origin = boundsCenter(current, _originCenter);
|
|
61
|
+
const originX = origin.x;
|
|
62
|
+
const originY = origin.y;
|
|
63
|
+
let best = null;
|
|
64
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
65
|
+
for (const candidate of order) {
|
|
66
|
+
if (candidate === current)
|
|
67
|
+
continue;
|
|
68
|
+
const center = boundsCenter(candidate, _candidateCenter);
|
|
69
|
+
const score = directionScore(direction, originX, originY, center.x, center.y);
|
|
70
|
+
if (score < bestScore) {
|
|
71
|
+
bestScore = score;
|
|
72
|
+
best = candidate;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (best !== null)
|
|
76
|
+
setFocusedNode(manager, best);
|
|
77
|
+
return best;
|
|
78
|
+
}
|
|
79
|
+
/** Moves focus to the previous stop in tab order; the reverse of `focusNextNode`. */
|
|
80
|
+
export function focusPreviousNode(manager) {
|
|
81
|
+
return stepFocus(manager, -1);
|
|
82
|
+
}
|
|
83
|
+
/** The node currently holding focus, or `null`. */
|
|
84
|
+
export function getFocusedNode(manager) {
|
|
85
|
+
return manager.focused;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Fills `out` (cleared first) with the manager's focus stops in tab order and returns it: every focusable,
|
|
89
|
+
* enabled node in the subtree, with explicit `tabIndex >= 0` stops sorted ascending ahead of the natural
|
|
90
|
+
* tree-order stops (`tabIndex -1`). A disabled node and its whole subtree are skipped. This is the engine
|
|
91
|
+
* primitive the navigation functions walk; call it directly to render a focus ring or inspect tab order.
|
|
92
|
+
**/
|
|
93
|
+
export function getFocusOrder(manager, out = []) {
|
|
94
|
+
out.length = 0;
|
|
95
|
+
collectFocusStops(manager.root, out);
|
|
96
|
+
out.sort(byTabIndexOrder);
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
/** Whether `node` is the manager's currently focused node. */
|
|
100
|
+
export function isNodeFocused(manager, node) {
|
|
101
|
+
return manager.focused === node;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Focuses a specific node (or `null` to clear focus), firing `onFocusOut` on the previously focused node
|
|
105
|
+
* and `onFocusIn` on the new one — both bubbling up to `root`, with `relatedTarget` set to the node on the
|
|
106
|
+
* other side of the change. Returns `false` without changing anything if `node` is non-null but not
|
|
107
|
+
* focusable (`setNodeFocusable` was never called); `true` otherwise, including a no-op refocus of the same
|
|
108
|
+
* node. `node` is assumed to live in the managed subtree; ancestry is not validated.
|
|
109
|
+
**/
|
|
110
|
+
export function setFocusedNode(manager, node) {
|
|
111
|
+
if (node !== null && !isNodeFocusable(node))
|
|
112
|
+
return false;
|
|
113
|
+
const previous = manager.focused;
|
|
114
|
+
if (node === previous)
|
|
115
|
+
return true;
|
|
116
|
+
manager.focused = node;
|
|
117
|
+
if (previous !== null)
|
|
118
|
+
emitFocusSignal(previous, manager.root, 'onFocusOut', node);
|
|
119
|
+
if (node !== null)
|
|
120
|
+
emitFocusSignal(node, manager.root, 'onFocusIn', previous);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
function arrowKeyDirection(key) {
|
|
124
|
+
switch (key) {
|
|
125
|
+
case 'ArrowDown':
|
|
126
|
+
return 'down';
|
|
127
|
+
case 'ArrowLeft':
|
|
128
|
+
return 'left';
|
|
129
|
+
case 'ArrowRight':
|
|
130
|
+
return 'right';
|
|
131
|
+
case 'ArrowUp':
|
|
132
|
+
return 'up';
|
|
133
|
+
default:
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Writes the world-bounds center of a node into `out` and returns it. Directional navigation compares
|
|
138
|
+
// these centers; the node is treated as a 2D spatial node (all display objects are).
|
|
139
|
+
function boundsCenter(node, out) {
|
|
140
|
+
const bounds = getNodeWorldBoundsRectangle(node);
|
|
141
|
+
out.x = bounds.x + bounds.width / 2;
|
|
142
|
+
out.y = bounds.y + bounds.height / 2;
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
// Stable-sort comparator: explicit `tabIndex >= 0` stops ascending, then natural (`-1`) stops. A stable
|
|
146
|
+
// sort keeps the DFS (tree) order among equal keys, so natural stops stay in document order.
|
|
147
|
+
function byTabIndexOrder(a, b) {
|
|
148
|
+
return focusOrderKey(a) - focusOrderKey(b);
|
|
149
|
+
}
|
|
150
|
+
function collectFocusStops(node, out) {
|
|
151
|
+
if (!node.enabled)
|
|
152
|
+
return;
|
|
153
|
+
if (isNodeFocusable(node))
|
|
154
|
+
out.push(node);
|
|
155
|
+
const children = getNodeRuntime(node).children;
|
|
156
|
+
if (children !== null) {
|
|
157
|
+
for (const child of children)
|
|
158
|
+
collectFocusStops(child, out);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Scores a candidate center against the origin for `direction`: distance along the axis plus twice the
|
|
162
|
+
// perpendicular offset (so aligned stops beat far-off-axis ones). Returns Infinity for anything not on the
|
|
163
|
+
// requested side, excluding it.
|
|
164
|
+
function directionScore(direction, ox, oy, cx, cy) {
|
|
165
|
+
const dx = cx - ox;
|
|
166
|
+
const dy = cy - oy;
|
|
167
|
+
let along;
|
|
168
|
+
let perpendicular;
|
|
169
|
+
switch (direction) {
|
|
170
|
+
case 'right':
|
|
171
|
+
along = dx;
|
|
172
|
+
perpendicular = dy;
|
|
173
|
+
break;
|
|
174
|
+
case 'left':
|
|
175
|
+
along = -dx;
|
|
176
|
+
perpendicular = dy;
|
|
177
|
+
break;
|
|
178
|
+
case 'down':
|
|
179
|
+
along = dy;
|
|
180
|
+
perpendicular = dx;
|
|
181
|
+
break;
|
|
182
|
+
default:
|
|
183
|
+
along = -dy;
|
|
184
|
+
perpendicular = dx;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
if (along <= DIRECTION_EPSILON)
|
|
188
|
+
return Number.POSITIVE_INFINITY;
|
|
189
|
+
return along + 2 * Math.abs(perpendicular);
|
|
190
|
+
}
|
|
191
|
+
// Bubbles a focus signal from `target` up to `root`, reusing the shared `_focusData` payload. Focus
|
|
192
|
+
// signals bubble (like pointer signals) but are not cancelable — there is no meaningful "prevent" for a
|
|
193
|
+
// focus change that has already happened.
|
|
194
|
+
function emitFocusSignal(target, root, name, relatedTarget) {
|
|
195
|
+
_focusData.relatedTarget = relatedTarget;
|
|
196
|
+
_focusData.target = target;
|
|
197
|
+
let current = target;
|
|
198
|
+
while (current !== null) {
|
|
199
|
+
_focusData.currentTarget = current;
|
|
200
|
+
const signals = getInteractionSignals(current);
|
|
201
|
+
const signal = signals !== null ? signals[name] : null;
|
|
202
|
+
if (signal !== null)
|
|
203
|
+
emitSignal(signal, _focusData);
|
|
204
|
+
if (current === root)
|
|
205
|
+
break;
|
|
206
|
+
current = getNodeParent(current);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function focusOrderKey(node) {
|
|
210
|
+
const tabIndex = getNodeTabIndex(node);
|
|
211
|
+
return tabIndex < 0 ? Number.POSITIVE_INFINITY : tabIndex;
|
|
212
|
+
}
|
|
213
|
+
// Advances focus by `delta` (+1 next, -1 previous) through the current focus order, wrapping when the
|
|
214
|
+
// manager allows it. Captures the target before emitting, so a slot re-entering navigation cannot corrupt
|
|
215
|
+
// the returned value. Returns the newly focused node, or `null` when there is nowhere to move.
|
|
216
|
+
function stepFocus(manager, delta) {
|
|
217
|
+
const order = getFocusOrder(manager, _orderScratch);
|
|
218
|
+
if (order.length === 0)
|
|
219
|
+
return null;
|
|
220
|
+
const current = manager.focused;
|
|
221
|
+
const from = current !== null ? order.indexOf(current) : -1;
|
|
222
|
+
let index;
|
|
223
|
+
if (from === -1) {
|
|
224
|
+
index = delta > 0 ? 0 : order.length - 1;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
index = from + delta;
|
|
228
|
+
if (index < 0 || index >= order.length) {
|
|
229
|
+
if (!manager.wrap)
|
|
230
|
+
return null;
|
|
231
|
+
index = (index + order.length) % order.length;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const next = order[index];
|
|
235
|
+
setFocusedNode(manager, next);
|
|
236
|
+
return next;
|
|
237
|
+
}
|
|
238
|
+
const DIRECTION_EPSILON = 0.0001;
|
|
239
|
+
const _candidateCenter = { x: 0, y: 0 };
|
|
240
|
+
const _focusData = { currentTarget: null, relatedTarget: null, target: null };
|
|
241
|
+
const _orderScratch = [];
|
|
242
|
+
const _originCenter = { x: 0, y: 0 };
|
|
243
|
+
//# sourceMappingURL=focusManager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focusManager.js","sourceRoot":"","sources":["../src/focusManager.ts"],"names":[],"mappings":"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"}
|