@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.
Files changed (72) hide show
  1. package/README.md +6 -0
  2. package/dist/src/primitives/Column.jsx +8 -7
  3. package/dist/src/primitives/Column.jsx.map +1 -1
  4. package/dist/src/primitives/Image.d.ts +8 -0
  5. package/dist/src/primitives/Image.jsx +24 -0
  6. package/dist/src/primitives/Image.jsx.map +1 -0
  7. package/dist/src/primitives/KeepAlive.d.ts +19 -5
  8. package/dist/src/primitives/KeepAlive.jsx +52 -21
  9. package/dist/src/primitives/KeepAlive.jsx.map +1 -1
  10. package/dist/src/primitives/Lazy.d.ts +6 -7
  11. package/dist/src/primitives/Lazy.jsx +23 -20
  12. package/dist/src/primitives/Lazy.jsx.map +1 -1
  13. package/dist/src/primitives/Row.jsx +8 -7
  14. package/dist/src/primitives/Row.jsx.map +1 -1
  15. package/dist/src/primitives/Virtual.d.ts +18 -0
  16. package/dist/src/primitives/Virtual.jsx +428 -0
  17. package/dist/src/primitives/Virtual.jsx.map +1 -0
  18. package/dist/src/primitives/VirtualGrid.d.ts +13 -0
  19. package/dist/src/primitives/VirtualGrid.jsx +139 -0
  20. package/dist/src/primitives/VirtualGrid.jsx.map +1 -0
  21. package/dist/src/primitives/VirtualList.d.ts +11 -0
  22. package/dist/src/primitives/VirtualList.jsx +96 -0
  23. package/dist/src/primitives/VirtualList.jsx.map +1 -0
  24. package/dist/src/primitives/VirtualRow.d.ts +13 -0
  25. package/dist/src/primitives/VirtualRow.jsx +97 -0
  26. package/dist/src/primitives/VirtualRow.jsx.map +1 -0
  27. package/dist/src/primitives/Visible.d.ts +0 -1
  28. package/dist/src/primitives/Visible.jsx +1 -1
  29. package/dist/src/primitives/Visible.jsx.map +1 -1
  30. package/dist/src/primitives/createFocusStack.d.ts +4 -4
  31. package/dist/src/primitives/createFocusStack.jsx +15 -6
  32. package/dist/src/primitives/createFocusStack.jsx.map +1 -1
  33. package/dist/src/primitives/index.d.ts +5 -1
  34. package/dist/src/primitives/index.js +5 -1
  35. package/dist/src/primitives/index.js.map +1 -1
  36. package/dist/src/primitives/types.d.ts +1 -0
  37. package/dist/src/primitives/useMouse.d.ts +6 -0
  38. package/dist/src/primitives/useMouse.js +26 -3
  39. package/dist/src/primitives/useMouse.js.map +1 -1
  40. package/dist/src/primitives/utils/handleNavigation.d.ts +0 -1
  41. package/dist/src/primitives/utils/handleNavigation.js +7 -5
  42. package/dist/src/primitives/utils/handleNavigation.js.map +1 -1
  43. package/dist/src/primitives/utils/withScrolling.d.ts +5 -1
  44. package/dist/src/primitives/utils/withScrolling.js +11 -5
  45. package/dist/src/primitives/utils/withScrolling.js.map +1 -1
  46. package/dist/src/render.d.ts +1 -0
  47. package/dist/src/render.js +4 -0
  48. package/dist/src/render.js.map +1 -1
  49. package/dist/src/universal.d.ts +25 -0
  50. package/dist/src/universal.js +232 -0
  51. package/dist/src/universal.js.map +1 -0
  52. package/dist/src/utils.d.ts +2 -0
  53. package/dist/src/utils.js +8 -0
  54. package/dist/src/utils.js.map +1 -1
  55. package/dist/tsconfig.tsbuildinfo +1 -1
  56. package/package.json +7 -4
  57. package/src/primitives/Column.tsx +10 -9
  58. package/src/primitives/Image.tsx +36 -0
  59. package/src/primitives/KeepAlive.tsx +124 -0
  60. package/src/primitives/Lazy.tsx +34 -38
  61. package/src/primitives/Row.tsx +11 -9
  62. package/src/primitives/Virtual.tsx +471 -0
  63. package/src/primitives/VirtualGrid.tsx +199 -0
  64. package/src/primitives/Visible.tsx +1 -2
  65. package/src/primitives/createFocusStack.tsx +18 -7
  66. package/src/primitives/index.ts +5 -1
  67. package/src/primitives/types.ts +1 -0
  68. package/src/primitives/useMouse.ts +52 -7
  69. package/src/primitives/utils/handleNavigation.ts +8 -5
  70. package/src/primitives/utils/withScrolling.ts +22 -14
  71. package/src/render.ts +5 -0
  72. package/src/utils.ts +10 -0
