@lightningtv/solid 3.0.0-0 → 3.0.0-10

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 (93) hide show
  1. package/LICENSE +1 -1
  2. package/dist/src/devtools/index.d.ts +6 -0
  3. package/dist/src/devtools/index.js +65 -0
  4. package/dist/src/devtools/index.js.map +1 -0
  5. package/dist/src/index.d.ts +1 -1
  6. package/dist/src/index.js +1 -1
  7. package/dist/src/index.js.map +1 -1
  8. package/dist/src/jsx-runtime.d.ts +0 -2
  9. package/dist/src/primitives/Column.jsx +9 -3
  10. package/dist/src/primitives/Column.jsx.map +1 -1
  11. package/dist/src/primitives/FPSCounter.jsx +60 -61
  12. package/dist/src/primitives/FPSCounter.jsx.map +1 -1
  13. package/dist/src/primitives/FadeInOut.d.ts +10 -0
  14. package/dist/src/primitives/FadeInOut.jsx +22 -0
  15. package/dist/src/primitives/FadeInOut.jsx.map +1 -0
  16. package/dist/src/primitives/Grid.d.ts +15 -6
  17. package/dist/src/primitives/Grid.jsx +36 -23
  18. package/dist/src/primitives/Grid.jsx.map +1 -1
  19. package/dist/src/primitives/Lazy.d.ts +4 -5
  20. package/dist/src/primitives/Lazy.jsx +16 -12
  21. package/dist/src/primitives/Lazy.jsx.map +1 -1
  22. package/dist/src/primitives/LazyUp.jsx +1 -0
  23. package/dist/src/primitives/LazyUp.jsx.map +1 -1
  24. package/dist/src/primitives/Marquee.d.ts +64 -0
  25. package/dist/src/primitives/Marquee.jsx +86 -0
  26. package/dist/src/primitives/Marquee.jsx.map +1 -0
  27. package/dist/src/primitives/Preserve.d.ts +4 -0
  28. package/dist/src/primitives/Preserve.jsx +11 -0
  29. package/dist/src/primitives/Preserve.jsx.map +1 -0
  30. package/dist/src/primitives/Row.jsx +9 -3
  31. package/dist/src/primitives/Row.jsx.map +1 -1
  32. package/dist/src/primitives/Suspense.d.ts +23 -0
  33. package/dist/src/primitives/Suspense.jsx +34 -0
  34. package/dist/src/primitives/Suspense.jsx.map +1 -0
  35. package/dist/src/primitives/announcer/announcer.d.ts +1 -0
  36. package/dist/src/primitives/announcer/announcer.js +6 -5
  37. package/dist/src/primitives/announcer/announcer.js.map +1 -1
  38. package/dist/src/primitives/announcer/index.d.ts +5 -1
  39. package/dist/src/primitives/announcer/index.js +8 -2
  40. package/dist/src/primitives/announcer/index.js.map +1 -1
  41. package/dist/src/primitives/announcer/speech.d.ts +2 -2
  42. package/dist/src/primitives/announcer/speech.js +67 -28
  43. package/dist/src/primitives/announcer/speech.js.map +1 -1
  44. package/dist/src/primitives/index.d.ts +6 -2
  45. package/dist/src/primitives/index.js +6 -2
  46. package/dist/src/primitives/index.js.map +1 -1
  47. package/dist/src/primitives/types.d.ts +2 -0
  48. package/dist/src/primitives/useHold.d.ts +27 -0
  49. package/dist/src/primitives/useHold.js +54 -0
  50. package/dist/src/primitives/useHold.js.map +1 -0
  51. package/dist/src/primitives/utils/chainFunctions.d.ts +30 -4
  52. package/dist/src/primitives/utils/chainFunctions.js +14 -3
  53. package/dist/src/primitives/utils/chainFunctions.js.map +1 -1
  54. package/dist/src/primitives/utils/createSpriteMap.js.map +1 -1
  55. package/dist/src/primitives/utils/handleNavigation.js +11 -2
  56. package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
  57. package/dist/src/primitives/utils/withScrolling.d.ts +3 -0
  58. package/dist/src/primitives/utils/withScrolling.js +15 -2
  59. package/dist/src/primitives/utils/withScrolling.js.map +1 -1
  60. package/dist/src/render.d.ts +5 -5
  61. package/dist/src/render.js +8 -6
  62. package/dist/src/render.js.map +1 -1
  63. package/dist/src/solidOpts.d.ts +7 -0
  64. package/dist/src/solidOpts.js +39 -7
  65. package/dist/src/solidOpts.js.map +1 -1
  66. package/dist/tsconfig.tsbuildinfo +1 -1
  67. package/{src/jsx-runtime.ts → jsx-runtime.d.ts} +2 -2
  68. package/package.json +33 -15
  69. package/src/devtools/index.ts +77 -0
  70. package/src/index.ts +1 -1
  71. package/src/primitives/Column.tsx +11 -4
  72. package/src/primitives/FPSCounter.tsx +61 -61
  73. package/src/primitives/FadeInOut.tsx +34 -0
  74. package/src/primitives/Grid.tsx +59 -35
  75. package/src/primitives/Lazy.tsx +27 -18
  76. package/src/primitives/Marquee.tsx +149 -0
  77. package/src/primitives/Preserve.tsx +18 -0
  78. package/src/primitives/Row.tsx +11 -4
  79. package/src/primitives/Suspense.tsx +41 -0
  80. package/src/primitives/announcer/announcer.ts +9 -10
  81. package/src/primitives/announcer/index.ts +12 -2
  82. package/src/primitives/announcer/speech.ts +82 -28
  83. package/src/primitives/index.ts +10 -2
  84. package/src/primitives/types.ts +9 -0
  85. package/src/primitives/useHold.ts +69 -0
  86. package/src/primitives/utils/chainFunctions.ts +40 -9
  87. package/src/primitives/utils/createSpriteMap.ts +2 -2
  88. package/src/primitives/utils/handleNavigation.ts +11 -2
  89. package/src/primitives/utils/withScrolling.ts +29 -5
  90. package/src/render.ts +12 -12
  91. package/src/solidOpts.ts +51 -7
  92. package/src/primitives/LazyUp.tsx +0 -71
  93. package/src/primitives/jsx-runtime.d.ts +0 -8
