@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.
- package/LICENSE +1 -1
- package/dist/src/devtools/index.d.ts +6 -0
- package/dist/src/devtools/index.js +65 -0
- package/dist/src/devtools/index.js.map +1 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/jsx-runtime.d.ts +0 -2
- package/dist/src/primitives/Column.jsx +9 -3
- package/dist/src/primitives/Column.jsx.map +1 -1
- package/dist/src/primitives/FPSCounter.jsx +60 -61
- package/dist/src/primitives/FPSCounter.jsx.map +1 -1
- package/dist/src/primitives/FadeInOut.d.ts +10 -0
- package/dist/src/primitives/FadeInOut.jsx +22 -0
- package/dist/src/primitives/FadeInOut.jsx.map +1 -0
- package/dist/src/primitives/Grid.d.ts +15 -6
- package/dist/src/primitives/Grid.jsx +36 -23
- package/dist/src/primitives/Grid.jsx.map +1 -1
- package/dist/src/primitives/Lazy.d.ts +4 -5
- package/dist/src/primitives/Lazy.jsx +16 -12
- package/dist/src/primitives/Lazy.jsx.map +1 -1
- package/dist/src/primitives/LazyUp.jsx +1 -0
- package/dist/src/primitives/LazyUp.jsx.map +1 -1
- package/dist/src/primitives/Marquee.d.ts +64 -0
- package/dist/src/primitives/Marquee.jsx +86 -0
- package/dist/src/primitives/Marquee.jsx.map +1 -0
- package/dist/src/primitives/Preserve.d.ts +4 -0
- package/dist/src/primitives/Preserve.jsx +11 -0
- package/dist/src/primitives/Preserve.jsx.map +1 -0
- package/dist/src/primitives/Row.jsx +9 -3
- package/dist/src/primitives/Row.jsx.map +1 -1
- package/dist/src/primitives/Suspense.d.ts +23 -0
- package/dist/src/primitives/Suspense.jsx +34 -0
- package/dist/src/primitives/Suspense.jsx.map +1 -0
- package/dist/src/primitives/announcer/announcer.d.ts +1 -0
- package/dist/src/primitives/announcer/announcer.js +6 -5
- package/dist/src/primitives/announcer/announcer.js.map +1 -1
- package/dist/src/primitives/announcer/index.d.ts +5 -1
- package/dist/src/primitives/announcer/index.js +8 -2
- package/dist/src/primitives/announcer/index.js.map +1 -1
- package/dist/src/primitives/announcer/speech.d.ts +2 -2
- package/dist/src/primitives/announcer/speech.js +67 -28
- package/dist/src/primitives/announcer/speech.js.map +1 -1
- package/dist/src/primitives/index.d.ts +6 -2
- package/dist/src/primitives/index.js +6 -2
- package/dist/src/primitives/index.js.map +1 -1
- package/dist/src/primitives/types.d.ts +2 -0
- package/dist/src/primitives/useHold.d.ts +27 -0
- package/dist/src/primitives/useHold.js +54 -0
- package/dist/src/primitives/useHold.js.map +1 -0
- package/dist/src/primitives/utils/chainFunctions.d.ts +30 -4
- package/dist/src/primitives/utils/chainFunctions.js +14 -3
- package/dist/src/primitives/utils/chainFunctions.js.map +1 -1
- package/dist/src/primitives/utils/createSpriteMap.js.map +1 -1
- package/dist/src/primitives/utils/handleNavigation.js +11 -2
- package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
- package/dist/src/primitives/utils/withScrolling.d.ts +3 -0
- package/dist/src/primitives/utils/withScrolling.js +15 -2
- package/dist/src/primitives/utils/withScrolling.js.map +1 -1
- package/dist/src/render.d.ts +5 -5
- package/dist/src/render.js +8 -6
- package/dist/src/render.js.map +1 -1
- package/dist/src/solidOpts.d.ts +7 -0
- package/dist/src/solidOpts.js +39 -7
- package/dist/src/solidOpts.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/{src/jsx-runtime.ts → jsx-runtime.d.ts} +2 -2
- package/package.json +33 -15
- package/src/devtools/index.ts +77 -0
- package/src/index.ts +1 -1
- package/src/primitives/Column.tsx +11 -4
- package/src/primitives/FPSCounter.tsx +61 -61
- package/src/primitives/FadeInOut.tsx +34 -0
- package/src/primitives/Grid.tsx +59 -35
- package/src/primitives/Lazy.tsx +27 -18
- package/src/primitives/Marquee.tsx +149 -0
- package/src/primitives/Preserve.tsx +18 -0
- package/src/primitives/Row.tsx +11 -4
- package/src/primitives/Suspense.tsx +41 -0
- package/src/primitives/announcer/announcer.ts +9 -10
- package/src/primitives/announcer/index.ts +12 -2
- package/src/primitives/announcer/speech.ts +82 -28
- package/src/primitives/index.ts +10 -2
- package/src/primitives/types.ts +9 -0
- package/src/primitives/useHold.ts +69 -0
- package/src/primitives/utils/chainFunctions.ts +40 -9
- package/src/primitives/utils/createSpriteMap.ts +2 -2
- package/src/primitives/utils/handleNavigation.ts +11 -2
- package/src/primitives/utils/withScrolling.ts +29 -5
- package/src/render.ts +12 -12
- package/src/solidOpts.ts +51 -7
- package/src/primitives/LazyUp.tsx +0 -71
- 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
|
|
77
|
-
(
|
|
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
|
-
|
|
6
|
-
|
|
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 =
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
if (isNaN(pause)) {
|
|
97
|
-
pause
|
|
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
|
-
|
|
100
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
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((
|
|
161
|
-
|
|
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 (
|
|
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
|
}
|
package/src/primitives/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
package/src/primitives/types.ts
CHANGED
|
@@ -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
|
-
|
|
1
|
+
import * as s from 'solid-js';
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
export function chainFunctions<T>(...args: (ChainableFunction | T)[]): T;
|
|
3
|
+
export type AnyFunction = (this: any, ...args: any[]) => any;
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 (
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|