@roku-sdk/components 0.0.0-stage → 2.19.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/BREAKING_CHANGES.md +84 -0
- package/LICENSE +21 -0
- package/README.md +19 -2
- package/core-jsx.d.ts +53 -0
- package/eslint.config.mjs +3 -0
- package/jsx.d.ts +273 -0
- package/package.json +85 -3
- package/src/__tests__/container.test.tsx +150 -0
- package/src/__tests__/effect-gradient-validation.test.tsx +133 -0
- package/src/__tests__/glyphs.test.ts +202 -0
- package/src/__tests__/modal-boundary-export.test.ts +12 -0
- package/src/__tests__/native-modal.test.tsx +224 -0
- package/src/__tests__/tween-release.test.tsx +125 -0
- package/src/__tests__/tween-stops-shape.test.tsx +165 -0
- package/src/__tests__/tween-without-compat.test.tsx +64 -0
- package/src/__tests__/tween.test.tsx +195 -0
- package/src/blended-busy-spinner.props.ts +37 -0
- package/src/blended-busy-spinner.tsx +13 -0
- package/src/bounding-tracker.props.ts +18 -0
- package/src/bounding-tracker.tsx +13 -0
- package/src/button-row.tsx +126 -0
- package/src/channel-store.props.ts +425 -0
- package/src/channel-store.tsx +76 -0
- package/src/container.tsx +130 -0
- package/src/core/jsx-runtime.ts +39 -0
- package/src/focusable-group.props.ts +8 -0
- package/src/focusable-group.tsx +17 -0
- package/src/focusable.props.ts +14 -0
- package/src/focusable.tsx +17 -0
- package/src/glyphs.ts +116 -0
- package/src/helpers/index.tsx +4 -0
- package/src/helpers/resolution-context.tsx +68 -0
- package/src/helpers/scale-for-device-resolution.ts +31 -0
- package/src/index.tsx +70 -0
- package/src/jsx-props.ts +87 -0
- package/src/kdfs.d.ts +14 -0
- package/src/kdfs.ts +15 -0
- package/src/key-handler.props.ts +33 -0
- package/src/key-handler.tsx +24 -0
- package/src/modal-dialog.props.ts +38 -0
- package/src/modal-dialog.tsx +41 -0
- package/src/retain-asset.ts +24 -0
- package/src/sg-intrinsics.json +1153 -0
- package/src/sg-nodes/__tests__/__snapshots__/font.test.tsx.snap +13 -0
- package/src/sg-nodes/__tests__/__snapshots__/prop-forwarding.test.tsx.snap +4409 -0
- package/src/sg-nodes/__tests__/core-props.test.ts +301 -0
- package/src/sg-nodes/__tests__/font.test.tsx +141 -0
- package/src/sg-nodes/__tests__/imperative-methods.test.ts +276 -0
- package/src/sg-nodes/__tests__/prop-forwarding.test.tsx +1049 -0
- package/src/sg-nodes/__tests__/solid-integration.test.tsx +719 -0
- package/src/sg-nodes/animated-image.props.ts +64 -0
- package/src/sg-nodes/animated-image.tsx +23 -0
- package/src/sg-nodes/animation.props.ts +292 -0
- package/src/sg-nodes/animation.tsx +190 -0
- package/src/sg-nodes/array-grid.ts +602 -0
- package/src/sg-nodes/audio.props.ts +302 -0
- package/src/sg-nodes/audio.tsx +57 -0
- package/src/sg-nodes/busy-spinner.props.ts +59 -0
- package/src/sg-nodes/busy-spinner.tsx +84 -0
- package/src/sg-nodes/button-group.props.ts +141 -0
- package/src/sg-nodes/button-group.tsx +30 -0
- package/src/sg-nodes/button.props.ts +119 -0
- package/src/sg-nodes/button.tsx +31 -0
- package/src/sg-nodes/check-list.props.ts +71 -0
- package/src/sg-nodes/check-list.tsx +55 -0
- package/src/sg-nodes/content-node.props.ts +1316 -0
- package/src/sg-nodes/content-node.tsx +153 -0
- package/src/sg-nodes/core-props.ts +197 -0
- package/src/sg-nodes/custom-keyboard.props.ts +108 -0
- package/src/sg-nodes/custom-keyboard.tsx +48 -0
- package/src/sg-nodes/dialog.props.ts +159 -0
- package/src/sg-nodes/dialog.tsx +36 -0
- package/src/sg-nodes/dynamic-key-grid.props.ts +22 -0
- package/src/sg-nodes/dynamic-key-grid.tsx +16 -0
- package/src/sg-nodes/dynamic-keyboard.props.ts +30 -0
- package/src/sg-nodes/dynamic-keyboard.tsx +18 -0
- package/src/sg-nodes/dynamic-mini-keyboard.props.ts +22 -0
- package/src/sg-nodes/dynamic-mini-keyboard.tsx +18 -0
- package/src/sg-nodes/dynamic-pinpad.props.ts +57 -0
- package/src/sg-nodes/dynamic-pinpad.tsx +18 -0
- package/src/sg-nodes/effect.props.ts +119 -0
- package/src/sg-nodes/effect.tsx +167 -0
- package/src/sg-nodes/font.props.ts +45 -0
- package/src/sg-nodes/font.tsx +80 -0
- package/src/sg-nodes/group.props.ts +105 -0
- package/src/sg-nodes/group.tsx +30 -0
- package/src/sg-nodes/label-list.props.ts +86 -0
- package/src/sg-nodes/label-list.tsx +64 -0
- package/src/sg-nodes/label.props.ts +207 -0
- package/src/sg-nodes/label.tsx +56 -0
- package/src/sg-nodes/layout-group.props.ts +69 -0
- package/src/sg-nodes/layout-group.tsx +75 -0
- package/src/sg-nodes/list-panel.props.ts +103 -0
- package/src/sg-nodes/list-panel.tsx +49 -0
- package/src/sg-nodes/markup-grid.props.ts +78 -0
- package/src/sg-nodes/markup-grid.tsx +38 -0
- package/src/sg-nodes/markup-list.props.ts +66 -0
- package/src/sg-nodes/markup-list.tsx +38 -0
- package/src/sg-nodes/mask-group.props.ts +78 -0
- package/src/sg-nodes/mask-group.tsx +59 -0
- package/src/sg-nodes/media-playback-methods.ts +20 -0
- package/src/sg-nodes/node.ts +77 -0
- package/src/sg-nodes/overhang.props.ts +169 -0
- package/src/sg-nodes/overhang.tsx +29 -0
- package/src/sg-nodes/panel-set.props.ts +71 -0
- package/src/sg-nodes/panel-set.tsx +53 -0
- package/src/sg-nodes/panel.props.ts +176 -0
- package/src/sg-nodes/panel.tsx +56 -0
- package/src/sg-nodes/poster-grid.props.ts +277 -0
- package/src/sg-nodes/poster-grid.tsx +45 -0
- package/src/sg-nodes/poster.props.ts +125 -0
- package/src/sg-nodes/poster.tsx +44 -0
- package/src/sg-nodes/radio-button-list.props.ts +59 -0
- package/src/sg-nodes/radio-button-list.tsx +68 -0
- package/src/sg-nodes/rectangle.props.ts +29 -0
- package/src/sg-nodes/rectangle.tsx +27 -0
- package/src/sg-nodes/row-list.props.ts +288 -0
- package/src/sg-nodes/row-list.tsx +47 -0
- package/src/sg-nodes/scene.props.ts +65 -0
- package/src/sg-nodes/scene.tsx +15 -0
- package/src/sg-nodes/scrollable-text.props.ts +133 -0
- package/src/sg-nodes/scrollable-text.tsx +64 -0
- package/src/sg-nodes/scrolling-label.props.ts +106 -0
- package/src/sg-nodes/scrolling-label.tsx +42 -0
- package/src/sg-nodes/simple-label.props.ts +156 -0
- package/src/sg-nodes/simple-label.tsx +59 -0
- package/src/sg-nodes/sound-effect.props.ts +130 -0
- package/src/sg-nodes/sound-effect.tsx +65 -0
- package/src/sg-nodes/standard-dialog.props.ts +520 -0
- package/src/sg-nodes/standard-dialog.tsx +428 -0
- package/src/sg-nodes/standard-keyboard-dialog.props.ts +84 -0
- package/src/sg-nodes/standard-keyboard-dialog.tsx +58 -0
- package/src/sg-nodes/standard-message-dialog.props.ts +56 -0
- package/src/sg-nodes/standard-message-dialog.tsx +47 -0
- package/src/sg-nodes/standard-pinpad-dialog.props.ts +64 -0
- package/src/sg-nodes/standard-pinpad-dialog.tsx +44 -0
- package/src/sg-nodes/standard-progress-dialog.props.ts +30 -0
- package/src/sg-nodes/standard-progress-dialog.tsx +40 -0
- package/src/sg-nodes/target-group.props.ts +278 -0
- package/src/sg-nodes/target-group.tsx +63 -0
- package/src/sg-nodes/target-list.props.ts +128 -0
- package/src/sg-nodes/target-list.tsx +66 -0
- package/src/sg-nodes/target-set.props.ts +63 -0
- package/src/sg-nodes/target-set.tsx +42 -0
- package/src/sg-nodes/time-grid.props.ts +646 -0
- package/src/sg-nodes/time-grid.tsx +87 -0
- package/src/sg-nodes/tween.props.ts +115 -0
- package/src/sg-nodes/types.ts +150 -0
- package/src/sg-nodes/video.props.ts +642 -0
- package/src/sg-nodes/video.tsx +75 -0
- package/src/sg-nodes/voice-text-edit-box.props.ts +91 -0
- package/src/sg-nodes/voice-text-edit-box.tsx +15 -0
- package/src/sg-nodes/zoom-row-list.props.ts +416 -0
- package/src/sg-nodes/zoom-row-list.tsx +42 -0
- package/src/standard-dialog-next/__tests__/standard-dialog-next.test.tsx +186 -0
- package/src/standard-dialog-next/index.ts +5 -0
- package/src/standard-dialog-next/standard-dialog-next.props.ts +80 -0
- package/src/standard-dialog-next/standard-dialog-next.tsx +326 -0
- package/src/test-helpers/tween-test-setup.ts +78 -0
- package/src/tween.tsx +173 -0
- package/src/types.ts +8 -0
- package/src/xml-components/blended-busy-spinner/BlendedBusySpinner.brs +13 -0
- package/src/xml-components/blended-busy-spinner/BlendedBusySpinner.xml +10 -0
- package/src/xml-components/blended-busy-spinner/BlendedBusySpinner.xml.d.ts +42 -0
- package/src/xml-components/bounding-tracker/BoundingTracker.brs +26 -0
- package/src/xml-components/bounding-tracker/BoundingTracker.xml +14 -0
- package/src/xml-components/bounding-tracker/BoundingTracker.xml.d.ts +42 -0
- package/src/xml-components/channel-store/channelStoreExt.brs +159 -0
- package/src/xml-components/channel-store/channelStoreExt.xml +29 -0
- package/src/xml-components/channel-store/channelStoreExt.xml.d.ts +57 -0
- package/src/xml-components/focus/focusable-group.brs +251 -0
- package/src/xml-components/focus/focusable-group.xml +15 -0
- package/src/xml-components/focus/focusable-group.xml.d.ts +42 -0
- package/src/xml-components/focus/focusable.xml +18 -0
- package/src/xml-components/focus/focusable.xml.d.ts +48 -0
- package/src/xml-components/keyboards/CustomAddressKDF.keyboard.json +547 -0
- package/src/xml-components/keyboards/CustomKeyboard.brs +322 -0
- package/src/xml-components/keyboards/CustomKeyboard.xml +45 -0
- package/src/xml-components/keyboards/CustomKeyboard.xml.d.ts +73 -0
- package/src/xml-components/keyboards/KeyGridExt.xml +4 -0
- package/src/xml-components/keyboards/KeyGridExt.xml.d.ts +37 -0
- package/src/xml-components/keyboards/KeyboardExt.xml +4 -0
- package/src/xml-components/keyboards/KeyboardExt.xml.d.ts +37 -0
- package/src/xml-components/keyboards/MOPPinPadCustomKDF.keyboard.json +151 -0
- package/src/xml-components/keyboards/MiniKeyboardExt.xml +4 -0
- package/src/xml-components/keyboards/MiniKeyboardExt.xml.d.ts +37 -0
- package/src/xml-components/keyboards/PhonePadCustomKDF.keyboard.json +42 -0
- package/src/xml-components/keyboards/PinPadExt.brs +102 -0
- package/src/xml-components/keyboards/PinPadExt.xml +17 -0
- package/src/xml-components/keyboards/PinPadExt.xml.d.ts +49 -0
- package/src/xml-components/keyboards/WifiCustomKDF.keyboard.json +1242 -0
- package/src/xml-components/keyhandler/KeyHandler.brs +30 -0
- package/src/xml-components/keyhandler/KeyHandler.xml +16 -0
- package/src/xml-components/keyhandler/KeyHandler.xml.d.ts +45 -0
- package/src/xml-components/managed-list-panel/ManagedListPanel.brs +137 -0
- package/src/xml-components/managed-list-panel/ManagedListPanel.xml +10 -0
- package/src/xml-components/managed-list-panel/ManagedListPanel.xml.d.ts +37 -0
- package/src/xml-components/modal/Modal.brs +105 -0
- package/src/xml-components/modal/Modal.xml +22 -0
- package/src/xml-components/modal/Modal.xml.d.ts +54 -0
- package/src/xml-components/modal/StandardDialogExt.xml +10 -0
- package/src/xml-components/modal/StandardDialogExt.xml.d.ts +37 -0
- package/src/xml-components/modal/StandardKeyboardDialogExt.xml +10 -0
- package/src/xml-components/modal/StandardKeyboardDialogExt.xml.d.ts +37 -0
- package/src/xml-components/modal/StandardMessageDialogExt.xml +10 -0
- package/src/xml-components/modal/StandardMessageDialogExt.xml.d.ts +37 -0
- package/src/xml-components/modal/StandardPinPadDialogExt.xml +10 -0
- package/src/xml-components/modal/StandardPinPadDialogExt.xml.d.ts +37 -0
- package/src/xml-components/modal/StandardProgressDialogExt.xml +10 -0
- package/src/xml-components/modal/StandardProgressDialogExt.xml.d.ts +37 -0
- package/src/xml-components/modal/tests/Modal.test.brs +195 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
' Copyright (c) 2026 Roku, Inc.
|
|
2
|
+
' SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
sub init()
|
|
5
|
+
m.top.observeField("change", "onChange")
|
|
6
|
+
m.top.observeField("focusedChild", "onFocusedChild")
|
|
7
|
+
m.focusableNodes = []
|
|
8
|
+
m.lastFocusedNode = invalid
|
|
9
|
+
m.focusablesParentNode = invalid
|
|
10
|
+
m.firstChangeEvent = true
|
|
11
|
+
end sub
|
|
12
|
+
|
|
13
|
+
' Handle node change events to manage focusable nodes
|
|
14
|
+
' Init is invoked when node is created but children are not appended yet.
|
|
15
|
+
' Children are added later, so we need to observe the change event
|
|
16
|
+
sub onChange(msg as Object)
|
|
17
|
+
data = msg.getData()
|
|
18
|
+
node = msg.getRoSGNode()
|
|
19
|
+
|
|
20
|
+
if data.Operation = "add"
|
|
21
|
+
handleAddOperation(msg, data)
|
|
22
|
+
else
|
|
23
|
+
handleNonAddOperation(node)
|
|
24
|
+
end if
|
|
25
|
+
|
|
26
|
+
' print "Focusable Group event:"; data.Operation
|
|
27
|
+
' print "Node: "; node.id
|
|
28
|
+
' print "Focusable Nodes: "; m.focusableNodes.count()
|
|
29
|
+
setupParentNodeObserver()
|
|
30
|
+
m.firstChangeEvent = false
|
|
31
|
+
end sub
|
|
32
|
+
|
|
33
|
+
' Handle when nodes are added to the group
|
|
34
|
+
sub handleAddOperation(msg as Object, data as Object)
|
|
35
|
+
child = msg.getRoSGNode().getChild(data.Index1)
|
|
36
|
+
if child <> invalid
|
|
37
|
+
addFocusableNode(child)
|
|
38
|
+
end if
|
|
39
|
+
end sub
|
|
40
|
+
|
|
41
|
+
' Handle operations other than 'add' (rebuild focusable nodes list)
|
|
42
|
+
' TODO: This can be expensive. Look at the operation field and optimize
|
|
43
|
+
sub handleNonAddOperation(node as Object)
|
|
44
|
+
m.focusableNodes = []
|
|
45
|
+
m.lastFocusedNode = invalid
|
|
46
|
+
if node <> invalid and node.getChildCount() > 0
|
|
47
|
+
for each child in node.getChildren(node.getChildCount(), 0)
|
|
48
|
+
addFocusableNode(child)
|
|
49
|
+
end for
|
|
50
|
+
end if
|
|
51
|
+
end sub
|
|
52
|
+
|
|
53
|
+
' Setup observer for parent node if needed
|
|
54
|
+
sub setupParentNodeObserver()
|
|
55
|
+
if m.firstChangeEvent = true and m.focusablesParentNode <> invalid
|
|
56
|
+
m.focusablesParentNode.observeField("change", "onChange")
|
|
57
|
+
end if
|
|
58
|
+
end sub
|
|
59
|
+
|
|
60
|
+
' Add a focusable node to the collection or search its children
|
|
61
|
+
sub addFocusableNode(node as Object)
|
|
62
|
+
if node = invalid
|
|
63
|
+
return
|
|
64
|
+
end if
|
|
65
|
+
|
|
66
|
+
if isFocusableNode(node)
|
|
67
|
+
addToFocusableCollection(node)
|
|
68
|
+
else
|
|
69
|
+
findFocusableNodes(node)
|
|
70
|
+
end if
|
|
71
|
+
end sub
|
|
72
|
+
|
|
73
|
+
' Add node to focusable collection and track its parent
|
|
74
|
+
sub addToFocusableCollection(node as Object)
|
|
75
|
+
m.focusableNodes.push(node)
|
|
76
|
+
|
|
77
|
+
if node.getParent() <> invalid and node.getParent().subtype() <> "focusable-group" and m.firstChangeEvent = true
|
|
78
|
+
if m.focusablesParentNode <> invalid and m.focusablesParentNode.id <> node.getParent().id
|
|
79
|
+
print "Warning: Each Focusable node has a different parent. This may lead to unexpected behavior."
|
|
80
|
+
end if
|
|
81
|
+
|
|
82
|
+
' Can each focusable be wrapped in different parent?
|
|
83
|
+
' If so, may be app should create a focusable group for each focusable node
|
|
84
|
+
m.focusablesParentNode = node.getParent()
|
|
85
|
+
end if
|
|
86
|
+
end sub
|
|
87
|
+
|
|
88
|
+
' Recursively search for focusable nodes in children
|
|
89
|
+
sub findFocusableNodes(node as Object)
|
|
90
|
+
if node = invalid or node.getChildCount() = 0
|
|
91
|
+
return
|
|
92
|
+
end if
|
|
93
|
+
|
|
94
|
+
for each child in node.getChildren(node.getChildCount(), 0)
|
|
95
|
+
addFocusableNode(child)
|
|
96
|
+
end for
|
|
97
|
+
end sub
|
|
98
|
+
|
|
99
|
+
function isFocusableNode(node as Object) as Boolean
|
|
100
|
+
return node.subtype() = "focusable"
|
|
101
|
+
end function
|
|
102
|
+
|
|
103
|
+
function onKeyEvent(key as String, press as Boolean) as Boolean
|
|
104
|
+
' When key is pressed, get the focused child
|
|
105
|
+
' look for the nextFocusDirection on the node
|
|
106
|
+
' if present, focus that node
|
|
107
|
+
handled = false
|
|
108
|
+
if press
|
|
109
|
+
inPathFocusables = getFocusableNodes(true) ' Should be only one focusable node in focus chain
|
|
110
|
+
if inPathFocusables.count() = 0 then return handled ' No focusable nodes in focus chain yet — bail early
|
|
111
|
+
item = inPathFocusables[0]
|
|
112
|
+
if item = invalid then return handled ' Safety guard against invalid array element
|
|
113
|
+
nextNodeId = item["nextFocus" + key] ' case insensitive
|
|
114
|
+
' print "Next NodeId: "; nextNodeId
|
|
115
|
+
if nextNodeId <> invalid and nextNodeId <> "" then
|
|
116
|
+
focusables = getFocusableNodes(false)
|
|
117
|
+
for i = 0 to focusables.count() - 1
|
|
118
|
+
searchResult = findClosestFocusableGroup(focusables[i], nextNodeId, invalid)
|
|
119
|
+
if searchResult <> invalid and searchResult.focusableGroup <> invalid and searchResult.focusableGroup.id = nextNodeId
|
|
120
|
+
handled = searchResult.focusableGroup.callfunc("pickFocus")
|
|
121
|
+
return handled
|
|
122
|
+
end if
|
|
123
|
+
if searchResult.nodeFound
|
|
124
|
+
if searchResult.focusableGroup <> invalid
|
|
125
|
+
handled = searchResult.focusableGroup.callfunc("assignFocus", nextNodeId, false)
|
|
126
|
+
else
|
|
127
|
+
' Moving focus within same focusable group
|
|
128
|
+
handled = assignFocus(nextNodeId, true)
|
|
129
|
+
end if
|
|
130
|
+
' print "Handled: "; handled
|
|
131
|
+
return handled
|
|
132
|
+
end if
|
|
133
|
+
end for
|
|
134
|
+
end if
|
|
135
|
+
end if
|
|
136
|
+
return handled
|
|
137
|
+
end function
|
|
138
|
+
|
|
139
|
+
function getFocusableNodes(isInFocusChain as Boolean) as Object
|
|
140
|
+
focusables = []
|
|
141
|
+
for i = 0 to m.focusableNodes.count() - 1
|
|
142
|
+
item = m.focusableNodes[i]
|
|
143
|
+
if item.isInFocusChain() = isInFocusChain
|
|
144
|
+
focusables.push(item)
|
|
145
|
+
end if
|
|
146
|
+
end for
|
|
147
|
+
return focusables
|
|
148
|
+
end function
|
|
149
|
+
|
|
150
|
+
' DFS to find the focus path for a given nodeId and closest focusable group
|
|
151
|
+
function findClosestFocusableGroup(rootNode as Object, nodeId as String, parentFocusableGroup as Object) as Object
|
|
152
|
+
result = {
|
|
153
|
+
nodeFound: false,
|
|
154
|
+
focusableGroup: invalid
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if rootNode <> invalid
|
|
158
|
+
' Update current focusable group if this node is one
|
|
159
|
+
currentFocusableGroup = parentFocusableGroup
|
|
160
|
+
if rootNode.subtype() = "focusable-group"
|
|
161
|
+
currentFocusableGroup = rootNode
|
|
162
|
+
end if
|
|
163
|
+
|
|
164
|
+
' Check if current node is the target
|
|
165
|
+
if rootNode.id = nodeId
|
|
166
|
+
result.nodeFound = true
|
|
167
|
+
result.focusableGroup = currentFocusableGroup
|
|
168
|
+
return result
|
|
169
|
+
end if
|
|
170
|
+
|
|
171
|
+
' Search children
|
|
172
|
+
for i = 0 to rootNode.getChildCount() - 1
|
|
173
|
+
child = rootNode.getChild(i)
|
|
174
|
+
if child <> invalid
|
|
175
|
+
childResult = findClosestFocusableGroup(child, nodeId, currentFocusableGroup)
|
|
176
|
+
if childResult.nodeFound
|
|
177
|
+
return childResult
|
|
178
|
+
end if
|
|
179
|
+
end if
|
|
180
|
+
end for
|
|
181
|
+
end if
|
|
182
|
+
|
|
183
|
+
return result
|
|
184
|
+
end function
|
|
185
|
+
|
|
186
|
+
' Assign focus to a node by its ID
|
|
187
|
+
function assignFocus(nodeId as String, withInSameFocusableGroup as Boolean) as Object
|
|
188
|
+
if m.lastFocusedNode <> invalid and m.top.rememberLastFocus and withInSameFocusableGroup = false then
|
|
189
|
+
' print "Setting Focus to last focused node: "; m.lastFocusedNode.id
|
|
190
|
+
m.lastFocusedNode.setFocus(true)
|
|
191
|
+
end if
|
|
192
|
+
|
|
193
|
+
foundNode = m.top.findNode(nodeId)
|
|
194
|
+
if foundNode <> invalid then
|
|
195
|
+
' print "Setting focus to: "; foundNode.id
|
|
196
|
+
foundNode.setFocus(true)
|
|
197
|
+
m.lastFocusedNode = foundNode
|
|
198
|
+
return true
|
|
199
|
+
end if
|
|
200
|
+
return false
|
|
201
|
+
end function
|
|
202
|
+
|
|
203
|
+
' Pick the first focusable node in the focusable group
|
|
204
|
+
function pickFocus() as Object
|
|
205
|
+
if m.lastFocusedNode <> invalid and m.top.rememberLastFocus
|
|
206
|
+
' print "Setting focus to last focused node: "; m.lastFocusedNode.id
|
|
207
|
+
m.lastFocusedNode.setFocus(true)
|
|
208
|
+
return true
|
|
209
|
+
end if
|
|
210
|
+
|
|
211
|
+
firstFocusable = invalid
|
|
212
|
+
if m.focusableNodes.count() > 0
|
|
213
|
+
for each focusable in m.focusableNodes
|
|
214
|
+
if focusable.hasPreferredFocus = true
|
|
215
|
+
' print "Setting initial focus to preferred focusable: "; focusable.getChild(0).id
|
|
216
|
+
firstFocusable = focusable
|
|
217
|
+
exit for
|
|
218
|
+
end if
|
|
219
|
+
end for
|
|
220
|
+
if firstFocusable = invalid
|
|
221
|
+
firstFocusable = m.focusableNodes[0]
|
|
222
|
+
end if
|
|
223
|
+
|
|
224
|
+
if firstFocusable <> invalid
|
|
225
|
+
firstFocusableChild = firstFocusable.getChild(0)
|
|
226
|
+
end if
|
|
227
|
+
|
|
228
|
+
if firstFocusableChild <> invalid
|
|
229
|
+
if firstFocusableChild.subtype() = "focusable-group"
|
|
230
|
+
return firstFocusableChild.callfunc("pickFocus")
|
|
231
|
+
else
|
|
232
|
+
' print "Setting focus to first child of first focusable group: "; firstFocusableChild.id
|
|
233
|
+
firstFocusableChild.setFocus(true)
|
|
234
|
+
' print "Setting initial focus to: "; firstFocusableChild.id
|
|
235
|
+
m.lastFocusedNode = firstFocusableChild
|
|
236
|
+
return true
|
|
237
|
+
end if
|
|
238
|
+
end if
|
|
239
|
+
end if
|
|
240
|
+
return false
|
|
241
|
+
end function
|
|
242
|
+
|
|
243
|
+
sub onFocusedChild(msg as Object)
|
|
244
|
+
data = msg.getData()
|
|
245
|
+
|
|
246
|
+
if m.top.isSameNode(data) ' this is the case when focus is set to this group
|
|
247
|
+
pickFocus()
|
|
248
|
+
end if
|
|
249
|
+
end sub
|
|
250
|
+
|
|
251
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
Copyright (c) 2026 Roku, Inc.
|
|
4
|
+
SPDX-License-Identifier: MIT
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
<component name="focusable-group" extends="Group">
|
|
9
|
+
<script type="text/brightscript" uri="focusable-group.brs" />
|
|
10
|
+
<interface>
|
|
11
|
+
<field id="rememberLastFocus" type="boolean" default="true" />
|
|
12
|
+
<function name="assignFocus" />
|
|
13
|
+
<function name="pickFocus" />
|
|
14
|
+
</interface>
|
|
15
|
+
</component>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Auto-generated TypeScript declarations for XML component import.
|
|
2
|
+
// DO NOT EDIT — this file is regenerated during build.
|
|
3
|
+
//
|
|
4
|
+
// Source component: focusable-group
|
|
5
|
+
// Extends: Group
|
|
6
|
+
|
|
7
|
+
import type { IGroupProps } from "@roku-sdk/components";
|
|
8
|
+
import type { TypedXmlComponent } from "@roku-sdk/interfaces";
|
|
9
|
+
|
|
10
|
+
interface focusableGroupFields {
|
|
11
|
+
/** @default "true" */
|
|
12
|
+
rememberLastFocus: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface IfocusableGroupProps extends IGroupProps, Partial<focusableGroupFields> {}
|
|
16
|
+
|
|
17
|
+
declare const _default: TypedXmlComponent<"focusable-group", focusableGroupFields>;
|
|
18
|
+
export default _default;
|
|
19
|
+
|
|
20
|
+
declare module "@roku-sdk/solid" {
|
|
21
|
+
namespace JSX {
|
|
22
|
+
interface IntrinsicElements {
|
|
23
|
+
focusableGroup: IfocusableGroupProps;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
declare module "@roku-sdk/solid/jsx-runtime" {
|
|
29
|
+
namespace JSX {
|
|
30
|
+
interface IntrinsicElements {
|
|
31
|
+
focusableGroup: IfocusableGroupProps;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
declare module "solid-js" {
|
|
37
|
+
namespace JSX {
|
|
38
|
+
interface IntrinsicElements {
|
|
39
|
+
focusableGroup: IfocusableGroupProps;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
Copyright (c) 2026 Roku, Inc.
|
|
4
|
+
SPDX-License-Identifier: MIT
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
<component name="focusable" extends="Group">
|
|
9
|
+
<interface>
|
|
10
|
+
<field id="nextFocusUp" type="string" />
|
|
11
|
+
<field id="nextFocusDown" type="string" />
|
|
12
|
+
<field id="nextFocusLeft" type="string" />
|
|
13
|
+
<field id="nextFocusRight" type="string" />
|
|
14
|
+
<field id="nextFocusBack" type="string" />
|
|
15
|
+
<field id="nextFocusOk" type="string" />
|
|
16
|
+
<field id="hasPreferredFocus" type="boolean" default="false" />
|
|
17
|
+
</interface>
|
|
18
|
+
</component>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Auto-generated TypeScript declarations for XML component import.
|
|
2
|
+
// DO NOT EDIT — this file is regenerated during build.
|
|
3
|
+
//
|
|
4
|
+
// Source component: focusable
|
|
5
|
+
// Extends: Group
|
|
6
|
+
|
|
7
|
+
import type { IGroupProps } from "@roku-sdk/components";
|
|
8
|
+
import type { TypedXmlComponent } from "@roku-sdk/interfaces";
|
|
9
|
+
|
|
10
|
+
interface focusableFields {
|
|
11
|
+
nextFocusUp: string;
|
|
12
|
+
nextFocusDown: string;
|
|
13
|
+
nextFocusLeft: string;
|
|
14
|
+
nextFocusRight: string;
|
|
15
|
+
nextFocusBack: string;
|
|
16
|
+
nextFocusOk: string;
|
|
17
|
+
/** @default "false" */
|
|
18
|
+
hasPreferredFocus: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IfocusableProps extends IGroupProps, Partial<focusableFields> {}
|
|
22
|
+
|
|
23
|
+
declare const _default: TypedXmlComponent<"focusable", focusableFields>;
|
|
24
|
+
export default _default;
|
|
25
|
+
|
|
26
|
+
declare module "@roku-sdk/solid" {
|
|
27
|
+
namespace JSX {
|
|
28
|
+
interface IntrinsicElements {
|
|
29
|
+
focusable: IfocusableProps;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare module "@roku-sdk/solid/jsx-runtime" {
|
|
35
|
+
namespace JSX {
|
|
36
|
+
interface IntrinsicElements {
|
|
37
|
+
focusable: IfocusableProps;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare module "solid-js" {
|
|
43
|
+
namespace JSX {
|
|
44
|
+
interface IntrinsicElements {
|
|
45
|
+
focusable: IfocusableProps;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|