@lightningtv/solid 3.0.0-9 → 3.0.1

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.
Files changed (228) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +6 -0
  3. package/dist/jsx-runtime.d.ts +14 -0
  4. package/dist/src/activeElement.d.ts +1 -1
  5. package/dist/src/core/animation.d.ts +35 -0
  6. package/dist/src/core/animation.js +119 -0
  7. package/dist/src/core/animation.js.map +1 -0
  8. package/dist/src/core/config.d.ts +49 -0
  9. package/dist/src/core/config.js +33 -0
  10. package/dist/src/core/config.js.map +1 -0
  11. package/dist/src/core/domRenderer.d.ts +115 -0
  12. package/dist/src/core/domRenderer.js +1152 -0
  13. package/dist/src/core/domRenderer.js.map +1 -0
  14. package/dist/src/core/elementNode.d.ts +463 -0
  15. package/dist/src/core/elementNode.js +833 -0
  16. package/dist/src/core/elementNode.js.map +1 -0
  17. package/dist/src/core/flex.d.ts +2 -0
  18. package/dist/src/core/flex.js +243 -0
  19. package/dist/src/core/flex.js.map +1 -0
  20. package/dist/src/core/focusKeyTypes.d.ts +42 -0
  21. package/dist/src/core/focusKeyTypes.js +2 -0
  22. package/dist/src/core/focusKeyTypes.js.map +1 -0
  23. package/dist/src/core/focusManager.d.ts +13 -0
  24. package/dist/src/core/focusManager.js +276 -0
  25. package/dist/src/core/focusManager.js.map +1 -0
  26. package/dist/src/core/index.d.ts +12 -0
  27. package/dist/src/core/index.js +12 -0
  28. package/dist/src/core/index.js.map +1 -0
  29. package/dist/src/core/intrinsicTypes.d.ts +90 -0
  30. package/dist/src/core/intrinsicTypes.js +2 -0
  31. package/dist/src/core/intrinsicTypes.js.map +1 -0
  32. package/dist/src/core/lightningInit.d.ts +89 -0
  33. package/dist/src/core/lightningInit.js +26 -0
  34. package/dist/src/core/lightningInit.js.map +1 -0
  35. package/dist/src/core/nodeTypes.d.ts +6 -0
  36. package/dist/src/core/nodeTypes.js +6 -0
  37. package/dist/src/core/nodeTypes.js.map +1 -0
  38. package/dist/src/core/shaders.d.ts +51 -0
  39. package/dist/src/core/shaders.js +446 -0
  40. package/dist/src/core/shaders.js.map +1 -0
  41. package/dist/src/core/states.d.ts +12 -0
  42. package/dist/src/core/states.js +84 -0
  43. package/dist/src/core/states.js.map +1 -0
  44. package/dist/src/core/utils.d.ts +39 -0
  45. package/dist/src/core/utils.js +164 -0
  46. package/dist/src/core/utils.js.map +1 -0
  47. package/dist/src/devtools/index.d.ts +1 -1
  48. package/dist/src/devtools/index.js +1 -1
  49. package/dist/src/devtools/index.js.map +1 -1
  50. package/dist/src/index.d.ts +3 -3
  51. package/dist/src/index.js +1 -1
  52. package/dist/src/index.js.map +1 -1
  53. package/dist/src/primitives/Column.jsx +9 -10
  54. package/dist/src/primitives/Column.jsx.map +1 -1
  55. package/dist/src/primitives/FPSCounter.jsx +15 -2
  56. package/dist/src/primitives/FPSCounter.jsx.map +1 -1
  57. package/dist/src/primitives/Image.d.ts +8 -0
  58. package/dist/src/primitives/Image.jsx +24 -0
  59. package/dist/src/primitives/Image.jsx.map +1 -0
  60. package/dist/src/primitives/KeepAlive.d.ts +30 -0
  61. package/dist/src/primitives/KeepAlive.jsx +77 -0
  62. package/dist/src/primitives/KeepAlive.jsx.map +1 -0
  63. package/dist/src/primitives/Lazy.d.ts +8 -7
  64. package/dist/src/primitives/Lazy.jsx +52 -20
  65. package/dist/src/primitives/Lazy.jsx.map +1 -1
  66. package/dist/src/primitives/LazyImport.d.ts +8 -0
  67. package/dist/src/primitives/LazyImport.js +40 -0
  68. package/dist/src/primitives/LazyImport.js.map +1 -0
  69. package/dist/src/primitives/Preserve.d.ts +4 -0
  70. package/dist/src/primitives/Preserve.jsx +11 -0
  71. package/dist/src/primitives/Preserve.jsx.map +1 -0
  72. package/dist/src/primitives/Row.jsx +9 -10
  73. package/dist/src/primitives/Row.jsx.map +1 -1
  74. package/dist/src/primitives/Suspense.d.ts +22 -0
  75. package/dist/src/primitives/Suspense.jsx +33 -0
  76. package/dist/src/primitives/Suspense.jsx.map +1 -0
  77. package/dist/src/primitives/Virtual.d.ts +18 -0
  78. package/dist/src/primitives/Virtual.jsx +443 -0
  79. package/dist/src/primitives/Virtual.jsx.map +1 -0
  80. package/dist/src/primitives/VirtualGrid.d.ts +13 -0
  81. package/dist/src/primitives/VirtualGrid.jsx +160 -0
  82. package/dist/src/primitives/VirtualGrid.jsx.map +1 -0
  83. package/dist/src/primitives/Visible.d.ts +0 -1
  84. package/dist/src/primitives/Visible.jsx +1 -1
  85. package/dist/src/primitives/Visible.jsx.map +1 -1
  86. package/dist/src/primitives/announcer/announcer.d.ts +1 -0
  87. package/dist/src/primitives/announcer/announcer.js +4 -3
  88. package/dist/src/primitives/announcer/announcer.js.map +1 -1
  89. package/dist/src/primitives/announcer/speech.d.ts +1 -1
  90. package/dist/src/primitives/announcer/speech.js +98 -8
  91. package/dist/src/primitives/announcer/speech.js.map +1 -1
  92. package/dist/src/primitives/createFocusStack.d.ts +4 -4
  93. package/dist/src/primitives/createFocusStack.jsx +15 -6
  94. package/dist/src/primitives/createFocusStack.jsx.map +1 -1
  95. package/dist/src/primitives/createTag.d.ts +8 -0
  96. package/dist/src/primitives/createTag.jsx +20 -0
  97. package/dist/src/primitives/createTag.jsx.map +1 -0
  98. package/dist/src/primitives/index.d.ts +13 -4
  99. package/dist/src/primitives/index.js +12 -3
  100. package/dist/src/primitives/index.js.map +1 -1
  101. package/dist/src/primitives/types.d.ts +3 -2
  102. package/dist/src/primitives/useFocusManager.d.ts +2 -2
  103. package/dist/src/primitives/useFocusManager.js +2 -2
  104. package/dist/src/primitives/useFocusManager.js.map +1 -1
  105. package/dist/src/primitives/useMouse.d.ts +18 -2
  106. package/dist/src/primitives/useMouse.js +171 -47
  107. package/dist/src/primitives/useMouse.js.map +1 -1
  108. package/dist/src/primitives/utils/createBlurredImage.d.ts +56 -0
  109. package/dist/src/primitives/utils/createBlurredImage.js +223 -0
  110. package/dist/src/primitives/utils/createBlurredImage.js.map +1 -0
  111. package/dist/src/primitives/utils/createSpriteMap.d.ts +2 -2
  112. package/dist/src/primitives/utils/createSpriteMap.js +3 -3
  113. package/dist/src/primitives/utils/createSpriteMap.js.map +1 -1
  114. package/dist/src/primitives/utils/handleNavigation.d.ts +79 -5
  115. package/dist/src/primitives/utils/handleNavigation.js +241 -69
  116. package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
  117. package/dist/src/primitives/utils/withScrolling.d.ts +12 -2
  118. package/dist/src/primitives/utils/withScrolling.js +59 -7
  119. package/dist/src/primitives/utils/withScrolling.js.map +1 -1
  120. package/dist/src/render.d.ts +5 -4
  121. package/dist/src/render.js +5 -1
  122. package/dist/src/render.js.map +1 -1
  123. package/dist/src/shaders/Rounded.d.ts +7 -0
  124. package/dist/src/shaders/Rounded.js +88 -0
  125. package/dist/src/shaders/Rounded.js.map +1 -0
  126. package/dist/src/shaders/RoundedWithBorder.d.ts +3 -0
  127. package/dist/src/shaders/RoundedWithBorder.js +217 -0
  128. package/dist/src/shaders/RoundedWithBorder.js.map +1 -0
  129. package/dist/src/shaders/index.d.ts +4 -0
  130. package/dist/src/shaders/index.js +5 -0
  131. package/dist/src/shaders/index.js.map +1 -0
  132. package/dist/src/shaders/templates/RoundedTemplate.d.ts +12 -0
  133. package/dist/src/shaders/templates/RoundedTemplate.js +48 -0
  134. package/dist/src/shaders/templates/RoundedTemplate.js.map +1 -0
  135. package/dist/src/shaders/templates/RoundedWithBorderTemplate.d.ts +20 -0
  136. package/dist/src/shaders/templates/RoundedWithBorderTemplate.js +93 -0
  137. package/dist/src/shaders/templates/RoundedWithBorderTemplate.js.map +1 -0
  138. package/dist/src/shaders/utils.d.ts +3 -0
  139. package/dist/src/shaders/utils.js +31 -0
  140. package/dist/src/shaders/utils.js.map +1 -0
  141. package/dist/src/solidOpts.d.ts +1 -7
  142. package/dist/src/solidOpts.js +9 -1
  143. package/dist/src/solidOpts.js.map +1 -1
  144. package/dist/src/types.d.ts +1 -13
  145. package/dist/src/utils.d.ts +3 -1
  146. package/dist/src/utils.js +9 -1
  147. package/dist/src/utils.js.map +1 -1
  148. package/dist/tsconfig.tsbuildinfo +1 -1
  149. package/jsx-runtime.d.ts +1 -1
  150. package/package.json +28 -16
  151. package/src/activeElement.ts +1 -1
  152. package/src/core/animation.ts +185 -0
  153. package/src/core/config.ts +89 -0
  154. package/src/core/domRenderer.ts +1300 -0
  155. package/src/core/elementNode.ts +1458 -0
  156. package/src/core/flex.ts +284 -0
  157. package/src/core/focusKeyTypes.ts +90 -0
  158. package/src/core/focusManager.ts +381 -0
  159. package/src/core/index.ts +13 -0
  160. package/src/core/intrinsicTypes.ts +199 -0
  161. package/src/core/lightningInit.ts +147 -0
  162. package/src/core/nodeTypes.ts +6 -0
  163. package/src/core/shaders.ts +567 -0
  164. package/src/core/states.ts +91 -0
  165. package/src/core/utils.ts +222 -0
  166. package/src/devtools/index.ts +1 -1
  167. package/src/index.ts +3 -3
  168. package/src/primitives/Column.tsx +10 -12
  169. package/src/primitives/FPSCounter.tsx +16 -2
  170. package/src/primitives/Image.tsx +36 -0
  171. package/src/primitives/KeepAlive.tsx +124 -0
  172. package/src/primitives/Lazy.tsx +66 -37
  173. package/src/primitives/LazyImport.ts +53 -0
  174. package/src/primitives/Preserve.tsx +18 -0
  175. package/src/primitives/Row.tsx +13 -14
  176. package/src/primitives/Suspense.tsx +39 -0
  177. package/src/primitives/Virtual.tsx +486 -0
  178. package/src/primitives/VirtualGrid.tsx +220 -0
  179. package/src/primitives/Visible.tsx +1 -2
  180. package/src/primitives/announcer/announcer.ts +10 -3
  181. package/src/primitives/announcer/speech.ts +113 -6
  182. package/src/primitives/createFocusStack.tsx +18 -7
  183. package/src/primitives/createTag.tsx +33 -0
  184. package/src/primitives/index.ts +13 -4
  185. package/src/primitives/types.ts +3 -2
  186. package/src/primitives/useFocusManager.ts +3 -3
  187. package/src/primitives/useMouse.ts +306 -67
  188. package/src/primitives/utils/createBlurredImage.ts +366 -0
  189. package/src/primitives/utils/createSpriteMap.ts +8 -6
  190. package/src/primitives/utils/handleNavigation.ts +300 -84
  191. package/src/primitives/utils/withScrolling.ts +76 -18
  192. package/src/render.ts +7 -3
  193. package/src/shaders/Rounded.ts +100 -0
  194. package/src/shaders/RoundedWithBorder.ts +245 -0
  195. package/src/shaders/index.ts +4 -0
  196. package/src/shaders/templates/RoundedTemplate.ts +57 -0
  197. package/src/shaders/templates/RoundedWithBorderTemplate.ts +110 -0
  198. package/src/shaders/utils.ts +44 -0
  199. package/src/solidOpts.ts +9 -7
  200. package/src/types.ts +1 -15
  201. package/src/utils.ts +11 -1
  202. package/dist/src/client.d.ts +0 -1
  203. package/dist/src/client.js +0 -2
  204. package/dist/src/client.js.map +0 -1
  205. package/dist/src/core.d.ts +0 -1
  206. package/dist/src/core.js +0 -3
  207. package/dist/src/core.js.map +0 -1
  208. package/dist/src/jsx-runtime.d.ts +0 -10
  209. package/dist/src/jsx-runtime.js +0 -2
  210. package/dist/src/jsx-runtime.js.map +0 -1
  211. package/dist/src/primitives/Infinite.d.ts +0 -15
  212. package/dist/src/primitives/Infinite.jsx +0 -59
  213. package/dist/src/primitives/Infinite.jsx.map +0 -1
  214. package/dist/src/primitives/LazyUp.d.ts +0 -11
  215. package/dist/src/primitives/LazyUp.jsx +0 -38
  216. package/dist/src/primitives/LazyUp.jsx.map +0 -1
  217. package/dist/src/primitives/sprite.d.ts +0 -9
  218. package/dist/src/primitives/sprite.js +0 -18
  219. package/dist/src/primitives/sprite.js.map +0 -1
  220. package/dist/src/primitives/utils/createFocusStack.d.ts +0 -24
  221. package/dist/src/primitives/utils/createFocusStack.js +0 -59
  222. package/dist/src/primitives/utils/createFocusStack.js.map +0 -1
  223. package/dist/src/primitives/utils/scrollToIndex.d.ts +0 -2
  224. package/dist/src/primitives/utils/scrollToIndex.js +0 -33
  225. package/dist/src/primitives/utils/scrollToIndex.js.map +0 -1
  226. package/dist/src/renderClient.d.ts +0 -21
  227. package/dist/src/renderClient.js +0 -64
  228. package/dist/src/renderClient.js.map +0 -1
