@lightningtv/solid 3.0.0-1 → 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 +1 -3
- 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/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.jsx +15 -11
- 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 +5 -1
- package/dist/src/primitives/index.js +5 -1
- 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 +14 -1
- 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} +3 -3
- package/package.json +23 -8
- 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/Grid.tsx +59 -35
- package/src/primitives/Lazy.tsx +21 -12
- 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 +9 -1
- 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 +25 -2
- package/src/render.ts +12 -12
- package/src/solidOpts.ts +51 -7
- package/src/primitives/jsx-runtime.d.ts +0 -8
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type Stage, type NodeProps, RendererMain } from '@lightningtv/solid';
|
|
2
2
|
import { createSignal } from 'solid-js';
|
|
3
3
|
|
|
4
4
|
const fpsStyle = {
|
|
@@ -90,90 +90,90 @@ export function setupFPS(root: any) {
|
|
|
90
90
|
|
|
91
91
|
export const FPSCounter = (props: NodeProps) => {
|
|
92
92
|
return (
|
|
93
|
-
<
|
|
94
|
-
<
|
|
95
|
-
<
|
|
96
|
-
<
|
|
93
|
+
<view {...props} style={fpsStyle}>
|
|
94
|
+
<view y={6}>
|
|
95
|
+
<text style={fpsLabel}>FPS:</text>
|
|
96
|
+
<text style={fpsValue} x={90}>
|
|
97
97
|
{fps().toString()}
|
|
98
|
-
</
|
|
99
|
-
</
|
|
98
|
+
</text>
|
|
99
|
+
</view>
|
|
100
100
|
|
|
101
|
-
<
|
|
102
|
-
<
|
|
103
|
-
<
|
|
101
|
+
<view y={6} x={160}>
|
|
102
|
+
<text style={fpsLabel}>AVG:</text>
|
|
103
|
+
<text style={fpsValue} x={100}>
|
|
104
104
|
{avgFps().toString()}
|
|
105
|
-
</
|
|
106
|
-
</
|
|
105
|
+
</text>
|
|
106
|
+
</view>
|
|
107
107
|
|
|
108
|
-
<
|
|
109
|
-
<
|
|
110
|
-
<
|
|
108
|
+
<view x={0} y={26}>
|
|
109
|
+
<text style={fpsLabel}>MIN:</text>
|
|
110
|
+
<text style={fpsValue} x={90}>
|
|
111
111
|
{minFps().toString()}
|
|
112
|
-
</
|
|
113
|
-
</
|
|
112
|
+
</text>
|
|
113
|
+
</view>
|
|
114
114
|
|
|
115
|
-
<
|
|
116
|
-
<
|
|
117
|
-
<
|
|
115
|
+
<view x={160} y={26}>
|
|
116
|
+
<text style={fpsLabel}>MAX:</text>
|
|
117
|
+
<text style={fpsValue} x={100}>
|
|
118
118
|
{maxFps().toString()}
|
|
119
|
-
</
|
|
120
|
-
</
|
|
119
|
+
</text>
|
|
120
|
+
</view>
|
|
121
121
|
|
|
122
|
-
<
|
|
123
|
-
<
|
|
124
|
-
<
|
|
122
|
+
<view display="flex" flexDirection="column" y={58} gap={4}>
|
|
123
|
+
<view height={infoFontSize}>
|
|
124
|
+
<text fontSize={infoFontSize} style={fpsLabel}>
|
|
125
125
|
criticalThreshold:
|
|
126
|
-
</
|
|
127
|
-
<
|
|
126
|
+
</text>
|
|
127
|
+
<text fontSize={infoFontSize} style={fpsLabel} x={230}>
|
|
128
128
|
{criticalThresholdSignal()}
|
|
129
|
-
</
|
|
130
|
-
</
|
|
129
|
+
</text>
|
|
130
|
+
</view>
|
|
131
131
|
|
|
132
|
-
<
|
|
133
|
-
<
|
|
132
|
+
<view height={infoFontSize}>
|
|
133
|
+
<text fontSize={infoFontSize} style={fpsLabel}>
|
|
134
134
|
targetThreshold:
|
|
135
|
-
</
|
|
136
|
-
<
|
|
135
|
+
</text>
|
|
136
|
+
<text fontSize={infoFontSize} style={fpsLabel} x={230}>
|
|
137
137
|
{targetThresholdSignal()}
|
|
138
|
-
</
|
|
139
|
-
</
|
|
138
|
+
</text>
|
|
139
|
+
</view>
|
|
140
140
|
|
|
141
|
-
<
|
|
142
|
-
<
|
|
141
|
+
<view height={infoFontSize}>
|
|
142
|
+
<text fontSize={infoFontSize} style={fpsLabel}>
|
|
143
143
|
renderableMemUsed:
|
|
144
|
-
</
|
|
145
|
-
<
|
|
144
|
+
</text>
|
|
145
|
+
<text fontSize={infoFontSize} style={fpsLabel} x={230}>
|
|
146
146
|
{renderableMemUsedSignal()}
|
|
147
|
-
</
|
|
148
|
-
</
|
|
147
|
+
</text>
|
|
148
|
+
</view>
|
|
149
149
|
|
|
150
|
-
<
|
|
151
|
-
<
|
|
150
|
+
<view height={infoFontSize}>
|
|
151
|
+
<text fontSize={infoFontSize} style={fpsLabel}>
|
|
152
152
|
memUsed:
|
|
153
|
-
</
|
|
154
|
-
<
|
|
153
|
+
</text>
|
|
154
|
+
<text fontSize={infoFontSize} style={fpsLabel} x={230}>
|
|
155
155
|
{memUsedSignal()}
|
|
156
|
-
</
|
|
157
|
-
</
|
|
156
|
+
</text>
|
|
157
|
+
</view>
|
|
158
158
|
|
|
159
|
-
<
|
|
160
|
-
<
|
|
159
|
+
<view height={infoFontSize}>
|
|
160
|
+
<text fontSize={infoFontSize} style={fpsLabel}>
|
|
161
161
|
renderableTexturesLoaded:
|
|
162
|
-
</
|
|
163
|
-
<
|
|
162
|
+
</text>
|
|
163
|
+
<text fontSize={infoFontSize} style={fpsLabel} x={230}>
|
|
164
164
|
{renderableTexturesLoadedSignal().toString()}
|
|
165
|
-
</
|
|
166
|
-
</
|
|
165
|
+
</text>
|
|
166
|
+
</view>
|
|
167
167
|
|
|
168
|
-
<
|
|
169
|
-
<
|
|
168
|
+
<view height={infoFontSize}>
|
|
169
|
+
<text fontSize={infoFontSize} style={fpsLabel}>
|
|
170
170
|
loadedTextures:
|
|
171
|
-
</
|
|
172
|
-
<
|
|
171
|
+
</text>
|
|
172
|
+
<text fontSize={infoFontSize} style={fpsLabel} x={230}>
|
|
173
173
|
{loadedTexturesSignal().toString()}
|
|
174
|
-
</
|
|
175
|
-
</
|
|
176
|
-
</
|
|
177
|
-
</
|
|
174
|
+
</text>
|
|
175
|
+
</view>
|
|
176
|
+
</view>
|
|
177
|
+
</view>
|
|
178
178
|
);
|
|
179
179
|
};
|
package/src/primitives/Grid.tsx
CHANGED
|
@@ -1,25 +1,51 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { For, createSignal, createMemo, createEffect, JSX } from "solid-js";
|
|
2
|
+
import { type NodeProps, ElementNode, NewOmit } from "@lightningtv/solid";
|
|
3
|
+
import { chainRefs } from "./utils/chainFunctions.js";
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
-
item:
|
|
5
|
+
export interface GridItemProps<T> {
|
|
6
|
+
item: T
|
|
7
|
+
index: number
|
|
8
|
+
width: number
|
|
9
|
+
height: number
|
|
10
|
+
x: number
|
|
11
|
+
y: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GridProps<T> extends NewOmit<NodeProps, 'children'> {
|
|
15
|
+
items: readonly T[];
|
|
16
|
+
children: (props: GridItemProps<T>) => JSX.Element,
|
|
6
17
|
itemHeight?: number;
|
|
7
18
|
itemWidth?: number;
|
|
8
19
|
itemOffset?: number;
|
|
9
|
-
items: T[];
|
|
10
20
|
columns?: number;
|
|
11
21
|
looping?: boolean;
|
|
12
22
|
scroll?: "auto" | "none";
|
|
13
23
|
onSelectedChanged?: (index: number, grid: ElementNode, elm?: ElementNode) => void;
|
|
14
|
-
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function Grid<T>(props: GridProps<T>): JSX.Element {
|
|
27
|
+
|
|
15
28
|
const [focusedIndex, setFocusedIndex] = createSignal(0);
|
|
16
29
|
const baseColumns = 4;
|
|
17
30
|
|
|
31
|
+
const itemWidth = () => props.itemWidth ?? 300
|
|
32
|
+
const itemHeight = () => props.itemHeight ?? 300
|
|
33
|
+
|
|
18
34
|
const columns = createMemo(() => props.columns || baseColumns);
|
|
19
|
-
const totalWidth = createMemo(() => (
|
|
20
|
-
const totalHeight = createMemo(() => (
|
|
35
|
+
const totalWidth = createMemo(() => itemWidth() + (props.itemOffset ?? 0));
|
|
36
|
+
const totalHeight = createMemo(() => itemHeight() + (props.itemOffset ?? 0));
|
|
37
|
+
|
|
38
|
+
function focus() {
|
|
39
|
+
const focusedElm = gridRef.children[focusedIndex()];
|
|
40
|
+
if (focusedElm instanceof ElementNode && !focusedElm.states.has('$focus')) {
|
|
41
|
+
focusedElm.setFocus();
|
|
42
|
+
props.onSelectedChanged?.call(gridRef, focusedIndex(), gridRef, focusedElm);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
21
47
|
|
|
22
|
-
|
|
48
|
+
function moveFocus(delta: number) {
|
|
23
49
|
if (!props.items || props.items.length === 0) return false;
|
|
24
50
|
const newIndex = focusedIndex() + delta;
|
|
25
51
|
|
|
@@ -37,13 +63,10 @@ export const Grid = <T,>(props: {
|
|
|
37
63
|
} else {
|
|
38
64
|
return false;
|
|
39
65
|
}
|
|
40
|
-
|
|
41
|
-
focusedElm.setFocus();
|
|
42
|
-
isFunction(props.onSelectedChanged) && props.onSelectedChanged.call(elm, focusedIndex(), elm, focusedElm);
|
|
43
|
-
return true;
|
|
66
|
+
return focus();
|
|
44
67
|
};
|
|
45
68
|
|
|
46
|
-
|
|
69
|
+
function handleHorizontalFocus(delta: number) {
|
|
47
70
|
if (!props.items || props.items.length === 0) return false;
|
|
48
71
|
const newIndex = focusedIndex() + delta;
|
|
49
72
|
const isWithinRow = Math.floor(newIndex / columns()) === Math.floor(focusedIndex() / columns());
|
|
@@ -57,45 +80,46 @@ export const Grid = <T,>(props: {
|
|
|
57
80
|
} else {
|
|
58
81
|
return false;
|
|
59
82
|
}
|
|
60
|
-
|
|
61
|
-
focusedElm.setFocus();
|
|
62
|
-
isFunction(props.onSelectedChanged) && props.onSelectedChanged.call(elm, focusedIndex(), elm, focusedElm);
|
|
63
|
-
return true;
|
|
83
|
+
return focus();
|
|
64
84
|
};
|
|
65
85
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
// Handle focus when items change - important for autofocus
|
|
87
|
+
createEffect(() => {
|
|
88
|
+
if (props.items && props.items.length > 0 && gridRef && gridRef.states.has('$focus')) {
|
|
89
|
+
queueMicrotask(focus)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
69
92
|
|
|
70
93
|
const scrollY = createMemo(() =>
|
|
71
94
|
props.scroll === "none" ? props.y ?? 0 : -Math.floor(focusedIndex() / columns()) * totalHeight() + (props.y || 0)
|
|
72
95
|
);
|
|
73
96
|
|
|
97
|
+
let gridRef!: ElementNode;
|
|
74
98
|
return (
|
|
75
|
-
<
|
|
76
|
-
transition={{ y: true }}
|
|
99
|
+
<view
|
|
77
100
|
{...props}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
ref={chainRefs(el => gridRef = el, props.ref)}
|
|
102
|
+
transition={{ y: true }}
|
|
103
|
+
onUp={() => moveFocus(-columns())}
|
|
104
|
+
onDown={() => moveFocus(columns())}
|
|
105
|
+
onLeft={() => handleHorizontalFocus(-1)}
|
|
106
|
+
onRight={() => handleHorizontalFocus(1)}
|
|
107
|
+
onFocus={() => handleHorizontalFocus(0)}
|
|
83
108
|
strictBounds={false}
|
|
84
109
|
y={scrollY()}
|
|
85
110
|
>
|
|
86
111
|
<For each={props.items}>
|
|
87
112
|
{(item, index) => (
|
|
88
|
-
<
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
width={
|
|
92
|
-
height={
|
|
113
|
+
<props.children
|
|
114
|
+
item={item}
|
|
115
|
+
index={index()}
|
|
116
|
+
width={itemWidth()}
|
|
117
|
+
height={itemHeight()}
|
|
93
118
|
x={(index() % columns()) * totalWidth()}
|
|
94
119
|
y={Math.floor(index() / columns()) * totalHeight()}
|
|
95
120
|
/>
|
|
96
121
|
)}
|
|
97
122
|
</For>
|
|
98
|
-
</
|
|
123
|
+
</view>
|
|
99
124
|
);
|
|
100
125
|
};
|
|
101
|
-
|
package/src/primitives/Lazy.tsx
CHANGED
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
type ValidComponent,
|
|
9
9
|
untrack,
|
|
10
10
|
type Accessor,
|
|
11
|
-
} from 'solid-js';
|
|
12
|
-
import {
|
|
11
|
+
} from 'solid-js'; // Dynamic removed
|
|
12
|
+
import { type NewOmit, scheduleTask, type NodeProps, Dynamic } from '@lightningtv/solid'; // Dynamic removed from imports
|
|
13
13
|
import { Row, Column } from '@lightningtv/solid/primitives';
|
|
14
14
|
|
|
15
15
|
type LazyProps<T extends readonly any[]> = NewOmit<NodeProps, 'children'> & {
|
|
@@ -28,26 +28,23 @@ function createLazy<T>(
|
|
|
28
28
|
keyHandler: (updateOffset: () => void) => Record<string, () => void>
|
|
29
29
|
) {
|
|
30
30
|
// Need at least one item so it can be focused
|
|
31
|
-
const [offset, setOffset] = createSignal(1);
|
|
31
|
+
const [offset, setOffset] = createSignal<number>(props.sync ? props.upCount : 1);
|
|
32
32
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
33
33
|
|
|
34
|
-
createEffect(() => setOffset(props.selected || 1));
|
|
34
|
+
createEffect(() => setOffset(offset => Math.max(offset, (props.selected || 0) + 1)));
|
|
35
35
|
|
|
36
|
-
if (props.sync) {
|
|
37
|
-
setOffset(props.upCount);
|
|
38
|
-
} else {
|
|
36
|
+
if (!props.sync || props.eaglerLoad) {
|
|
39
37
|
createEffect(() => {
|
|
40
38
|
if (props.each) {
|
|
41
|
-
let count = untrack(offset);
|
|
42
|
-
|
|
43
39
|
const loadItems = () => {
|
|
40
|
+
let count = untrack(offset);
|
|
44
41
|
if (count < props.upCount) {
|
|
45
42
|
setOffset(count + 1);
|
|
46
43
|
timeoutId = setTimeout(loadItems, 16); // ~60fps
|
|
47
44
|
count++;
|
|
48
45
|
} else if (props.eagerLoad) {
|
|
49
46
|
const maxOffset = props.each ? props.each.length : 0;
|
|
50
|
-
if (offset()
|
|
47
|
+
if (offset() >= maxOffset) return;
|
|
51
48
|
setOffset((prev) => Math.min(prev + 1, maxOffset));
|
|
52
49
|
scheduleTask(loadItems);
|
|
53
50
|
}
|
|
@@ -57,13 +54,25 @@ function createLazy<T>(
|
|
|
57
54
|
});
|
|
58
55
|
}
|
|
59
56
|
|
|
60
|
-
const items = createMemo(() => (
|
|
57
|
+
const items = createMemo(() => (
|
|
58
|
+
Array.isArray(props.each) ? props.each.slice(0, offset()) : [])
|
|
59
|
+
);
|
|
61
60
|
|
|
62
61
|
const updateOffset = () => {
|
|
63
62
|
const maxOffset = props.each ? props.each.length : 0;
|
|
64
63
|
if (offset() >= maxOffset) return;
|
|
65
64
|
|
|
66
|
-
if (
|
|
65
|
+
if (!props.delay) {
|
|
66
|
+
setOffset((prev) => Math.min(prev + 1, maxOffset));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (timeoutId) {
|
|
71
|
+
clearTimeout(timeoutId);
|
|
72
|
+
//Moving faster than the delay so need to go sync
|
|
73
|
+
setOffset((prev) => Math.min(prev + 1, maxOffset));
|
|
74
|
+
}
|
|
75
|
+
|
|
67
76
|
timeoutId = setTimeout(() => {
|
|
68
77
|
setOffset((prev) => Math.min(prev + 1, maxOffset));
|
|
69
78
|
timeoutId = null;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as s from 'solid-js'
|
|
2
|
+
import * as lng from '@lightningtv/solid'
|
|
3
|
+
import { chainFunctions } from './utils/chainFunctions.js'
|
|
4
|
+
|
|
5
|
+
export interface MarqueeAnimationProps {
|
|
6
|
+
/** delay in ms between animations, @default 1000 */
|
|
7
|
+
delay?: number
|
|
8
|
+
/** pixels per second, @default 200 */
|
|
9
|
+
speed?: number
|
|
10
|
+
/**
|
|
11
|
+
* distance between the end of the text and the start of the next animation
|
|
12
|
+
* @default `0.5 * clipWidth`
|
|
13
|
+
*/
|
|
14
|
+
scrollGap?: number
|
|
15
|
+
/** easing function, @default 'linear' */
|
|
16
|
+
easing?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MarqueeControlProps {
|
|
20
|
+
/** whether to scroll the text or show it contained */
|
|
21
|
+
marquee: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MarqueeTextProps
|
|
25
|
+
extends lng.TextProps, MarqueeControlProps, MarqueeAnimationProps {
|
|
26
|
+
/** width of the container */
|
|
27
|
+
clipWidth: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MarqueeProps
|
|
31
|
+
extends lng.NewOmit<lng.NodeProps, 'children'>, MarqueeControlProps, MarqueeAnimationProps {
|
|
32
|
+
textProps?: lng.TextProps
|
|
33
|
+
children: lng.TextProps['children']
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SAFETY_MARGIN = 10
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* MarqueeText is a component that scrolls text when it overflows the container.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* <view width={400} height={28} clipping>
|
|
44
|
+
* <MarqueeText
|
|
45
|
+
* clipWidth={400}
|
|
46
|
+
* marquee={inFocus()}
|
|
47
|
+
* speed={200}
|
|
48
|
+
* delay={1000}
|
|
49
|
+
* scrollGap={24}
|
|
50
|
+
* easing='ease-in-out'
|
|
51
|
+
* >
|
|
52
|
+
* This is a long text that will scroll when it overflows the container.
|
|
53
|
+
* </MarqueeText>
|
|
54
|
+
* </view>
|
|
55
|
+
*/
|
|
56
|
+
export function MarqueeText(props: MarqueeTextProps) {
|
|
57
|
+
|
|
58
|
+
const speed = s.createMemo(() => props.speed || 200)
|
|
59
|
+
const delay = s.createMemo(() => props.delay ?? 1000)
|
|
60
|
+
const scrollGap = s.createMemo(() => props.scrollGap ?? (props.clipWidth * 0.5))
|
|
61
|
+
|
|
62
|
+
const [textWidth, setTextWidth] = s.createSignal(0)
|
|
63
|
+
|
|
64
|
+
const isTextOverflowing = s.createMemo(() => textWidth() > props.clipWidth - SAFETY_MARGIN)
|
|
65
|
+
const shouldScroll = s.createMemo(() => props.marquee && isTextOverflowing())
|
|
66
|
+
|
|
67
|
+
const wasFocusedBefore = s.createMemo<boolean>(p => p || props.marquee, false)
|
|
68
|
+
|
|
69
|
+
s.createEffect(() => {
|
|
70
|
+
if (shouldScroll()) {
|
|
71
|
+
|
|
72
|
+
let options: lng.AnimationSettings = {
|
|
73
|
+
duration: (textWidth() + scrollGap()) / speed() * 1000,
|
|
74
|
+
delay: delay(),
|
|
75
|
+
loop: true,
|
|
76
|
+
easing: props.easing,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
text1.lng.x = 0
|
|
80
|
+
text2.lng.x = textWidth() + scrollGap()
|
|
81
|
+
|
|
82
|
+
let a1 = text1.lng.animate!({x: -textWidth() -scrollGap()}, options).start()
|
|
83
|
+
let a2 = text2.lng.animate!({x: 0}, options).start()
|
|
84
|
+
|
|
85
|
+
s.onCleanup(() => {
|
|
86
|
+
a1.stop()
|
|
87
|
+
a2.stop()
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const events = {loaded(el: lng.ElementNode) {setTextWidth(el.width)}}
|
|
93
|
+
|
|
94
|
+
let text1!: lng.ElementNode
|
|
95
|
+
let text2!: lng.ElementNode
|
|
96
|
+
return (
|
|
97
|
+
<>
|
|
98
|
+
{wasFocusedBefore() && <>
|
|
99
|
+
<text {...props} ref={text1} hidden={!shouldScroll()} rtt maxLines={1} onEvent={events} />
|
|
100
|
+
<text {...props} ref={text2} hidden={!shouldScroll()} rtt maxLines={1} />
|
|
101
|
+
</>}
|
|
102
|
+
<text {...props} maxLines={1} hidden={shouldScroll()} contain='width' />
|
|
103
|
+
</>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Marquee is a component that scrolls text when it overflows the container.
|
|
109
|
+
* It uses the {@link MarqueeText} component to do the actual scrolling.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```tsx
|
|
113
|
+
* <Marquee
|
|
114
|
+
* width={400}
|
|
115
|
+
* marquee={inFocus()}
|
|
116
|
+
* easing='ease-in-out'
|
|
117
|
+
* textProps={{
|
|
118
|
+
* fontSize: 28,
|
|
119
|
+
* }}
|
|
120
|
+
* >
|
|
121
|
+
* This is a long text that will scroll when it overflows the container.
|
|
122
|
+
* </Marquee>
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export function Marquee(props: MarqueeProps) {
|
|
126
|
+
const [clipWidth, setClipWidth] = s.createSignal(props.width || 0);
|
|
127
|
+
const clipHeight = s.createMemo(() => props.height || props.textProps?.lineHeight || ((props.textProps?.fontSize || 16) * 1.5));
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<view
|
|
131
|
+
{...props}
|
|
132
|
+
height={clipHeight()}
|
|
133
|
+
onLayout={/* @once */ chainFunctions(props.onLayout, (e: lng.ElementNode) => setClipWidth(e.width))}
|
|
134
|
+
clipping={props.marquee}
|
|
135
|
+
>
|
|
136
|
+
<MarqueeText
|
|
137
|
+
{...props.textProps}
|
|
138
|
+
marquee={props.marquee}
|
|
139
|
+
clipWidth={clipWidth()}
|
|
140
|
+
speed={props.speed}
|
|
141
|
+
delay={props.delay}
|
|
142
|
+
scrollGap={props.scrollGap}
|
|
143
|
+
easing={props.easing}
|
|
144
|
+
>
|
|
145
|
+
{props.children}
|
|
146
|
+
</MarqueeText>
|
|
147
|
+
</view>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as s from 'solid-js'
|
|
2
|
+
import * as lng from '@lightningtv/solid'
|
|
3
|
+
|
|
4
|
+
function Preserve(props: lng.NodeProps): s.JSX.Element {
|
|
5
|
+
|
|
6
|
+
let view = <view {...props} /> as any as lng.ElementNode
|
|
7
|
+
|
|
8
|
+
view.preserve = true;
|
|
9
|
+
|
|
10
|
+
view.onRender ??= () => {view.hidden = false}
|
|
11
|
+
view.onRemove ??= () => {view.hidden = true}
|
|
12
|
+
|
|
13
|
+
s.onCleanup(() => {view.destroy()})
|
|
14
|
+
|
|
15
|
+
return view as any as s.JSX.Element
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default Preserve;
|
package/src/primitives/Row.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Component } from 'solid-js';
|
|
2
|
-
import { combineStyles, type NodeStyles, View, type ElementNode } from '@lightningtv/solid';
|
|
2
|
+
import { combineStyles, type NodeStyles, View, type ElementNode, Config } from '@lightningtv/solid';
|
|
3
3
|
import { chainFunctions } from './utils/chainFunctions.js';
|
|
4
4
|
import {
|
|
5
5
|
handleNavigation,
|
|
@@ -30,17 +30,24 @@ function scrollToIndex(this: ElementNode, index: number) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export const Row: Component<RowProps> = (props) => {
|
|
33
|
+
const gridFocus = onGridFocus(props.onSelectedChanged);
|
|
34
|
+
const refocus = (elm: ElementNode) => {
|
|
35
|
+
if (elm.states.has(Config.focusStateKey)) {
|
|
36
|
+
gridFocus.call(elm);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
33
40
|
return (
|
|
34
|
-
<
|
|
41
|
+
<view
|
|
35
42
|
{...props}
|
|
36
43
|
selected={props.selected || 0}
|
|
37
44
|
onLeft={/* @once */ chainFunctions(props.onLeft, onLeft)}
|
|
38
45
|
onRight={/* @once */ chainFunctions(props.onRight, onRight)}
|
|
39
|
-
forwardFocus={
|
|
46
|
+
forwardFocus={gridFocus}
|
|
40
47
|
scrollToIndex={scrollToIndex}
|
|
41
48
|
onLayout={
|
|
42
49
|
/* @once */
|
|
43
|
-
props.selected ? chainFunctions(props.onLayout, scroll) : props.onLayout
|
|
50
|
+
props.selected ? chainFunctions(props.onLayout, refocus, scroll) : props.onLayout
|
|
44
51
|
}
|
|
45
52
|
onSelectedChanged={
|
|
46
53
|
/* @once */ chainFunctions(
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as s from 'solid-js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tracks all resources inside a component and renders a fallback until they are all resolved.
|
|
5
|
+
*
|
|
6
|
+
* ```tsx
|
|
7
|
+
* const [data] = createResource(async () => ...);
|
|
8
|
+
*
|
|
9
|
+
* <Suspense fallback={<LoadingIndicator />}>
|
|
10
|
+
* <view>
|
|
11
|
+
* <text>{data()}</text>
|
|
12
|
+
* </view>
|
|
13
|
+
* </Suspense>
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* This is a modified version of the SolidJS Suspense component that works with Lightning.
|
|
17
|
+
*
|
|
18
|
+
* @see https://docs.solidjs.com/reference/components/suspense
|
|
19
|
+
*/
|
|
20
|
+
function Suspense(props: {
|
|
21
|
+
fallback?: s.JSX.Element;
|
|
22
|
+
children: s.JSX.Element;
|
|
23
|
+
}): s.JSX.Element {
|
|
24
|
+
|
|
25
|
+
let children: s.JSX.Element;
|
|
26
|
+
|
|
27
|
+
let suspense = s.Suspense({
|
|
28
|
+
get children() {
|
|
29
|
+
return [children = s.children(() => props.children) as any];
|
|
30
|
+
},
|
|
31
|
+
}) as any as () => s.JSX.Element;
|
|
32
|
+
|
|
33
|
+
return <>
|
|
34
|
+
{suspense() ?? props.fallback}
|
|
35
|
+
<view hidden>
|
|
36
|
+
{suspense() ? null : children}
|
|
37
|
+
</view>
|
|
38
|
+
</>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default Suspense;
|
|
@@ -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) {
|