@roku-sdk/solid 0.0.0-stage → 2.18.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/src/focus.ts ADDED
@@ -0,0 +1,142 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /**
5
+ * SG focus requested through the `gainFocus` prop or a node's `focus()` / `blur()` methods.
6
+ *
7
+ * Only those two APIs feed this module. Focus moved any other way (BrightScript, firmware,
8
+ * a native component handling keys) is not seen here, so `focused` can be stale after it.
9
+ * An app that uses neither API never sets `focused`, and removals and commits pass through
10
+ * without sending any focus call.
11
+ *
12
+ * `FocusBoundary` (`@roku-sdk/navigation`) doesn't use either API. Its focus is virtual and
13
+ * lives in TS, while SG focus stays on the anchor node (a `FocusBoundaryBase`) the whole
14
+ * time, which routes every key to TS. An app whose focus is managed entirely by
15
+ * `FocusBoundary` never reaches this module. It matters when SG focus has to leave the anchor
16
+ * for a real node, e.g. a native component that handles its own keys, and the restore below
17
+ * is what hands focus back to the anchor afterwards.
18
+ */
19
+
20
+ import type { logger } from "@roku-sdk/common";
21
+ import { AnchorNode, AnNodeRef, DomNode, TreeManipulationApi } from "@roku-sdk/interfaces";
22
+
23
+ type Logger = ReturnType<typeof logger.getLogger>;
24
+
25
+ let tmApi: TreeManipulationApi;
26
+ let focusLogger: Logger;
27
+ // The placeholder anchor ref solid.ts passes on every tree call.
28
+ let NO_ANCHOR: AnNodeRef;
29
+
30
+ // The node the SDK last gave SG focus to. SceneGraph has one focused node per scene,
31
+ // so one slot covers every anchor node. This is the only focus state that persists across
32
+ // transactions and is shared by all anchor nodes; everything below is cleared in commitFocus.
33
+ let focused: DomNode | undefined;
34
+
35
+ // Local to the transaction.
36
+ let pending: DomNode | undefined;
37
+ let requestCount = 0;
38
+ // Set when the focused node is released or removed; preCommit moves SG focus here
39
+ // unless another node requests focus in the same transaction.
40
+ let restoreAnchor: AnchorNode | undefined;
41
+
42
+ /**
43
+ * The anchor node a node is rendered under: the root of its parent chain, which is the
44
+ * AnchorNode passed to `render`. `undefined` for a node not attached under an anchor.
45
+ */
46
+ function findOwningAnchor(node: DomNode | undefined): AnchorNode | undefined {
47
+ let current = node;
48
+ while (current?.parent) {
49
+ current = current.parent;
50
+ }
51
+ return current && current.t === undefined ? (current as AnchorNode) : undefined;
52
+ }
53
+
54
+ export function initFocus(api: TreeManipulationApi, log: Logger, noAnchor: AnNodeRef): void {
55
+ tmApi = api;
56
+ focusLogger = log;
57
+ NO_ANCHOR = noAnchor;
58
+ }
59
+
60
+ /** `gainFocus=true` and `focus()`. Applied in `commitFocus`; the last request wins. */
61
+ export function requestFocus(node: DomNode): void {
62
+ pending = node;
63
+ requestCount++;
64
+ }
65
+
66
+ /** `gainFocus=false` and `blur()`. */
67
+ export function releaseFocus(node: DomNode): void {
68
+ if (pending === node) {
69
+ pending = undefined;
70
+ requestCount = Math.max(0, requestCount - 1);
71
+ }
72
+ tmApi.setFocus(NO_ANCHOR, node.r, false);
73
+ if (focused === node) {
74
+ restoreAnchor = findOwningAnchor(node);
75
+ focused = undefined;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Called for every node of a removed subtree, before its parent pointer is cleared.
81
+ * `attachParent` is the parent the subtree was removed from.
82
+ */
83
+ export function focusNodeRemoved(node: DomNode, attachParent: DomNode | undefined): void {
84
+ if (node === focused) {
85
+ restoreAnchor = findOwningAnchor(attachParent);
86
+ focused = undefined;
87
+ }
88
+ if (node === pending) {
89
+ pending = undefined;
90
+ requestCount = Math.max(0, requestCount - 1);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Drops every reference into a destroyed anchor node's tree. Called once the anchor's render
96
+ * root is disposed, before the transaction commits: a cleanup that released focus during
97
+ * dispose must not re-anchor to the dead anchor, and `focused` must not keep its tree alive.
98
+ */
99
+ export function forgetAnchor(anchor: AnchorNode): void {
100
+ if (restoreAnchor === anchor) {
101
+ restoreAnchor = undefined;
102
+ }
103
+ if (focused && findOwningAnchor(focused) === anchor) {
104
+ focused = undefined;
105
+ }
106
+ if (pending && findOwningAnchor(pending) === anchor) {
107
+ pending = undefined;
108
+ requestCount = Math.max(0, requestCount - 1);
109
+ }
110
+ }
111
+
112
+ /** Sends the transaction's focus change. Runs in preCommit after removals and appends. */
113
+ export function commitFocus(): void {
114
+ if (requestCount > 1) {
115
+ const count = requestCount;
116
+ focusLogger.warn(
117
+ () => `Multiple nodes (${count}) requested focus in one transaction. Only last one will receive focus.`
118
+ );
119
+ }
120
+ const node = pending;
121
+ const anchor = restoreAnchor;
122
+ pending = undefined;
123
+ restoreAnchor = undefined;
124
+ requestCount = 0;
125
+
126
+ if (node) {
127
+ tmApi.setFocus(NO_ANCHOR, node.r, true);
128
+ focused = node;
129
+ focusLogger.trace(() => `setFocus: ${node.t}`);
130
+ } else if (anchor?.canReceiveFocus) {
131
+ tmApi.setFocus(NO_ANCHOR, anchor.r, true);
132
+ focusLogger.trace(() => `setFocus re-anchored to anchor node`);
133
+ }
134
+ }
135
+
136
+ // Invoked only from unit tests
137
+ export function resetFocus(): void {
138
+ focused = undefined;
139
+ pending = undefined;
140
+ restoreAnchor = undefined;
141
+ requestCount = 0;
142
+ }
@@ -0,0 +1,6 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+ globalThis.__DEV__ = true;
4
+ globalThis.__SIGNAL__ = true;
5
+ export * from "./index.ts";
6
+ export { createSignal, createMemo, createEffect } from "./dev-wrappers";
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ export * from "./solid";
5
+ export { registerImperativeMethods, setPropMethod } from "./dom-node";
6
+ export { forgetAnchor } from "./focus";
@@ -0,0 +1,5 @@
1
+ // Copyright (c) 2026 Roku, Inc.
2
+ // SPDX-License-Identifier: MIT
3
+ // Re-export solid-js JSX runtime for @roku-sdk/solid
4
+ // This allows TypeScript to use jsxImportSource: "@roku-sdk/solid"
5
+ export * from "solid-js/jsx-runtime";