@@ -0,0 +1,220 @@
1
+ import * as s from 'solid-js';
2
+ import * as lng from '@lightningtv/solid';
3
+ import * as lngp from '@lightningtv/solid/primitives';
4
+ import { List } from '@solid-primitives/list';
5
+ import * as utils from '../utils.js';
6
+
7
+ const columnScroll = lngp.withScrolling(false);
8
+
9
+ const rowStyles: lng.NodeStyles = {
10
+ display: 'flex',
11
+ flexWrap: 'wrap',
12
+ transition: {
13
+ y: true,
14
+ },
15
+ };
16
+
17
+ export type VirtualGridProps<T> = lng.NewOmit<lngp.RowProps, 'children'> & {
18
+ each: readonly T[] | undefined | null | false;
19
+ columns: number; // items per row
20
+ rows?: number; // number of visible rows (default: 1)
21
+ buffer?: number;
22
+ onEndReached?: () => void;
23
+ onEndReachedThreshold?: number;
24
+ children: (item: s.Accessor<T>, index: s.Accessor<number>) => s.JSX.Element;
25
+ };
26
+
27
+ export function VirtualGrid<T>(props: VirtualGridProps<T>): s.JSX.Element {
28
+ const bufferSize = () => props.buffer ?? 2;
29
+ const [ cursor, setCursor ] = s.createSignal(props.selected ?? 0);
30
+ const items = s.createMemo(() => props.each || []);
31
+ const itemCount = () => items().length;
32
+ const itemsPerRow = () => props.columns;
33
+ const numberOfRows = () => props.rows ?? 1;
34
+ const totalVisibleItems = () => itemsPerRow() * numberOfRows();
35
+
36
+ const start = s.createMemo(() => {
37
+ const perRow = itemsPerRow();
38
+ const newRowIndex = Math.floor(cursor() / perRow);
39
+ const rawStart = newRowIndex * perRow - bufferSize() * perRow;
40
+ return Math.max(0, rawStart);
41
+ });
42
+
43
+ const end = s.createMemo(() => {
44
+ const perRow = itemsPerRow();
45
+ const newRowIndex = Math.floor(cursor() / perRow);
46
+ const rawEnd = (newRowIndex + bufferSize()) * perRow + totalVisibleItems();
47
+ return Math.min(items().length, rawEnd);
48
+ });
49
+
50
+ const [slice, setSlice] = s.createSignal(items().slice(start(), end()));
51
+
52
+ let viewRef!: lngp.NavigableElement;
53
+
54
+ function onVerticalNav(dir: -1 | 1): lngp.KeyHandler {
55
+ return function () {
56
+ const perRow = itemsPerRow();
57
+ const currentRowIndex = Math.floor(cursor() / perRow);
58
+ const maxRows = Math.floor(items().length / perRow);
59
+
60
+ if (
61
+ currentRowIndex === 0 && dir === -1
62
+ || currentRowIndex === maxRows && dir === 1
63
+ ) return;
64
+
65
+ const selected = this.selected || 0;
66
+ const offset = dir * perRow;
67
+ const newIndex = utils.clamp(selected + offset, 0, items().length - 1);
68
+ const lastIdx = selected;
69
+ this.selected = newIndex;
70
+ const active = this.children[this.selected];
71
+
72
+ if (active instanceof lng.ElementNode) {
73
+ active.setFocus();
74
+ chainedOnSelectedChanged.call(
75
+ this as lngp.NavigableElement,
76
+ this.selected,
77
+ this as lngp.NavigableElement,
78
+ active,
79
+ lastIdx
80
+ );
81
+ return true;
82
+ }
83
+ };
84
+ }
85
+
86
+ const onUp = onVerticalNav(-1);
87
+ const onDown = onVerticalNav(1);
88
+
89
+ const onSelectedChanged: lngp.OnSelectedChanged = function (_idx, elm, active, _lastIdx,) {
90
+ let idx = _idx;
91
+ let lastIdx = _lastIdx;
92
+ const perRow = itemsPerRow();
93
+ const newRowIndex = Math.floor(idx / perRow);
94
+ const prevRowIndex = Math.floor((lastIdx || 0) / perRow);
95
+ const prevStart = start();
96
+
97
+ setCursor(prevStart + idx);
98
+ if (newRowIndex === prevRowIndex) return;
99
+
100
+ setSlice(items().slice(start(), end()));
101
+
102
+ // this.selected is relative to the slice
103
+ // and it doesn't get corrected automatically after children change
104
+ const idxCorrection = prevStart - start();
105
+ if (lastIdx) lastIdx += idxCorrection;
106
+ idx += idxCorrection;
107
+ this.selected += idxCorrection;
108
+
109
+ if (props.onEndReachedThreshold !== undefined && cursor() >= items().length - props.onEndReachedThreshold) {
110
+ props.onEndReached?.();
111
+ }
112
+
113
+ queueMicrotask(() => {
114
+ const prevRowY = this.y + active.y;
115
+ this.updateLayout();
116
+ this.lng.y = prevRowY - active.y;
117
+ columnScroll(idx, elm, active, lastIdx);
118
+ });
119
+ };
120
+
121
+ const chainedOnSelectedChanged = lngp.chainFunctions(props.onSelectedChanged, onSelectedChanged)!;
122
+
123
+ let cachedSelected: number | undefined;
124
+ const updateSelected = ([selected, _items]: [number?, any?]) => {
125
+ if (!viewRef || selected == null) return;
126
+
127
+ if (cachedSelected !== undefined) {
128
+ selected = cachedSelected;
129
+ cachedSelected = undefined;
130
+ }
131
+
132
+ if (selected >= items().length && props.onEndReached) {
133
+ props.onEndReached?.();
134
+ cachedSelected = selected;
135
+ return;
136
+ }
137
+
138
+ const item = items()[selected];
139
+ let active = viewRef.children.find(x => x.item === item);
140
+ const lastSelected = viewRef.selected;
141
+
142
+ if (active instanceof lng.ElementNode) {
143
+ viewRef.selected = viewRef.children.indexOf(active);
144
+ if (lng.hasFocus(viewRef)) {
145
+ // force focus as scrollToIndex is manually called
146
+ active.setFocus();
147
+ }
148
+ chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
149
+ } else {
150
+ setCursor(selected);
151
+ setSlice(items().slice(start(), end()));
152
+
153
+ queueMicrotask(() => {
154
+ viewRef.updateLayout();
155
+ active = viewRef.children.find(x => x.item === item);
156
+ if (active instanceof lng.ElementNode) {
157
+ viewRef.selected = viewRef.children.indexOf(active);
158
+ if (lng.hasFocus(viewRef)) {
159
+ active.setFocus();
160
+ }
161
+ chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
162
+ }
163
+ });
164
+ }
165
+ };
166
+
167
+ const scrollToIndex = (index: number) => {
168
+ s.untrack(() => updateSelected([index]));
169
+ }
170
+
171
+ s.createEffect(s.on([() => props.selected, items], updateSelected));
172
+
173
+ s.createEffect(
174
+ s.on(items, (gridItems, _prevGridItems, prevSize) => {
175
+ if (!viewRef) return;
176
+
177
+ if (cachedSelected !== undefined) {
178
+ // This occurs when VG is reloaded and user wants to select a paginated item
179
+ updateSelected([cachedSelected]);
180
+ return gridItems.length;
181
+ }
182
+
183
+ if (gridItems.length === 0) {
184
+ setCursor(0);
185
+ cachedSelected = undefined;
186
+ setSlice([]);
187
+ } else if (cursor() >= itemCount()) {
188
+ updateSelected([Math.max(0, itemCount() - 1)]);
189
+ } else if (prevSize === 0) {
190
+ updateSelected([0]);
191
+ } else {
192
+ setSlice(items().slice(start(), end()));
193
+ }
194
+
195
+ return gridItems.length;
196
+ }, { defer: true })
197
+ );
198
+
199
+
200
+ return (
201
+ <view
202
+ {...props}
203
+ scroll={props.scroll || 'always'}
204
+ ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)}
205
+ selected={props.selected || 0}
206
+ cursor={cursor()}
207
+ onLeft={/* @once */ lngp.chainFunctions(props.onLeft, lngp.navigableHandleNavigation)}
208
+ onRight={/* @once */ lngp.chainFunctions(props.onRight, lngp.navigableHandleNavigation)}
209
+ onUp={/* @once */ lngp.chainFunctions(props.onUp, onUp)}
210
+ onDown={/* @once */ lngp.chainFunctions(props.onDown, onDown)}
211
+ forwardFocus={/* @once */ lngp.navigableForwardFocus}
212
+ onCreate={/* @once */ props.selected ? lngp.chainFunctions(props.onCreate, columnScroll) : props.onCreate}
213
+ scrollToIndex={/* @once */ scrollToIndex}
214
+ onSelectedChanged={/* @once */ chainedOnSelectedChanged}
215
+ style={/* @once */ lng.combineStyles(props.style, rowStyles)}
216
+ >
217
+ <List each={slice()}>{props.children}</List>
218
+ </view>
219
+ );
220
+ }
@@ -13,7 +13,6 @@ import { ElementNode } from '@lightningtv/solid';
13
13
  export function Visible<T>(props: {
14
14
  when: T | undefined | null | false;
15
15
  keyed?: boolean;
16
- fallback?: JSX.Element;
17
16
  children: JSX.Element;
18
17
  }): JSX.Element {
19
18
  let child: ChildrenReturn | undefined;
@@ -55,6 +54,6 @@ export function Visible<T>(props: {
55
54
  }
56
55
  });
