@lightningtv/solid 3.0.0-2 → 3.0.0-20
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/README.md +6 -0
- package/dist/src/jsx-runtime.d.ts +1 -3
- package/dist/src/primitives/Column.jsx +9 -10
- package/dist/src/primitives/Column.jsx.map +1 -1
- package/dist/src/primitives/Grid.d.ts +15 -6
- package/dist/src/primitives/Grid.jsx +35 -22
- package/dist/src/primitives/Grid.jsx.map +1 -1
- package/dist/src/primitives/Image.d.ts +8 -0
- package/dist/src/primitives/Image.jsx +24 -0
- package/dist/src/primitives/Image.jsx.map +1 -0
- package/dist/src/primitives/KeepAlive.d.ts +30 -0
- package/dist/src/primitives/KeepAlive.jsx +77 -0
- package/dist/src/primitives/KeepAlive.jsx.map +1 -0
- package/dist/src/primitives/Lazy.d.ts +8 -7
- package/dist/src/primitives/Lazy.jsx +49 -23
- package/dist/src/primitives/Lazy.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 -10
- package/dist/src/primitives/Row.jsx.map +1 -1
- package/dist/src/primitives/Suspense.d.ts +22 -0
- package/dist/src/primitives/Suspense.jsx +33 -0
- package/dist/src/primitives/Suspense.jsx.map +1 -0
- package/dist/src/primitives/Virtual.d.ts +18 -0
- package/dist/src/primitives/Virtual.jsx +434 -0
- package/dist/src/primitives/Virtual.jsx.map +1 -0
- package/dist/src/primitives/VirtualGrid.d.ts +13 -0
- package/dist/src/primitives/VirtualGrid.jsx +139 -0
- package/dist/src/primitives/VirtualGrid.jsx.map +1 -0
- package/dist/src/primitives/VirtualList.d.ts +11 -0
- package/dist/src/primitives/VirtualList.jsx +96 -0
- package/dist/src/primitives/VirtualList.jsx.map +1 -0
- package/dist/src/primitives/VirtualRow.d.ts +13 -0
- package/dist/src/primitives/VirtualRow.jsx +97 -0
- package/dist/src/primitives/VirtualRow.jsx.map +1 -0
- package/dist/src/primitives/Visible.d.ts +0 -1
- package/dist/src/primitives/Visible.jsx +1 -1
- package/dist/src/primitives/Visible.jsx.map +1 -1
- package/dist/src/primitives/announcer/announcer.d.ts +2 -0
- package/dist/src/primitives/announcer/announcer.js +7 -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 +157 -28
- package/dist/src/primitives/announcer/speech.js.map +1 -1
- package/dist/src/primitives/createFocusStack.d.ts +4 -4
- package/dist/src/primitives/createFocusStack.jsx +15 -6
- package/dist/src/primitives/createFocusStack.jsx.map +1 -1
- package/dist/src/primitives/createTag.d.ts +8 -0
- package/dist/src/primitives/createTag.jsx +20 -0
- package/dist/src/primitives/createTag.jsx.map +1 -0
- package/dist/src/primitives/index.d.ts +13 -3
- package/dist/src/primitives/index.js +13 -3
- package/dist/src/primitives/index.js.map +1 -1
- package/dist/src/primitives/types.d.ts +3 -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/useMouse.d.ts +24 -1
- package/dist/src/primitives/useMouse.js +153 -47
- package/dist/src/primitives/useMouse.js.map +1 -1
- 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/createBlurredImage.d.ts +56 -0
- package/dist/src/primitives/utils/createBlurredImage.js +223 -0
- package/dist/src/primitives/utils/createBlurredImage.js.map +1 -0
- package/dist/src/primitives/utils/createSpriteMap.d.ts +2 -2
- package/dist/src/primitives/utils/createSpriteMap.js.map +1 -1
- package/dist/src/primitives/utils/handleNavigation.d.ts +85 -5
- package/dist/src/primitives/utils/handleNavigation.js +242 -69
- package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
- package/dist/src/primitives/utils/withScrolling.d.ts +8 -1
- package/dist/src/primitives/utils/withScrolling.js +25 -6
- package/dist/src/primitives/utils/withScrolling.js.map +1 -1
- package/dist/src/render.d.ts +6 -5
- package/dist/src/render.js +4 -0
- package/dist/src/render.js.map +1 -1
- package/dist/src/solidOpts.d.ts +3 -2
- package/dist/src/solidOpts.js +31 -15
- package/dist/src/solidOpts.js.map +1 -1
- package/dist/src/universal.d.ts +25 -0
- package/dist/src/universal.js +232 -0
- package/dist/src/universal.js.map +1 -0
- package/dist/src/utils.d.ts +2 -0
- package/dist/src/utils.js +8 -0
- package/dist/src/utils.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/jsx-runtime.d.ts +2 -4
- package/package.json +19 -10
- package/src/primitives/Column.tsx +10 -12
- package/src/primitives/Grid.tsx +57 -33
- package/src/primitives/Image.tsx +36 -0
- package/src/primitives/KeepAlive.tsx +124 -0
- package/src/primitives/Lazy.tsx +60 -37
- package/src/primitives/Marquee.tsx +149 -0
- package/src/primitives/Preserve.tsx +18 -0
- package/src/primitives/Row.tsx +11 -12
- package/src/primitives/Suspense.tsx +39 -0
- package/src/primitives/Virtual.tsx +478 -0
- package/src/primitives/VirtualGrid.tsx +199 -0
- package/src/primitives/Visible.tsx +1 -2
- package/src/primitives/announcer/announcer.ts +16 -10
- package/src/primitives/announcer/index.ts +12 -2
- package/src/primitives/announcer/speech.ts +188 -27
- package/src/primitives/createFocusStack.tsx +18 -7
- package/src/primitives/createTag.tsx +31 -0
- package/src/primitives/index.ts +17 -3
- package/src/primitives/types.ts +10 -0
- package/src/primitives/useHold.ts +69 -0
- package/src/primitives/useMouse.ts +283 -66
- package/src/primitives/utils/chainFunctions.ts +40 -9
- package/src/primitives/utils/createBlurredImage.ts +366 -0
- package/src/primitives/utils/createSpriteMap.ts +6 -4
- package/src/primitives/utils/handleNavigation.ts +307 -84
- package/src/primitives/utils/withScrolling.ts +47 -16
- package/src/render.ts +9 -7
- package/src/solidOpts.ts +34 -19
- package/src/utils.ts +10 -0
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,39 +80,41 @@ 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
99
|
<view
|
|
76
|
-
transition={{ y: true }}
|
|
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
|
/>
|
|
@@ -98,4 +123,3 @@ export const Grid = <T,>(props: {
|
|
|
98
123
|
</view>
|
|
99
124
|
);
|
|
100
125
|
};
|
|
101
|
-
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Component, createRenderEffect, createSignal } from 'solid-js';
|
|
2
|
+
import { renderer, type NodeProps, type ImageTexture} from '@lightningtv/solid';
|
|
3
|
+
export interface ImageProps extends NodeProps {
|
|
4
|
+
src: string;
|
|
5
|
+
/* image to load while src is being loaded */
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
fallback?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Image: Component<ImageProps> = (props) => {
|
|
11
|
+
const [texture, setTexture] = createSignal<any>(null);
|
|
12
|
+
const [src, setSrc] = createSignal<string | null>(props.placeholder || null);
|
|
13
|
+
|
|
14
|
+
createRenderEffect(() => {
|
|
15
|
+
const srcTexture = renderer.createTexture('ImageTexture', props) as ImageTexture;
|
|
16
|
+
|
|
17
|
+
if (props.fallback) {
|
|
18
|
+
srcTexture.once('failed', () => {
|
|
19
|
+
if (props.fallback === props.placeholder) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
setSrc(props.fallback!);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
srcTexture.getTextureData().then(resp => {
|
|
27
|
+
// if texture fails to load, this is still called after the failed handler
|
|
28
|
+
if (resp.data)
|
|
29
|
+
setTexture(srcTexture);
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<view {...props} src={src()} color={props.color || 0xffffffff} texture={texture()} />
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Route, RoutePreloadFuncArgs, RouteProps } from "@solidjs/router";
|
|
2
|
+
import * as s from 'solid-js';
|
|
3
|
+
import { ElementNode } from "@lightningtv/solid";
|
|
4
|
+
|
|
5
|
+
export interface KeepAliveElement {
|
|
6
|
+
id: string;
|
|
7
|
+
owner: s.Owner | null;
|
|
8
|
+
children: s.JSX.Element;
|
|
9
|
+
routeSignal?: s.Signal<unknown>;
|
|
10
|
+
dispose: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const keepAliveElements = new Map<string, KeepAliveElement>();
|
|
14
|
+
|
|
15
|
+
export const storeKeepAlive = (
|
|
16
|
+
element: KeepAliveElement
|
|
17
|
+
): KeepAliveElement | undefined => {
|
|
18
|
+
if (keepAliveElements.has(element.id)) {
|
|
19
|
+
console.warn(`[KeepAlive] Element with id "${element.id}" already in cache. Recreating.`);
|
|
20
|
+
return element;
|
|
21
|
+
}
|
|
22
|
+
keepAliveElements.set(element.id, element);
|
|
23
|
+
return element;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const removeKeepAlive = (id: string): void => {
|
|
27
|
+
const element = keepAliveElements.get(id);
|
|
28
|
+
if (element) {
|
|
29
|
+
element.dispose();
|
|
30
|
+
keepAliveElements.delete(id);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
interface KeepAliveProps {
|
|
35
|
+
id: string;
|
|
36
|
+
shouldDispose?: (key: string) => boolean;
|
|
37
|
+
onRemove?: ElementNode['onRemove'];
|
|
38
|
+
onRender?: ElementNode['onRender'];
|
|
39
|
+
transition?: ElementNode['transition'];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function wrapChildren(props: s.ParentProps<KeepAliveProps>) {
|
|
43
|
+
const onRemove = props.onRemove || ((elm: ElementNode) => { elm.alpha = 0; });
|
|
44
|
+
const onRender = props.onRender || ((elm: ElementNode) => { elm.alpha = 1; });
|
|
45
|
+
const transition = props.transition || { alpha: true };
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<view
|
|
49
|
+
preserve
|
|
50
|
+
onRemove={onRemove}
|
|
51
|
+
onRender={onRender}
|
|
52
|
+
forwardFocus={0}
|
|
53
|
+
transition={transition}
|
|
54
|
+
{...props}
|
|
55
|
+
/>)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const KeepAlive = (props: s.ParentProps<KeepAliveProps>) => {
|
|
59
|
+
let existing = keepAliveElements.get(props.id)
|
|
60
|
+
|
|
61
|
+
if (existing && props.shouldDispose?.(props.id)) {
|
|
62
|
+
existing.dispose();
|
|
63
|
+
keepAliveElements.delete(props.id);
|
|
64
|
+
existing = undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!existing) {
|
|
68
|
+
return s.createRoot((dispose) => {
|
|
69
|
+
const children = wrapChildren(props);
|
|
70
|
+
storeKeepAlive({
|
|
71
|
+
id: props.id,
|
|
72
|
+
owner: s.getOwner(),
|
|
73
|
+
children,
|
|
74
|
+
dispose,
|
|
75
|
+
});
|
|
76
|
+
return children;
|
|
77
|
+
});
|
|
78
|
+
} else if (existing && !existing.children) {
|
|
79
|
+
existing.children = s.runWithOwner(existing.owner, () => wrapChildren(props));
|
|
80
|
+
}
|
|
81
|
+
return existing.children;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const KeepAliveRoute = <S extends string>(props: RouteProps<S> & {
|
|
85
|
+
id?: string,
|
|
86
|
+
path: string,
|
|
87
|
+
component: s.Component<RouteProps<S>>,
|
|
88
|
+
shouldDispose?: (key: string) => boolean,
|
|
89
|
+
onRemove?: ElementNode['onRemove'];
|
|
90
|
+
onRender?: ElementNode['onRender'];
|
|
91
|
+
transition?: ElementNode['transition'];
|
|
92
|
+
}) => {
|
|
93
|
+
const key = props.id || props.path;
|
|
94
|
+
|
|
95
|
+
const preload = props.preload ? (preloadProps: RoutePreloadFuncArgs) => {
|
|
96
|
+
let existing = keepAliveElements.get(key)
|
|
97
|
+
|
|
98
|
+
if (existing && props.shouldDispose?.(key)) {
|
|
99
|
+
existing.dispose();
|
|
100
|
+
keepAliveElements.delete(key);
|
|
101
|
+
existing = undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!existing) {
|
|
105
|
+
return s.createRoot((dispose) => {
|
|
106
|
+
storeKeepAlive({
|
|
107
|
+
id: key,
|
|
108
|
+
owner: s.getOwner(),
|
|
109
|
+
dispose,
|
|
110
|
+
children: null,
|
|
111
|
+
});
|
|
112
|
+
return props.preload!(preloadProps);
|
|
113
|
+
});
|
|
114
|
+
} else if (existing.children) {
|
|
115
|
+
(existing.children as unknown as ElementNode)?.setFocus();
|
|
116
|
+
}
|
|
117
|
+
} : undefined;
|
|
118
|
+
|
|
119
|
+
return (<Route {...props} preload={preload} component={(childProps) =>
|
|
120
|
+
<KeepAlive id={key} onRemove={props.onRemove} onRender={props.onRender} transition={props.transition}>
|
|
121
|
+
{props.component(childProps)}
|
|
122
|
+
</KeepAlive>
|
|
123
|
+
}/>);
|
|
124
|
+
};
|
package/src/primitives/Lazy.tsx
CHANGED
|
@@ -1,54 +1,55 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
createMemo,
|
|
5
|
-
createSignal,
|
|
6
|
-
Show,
|
|
7
|
-
type JSX,
|
|
8
|
-
type ValidComponent,
|
|
9
|
-
untrack,
|
|
10
|
-
type Accessor,
|
|
11
|
-
} from 'solid-js';
|
|
12
|
-
import { Dynamic, type NewOmit, scheduleTask, type NodeProps } from '@lightningtv/solid';
|
|
13
|
-
import { Row, Column } from '@lightningtv/solid/primitives';
|
|
1
|
+
import * as s from 'solid-js';
|
|
2
|
+
import * as lng from '@lightningtv/solid';
|
|
3
|
+
import * as lngp from '@lightningtv/solid/primitives';
|
|
14
4
|
|
|
15
|
-
type LazyProps<T extends readonly any[]> = NewOmit<NodeProps, 'children'> & {
|
|
5
|
+
type LazyProps<T extends readonly any[]> = lng.NewOmit<lng.NodeProps, 'children'> & {
|
|
16
6
|
each: T | undefined | null | false;
|
|
17
|
-
fallback?: JSX.Element;
|
|
18
7
|
upCount: number;
|
|
8
|
+
buffer?: number;
|
|
19
9
|
delay?: number;
|
|
20
10
|
sync?: boolean;
|
|
21
11
|
eagerLoad?: boolean;
|
|
22
|
-
|
|
12
|
+
noRefocus?: boolean;
|
|
13
|
+
children: (item: s.Accessor<T[number]>, index: number) => s.JSX.Element;
|
|
23
14
|
};
|
|
24
15
|
|
|
25
16
|
function createLazy<T>(
|
|
26
|
-
component: ValidComponent,
|
|
17
|
+
component: s.ValidComponent,
|
|
27
18
|
props: LazyProps<readonly T[]>,
|
|
28
|
-
keyHandler: (updateOffset: () => void) => Record<string, () => void>
|
|
19
|
+
keyHandler: (updateOffset: (event: KeyboardEvent, container: lng.ElementNode) => void) => Record<string, (event: KeyboardEvent, container: lng.ElementNode) => void>
|
|
29
20
|
) {
|
|
30
21
|
// Need at least one item so it can be focused
|
|
31
|
-
const [offset, setOffset] = createSignal(
|
|
22
|
+
const [offset, setOffset] = s.createSignal<number>(props.sync ? props.upCount : 0);
|
|
32
23
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
24
|
+
let viewRef!: lngp.NavigableElement;
|
|
25
|
+
let itemLength: number = 0;
|
|
33
26
|
|
|
34
|
-
|
|
27
|
+
const buffer = s.createMemo(() => {
|
|
28
|
+
if (typeof props.buffer === 'number') {
|
|
29
|
+
return props.buffer;
|
|
30
|
+
}
|
|
31
|
+
const scroll = props.scroll || props.style?.scroll;
|
|
32
|
+
if (!scroll || scroll === 'auto' || scroll === 'always') return props.upCount + 1;
|
|
33
|
+
if (scroll === 'center') return Math.ceil(props.upCount / 2) + 1;
|
|
34
|
+
return 2;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
s.createRenderEffect(() => setOffset(offset => Math.max(offset, (props.selected || 0) + buffer())));
|
|
35
38
|
|
|
36
|
-
if (props.sync) {
|
|
37
|
-
|
|
38
|
-
} else {
|
|
39
|
-
createEffect(() => {
|
|
39
|
+
if (!props.sync || props.eagerLoad) {
|
|
40
|
+
s.createEffect(() => {
|
|
40
41
|
if (props.each) {
|
|
41
42
|
const loadItems = () => {
|
|
42
|
-
let count = untrack(offset);
|
|
43
|
+
let count = s.untrack(offset);
|
|
43
44
|
if (count < props.upCount) {
|
|
44
45
|
setOffset(count + 1);
|
|
45
46
|
timeoutId = setTimeout(loadItems, 16); // ~60fps
|
|
46
47
|
count++;
|
|
47
48
|
} else if (props.eagerLoad) {
|
|
48
49
|
const maxOffset = props.each ? props.each.length : 0;
|
|
49
|
-
if (
|
|
50
|
+
if (count >= maxOffset) return;
|
|
50
51
|
setOffset((prev) => Math.min(prev + 1, maxOffset));
|
|
51
|
-
scheduleTask(loadItems);
|
|
52
|
+
lng.scheduleTask(loadItems);
|
|
52
53
|
}
|
|
53
54
|
};
|
|
54
55
|
loadItems();
|
|
@@ -56,11 +57,30 @@ function createLazy<T>(
|
|
|
56
57
|
});
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
const items = createMemo(() =>
|
|
60
|
+
const items: s.Accessor<T[]> = s.createMemo(() => {
|
|
61
|
+
if (Array.isArray(props.each)) {
|
|
62
|
+
if (itemLength != props.each.length) {
|
|
63
|
+
itemLength = props.each.length;
|
|
64
|
+
if (viewRef && !viewRef.noRefocus && lng.hasFocus(viewRef)) {
|
|
65
|
+
queueMicrotask(viewRef.setFocus);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return props.each.slice(0, offset());
|
|
69
|
+
}
|
|
70
|
+
itemLength = 0;
|
|
71
|
+
return [];
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function lazyScrollToIndex(this: lngp.NavigableElement, index: number) {
|
|
75
|
+
setOffset(Math.max(index, 0) + buffer())
|
|
76
|
+
queueMicrotask(() => viewRef.scrollToIndex(index));
|
|
77
|
+
}
|
|
60
78
|
|
|
61
|
-
const updateOffset = () => {
|
|
79
|
+
const updateOffset = (_event: KeyboardEvent, container: lng.ElementNode) => {
|
|
62
80
|
const maxOffset = props.each ? props.each.length : 0;
|
|
63
|
-
|
|
81
|
+
const selected = container.selected || 0;
|
|
82
|
+
const numChildren = container.children.length;
|
|
83
|
+
if (offset() >= maxOffset || selected < numChildren - buffer()) return;
|
|
64
84
|
|
|
65
85
|
if (!props.delay) {
|
|
66
86
|
setOffset((prev) => Math.min(prev + 1, maxOffset));
|
|
@@ -82,18 +102,21 @@ function createLazy<T>(
|
|
|
82
102
|
const handler = keyHandler(updateOffset);
|
|
83
103
|
|
|
84
104
|
return (
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
105
|
+
<lng.Dynamic
|
|
106
|
+
{...props}
|
|
107
|
+
component={component}
|
|
108
|
+
{/* @once */ ...handler}
|
|
109
|
+
lazyScrollToIndex={lazyScrollToIndex}
|
|
110
|
+
ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)} >
|
|
111
|
+
<s.Index each={items()} children={props.children} />
|
|
112
|
+
</lng.Dynamic>
|
|
90
113
|
);
|
|
91
114
|
}
|
|
92
115
|
|
|
93
116
|
export function LazyRow<T extends readonly any[]>(props: LazyProps<T>) {
|
|
94
|
-
return createLazy(Row, props, (updateOffset) => ({ onRight: updateOffset }));
|
|
117
|
+
return createLazy(lngp.Row, props, (updateOffset) => ({ onRight: updateOffset }));
|
|
95
118
|
}
|
|
96
119
|
|
|
97
120
|
export function LazyColumn<T extends readonly any[]>(props: LazyProps<T>) {
|
|
98
|
-
return createLazy(Column, props, (updateOffset) => ({ onDown: updateOffset }));
|
|
121
|
+
return createLazy(lngp.Column, props, (updateOffset) => ({ onDown: updateOffset }));
|
|
99
122
|
}
|
|
@@ -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,12 +1,12 @@
|
|
|
1
1
|
import { type Component } from 'solid-js';
|
|
2
|
-
import { combineStyles, type NodeStyles,
|
|
2
|
+
import { combineStyles, type NodeStyles, type ElementNode } from '@lightningtv/solid';
|
|
3
3
|
import { chainFunctions } from './utils/chainFunctions.js';
|
|
4
4
|
import {
|
|
5
5
|
handleNavigation,
|
|
6
|
-
|
|
6
|
+
navigableForwardFocus
|
|
7
7
|
} from './utils/handleNavigation.js';
|
|
8
|
-
import { withScrolling } from './utils/withScrolling.js';
|
|
9
8
|
import type { RowProps } from './types.js';
|
|
9
|
+
import { scrollRow } from './utils/withScrolling.js';
|
|
10
10
|
|
|
11
11
|
const RowStyles: NodeStyles = {
|
|
12
12
|
display: 'flex',
|
|
@@ -19,16 +19,15 @@ const RowStyles: NodeStyles = {
|
|
|
19
19
|
},
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
const onLeft = handleNavigation('left');
|
|
23
|
-
const onRight = handleNavigation('right');
|
|
24
|
-
const scroll = withScrolling(true);
|
|
25
|
-
|
|
26
22
|
function scrollToIndex(this: ElementNode, index: number) {
|
|
27
23
|
this.selected = index;
|
|
28
|
-
|
|
29
|
-
this.setFocus();
|
|
24
|
+
scrollRow(index, this);
|
|
25
|
+
this.children[index]?.setFocus();
|
|
30
26
|
}
|
|
31
27
|
|
|
28
|
+
const onLeft = handleNavigation('left');
|
|
29
|
+
const onRight = handleNavigation('right');
|
|
30
|
+
|
|
32
31
|
export const Row: Component<RowProps> = (props) => {
|
|
33
32
|
return (
|
|
34
33
|
<view
|
|
@@ -36,16 +35,16 @@ export const Row: Component<RowProps> = (props) => {
|
|
|
36
35
|
selected={props.selected || 0}
|
|
37
36
|
onLeft={/* @once */ chainFunctions(props.onLeft, onLeft)}
|
|
38
37
|
onRight={/* @once */ chainFunctions(props.onRight, onRight)}
|
|
39
|
-
forwardFocus={
|
|
38
|
+
forwardFocus={navigableForwardFocus}
|
|
40
39
|
scrollToIndex={scrollToIndex}
|
|
41
40
|
onLayout={
|
|
42
41
|
/* @once */
|
|
43
|
-
props.selected ? chainFunctions(props.onLayout,
|
|
42
|
+
props.selected ? chainFunctions(props.onLayout, scrollRow) : props.onLayout
|
|
44
43
|
}
|
|
45
44
|
onSelectedChanged={
|
|
46
45
|
/* @once */ chainFunctions(
|
|
47
46
|
props.onSelectedChanged,
|
|
48
|
-
props.scroll !== 'none' ?
|
|
47
|
+
props.scroll !== 'none' ? scrollRow : undefined,
|
|
49
48
|
)
|
|
50
49
|
}
|
|
51
50
|
style={/* @once */ combineStyles(props.style, RowStyles)}
|