@lightningtv/solid 3.0.0-17 → 3.0.0-18

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.
Files changed (58) hide show
  1. package/dist/src/primitives/Column.jsx +8 -7
  2. package/dist/src/primitives/Column.jsx.map +1 -1
  3. package/dist/src/primitives/KeepAlive.d.ts +4 -3
  4. package/dist/src/primitives/KeepAlive.jsx +70 -9
  5. package/dist/src/primitives/KeepAlive.jsx.map +1 -1
  6. package/dist/src/primitives/Lazy.d.ts +6 -7
  7. package/dist/src/primitives/Lazy.jsx +23 -20
  8. package/dist/src/primitives/Lazy.jsx.map +1 -1
  9. package/dist/src/primitives/Row.jsx +8 -7
  10. package/dist/src/primitives/Row.jsx.map +1 -1
  11. package/dist/src/primitives/Virtual.d.ts +18 -0
  12. package/dist/src/primitives/Virtual.jsx +426 -0
  13. package/dist/src/primitives/Virtual.jsx.map +1 -0
  14. package/dist/src/primitives/VirtualGrid.d.ts +13 -0
  15. package/dist/src/primitives/VirtualGrid.jsx +139 -0
  16. package/dist/src/primitives/VirtualGrid.jsx.map +1 -0
  17. package/dist/src/primitives/VirtualList.d.ts +11 -0
  18. package/dist/src/primitives/VirtualList.jsx +96 -0
  19. package/dist/src/primitives/VirtualList.jsx.map +1 -0
  20. package/dist/src/primitives/VirtualRow.d.ts +13 -0
  21. package/dist/src/primitives/VirtualRow.jsx +97 -0
  22. package/dist/src/primitives/VirtualRow.jsx.map +1 -0
  23. package/dist/src/primitives/Visible.d.ts +0 -1
  24. package/dist/src/primitives/Visible.jsx +1 -1
  25. package/dist/src/primitives/Visible.jsx.map +1 -1
  26. package/dist/src/primitives/index.d.ts +4 -1
  27. package/dist/src/primitives/index.js +4 -1
  28. package/dist/src/primitives/index.js.map +1 -1
  29. package/dist/src/primitives/types.d.ts +1 -0
  30. package/dist/src/primitives/utils/handleNavigation.d.ts +0 -1
  31. package/dist/src/primitives/utils/handleNavigation.js +7 -5
  32. package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
  33. package/dist/src/primitives/utils/withScrolling.d.ts +5 -1
  34. package/dist/src/primitives/utils/withScrolling.js +8 -3
  35. package/dist/src/primitives/utils/withScrolling.js.map +1 -1
  36. package/dist/src/render.d.ts +2 -1
  37. package/dist/src/render.js +4 -0
  38. package/dist/src/render.js.map +1 -1
  39. package/dist/src/universal.d.ts +25 -0
  40. package/dist/src/universal.js +232 -0
  41. package/dist/src/universal.js.map +1 -0
  42. package/dist/src/utils.d.ts +2 -0
  43. package/dist/src/utils.js +8 -0
  44. package/dist/src/utils.js.map +1 -1
  45. package/dist/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +6 -3
  47. package/src/primitives/Column.tsx +10 -9
  48. package/src/primitives/Lazy.tsx +34 -38
  49. package/src/primitives/Row.tsx +11 -9
  50. package/src/primitives/Virtual.tsx +469 -0
  51. package/src/primitives/VirtualGrid.tsx +199 -0
  52. package/src/primitives/Visible.tsx +1 -2
  53. package/src/primitives/index.ts +8 -1
  54. package/src/primitives/types.ts +1 -0
  55. package/src/primitives/utils/handleNavigation.ts +8 -5
  56. package/src/primitives/utils/withScrolling.ts +20 -13
  57. package/src/render.ts +5 -0
  58. 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 : props.fallback;
57
+ return c || child ? child : null;
59
58
  }) as unknown as JSX.Element;
60
59
  };
@@ -16,7 +16,14 @@ export * from './Suspense.jsx';
16
16
  export * from './Marquee.jsx';
17
17
  export * from './createFocusStack.jsx';
18
18
  export * from './useHold.js';
19
- export { withScrolling } from './utils/withScrolling.js';
19
+ export {
20
+ withScrolling,
21
+ scrollColumn,
22
+ scrollRow,
23
+ } from './utils/withScrolling.js';
24
+ export * from './VirtualGrid.jsx';
25
+ export * from './Virtual.jsx';
26
+ export * from './utils/withScrolling.js';
20
27
  export {
21
28
  type AnyFunction,
22
29
  chainFunctions,
@@ -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 {
@@ -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]!.skipFocus) {
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 (lastSelected !== index || !lng.hasFocus(el)) {
47
- el.onSelectedChanged?.(index, el, child as lng.ElementNode, lastSelected);
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
- export function withScrolling(isRow: boolean) {
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.width : lng.stage.root.height;
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;
@@ -90,10 +94,9 @@ export function withScrolling(isRow: boolean) {
90
94
 
91
95
  // Allows manual position control
92
96
  const targetPosition = componentRef._targetPosition ?? componentRef[axis];
93
- const rootPosition =
94
- isIncrementing || scroll === 'auto'
95
- ? Math.min(targetPosition, componentRef[axis])
96
- : Math.max(targetPosition, componentRef[axis]);
97
+ const rootPosition = isIncrementing
98
+ ? Math.min(targetPosition, componentRef[axis])
99
+ : Math.max(targetPosition, componentRef[axis]);
97
100
  componentRef.offset = componentRef.offset ?? rootPosition;
98
101
  const offset = componentRef.offset;
99
102
  selectedElement =
@@ -156,7 +159,8 @@ export function withScrolling(isRow: boolean) {
156
159
  nextPosition = rootPosition + selectedSize + gap;
157
160
  }
158
161
  } else if (isIncrementing) {
159
- nextPosition = -selectedPosition + offset;
162
+ //nextPosition = -selectedPosition + offset;
163
+ nextPosition = rootPosition - selectedSize - gap;
160
164
  } else {
161
165
  nextPosition = rootPosition + selectedSize + gap;
162
166
  }
@@ -186,3 +190,6 @@ export function withScrolling(isRow: boolean) {
186
190
  }
187
191
  };
188
192
  }
193
+
194
+ export const scrollRow = /* @__PURE__ */ withScrolling(true);
195
+ 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
+ }