57
56
 
58
- return c ? child : props.fallback;
57
+ return c || child ? child : null;
59
58
  }) as unknown as JSX.Element;
60
59
  };
@@ -109,18 +109,24 @@ function onFocusChangeCore(focusPath: ElementNode[] = []) {
109
109
  }
110
110
  }
111
111
 
112
- function textToSpeech(toSpeak: SpeechType, lang: string, voice?: string) {
112
+ function textToSpeech(
113
+ toSpeak: SpeechType,
114
+ aria: boolean,
115
+ lang: string,
116
+ voice?: string,
117
+ ) {
113
118
  if (voiceOutDisabled) {
114
119
  return;
115
120
  }
116
121
 
117
- return (currentlySpeaking = SpeechEngine(toSpeak, lang, voice));
122
+ return (currentlySpeaking = SpeechEngine(toSpeak, aria, lang, voice));
118
123
  }
119
124
 
120
125
  export interface Announcer {
121
126
  debug: boolean;
122
127
  enabled: boolean;
123
128
  lang: string;
129
+ aria: boolean;
124
130
  voice?: string;
125
131
  cancel: VoidFunction;
126
132
  clearPrevFocus: (depth?: number) => void;
@@ -140,6 +146,7 @@ export const Announcer: Announcer = {
140
146
  debug: false,
141
147
  enabled: true,
142
148
  lang: 'en-US',
149
+ aria: false,
143
150
  cancel: function () {
144
151
  currentlySpeaking && currentlySpeaking.cancel();
145
152
  },
@@ -153,7 +160,7 @@ export const Announcer: Announcer = {
153
160
  currentlySpeaking.append(text);
154
161
  } else {
155
162
  Announcer.cancel();
156
- textToSpeech(text, Announcer.lang, Announcer.voice);
163
+ textToSpeech(text, Announcer.aria, Announcer.lang, Announcer.voice);
157
164
  }
158
165
 
159
166
  if (notification) {
@@ -12,6 +12,11 @@ export interface SeriesResult {
12
12
  cancel: () => void;
13
13
  }
14
14
 
15
+ // Aria label
16
+ type AriaLabel = { text: string; lang: string };
17
+ const ARIA_PARENT_ID = 'aria-parent';
18
+ let ariaLabelPhrases: AriaLabel[] = [];
19
+
15
20
  /* global SpeechSynthesisErrorEvent */
16
21
  function flattenStrings(series: SpeechType[] = []): SpeechType[] {
17
22
  const flattenedSeries = [];
@@ -40,6 +45,82 @@ function delay(pause: number) {
40
45
  });
41
46
  }
42
47
 
48
+ /**
49
+ * @description This function is called at the end of the speak series
50
+ * @param Phrase is an object containing the text and the language
51
+ */
52
+ function addChildrenToAriaDiv(phrase: AriaLabel) {
53
+ if (phrase?.text?.trim().length === 0) return;
54
+ ariaLabelPhrases.push(phrase);
55
+ }
56
+
57
+ /**
58
+ * @description This function is triggered finally when the speak series is finished and we are to speak the aria labels
59
+ */
60
+ function focusElementForAria() {
61
+ const element = createAriaElement();
62
+
63
+ if (!element) {
64
+ console.error(`ARIA div not found: ${ARIA_PARENT_ID}`);
65
+ return;
66
+ }
67
+
68
+ for (const object of ariaLabelPhrases) {
69
+ const span = document.createElement('span');
70
+
71
+ // TODO: Not sure LG or Samsung support lang attribute on span or switching language
72
+ span.setAttribute('lang', object.lang);
73
+ span.setAttribute('aria-label', object.text);
74
+ element.appendChild(span);
75
+ }
76
+
77
+ // Cleanup
78
+ setTimeout(() => {
79
+ ariaLabelPhrases = [];
80
+ cleanAriaLabelParent();
81
+ focusCanvas();
82
+ }, 100);
83
+ }
84
+
85
+ /**
86
+ * @description Clean the aria label parent after speaking
87
+ */
88
+ function cleanAriaLabelParent(): void {
89
+ const parentTag = document.getElementById(ARIA_PARENT_ID);
90
+ if (parentTag) {
91
+ while (parentTag.firstChild) {
92
+ parentTag.removeChild(parentTag.firstChild);
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * @description Focus the canvas element
99
+ */
100
+ function focusCanvas(): void {
101
+ const canvas = document.getElementById('app')?.firstChild as HTMLElement;
102
+ canvas?.focus();
103
+ }
104
+
105
+ /**
106
+ * @description Create the aria element in the DOM if it doesn't exist
107
+ * @private For xbox, we may need to create a different element each time we wanna use aria
108
+ */
109
+ function createAriaElement(): HTMLDivElement | HTMLElement {
110
+ const aria_container = document.getElementById(ARIA_PARENT_ID);
111
+
112
+ if (!aria_container) {
113
+ const element = document.createElement('div');
114
+ element.setAttribute('id', ARIA_PARENT_ID);
115
+ element.setAttribute('aria-live', 'assertive');
116
+ element.setAttribute('tabindex', '0');
117
+ document.body.appendChild(element);
118
+ return element;
119
+ }
120
+
121
+ return aria_container;
122
+ }
123
+
43
124
  /**
44
125
  * Speak a string
45
126
  *
@@ -82,6 +163,7 @@ function speak(
82
163
 
83
164
  function speakSeries(
84
165
  series: SpeechType,
166
+ aria: boolean,
85
167
  lang: string,
86
168
  voice?: string,
87
169
  root = true,
@@ -118,7 +200,8 @@ function speakSeries(
118
200
 
119
201
  while (active && retriesLeft > 0) {
120
202
  try {
121
- await speak(phrase, utterances, lang, voice);
203
+ if (aria) addChildrenToAriaDiv({ text: phrase, lang });
204
+ else await speak(phrase, utterances, lang, voice);
122
205
  retriesLeft = 0; // Exit retry loop on success
123
206
  } catch (e) {
124
207
  if (e instanceof SpeechSynthesisErrorEvent) {
@@ -152,8 +235,12 @@ function speakSeries(
152
235
 
153
236
  while (active && retriesLeft > 0) {
154
237
  try {
155
- await speak(text, utterances, objectLang, objectVoice?.name);
156
- retriesLeft = 0; // Exit retry loop on success
238
+ if (text) {
239
+ if (aria) addChildrenToAriaDiv({ text, lang: objectLang });
240
+ else
241
+ await speak(text, utterances, objectLang, objectVoice?.name);
242
+ retriesLeft = 0; // Exit retry loop on success
243
+ }
157
244
  } catch (e) {
158
245
  if (e instanceof SpeechSynthesisErrorEvent) {
159
246
  if (e.error === 'network') {
@@ -178,18 +265,22 @@ function speakSeries(
178
265
  }
179
266
  } else if (typeof phrase === 'function') {
180
267
  // Handle functions
181
- const seriesResult = speakSeries(phrase(), lang, voice, false);
268
+ const seriesResult = speakSeries(phrase(), aria, lang, voice, false);
182
269
  nestedSeriesResults.push(seriesResult);
183
270
  await seriesResult.series;
184
271
  } else if (Array.isArray(phrase)) {
185
272
  // Handle nested arrays
186
- const seriesResult = speakSeries(phrase, lang, voice, false);
273
+ const seriesResult = speakSeries(phrase, aria, lang, voice, false);
187
274
  nestedSeriesResults.push(seriesResult);
188
275
  await seriesResult.series;
189
276
  }
190
277
  }
191
278
  } finally {
192
279
  active = false;
280
+ // Call completion logic only for the original (root) series
281
+ if (root && aria) {
282
+ focusElementForAria();
283
+ }
193
284
  }
194
285
  })();
195
286
 
@@ -205,7 +296,21 @@ function speakSeries(
205
296
  if (!active) {
206
297
  return;
207
298
  }
299
+
208
300
  if (root) {
301
+ if (aria) {
302
+ const element = createAriaElement();
303
+
304
+ if (element) {
305
+ ariaLabelPhrases = [];
306
+ cleanAriaLabelParent();
307
+ element.focus();
308
+ focusCanvas();
309
+ }
310
+
311
+ return;
312
+ }
313
+
209
314
  synth.cancel(); // Cancel all ongoing speech
210
315
  }
211
316
  nestedSeriesResults.forEach((nestedSeriesResult) => {
@@ -215,13 +320,15 @@ function speakSeries(
215
320
  },
216
321
  };
217
322
  }
323
+
218
324
  let currentSeries: SeriesResult | undefined;
219
325
  export default function (
220
326
  toSpeak: SpeechType,
327
+ aria: boolean,
221
328
  lang: string = 'en-US',
222
329
  voice?: string,
223
330
  ) {
224
331
  currentSeries && currentSeries.cancel();
225
- currentSeries = speakSeries(toSpeak, lang, voice);
332
+ currentSeries = speakSeries(toSpeak, aria, lang, voice);
226
333
  return currentSeries;
227
334
  }
@@ -17,7 +17,7 @@
17
17
  * - `restoreFocus()`: Restores focus to the last stored element and removes it from the stack. Returns `true` if successful, `false` otherwise.
18
18
  * - `clearFocusStack()`: Empties the focus stack.
19
19
  */
20
- import { createSignal, createContext, useContext, JSX } from 'solid-js';
20
+ import * as s from 'solid-js';
21
21
  import { type ElementNode } from '@lightningtv/solid';
22
22
 
23
23
  interface FocusStackContextType {
@@ -26,13 +26,16 @@ interface FocusStackContextType {
26
26
  clearFocusStack: () => void;
27
27
  }
28
28
 
29
- const FocusStackContext = createContext<FocusStackContextType | undefined>(undefined);
29
+ const FocusStackContext = s.createContext<FocusStackContextType | undefined>(undefined);
30
30
 
31
- export function FocusStackProvider(props: { children: JSX.Element}) {
32
- const [_focusStack, setFocusStack] = createSignal<ElementNode[]>([]);
31
+ export function FocusStackProvider(props: { children: s.JSX.Element}) {
32
+ const [_focusStack, setFocusStack] = s.createSignal<ElementNode[]>([]);
33
33
 
34
34
  function storeFocus(element: ElementNode, prevElement?: ElementNode) {
35
- setFocusStack((stack) => [...stack, prevElement || element]);
35
+ const elm = prevElement || element;
36
+ if (elm) {
37
+ setFocusStack(stack => [...stack, elm]);
38
+ }
36
39
  }
37
40
 
38
41
  function restoreFocus(): boolean {
@@ -59,10 +62,18 @@ export function FocusStackProvider(props: { children: JSX.Element}) {
59
62
  );
60
63
  }
61
64
 
62
- export function useFocusStack() {
63
- const context = useContext(FocusStackContext);
65
+ export function useFocusStack(autoClear = true) {
66
+ const context = s.useContext(FocusStackContext);
64
67
  if (!context) {
65
68
  throw new Error("useFocusStack must be used within a FocusStackProvider");
66
69
  }
70
+
71
+ if (autoClear) {
72
+ s.onCleanup(() => {
73
+ // delay clearing the focus stack so restoreFocus can happen first.
74
+ setTimeout(() => context.clearFocusStack(), 5);
75
+ });
76
+ }
77
+
67
78
  return context;
68
79
  }
@@ -0,0 +1,33 @@
1
+ import * as s from 'solid-js'
2
+ import * as lng from '@lightningtv/solid'
3
+
4
+ interface Destroyable {
5
+ (props: lng.NodeProps): s.JSX.Element;
6
+ destroy: () => void;
7
+ }
8
+
9
+ export function createTag(children: s.JSX.Element): Destroyable {
10
+ const [texture, setTexture] = s.createSignal<lng.Texture | null | undefined>(null);
11
+ const Tag = <view
12
+ x={lng.rootNode.w - 1}
13
+ y={lng.rootNode.h - 1}
14
+ display='flex'
15
+ onLayout={(n) => {
16
+ if (n.preFlexwidth && n.width !== n.preFlexwidth) {
17
+ n.rtt = true;
18
+ setTimeout(() => setTexture(n.texture), 1);
19
+ }
20
+ }}
21
+ parent={lng.rootNode} children={children}
22
+ textureOptions={{
23
+ preventCleanup: true
24
+ }} /> as any as lng.ElementNode
25
+ Tag.render(false);
26
+
27
+ const TagComponent = (props: lng.NodeProps) => {
28
+ return <view color={0xffffffff} autosize {...props} texture={texture()} />;
29
+ };
30
+ TagComponent.destroy = () => Tag.destroy();
31
+
32
+ return TagComponent;
33
+ }
@@ -4,6 +4,8 @@ export * from './createInfiniteItems.js';
4
4
  export * from './useMouse.js';
5
5
  export * from './portal.jsx';
6
6
  export * from './Lazy.jsx';
7
+ export * from './LazyImport.js';
8
+ export * from './Image.jsx';
7
9
  export * from './Visible.jsx';
8
10
  export * from './router.js';
9
11
  export * from './Column.jsx';
@@ -11,17 +13,24 @@ export * from './Row.jsx';
11
13
  export * from './Grid.jsx';
12
14
  export * from './FPSCounter.jsx';
13
15
  export * from './FadeInOut.jsx';
14
- export * from './createFocusStack.jsx';
16
+ export * from './Preserve.jsx';
17
+ export * from './Suspense.jsx';
15
18
  export * from './Marquee.jsx';
19
+ export * from './createFocusStack.jsx';
16
20
  export * from './useHold.js';
17
- export { withScrolling } from './utils/withScrolling.js';
21
+ export * from './KeepAlive.jsx';
22
+ export * from './VirtualGrid.jsx';
23
+ export * from './Virtual.jsx';
24
+ export * from './utils/withScrolling.js';
25
+ export * from './createTag.jsx';
18
26
  export {
19
27
  type AnyFunction,
20
28
  chainFunctions,
21
29
  chainRefs,
22
30
  } from './utils/chainFunctions.js';
23
- export { handleNavigation, onGridFocus } from './utils/handleNavigation.js';
31
+ export * from './utils/handleNavigation.js';
24
32
  export { createSpriteMap, type SpriteDef } from './utils/createSpriteMap.js';
33
+ export { createBlurredImage } from './utils/createBlurredImage.js';
25
34
 
26
35
  export type * from './types.js';
27
- export type { KeyHandler } from '@lightningtv/core/focusManager';
36
+ export type { KeyHandler } from '../core/focusManager.js';
@@ -1,5 +1,5 @@
1
- import type { ElementNode, NodeProps, NodeStyles } from '@lightningtv/solid';
2
- import type { KeyHandler } from '@lightningtv/core/focusManager';
1
+ import type { ElementNode, NodeProps, NodeStyles } from '../index.js';
2
+ import type { KeyHandler } from '../core/focusManager.js';
3
3
 
4
4
  export type OnSelectedChanged = (
5
5
  this: NavigableElement,
@@ -54,6 +54,7 @@ export interface NavigableProps extends NodeProps {
54
54
  // @ts-expect-error animationSettings is not identical - weird
55
55
  export interface NavigableElement extends ElementNode, NavigableProps {
56
56
  selected: number;
57
+ scrollToIndex: (this: NavigableElement, index: number) => void;
57
58
  }
58
59
 
59
60
  export interface NavigableStyleProperties {
@@ -6,13 +6,13 @@ import {
6
6
  getOwner,
7
7
  runWithOwner,
8
8
  } from 'solid-js';
9
- import { Config } from '@lightningtv/core';
10
- import type { ElementNode } from '@lightningtv/core';
9
+ import { Config } from '../core/index.js';
10
+ import type { ElementNode } from '../core/index.js';
11
11
  import {
12
12
  useFocusManager as useFocusManagerCore,
13
13
  type KeyMap,
14
14
  type KeyHoldOptions,
15
- } from '@lightningtv/core/focusManager';
15
+ } from '../core/focusManager.js';
16
16
  import { activeElement, setActiveElement } from '../activeElement.js';
17
17
 
18
18
  const [focusPath, setFocusPath] = createSignal<ElementNode[]>([]);