@lightningtv/solid 3.0.0-18 → 3.0.0-19
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/README.md +6 -0
- 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 +19 -6
- package/dist/src/primitives/KeepAlive.jsx +35 -65
- package/dist/src/primitives/KeepAlive.jsx.map +1 -1
- package/dist/src/primitives/Virtual.jsx +5 -3
- package/dist/src/primitives/Virtual.jsx.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/index.d.ts +2 -1
- package/dist/src/primitives/index.js +2 -1
- package/dist/src/primitives/index.js.map +1 -1
- package/dist/src/primitives/useMouse.d.ts +6 -0
- package/dist/src/primitives/useMouse.js +26 -3
- package/dist/src/primitives/useMouse.js.map +1 -1
- package/dist/src/primitives/utils/withScrolling.js +3 -2
- package/dist/src/primitives/utils/withScrolling.js.map +1 -1
- package/dist/src/render.d.ts +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/primitives/Image.tsx +36 -0
- package/src/primitives/KeepAlive.tsx +124 -0
- package/src/primitives/Virtual.tsx +6 -4
- package/src/primitives/createFocusStack.tsx +18 -7
- package/src/primitives/index.ts +2 -5
- package/src/primitives/useMouse.ts +52 -7
- package/src/primitives/utils/withScrolling.ts +2 -1
|
@@ -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
|
+
};
|
|
@@ -300,8 +300,10 @@ function createVirtual<T>(
|
|
|
300
300
|
if (itemCount() === 0) return;
|
|
301
301
|
|
|
302
302
|
lastNavTime = performance.now();
|
|
303
|
-
|
|
304
|
-
|
|
303
|
+
if (originalPosition !== undefined) {
|
|
304
|
+
viewRef.lng[axis] = originalPosition;
|
|
305
|
+
targetPosition = originalPosition;
|
|
306
|
+
}
|
|
305
307
|
|
|
306
308
|
updateSelected([utils.clamp(index, 0, itemCount() - 1)]);
|
|
307
309
|
});
|
|
@@ -405,9 +407,9 @@ function createVirtual<T>(
|
|
|
405
407
|
// offset just for wrap so we keep one item before
|
|
406
408
|
queueMicrotask(() => {
|
|
407
409
|
const childSize = computeSize(slice().selected);
|
|
408
|
-
viewRef.lng[axis] = viewRef.lng[axis]
|
|
410
|
+
viewRef.lng[axis] = (viewRef.lng[axis] || 0) + (childSize * -1);
|
|
409
411
|
// Original Position is offset to support scrollToIndex
|
|
410
|
-
originalPosition = viewRef.lng[axis]
|
|
412
|
+
originalPosition = viewRef.lng[axis];
|
|
411
413
|
targetPosition = viewRef.lng[axis];
|
|
412
414
|
});
|
|
413
415
|
}));
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - `restoreFocus()`: Restores focus to the last stored element and removes it from the stack. Returns `true` if successful, `false` otherwise.
|
|
18
18
|
* - `clearFocusStack()`: Empties the focus stack.
|
|
19
19
|
*/
|
|
20
|
-
import
|
|
20
|
+
import * as s from 'solid-js';
|
|
21
21
|
import { type ElementNode } from '@lightningtv/solid';
|
|
22
22
|
|
|
23
23
|
interface FocusStackContextType {
|
|
@@ -26,13 +26,16 @@ interface FocusStackContextType {
|
|
|
26
26
|
clearFocusStack: () => void;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const FocusStackContext = createContext<FocusStackContextType | undefined>(undefined);
|
|
29
|
+
const FocusStackContext = s.createContext<FocusStackContextType | undefined>(undefined);
|
|
30
30
|
|
|
31
|
-
export function FocusStackProvider(props: { children: JSX.Element}) {
|
|
32
|
-
const [_focusStack, setFocusStack] = createSignal<ElementNode[]>([]);
|
|
31
|
+
export function FocusStackProvider(props: { children: s.JSX.Element}) {
|
|
32
|
+
const [_focusStack, setFocusStack] = s.createSignal<ElementNode[]>([]);
|
|
33
33
|
|
|
34
34
|
function storeFocus(element: ElementNode, prevElement?: ElementNode) {
|
|
35
|
-
|
|
35
|
+
const elm = prevElement || element;
|
|
36
|
+
if (elm) {
|
|
37
|
+
setFocusStack(stack => [...stack, elm]);
|
|
38
|
+
}
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
function restoreFocus(): boolean {
|
|
@@ -59,10 +62,18 @@ export function FocusStackProvider(props: { children: JSX.Element}) {
|
|
|
59
62
|
);
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
export function useFocusStack() {
|
|
63
|
-
const context = useContext(FocusStackContext);
|
|
65
|
+
export function useFocusStack(autoClear = true) {
|
|
66
|
+
const context = s.useContext(FocusStackContext);
|
|
64
67
|
if (!context) {
|
|
65
68
|
throw new Error("useFocusStack must be used within a FocusStackProvider");
|
|
66
69
|
}
|
|
70
|
+
|
|
71
|
+
if (autoClear) {
|
|
72
|
+
s.onCleanup(() => {
|
|
73
|
+
// delay clearing the focus stack so restoreFocus can happen first.
|
|
74
|
+
setTimeout(() => context.clearFocusStack(), 5);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
return context;
|
|
68
79
|
}
|
package/src/primitives/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from './createInfiniteItems.js';
|
|
|
4
4
|
export * from './useMouse.js';
|
|
5
5
|
export * from './portal.jsx';
|
|
6
6
|
export * from './Lazy.jsx';
|
|
7
|
+
export * from './Image.jsx';
|
|
7
8
|
export * from './Visible.jsx';
|
|
8
9
|
export * from './router.js';
|
|
9
10
|
export * from './Column.jsx';
|
|
@@ -16,11 +17,7 @@ export * from './Suspense.jsx';
|
|
|
16
17
|
export * from './Marquee.jsx';
|
|
17
18
|
export * from './createFocusStack.jsx';
|
|
18
19
|
export * from './useHold.js';
|
|
19
|
-
export
|
|
20
|
-
withScrolling,
|
|
21
|
-
scrollColumn,
|
|
22
|
-
scrollRow,
|
|
23
|
-
} from './utils/withScrolling.js';
|
|
20
|
+
export * from './KeepAlive.jsx';
|
|
24
21
|
export * from './VirtualGrid.jsx';
|
|
25
22
|
export * from './Virtual.jsx';
|
|
26
23
|
export * from './utils/withScrolling.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ElementText,
|
|
1
|
+
import type { ElementText, TextNode } from '@lightningtv/core';
|
|
2
2
|
import {
|
|
3
3
|
ElementNode,
|
|
4
4
|
activeElement,
|
|
@@ -6,12 +6,24 @@ import {
|
|
|
6
6
|
isTextNode,
|
|
7
7
|
rootNode,
|
|
8
8
|
Config,
|
|
9
|
+
isFunc,
|
|
9
10
|
} from '@lightningtv/solid';
|
|
10
11
|
import { makeEventListener } from '@solid-primitives/event-listener';
|
|
11
12
|
import { useMousePosition } from '@solid-primitives/mouse';
|
|
12
13
|
import { createScheduled, throttle } from '@solid-primitives/scheduled';
|
|
13
14
|
import { createEffect } from 'solid-js';
|
|
14
15
|
|
|
16
|
+
declare module '@lightningtv/core' {
|
|
17
|
+
interface ElementNode {
|
|
18
|
+
/** function to be called on mouse click */
|
|
19
|
+
onMouseClick?: (
|
|
20
|
+
this: ElementNode,
|
|
21
|
+
event: MouseEvent,
|
|
22
|
+
active: ElementNode,
|
|
23
|
+
) => void;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
function createKeyboardEvent(
|
|
16
28
|
key: string,
|
|
17
29
|
keyCode: number,
|
|
@@ -29,6 +41,7 @@ function createKeyboardEvent(
|
|
|
29
41
|
});
|
|
30
42
|
}
|
|
31
43
|
|
|
44
|
+
let scrollTimeout: number;
|
|
32
45
|
const handleScroll = throttle((e: WheelEvent): void => {
|
|
33
46
|
const deltaY = e.deltaY;
|
|
34
47
|
if (deltaY < 0) {
|
|
@@ -36,6 +49,14 @@ const handleScroll = throttle((e: WheelEvent): void => {
|
|
|
36
49
|
} else if (deltaY > 0) {
|
|
37
50
|
document.body.dispatchEvent(createKeyboardEvent('ArrowDown', 40));
|
|
38
51
|
}
|
|
52
|
+
|
|
53
|
+
// clear the last timeout if the user is still scrolling
|
|
54
|
+
clearTimeout(scrollTimeout);
|
|
55
|
+
// after 250ms of no scroll events, we send a keyup event to stop the scrolling
|
|
56
|
+
scrollTimeout = setTimeout(() => {
|
|
57
|
+
document.body.dispatchEvent(createKeyboardEvent('ArrowUp', 38, 'keyup'));
|
|
58
|
+
document.body.dispatchEvent(createKeyboardEvent('ArrowDown', 40, 'keyup'));
|
|
59
|
+
}, 250);
|
|
39
60
|
}, 250);
|
|
40
61
|
|
|
41
62
|
const handleClick = (e: MouseEvent): void => {
|
|
@@ -46,18 +67,42 @@ const handleClick = (e: MouseEvent): void => {
|
|
|
46
67
|
testCollision(
|
|
47
68
|
e.clientX,
|
|
48
69
|
e.clientY,
|
|
49
|
-
(active.lng.absX as number) || 0 * precision,
|
|
50
|
-
(active.lng.absY as number) || 0 * precision,
|
|
51
|
-
active.width || 0 * precision,
|
|
52
|
-
active.height || 0 * precision,
|
|
70
|
+
((active.lng.absX as number) || 0) * precision,
|
|
71
|
+
((active.lng.absY as number) || 0) * precision,
|
|
72
|
+
(active.width || 0) * precision,
|
|
73
|
+
(active.height || 0) * precision,
|
|
53
74
|
)
|
|
54
75
|
) {
|
|
76
|
+
if (isFunc(active.onMouseClick)) {
|
|
77
|
+
active.onMouseClick.call(active, e, active);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
55
81
|
document.dispatchEvent(createKeyboardEvent('Enter', 13));
|
|
56
82
|
setTimeout(
|
|
57
83
|
() =>
|
|
58
84
|
document.body.dispatchEvent(createKeyboardEvent('Enter', 13, 'keyup')),
|
|
59
85
|
1,
|
|
60
86
|
);
|
|
87
|
+
} else {
|
|
88
|
+
let parent = active?.parent;
|
|
89
|
+
while (parent) {
|
|
90
|
+
if (
|
|
91
|
+
isFunc(parent.onMouseClick) &&
|
|
92
|
+
testCollision(
|
|
93
|
+
e.clientX,
|
|
94
|
+
e.clientY,
|
|
95
|
+
((parent.lng.absX as number) || 0) * precision,
|
|
96
|
+
((parent.lng.absY as number) || 0) * precision,
|
|
97
|
+
(parent.width || 0) * precision,
|
|
98
|
+
(parent.height || 0) * precision,
|
|
99
|
+
)
|
|
100
|
+
) {
|
|
101
|
+
parent.onMouseClick.call(parent, e, active!);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
parent = parent.parent;
|
|
105
|
+
}
|
|
61
106
|
}
|
|
62
107
|
};
|
|
63
108
|
|
|
@@ -95,8 +140,8 @@ function getChildrenByPosition(
|
|
|
95
140
|
testCollision(
|
|
96
141
|
x,
|
|
97
142
|
y,
|
|
98
|
-
(currentNode.lng.absX as number) || 0 * precision,
|
|
99
|
-
(currentNode.lng.absY as number) || 0 * precision,
|
|
143
|
+
((currentNode.lng.absX as number) || 0) * precision,
|
|
144
|
+
((currentNode.lng.absY as number) || 0) * precision,
|
|
100
145
|
(currentNode.width || 0) * precision,
|
|
101
146
|
(currentNode.height || 0) * precision,
|
|
102
147
|
)
|
|
@@ -73,6 +73,7 @@ export function withScrolling(isRow: boolean): Scroller {
|
|
|
73
73
|
if (componentRef.parent!.clipping) {
|
|
74
74
|
const p = componentRef.parent!;
|
|
75
75
|
componentRef.endOffset =
|
|
76
|
+
componentRef.endOffset ??
|
|
76
77
|
screenSize - ((isRow ? p.absX : p.absY) || 0) - p[dimension];
|
|
77
78
|
}
|
|
78
79
|
|
|
@@ -118,7 +119,7 @@ export function withScrolling(isRow: boolean): Scroller {
|
|
|
118
119
|
screenSize -
|
|
119
120
|
containerSize -
|
|
120
121
|
screenOffset -
|
|
121
|
-
(componentRef.endOffset
|
|
122
|
+
(componentRef.endOffset ?? 2 * gap),
|
|
122
123
|
offset,
|
|
123
124
|
);
|
|
124
125
|
|