@lightningtv/solid 3.0.0-2 → 3.0.0-21

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 (206) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +6 -0
  3. package/dist/src/activeElement.d.ts +1 -1
  4. package/dist/src/core/animation.d.ts +35 -0
  5. package/dist/src/core/animation.js +120 -0
  6. package/dist/src/core/animation.js.map +1 -0
  7. package/dist/src/core/config.d.ts +47 -0
  8. package/dist/src/core/config.js +23 -0
  9. package/dist/src/core/config.js.map +1 -0
  10. package/dist/src/core/domRenderer.d.ts +117 -0
  11. package/dist/src/core/domRenderer.js +1160 -0
  12. package/dist/src/core/domRenderer.js.map +1 -0
  13. package/dist/src/core/elementNode.d.ts +209 -0
  14. package/dist/src/core/elementNode.js +829 -0
  15. package/dist/src/core/elementNode.js.map +1 -0
  16. package/dist/src/core/flex.d.ts +2 -0
  17. package/dist/src/core/flex.js +243 -0
  18. package/dist/src/core/flex.js.map +1 -0
  19. package/dist/src/core/focusKeyTypes.d.ts +42 -0
  20. package/dist/src/core/focusKeyTypes.js +2 -0
  21. package/dist/src/core/focusKeyTypes.js.map +1 -0
  22. package/dist/src/core/focusManager.d.ts +13 -0
  23. package/dist/src/core/focusManager.js +269 -0
  24. package/dist/src/core/focusManager.js.map +1 -0
  25. package/dist/src/core/index.d.ts +12 -0
  26. package/dist/src/core/index.js +12 -0
  27. package/dist/src/core/index.js.map +1 -0
  28. package/dist/src/core/intrinsicTypes.d.ts +90 -0
  29. package/dist/src/core/intrinsicTypes.js +2 -0
  30. package/dist/src/core/intrinsicTypes.js.map +1 -0
  31. package/dist/src/core/lightningInit.d.ts +89 -0
  32. package/dist/src/core/lightningInit.js +26 -0
  33. package/dist/src/core/lightningInit.js.map +1 -0
  34. package/dist/src/core/nodeTypes.d.ts +6 -0
  35. package/dist/src/core/nodeTypes.js +6 -0
  36. package/dist/src/core/nodeTypes.js.map +1 -0
  37. package/dist/src/core/shaders.d.ts +51 -0
  38. package/dist/src/core/shaders.js +446 -0
  39. package/dist/src/core/shaders.js.map +1 -0
  40. package/dist/src/core/states.d.ts +12 -0
  41. package/dist/src/core/states.js +84 -0
  42. package/dist/src/core/states.js.map +1 -0
  43. package/dist/src/core/timings.d.ts +36 -0
  44. package/dist/src/core/timings.js +199 -0
  45. package/dist/src/core/timings.js.map +1 -0
  46. package/dist/src/core/utils.d.ts +39 -0
  47. package/dist/src/core/utils.js +164 -0
  48. package/dist/src/core/utils.js.map +1 -0
  49. package/dist/src/devtools/index.d.ts +1 -1
  50. package/dist/src/devtools/index.js +1 -1
  51. package/dist/src/devtools/index.js.map +1 -1
  52. package/dist/src/index.d.ts +3 -3
  53. package/dist/src/index.js +1 -1
  54. package/dist/src/index.js.map +1 -1
  55. package/dist/src/jsx-runtime.d.ts +1 -3
  56. package/dist/src/primitives/Column.jsx +9 -10
  57. package/dist/src/primitives/Column.jsx.map +1 -1
  58. package/dist/src/primitives/FPSCounter.jsx +14 -1
  59. package/dist/src/primitives/FPSCounter.jsx.map +1 -1
  60. package/dist/src/primitives/Grid.d.ts +15 -6
  61. package/dist/src/primitives/Grid.jsx +35 -22
  62. package/dist/src/primitives/Grid.jsx.map +1 -1
  63. package/dist/src/primitives/Image.d.ts +8 -0
  64. package/dist/src/primitives/Image.jsx +24 -0
  65. package/dist/src/primitives/Image.jsx.map +1 -0
  66. package/dist/src/primitives/KeepAlive.d.ts +30 -0
  67. package/dist/src/primitives/KeepAlive.jsx +77 -0
  68. package/dist/src/primitives/KeepAlive.jsx.map +1 -0
  69. package/dist/src/primitives/Lazy.d.ts +8 -7
  70. package/dist/src/primitives/Lazy.jsx +52 -23
  71. package/dist/src/primitives/Lazy.jsx.map +1 -1
  72. package/dist/src/primitives/Marquee.d.ts +64 -0
  73. package/dist/src/primitives/Marquee.jsx +86 -0
  74. package/dist/src/primitives/Marquee.jsx.map +1 -0
  75. package/dist/src/primitives/Preserve.d.ts +4 -0
  76. package/dist/src/primitives/Preserve.jsx +11 -0
  77. package/dist/src/primitives/Preserve.jsx.map +1 -0
  78. package/dist/src/primitives/Row.jsx +9 -10
  79. package/dist/src/primitives/Row.jsx.map +1 -1
  80. package/dist/src/primitives/Suspense.d.ts +22 -0
  81. package/dist/src/primitives/Suspense.jsx +33 -0
  82. package/dist/src/primitives/Suspense.jsx.map +1 -0
  83. package/dist/src/primitives/Virtual.d.ts +18 -0
  84. package/dist/src/primitives/Virtual.jsx +434 -0
  85. package/dist/src/primitives/Virtual.jsx.map +1 -0
  86. package/dist/src/primitives/VirtualGrid.d.ts +13 -0
  87. package/dist/src/primitives/VirtualGrid.jsx +160 -0
  88. package/dist/src/primitives/VirtualGrid.jsx.map +1 -0
  89. package/dist/src/primitives/VirtualList.d.ts +11 -0
  90. package/dist/src/primitives/VirtualList.jsx +96 -0
  91. package/dist/src/primitives/VirtualList.jsx.map +1 -0
  92. package/dist/src/primitives/VirtualRow.d.ts +13 -0
  93. package/dist/src/primitives/VirtualRow.jsx +97 -0
  94. package/dist/src/primitives/VirtualRow.jsx.map +1 -0
  95. package/dist/src/primitives/Visible.d.ts +0 -1
  96. package/dist/src/primitives/Visible.jsx +1 -1
  97. package/dist/src/primitives/Visible.jsx.map +1 -1
  98. package/dist/src/primitives/announcer/announcer.d.ts +2 -0
  99. package/dist/src/primitives/announcer/announcer.js +7 -5
  100. package/dist/src/primitives/announcer/announcer.js.map +1 -1
  101. package/dist/src/primitives/announcer/index.d.ts +5 -1
  102. package/dist/src/primitives/announcer/index.js +8 -2
  103. package/dist/src/primitives/announcer/index.js.map +1 -1
  104. package/dist/src/primitives/announcer/speech.d.ts +2 -2
  105. package/dist/src/primitives/announcer/speech.js +157 -28
  106. package/dist/src/primitives/announcer/speech.js.map +1 -1
  107. package/dist/src/primitives/createFocusStack.d.ts +4 -4
  108. package/dist/src/primitives/createFocusStack.jsx +15 -6
  109. package/dist/src/primitives/createFocusStack.jsx.map +1 -1
  110. package/dist/src/primitives/createTag.d.ts +8 -0
  111. package/dist/src/primitives/createTag.jsx +20 -0
  112. package/dist/src/primitives/createTag.jsx.map +1 -0
  113. package/dist/src/primitives/index.d.ts +14 -4
  114. package/dist/src/primitives/index.js +13 -3
  115. package/dist/src/primitives/index.js.map +1 -1
  116. package/dist/src/primitives/types.d.ts +5 -2
  117. package/dist/src/primitives/useFocusManager.d.ts +2 -2
  118. package/dist/src/primitives/useFocusManager.js +2 -2
  119. package/dist/src/primitives/useFocusManager.js.map +1 -1
  120. package/dist/src/primitives/useHold.d.ts +27 -0
  121. package/dist/src/primitives/useHold.js +54 -0
  122. package/dist/src/primitives/useHold.js.map +1 -0
  123. package/dist/src/primitives/useMouse.d.ts +18 -2
  124. package/dist/src/primitives/useMouse.js +171 -47
  125. package/dist/src/primitives/useMouse.js.map +1 -1
  126. package/dist/src/primitives/utils/chainFunctions.d.ts +30 -4
  127. package/dist/src/primitives/utils/chainFunctions.js +14 -3
  128. package/dist/src/primitives/utils/chainFunctions.js.map +1 -1
  129. package/dist/src/primitives/utils/createBlurredImage.d.ts +56 -0
  130. package/dist/src/primitives/utils/createBlurredImage.js +223 -0
  131. package/dist/src/primitives/utils/createBlurredImage.js.map +1 -0
  132. package/dist/src/primitives/utils/createSpriteMap.d.ts +2 -2
  133. package/dist/src/primitives/utils/createSpriteMap.js +1 -1
  134. package/dist/src/primitives/utils/createSpriteMap.js.map +1 -1
  135. package/dist/src/primitives/utils/handleNavigation.d.ts +79 -5
  136. package/dist/src/primitives/utils/handleNavigation.js +242 -69
  137. package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
  138. package/dist/src/primitives/utils/withScrolling.d.ts +14 -2
  139. package/dist/src/primitives/utils/withScrolling.js +66 -7
  140. package/dist/src/primitives/utils/withScrolling.js.map +1 -1
  141. package/dist/src/render.d.ts +8 -7
  142. package/dist/src/render.js +5 -1
  143. package/dist/src/render.js.map +1 -1
  144. package/dist/src/solidOpts.d.ts +1 -7
  145. package/dist/src/solidOpts.js +32 -16
  146. package/dist/src/solidOpts.js.map +1 -1
  147. package/dist/src/types.d.ts +1 -13
  148. package/dist/src/universal.d.ts +25 -0
  149. package/dist/src/universal.js +232 -0
  150. package/dist/src/universal.js.map +1 -0
  151. package/dist/src/utils.d.ts +3 -1
  152. package/dist/src/utils.js +9 -1
  153. package/dist/src/utils.js.map +1 -1
  154. package/dist/tsconfig.tsbuildinfo +1 -1
  155. package/jsx-runtime.d.ts +2 -4
  156. package/package.json +17 -15
  157. package/src/activeElement.ts +1 -1
  158. package/src/core/animation.ts +183 -0
  159. package/src/core/config.ts +77 -0
  160. package/src/core/domRenderer.ts +1308 -0
  161. package/src/core/elementNode.ts +1198 -0
  162. package/src/core/flex.ts +284 -0
  163. package/src/core/focusKeyTypes.ts +87 -0
  164. package/src/core/focusManager.ts +359 -0
  165. package/src/core/index.ts +13 -0
  166. package/src/core/intrinsicTypes.ts +199 -0
  167. package/src/core/lightningInit.ts +147 -0
  168. package/src/core/nodeTypes.ts +6 -0
  169. package/src/core/shaders.ts +567 -0
  170. package/src/core/states.ts +91 -0
  171. package/src/core/timings.ts +261 -0
  172. package/src/core/utils.ts +222 -0
  173. package/src/devtools/index.ts +1 -1
  174. package/src/index.ts +3 -3
  175. package/src/primitives/Column.tsx +10 -12
  176. package/src/primitives/FPSCounter.tsx +15 -1
  177. package/src/primitives/Grid.tsx +57 -33
  178. package/src/primitives/Image.tsx +36 -0
  179. package/src/primitives/KeepAlive.tsx +124 -0
  180. package/src/primitives/Lazy.tsx +66 -37
  181. package/src/primitives/Marquee.tsx +149 -0
  182. package/src/primitives/Preserve.tsx +18 -0
  183. package/src/primitives/Row.tsx +13 -14
  184. package/src/primitives/Suspense.tsx +39 -0
  185. package/src/primitives/Virtual.tsx +478 -0
  186. package/src/primitives/VirtualGrid.tsx +220 -0
  187. package/src/primitives/Visible.tsx +1 -2
  188. package/src/primitives/announcer/announcer.ts +16 -10
  189. package/src/primitives/announcer/index.ts +12 -2
  190. package/src/primitives/announcer/speech.ts +188 -27
  191. package/src/primitives/createFocusStack.tsx +18 -7
  192. package/src/primitives/createTag.tsx +31 -0
  193. package/src/primitives/index.ts +18 -4
  194. package/src/primitives/types.ts +12 -2
  195. package/src/primitives/useFocusManager.ts +3 -3
  196. package/src/primitives/useHold.ts +69 -0
  197. package/src/primitives/useMouse.ts +306 -67
  198. package/src/primitives/utils/chainFunctions.ts +40 -9
  199. package/src/primitives/utils/createBlurredImage.ts +366 -0
  200. package/src/primitives/utils/createSpriteMap.ts +6 -4
  201. package/src/primitives/utils/handleNavigation.ts +300 -84
  202. package/src/primitives/utils/withScrolling.ts +91 -18
  203. package/src/render.ts +10 -8
  204. package/src/solidOpts.ts +31 -24
  205. package/src/types.ts +1 -15
  206. package/src/utils.ts +11 -1