@@ -1,56 +1,44 @@
1
- import {
2
- Index,
3
- createEffect,
4
- createRenderEffect,
5
- createMemo,
6
- createSignal,
7
- createReaction,
8
- Show,
9
- type JSX,
10
- type ValidComponent,
11
- untrack,
12
- type Accessor,
13
- } from 'solid-js'; // Dynamic removed
14
- import { type NewOmit, scheduleTask, type NodeProps, Dynamic, ElementNode } from '@lightningtv/solid'; // Dynamic removed from imports
15
- 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';
16
4
 
17
- type LazyProps<T extends readonly any[]> = NewOmit<NodeProps, 'children'> & {
5
+ type LazyProps<T extends readonly any[]> = lng.NewOmit<lng.NodeProps, 'children'> & {
18
6
  each: T | undefined | null | false;
19
- fallback?: JSX.Element;
20
7
  upCount: number;
21
8
  buffer?: number;
22
9
  delay?: number;
23
10
  sync?: boolean;
24
11
  eagerLoad?: boolean;
25
- children: (item: Accessor<T[number]>, index: number) => JSX.Element;
12
+ children: (item: s.Accessor<T[number]>, index: number) => s.JSX.Element;
26
13
  };
27
14
 
28
15
  function createLazy<T>(
29
- component: ValidComponent,
16
+ component: s.ValidComponent,
30
17
  props: LazyProps<readonly T[]>,
31
- keyHandler: (updateOffset: (event: KeyboardEvent, container: ElementNode) => void) => Record<string, (event: KeyboardEvent, container: ElementNode) => void>
18
+ keyHandler: (updateOffset: (event: KeyboardEvent, container: lng.ElementNode) => void) => Record<string, (event: KeyboardEvent, container: lng.ElementNode) => void>
32
19
  ) {
33
20
  // Need at least one item so it can be focused
34
- const [offset, setOffset] = createSignal<number>(props.sync ? props.upCount : 0);
21
+ const [offset, setOffset] = s.createSignal<number>(props.sync ? props.upCount : 0);
35
22
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
23
+ let viewRef!: lngp.NavigableElement;
36
24
 
37
- const buffer = createMemo(() => {
25
+ const buffer = s.createMemo(() => {
38
26
  if (typeof props.buffer === 'number') {
39
27
  return props.buffer;
40
28
  }
41
29
  const scroll = props.scroll || props.style?.scroll;
42
- if (!scroll || scroll === 'auto' || scroll === 'always') return props.upCount;
43
- if (scroll === 'center') return Math.ceil(props.upCount / 2);
30
+ if (!scroll || scroll === 'auto' || scroll === 'always') return props.upCount + 1;
31
+ if (scroll === 'center') return Math.ceil(props.upCount / 2) + 1;
44
32
  return 2;
45
33
  });
46
34
 
47
- createRenderEffect(() => setOffset(offset => Math.max(offset, (props.selected || 0) + buffer())));
35
+ s.createRenderEffect(() => setOffset(offset => Math.max(offset, (props.selected || 0) + buffer())));
48
36
 
49
- if (!props.sync || props.eaglerLoad) {
50
- createEffect(() => {
37
+ if (!props.sync || props.eagerLoad) {
38
+ s.createEffect(() => {
51
39
  if (props.each) {
52
40
  const loadItems = () => {
53
- let count = untrack(offset);
41
+ let count = s.untrack(offset);
54
42
  if (count < props.upCount) {
55
43
  setOffset(count + 1);
56
44
  timeoutId = setTimeout(loadItems, 16); // ~60fps
@@ -59,7 +47,7 @@ function createLazy<T>(
59
47
  const maxOffset = props.each ? props.each.length : 0;
60
48
  if (count >= maxOffset) return;
61
49
  setOffset((prev) => Math.min(prev + 1, maxOffset));
62
- scheduleTask(loadItems);
50
+ lng.scheduleTask(loadItems);
63
51
  }
64
52
  };
65
53
  loadItems();
@@ -67,11 +55,16 @@ function createLazy<T>(
67
55
  });
68
56
  }
69
57
 
70
- const items = createMemo(() => (
58
+ const items: s.Accessor<T[]> = s.createMemo(() => (
71
59
  Array.isArray(props.each) ? props.each.slice(0, offset()) : [])
72
60
  );
73
61
 
74
- const updateOffset = (_event: KeyboardEvent, container: ElementNode) => {
62
+ function lazyScrollToIndex(this: lngp.NavigableElement, index: number) {
63
+ setOffset(Math.max(index, 0) + buffer())
64
+ queueMicrotask(() => viewRef.scrollToIndex(index));
65
+ }
66
+
67
+ const updateOffset = (_event: KeyboardEvent, container: lng.ElementNode) => {
75
68
  const maxOffset = props.each ? props.each.length : 0;
76
69
  const selected = container.selected || 0;
77
70
  const numChildren = container.children.length;
@@ -97,18 +90,21 @@ function createLazy<T>(
97
90
  const handler = keyHandler(updateOffset);
98
91
 
99
92
  return (
100
- <Show when={items()} fallback={props.fallback}>
101
- <Dynamic component={component} {...props} {/* @once */ ...handler}>
102
- <Index each={items()} children={props.children} />
103
- </Dynamic>
104
- </Show>
93
+ <lng.Dynamic
94
+ {...props}
95
+ component={component}
96
+ {/* @once */ ...handler}
97
+ lazyScrollToIndex={lazyScrollToIndex}
98
+ ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)} >
99
+ <s.Index each={items()} children={props.children} />
100
+ </lng.Dynamic>
105
101
  );
106
102
  }
107
103
 
108
104
  export function LazyRow<T extends readonly any[]>(props: LazyProps<T>) {
109
- return createLazy(Row, props, (updateOffset) => ({ onRight: updateOffset }));
105
+ return createLazy(lngp.Row, props, (updateOffset) => ({ onRight: updateOffset }));
110
106
  }
111
107
 
112
108
  export function LazyColumn<T extends readonly any[]>(props: LazyProps<T>) {
113
- return createLazy(Column, props, (updateOffset) => ({ onDown: updateOffset }));
109
+ return createLazy(lngp.Column, props, (updateOffset) => ({ onDown: updateOffset }));
114
110
  }
@@ -2,10 +2,11 @@ import { type Component } from 'solid-js';
2
2
  import { combineStyles, type NodeStyles, type ElementNode } from '@lightningtv/solid';
3
3
  import { chainFunctions } from './utils/chainFunctions.js';
4
4
  import {
5
- navigableForwardFocus, navigableHandleNavigation
5
+ handleNavigation,
6
+ navigableForwardFocus
6
7
  } from './utils/handleNavigation.js';
7
- import { withScrolling } from './utils/withScrolling.js';
8
8
  import type { RowProps } from './types.js';
9
+ import { scrollRow } from './utils/withScrolling.js';
9
10
 
10
11
  const RowStyles: NodeStyles = {
11
12
  display: 'flex',
@@ -18,31 +19,32 @@ const RowStyles: NodeStyles = {
18
19
  },
19
20
  };
20
21
 
21
- const scroll = withScrolling(true);
22
-
23
22
  function scrollToIndex(this: ElementNode, index: number) {
24
23
  this.selected = index;
25
- scroll(index, this);
24
+ scrollRow(index, this);
26
25
  this.children[index]?.setFocus();
27
26
  }
28
27
 
28
+ const onLeft = handleNavigation('left');
29
+ const onRight = handleNavigation('right');
30
+
29
31
  export const Row: Component<RowProps> = (props) => {
30
32
  return (
31
33
  <view
32
34
  {...props}
33
35
  selected={props.selected || 0}
34
- onLeft={/* @once */ chainFunctions(props.onLeft, navigableHandleNavigation)}
35
- onRight={/* @once */ chainFunctions(props.onRight, navigableHandleNavigation)}
36
+ onLeft={/* @once */ chainFunctions(props.onLeft, onLeft)}
37
+ onRight={/* @once */ chainFunctions(props.onRight, onRight)}
36
38
  forwardFocus={navigableForwardFocus}
37
39
  scrollToIndex={scrollToIndex}
38
40
  onLayout={
39
41
  /* @once */
40
- props.selected ? chainFunctions(props.onLayout, scroll) : props.onLayout
42
+ props.selected ? chainFunctions(props.onLayout, scrollRow) : props.onLayout
41
43
  }
42
44
  onSelectedChanged={
43
45
  /* @once */ chainFunctions(
44
46
  props.onSelectedChanged,
45
- props.scroll !== 'none' ? scroll : undefined,
47
+ props.scroll !== 'none' ? scrollRow : undefined,
46
48
  )
47
49
  }
48
50
  style={/* @once */ combineStyles(props.style, RowStyles)}
@@ -0,0 +1,471 @@
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
+ export type VirtualProps<T> = lng.NewOmit<lngp.RowProps, 'children'> & {
8
+ each: readonly T[] | undefined | null | false;
9
+ displaySize: number;
10
+ bufferSize?: number;
11
+ wrap?: boolean;
12
+ scrollIndex?: number;
13
+ onEndReached?: () => void;
14
+ onEndReachedThreshold?: number;
15
+ debugInfo?: boolean;
16
+ factorScale?: boolean;
17
+ uniformSize?: boolean;
18
+ children: (item: s.Accessor<T>, index: s.Accessor<number>) => s.JSX.Element;
19
+ };
20
+
21
+ function createVirtual<T>(
22
+ component: typeof lngp.Row | typeof lngp.Column,
23
+ props: VirtualProps<T>,
24
+ keyHandlers: Record<string, lng.KeyHandler>
25
+ ) {
26
+ const isRow = component === lngp.Row;
27
+ const axis = isRow ? 'x' : 'y';
28
+ const [cursor, setCursor] = s.createSignal(props.selected ?? 0);
29
+ const bufferSize = s.createMemo(() => props.bufferSize || 2);
30
+ const scrollIndex = s.createMemo(() => props.scrollIndex || 0);
31
+ const items = s.createMemo(() => props.each || []);
32
+ const itemCount = s.createMemo(() => items().length);
33
+ const scrollType = s.createMemo(() => props.scroll || 'auto');
34
+
35
+ const selected = () => {
36
+ if (props.wrap) {
37
+ return Math.max(bufferSize(), scrollIndex());
38
+ }
39
+ return props.selected || 0;
40
+ };
41
+
42
+ let cachedScaledSize: number | undefined;
43
+ let targetPosition: number | undefined;
44
+ let cachedAnimationController: lng.IAnimationController | undefined;
45
+ const uniformSize = s.createMemo(() => {
46
+ return props.uniformSize !== false;
47
+ });
48
+
49
+ type SliceState = { start: number; slice: T[]; selected: number, delta: number, shiftBy: number, atStart: boolean };
50
+ const [slice, setSlice] = s.createSignal<SliceState>({
51
+ start: 0,
52
+ slice: [],
53
+ selected: 0,
54
+ delta: 0,
55
+ shiftBy: 0,
56
+ atStart: true,
57
+ });
58
+
59
+ function normalizeDeltaForWindow(delta: number, windowLen: number): number {
60
+ if (!windowLen) return 0;
61
+ const half = windowLen / 2;
62
+ if (delta > half) return delta - windowLen;
63
+ if (delta < -half) return delta + windowLen;
64
+ return delta;
65
+ }
66
+
67
+ function computeSize(selected: number = 0) {
68
+ if (uniformSize() && cachedScaledSize) {
69
+ return cachedScaledSize;
70
+ } else if (viewRef) {
71
+ const gap = viewRef.gap || 0;
72
+ const dimension = isRow ? 'width' : 'height'; // This can't be moved up as it depends on viewRef
73
+ const prevSelectedChild = viewRef.children[selected];
74
+
75
+ if (prevSelectedChild instanceof lng.ElementNode) {
76
+ const itemSize = prevSelectedChild[dimension] || 0;
77
+ const focusStyle = (prevSelectedChild.style?.focus as lng.NodeStyles);
78
+ const scale = (focusStyle?.scale ?? prevSelectedChild.scale ?? 1);
79
+ const scaledSize = itemSize * (props.factorScale ? scale : 1) + gap;
80
+ cachedScaledSize = scaledSize;
81
+ return scaledSize;
82
+ }
83
+ }
84
+ return 0;
85
+ }
86
+
87
+ function computeSlice(c: number, delta: number, prev: SliceState): SliceState {
88
+ const total = itemCount();
89
+ if (total === 0) return { start: 0, slice: [], selected: 0, delta, shiftBy: 0, atStart: true };
90
+
91
+ const length = props.displaySize + bufferSize();
92
+ let start = prev.start;
93
+ let selected = prev.selected;
94
+ let atStart = prev.atStart;
95
+ let shiftBy = -delta;
96
+
97
+ switch (scrollType()) {
98
+ case 'always':
99
+ if (props.wrap) {
100
+ start = utils.mod(c - 1, total);
101
+ selected = 1;
102
+ } else {
103
+ start = utils.clamp(
104
+ c - bufferSize(),
105
+ 0,
106
+ Math.max(0, total - props.displaySize - bufferSize()),
107
+ );
108
+ if (delta === 0 && c > 3) {
109
+ shiftBy = c < 3 ? -c : -2;
110
+ selected = 2;
111
+ } else {
112
+ selected =
113
+ c < bufferSize()
114
+ ? c
115
+ : c >= total - props.displaySize
116
+ ? c - (total - props.displaySize) + bufferSize()
117
+ : bufferSize();
118
+ }
119
+ }
120
+ break;
121
+
122
+ case 'auto':
123
+ if (props.wrap) {
124
+ if (delta === 0) {
125
+ selected = scrollIndex() || 1;
126
+ start = utils.mod(c - (scrollIndex() || 1), total);
127
+ } else {
128
+ start = utils.mod(c - (prev.selected || 1), total);
129
+ }
130
+ } else {
131
+ if (delta < 0) {
132
+ // Moving left
133
+ if (prev.start > 0 && prev.selected >= props.displaySize) {
134
+ // Move selection left inside slice
135
+ start = prev.start;
136
+ selected = prev.selected - 1;
137
+ } else if (prev.start > 0) {
138
+ // Move selection left inside slice
139
+ start = prev.start - 1;
140
+ selected = prev.selected;
141
+ // shiftBy = 0;
142
+ } else if (prev.start === 0 && !prev.atStart) {
143
+ start = 0;
144
+ selected = prev.selected - 1;
145
+ atStart = true;
146
+ } else if (selected >= props.displaySize - 1) {
147
+ // Shift window left, keep selection pinned
148
+ start = 0;
149
+ selected = prev.selected - 1;
150
+ } else {
151
+ start = 0;
152
+ selected = prev.selected - 1;
153
+ shiftBy = 0;
154
+ }
155
+ } else if (delta > 0) {
156
+ // Moving right
157
+ if (prev.selected < scrollIndex()) {
158
+ // Move selection right inside slice
159
+ start = prev.start;
160
+ selected = prev.selected + 1;
161
+ shiftBy = 0;
162
+ } else if (prev.selected === scrollIndex() || atStart) {
163
+ start = prev.start;
164
+ selected = prev.selected + 1;
165
+ atStart = false;
166
+ } else if (prev.start === 0 && prev.selected === 0) {
167
+ start = 0;
168
+ selected = 1;
169
+ atStart = false;
170
+ } else if (prev.start >= total - props.displaySize) {
171
+ // At end: clamp slice, selection drifts right
172
+ start = prev.start;
173
+ selected = c - start;
174
+ shiftBy = 0;
175
+ } else {
176
+ // Shift window right, keep selection pinned
177
+ start = prev.start + 1;
178
+ selected = Math.max(prev.selected, scrollIndex() + 1);;
179
+ }
180
+ } else {
181
+ // Initial setup
182
+ if (c > 0) {
183
+ start = Math.min(c - (scrollIndex() || 1), total - props.displaySize - bufferSize());
184
+ selected = Math.max(scrollIndex() || 1, c - start);
185
+ shiftBy = total - c < 3 ? c - total : -1;
186
+ atStart = false;
187
+ } else {
188
+ start = prev.start;
189
+ selected = prev.selected;
190
+ }
191
+ }
192
+ }
193
+ break;
194
+
195
+ case 'edge':
196
+ const startScrolling = Math.max(1, props.displaySize + (atStart ? -1 : 0));
197
+ if (props.wrap) {
198
+ if (delta > 0) {
199
+ if (prev.selected < startScrolling) {
200
+ selected = prev.selected + 1;
201
+ shiftBy = 0;
202
+ } else if (prev.selected === startScrolling && atStart) {
203
+ selected = prev.selected + 1;
204
+ atStart = false;
205
+ } else {
206
+ start = utils.mod(prev.start + 1, total);
207
+ selected = prev.selected;
208
+ }
209
+ } else if (delta < 0) {
210
+ if (prev.selected > 1) {
211
+ selected = prev.selected - 1;
212
+ shiftBy = 0;
213
+ } else {
214
+ start = utils.mod(prev.start - 1, total);
215
+ selected = 1;
216
+ }
217
+ } else {
218
+ start = utils.mod(c - 1, total);
219
+ selected = 1;
220
+ shiftBy = -1;
221
+ atStart = false;
222
+ }
223
+ } else {
224
+ if (delta === 0 && c > 0) {
225
+ //initial setup
226
+ selected = c > startScrolling ? startScrolling : c;
227
+ start = Math.max(0, c - startScrolling + 1);
228
+ shiftBy = c > startScrolling ? -1 : 0;
229
+ atStart = c < startScrolling;
230
+ } else if (delta > 0) {
231
+ if (prev.selected < startScrolling) {
232
+ selected = prev.selected + 1;
233
+ shiftBy = 0;
234
+ } else if (prev.selected === startScrolling && atStart) {
235
+ selected = prev.selected + 1;
236
+ atStart = false;
237
+ } else {
238
+ start = prev.start + 1;
239
+ selected = prev.selected;
240
+ atStart = false;
241
+ }
242
+ } else if (delta < 0) {
243
+ if (prev.selected > 1) {
244
+ selected = prev.selected - 1;
245
+ shiftBy = 0;
246
+ } else if (c > 1) {
247
+ start = Math.max(0, c - 1);
248
+ selected = 1;
249
+ } else if (c === 1) {
250
+ start = 0;
251
+ selected = 1;
252
+ } else {
253
+ start = 0;
254
+ selected = 0;
255
+ shiftBy = atStart ? 0 : shiftBy;
256
+ atStart = true;
257
+ }
258
+ }
259
+ }
260
+ break;
261
+
262
+ case 'none':
263
+ default:
264
+ start = 0;
265
+ selected = c;
266
+ shiftBy = 0;
267
+ break;
268
+ }
269
+
270
+ let newSlice = prev.slice;
271
+ if (start !== prev.start || newSlice.length === 0) {
272
+ newSlice = props.wrap
273
+ ? Array.from(
274
+ { length },
275
+ (_, i) => items()[utils.mod(start + i, total)],
276
+ ) as T[]
277
+ : items().slice(start, start + length);
278
+ }
279
+
280
+ const state: SliceState = { start, slice: newSlice, selected, delta, shiftBy, atStart };
281
+
282
+ if (props.debugInfo) {
283
+ console.log(`[Virtual]`, {
284
+ cursor: c,
285
+ delta,
286
+ start,
287
+ selected,
288
+ shiftBy,
289
+ slice: state.slice,
290
+ });
291
+ }
292
+
293
+ return state;
294
+ }
295
+
296
+ let viewRef!: lngp.NavigableElement;
297
+
298
+ function scrollToIndex(this: lng.ElementNode, index: number) {
299
+ s.untrack(() => {
300
+ if (itemCount() === 0) return;
301
+
302
+ lastNavTime = performance.now();
303
+ if (originalPosition !== undefined) {
304
+ viewRef.lng[axis] = originalPosition;
305
+ targetPosition = originalPosition;
306
+ }
307
+
308
+ updateSelected([utils.clamp(index, 0, itemCount() - 1)]);
309
+ });
310
+ }
311
+
312
+ let lastNavTime = 0;
313
+ function getAdaptiveDuration(duration: number = 250) {
314
+ const now = performance.now();
315
+ const delta = now - lastNavTime;
316
+ lastNavTime = now;
317
+ if (delta < duration) return delta;
318
+ return duration;
319
+ }
320
+
321
+ let originalPosition: number | undefined;
322
+ const onSelectedChanged: lngp.OnSelectedChanged = function (_idx, elm, _active, _lastIdx) {
323
+ let idx = _idx;
324
+ let lastIdx = _lastIdx || 0;
325
+ let active = _active;
326
+ const noChange = idx === lastIdx;
327
+ const total = itemCount();
328
+ originalPosition = originalPosition ?? elm[axis];
329
+
330
+ if (props.onSelectedChanged) {
331
+ props.onSelectedChanged.call(this as lngp.NavigableElement, idx, this as lngp.NavigableElement, active, lastIdx);
332
+ }
333
+
334
+ if (noChange) return;
335
+
336
+ const rawDelta = idx - (lastIdx ?? 0);
337
+ const windowLen =
338
+ elm?.children?.length ?? props.displaySize + bufferSize();
339
+ const delta = props.wrap
340
+ ? normalizeDeltaForWindow(rawDelta, windowLen)
341
+ : rawDelta;
342
+
343
+ setCursor(c => {
344
+ const next = c + delta;
345
+ return props.wrap
346
+ ? utils.mod(next, total)
347
+ : utils.clamp(next, 0, total - 1);
348
+ });
349
+
350
+ const newState = computeSlice(cursor(), delta, slice());
351
+ setSlice(newState);
352
+ elm.selected = newState.selected;
353
+
354
+ if (
355
+ props.onEndReachedThreshold !== undefined &&
356
+ cursor() >= itemCount() - props.onEndReachedThreshold
357
+ ) {
358
+ props.onEndReached?.();
359
+ }
360
+
361
+ if (newState.shiftBy === 0) return;
362
+
363
+ const prevChildPos = (targetPosition ?? this[axis]) + active[axis];
364
+
365
+ queueMicrotask(() => {
366
+ elm.updateLayout();
367
+ const childSize = computeSize(slice().selected);
368
+
369
+ if (cachedAnimationController && cachedAnimationController.state === 'running') {
370
+ cachedAnimationController.stop();;
371
+ }
372
+
373
+ if (lng.Config.animationsEnabled) {
374
+ this.lng[axis] = prevChildPos - active[axis];
375
+ let offset = this.lng[axis] + (childSize * slice().shiftBy);
376
+ targetPosition = offset;
377
+ cachedAnimationController = this.animate(
378
+ { [axis]: offset },
379
+ { ...this.animationSettings, duration: getAdaptiveDuration(this.animationSettings?.duration)}
380
+ ).start();
381
+ } else {
382
+ this.lng[axis] = this.lng[axis]! + (childSize * slice().shiftBy);
383
+ }
384
+ });
385
+ };
386
+
387
+ const updateSelected = ([sel, _items]: [number?, any?]) => {
388
+ if (!viewRef || sel === undefined || itemCount() === 0) return;
389
+ const item = items()[sel];
390
+ setCursor(sel);
391
+ const newState = computeSlice(cursor(), 0, slice());
392
+ setSlice(newState);
393
+
394
+ queueMicrotask(() => {
395
+ viewRef.updateLayout();
396
+ let activeIndex = viewRef.children.findIndex(x => x.item === item);
397
+ if (activeIndex === -1) return;
398
+ viewRef.selected = activeIndex;
399
+ viewRef.children[activeIndex]?.setFocus();
400
+ });
401
+ };
402
+
403
+ let doOnce = false;
404
+ s.createEffect(s.on([() => props.wrap, items], () => {
405
+ if (!viewRef || itemCount() === 0 || !props.wrap || doOnce) return;
406
+ doOnce = true;
407
+ // offset just for wrap so we keep one item before
408
+ queueMicrotask(() => {
409
+ const childSize = computeSize(slice().selected);
410
+ viewRef.lng[axis] = (viewRef.lng[axis] || 0) + (childSize * -1);
411
+ // Original Position is offset to support scrollToIndex
412
+ originalPosition = viewRef.lng[axis];
413
+ targetPosition = viewRef.lng[axis];
414
+ });
415
+ }));
416
+
417
+ s.createEffect(s.on([() => props.selected, items], updateSelected));
418
+
419
+ s.createEffect(s.on(items, () => {
420
+ if (!viewRef || itemCount() === 0) return;
421
+ if (cursor() >= itemCount()) {
422
+ setCursor(itemCount() - 1);
423
+ }
424
+ const newState = computeSlice(cursor(), 0, slice());
425
+ setSlice(newState);
426
+ viewRef.selected = newState.selected;
427
+ }));
428
+
429
+ return (<view
430
+ {...props}
431
+ {...keyHandlers}
432
+ ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)}
433
+ selected={selected()}
434
+ cursor={cursor()}
435
+ forwardFocus={/* @once */ lngp.navigableForwardFocus}
436
+ scrollToIndex={/* @once */ scrollToIndex}
437
+ onSelectedChanged={/* @once */ onSelectedChanged}
438
+ style={/* @once */ lng.combineStyles(
439
+ props.style,
440
+ component === lngp.Row
441
+ ? {
442
+ display: 'flex',
443
+ gap: 30,
444
+ transition: { x: { duration: 250, easing: 'ease-out' } },
445
+ }
446
+ : {
447
+ display: 'flex',
448
+ flexDirection: 'column',
449
+ gap: 30,
450
+ transition: { y: { duration: 250, easing: 'ease-out' } },
451
+ }
452
+ )}
453
+ >
454
+ <List each={slice().slice}>{props.children}</List>
455
+ </view>
456
+ );
457
+ }
458
+
459
+ export function VirtualRow<T>(props: VirtualProps<T>) {
460
+ return createVirtual(lngp.Row, props, {
461
+ onLeft: lngp.chainFunctions(props.onLeft, lngp.handleNavigation('left')) as lng.KeyHandler,
462
+ onRight: lngp.chainFunctions(props.onRight, lngp.handleNavigation('right')) as lng.KeyHandler,
463
+ });
464
+ }
465
+
466
+ export function VirtualColumn<T>(props: VirtualProps<T>) {
467
+ return createVirtual(lngp.Column, props, {
468
+ onUp: lngp.chainFunctions(props.onUp, lngp.handleNavigation('up')) as lng.KeyHandler,
469
+ onDown: lngp.chainFunctions(props.onDown, lngp.handleNavigation('down')) as lng.KeyHandler,
470
+ });
471
+ }