@@ -73,8 +73,9 @@ function onFocusChangeCore(focusPath: ElementNode[] = []) {
73
73
  prevFocusPath = focusPath.slice(0);
74
74
 
75
75
  const toAnnounceText: SpeechType[] = [];
76
- const toAnnounce = focusDiff.reduce(
77
- (acc: [string, string, SpeechType][], elm) => {
76
+ const toAnnounce = focusDiff
77
+ .reverse()
78
+ .reduce((acc: [string, string, SpeechType][], elm) => {
78
79
  if (elm.announce) {
79
80
  acc.push([getElmName(elm), 'Announce', elm.announce]);
80
81
  toAnnounceText.push(elm.announce);
@@ -85,9 +86,7 @@ function onFocusChangeCore(focusPath: ElementNode[] = []) {
85
86
  acc.push([getElmName(elm), 'No Announce', '']);
86
87
  }
87
88
  return acc;
88
- },
89
- [],
90
- );
89
+ }, []);
91
90
 
92
91
  focusDiff.reverse().reduce((acc, elm) => {
93
92
  if (elm.announceContext) {
@@ -110,18 +109,19 @@ function onFocusChangeCore(focusPath: ElementNode[] = []) {
110
109
  }
111
110
  }
112
111
 
113
- function textToSpeech(toSpeak: SpeechType, lang: string) {
112
+ function textToSpeech(toSpeak: SpeechType, lang: string, voice?: string) {
114
113
  if (voiceOutDisabled) {
115
114
  return;
116
115
  }
117
116
 
118
- return (currentlySpeaking = SpeechEngine(toSpeak, lang));
117
+ return (currentlySpeaking = SpeechEngine(toSpeak, lang, voice));
119
118
  }
120
119
 
121
120
  export interface Announcer {
122
121
  debug: boolean;
123
122
  enabled: boolean;
124
123
  lang: string;
124
+ voice?: string;
125
125
  cancel: VoidFunction;
126
126
  clearPrevFocus: (depth?: number) => void;
127
127
  speak: (
@@ -147,14 +147,13 @@ export const Announcer: Announcer = {
147
147
  prevFocusPath = prevFocusPath.slice(0, depth);
148
148
  resetFocusPathTimer();
149
149
  },
150
- speak: function (text, { append = false, notification = false} = {}) {
150
+ speak: function (text, { append = false, notification = false } = {}) {
151
151
  if (Announcer.onFocusChange && Announcer.enabled) {
152
- Announcer.onFocusChange.flush();
153
152
  if (append && currentlySpeaking && currentlySpeaking.active) {
154
153
  currentlySpeaking.append(text);
155
154
  } else {
156
155
  Announcer.cancel();
157
- textToSpeech(text, Announcer.lang);
156
+ textToSpeech(text, Announcer.lang, Announcer.voice);
158
157
  }
159
158
 
160
159
  if (notification) {
@@ -2,9 +2,19 @@ import { createEffect, on } from 'solid-js';
2
2
  import { Announcer } from './announcer.js';
3
3
  import { focusPath } from '../useFocusManager.js';
4
4
 
5
- export const useAnnouncer = () => {
6
- Announcer.setupTimers();
5
+ let doOnce = false;
6
+ export const useAnnouncer = (options?: {
7
+ focusDebounce?: number;
8
+ focusChangeTimeout?: number;
9
+ }) => {
10
+ if (doOnce) {
11
+ return Announcer;
12
+ }
13
+ doOnce = true;
14
+ Announcer.setupTimers(options);
7
15
  createEffect(on(focusPath, Announcer.onFocusChange!, { defer: true }));
8
16
 
9
17
  return Announcer;
10
18
  };
19
+
20
+ export { Announcer };
@@ -1,4 +1,8 @@
1
- type CoreSpeechType = string | (() => SpeechType) | SpeechType[];
1
+ type CoreSpeechType =
2
+ | string
3
+ | (() => SpeechType)
4
+ | SpeechType[]
5
+ | SpeechSynthesisUtterance;
2
6
  export type SpeechType = CoreSpeechType | Promise<CoreSpeechType>;
3
7
 
4
8
  export interface SeriesResult {
@@ -48,11 +52,23 @@ function speak(
48
52
  phrase: string,
49
53
  utterances: SpeechSynthesisUtterance[],
50
54
  lang = 'en-US',
55
+ voiceName?: string,
51
56
  ) {
52
57
  const synth = window.speechSynthesis;
58
+
53
59
  return new Promise<void>((resolve, reject) => {
60
+ let selectedVoice;
61
+ if (voiceName) {
62
+ const availableVoices = synth.getVoices();
63
+ selectedVoice =
64
+ availableVoices.find((v) => v.name === voiceName) || availableVoices[0];
65
+ }
66
+
54
67
  const utterance = new SpeechSynthesisUtterance(phrase);
55
68
  utterance.lang = lang;
69
+ if (selectedVoice) {
70
+ utterance.voice = selectedVoice;
71
+ }
56
72
  utterance.onend = () => {
57
73
  resolve();
58
74
  };
@@ -67,6 +83,7 @@ function speak(
67
83
  function speakSeries(
68
84
  series: SpeechType,
69
85
  lang: string,
86
+ voice?: string,
70
87
  root = true,
71
88
  ): SeriesResult {
72
89
  const synth = window.speechSynthesis;
@@ -74,11 +91,6 @@ function speakSeries(
74
91
  Array.isArray(series) ? series : [series],
75
92
  );
76
93
  const nestedSeriesResults: SeriesResult[] = [];
77
- /*
78
- We hold this array of SpeechSynthesisUtterances in order to prevent them from being
79
- garbage collected prematurely on STB hardware which can cause the 'onend' events of
80
- utterances to not fire consistently.
81
- */
82
94
  const utterances: SpeechSynthesisUtterance[] = [];
83
95
  let active: boolean = true;
84
96
 
@@ -87,24 +99,61 @@ function speakSeries(
87
99
  while (active && remainingPhrases.length) {
88
100
  const phrase = await Promise.resolve(remainingPhrases.shift());
89
101
  if (!active) {
90
- // Exit
91
- // Need to check this after the await in case it was cancelled in between
92
- break;
93
- } else if (typeof phrase === 'string' && phrase.includes('PAUSE-')) {
94
- // Pause it
95
- let pause = Number(phrase.split('PAUSE-')[1]) * 1000;
96
- if (isNaN(pause)) {
97
- pause = 0;
102
+ break; // Exit if canceled
103
+ }
104
+
105
+ if (typeof phrase === 'string' && phrase.includes('PAUSE-')) {
106
+ // Handle pauses
107
+ const pause = Number(phrase.split('PAUSE-')[1]) * 1000;
108
+ if (!isNaN(pause)) {
109
+ await delay(pause);
110
+ }
111
+ } else if (typeof phrase === 'string') {
112
+ if (!phrase) {
113
+ continue; // Skip empty strings
114
+ }
115
+ // Handle regular strings with retry logic
116
+ const totalRetries = 3;
117
+ let retriesLeft = totalRetries;
118
+
119
+ while (active && retriesLeft > 0) {
120
+ try {
121
+ await speak(phrase, utterances, lang, voice);
122
+ retriesLeft = 0; // Exit retry loop on success
123
+ } catch (e) {
124
+ if (e instanceof SpeechSynthesisErrorEvent) {
125
+ if (e.error === 'network') {
126
+ retriesLeft--;
127
+ console.warn(
128
+ `Speech synthesis network error. Retries left: ${retriesLeft}`,
129
+ );
130
+ await delay(500 * (totalRetries - retriesLeft));
131
+ } else if (
132
+ e.error === 'canceled' ||
133
+ e.error === 'interrupted'
134
+ ) {
135
+ // Cancel or interrupt error (ignore)
136
+ retriesLeft = 0;
137
+ } else {
138
+ throw new Error(`SpeechSynthesisErrorEvent: ${e.error}`);
139
+ }
140
+ } else {
141
+ throw e;
142
+ }
143
+ }
98
144
  }
99
- await delay(pause);
100
- } else if (typeof phrase === 'string' && phrase.length) {
101
- // Speak it
145
+ } else if (phrase instanceof SpeechSynthesisUtterance) {
146
+ // Handle SpeechSynthesisUtterance objects with retry logic
102
147
  const totalRetries = 3;
103
148
  let retriesLeft = totalRetries;
149
+ const text = phrase.text;
150
+ const objectLang = phrase?.lang;
151
+ const objectVoice = phrase?.voice;
152
+
104
153
  while (active && retriesLeft > 0) {
105
154
  try {
106
- await speak(phrase, utterances, lang);
107
- retriesLeft = 0;
155
+ await speak(text, utterances, objectLang, objectVoice?.name);
156
+ retriesLeft = 0; // Exit retry loop on success
108
157
  } catch (e) {
109
158
  if (e instanceof SpeechSynthesisErrorEvent) {
110
159
  if (e.error === 'network') {
@@ -128,12 +177,13 @@ function speakSeries(
128
177
  }
129
178
  }
130
179
  } else if (typeof phrase === 'function') {
131
- const seriesResult = speakSeries(phrase(), lang, false);
180
+ // Handle functions
181
+ const seriesResult = speakSeries(phrase(), lang, voice, false);
132
182
  nestedSeriesResults.push(seriesResult);
133
183
  await seriesResult.series;
134
184
  } else if (Array.isArray(phrase)) {
135
- // Speak it (recursively)
136
- const seriesResult = speakSeries(phrase, lang, false);
185
+ // Handle nested arrays
186
+ const seriesResult = speakSeries(phrase, lang, voice, false);
137
187
  nestedSeriesResults.push(seriesResult);
138
188
  await seriesResult.series;
139
189
  }
@@ -142,6 +192,7 @@ function speakSeries(
142
192
  active = false;
143
193
  }
144
194
  })();
195
+
145
196
  return {
146
197
  series: seriesChain,
147
198
  get active() {
@@ -155,19 +206,22 @@ function speakSeries(
155
206
  return;
156
207
  }
157
208
  if (root) {
158
- synth.cancel();
209
+ synth.cancel(); // Cancel all ongoing speech
159
210
  }
160
- nestedSeriesResults.forEach((nestedSeriesResults) => {
161
- nestedSeriesResults.cancel();
211
+ nestedSeriesResults.forEach((nestedSeriesResult) => {
212
+ nestedSeriesResult.cancel();
162
213
  });
163
214
  active = false;
164
215
  },
165
216
  };
166
217
  }
167
-
168
218
  let currentSeries: SeriesResult | undefined;
169
- export default function (toSpeak: SpeechType, lang: string = 'en-US') {
219
+ export default function (
220
+ toSpeak: SpeechType,
221
+ lang: string = 'en-US',
222
+ voice?: string,
223
+ ) {
170
224
  currentSeries && currentSeries.cancel();
171
- currentSeries = speakSeries(toSpeak, lang);
225
+ currentSeries = speakSeries(toSpeak, lang, voice);
172
226
  return currentSeries;
173
227
  }
@@ -3,7 +3,6 @@ export * from './announcer/index.js';
3
3
  export * from './createInfiniteItems.js';
4
4
  export * from './useMouse.js';
5
5
  export * from './portal.jsx';
6
- export * from './LazyUp.jsx';
7
6
  export * from './Lazy.jsx';
8
7
  export * from './Visible.jsx';
9
8
  export * from './router.js';
@@ -11,9 +10,18 @@ export * from './Column.jsx';
11
10
  export * from './Row.jsx';
12
11
  export * from './Grid.jsx';
13
12
  export * from './FPSCounter.jsx';
13
+ export * from './FadeInOut.jsx';
14
+ export * from './Preserve.jsx';
15
+ export * from './Suspense.jsx';
16
+ export * from './Marquee.jsx';
14
17
  export * from './createFocusStack.jsx';
18
+ export * from './useHold.js';
15
19
  export { withScrolling } from './utils/withScrolling.js';
16
- export { chainFunctions } from './utils/chainFunctions.js';
20
+ export {
21
+ type AnyFunction,
22
+ chainFunctions,
23
+ chainRefs,
24
+ } from './utils/chainFunctions.js';
17
25
  export { handleNavigation, onGridFocus } from './utils/handleNavigation.js';
18
26
  export { createSpriteMap, type SpriteDef } from './utils/createSpriteMap.js';
19
27
 
@@ -1,5 +1,6 @@
1
1
  import type { ElementNode, NodeProps, NodeStyles } from '@lightningtv/solid';
2
2
  import type { KeyHandler } from '@lightningtv/core/focusManager';
3
+
3
4
  export type OnSelectedChanged = (
4
5
  this: NavigableElement,
5
6
  selectedIndex: number,
@@ -7,6 +8,7 @@ export type OnSelectedChanged = (
7
8
  active: ElementNode,
8
9
  lastSelectedIndex?: number,
9
10
  ) => void;
11
+
10
12
  export interface NavigableProps extends NodeProps {
11
13
  /** function to be called when the selected of the component changes */
12
14
  onSelectedChanged?: OnSelectedChanged;
@@ -40,6 +42,13 @@ export interface NavigableProps extends NodeProps {
40
42
  * Wrap the row so active goes back to the beginning of the row
41
43
  */
42
44
  wrap?: boolean;
45
+
46
+ /** function to be called when scrolled */
47
+ onScrolled?: (
48
+ elm: NavigableElement,
49
+ offset: number,
50
+ isInitial: boolean,
51
+ ) => void;
43
52
  }
44
53
 
45
54
  // @ts-expect-error animationSettings is not identical - weird
@@ -0,0 +1,69 @@
1
+ import { createMemo } from 'solid-js';
2
+
3
+ export type UseHoldProps = {
4
+ onHold: () => void;
5
+ onEnter: () => void;
6
+ onRelease?: () => void;
7
+ holdThreshold?: number;
8
+ performOnEnterImmediately?: boolean;
9
+ };
10
+
11
+ /**
12
+ * @example
13
+ * const [holdRight, releaseRight] = useHold({
14
+ * onHold: handleHoldRight,
15
+ * onEnter: handleOnRight,
16
+ * onRelease: handleReleaseHold,
17
+ * holdThreshold: 200,
18
+ * performOnEnterImmediately: true
19
+ * });
20
+ *
21
+ * <View
22
+ * onRight={holdRight}
23
+ * onRightRelease={releaseRight}
24
+ * />
25
+ *
26
+ * @param {UseHoldProps} props - The properties for configuring the hold behavior.
27
+ * @returns {[() => boolean, () => boolean]} A tuple containing `startHold` and `releaseHold` functions.
28
+ */
29
+
30
+ export function useHold(props: UseHoldProps) {
31
+ const holdThreshold = createMemo(() => props.holdThreshold ?? 500);
32
+ const performOnEnterImmediately = createMemo(
33
+ () => props.performOnEnterImmediately ?? false,
34
+ );
35
+
36
+ let holdTimeout = -1;
37
+ let wasHeld = false;
38
+
39
+ const startHold = () => {
40
+ if (holdTimeout === -1) {
41
+ if (performOnEnterImmediately()) {
42
+ props.onEnter();
43
+ }
44
+ holdTimeout = setTimeout(() => {
45
+ wasHeld = true;
46
+ props.onHold();
47
+ }, holdThreshold()) as unknown as number;
48
+ }
49
+ return true;
50
+ };
51
+
52
+ const releaseHold = () => {
53
+ if (holdTimeout !== -1) {
54
+ clearTimeout(holdTimeout);
55
+ holdTimeout = -1;
56
+ if (!wasHeld) {
57
+ if (!performOnEnterImmediately()) props.onEnter();
58
+ return;
59
+ }
60
+ props.onRelease?.();
61
+ wasHeld = false;
62
+ }
63
+ return true;
64
+ };
65
+
66
+ return [startHold, releaseHold];
67
+ }
68
+
69
+ export default useHold;
@@ -1,13 +1,29 @@
1
- type ChainableFunction = (...args: unknown[]) => unknown;
1
+ import * as s from 'solid-js';
2
2
 
3
- export function chainFunctions(...args: ChainableFunction[]): ChainableFunction;
4
- export function chainFunctions<T>(...args: (ChainableFunction | T)[]): T;
3
+ export type AnyFunction = (this: any, ...args: any[]) => any;
5
4
 
6
- // take an array of functions and if you return true from a function, it will stop the chain
7
- export function chainFunctions<T extends ChainableFunction>(
8
- ...args: (ChainableFunction | T)[]
9
- ) {
10
- const onlyFunctions = args.filter((func) => typeof func === 'function');
5
+ /**
6
+ * take an array of functions and if you return `true` from a function, it will stop the chain
7
+ * @param fns list of functions to chain together, can be `undefined`, `null`, or `false` to skip them
8
+ * @returns a function that will call each function in the list until one returns `true` or all functions are called.
9
+ * If no functions are provided, it will return `undefined`.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * function Button (props: NodeProps) {
14
+ * function onEnter (el: ElementNode) {...}
15
+ * return <view onEnter={chainFunctions(props.onEnter, onEnter)} />
16
+ * }
17
+ * ```
18
+ */
19
+ export function chainFunctions<T extends AnyFunction>(...fns: T[]): T;
20
+ export function chainFunctions<T extends AnyFunction>(
21
+ ...fns: (T | undefined | null | false)[]
22
+ ): T | undefined;
23
+ export function chainFunctions(
24
+ ...fns: (AnyFunction | undefined | null | false)[]
25
+ ): AnyFunction | undefined {
26
+ const onlyFunctions = fns.filter((func) => typeof func === 'function');
11
27
  if (onlyFunctions.length === 0) {
12
28
  return undefined;
13
29
  }
@@ -16,7 +32,7 @@ export function chainFunctions<T extends ChainableFunction>(
16
32
  return onlyFunctions[0];
17
33
  }
18
34
 
19
- return function (this: unknown | T, ...innerArgs: unknown[]) {
35
+ return function (...innerArgs) {
20
36
  let result;
21
37
  for (const func of onlyFunctions) {
22
38
  result = func.apply(this, innerArgs);
@@ -27,3 +43,18 @@ export function chainFunctions<T extends ChainableFunction>(
27
43
  return result;
28
44
  };
29
45
  }
46
+
47
+ /**
48
+ * Utility for chaining multiple `ref` assignments with `props.ref` forwarding.
49
+ * @param refs list of ref setters. Can be a `props.ref` prop for ref forwarding or a setter to a local variable (`el => ref = el`).
50
+ * @example
51
+ * ```tsx
52
+ * function Button (props: NodeProps) {
53
+ * let localRef: ElementNode | undefined
54
+ * return <view ref={chainRefs(props.ref, el => localRef = el)} />
55
+ * }
56
+ * ```
57
+ */
58
+ export const chainRefs = chainFunctions as <T>(
59
+ ...refs: (s.Ref<T> | undefined)[]
60
+ ) => (el: T) => void;
@@ -1,4 +1,4 @@
1
- import { renderer, type TextureMap } from '@lightningtv/core';
1
+ import { type TextureMap, renderer } from '@lightningtv/core';
2
2
 
3
3
  export interface SpriteDef {
4
4
  name: string | number;
@@ -26,7 +26,7 @@ export function createSpriteMap(
26
26
  y,
27
27
  width,
28
28
  height,
29
- });
29
+ }) as InstanceType<TextureMap['SubTexture']>;
30
30
  return acc;
31
31
  }, {});
32
32
  }
@@ -6,6 +6,15 @@ export function onGridFocus(onSelectedChanged: OnSelectedChanged | undefined) {
6
6
  return function (this: ElementNode) {
7
7
  if (!this || this.children.length === 0) return false;
8
8
 
9
+ // if a child already has focus, assume that should be selected
10
+ this.children.find((child, index) => {
11
+ if (child.states.has(Config.focusStateKey)) {
12
+ this.selected = index;
13
+ return true;
14
+ }
15
+ return false;
16
+ });
17
+
9
18
  this.selected = this.selected || 0;
10
19
  let child = this.selected
11
20
  ? this.children[this.selected]
@@ -73,8 +82,8 @@ export function handleNavigation(
73
82
  return false;
74
83
  }
75
84
  }
76
- const active = this.children[this.selected || 0];
77
- assertTruthy(active instanceof ElementNode);
85
+ const active = this.children[this.selected || 0] || this.children[0];
86
+ if (!(active instanceof ElementNode)) return false;
78
87
  const navigableThis = this as NavigableElement;
79
88
 
80
89
  navigableThis.onSelectedChanged &&
@@ -8,11 +8,18 @@ import type {
8
8
  // Adds properties expected by withScrolling
9
9
  export interface ScrollableElement extends ElementNode {
10
10
  scrollIndex?: number;
11
+ scroll?: 'always' | 'none' | 'edge' | 'auto' | 'center';
11
12
  selected: number;
12
13
  offset?: number;
13
14
  endOffset?: number;
15
+ onScrolled?: (
16
+ elm: ScrollableElement,
17
+ offset: number,
18
+ isInitial: boolean,
19
+ ) => void;
14
20
  _targetPosition?: number;
15
21
  _screenOffset?: number;
22
+ _initialPosition?: number;
16
23
  }
17
24
 
18
25
  // From the renderer, not exported
@@ -48,7 +55,11 @@ export function withScrolling(isRow: boolean) {
48
55
  )
49
56
  return;
50
57
 
51
- const lng = componentRef.lng as INode;
58
+ if (componentRef._initialPosition === undefined) {
59
+ componentRef._initialPosition = componentRef[axis];
60
+ }
61
+
62
+ const lng = componentRef.lng as unknown as INode;
52
63
  const screenSize = isRow ? lng.stage.root.width : lng.stage.root.height;
53
64
  // Determine if movement is incremental or decremental
54
65
  const isIncrementing =
@@ -68,13 +79,21 @@ export function withScrolling(isRow: boolean) {
68
79
 
69
80
  const screenOffset = componentRef._screenOffset;
70
81
  const gap = componentRef.gap || 0;
71
- const scroll = componentRef.scroll || 'auto';
82
+ // when creating we set scroll to always so we setup the right location for selected and scrollIndex
83
+ const scroll =
84
+ componentRef.scroll ||
85
+ (lastSelected === undefined
86
+ ? componentRef.scrollIndex
87
+ ? 'center'
88
+ : 'always'
89
+ : 'auto');
72
90
 
73
91
  // Allows manual position control
74
92
  const targetPosition = componentRef._targetPosition ?? componentRef[axis];
75
- const rootPosition = isIncrementing
76
- ? Math.min(targetPosition, componentRef[axis])
77
- : Math.max(targetPosition, componentRef[axis]);
93
+ const rootPosition =
94
+ isIncrementing || scroll === 'auto'
95
+ ? Math.min(targetPosition, componentRef[axis])
96
+ : Math.max(targetPosition, componentRef[axis]);
78
97
  componentRef.offset = componentRef.offset ?? rootPosition;
79
98
  const offset = componentRef.offset;
80
99
  selectedElement =
@@ -156,6 +175,11 @@ export function withScrolling(isRow: boolean) {
156
175
 
157
176
  // Update position if it has changed
158
177
  if (componentRef[axis] !== nextPosition) {
178
+ if (componentRef.onScrolled) {
179
+ const isInitial = nextPosition === componentRef._initialPosition;
180
+ componentRef.onScrolled(componentRef, nextPosition, isInitial);
181
+ }
182
+
159
183
  componentRef[axis] = nextPosition;
160
184
  // Store the new position to keep track during animations
161
185
  componentRef._targetPosition = nextPosition;
package/src/render.ts CHANGED
@@ -4,7 +4,6 @@ import {
4
4
  type NodeProps,
5
5
  type TextProps,
6
6
  startLightningRenderer,
7
- type RendererMain,
8
7
  type RendererMainSettings,
9
8
  } from '@lightningtv/core';
10
9
  import nodeOpts from './solidOpts.js';
@@ -14,14 +13,15 @@ import {
14
13
  createRenderEffect,
15
14
  untrack,
16
15
  type JSXElement,
17
- type ValidComponent,
16
+ createRoot,
17
+ type Component,
18
18
  } from 'solid-js';
19
19
  import type { SolidNode } from './types.js';
20
20
  import { activeElement, setActiveElement } from './activeElement.js';
21
21
 
22
22
  const solidRenderer = solidCreateRenderer<SolidNode>(nodeOpts);
23
23
 
24
- let renderer: RendererMain;
24
+ let renderer;
25
25
  export const rootNode = nodeOpts.createElement('App');
26
26
 
27
27
  const render = function (code: () => JSXElement) {
@@ -71,11 +71,13 @@ type Task = () => void;
71
71
  const taskQueue: Task[] = [];
72
72
  let tasksEnabled = false;
73
73
 
74
- createRenderEffect(() => {
75
- // should change whenever a keypress occurs, so we disable the task queue
76
- // until the renderer is idle again.
77
- activeElement();
78
- tasksEnabled = false;
74
+ createRoot(() => {
75
+ createRenderEffect(() => {
76
+ // should change whenever a keypress occurs, so we disable the task queue
77
+ // until the renderer is idle again.
78
+ activeElement();
79
+ tasksEnabled = false;
80
+ });
79
81
  });
80
82
 
81
83
  export function setTasksEnabled(enabled: boolean): void {
@@ -117,10 +119,8 @@ function processTasks(): void {
117
119
  * ```
118
120
  * @description https://www.solidjs.com/docs/latest/api#dynamic
119
121
  */
120
- export function Dynamic<T>(
121
- props: T & {
122
- component?: ValidComponent;
123
- },
122
+ export function Dynamic<T extends Record<string, any>>(
123
+ props: T & { component?: Component<T> | undefined | null },
124
124
  ): JSXElement {
125
125
  const [p, others] = splitProps(props, ['component']);
126
126