@lightningtv/solid 2.9.4 → 2.9.6

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.
@@ -0,0 +1,192 @@
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
+ gap: 30,
13
+ transition: {
14
+ y: {
15
+ duration: 250,
16
+ easing: 'ease-out',
17
+ },
18
+ },
19
+ };
20
+
21
+ function scrollToIndex(this: lng.ElementNode, index: number) {
22
+ this.selected = index;
23
+ columnScroll(index, this);
24
+ this.setFocus();
25
+ }
26
+
27
+ export type VirtualGridProps<T> = lng.NewOmit<lngp.RowProps, 'children'> & {
28
+ each: readonly T[] | undefined | null | false;
29
+ itemsPerRow: number; // items per row
30
+ numberOfRows?: number; // number of visible rows (default: 1)
31
+ rowsBuffer?: number;
32
+ onEndReached?: () => void;
33
+ children: (item: s.Accessor<T>, index: s.Accessor<number>) => s.JSX.Element;
34
+ };
35
+
36
+ export function VirtualGrid<T>(props: VirtualGridProps<T>): s.JSX.Element {
37
+ const bufferSize = () => props.rowsBuffer ?? 2;
38
+ const [ cursor, setCursor ] = s.createSignal(props.selected ?? 0);
39
+ const items = s.createMemo(() => props.each || []);
40
+ const itemsPerRow = () => props.itemsPerRow;
41
+ const numberOfRows = () => props.numberOfRows ?? 1;
42
+ const totalVisibleItems = () => itemsPerRow() * numberOfRows();
43
+
44
+ const start = s.createMemo(() => {
45
+ const perRow = itemsPerRow();
46
+ const newRowIndex = Math.floor(cursor() / perRow);
47
+
48
+ return utils.clamp(
49
+ newRowIndex * perRow - bufferSize() * perRow,
50
+ 0,
51
+ Math.max(0, items().length - totalVisibleItems())
52
+ );
53
+ })
54
+
55
+ const end = s.createMemo(() => {
56
+ const perRow = itemsPerRow();
57
+ const newRowIndex = Math.floor(cursor() / perRow);
58
+
59
+ return Math.min(
60
+ items().length,
61
+ (newRowIndex + bufferSize()) * perRow + totalVisibleItems()
62
+ );
63
+ })
64
+
65
+ const [slice, setSlice] = s.createSignal(items().slice(start(), end()));
66
+
67
+ let viewRef!: lngp.NavigableElement;
68
+
69
+ const onLeft = lngp.handleNavigation('left');
70
+ const onRight = lngp.handleNavigation('right');
71
+
72
+ const onUp: lngp.KeyHandler = function () {
73
+ const perRow = itemsPerRow();
74
+ const selected = this.selected || 0;
75
+ if (selected < perRow) return false;
76
+
77
+ const newIndex = utils.clamp(selected - perRow, 0, items().length - 1);
78
+ const lastIdx = selected;
79
+ this.selected = newIndex;
80
+ const active = this.children[this.selected];
81
+ if (active instanceof lng.ElementNode) {
82
+ active.setFocus();
83
+ chainedOnSelectedChanged.call(this as lngp.NavigableElement, this.selected, this as lngp.NavigableElement, active, lastIdx);
84
+ }
85
+ };
86
+
87
+ const onDown: lngp.KeyHandler = function () {
88
+ const perRow = itemsPerRow();
89
+ const selected = this.selected || 0;
90
+
91
+ const newIndex = utils.clamp(selected + perRow, 0, items().length - 1);
92
+ if (newIndex !== selected) {
93
+ const lastIdx = selected;
94
+ this.selected = newIndex;
95
+ const active = this.children[this.selected];
96
+ if (active instanceof lng.ElementNode) {
97
+ active.setFocus();
98
+ chainedOnSelectedChanged.call(this as lngp.NavigableElement, this.selected, this as lngp.NavigableElement, active, lastIdx);
99
+ }
100
+ }
101
+ };
102
+
103
+ const onSelectedChanged: lngp.OnSelectedChanged = function (_idx, elm, active, _lastIdx,) {
104
+ let idx = _idx;
105
+ let lastIdx = _lastIdx;
106
+ const perRow = itemsPerRow();
107
+ const newRowIndex = Math.floor(idx / perRow);
108
+ const prevRowIndex = Math.floor((lastIdx || 0) / perRow);
109
+ const prevStart = start();
110
+
111
+ if (newRowIndex === prevRowIndex) return;
112
+
113
+ setCursor(prevStart + idx);
114
+ setSlice(items().slice(start(), end()));
115
+
116
+ // this.selected is relative to the slice
117
+ // and it doesn't get corrected automatically after children change
118
+ const idxCorrection = prevStart - start();
119
+ if (lastIdx) lastIdx += idxCorrection;
120
+ idx += idxCorrection;
121
+ this.selected += idxCorrection;
122
+
123
+ if (cursor() >= items().length - perRow * bufferSize()) {
124
+ props.onEndReached?.();
125
+ }
126
+
127
+ queueMicrotask(() => {
128
+ const prevRowY = this.y + active.y;
129
+ this.updateLayout();
130
+ // if (prevRowY > active.y) {
131
+ // }
132
+ this.lng.y = prevRowY - active.y;
133
+ // this.y = prevRowY - active.y;
134
+ columnScroll(idx, elm, active, lastIdx);
135
+ });
136
+ };
137
+
138
+ const chainedOnSelectedChanged = lngp.chainFunctions(props.onSelectedChanged, onSelectedChanged)!;
139
+
140
+ s.createEffect(
141
+ s.on([() => props.selected, items], ([selected]) => {
142
+ if (!viewRef || selected == null) return;
143
+
144
+ const item = items()[selected];
145
+ let active = viewRef.children.find(x => x.item === item);
146
+ const lastSelected = viewRef.selected;
147
+
148
+ if (active instanceof lng.ElementNode) {
149
+ viewRef.selected = viewRef.children.indexOf(active);
150
+ chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
151
+ } else {
152
+ setSlice(items().slice(start(), end()));
153
+
154
+ queueMicrotask(() => {
155
+ viewRef.updateLayout();
156
+ active = viewRef.children.find(x => x.item === item);
157
+ if (active instanceof lng.ElementNode) {
158
+ viewRef.selected = viewRef.children.indexOf(active);
159
+ chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
160
+ }
161
+ });
162
+ }
163
+ })
164
+ );
165
+
166
+ s.createEffect(
167
+ s.on(items, () => {
168
+ if (!viewRef) return;
169
+ setSlice(items().slice(start(), end()));
170
+ }, { defer: true })
171
+ );
172
+
173
+ return (
174
+ <view
175
+ {...props}
176
+ ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)}
177
+ selected={props.selected || 0}
178
+ cursor={cursor()}
179
+ onLeft={/* @once */ lngp.chainFunctions(props.onLeft, onLeft)}
180
+ onRight={/* @once */ lngp.chainFunctions(props.onRight, onRight)}
181
+ onUp={/* @once */ lngp.chainFunctions(props.onUp, onUp)}
182
+ onDown={/* @once */ lngp.chainFunctions(props.onDown, onDown)}
183
+ forwardFocus={/* @once */ lngp.onGridFocus(chainedOnSelectedChanged)}
184
+ onCreate={/* @once */ props.selected ? lngp.chainFunctions(props.onCreate, columnScroll) : props.onCreate}
185
+ scrollToIndex={/* @once */ scrollToIndex}
186
+ onSelectedChanged={/* @once */ chainedOnSelectedChanged}
187
+ style={/* @once */ lng.combineStyles(props.style, rowStyles)}
188
+ >
189
+ <List each={slice()}>{props.children}</List>
190
+ </view>
191
+ );
192
+ }
@@ -0,0 +1,137 @@
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 rowOnLeft = lngp.handleNavigation('left');
8
+ const rowOnRight = lngp.handleNavigation('right');
9
+ const rowScroll = lngp.withScrolling(true);
10
+
11
+ const rowStyles: lng.NodeStyles = {
12
+ display: 'flex',
13
+ gap: 30,
14
+ transition: {
15
+ x: {
16
+ duration: 250,
17
+ easing: 'ease-out',
18
+ },
19
+ },
20
+ };
21
+
22
+ function scrollToIndex(this: lng.ElementNode, index: number) {
23
+ this.selected = index;
24
+ rowScroll(index, this);
25
+ this.setFocus();
26
+ }
27
+
28
+ export type VirtualListProps<T> = lng.NewOmit<lngp.RowProps, 'children'> & {
29
+ each: readonly T[] | undefined | null | false;
30
+ displaySize: number;
31
+ bufferSize?: number;
32
+ fallback?: s.JSX.Element;
33
+ children: (item: s.Accessor<T>, index: s.Accessor<number>) => s.JSX.Element;
34
+ };
35
+
36
+ export function VirtualList<T>(props: VirtualListProps<T>): s.JSX.Element {
37
+
38
+ const [ cursor, setCursor ] = s.createSignal(props.selected ?? 0);
39
+
40
+ const bufferSize = () => props.bufferSize ?? 1;
41
+
42
+ const items = s.createMemo(() => props.each || []);
43
+
44
+ const start = () =>
45
+ utils.clamp(cursor() - bufferSize(), 0, Math.max(0, items().length - props.displaySize - bufferSize()));
46
+
47
+ const end = () =>
48
+ Math.min(items().length, cursor() + props.displaySize + bufferSize());
49
+
50
+ const [ slice, setSlice ] = s.createSignal(items().slice(start(), end()));
51
+
52
+ s.createEffect(s.on([ () => props.selected, items ], ([selected]) => {
53
+ if (!viewRef || !selected) return;
54
+
55
+ const item = items()![selected];
56
+ let active = viewRef.children.find(x => x.item === item);
57
+ const lastSelected = viewRef.selected;
58
+
59
+ if (active instanceof lng.ElementNode) {
60
+ viewRef.selected = viewRef.children.indexOf(active);
61
+ chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
62
+ }
63
+ else {
64
+ setCursor(selected);
65
+ setSlice(items().slice(start(), end()));
66
+ queueMicrotask(() => {
67
+ viewRef.updateLayout();
68
+ active = viewRef.children.find(x => x.item === item);
69
+ if (active instanceof lng.ElementNode) {
70
+ viewRef.selected = viewRef.children.indexOf(active);
71
+ chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
72
+ }
73
+ });
74
+ }
75
+ }));
76
+
77
+ s.createEffect(s.on(items, () => {
78
+ if (!viewRef) return;
79
+ setSlice(items().slice(start(), end()));
80
+ }, { defer: true }));
81
+
82
+ const onSelectedChanged: lngp.OnSelectedChanged = function (
83
+ _idx, elm, active, _lastIdx,
84
+ ) {
85
+ let idx = _idx;
86
+ let lastIdx = _lastIdx;
87
+
88
+ const prevChildX = this.x + active.x;
89
+ const prevStart = start();
90
+
91
+ // Update the displayed slice of items
92
+ setCursor(prevStart + idx);
93
+ setSlice(items().slice(start(), end()));
94
+
95
+ // this.selected is relative to the slice
96
+ // and it doesn't get corrected automatically after children change
97
+ const idxCorrection = prevStart - start();
98
+ if (lastIdx) lastIdx += idxCorrection;
99
+ idx += idxCorrection;
100
+ this.selected += idxCorrection;
101
+
102
+ // Microtask & this.updateLayout() to make sure the child position is recalculated
103
+ queueMicrotask(() => {
104
+ this.updateLayout();
105
+
106
+ // Correct this.x for changes to children, bypass animation
107
+ this.lng.x = prevChildX - active.x;
108
+
109
+ // smoothly scroll to new selected element
110
+ rowScroll(idx, elm, active, lastIdx);
111
+ });
112
+ };
113
+
114
+ const chainedOnSelectedChanged = lngp.chainFunctions(props.onSelectedChanged, onSelectedChanged)!;
115
+
116
+ let viewRef!: lngp.NavigableElement;
117
+ return <>
118
+ <view
119
+ {...props}
120
+ ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)}
121
+ selected={props.selected || 0}
122
+ cursor={cursor()}
123
+ onLeft={/* @once */ lngp.chainFunctions(props.onLeft, rowOnLeft)}
124
+ onRight={/* @once */ lngp.chainFunctions(props.onRight, rowOnRight)}
125
+ forwardFocus={/* once */ lngp.onGridFocus(chainedOnSelectedChanged)}
126
+ scrollToIndex={scrollToIndex}
127
+ onCreate={/* @once */
128
+ props.selected ? lngp.chainFunctions(props.onCreate, rowScroll) : props.onCreate
129
+ }
130
+ /* lngp.NavigableElement.onSelectedChanged is used by lngp.handleNavigation */
131
+ onSelectedChanged={chainedOnSelectedChanged}
132
+ style={/* @once */ lng.combineStyles(props.style, rowStyles)}
133
+ >
134
+ <List each={slice()}>{props.children}</List>
135
+ </view>
136
+ </>;
137
+ }
@@ -16,6 +16,8 @@ export * from './Suspense.jsx';
16
16
  export * from './Marquee.jsx';
17
17
  export * from './createFocusStack.jsx';
18
18
  export * from './useHold.js';
19
+ export * from './VirtualGrid.jsx';
20
+ export * from './VirtualList.jsx';
19
21
  export { withScrolling } from './utils/withScrolling.js';
20
22
  export {
21
23
  type AnyFunction,
package/src/utils.ts CHANGED
@@ -61,3 +61,8 @@ export function combineStylesMemo<T extends Styles>(
61
61
  ...style1,
62
62
  }));
63
63
  }
64
+
65
+ export const clamp = (value: number, min = 0, max = 100) =>
66
+ min < max
67
+ ? Math.min(Math.max(value, min), max)
68
+ : Math.min(Math.max(value, max), min);