@lightningtv/solid 3.0.0-17 → 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/Column.jsx +8 -7
- package/dist/src/primitives/Column.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 +19 -5
- package/dist/src/primitives/KeepAlive.jsx +52 -21
- package/dist/src/primitives/KeepAlive.jsx.map +1 -1
- package/dist/src/primitives/Lazy.d.ts +6 -7
- package/dist/src/primitives/Lazy.jsx +23 -20
- package/dist/src/primitives/Lazy.jsx.map +1 -1
- package/dist/src/primitives/Row.jsx +8 -7
- package/dist/src/primitives/Row.jsx.map +1 -1
- package/dist/src/primitives/Virtual.d.ts +18 -0
- package/dist/src/primitives/Virtual.jsx +428 -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/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 +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 +1 -0
- 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/handleNavigation.d.ts +0 -1
- package/dist/src/primitives/utils/handleNavigation.js +7 -5
- package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
- package/dist/src/primitives/utils/withScrolling.d.ts +5 -1
- package/dist/src/primitives/utils/withScrolling.js +11 -5
- package/dist/src/primitives/utils/withScrolling.js.map +1 -1
- package/dist/src/render.d.ts +1 -0
- package/dist/src/render.js +4 -0
- package/dist/src/render.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/package.json +7 -4
- package/src/primitives/Column.tsx +10 -9
- package/src/primitives/Image.tsx +36 -0
- package/src/primitives/KeepAlive.tsx +124 -0
- package/src/primitives/Lazy.tsx +34 -38
- package/src/primitives/Row.tsx +11 -9
- package/src/primitives/Virtual.tsx +471 -0
- package/src/primitives/VirtualGrid.tsx +199 -0
- package/src/primitives/Visible.tsx +1 -2
- package/src/primitives/createFocusStack.tsx +18 -7
- package/src/primitives/index.ts +5 -1
- package/src/primitives/types.ts +1 -0
- package/src/primitives/useMouse.ts +52 -7
- package/src/primitives/utils/handleNavigation.ts +8 -5
- package/src/primitives/utils/withScrolling.ts +22 -14
- package/src/render.ts +5 -0
- package/src/utils.ts +10 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import * as s from 'solid-js';
|
|
2
|
+
import * as lng from '@lightningtv/solid';
|
|
3
|
+
import * as lngp from '@lightningtv/solid/primitives';
|
|
4
|
+
import { List } from '@solid-primitives/list';
|
|
5
|
+
import * as utils from '../utils.js';
|
|
6
|
+
|
|
7
|
+
const columnScroll = lngp.withScrolling(false);
|
|
8
|
+
|
|
9
|
+
const rowStyles: lng.NodeStyles = {
|
|
10
|
+
display: 'flex',
|
|
11
|
+
flexWrap: 'wrap',
|
|
12
|
+
transition: {
|
|
13
|
+
y: true,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type VirtualGridProps<T> = lng.NewOmit<lngp.RowProps, 'children'> & {
|
|
18
|
+
each: readonly T[] | undefined | null | false;
|
|
19
|
+
columns: number; // items per row
|
|
20
|
+
rows?: number; // number of visible rows (default: 1)
|
|
21
|
+
buffer?: number;
|
|
22
|
+
onEndReached?: () => void;
|
|
23
|
+
onEndReachedThreshold?: number;
|
|
24
|
+
children: (item: s.Accessor<T>, index: s.Accessor<number>) => s.JSX.Element;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function VirtualGrid<T>(props: VirtualGridProps<T>): s.JSX.Element {
|
|
28
|
+
const bufferSize = () => props.buffer ?? 2;
|
|
29
|
+
const [ cursor, setCursor ] = s.createSignal(props.selected ?? 0);
|
|
30
|
+
const items = s.createMemo(() => props.each || []);
|
|
31
|
+
const itemsPerRow = () => props.columns;
|
|
32
|
+
const numberOfRows = () => props.rows ?? 1;
|
|
33
|
+
const totalVisibleItems = () => itemsPerRow() * numberOfRows();
|
|
34
|
+
|
|
35
|
+
const start = s.createMemo(() => {
|
|
36
|
+
const perRow = itemsPerRow();
|
|
37
|
+
const newRowIndex = Math.floor(cursor() / perRow);
|
|
38
|
+
const rawStart = newRowIndex * perRow - bufferSize() * perRow;
|
|
39
|
+
return Math.max(0, rawStart);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const end = s.createMemo(() => {
|
|
43
|
+
const perRow = itemsPerRow();
|
|
44
|
+
const newRowIndex = Math.floor(cursor() / perRow);
|
|
45
|
+
const rawEnd = (newRowIndex + bufferSize()) * perRow + totalVisibleItems();
|
|
46
|
+
return Math.min(items().length, rawEnd);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const [slice, setSlice] = s.createSignal(items().slice(start(), end()));
|
|
50
|
+
|
|
51
|
+
let viewRef!: lngp.NavigableElement;
|
|
52
|
+
|
|
53
|
+
function onVerticalNav(dir: -1 | 1): lngp.KeyHandler {
|
|
54
|
+
return function () {
|
|
55
|
+
const perRow = itemsPerRow();
|
|
56
|
+
const currentRowIndex = Math.floor(cursor() / perRow);
|
|
57
|
+
const maxRows = Math.floor(items().length / perRow);
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
currentRowIndex === 0 && dir === -1
|
|
61
|
+
|| currentRowIndex === maxRows && dir === 1
|
|
62
|
+
) return;
|
|
63
|
+
|
|
64
|
+
const selected = this.selected || 0;
|
|
65
|
+
const offset = dir * perRow;
|
|
66
|
+
const newIndex = utils.clamp(selected + offset, 0, items().length - 1);
|
|
67
|
+
const lastIdx = selected;
|
|
68
|
+
this.selected = newIndex;
|
|
69
|
+
const active = this.children[this.selected];
|
|
70
|
+
|
|
71
|
+
if (active instanceof lng.ElementNode) {
|
|
72
|
+
active.setFocus();
|
|
73
|
+
chainedOnSelectedChanged.call(
|
|
74
|
+
this as lngp.NavigableElement,
|
|
75
|
+
this.selected,
|
|
76
|
+
this as lngp.NavigableElement,
|
|
77
|
+
active,
|
|
78
|
+
lastIdx
|
|
79
|
+
);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const onUp = onVerticalNav(-1);
|
|
86
|
+
const onDown = onVerticalNav(1);
|
|
87
|
+
|
|
88
|
+
const onSelectedChanged: lngp.OnSelectedChanged = function (_idx, elm, active, _lastIdx,) {
|
|
89
|
+
let idx = _idx;
|
|
90
|
+
let lastIdx = _lastIdx;
|
|
91
|
+
const perRow = itemsPerRow();
|
|
92
|
+
const newRowIndex = Math.floor(idx / perRow);
|
|
93
|
+
const prevRowIndex = Math.floor((lastIdx || 0) / perRow);
|
|
94
|
+
const prevStart = start();
|
|
95
|
+
|
|
96
|
+
setCursor(prevStart + idx);
|
|
97
|
+
if (newRowIndex === prevRowIndex) return;
|
|
98
|
+
|
|
99
|
+
setSlice(items().slice(start(), end()));
|
|
100
|
+
|
|
101
|
+
// this.selected is relative to the slice
|
|
102
|
+
// and it doesn't get corrected automatically after children change
|
|
103
|
+
const idxCorrection = prevStart - start();
|
|
104
|
+
if (lastIdx) lastIdx += idxCorrection;
|
|
105
|
+
idx += idxCorrection;
|
|
106
|
+
this.selected += idxCorrection;
|
|
107
|
+
|
|
108
|
+
if (props.onEndReachedThreshold !== undefined && cursor() >= items().length - props.onEndReachedThreshold) {
|
|
109
|
+
props.onEndReached?.();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
queueMicrotask(() => {
|
|
113
|
+
const prevRowY = this.y + active.y;
|
|
114
|
+
this.updateLayout();
|
|
115
|
+
this.lng.y = prevRowY - active.y;
|
|
116
|
+
columnScroll(idx, elm, active, lastIdx);
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const chainedOnSelectedChanged = lngp.chainFunctions(props.onSelectedChanged, onSelectedChanged)!;
|
|
121
|
+
|
|
122
|
+
let cachedSelected: number | undefined;
|
|
123
|
+
const updateSelected = ([selected, _items]: [number?, any?]) => {
|
|
124
|
+
if (!viewRef || selected == null) return;
|
|
125
|
+
|
|
126
|
+
if (cachedSelected !== undefined) {
|
|
127
|
+
selected = cachedSelected;
|
|
128
|
+
cachedSelected = undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (selected >= items().length && props.onEndReached) {
|
|
132
|
+
props.onEndReached?.();
|
|
133
|
+
cachedSelected = selected;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const item = items()[selected];
|
|
138
|
+
let active = viewRef.children.find(x => x.item === item);
|
|
139
|
+
const lastSelected = viewRef.selected;
|
|
140
|
+
|
|
141
|
+
if (active instanceof lng.ElementNode) {
|
|
142
|
+
viewRef.selected = viewRef.children.indexOf(active);
|
|
143
|
+
active.setFocus();
|
|
144
|
+
chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
|
|
145
|
+
} else {
|
|
146
|
+
setCursor(selected);
|
|
147
|
+
setSlice(items().slice(start(), end()));
|
|
148
|
+
|
|
149
|
+
queueMicrotask(() => {
|
|
150
|
+
viewRef.updateLayout();
|
|
151
|
+
active = viewRef.children.find(x => x.item === item);
|
|
152
|
+
if (active instanceof lng.ElementNode) {
|
|
153
|
+
viewRef.selected = viewRef.children.indexOf(active);
|
|
154
|
+
active.setFocus();
|
|
155
|
+
chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const scrollToIndex = (index: number) => {
|
|
162
|
+
s.untrack(() => updateSelected([index]));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
s.createEffect(s.on([() => props.selected, items], updateSelected));
|
|
166
|
+
|
|
167
|
+
s.createEffect(
|
|
168
|
+
s.on(items, () => {
|
|
169
|
+
if (!viewRef) return;
|
|
170
|
+
if (cachedSelected !== undefined) {
|
|
171
|
+
updateSelected([cachedSelected]);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
setSlice(items().slice(start(), end()));
|
|
175
|
+
}, { defer: true })
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<view
|
|
181
|
+
{...props}
|
|
182
|
+
scroll={props.scroll || 'always'}
|
|
183
|
+
ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)}
|
|
184
|
+
selected={props.selected || 0}
|
|
185
|
+
cursor={cursor()}
|
|
186
|
+
onLeft={/* @once */ lngp.chainFunctions(props.onLeft, lngp.navigableHandleNavigation)}
|
|
187
|
+
onRight={/* @once */ lngp.chainFunctions(props.onRight, lngp.navigableHandleNavigation)}
|
|
188
|
+
onUp={/* @once */ lngp.chainFunctions(props.onUp, onUp)}
|
|
189
|
+
onDown={/* @once */ lngp.chainFunctions(props.onDown, onDown)}
|
|
190
|
+
forwardFocus={/* @once */ lngp.navigableForwardFocus}
|
|
191
|
+
onCreate={/* @once */ props.selected ? lngp.chainFunctions(props.onCreate, columnScroll) : props.onCreate}
|
|
192
|
+
scrollToIndex={/* @once */ scrollToIndex}
|
|
193
|
+
onSelectedChanged={/* @once */ chainedOnSelectedChanged}
|
|
194
|
+
style={/* @once */ lng.combineStyles(props.style, rowStyles)}
|
|
195
|
+
>
|
|
196
|
+
<List each={slice()}>{props.children}</List>
|
|
197
|
+
</view>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
@@ -13,7 +13,6 @@ import { ElementNode } from '@lightningtv/solid';
|
|
|
13
13
|
export function Visible<T>(props: {
|
|
14
14
|
when: T | undefined | null | false;
|
|
15
15
|
keyed?: boolean;
|
|
16
|
-
fallback?: JSX.Element;
|
|
17
16
|
children: JSX.Element;
|
|
18
17
|
}): JSX.Element {
|
|
19
18
|
let child: ChildrenReturn | undefined;
|
|
@@ -55,6 +54,6 @@ export function Visible<T>(props: {
|
|
|
55
54
|
}
|
|
56
55
|
});
|
|
57
56
|
|
|
58
|
-
return c ? child :
|
|
57
|
+
return c || child ? child : null;
|
|
59
58
|
}) as unknown as JSX.Element;
|
|
60
59
|
};
|
|
@@ -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,7 +17,10 @@ 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
|
+
export * from './KeepAlive.jsx';
|
|
21
|
+
export * from './VirtualGrid.jsx';
|
|
22
|
+
export * from './Virtual.jsx';
|
|
23
|
+
export * from './utils/withScrolling.js';
|
|
20
24
|
export {
|
|
21
25
|
type AnyFunction,
|
|
22
26
|
chainFunctions,
|
package/src/primitives/types.ts
CHANGED
|
@@ -54,6 +54,7 @@ export interface NavigableProps extends NodeProps {
|
|
|
54
54
|
// @ts-expect-error animationSettings is not identical - weird
|
|
55
55
|
export interface NavigableElement extends ElementNode, NavigableProps {
|
|
56
56
|
selected: number;
|
|
57
|
+
scrollToIndex: (this: NavigableElement, index: number) => void;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export interface NavigableStyleProperties {
|
|
@@ -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
|
)
|
|
@@ -24,7 +24,7 @@ function findFirstFocusableChildIdx(
|
|
|
24
24
|
i = (i + el.children.length) % el.children.length;
|
|
25
25
|
} else break;
|
|
26
26
|
}
|
|
27
|
-
if (!el.children[i]
|
|
27
|
+
if (!el.children[i]?.skipFocus) {
|
|
28
28
|
return i;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -41,12 +41,14 @@ function selectChild(el: lngp.NavigableElement, index: number): boolean {
|
|
|
41
41
|
|
|
42
42
|
const lastSelected = el.selected;
|
|
43
43
|
el.selected = index;
|
|
44
|
-
child.setFocus();
|
|
45
44
|
|
|
46
|
-
if (
|
|
47
|
-
|
|
45
|
+
if (!lng.isFocused(child)) {
|
|
46
|
+
child.setFocus();
|
|
48
47
|
}
|
|
49
48
|
|
|
49
|
+
// Always call onSelectedChanged on first focus for clients
|
|
50
|
+
el.onSelectedChanged?.(index, el, child as lng.ElementNode, lastSelected);
|
|
51
|
+
|
|
50
52
|
return true;
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -90,10 +92,11 @@ export const navigableForwardFocus: lng.ForwardFocusHandler = function () {
|
|
|
90
92
|
let selected = navigable.selected;
|
|
91
93
|
selected = idxInArray(selected, this.children) ? selected : 0;
|
|
92
94
|
selected = findFirstFocusableChildIdx(navigable, selected);
|
|
95
|
+
// update selected as firstfocusable maybe different if first element has skipFocus
|
|
96
|
+
navigable.selected = selected;
|
|
93
97
|
return selectChild(navigable, selected);
|
|
94
98
|
};
|
|
95
99
|
|
|
96
|
-
/** @deprecated Use {@link navigableHandleNavigation} instead */
|
|
97
100
|
export function handleNavigation(
|
|
98
101
|
direction: 'up' | 'right' | 'down' | 'left',
|
|
99
102
|
): lng.KeyHandler {
|
|
@@ -5,6 +5,13 @@ import type {
|
|
|
5
5
|
Styles,
|
|
6
6
|
} from '@lightningtv/core';
|
|
7
7
|
|
|
8
|
+
export type Scroller = (
|
|
9
|
+
selected: number | ElementNode,
|
|
10
|
+
component?: ElementNode,
|
|
11
|
+
selectedElement?: ElementNode | ElementText,
|
|
12
|
+
lastSelected?: number,
|
|
13
|
+
) => void;
|
|
14
|
+
|
|
8
15
|
// Adds properties expected by withScrolling
|
|
9
16
|
export interface ScrollableElement extends ElementNode {
|
|
10
17
|
scrollIndex?: number;
|
|
@@ -33,16 +40,12 @@ const isNotShown = (node: ElementNode | ElementText) => {
|
|
|
33
40
|
Always scroll moves the list every time
|
|
34
41
|
*/
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
/** Use {@link scrollRow} or {@link scrollColumn} */
|
|
44
|
+
export function withScrolling(isRow: boolean): Scroller {
|
|
37
45
|
const dimension = isRow ? 'width' : 'height';
|
|
38
46
|
const axis = isRow ? 'x' : 'y';
|
|
39
47
|
|
|
40
|
-
return (
|
|
41
|
-
selected: number | ElementNode,
|
|
42
|
-
component?: ElementNode,
|
|
43
|
-
selectedElement?: ElementNode | ElementText,
|
|
44
|
-
lastSelected?: number,
|
|
45
|
-
) => {
|
|
48
|
+
return (selected, component, selectedElement, lastSelected) => {
|
|
46
49
|
let componentRef = component as ScrollableElement;
|
|
47
50
|
if (typeof selected !== 'number') {
|
|
48
51
|
componentRef = selected as ScrollableElement;
|
|
@@ -51,6 +54,7 @@ export function withScrolling(isRow: boolean) {
|
|
|
51
54
|
if (
|
|
52
55
|
!componentRef ||
|
|
53
56
|
componentRef.scroll === 'none' ||
|
|
57
|
+
selected === lastSelected ||
|
|
54
58
|
!componentRef.children.length
|
|
55
59
|
)
|
|
56
60
|
return;
|
|
@@ -60,7 +64,7 @@ export function withScrolling(isRow: boolean) {
|
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
const lng = componentRef.lng as unknown as INode;
|
|
63
|
-
const screenSize = isRow ? lng.stage.root.
|
|
67
|
+
const screenSize = isRow ? lng.stage.root.w : lng.stage.root.h;
|
|
64
68
|
// Determine if movement is incremental or decremental
|
|
65
69
|
const isIncrementing =
|
|
66
70
|
lastSelected === undefined || lastSelected - 1 !== selected;
|
|
@@ -69,6 +73,7 @@ export function withScrolling(isRow: boolean) {
|
|
|
69
73
|
if (componentRef.parent!.clipping) {
|
|
70
74
|
const p = componentRef.parent!;
|
|
71
75
|
componentRef.endOffset =
|
|
76
|
+
componentRef.endOffset ??
|
|
72
77
|
screenSize - ((isRow ? p.absX : p.absY) || 0) - p[dimension];
|
|
73
78
|
}
|
|
74
79
|
|
|
@@ -90,10 +95,9 @@ export function withScrolling(isRow: boolean) {
|
|
|
90
95
|
|
|
91
96
|
// Allows manual position control
|
|
92
97
|
const targetPosition = componentRef._targetPosition ?? componentRef[axis];
|
|
93
|
-
const rootPosition =
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
: Math.max(targetPosition, componentRef[axis]);
|
|
98
|
+
const rootPosition = isIncrementing
|
|
99
|
+
? Math.min(targetPosition, componentRef[axis])
|
|
100
|
+
: Math.max(targetPosition, componentRef[axis]);
|
|
97
101
|
componentRef.offset = componentRef.offset ?? rootPosition;
|
|
98
102
|
const offset = componentRef.offset;
|
|
99
103
|
selectedElement =
|
|
@@ -115,7 +119,7 @@ export function withScrolling(isRow: boolean) {
|
|
|
115
119
|
screenSize -
|
|
116
120
|
containerSize -
|
|
117
121
|
screenOffset -
|
|
118
|
-
(componentRef.endOffset
|
|
122
|
+
(componentRef.endOffset ?? 2 * gap),
|
|
119
123
|
offset,
|
|
120
124
|
);
|
|
121
125
|
|
|
@@ -156,7 +160,8 @@ export function withScrolling(isRow: boolean) {
|
|
|
156
160
|
nextPosition = rootPosition + selectedSize + gap;
|
|
157
161
|
}
|
|
158
162
|
} else if (isIncrementing) {
|
|
159
|
-
nextPosition = -selectedPosition + offset;
|
|
163
|
+
//nextPosition = -selectedPosition + offset;
|
|
164
|
+
nextPosition = rootPosition - selectedSize - gap;
|
|
160
165
|
} else {
|
|
161
166
|
nextPosition = rootPosition + selectedSize + gap;
|
|
162
167
|
}
|
|
@@ -186,3 +191,6 @@ export function withScrolling(isRow: boolean) {
|
|
|
186
191
|
}
|
|
187
192
|
};
|
|
188
193
|
}
|
|
194
|
+
|
|
195
|
+
export const scrollRow = /* @__PURE__ */ withScrolling(true);
|
|
196
|
+
export const scrollColumn = /* @__PURE__ */ withScrolling(false);
|
package/src/render.ts
CHANGED
|
@@ -156,3 +156,8 @@ export const Text = (props: TextProps) => {
|
|
|
156
156
|
spread(el, props, false);
|
|
157
157
|
return el as unknown as JSXElement;
|
|
158
158
|
};
|
|
159
|
+
|
|
160
|
+
export function registerDefaultShader(name: string, shader: any) {
|
|
161
|
+
// noop for v2
|
|
162
|
+
// renderer.stage.shManager.registerShaderType('rounded', Rounded);
|
|
163
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -61,3 +61,13 @@ export function combineStylesMemo<T extends Styles>(
|
|
|
61
61
|
...style1,
|
|
62
62
|
}));
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
export const clamp = (value: number, min: number, max: number) =>
|
|
66
|
+
min < max
|
|
67
|
+
? Math.min(Math.max(value, min), max)
|
|
68
|
+
: Math.min(Math.max(value, max), min);
|
|
69
|
+
|
|
70
|
+
export function mod(n: number, m: number): number {
|
|
71
|
+
if (m === 0) return 0;
|
|
72
|
+
return ((n % m) + m) % m;
|
|
73
|
+
}
|