Navigation types
Types shared by several @roku-sdk/navigation exports, or used by none of them directly
import type {
ConditionalKeyHandler,
FocusId,
FocusPathContextValue,
IFocusBoundaryBaseProps,
KeyHandlerEntry,
KeyHandlerMap,
KeyPressCaptureRegistry,
KeyPressHandler,
KeyPressHandlerMap,
KeyResult,
NavigationMap,
} from "@roku-sdk/navigation";
import type {
FocusRelay,
FocusRelayTarget,
} from "@roku-sdk/navigation/focus-relay";Each is documented here once and linked from every page that uses it.
Types
ConditionalKeyHandler
A key handler with a reactive gate. The when accessor determines whether this key is currently claimed (included in handledKeys). When when() returns false, BrightScript won't claim the key and it bubbles through SG.
| Name | Type | Description |
|---|---|---|
handle | KeyPressHandler | Called on key-down and key-up while when() returns true. |
when | Accessor<boolean> | Whether the key is currently claimed. While it returns false, the key is left out of handledKeys and handle is not called. |
FocusId
A virtual focus identity, optionally carrying a human-readable debug label.
Focus identity is reference identity: the boundary keys its registry on the object itself, so two distinct objects are two distinct focusables no matter what they contain. id is purely diagnostic — it is never used for lookup, equality, or navigation, and two focusables may legitimately share the same id.
The object & intersection is load-bearing. { id?: string } on its own is a TypeScript weak type: assigning a value with no properties in common produces error TS2559, which would reject every existing caller that passes an arbitrary object (e.g. a component's own state object) as its identity. Intersecting with object keeps the type as permissive as the object it replaces, so this is a source-compatible widening rather than a breaking change.
Example
// Unlabelled — still perfectly valid, shows as "(unlabelled)" in debug logs.
const itemId: FocusId = {};
// Labelled — the label appears in `handleKeyEvent` debug output.
const itemId: FocusId = { id: "hero-row-item-0" };type FocusId = object & { id: string };FocusPathContextValue
Deprecated.
Use IFocusPathContext. Removed in the next major release.
Former name of IFocusPathContext.
type FocusPathContextValue = IFocusPathContext;FocusRelay
A stable focus parent for a subtree whose displaying FocusBoundary can change.
Focusables and nested boundaries inside a FocusRelayProvider register with the relay. The relay forwards those registrations, focus queries and keys to whichever boundary it is bound to, and moves the registrations when it is rebound.
FocusRelay.bind
Moves the relay, and every registration made through it, to another boundary.
Binding to the current target, or to a target whose boundary has unmounted, does nothing. Every target a relay is bound to must sit under the same RootFocusBoundary.
FocusRelay.unbind
Detaches the relay from its current boundary and removes its registrations there.
Call it when the subtree the relay serves is disposed. The relay forwards nothing until it is bound again.
FocusRelayTarget
An opaque handle to a FocusBoundary that focus relays can bind to.
Obtain one with useFocusRelayTarget and pass it to createFocusRelay or FocusRelay.bind. The handle stays valid for the lifetime of its boundary; binding to it after the boundary has unmounted does nothing.
| Name | Type | Description |
|---|---|---|
[focusRelayTargetBrand] | true | Type-only brand that keeps the handle opaque. It has no runtime value; never read or set it. |
IFocusBoundaryBaseProps
Props for the FocusBoundaryBase bridge component.
This mirrors the <interface> of src/focus-boundary/xml-components/focus-boundary-base/FocusBoundaryBaseShim.xml, and is checked in rather than re-exported from the generated FocusBoundaryBaseShim.xml.d.ts. That declaration is a build artifact — .gitignore keeps *.xml.d.ts out of git and only rk build writes it — so a plain tsc run (the consumer typecheck, a registry consumer) resolves the .xml import through the *.xml wildcard in xml.d.ts, which exports a default only. Naming the interface here gives it a declaration that does not depend on build order.
Keep in sync with the XML interface. The componentMapping entry for FocusBoundaryBaseShim and FocusBoundaryBase in @roku-sdk/tooling name this interface. XML components on existing firmware must extend FocusBoundaryBaseShim; TypeScript descriptors retain FocusBoundaryBase and select their base at registration.
| Name | Type | Description |
|---|---|---|
focusActive? | boolean | Written by BrightScript when SceneGraph focus enters or leaves this node's subtree. |
focusPath? | string | Focus path as "/"-separated indices from the root boundary to the focused leaf (e.g. "1/0/2"). Written by TypeScript when virtual focus changes. |
handleAllKeys? | boolean | When true, BrightScript claims and forwards every key event without consulting handledKeys. Mirrors KeyHandler.handleAllKeys. |
handledKeys? | Record<string, unknown> | Written by TypeScript (FocusBoundary) to declare which keys the virtual focus tree currently handles. Record<keyName, boolean>; empty claims all keys. |
keyEvent? | Record<string, unknown> | Written by BrightScript on every key event. TypeScript observes this reactively via the anchor node's keyEvent input field. |
Extends IGroupPropsCompat.
KeyHandlerEntry
A single key handler entry: either a plain handler (always active) or a conditional handler with a reactive when gate.
type KeyHandlerEntry = KeyPressHandler | ConditionalKeyHandler;KeyHandlerMap
Declarative key handler map. Keys are remote key names (e.g. "OK", "back"). Values are either plain handlers or conditional handlers.
The set of keys declared here is used to:
- Derive
handledKeysfor BrightScript (which keys to claim from SG) - Route key events directly to the appropriate handler (no if-chain needed)
Directional keys (up, down, left, right) are owned by the boundary's navigation system and should NOT be declared here.
A declared key is consumed unless its handler returns bubble, which declines it: the boundary carries on to directional navigation, then to its own onUnhandledKey, then to the parent boundary. That is the runtime counterpart to the when gate — use when when the component never owns the key in that state (BrightScript then never claims it), and bubble when it owns the key but did nothing with this particular press.
Example
// Always handles OK (fire on key-down only via the onPress helper)
onKey: { OK: onPress(() => props.onSelect?.()) }
// RCButton-aligned enum members (values are runtime key strings):
onKey: { [RemoteKey.Select]: onPress(() => props.onSelect?.()) }
// Both edges — take the raw (press) => void handler
onKey: { OK: (press) => { if (press) onDown(); else onUp(); } }
// Owns `right`, but declines it at the end of the row so the boundary can navigate
onKey: { right: onPress(() => tryMoveRight() || bubble) }
// A concise setter consumes: `back` closes the panel and stops here
onKey: { back: onPress(() => setOpen(false)) }
// Conditionally handles OK (reactive gate)
onKey: { OK: { when: () => !props.disabled, handle: (press) => { if (press) props.onSelect?.() } } }type KeyHandlerMap = Partial<Record<RemoteKey, KeyHandlerEntry>> & Record<string, KeyHandlerEntry>;KeyPressCaptureRegistry
Deprecated.
Test hook that was never meant to be public. It will be removed from the public exports in the next major release.
Tree-wide store of captured presses, created once by the root boundary and shared with every nested boundary through the context.
| Name | Type | Description |
|---|---|---|
capture | (key: string, entry: KeyPressCapture) => void | Records entry as the capture for key, replacing any earlier capture of that key. |
capturedKeys | Accessor<Record<string, boolean>> | Reactive set of captured key names, unioned into handledKeys so BRS keeps forwarding. |
count | number | Number of presses currently captured. Read as the guard on the release dispatch path, so a tree that never captures pays a single integer comparison per key-up. |
releaseBoundary | (ownerBoundary: object) => void | Drops every capture owned by a boundary that is going away. |
releaseFocusId | (ownerFocusId: object) => void | Drops every capture owned by a leaf that is unregistering. |
take | (key: string) => KeyPressCapture | undefined | Removes and returns the capture for key, if any. |
KeyPressHandler
A key handler, called for both press and release.
The return value says whether the handler consumed the key. Returning bubble declines it: the key carries on to directional navigation, then the boundary's onUnhandledKey, then the parent boundary. Anything else consumes it, including no return at all, false, and whatever a SolidJS setter returns, so back: onPress(() => setOpen(false)) consumes Back.
Declining is what lets a fallback that only sometimes acts stay out of the way. A grid asked to move past its last row returns bubble so the key reaches whatever is outside it.
Both edges run the same dispatch chain, so a handler is asked about releases too. A press-only handler built with onPress consumes the release automatically; one written by hand consumes the release too unless it returns bubble for it.
type KeyPressHandler = (press: boolean) => KeyResult;KeyPressHandlerMap
Boundary-level fallback handlers keyed by firmware key strings.
An entry that returns bubble declines its key, so the boundary bubbles it to its parent instead of stopping there. Anything else, false included, consumes. See KeyPressHandler.
Example
onUnhandledKey={{
down: onPress(() => grid.tryMoveDown() || bubble),
back: onPress(() => goBack())
}}type KeyPressHandlerMap = Partial<Record<RemoteKey, KeyPressHandler>> & Record<string, KeyPressHandler>;KeyResult
What a key handler returns. Any value means "consumed"; return bubble to pass the key on.
This is an explicit union rather than unknown on purpose: unknown | typeof bubble collapses to unknown, which would hide bubble from hovers and from the emitted .d.ts. void alone is not enough either, because void | typeof bubble rejects a concise SolidJS setter such as () => setOpen(false) (setters return their value). Do not "simplify" it.
type KeyResult = void | object | null | undefined | typeof bubble;NavigationMap
Explicit navigation map for irregular layouts. Maps each focusable ref to its directional neighbors. When provided on FocusBoundary, takes precedence over the navigation prop.
Example
const playRef = {};
const pauseRef = {};
const stopRef = {};
const map: NavigationMap = new Map([
[playRef, { right: stopRef, down: pauseRef }],
[pauseRef, { up: playRef, right: stopRef }],
[stopRef, { left: playRef, up: pauseRef }]
]);type NavigationMap = Map<object, Partial<Record<"up" | "down" | "left" | "right", object>>>;Updated about 3 hours ago