@@ -1,101 +1,317 @@
1
- import { ElementNode, assertTruthy, Config } from '@lightningtv/core';
2
- import { type KeyHandler } from '@lightningtv/core/focusManager';
3
- import type { NavigableElement, OnSelectedChanged } from '../types.js';
4
-
5
- export function onGridFocus(onSelectedChanged: OnSelectedChanged | undefined) {
6
- return function (this: ElementNode) {
7
- if (!this || this.children.length === 0) return false;
8
-
9
- this.selected = this.selected || 0;
10
- let child = this.selected
11
- ? this.children[this.selected]
12
- : this.selectedNode;
13
-
14
- while (child?.skipFocus) {
15
- this.selected++;
16
- child = this.children[this.selected];
1
+ import * as s from 'solid-js';
2
+ import * as lng from '../../index.js';
3
+ import * as lngp from '../index.js';
4
+
5
+ function idxInArray(idx: number, arr: readonly any[]): boolean {
6
+ return idx === 0 || (idx >= 0 && idx < arr.length);
7
+ }
8
+
9
+ function findFirstFocusableChildIdx(
10
+ el: lngp.NavigableElement,
11
+ from = 0,
12
+ delta = 1,
13
+ ): number {
14
+ for (let i = from; ; i += delta) {
15
+ if (!idxInArray(i, el.children)) {
16
+ if (el.wrap) {
17
+ i = (i + el.children.length) % el.children.length;
18
+ } else break;
19
+ }
20
+ if (!el.children[i]?.skipFocus) {
21
+ return i;
17
22
  }
18
- if (!(child instanceof ElementNode)) return false;
23
+ }
24
+ return -1;
25
+ }
26
+
27
+ function selectChild(el: lngp.NavigableElement, index: number): boolean {
28
+ const child = el.children[index];
29
+
30
+ if (child == null || child.skipFocus) {
31
+ el.selected = -1;
32
+ return false;
33
+ }
34
+
35
+ const lastSelected = el.selected;
36
+ el.selected = index;
37
+
38
+ if (!lng.isFocused(child)) {
19
39
  child.setFocus();
40
+ }
20
41
 
21
- if (onSelectedChanged) {
22
- const grid = this as NavigableElement;
23
- onSelectedChanged.call(grid, grid.selected, grid, child);
24
- }
25
- return true;
42
+ // Always call onSelectedChanged on first focus for clients
43
+ el.onSelectedChanged?.(index, el, child as lng.ElementNode, lastSelected);
44
+
45
+ return true;
46
+ }
47
+
48
+ /** @deprecated Use {@link navigableForwardFocus} instead */
49
+ export function onGridFocus(
50
+ _?: lngp.OnSelectedChanged,
51
+ ): lng.ForwardFocusHandler {
52
+ return function () {
53
+ return navigableForwardFocus.call(this, this);
26
54
  };
27
55
  }
28
56
 
57
+ /**
58
+ * Forwards focus to the first focusable child of a {@link lngp.NavigableElement} and
59
+ * selects it.
60
+ *
61
+ * @example
62
+ * ```tsx
63
+ * <view
64
+ * selected={0}
65
+ * forwardFocus={navigableForwardFocus}
66
+ * onSelectedChanged={(idx, el, child, lastIdx) => {...}}
67
+ * >
68
+ * ```
69
+ */
70
+ export const navigableForwardFocus: lng.ForwardFocusHandler = function () {
71
+ const navigable = this as lngp.NavigableElement;
72
+
73
+ let selected = navigable.selected;
74
+
75
+ if (selected !== 0) {
76
+ selected = lng.clamp(selected, 0, this.children.length - 1);
77
+ while (!idxInArray(selected, this.children)) {
78
+ selected--;
79
+ }
80
+ }
81
+
82
+ selected = findFirstFocusableChildIdx(navigable, selected);
83
+ // update selected as firstfocusable maybe different if first element has skipFocus
84
+ navigable.selected = selected;
85
+ return selectChild(navigable, selected);
86
+ };
87
+
29
88
  export function handleNavigation(
30
89
  direction: 'up' | 'right' | 'down' | 'left',
31
- ): KeyHandler {
90
+ ): lng.KeyHandler {
32
91
  return function () {
33
- const numChildren = this.children.length;
34
- const wrap = this.wrap;
35
- const lastSelected = this.selected || 0;
92
+ return moveSelection(
93
+ this as lngp.NavigableElement,
94
+ direction === 'up' || direction === 'left' ? -1 : 1,
95
+ );
96
+ };
97
+ }
36
98
 
37
- if (numChildren === 0) {
99
+ /**
100
+ * Handles navigation key events for navigable elements, \
101
+ * such as {@link lngp.Row} and {@link lngp.Column}.
102
+ *
103
+ * Uses {@link moveSelection} to select the next or previous child based on the key pressed.
104
+ *
105
+ * @example
106
+ * ```tsx
107
+ * <view
108
+ * selected={0}
109
+ * onUp={navigableHandleNavigation}
110
+ * onDown={navigableHandleNavigation}
111
+ * onSelectedChanged={(idx, el, child, lastIdx) => {...}}
112
+ * >
113
+ * ```
114
+ */
115
+ export const navigableHandleNavigation: lng.KeyHandler = function (e) {
116
+ return moveSelection(
117
+ this as lngp.NavigableElement,
118
+ e.key === 'ArrowUp' || e.key === 'ArrowLeft' ? -1 : 1,
119
+ );
120
+ };
121
+
122
+ /**
123
+ * Moves the selection within a {@link lngp.NavigableElement}.
124
+ */
125
+ export function moveSelection(
126
+ el: lngp.NavigableElement,
127
+ delta: number,
128
+ ): boolean {
129
+ let selected = findFirstFocusableChildIdx(el, el.selected + delta, delta);
130
+
131
+ if (selected === -1) {
132
+ if (
133
+ !idxInArray(el.selected, el.children) ||
134
+ el.children[el.selected]?.skipFocus ||
135
+ lng.isFocused(el.children[el.selected]!)
136
+ ) {
38
137
  return false;
39
138
  }
139
+ selected = el.selected;
140
+ }
40
141
 
41
- if (direction === 'right' || direction === 'down') {
42
- do {
43
- this.selected = ((this.selected || 0) % numChildren) + 1;
44
- if (this.selected >= numChildren) {
45
- if (!wrap) {
46
- this.selected = -1;
47
- break;
48
- }
49
- this.selected = 0;
50
- }
51
- } while (this.children[this.selected]?.skipFocus);
52
- } else if (direction === 'left' || direction === 'up') {
53
- do {
54
- this.selected = ((this.selected || 0) % numChildren) - 1;
55
- if (this.selected < 0) {
56
- if (!wrap) {
57
- this.selected = -1;
58
- break;
59
- }
60
- this.selected = numChildren - 1;
61
- }
62
- } while (this.children[this.selected]?.skipFocus);
63
- }
142
+ const active = el.children[selected]!;
64
143
 
65
- if (this.selected === -1) {
66
- this.selected = lastSelected;
67
- if (
68
- this.children[this.selected]?.states!.has(
69
- Config.focusStateKey || '$focus',
70
- )
71
- ) {
72
- // This child is already focused, so bubble up to next handler
73
- return false;
144
+ if (el.plinko) {
145
+ // Set the next item to have the same selected index
146
+ // so we move up / down directly
147
+ const lastSelectedChild = el.children[el.selected];
148
+ lng.assertTruthy(lastSelectedChild instanceof lng.ElementNode);
149
+
150
+ const num = lastSelectedChild.selected || 0;
151
+ active.selected =
152
+ num < active.children.length ? num : active.children.length - 1;
153
+ }
154
+
155
+ return selectChild(el, selected);
156
+ }
157
+
158
+ function distanceBetweenRectCenters(a: lng.Rect, b: lng.Rect): number {
159
+ const dx = Math.abs(a.x + a.width / 2 - (b.x + b.width / 2)) / 2;
160
+ const dy = Math.abs(a.y + a.height / 2 - (b.y + b.height / 2)) / 2;
161
+ return Math.sqrt(dx * dx + dy * dy);
162
+ }
163
+
164
+ function findClosestFocusableChildIdx(
165
+ el: lng.ElementNode,
166
+ prevEl: lng.ElementNode,
167
+ ): number {
168
+ // select child closest to the previous active element
169
+ const prevRect = lng.getElementScreenRect(prevEl);
170
+ const elRect = lng.getElementScreenRect(el);
171
+ const childRect: lng.Rect = { x: 0, y: 0, width: 0, height: 0 };
172
+
173
+ let closestIdx = -1;
174
+ let closestDist = Infinity;
175
+
176
+ for (const [idx, child] of el.children.entries()) {
177
+ if (!child.skipFocus) {
178
+ lng.getElementScreenRect(child, el, childRect);
179
+ childRect.x += elRect.x;
180
+ childRect.y += elRect.y;
181
+ const distance = distanceBetweenRectCenters(prevRect, childRect);
182
+ if (distance < closestDist) {
183
+ closestDist = distance;
184
+ closestIdx = idx;
74
185
  }
75
186
  }
76
- const active = this.children[this.selected || 0];
77
- assertTruthy(active instanceof ElementNode);
78
- const navigableThis = this as NavigableElement;
79
-
80
- navigableThis.onSelectedChanged &&
81
- navigableThis.onSelectedChanged.call(
82
- navigableThis,
83
- navigableThis.selected,
84
- navigableThis,
85
- active,
86
- lastSelected,
87
- );
88
-
89
- if (this.plinko) {
90
- // Set the next item to have the same selected index
91
- // so we move up / down directly
92
- const lastSelectedChild = this.children[lastSelected];
93
- assertTruthy(lastSelectedChild instanceof ElementNode);
94
- const num = lastSelectedChild.selected || 0;
95
- active.selected =
96
- num < active.children.length ? num : active.children.length - 1;
97
- }
98
- active.setFocus();
99
- return true;
100
- };
187
+ }
188
+
189
+ return closestIdx;
101
190
  }
191
+
192
+ /**
193
+ * Forwards focus to the closest or first focusable child of a {@link lngp.NavigableElement} and
194
+ * selects it.
195
+ *
196
+ * To determine the closest child, it uses the distance between the center of the previous focused element
197
+ * and the center of each child element.
198
+ *
199
+ * @example
200
+ * ```tsx
201
+ * <view
202
+ * selected={0}
203
+ * forwardFocus={spatialForwardFocus}
204
+ * onSelectedChanged={(idx, el, child, lastIdx) => {...}}
205
+ * >
206
+ * ```
207
+ */
208
+ export const spatialForwardFocus: lng.ForwardFocusHandler = function () {
209
+ const prevEl = s.untrack(lng.activeElement);
210
+ if (prevEl) {
211
+ const idx = findClosestFocusableChildIdx(this, prevEl);
212
+ const selected = selectChild(this as lngp.NavigableElement, idx);
213
+ if (selected) return true;
214
+ }
215
+ const idx = findFirstFocusableChildIdx(this as lngp.NavigableElement);
216
+ return selectChild(this as lngp.NavigableElement, idx);
217
+ };
218
+
219
+ /**
220
+ * Handles spatial navigation within a {@link lngp.NavigableElement} by moving focus
221
+ * based on the arrow keys pressed.
222
+ *
223
+ * This function allows for navigation in a grid-like manner for flex-wrap containers, \
224
+ * where pressing the arrow keys will either:
225
+ * - move focus to the next/prev child in the same row/column
226
+ * - or find the closest child in the next/prev row/column.
227
+ *
228
+ * @example
229
+ * ```tsx
230
+ * <view
231
+ * selected={0}
232
+ * display="flex"
233
+ * flexWrap="wrap"
234
+ * onUp={spatialHandleNavigation}
235
+ * onDown={spatialHandleNavigation}
236
+ * onSelectedChanged={(idx, el, child, lastIdx) => {...}}
237
+ * >
238
+ * ```
239
+ */
240
+ export const spatialHandleNavigation: lng.KeyHandler = function (e) {
241
+ let selected = this.selected;
242
+
243
+ if (typeof selected !== 'number' || !idxInArray(selected, this.children)) {
244
+ selected = findFirstFocusableChildIdx(this as lngp.NavigableElement);
245
+ return selectChild(this as lngp.NavigableElement, selected);
246
+ }
247
+
248
+ const prevChild = this.children[selected]!;
249
+
250
+ const move = { x: 0, y: 0 };
251
+ switch (e.key) {
252
+ case 'ArrowLeft':
253
+ move.x = -1;
254
+ break;
255
+ case 'ArrowRight':
256
+ move.x = 1;
257
+ break;
258
+ case 'ArrowUp':
259
+ move.y = -1;
260
+ break;
261
+ case 'ArrowDown':
262
+ move.y = 1;
263
+ break;
264
+ default:
265
+ return false;
266
+ }
267
+
268
+ const flexDir = this.flexDirection === 'column' ? 'y' : 'x';
269
+ const crossDir = flexDir === 'x' ? 'y' : 'x';
270
+ const flexDelta = move[flexDir];
271
+ const crossDelta = move[crossDir];
272
+
273
+ // Select next/prev child in the current column/row
274
+ if (flexDelta !== 0) {
275
+ for (
276
+ let i = selected + flexDelta;
277
+ idxInArray(i, this.children);
278
+ i += flexDelta
279
+ ) {
280
+ const child = this.children[i]!;
281
+ if (child.skipFocus) continue;
282
+
283
+ // Different column/row
284
+ if (child[crossDir] !== prevChild[crossDir]) break;
285
+
286
+ return selectChild(this as lngp.NavigableElement, i);
287
+ }
288
+ }
289
+ // Find child in next/prev column/row
290
+ else {
291
+ let closestIdx = -1;
292
+ let closestDist = Infinity;
293
+
294
+ for (
295
+ let i = selected + crossDelta;
296
+ idxInArray(i, this.children);
297
+ i += crossDelta
298
+ ) {
299
+ const child = this.children[i]!;
300
+ if (child.skipFocus) continue;
301
+
302
+ // Same column/row, skip
303
+ if (child[crossDir] === prevChild[crossDir]) continue;
304
+
305
+ // Different column/row, check distance
306
+ const distance = Math.abs(child[flexDir] - prevChild[flexDir]);
307
+ if (distance >= closestDist) break; // getting further away
308
+
309
+ closestDist = distance;
310
+ closestIdx = i;
311
+ }
312
+
313
+ return selectChild(this as lngp.NavigableElement, closestIdx);
314
+ }
315
+
316
+ return false;
317
+ };
@@ -3,16 +3,31 @@ import type {
3
3
  ElementText,
4
4
  INode,
5
5
  Styles,
6
- } from '@lightningtv/core';
6
+ } from '../../core/index.js';
7
+
8
+ export type Scroller = (
9
+ selected: number | ElementNode,
10
+ component?: ElementNode,
11
+ selectedElement?: ElementNode | ElementText,
12
+ lastSelected?: number,
13
+ ) => void;
7
14
 
8
15
  // Adds properties expected by withScrolling
9
16
  export interface ScrollableElement extends ElementNode {
10
17
  scrollIndex?: number;
18
+ scroll?: 'always' | 'none' | 'edge' | 'auto' | 'center' | 'bounded';
11
19
  selected: number;
12
20
  offset?: number;
13
21
  endOffset?: number;
22
+ upCount?: number;
23
+ onScrolled?: (
24
+ elm: ScrollableElement,
25
+ offset: number,
26
+ isInitial: boolean,
27
+ ) => void;
14
28
  _targetPosition?: number;
15
29
  _screenOffset?: number;
30
+ _initialPosition?: number;
16
31
  }
17
32
 
18
33
  // From the renderer, not exported
@@ -26,16 +41,25 @@ const isNotShown = (node: ElementNode | ElementText) => {
26
41
  Always scroll moves the list every time
27
42
  */
28
43
 
29
- export function withScrolling(isRow: boolean) {
44
+ /**
45
+ * Checks if the selected index is in the non-scrollable zone (last upCount items).
46
+ */
47
+ export function checkIsInNonScrollableZone(
48
+ componentRef: ScrollableElement,
49
+ ): boolean {
50
+ const totalItems = componentRef.children.length;
51
+ const upCount = componentRef.upCount || 6;
52
+ const selected = componentRef.selected || 0;
53
+ const nonScrollableZoneStart = Math.max(0, totalItems - upCount);
54
+ return selected >= nonScrollableZoneStart;
55
+ }
56
+
57
+ /** @deprecated Use {@link scrollRow} or {@link scrollColumn} */
58
+ export function withScrolling(isRow: boolean): Scroller {
30
59
  const dimension = isRow ? 'width' : 'height';
31
60
  const axis = isRow ? 'x' : 'y';
32
61
 
33
- return (
34
- selected: number | ElementNode,
35
- component?: ElementNode,
36
- selectedElement?: ElementNode | ElementText,
37
- lastSelected?: number,
38
- ) => {
62
+ return (selected, component, selectedElement, lastSelected) => {
39
63
  let componentRef = component as ScrollableElement;
40
64
  if (typeof selected !== 'number') {
41
65
  componentRef = selected as ScrollableElement;
@@ -44,12 +68,17 @@ export function withScrolling(isRow: boolean) {
44
68
  if (
45
69
  !componentRef ||
46
70
  componentRef.scroll === 'none' ||
71
+ selected === lastSelected ||
47
72
  !componentRef.children.length
48
73
  )
49
74
  return;
50
75
 
51
- const lng = componentRef.lng as INode;
52
- const screenSize = isRow ? lng.stage.root.width : lng.stage.root.height;
76
+ if (componentRef._initialPosition === undefined) {
77
+ componentRef._initialPosition = componentRef[axis];
78
+ }
79
+
80
+ const lng = componentRef.lng as unknown as INode;
81
+ const screenSize = isRow ? lng.stage.root.w : lng.stage.root.h;
53
82
  // Determine if movement is incremental or decremental
54
83
  const isIncrementing =
55
84
  lastSelected === undefined || lastSelected - 1 !== selected;
@@ -58,6 +87,7 @@ export function withScrolling(isRow: boolean) {
58
87
  if (componentRef.parent!.clipping) {
59
88
  const p = componentRef.parent!;
60
89
  componentRef.endOffset =
90
+ componentRef.endOffset ??
61
91
  screenSize - ((isRow ? p.absX : p.absY) || 0) - p[dimension];
62
92
  }
63
93
 
@@ -68,14 +98,20 @@ export function withScrolling(isRow: boolean) {
68
98
 
69
99
  const screenOffset = componentRef._screenOffset;
70
100
  const gap = componentRef.gap || 0;
71
- const scroll = componentRef.scroll || 'auto';
101
+ // when creating we set scroll to always so we setup the right location for selected and scrollIndex
102
+ const scroll =
103
+ componentRef.scroll ||
104
+ (lastSelected === undefined
105
+ ? componentRef.scrollIndex
106
+ ? 'center'
107
+ : 'always'
108
+ : 'auto');
72
109
 
73
110
  // Allows manual position control
74
111
  const targetPosition = componentRef._targetPosition ?? componentRef[axis];
75
- const rootPosition =
76
- isIncrementing || scroll === 'auto'
77
- ? Math.min(targetPosition, componentRef[axis])
78
- : Math.max(targetPosition, componentRef[axis]);
112
+ const rootPosition = isIncrementing
113
+ ? Math.min(targetPosition, componentRef[axis])
114
+ : Math.max(targetPosition, componentRef[axis]);
79
115
  componentRef.offset = componentRef.offset ?? rootPosition;
80
116
  const offset = componentRef.offset;
81
117
  selectedElement =
@@ -97,7 +133,7 @@ export function withScrolling(isRow: boolean) {
97
133
  screenSize -
98
134
  containerSize -
99
135
  screenOffset -
100
- (componentRef.endOffset || 2 * gap),
136
+ (componentRef.endOffset ?? 2 * gap),
101
137
  offset,
102
138
  );
103
139
 
@@ -113,6 +149,35 @@ export function withScrolling(isRow: boolean) {
113
149
  nextPosition = -selectedPosition + (screenSize - selectedSizeScaled) / 2;
114
150
  } else if (scroll === 'always') {
115
151
  nextPosition = -selectedPosition + offset;
152
+ } else if (scroll === 'bounded') {
153
+ const totalItems = componentRef.children.length;
154
+ const upCount = componentRef.upCount || 6;
155
+ const nonScrollableZoneStart = Math.max(0, totalItems - upCount);
156
+ const isInNonScrollableZone = selected >= nonScrollableZoneStart;
157
+ const isFirstOfNonScrollableZone = selected === nonScrollableZoneStart;
158
+ const isEnteringZone =
159
+ isFirstOfNonScrollableZone &&
160
+ lastSelected !== undefined &&
161
+ lastSelected < nonScrollableZoneStart;
162
+
163
+ if (!isInNonScrollableZone) {
164
+ nextPosition = -selectedPosition + offset;
165
+ } else if (isIncrementing) {
166
+ if (isEnteringZone) {
167
+ const firstOfZoneElement =
168
+ componentRef.children[nonScrollableZoneStart];
169
+ const firstOfZonePosition = firstOfZoneElement?.[axis] ?? 0;
170
+ nextPosition = firstOfZoneElement
171
+ ? -firstOfZonePosition + offset
172
+ : rootPosition;
173
+ } else {
174
+ nextPosition = rootPosition;
175
+ }
176
+ } else if (isFirstOfNonScrollableZone) {
177
+ nextPosition = -selectedPosition + offset;
178
+ } else {
179
+ nextPosition = rootPosition;
180
+ }
116
181
  } else if (scroll === 'center') {
117
182
  const centerPosition =
118
183
  -selectedPosition +
@@ -138,7 +203,7 @@ export function withScrolling(isRow: boolean) {
138
203
  nextPosition = rootPosition + selectedSize + gap;
139
204
  }
140
205
  } else if (isIncrementing) {
141
- nextPosition = -selectedPosition + offset;
206
+ nextPosition = rootPosition - selectedSize - gap;
142
207
  } else {
143
208
  nextPosition = rootPosition + selectedSize + gap;
144
209
  }
@@ -151,15 +216,23 @@ export function withScrolling(isRow: boolean) {
151
216
 
152
217
  // Prevent container from moving beyond bounds
153
218
  nextPosition =
154
- isIncrementing && scroll !== 'always'
219
+ isIncrementing && scroll !== 'always' && scroll !== 'bounded'
155
220
  ? Math.max(nextPosition, maxOffset)
156
221
  : Math.min(nextPosition, offset);
157
222
 
158
223
  // Update position if it has changed
159
224
  if (componentRef[axis] !== nextPosition) {
225
+ if (componentRef.onScrolled) {
226
+ const isInitial = nextPosition === componentRef._initialPosition;
227
+ componentRef.onScrolled(componentRef, nextPosition, isInitial);
228
+ }
229
+
160
230
  componentRef[axis] = nextPosition;
161
231
  // Store the new position to keep track during animations
162
232
  componentRef._targetPosition = nextPosition;
163
233
  }
164
234
  };
165
235
  }
236
+
237
+ export const scrollRow = /* @__PURE__ */ withScrolling(true);
238
+ export const scrollColumn = /* @__PURE__ */ withScrolling(false);
package/src/render.ts CHANGED
@@ -5,8 +5,7 @@ import {
5
5
  type TextProps,
6
6
  startLightningRenderer,
7
7
  type RendererMainSettings,
8
- type IRendererMain,
9
- } from '@lightningtv/core';
8
+ } from './core/index.js';
10
9
  import nodeOpts from './solidOpts.js';
11
10
  import {
12
11
  splitProps,
@@ -14,15 +13,15 @@ import {
14
13
  createRenderEffect,
15
14
  untrack,
16
15
  type JSXElement,
17
- type ValidComponent,
18
16
  createRoot,
17
+ type Component,
19
18
  } from 'solid-js';
20
19
  import type { SolidNode } from './types.js';
21
20
  import { activeElement, setActiveElement } from './activeElement.js';
22
21
 
23
22
  const solidRenderer = solidCreateRenderer<SolidNode>(nodeOpts);
24
23
 
25
- let renderer: IRendererMain;
24
+ let renderer;
26
25
  export const rootNode = nodeOpts.createElement('App');
27
26
 
28
27
  const render = function (code: () => JSXElement) {
@@ -120,10 +119,8 @@ function processTasks(): void {
120
119
  * ```
121
120
  * @description https://www.solidjs.com/docs/latest/api#dynamic
122
121
  */
123
- export function Dynamic<T>(
124
- props: T & {
125
- component?: ValidComponent;
126
- },
122
+ export function Dynamic<T extends Record<string, any>>(
123
+ props: T & { component?: Component<T> | undefined | null },
127
124
  ): JSXElement {
128
125
  const [p, others] = splitProps(props, ['component']);
129
126
 
@@ -159,3 +156,8 @@ export const Text = (props: TextProps) => {
159
156
  spread(el, props, false);
160
157
  return el as unknown as JSXElement;
161
158
  };
159
+
160
+ export function registerDefaultShader(name: string, shader: any) {
161
+ // noop for v2
162
+ // renderer.stage.shManager.registerShaderType('rounded', Rounded);
163
+ }