@lightningtv/solid 2.10.6 → 2.10.8

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.
@@ -10,24 +10,22 @@ export type VirtualProps<T> = lng.NewOmit<lngp.RowProps, 'children'> & {
10
10
  bufferSize?: number;
11
11
  wrap?: boolean;
12
12
  scrollIndex?: number;
13
- doScroll?: lngp.Scroller;
14
13
  onEndReached?: () => void;
15
14
  onEndReachedThreshold?: number;
16
- fallback?: s.JSX.Element;
15
+ debugInfo?: boolean;
16
+ factorScale?: boolean;
17
+ uniformSize?: boolean;
17
18
  children: (item: s.Accessor<T>, index: s.Accessor<number>) => s.JSX.Element;
18
19
  };
19
20
 
20
21
  function createVirtual<T>(
21
22
  component: typeof lngp.Row | typeof lngp.Column,
22
23
  props: VirtualProps<T>,
23
- scrollFn: ReturnType<typeof lngp.withScrolling>,
24
24
  keyHandlers: Record<string, lng.KeyHandler>
25
25
  ) {
26
26
  const [cursor, setCursor] = s.createSignal(props.selected ?? 0);
27
27
  const bufferSize = s.createMemo(() => props.bufferSize || 2);
28
- const scrollIndex = s.createMemo(() => {
29
- return props.scrollIndex || 0;
30
- });
28
+ const scrollIndex = s.createMemo(() => props.scrollIndex || 0);
31
29
  const items = s.createMemo(() => props.each || []);
32
30
  const itemCount = s.createMemo(() => items().length);
33
31
  const scrollType = s.createMemo(() => props.scroll || 'auto');
@@ -39,55 +37,270 @@ function createVirtual<T>(
39
37
  return props.selected || 0;
40
38
  };
41
39
 
42
- const start = () => {
43
- if (itemCount() === 0) return 0;
44
- if (props.wrap) {
45
- return utils.mod(cursor() - Math.max(bufferSize(), scrollIndex()), itemCount());
46
- }
47
- if (scrollType() === 'always') {
48
- return Math.min(Math.max(cursor() - bufferSize(), 0), itemCount() - props.displaySize - bufferSize());
49
- }
50
- if (scrollType() === 'auto') {
51
- return utils.clamp(cursor() - Math.max(bufferSize(), scrollIndex()), 0, Math.max(0, itemCount() - props.displaySize - bufferSize()));
52
- }
53
- return utils.clamp(cursor() - bufferSize(), 0, Math.max(0, itemCount() - props.displaySize));
54
- };
40
+ let cachedScaledSize: number | undefined;
41
+ let targetPosition: number | undefined;
42
+ let cachedAnimationController: lng.IAnimationController | undefined;
43
+ const uniformSize = s.createMemo(() => {
44
+ return props.uniformSize !== false;
45
+ });
55
46
 
56
- const end = () => {
57
- if (itemCount() === 0) return 0;
58
- if (props.wrap) {
59
- return (start() + props.displaySize + bufferSize()) % itemCount();
60
- }
61
- return Math.min(itemCount(), start() + props.displaySize + bufferSize());
62
- };
47
+ type SliceState = { start: number; slice: T[]; selected: number, delta: number, shiftBy: number, atStart: boolean };
48
+ const [slice, setSlice] = s.createSignal<SliceState>({
49
+ start: 0,
50
+ slice: [],
51
+ selected: 0,
52
+ delta: 0,
53
+ shiftBy: 0,
54
+ atStart: true,
55
+ });
63
56
 
64
- const getSlice = s.createMemo(() => {
65
- if (itemCount() === 0) return [];
66
- if (!props.wrap) {
67
- return items().slice(start(), end());
57
+ function normalizeDeltaForWindow(delta: number, windowLen: number): number {
58
+ if (!windowLen) return 0;
59
+ const half = windowLen / 2;
60
+ if (delta > half) return delta - windowLen;
61
+ if (delta < -half) return delta + windowLen;
62
+ return delta;
68
63
  }
69
- // Wrapping slice
70
- const sIdx = start();
71
- const eIdx = (sIdx + props.displaySize + bufferSize()) % itemCount();
72
- if (sIdx < eIdx) {
73
- return items().slice(sIdx, eIdx);
64
+
65
+ function computeSize(selected: number = 0) {
66
+ if (uniformSize() && cachedScaledSize) {
67
+ return cachedScaledSize;
68
+ } else if (viewRef) {
69
+ const gap = viewRef.gap || 0;
70
+ const isRow = component === lngp.Row;
71
+ const dimension = isRow ? 'width' : 'height';
72
+ const prevSelectedChild = viewRef.children[selected];
73
+
74
+ if (prevSelectedChild instanceof lng.ElementNode) {
75
+ const itemSize = prevSelectedChild[dimension] || 0;
76
+ const focusStyle = (prevSelectedChild.style?.focus as lng.NodeStyles);
77
+ const scale = (focusStyle?.scale ?? prevSelectedChild.scale ?? 1);
78
+ const scaledSize = itemSize * (props.factorScale ? scale : 1) + gap;
79
+ cachedScaledSize = scaledSize;
80
+ return scaledSize;
81
+ }
82
+ }
83
+ return 0;
74
84
  }
75
- return [...items().slice(sIdx), ...items().slice(0, eIdx)];
76
- });
77
85
 
78
- const [slice, setSlice] = s.createSignal(getSlice());
86
+ function computeSlice(c: number, delta: number, prev: SliceState): SliceState {
87
+ const total = itemCount();
88
+ if (total === 0) return { start: 0, slice: [], selected: 0, delta, shiftBy: 0, atStart: true };
89
+
90
+ const length = props.displaySize + bufferSize();
91
+ let start = prev.start;
92
+ let selected = prev.selected;
93
+ let atStart = prev.atStart;
94
+ let shiftBy = -delta;
95
+
96
+ switch (scrollType()) {
97
+ case 'always':
98
+ if (props.wrap) {
99
+ start = utils.mod(c - 1, total);
100
+ selected = 1;
101
+ } else {
102
+ start = utils.clamp(
103
+ c - bufferSize(),
104
+ 0,
105
+ Math.max(0, total - props.displaySize - bufferSize()),
106
+ );
107
+ if (delta === 0 && c > 3) {
108
+ shiftBy = c < 3 ? -c : -2;
109
+ selected = 2;
110
+ } else {
111
+ selected =
112
+ c < bufferSize()
113
+ ? c
114
+ : c >= total - props.displaySize
115
+ ? c - (total - props.displaySize) + bufferSize()
116
+ : bufferSize();
117
+ }
118
+ }
119
+ break;
120
+
121
+ case 'auto':
122
+ if (props.wrap) {
123
+ if (scrollIndex() && prev.selected < scrollIndex()) {
124
+ start = total - 1;
125
+ selected = Math.max(1, prev.selected + delta);
126
+ } else {
127
+ start = utils.mod(c - (scrollIndex() || 1), total);
128
+ selected = Math.max(1, prev.selected);
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()) {
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 - 1);
197
+ if (props.wrap) {
198
+ if (delta > 0) {
199
+ if (prev.selected < startScrolling) {
200
+ selected = prev.selected + 1;
201
+ shiftBy = 0;
202
+ } else {
203
+ start = utils.mod(prev.start + 1, total);
204
+ selected = startScrolling;
205
+ }
206
+ } else if (delta < 0) {
207
+ if (prev.selected > 1) {
208
+ selected = prev.selected - 1;
209
+ shiftBy = 0;
210
+ } else {
211
+ start = utils.mod(prev.start - 1, total);
212
+ selected = 1;
213
+ }
214
+ } else {
215
+ start = utils.mod(c - 1, total);
216
+ selected = 1;
217
+ }
218
+ } else {
219
+ if (delta === 0 && c > 0) {
220
+ //initial setup
221
+ selected = c > startScrolling ? startScrolling : c;
222
+ start = Math.max(0, c - startScrolling + 1);
223
+ shiftBy = c > startScrolling ? -1 : 0;
224
+ atStart = c < startScrolling;
225
+ } else if (delta > 0) {
226
+ if (prev.selected < startScrolling - 1) {
227
+ selected = prev.selected + 1;
228
+ shiftBy = 0;
229
+ } else {
230
+ start = prev.start + 1;
231
+ selected = prev.selected;
232
+ atStart = false;
233
+ }
234
+ } else if (delta < 0) {
235
+ if (prev.selected > 1) {
236
+ selected = prev.selected - 1;
237
+ shiftBy = 0;
238
+ } else if (c > 1) {
239
+ start = Math.max(0, c - 1);
240
+ selected = 1;
241
+ } else if (!atStart) {
242
+ start = 0;
243
+ selected = 0;
244
+ atStart = true;
245
+ } else {
246
+ start = 0;
247
+ selected = 0;
248
+ shiftBy = 0;
249
+ atStart = true;
250
+ }
251
+ }
252
+ }
253
+ break;
254
+
255
+ case 'none':
256
+ default:
257
+ start = 0;
258
+ selected = c;
259
+ shiftBy = 0;
260
+ break;
261
+ }
262
+
263
+ let newSlice = prev.slice;
264
+ if (start !== prev.start || newSlice.length === 0) {
265
+ newSlice = props.wrap
266
+ ? Array.from(
267
+ { length },
268
+ (_, i) => items()[utils.mod(start + i, total)],
269
+ ) as T[]
270
+ : items().slice(start, start + length);
271
+ }
272
+
273
+ const state: SliceState = { start, slice: newSlice, selected, delta, shiftBy, atStart };
274
+
275
+ if (props.debugInfo) {
276
+ console.log(`[Virtual]`, {
277
+ cursor: c,
278
+ delta,
279
+ start,
280
+ selected,
281
+ shiftBy,
282
+ slice: state.slice,
283
+ });
284
+ }
285
+
286
+ return state;
287
+ }
79
288
 
80
289
  let viewRef!: lngp.NavigableElement;
81
290
 
82
291
  function scrollToIndex(this: lng.ElementNode, index: number) {
83
292
  if (itemCount() === 0) return;
84
- let target = index;
85
- if (props.wrap) {
86
- target = utils.mod(index, itemCount());
87
- } else {
88
- target = utils.clamp(index, 0, itemCount() - 1);
89
- }
90
- updateSelected([target]);
293
+ updateSelected([utils.clamp(index, 0, itemCount() - 1)]);
294
+ }
295
+
296
+ let lastNavTime = 0;
297
+
298
+ function getAdaptiveDuration(duration: number = 250) {
299
+ const now = performance.now();
300
+ const delta = now - lastNavTime;
301
+ lastNavTime = now;
302
+ if (delta < duration) return delta;
303
+ return duration;
91
304
  }
92
305
 
93
306
  const onSelectedChanged: lngp.OnSelectedChanged = function (_idx, elm, _active, _lastIdx) {
@@ -95,116 +308,108 @@ function createVirtual<T>(
95
308
  let lastIdx = _lastIdx || 0;
96
309
  let active = _active;
97
310
  const initialRun = idx === lastIdx;
311
+ const total = itemCount();
312
+ const isRow = component === lngp.Row;
313
+ const axis = isRow ? 'x' : 'y';
314
+
315
+ if (props.onSelectedChanged) {
316
+ props.onSelectedChanged.call(this as lngp.NavigableElement, idx, this as lngp.NavigableElement, active, lastIdx);
317
+ }
98
318
 
99
319
  if (initialRun && !props.wrap) return;
100
320
 
321
+ const rawDelta = idx - (lastIdx ?? 0);
322
+ const windowLen =
323
+ elm?.children?.length ?? props.displaySize + bufferSize();
324
+ const delta = props.wrap
325
+ ? normalizeDeltaForWindow(rawDelta, windowLen)
326
+ : rawDelta;
327
+
101
328
  if (!initialRun) {
102
- if (props.wrap) {
103
- setCursor(c => utils.mod(c + idx - lastIdx, itemCount()));
104
- } else {
105
- setCursor(c => utils.clamp(c + idx - lastIdx, 0, Math.max(0, itemCount() - 1)));
106
- }
329
+ setCursor(c => {
330
+ const next = c + delta;
331
+ return props.wrap
332
+ ? utils.mod(next, total)
333
+ : utils.clamp(next, 0, total - 1);
334
+ });
107
335
 
108
- setSlice(getSlice());
336
+ const newState = computeSlice(cursor(), delta, slice());
337
+ setSlice(newState);
338
+ elm.selected = newState.selected;
109
339
 
110
- const c = cursor();
111
- const scroll = scrollType();
112
- if (props.wrap) {
113
- this.selected = Math.max(bufferSize(), scrollIndex());
114
- } else if (props.scrollIndex) {
115
- this.selected = Math.min(c, props.scrollIndex);
116
- if (c >= itemCount() - props.displaySize + bufferSize()) {
117
- this.selected = c - (itemCount() - props.displaySize) + bufferSize();
118
- }
119
- } else if (scroll === 'always' || scroll === 'auto') {
120
- if (c < bufferSize()) {
121
- this.selected = c;
122
- } else if (c >= itemCount() - props.displaySize) {
123
- this.selected = c - (itemCount() - props.displaySize) + bufferSize();
124
- } else {
125
- this.selected = bufferSize();
126
- }
127
- }
128
-
129
- if (props.onEndReachedThreshold !== undefined && cursor() >= items().length - props.onEndReachedThreshold) {
340
+ if (
341
+ props.onEndReachedThreshold !== undefined &&
342
+ cursor() >= itemCount() - props.onEndReachedThreshold
343
+ ) {
130
344
  props.onEndReached?.();
131
345
  }
346
+
347
+ if (newState.shiftBy === 0) return;
132
348
  }
133
- const isRow = component === lngp.Row;
134
- const prevChildPos = isRow
135
- ? this.x + active.x
136
- : this.y + active.y;
349
+
350
+ const prevChildPos = (targetPosition ?? this[axis]) + active[axis];
137
351
 
138
352
  queueMicrotask(() => {
139
- this.updateLayout();
140
- if (this._initialPosition === undefined && props.wrap) {
141
- this.offset = 0;
142
- const axis = isRow ? 'x' : 'y';
143
- this._initialPosition = this[axis];
144
- if (scrollIndex() > 0) {
145
- active = this.children[1] as lng.ElementNode;
146
- }
147
- }
148
- if (component === lngp.Row) {
149
- this.lng.x = this._targetPosition = prevChildPos - active.x;
150
- } else {
151
- this.lng.y = this._targetPosition = prevChildPos - active.y;
353
+ elm.updateLayout();
354
+ const childSize = computeSize(slice().selected);
355
+
356
+ if (cachedAnimationController && cachedAnimationController.state === 'running') {
357
+ cachedAnimationController.stop();;
152
358
  }
153
- scrollFn(idx, elm, active, lastIdx);
359
+ this.lng[axis] = prevChildPos - active[axis];
360
+ let offset = this.lng[axis] + (childSize * slice().shiftBy);
361
+ targetPosition = offset;
362
+ cachedAnimationController = this.animate(
363
+ { [axis]: offset },
364
+ { ...this.animationSettings, duration: getAdaptiveDuration(this.animationSettings?.duration)}
365
+ ).start();
154
366
  });
155
367
  };
156
368
 
157
- const chainedOnSelectedChanged = lngp.chainFunctions(props.onSelectedChanged, onSelectedChanged)!;
158
-
159
- const updateSelected = ([selected, _items]: [number?, any?]) => {
160
- if (!viewRef || selected === undefined) return;
161
- const sel = selected;
369
+ const updateSelected = ([sel, _items]: [number?, any?]) => {
370
+ if (!viewRef || sel === undefined || itemCount() === 0) return;
162
371
  const item = items()[sel];
163
- let active = viewRef.children.find(x => x.item === item);
164
- const lastSelected = viewRef.selected;
165
-
166
- if (active instanceof lng.ElementNode) {
167
- viewRef.selected = viewRef.children.indexOf(active);
168
- chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
169
- active.setFocus();
170
- } else {
171
- setCursor(sel);
172
- setSlice(getSlice());
173
- queueMicrotask(() => {
174
- viewRef.updateLayout();
175
- active = viewRef.children.find(x => x.item === item);
176
- if (active instanceof lng.ElementNode) {
177
- viewRef.selected = viewRef.children.indexOf(active);
178
- chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
179
- }
180
- });
181
- }
372
+ setCursor(sel);
373
+ const newState = computeSlice(cursor(), 0, slice());
374
+ setSlice(newState);
375
+
376
+ queueMicrotask(() => {
377
+ viewRef.updateLayout();
378
+ if (slice().shiftBy) {
379
+ const isRow = component === lngp.Row;
380
+ const axis = isRow ? 'x' : 'y';
381
+ const childSize = computeSize(slice().selected);
382
+ viewRef.lng[axis] = viewRef.lng[axis]! + (childSize * slice().shiftBy);
383
+ targetPosition = viewRef.lng[axis];
384
+ }
385
+ let activeIndex = viewRef.children.findIndex(x => x.item === item);
386
+ if (activeIndex === -1) return;
387
+ viewRef.selected = activeIndex;
388
+ viewRef.children[activeIndex]?.setFocus();
389
+ });
182
390
  };
183
391
 
184
- s.createEffect(s.on([() => props.selected, items], updateSelected));
392
+ s.createEffect(s.on([() => props.selected, items], updateSelected, { defer: true }));
185
393
 
186
394
  s.createEffect(s.on(items, () => {
187
- if (!viewRef) return;
395
+ if (!viewRef || itemCount() === 0) return;
188
396
  if (cursor() >= itemCount()) {
189
- setCursor(Math.max(0, itemCount() - 1));
397
+ setCursor(itemCount() - 1);
190
398
  }
191
- setSlice(getSlice());
192
- }, { defer: true }));
399
+ const newState = computeSlice(cursor(), 0, slice());
400
+ setSlice(newState);
401
+ viewRef.selected = newState.selected;
402
+ }));
193
403
 
194
404
  return (<view
195
405
  {...props}
406
+ {...keyHandlers}
196
407
  ref={lngp.chainRefs(el => { viewRef = el as lngp.NavigableElement; }, props.ref)}
197
408
  selected={selected()}
198
409
  cursor={cursor()}
199
- {...keyHandlers}
200
410
  forwardFocus={/* @once */ lngp.navigableForwardFocus}
201
411
  scrollToIndex={/* @once */ scrollToIndex}
202
- onCreate={/* @once */
203
- props.selected
204
- ? lngp.chainFunctions(props.onCreate, scrollFn)
205
- : props.onCreate
206
- }
207
- onSelectedChanged={/* @once */ chainedOnSelectedChanged}
412
+ onSelectedChanged={/* @once */ onSelectedChanged}
208
413
  style={/* @once */ lng.combineStyles(
209
414
  props.style,
210
415
  component === lngp.Row
@@ -221,21 +426,21 @@ function createVirtual<T>(
221
426
  }
222
427
  )}
223
428
  >
224
- <List each={slice()}>{props.children}</List>
429
+ <List each={slice().slice}>{props.children}</List>
225
430
  </view>
226
431
  );
227
432
  }
228
433
 
229
434
  export function VirtualRow<T>(props: VirtualProps<T>) {
230
- return createVirtual(lngp.Row, props, props.doScroll || lngp.withScrolling(true), {
231
- onLeft: lngp.chainFunctions(props.onLeft, lngp.navigableHandleNavigation) as lng.KeyHandler,
232
- onRight: lngp.chainFunctions(props.onRight, lngp.navigableHandleNavigation) as lng.KeyHandler,
435
+ return createVirtual(lngp.Row, props, {
436
+ onLeft: lngp.chainFunctions(props.onLeft, lngp.handleNavigation('left')) as lng.KeyHandler,
437
+ onRight: lngp.chainFunctions(props.onRight, lngp.handleNavigation('right')) as lng.KeyHandler,
233
438
  });
234
439
  }
235
440
 
236
441
  export function VirtualColumn<T>(props: VirtualProps<T>) {
237
- return createVirtual(lngp.Column, props, props.doScroll || lngp.withScrolling(false), {
238
- onUp: lngp.chainFunctions(props.onUp, lngp.navigableHandleNavigation) as lng.KeyHandler,
239
- onDown: lngp.chainFunctions(props.onDown, lngp.navigableHandleNavigation) as lng.KeyHandler,
442
+ return createVirtual(lngp.Column, props, {
443
+ onUp: lngp.chainFunctions(props.onUp, lngp.handleNavigation('up')) as lng.KeyHandler,
444
+ onDown: lngp.chainFunctions(props.onDown, lngp.handleNavigation('down')) as lng.KeyHandler,
240
445
  });
241
446
  }
@@ -14,12 +14,6 @@ const rowStyles: lng.NodeStyles = {
14
14
  },
15
15
  };
16
16
 
17
- function scrollToIndex(this: lng.ElementNode, index: number) {
18
- this.selected = index;
19
- columnScroll(index, this);
20
- this.setFocus();
21
- }
22
-
23
17
  export type VirtualGridProps<T> = lng.NewOmit<lngp.RowProps, 'children'> & {
24
18
  each: readonly T[] | undefined | null | false;
25
19
  columns: number; // items per row
@@ -99,9 +93,9 @@ export function VirtualGrid<T>(props: VirtualGridProps<T>): s.JSX.Element {
99
93
  const prevRowIndex = Math.floor((lastIdx || 0) / perRow);
100
94
  const prevStart = start();
101
95
 
96
+ setCursor(prevStart + idx);
102
97
  if (newRowIndex === prevRowIndex) return;
103
98
 
104
- setCursor(prevStart + idx);
105
99
  setSlice(items().slice(start(), end()));
106
100
 
107
101
  // this.selected is relative to the slice
@@ -118,49 +112,70 @@ export function VirtualGrid<T>(props: VirtualGridProps<T>): s.JSX.Element {
118
112
  queueMicrotask(() => {
119
113
  const prevRowY = this.y + active.y;
120
114
  this.updateLayout();
121
- // if (prevRowY > active.y) {
122
- // }
123
115
  this.lng.y = prevRowY - active.y;
124
- // this.y = prevRowY - active.y;
125
116
  columnScroll(idx, elm, active, lastIdx);
126
117
  });
127
118
  };
128
119
 
129
120
  const chainedOnSelectedChanged = lngp.chainFunctions(props.onSelectedChanged, onSelectedChanged)!;
130
121
 
131
- s.createEffect(
132
- s.on([() => props.selected, items], ([selected]) => {
133
- if (!viewRef || selected == null) return;
122
+ let cachedSelected: number | undefined;
123
+ const updateSelected = ([selected, _items]: [number?, any?]) => {
124
+ if (!viewRef || selected == null) return;
134
125
 
135
- const item = items()[selected];
136
- let active = viewRef.children.find(x => x.item === item);
137
- const lastSelected = viewRef.selected;
126
+ if (cachedSelected !== undefined) {
127
+ selected = cachedSelected;
128
+ cachedSelected = undefined;
129
+ }
138
130
 
139
- if (active instanceof lng.ElementNode) {
140
- viewRef.selected = viewRef.children.indexOf(active);
141
- chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
142
- } else {
143
- setSlice(items().slice(start(), end()));
144
-
145
- queueMicrotask(() => {
146
- viewRef.updateLayout();
147
- active = viewRef.children.find(x => x.item === item);
148
- if (active instanceof lng.ElementNode) {
149
- viewRef.selected = viewRef.children.indexOf(active);
150
- chainedOnSelectedChanged.call(viewRef, viewRef.selected, viewRef, active, lastSelected);
151
- }
152
- });
153
- }
154
- })
155
- );
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
+ updateSelected([index]);
163
+ }
164
+
165
+ s.createEffect(s.on([() => props.selected, items], updateSelected));
156
166
 
157
167
  s.createEffect(
158
168
  s.on(items, () => {
159
169
  if (!viewRef) return;
170
+ if (cachedSelected !== undefined) {
171
+ updateSelected([cachedSelected]);
172
+ return;
173
+ }
160
174
  setSlice(items().slice(start(), end()));
161
175
  }, { defer: true })
162
176
  );
163
177
 
178
+
164
179
  return (
165
180
  <view
166
181
  {...props}