@lightningtv/solid 0.0.2 → 0.0.4
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/dist/esm/index.js +466 -5
- package/dist/esm/index.js.map +1 -1
- package/dist/source/index.js +1 -0
- package/dist/source/primitives/announcer/announcer.js +121 -0
- package/dist/source/primitives/announcer/index.js +8 -0
- package/dist/source/primitives/announcer/speech.js +152 -0
- package/dist/source/primitives/createInfiniteItems.js +45 -0
- package/dist/source/primitives/createSpriteMap.js +17 -0
- package/dist/source/primitives/index.js +5 -0
- package/dist/source/primitives/useFocusManager.js +88 -0
- package/dist/source/primitives/withPadding.js +48 -0
- package/dist/source/solidOpts.js +2 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/primitives/announcer/announcer.d.ts +37 -0
- package/dist/types/primitives/announcer/index.d.ts +2 -0
- package/dist/types/primitives/announcer/speech.d.ts +10 -0
- package/dist/types/primitives/createInfiniteItems.d.ts +27 -0
- package/dist/types/primitives/createSpriteMap.d.ts +8 -0
- package/dist/types/primitives/index.d.ts +5 -0
- package/dist/types/primitives/useFocusManager.d.ts +46 -0
- package/dist/types/primitives/withPadding.d.ts +3 -0
- package/package.json +5 -2
- package/src/index.ts +1 -0
- package/src/primitives/announcer/announcer.ts +190 -0
- package/src/primitives/announcer/index.ts +10 -0
- package/src/primitives/announcer/speech.ts +174 -0
- package/src/primitives/createInfiniteItems.ts +66 -0
- package/src/primitives/createSpriteMap.ts +31 -0
- package/src/primitives/index.ts +5 -0
- package/src/primitives/jsx-runtime.d.ts +11 -0
- package/src/primitives/useFocusManager.ts +194 -0
- package/src/primitives/withPadding.ts +56 -0
- package/src/solidOpts.ts +2 -2
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
type CoreSpeechType = string | (() => SpeechType) | SpeechType[];
|
|
2
|
+
export type SpeechType = CoreSpeechType | Promise<CoreSpeechType>;
|
|
3
|
+
|
|
4
|
+
export interface SeriesResult {
|
|
5
|
+
series: Promise<void>;
|
|
6
|
+
readonly active: boolean;
|
|
7
|
+
append: (toSpeak: SpeechType) => void;
|
|
8
|
+
cancel: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* global SpeechSynthesisErrorEvent */
|
|
12
|
+
function flattenStrings(series: SpeechType[] = []): SpeechType[] {
|
|
13
|
+
const flattenedSeries = [];
|
|
14
|
+
|
|
15
|
+
let i;
|
|
16
|
+
for (i = 0; i < series.length; i++) {
|
|
17
|
+
const s = series[i];
|
|
18
|
+
if (typeof s === 'string' && !s.includes('PAUSE-')) {
|
|
19
|
+
flattenedSeries.push(series[i]);
|
|
20
|
+
} else {
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// add a "word boundary" to ensure the Announcer doesn't automatically try to
|
|
25
|
+
// interpret strings that look like dates but are not actually dates
|
|
26
|
+
// for example, if "Rising Sun" and "1993" are meant to be two separate lines,
|
|
27
|
+
// when read together, "Sun 1993" is interpretted as "Sunday 1993"
|
|
28
|
+
return ([flattenedSeries.join(',\b ')] as SpeechType[]).concat(
|
|
29
|
+
series.slice(i),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function delay(pause: number) {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
setTimeout(resolve, pause);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Speak a string
|
|
41
|
+
*
|
|
42
|
+
* @param phrase Phrase to speak
|
|
43
|
+
* @param utterances An array which the new SpeechSynthesisUtterance instance representing this utterance will be appended
|
|
44
|
+
* @param lang Language to speak in
|
|
45
|
+
* @return {Promise<void>} Promise resolved when the utterance has finished speaking, and rejected if there's an error
|
|
46
|
+
*/
|
|
47
|
+
function speak(
|
|
48
|
+
phrase: string,
|
|
49
|
+
utterances: SpeechSynthesisUtterance[],
|
|
50
|
+
lang = 'en-US',
|
|
51
|
+
) {
|
|
52
|
+
const synth = window.speechSynthesis;
|
|
53
|
+
return new Promise<void>((resolve, reject) => {
|
|
54
|
+
const utterance = new SpeechSynthesisUtterance(phrase);
|
|
55
|
+
utterance.lang = lang;
|
|
56
|
+
utterance.onend = () => {
|
|
57
|
+
resolve();
|
|
58
|
+
};
|
|
59
|
+
utterance.onerror = (e) => {
|
|
60
|
+
reject(e);
|
|
61
|
+
};
|
|
62
|
+
utterances.push(utterance);
|
|
63
|
+
synth.speak(utterance);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function speakSeries(
|
|
68
|
+
series: SpeechType,
|
|
69
|
+
lang: string,
|
|
70
|
+
root = true,
|
|
71
|
+
): SeriesResult {
|
|
72
|
+
const synth = window.speechSynthesis;
|
|
73
|
+
const remainingPhrases = flattenStrings(
|
|
74
|
+
Array.isArray(series) ? series : [series],
|
|
75
|
+
);
|
|
76
|
+
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
|
+
const utterances: SpeechSynthesisUtterance[] = [];
|
|
83
|
+
let active: boolean = true;
|
|
84
|
+
|
|
85
|
+
const seriesChain = (async () => {
|
|
86
|
+
try {
|
|
87
|
+
while (active && remainingPhrases.length) {
|
|
88
|
+
const phrase = await Promise.resolve(remainingPhrases.shift());
|
|
89
|
+
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;
|
|
98
|
+
}
|
|
99
|
+
await delay(pause);
|
|
100
|
+
} else if (typeof phrase === 'string' && phrase.length) {
|
|
101
|
+
// Speak it
|
|
102
|
+
const totalRetries = 3;
|
|
103
|
+
let retriesLeft = totalRetries;
|
|
104
|
+
while (active && retriesLeft > 0) {
|
|
105
|
+
try {
|
|
106
|
+
await speak(phrase, utterances, lang);
|
|
107
|
+
retriesLeft = 0;
|
|
108
|
+
} catch (e) {
|
|
109
|
+
// eslint-disable-next-line no-undef
|
|
110
|
+
if (e instanceof SpeechSynthesisErrorEvent) {
|
|
111
|
+
if (e.error === 'network') {
|
|
112
|
+
retriesLeft--;
|
|
113
|
+
console.warn(
|
|
114
|
+
`Speech synthesis network error. Retries left: ${retriesLeft}`,
|
|
115
|
+
);
|
|
116
|
+
await delay(500 * (totalRetries - retriesLeft));
|
|
117
|
+
} else if (
|
|
118
|
+
e.error === 'canceled' ||
|
|
119
|
+
e.error === 'interrupted'
|
|
120
|
+
) {
|
|
121
|
+
// Cancel or interrupt error (ignore)
|
|
122
|
+
retriesLeft = 0;
|
|
123
|
+
} else {
|
|
124
|
+
throw new Error(`SpeechSynthesisErrorEvent: ${e.error}`);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
throw e;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} else if (typeof phrase === 'function') {
|
|
132
|
+
const seriesResult = speakSeries(phrase(), lang, false);
|
|
133
|
+
nestedSeriesResults.push(seriesResult);
|
|
134
|
+
await seriesResult.series;
|
|
135
|
+
} else if (Array.isArray(phrase)) {
|
|
136
|
+
// Speak it (recursively)
|
|
137
|
+
const seriesResult = speakSeries(phrase, lang, false);
|
|
138
|
+
nestedSeriesResults.push(seriesResult);
|
|
139
|
+
await seriesResult.series;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} finally {
|
|
143
|
+
active = false;
|
|
144
|
+
}
|
|
145
|
+
})();
|
|
146
|
+
return {
|
|
147
|
+
series: seriesChain,
|
|
148
|
+
get active() {
|
|
149
|
+
return active;
|
|
150
|
+
},
|
|
151
|
+
append: (toSpeak: SpeechType) => {
|
|
152
|
+
remainingPhrases.push(toSpeak);
|
|
153
|
+
},
|
|
154
|
+
cancel: () => {
|
|
155
|
+
if (!active) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (root) {
|
|
159
|
+
synth.cancel();
|
|
160
|
+
}
|
|
161
|
+
nestedSeriesResults.forEach((nestedSeriesResults) => {
|
|
162
|
+
nestedSeriesResults.cancel();
|
|
163
|
+
});
|
|
164
|
+
active = false;
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let currentSeries: SeriesResult | undefined;
|
|
170
|
+
export default function (toSpeak: SpeechType, lang: string = 'en-US') {
|
|
171
|
+
currentSeries && currentSeries.cancel();
|
|
172
|
+
currentSeries = speakSeries(toSpeak, lang);
|
|
173
|
+
return currentSeries;
|
|
174
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Accessor,
|
|
3
|
+
batch,
|
|
4
|
+
type Setter,
|
|
5
|
+
createComputed,
|
|
6
|
+
createResource,
|
|
7
|
+
createSignal,
|
|
8
|
+
} from 'solid-js';
|
|
9
|
+
|
|
10
|
+
// Adopted from https://github.com/solidjs-community/solid-primitives/blob/main/packages/pagination/src/index.ts
|
|
11
|
+
// As we don't have intersection observer in Lightning, we can't use the original implementation
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Provides an easy way to implement infinite items.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* const [items, loader, { item, setItem, setItems, end, setEnd }] = createInfiniteScroll(fetcher);
|
|
18
|
+
* ```
|
|
19
|
+
* @param fetcher `(item: number) => Promise<T[]>`
|
|
20
|
+
* @return `items()` is an accessor contains array of contents
|
|
21
|
+
* @property `items.loading` is a boolean indicator for the loading state
|
|
22
|
+
* @property `items.error` contains any error encountered
|
|
23
|
+
* @method `page` is an accessor that contains page number
|
|
24
|
+
* @method `setPage` allows to manually change the page number
|
|
25
|
+
* @method `setItems` allows to manually change the contents of the item
|
|
26
|
+
* @method `end` is a boolean indicator for end of the item
|
|
27
|
+
* @method `setEnd` allows to manually change the end
|
|
28
|
+
*/
|
|
29
|
+
export function createInfiniteItems<T>(
|
|
30
|
+
fetcher: (item: number) => Promise<T[]>,
|
|
31
|
+
): [
|
|
32
|
+
items: Accessor<T[]>,
|
|
33
|
+
options: {
|
|
34
|
+
page: Accessor<number>;
|
|
35
|
+
setPage: Setter<number>;
|
|
36
|
+
setItems: Setter<T[]>;
|
|
37
|
+
end: Accessor<boolean>;
|
|
38
|
+
setEnd: Setter<boolean>;
|
|
39
|
+
},
|
|
40
|
+
] {
|
|
41
|
+
const [items, setItems] = createSignal<T[]>([]);
|
|
42
|
+
const [page, setPage] = createSignal(0);
|
|
43
|
+
const [end, setEnd] = createSignal(false);
|
|
44
|
+
|
|
45
|
+
const [contents] = createResource(page, fetcher);
|
|
46
|
+
|
|
47
|
+
createComputed(() => {
|
|
48
|
+
const content = contents();
|
|
49
|
+
if (!content) return;
|
|
50
|
+
batch(() => {
|
|
51
|
+
if (content.length === 0) setEnd(true);
|
|
52
|
+
setItems((p) => [...p, ...content]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return [
|
|
57
|
+
items,
|
|
58
|
+
{
|
|
59
|
+
page,
|
|
60
|
+
setPage,
|
|
61
|
+
setItems,
|
|
62
|
+
end,
|
|
63
|
+
setEnd,
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { renderer } from '@lightningtv/solid';
|
|
2
|
+
import type { SpecificTextureRef } from '@lightningjs/renderer';
|
|
3
|
+
|
|
4
|
+
export interface SpriteDef {
|
|
5
|
+
name: string;
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createSpriteMap(src: string, subTextures: SpriteDef[]) {
|
|
13
|
+
const spriteMapTexture = renderer.createTexture('ImageTexture', {
|
|
14
|
+
src,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return subTextures.reduce<Record<string, SpecificTextureRef<'SubTexture'>>>(
|
|
18
|
+
(acc, t) => {
|
|
19
|
+
const { x, y, width, height } = t;
|
|
20
|
+
acc[t.name] = renderer.createTexture('SubTexture', {
|
|
21
|
+
texture: spriteMapTexture,
|
|
22
|
+
x,
|
|
23
|
+
y,
|
|
24
|
+
width,
|
|
25
|
+
height,
|
|
26
|
+
});
|
|
27
|
+
return acc;
|
|
28
|
+
},
|
|
29
|
+
{},
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { createEffect, on, createSignal, untrack } from 'solid-js';
|
|
2
|
+
import { useKeyDownEvent } from '@solid-primitives/keyboard';
|
|
3
|
+
import {
|
|
4
|
+
activeElement,
|
|
5
|
+
ElementNode,
|
|
6
|
+
isFunc,
|
|
7
|
+
isArray,
|
|
8
|
+
} from '@lightningtv/solid';
|
|
9
|
+
|
|
10
|
+
export interface DefaultKeyMap {
|
|
11
|
+
Left: string | number | (string | number)[];
|
|
12
|
+
Right: string | number | (string | number)[];
|
|
13
|
+
Up: string | number | (string | number)[];
|
|
14
|
+
Down: string | number | (string | number)[];
|
|
15
|
+
Enter: string | number | (string | number)[];
|
|
16
|
+
Last: string | number | (string | number)[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface KeyMap extends DefaultKeyMap {}
|
|
20
|
+
|
|
21
|
+
export type KeyHandlerReturn = boolean | void;
|
|
22
|
+
|
|
23
|
+
export type KeyHandler = (
|
|
24
|
+
this: ElementNode,
|
|
25
|
+
e: KeyboardEvent,
|
|
26
|
+
target: ElementNode,
|
|
27
|
+
handlerElm: ElementNode,
|
|
28
|
+
) => KeyHandlerReturn;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generates a map of event handlers for each key in the KeyMap
|
|
32
|
+
*/
|
|
33
|
+
type KeyMapEventHandlers = {
|
|
34
|
+
[K in keyof KeyMap as `on${Capitalize<K>}`]?: KeyHandler;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
declare module '@lightningtv/solid' {
|
|
38
|
+
/**
|
|
39
|
+
* Augment the existing IntrinsicCommonProps interface with our own
|
|
40
|
+
* FocusManager-specific properties.
|
|
41
|
+
*/
|
|
42
|
+
interface IntrinsicCommonProps extends KeyMapEventHandlers {
|
|
43
|
+
onFocus?: (
|
|
44
|
+
currentFocusedElm: ElementNode | undefined,
|
|
45
|
+
prevFocusedElm: ElementNode | undefined,
|
|
46
|
+
) => void;
|
|
47
|
+
onBlur?: (
|
|
48
|
+
currentFocusedElm: ElementNode | undefined,
|
|
49
|
+
prevFocusedElm: ElementNode | undefined,
|
|
50
|
+
) => void;
|
|
51
|
+
onKeyPress?: (
|
|
52
|
+
this: ElementNode,
|
|
53
|
+
e: KeyboardEvent,
|
|
54
|
+
mappedKeyEvent: string | undefined,
|
|
55
|
+
handlerElm: ElementNode,
|
|
56
|
+
currentFocusedElm: ElementNode,
|
|
57
|
+
) => KeyHandlerReturn;
|
|
58
|
+
onSelectedChanged?: (
|
|
59
|
+
container: ElementNode,
|
|
60
|
+
activeElm: ElementNode,
|
|
61
|
+
selectedIndex: number | undefined,
|
|
62
|
+
lastSelectedIndex: number | undefined,
|
|
63
|
+
) => void;
|
|
64
|
+
skipFocus?: boolean;
|
|
65
|
+
wrap?: boolean;
|
|
66
|
+
plinko?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface IntrinsicNodeStyleProps {
|
|
70
|
+
// TODO: Refactor states to use a $ prefix
|
|
71
|
+
focus?: IntrinsicNodeStyleProps;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface IntrinsicTextNodeStyleProps {
|
|
75
|
+
// TODO: Refactor states to use a $ prefix
|
|
76
|
+
focus?: IntrinsicTextNodeStyleProps;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface TextNode {
|
|
80
|
+
skipFocus?: undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const keyMapEntries: Record<string | number, string> = {
|
|
85
|
+
ArrowLeft: 'Left',
|
|
86
|
+
ArrowRight: 'Right',
|
|
87
|
+
ArrowUp: 'Up',
|
|
88
|
+
ArrowDown: 'Down',
|
|
89
|
+
Enter: 'Enter',
|
|
90
|
+
l: 'Last',
|
|
91
|
+
' ': 'Space',
|
|
92
|
+
Backspace: 'Back',
|
|
93
|
+
Escape: 'Escape',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const [focusPath, setFocusPath] = createSignal<ElementNode[]>([]);
|
|
97
|
+
export { focusPath };
|
|
98
|
+
export const useFocusManager = (userKeyMap?: Partial<KeyMap>) => {
|
|
99
|
+
const keypressEvent = useKeyDownEvent();
|
|
100
|
+
if (userKeyMap) {
|
|
101
|
+
// Flatten the userKeyMap to a hash
|
|
102
|
+
for (const [key, value] of Object.entries(userKeyMap)) {
|
|
103
|
+
if (isArray(value)) {
|
|
104
|
+
value.forEach((v) => {
|
|
105
|
+
keyMapEntries[v] = key;
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
keyMapEntries[value] = key;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
createEffect(
|
|
113
|
+
on(
|
|
114
|
+
activeElement,
|
|
115
|
+
(
|
|
116
|
+
currentFocusedElm: ElementNode,
|
|
117
|
+
prevFocusedElm: ElementNode | undefined,
|
|
118
|
+
prevFocusPath: ElementNode[] = [],
|
|
119
|
+
) => {
|
|
120
|
+
const newFocusedElms = [];
|
|
121
|
+
let current = currentFocusedElm;
|
|
122
|
+
|
|
123
|
+
const fp: ElementNode[] = [];
|
|
124
|
+
while (current) {
|
|
125
|
+
if (!current.states.has('focus')) {
|
|
126
|
+
current.states.add('focus');
|
|
127
|
+
isFunc(current.onFocus) &&
|
|
128
|
+
current.onFocus.call(current, currentFocusedElm, prevFocusedElm);
|
|
129
|
+
|
|
130
|
+
newFocusedElms.push(current);
|
|
131
|
+
}
|
|
132
|
+
fp.push(current);
|
|
133
|
+
current = current.parent!;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
prevFocusPath.forEach((elm) => {
|
|
137
|
+
if (!fp.includes(elm)) {
|
|
138
|
+
elm.states.remove('focus');
|
|
139
|
+
isFunc(elm.onBlur) &&
|
|
140
|
+
elm.onBlur.call(elm, currentFocusedElm, prevFocusedElm);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
setFocusPath(fp);
|
|
145
|
+
return fp;
|
|
146
|
+
},
|
|
147
|
+
{ defer: true },
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
createEffect(() => {
|
|
152
|
+
const e = keypressEvent();
|
|
153
|
+
|
|
154
|
+
if (e) {
|
|
155
|
+
// Search keyMap for the value of the pressed key or keyCode if value undefined
|
|
156
|
+
const mappedKeyEvent = keyMapEntries[e.key] || keyMapEntries[e.keyCode];
|
|
157
|
+
untrack(() => {
|
|
158
|
+
const fp = focusPath();
|
|
159
|
+
let finalFocusElm: ElementNode | undefined = undefined;
|
|
160
|
+
for (const elm of fp) {
|
|
161
|
+
finalFocusElm = finalFocusElm || elm;
|
|
162
|
+
if (mappedKeyEvent) {
|
|
163
|
+
const onKeyHandler =
|
|
164
|
+
elm[`on${mappedKeyEvent}` as keyof KeyMapEventHandlers];
|
|
165
|
+
if (isFunc(onKeyHandler)) {
|
|
166
|
+
if (onKeyHandler.call(elm, e, elm, finalFocusElm) === true) {
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
console.log(`Unhandled key event: ${e.key || e.keyCode}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (isFunc(elm.onKeyPress)) {
|
|
175
|
+
if (
|
|
176
|
+
elm.onKeyPress.call(
|
|
177
|
+
elm,
|
|
178
|
+
e,
|
|
179
|
+
mappedKeyEvent,
|
|
180
|
+
elm,
|
|
181
|
+
finalFocusElm,
|
|
182
|
+
) === true
|
|
183
|
+
) {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return focusPath;
|
|
194
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ElementNode } from '@lightningtv/core';
|
|
2
|
+
|
|
3
|
+
export type withPaddingInput =
|
|
4
|
+
| number
|
|
5
|
+
| [number, number]
|
|
6
|
+
| [number, number, number]
|
|
7
|
+
| [number, number, number, number];
|
|
8
|
+
|
|
9
|
+
// To use with TS import withPadding and then put withPadding; on the next line to prevent tree shaking
|
|
10
|
+
export function withPadding(el: ElementNode, padding: () => withPaddingInput) {
|
|
11
|
+
const pad = padding();
|
|
12
|
+
let top: number, left: number, right: number, bottom: number;
|
|
13
|
+
|
|
14
|
+
if (Array.isArray(pad)) {
|
|
15
|
+
// top right bottom left
|
|
16
|
+
if (pad.length === 2) {
|
|
17
|
+
top = bottom = pad[0]!;
|
|
18
|
+
left = right = pad[1]!;
|
|
19
|
+
} else if (pad.length === 3) {
|
|
20
|
+
top = pad[0]!;
|
|
21
|
+
left = right = pad[1]!;
|
|
22
|
+
bottom = pad[2]!;
|
|
23
|
+
} else {
|
|
24
|
+
[top, right, bottom, left] = pad;
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
top = right = bottom = left = pad;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
el.onBeforeLayout = (node, size) => {
|
|
31
|
+
if (size) {
|
|
32
|
+
el.width =
|
|
33
|
+
el.children.reduce((acc, c) => {
|
|
34
|
+
return acc + (c.width || 0);
|
|
35
|
+
}, 0) +
|
|
36
|
+
left +
|
|
37
|
+
right;
|
|
38
|
+
const firstChild = el.children[0];
|
|
39
|
+
if (firstChild) {
|
|
40
|
+
// set padding or marginLeft for flex
|
|
41
|
+
firstChild.x = left;
|
|
42
|
+
firstChild.marginLeft = left;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let maxHeight = 0;
|
|
46
|
+
el.children.forEach((c) => {
|
|
47
|
+
c.y = top;
|
|
48
|
+
c.marginTop = top;
|
|
49
|
+
maxHeight = Math.max(maxHeight, c.height || 0);
|
|
50
|
+
});
|
|
51
|
+
el.height = maxHeight + top + bottom;
|
|
52
|
+
// let flex know we need to re-layout
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
package/src/solidOpts.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { assertTruthy } from '@lightningjs/renderer/utils';
|
|
2
2
|
import type { SolidNode, TextNode } from '@lightningtv/core';
|
|
3
|
-
import { ElementNode,
|
|
3
|
+
import { ElementNode, NodeType, log } from '@lightningtv/core';
|
|
4
4
|
import type { createRenderer } from 'solid-js/universal';
|
|
5
5
|
|
|
6
6
|
export type SolidRendererOptions = Parameters<
|
|
@@ -13,7 +13,7 @@ export default {
|
|
|
13
13
|
},
|
|
14
14
|
createTextNode(text: string): TextNode {
|
|
15
15
|
// A text node is just a string - not the <text> node
|
|
16
|
-
return { type:
|
|
16
|
+
return { type: NodeType.Text, text, parent: undefined };
|
|
17
17
|
},
|
|
18
18
|
replaceText(node: TextNode, value: string): void {
|
|
19
19
|
log('Replace Text: ', node, value);
|