ng-virtual-list 20.7.2 → 20.7.4

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 CHANGED
@@ -1,29 +1,60 @@
1
1
  # NgVirtualList
2
2
 
3
- Maximum performance for extremely large lists.<br/>
4
- Flexible, and actively maintained Angular library that excels with high-performance, feature-rich virtualized lists—including grouping, sticky headers, snapping, animations, single and multiple selection of elements and both scroll directions. Whether you're rendering millions of items or building interactive list components, it delivers scalability and customization.
3
+ 🚀 High-performance virtual scrolling for Angular apps. Render 100,000+ items in Angular without breaking a sweat. Smooth, customizable, and developer-friendly.
4
+
5
+ Flexible, and actively maintained Angular library that excels with high-performance, feature-rich virtualized lists—including grouping, sticky headers, snapping, animations, single and multiple selection of elements and both scroll directions. Whether you're rendering millions of items or building interactive list components, it delivers scalability and customization. Angular (14–20) compatibility.
5
6
 
6
7
  <img width="1033" height="171" alt="logo" src="https://github.com/user-attachments/assets/b559cfde-405a-4361-b71b-6715478d997d" />
7
8
 
8
- Angular version 20.X.X.
9
+ <b>Angular version 20.X.X</b>.
10
+
11
+ ![npm](https://img.shields.io/npm/v/ng-virtual-list)
12
+ ![npm downloads](https://img.shields.io/npm/dm/ng-virtual-list)
13
+ ![npm total downloads](https://img.shields.io/npm/dt/ng-virtual-list)
9
14
 
10
15
  [Live Demo](https://ng-virtual-list-chat-demo.eugene-grebennikov.pro/)
16
+ [(Code)](https://github.com/DjonnyX/ng-virtual-list-demo)
11
17
 
12
18
  [Live Examples](https://ng-virtual-list.eugene-grebennikov.pro/)
19
+ [(Code)](https://github.com/DjonnyX/ng-virtual-list-demo/tree/main/src/app)
20
+
21
+ <br/>
22
+
23
+ ## ✨ Why use ng-virtual-list?
24
+
25
+ ⚡ Blazing fast — only renders what’s visible (plus a smart buffer).<br/>
26
+ 📱 Works everywhere — smooth on desktop & mobile.<br/>
27
+ 🔀 Flexible layouts — vertical, horizontal, grouped lists, sticky headers.<br/>
28
+ 📏 Dynamic sizes — handles items of varying height/width.<br/>
29
+ 🎯 Precise control — scroll to an ID, or snap to positions.<br/>
30
+ 🔌 Angular-friendly — simple inputs/outputs, trackBy support.<br/>
13
31
 
14
32
  <br/>
15
33
 
16
- | **Pros** | **Description** |
17
- | --- | --- |
18
- | **High performance** | Only renders items visible in the viewport (plus a buffer), reducing DOM overhead and improving responsiveness—even with very large datasets |
19
- | **Grouped lists with sticky headers & snapping** | Supports grouping items, sticky headers, and optional “snap” behavior for clean section scrolling |
20
- | **Angular (14–20) compatibility** | Compatible with Angular versions 14 through 20.x, ensuring seamless integration in modern Angular projects |
21
- | **Scroll-to capabilities** | Allows programmatic navigation to specific items by ID |
22
- | **TypeScript support** | Comes with typing for safety and better integration in TypeScript projects |
34
+ ## ⚙️ Key Features
35
+
36
+ Virtualization modes
37
+ - Fixed size (fastest)
38
+ - Dynamic size (auto-measured)
39
+ - Scrolling control
40
+ - Scroll to item ID
41
+ - Smooth or instant scroll
42
+ - Custom snapping behavior
43
+ - Advanced layouts
44
+ - Grouped lists with sticky headers
45
+ - Horizontal or vertical scrolling
46
+ - Advanced layouts
47
+ - Grouped lists with sticky headers
48
+ - Horizontal or vertical scrolling
49
+ - Selecting elements
50
+ - Single selection
51
+ - Multiple selection
52
+ - Performance tuning
53
+ - bufferSize and maxBufferSize for fine-grained control
23
54
 
24
55
  <br/>
25
56
 
26
- ## When to Use It: Ideal Use Cases
57
+ ## 📱 When to Use It: Ideal Use Cases
27
58
 
28
59
  Drawing on general virtual-scroll insights and ng-virtual-list features:
29
60
 
@@ -44,28 +75,51 @@ As each item may contain images, nested components, or interactions, virtual ren
44
75
 
45
76
  Single and multiple selection of elements
46
77
 
78
+ Navigating with the keyboard
79
+
80
+ Support for element animation
81
+
47
82
  <br/>
48
83
 
49
- ## Installation
84
+ ## 📦 Installation
50
85
 
51
86
  ```bash
52
87
  npm i ng-virtual-list
53
88
  ```
54
89
 
55
- ## Examples
90
+ <br/>
91
+
92
+ ## 🚀 Quick Start
93
+ ```html
94
+ <ng-virtual-list [items]="items" [bufferSize]="5" [itemRenderer]="itemRenderer" [itemSize]="64"></ng-virtual-list>
95
+
96
+ <ng-template #itemRenderer let-data="data">
97
+ @if (data) {
98
+ <span>{{data.name}}</span>
99
+ }
100
+ </ng-template>
101
+ ```
102
+ ```ts
103
+ items = Array.from({ length: 100000 }, (_, i) => ({ id: i, name: `Item #${i}` }));
104
+ ```
105
+
106
+ <br/>
56
107
 
57
- ### Horizontal virtual list
108
+ ## 📱 Examples
109
+
110
+ ### Horizontal virtual list (Single selection)
58
111
 
59
112
  ![preview](https://github.com/user-attachments/assets/5a16d4b3-5e66-4d53-ae90-d0eab0b246a1)
60
113
 
61
114
  Template:
62
115
  ```html
63
116
  <ng-virtual-list class="list" direction="horizontal" [items]="horizontalItems" [bufferSize]="50"
64
- [itemRenderer]="horizontalItemRenderer" [itemSize]="64" (onItemClick)="onItemClick($event)"></ng-virtual-list>
117
+ [itemRenderer]="horizontalItemRenderer" [itemSize]="64" [methodForSelecting]="'select'"
118
+ [selectedIds]="2" (onSelect)="onSelect($event)" (onItemClick)="onItemClick($event)"></ng-virtual-list>
65
119
 
66
- <ng-template #horizontalItemRenderer let-data="data">
120
+ <ng-template #horizontalItemRenderer let-data="data" let-config="config">
67
121
  @if (data) {
68
- <div class="list__h-container">
122
+ <div [ngClass]="{'list__h-container': true, 'selected': config.selected}">
69
123
  <span>{{data.name}}</span>
70
124
  </div>
71
125
  }
@@ -80,10 +134,7 @@ interface ICollectionItem {
80
134
  name: string;
81
135
  }
82
136
 
83
- const HORIZONTAL_ITEMS: IVirtualListCollection<ICollectionItem> = [];
84
- for (let i = 0, l = 1000000; i < l; i++) {
85
- HORIZONTAL_ITEMS.push({ id: i + 1, name: `${i}` });
86
- }
137
+ const HORIZONTAL_ITEMS: IVirtualListCollection<ICollectionItem> = Array.from({ length: 100000 }, (_, i) => ({ id: i, name: `${i}` }));
87
138
 
88
139
  @Component({
89
140
  selector: 'app-root',
@@ -99,19 +150,24 @@ export class AppComponent {
99
150
  console.info(`Click: (ID: ${item.id}) Item ${item.data.name}`);
100
151
  }
101
152
  }
153
+
154
+ onSelect(data: Array<Id> | Id | undefined) {
155
+ console.info(`Select: ${JSON.stringify(data)}`);
156
+ }
102
157
  }
103
158
  ```
104
159
 
105
- ### Horizontal grouped virtual list
160
+ ### Horizontal grouped virtual list (Multiple selection)
106
161
 
107
162
  ![preview](https://github.com/user-attachments/assets/99584660-dc0b-4cd0-9439-9b051163c077)
108
163
 
109
164
  Template:
110
165
  ```html
111
166
  <ng-virtual-list class="list" direction="horizontal" [items]="horizontalGroupItems" [itemRenderer]="horizontalGroupItemRenderer"
112
- [bufferSize]="50" [itemConfigMap]="horizontalGroupItemConfigMap" [itemSize]="54" [snap]="true" (onItemClick)="onItemClick($event)"></ng-virtual-list>
167
+ [bufferSize]="50" [itemConfigMap]="horizontalGroupItemConfigMap" [itemSize]="54" [snap]="true" [methodForSelecting]="'multi-select'"
168
+ [selectedIds]="[3,2]" (onSelect)="onSelect($event)" (onItemClick)="onItemClick($event)"></ng-virtual-list>
113
169
 
114
- <ng-template #horizontalGroupItemRenderer let-data="data">
170
+ <ng-template #horizontalGroupItemRenderer let-data="data" let-config="config">
115
171
  @if (data) {
116
172
  @switch (data.type) {
117
173
  @case ("group-header") {
@@ -120,7 +176,7 @@ Template:
120
176
  </div>
121
177
  }
122
178
  @default {
123
- <div class="list__h-container">
179
+ <div [ngClass]="{'list__h-container': true, 'selected': config.selected}">
124
180
  <span>{{data.name}}</span>
125
181
  </div>
126
182
  }
@@ -168,6 +224,10 @@ export class AppComponent {
168
224
  console.info(`Click: (ID: ${item.id}) Item ${item.data.name}`);
169
225
  }
170
226
  }
227
+
228
+ onSelect(data: Array<Id> | Id | undefined) {
229
+ console.info(`Select: ${JSON.stringify(data)}`);
230
+ }
171
231
  }
172
232
  ```
173
233
 
@@ -437,7 +497,9 @@ export class AppComponent {
437
497
  }
438
498
  ```
439
499
 
440
- ## Stylization
500
+ <br/>
501
+
502
+ ## 🖼️ Stylization
441
503
 
442
504
  List items are encapsulated in shadowDOM, so to override default styles you need to use ::part access
443
505
 
@@ -521,7 +583,9 @@ Selecting even elements:
521
583
  }
522
584
  ```
523
585
 
524
- ## API
586
+ <br/>
587
+
588
+ ## 📚 API
525
589
 
526
590
  [NgVirtualListComponent](https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/ng-virtual-list.component.ts)
527
591
 
@@ -536,7 +600,7 @@ Inputs
536
600
  | maxBufferSize | number? = 100 | Maximum number of elements outside the scope of visibility. Default value is 100. If maxBufferSize is set to be greater than bufferSize, then adaptive buffer mode is enabled. The greater the scroll size, the more elements are allocated for rendering. |
537
601
  | itemRenderer | TemplateRef | Rendering element template. |
538
602
  | methodForSelecting | [MethodForSelecting](https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/enums/method-for-selecting.ts) | Method for selecting list items. Default value is 'none'. 'select' - List items are selected one by one. 'multi-select' - Multiple selection of list items. 'none' - List items are not selectable. |
539
- | itemConfigMap | [IVirtualListItemConfigMap?](https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/models/item-config-map.model.ts) | Sets sticky position and selectable for the list item element. If sticky position is greater than 0, then sticky position is applied. If the sticky value is greater than `0`, then the sticky position mode is enabled for the element. `1` - position start, `2` - position end. Default value is `0`. Selectable determines whether an element can be selected or not. Default value is `true`. |
603
+ | itemConfigMap | [IVirtualListItemConfigMap?](https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/models/item-config-map.model.ts) | Sets `sticky` position and `selectable` for the list item element. If `sticky` position is greater than `0`, then `sticky` position is applied. If the `sticky` value is greater than `0`, then the `sticky` position mode is enabled for the element. `1` - position start, `2` - position end. Default value is `0`. `selectable` determines whether an element can be selected or not. Default value is `true`. |
540
604
  | selectByClick | boolean? = true | If `false`, the element is selected using the config.select method passed to the template; if `true`, the element is selected by clicking on it. The default value is `true`. |
541
605
  | snap | boolean? = false | Determines whether elements will snap. Default value is "false". |
542
606
  | snappingMethod | [SnappingMethod? = 'normal'](https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/enums/snapping-method.ts) | Snapping method. 'normal' - Normal group rendering. 'advanced' - The group is rendered on a transparent background. List items below the group are not rendered. |
@@ -571,20 +635,28 @@ Methods
571
635
 
572
636
  <br/>
573
637
 
574
- ## Previous versions
638
+ ## 📦 Previous versions
575
639
 
576
640
  | Angular version | ng-virtual-list version | git | npm |
577
641
  |--|--|--|--|
578
- | 19.x | 19.7.3 | [19.x](https://github.com/DjonnyX/ng-virtual-list/tree/19.x) | [19.7.3](https://www.npmjs.com/package/ng-virtual-list/v/19.7.3) |
579
- | 18.x | 18.7.2 | [18.x](https://github.com/DjonnyX/ng-virtual-list/tree/18.x) | [18.7.2](https://www.npmjs.com/package/ng-virtual-list/v/18.7.2) |
580
- | 17.x | 17.7.2 | [17.x](https://github.com/DjonnyX/ng-virtual-list/tree/17.x) | [17.7.2](https://www.npmjs.com/package/ng-virtual-list/v/17.7.2) |
581
- | 16.x | 16.7.2 | [16.x](https://github.com/DjonnyX/ng-virtual-list/tree/16.x) | [16.7.2](https://www.npmjs.com/package/ng-virtual-list/v/16.7.2) |
582
- | 15.x | 15.7.2 | [15.x](https://github.com/DjonnyX/ng-virtual-list/tree/15.x) | [15.7.2](https://www.npmjs.com/package/ng-virtual-list/v/15.7.2) |
583
- | 14.x | 14.7.2 | [14.x](https://github.com/DjonnyX/ng-virtual-list/tree/14.x) | [14.7.2](https://www.npmjs.com/package/ng-virtual-list/v/14.7.2) |
642
+ | 19.x | 19.7.6 | [19.x](https://github.com/DjonnyX/ng-virtual-list/tree/19.x) | [19.7.6](https://www.npmjs.com/package/ng-virtual-list/v/19.7.6) |
643
+ | 18.x | 18.7.4 | [18.x](https://github.com/DjonnyX/ng-virtual-list/tree/18.x) | [18.7.4](https://www.npmjs.com/package/ng-virtual-list/v/18.7.4) |
644
+ | 17.x | 17.7.4 | [17.x](https://github.com/DjonnyX/ng-virtual-list/tree/17.x) | [17.7.4](https://www.npmjs.com/package/ng-virtual-list/v/17.7.4) |
645
+ | 16.x | 16.7.4 | [16.x](https://github.com/DjonnyX/ng-virtual-list/tree/16.x) | [16.7.4](https://www.npmjs.com/package/ng-virtual-list/v/16.7.4) |
646
+ | 15.x | 15.7.4 | [15.x](https://github.com/DjonnyX/ng-virtual-list/tree/15.x) | [15.7.4](https://www.npmjs.com/package/ng-virtual-list/v/15.7.4) |
647
+ | 14.x | 14.7.4 | [14.x](https://github.com/DjonnyX/ng-virtual-list/tree/14.x) | [14.7.4](https://www.npmjs.com/package/ng-virtual-list/v/14.7.4) |
648
+
649
+ <br/>
650
+
651
+ ## 🤝 Contributing
652
+
653
+ PRs and feature requests are welcome!
654
+ Open an issue or start a discussion to shape the future of [ng-virtual-list](https://github.com/DjonnyX/ng-virtual-list/).
655
+ Try it out, star ⭐ the repo, and let us know what you’re building.
584
656
 
585
657
  <br/>
586
658
 
587
- ## License
659
+ ## 📄 License
588
660
 
589
661
  MIT License
590
662
 
@@ -606,4 +678,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
606
678
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
607
679
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
608
680
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
609
- SOFTWARE.
681
+ SOFTWARE.
@@ -2,7 +2,7 @@ import * as i1 from '@angular/common';
2
2
  import { CommonModule } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
4
  import { Injectable, inject, signal, ElementRef, ChangeDetectionStrategy, Component, viewChild, output, input, ViewContainerRef, ViewChild, ViewEncapsulation } from '@angular/core';
5
- import { Subject, tap, combineLatest, map, filter, distinctUntilChanged, switchMap, of } from 'rxjs';
5
+ import { Subject, tap, fromEvent, combineLatest, map, filter, distinctUntilChanged, switchMap, of } from 'rxjs';
6
6
  import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
7
7
  import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
8
8
 
@@ -109,10 +109,11 @@ const CLASS_LIST_VERTICAL = 'vertical';
109
109
  const CLASS_LIST_HORIZONTAL = 'horizontal';
110
110
  // styles
111
111
  const PART_DEFAULT_ITEM = 'item';
112
- const PART_ITEM_ODD = ' odd';
113
- const PART_ITEM_EVEN = ' even';
114
- const PART_ITEM_SNAPPED = ' snapped';
115
- const PART_ITEM_SELECTED = ' selected';
112
+ const PART_ITEM_ODD = ' item-odd';
113
+ const PART_ITEM_EVEN = ' item-even';
114
+ const PART_ITEM_SNAPPED = ' item-snapped';
115
+ const PART_ITEM_SELECTED = ' item-selected';
116
+ const PART_ITEM_FOCUSED = ' item-focused';
116
117
 
117
118
  /**
118
119
  * Virtual List Item Interface
@@ -158,6 +159,7 @@ class NgVirtualListService {
158
159
  }
159
160
  selectByClick = DEFAULT_SELECT_BY_CLICK;
160
161
  _trackBox;
162
+ listElement = null;
161
163
  constructor() {
162
164
  this._$methodOfSelecting.pipe(takeUntilDestroyed(), tap(v => {
163
165
  switch (v) {
@@ -265,7 +267,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImpor
265
267
  }]
266
268
  }], ctorParameters: () => [] });
267
269
 
268
- const ATTR_AREA_SELECTED = 'area-selected';
270
+ const ATTR_AREA_SELECTED = 'area-selected', TABINDEX = 'index', KEY_SPACE = " ", KEY_ARR_LEFT = "ArrowLeft", KEY_ARR_UP = "ArrowUp", KEY_ARR_RIGHT = "ArrowRight", KEY_ARR_DOWN = "ArrowDown";
269
271
  /**
270
272
  * Virtual list item component
271
273
  * @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/components/ng-virtual-list-item.component.ts
@@ -281,6 +283,7 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
281
283
  _isSelected = false;
282
284
  config = signal({});
283
285
  measures = signal(undefined);
286
+ focus = signal(false);
284
287
  _part = PART_DEFAULT_ITEM;
285
288
  get part() { return this._part; }
286
289
  regular = false;
@@ -331,23 +334,71 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
331
334
  super();
332
335
  this._id = this._service.generateComponentId();
333
336
  const $data = toObservable(this.data);
337
+ fromEvent(this.element, 'focusin').pipe(takeUntilDestroyed(), tap(e => {
338
+ this.focus.set(true);
339
+ this.updateConfig(this._data);
340
+ this.updatePartStr(this._data, this._isSelected);
341
+ })).subscribe(),
342
+ fromEvent(this.element, 'focusout').pipe(takeUntilDestroyed(), tap(e => {
343
+ this.focus.set(false);
344
+ this.updateConfig(this._data);
345
+ this.updatePartStr(this._data, this._isSelected);
346
+ })).subscribe(),
347
+ fromEvent(this.element, 'keydown').pipe(takeUntilDestroyed(), tap(e => {
348
+ switch (e.key) {
349
+ case KEY_SPACE: {
350
+ e.stopImmediatePropagation();
351
+ e.preventDefault();
352
+ this._service.select(this._data);
353
+ break;
354
+ }
355
+ case KEY_ARR_LEFT:
356
+ if (!this.config().isVertical) {
357
+ e.stopImmediatePropagation();
358
+ e.preventDefault();
359
+ this.focusPrev();
360
+ }
361
+ break;
362
+ case KEY_ARR_UP:
363
+ if (this.config().isVertical) {
364
+ e.stopImmediatePropagation();
365
+ e.preventDefault();
366
+ this.focusPrev();
367
+ }
368
+ break;
369
+ case KEY_ARR_RIGHT:
370
+ if (!this.config().isVertical) {
371
+ e.stopImmediatePropagation();
372
+ e.preventDefault();
373
+ this.focusNext();
374
+ }
375
+ break;
376
+ case KEY_ARR_DOWN:
377
+ if (this.config().isVertical) {
378
+ e.stopImmediatePropagation();
379
+ e.preventDefault();
380
+ this.focusNext();
381
+ }
382
+ break;
383
+ }
384
+ })).subscribe();
334
385
  combineLatest([$data, this._service.$methodOfSelecting, this._service.$selectedIds]).pipe(takeUntilDestroyed(), map(([, m, ids]) => ({ method: m, ids })), tap(({ method, ids }) => {
335
386
  switch (method) {
336
387
  case MethodsForSelectingTypes.SELECT: {
337
388
  const id = ids, isSelected = id === this.itemId;
338
- this._elementRef.nativeElement.setAttribute(ATTR_AREA_SELECTED, String(isSelected));
389
+ this.element.setAttribute(ATTR_AREA_SELECTED, String(isSelected));
339
390
  this._isSelected = isSelected;
340
391
  break;
341
392
  }
342
393
  case MethodsForSelectingTypes.MULTI_SELECT: {
343
394
  const actualIds = ids, isSelected = this.itemId !== undefined && actualIds && actualIds.includes(this.itemId);
344
- this._elementRef.nativeElement.setAttribute(ATTR_AREA_SELECTED, String(isSelected));
395
+ this.element.setAttribute(ATTR_AREA_SELECTED, String(isSelected));
345
396
  this._isSelected = isSelected;
346
397
  break;
347
398
  }
348
399
  case MethodsForSelectingTypes.NONE:
349
400
  default: {
350
- this._elementRef.nativeElement.removeAttribute(ATTR_AREA_SELECTED);
401
+ this.element.removeAttribute(ATTR_AREA_SELECTED);
351
402
  this._isSelected = false;
352
403
  break;
353
404
  }
@@ -357,11 +408,29 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
357
408
  this.updateMeasures(this._data);
358
409
  })).subscribe();
359
410
  }
411
+ focusNext() {
412
+ const tabIndex = this.config()?.tabIndex ?? 0;
413
+ if (this._service.listElement && tabIndex > 0) {
414
+ const el = this._service.listElement.querySelector(`[${TABINDEX}="${tabIndex + 1}"]`);
415
+ if (el) {
416
+ el.focus();
417
+ }
418
+ }
419
+ }
420
+ focusPrev() {
421
+ const tabIndex = this.config()?.tabIndex ?? 0;
422
+ if (this._service.listElement && tabIndex > 1) {
423
+ const el = this._service.listElement.querySelector(`[${TABINDEX}="${tabIndex - 1}"]`);
424
+ if (el) {
425
+ el.focus();
426
+ }
427
+ }
428
+ }
360
429
  updateMeasures(v) {
361
430
  this.measures.set(v?.measures ? { ...v.measures } : undefined);
362
431
  }
363
432
  updateConfig(v) {
364
- this.config.set({ ...v?.config || {}, selected: this._isSelected, select: this._selectHandler(v) });
433
+ this.config.set({ ...v?.config || {}, selected: this._isSelected, select: this._selectHandler(v), focus: this.focus() });
365
434
  }
366
435
  update() {
367
436
  const data = this._data, regular = this.regular, length = this._regularLength;
@@ -401,6 +470,9 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
401
470
  if (isSelected) {
402
471
  part += PART_ITEM_SELECTED;
403
472
  }
473
+ if (this.focus()) {
474
+ part += PART_ITEM_FOCUSED;
475
+ }
404
476
  this._part = part;
405
477
  }
406
478
  getBounds() {
@@ -445,14 +517,14 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
445
517
  this._service.itemClick(this._data);
446
518
  }
447
519
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: NgVirtualListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
448
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: NgVirtualListItemComponent, isStandalone: true, selector: "ng-virtual-list-item", host: { attributes: { "role": "listitem" }, classAttribute: "ngvl__item" }, usesInheritance: true, ngImport: i0, template: "@let item = data();\r\n@let _config = config();\r\n@let _measures = measures();\r\n@let renderer = itemRenderer();\r\n\r\n@if (item) {\r\n <div #listItem [part]=\"part\" class=\"ngvl-item__container\"\r\n [ngClass]=\"{'snapped': item.config.snapped, 'snapped-out': item.config.snappedOut}\" (click)=\"onClickHandler()\">\r\n @if (renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\"\r\n [ngTemplateOutletContext]=\"{data: item.data, measures: _measures, config: _config}\" />\r\n }\r\n </div>\r\n}", styles: [":host{display:block;position:absolute;left:0;top:0;box-sizing:border-box;overflow:hidden}.ngvl-item__container{margin:0;padding:0;overflow:hidden;background-color:#fff;width:inherit;height:inherit}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
520
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: NgVirtualListItemComponent, isStandalone: true, selector: "ng-virtual-list-item", host: { attributes: { "role": "listitem" }, classAttribute: "ngvl__item" }, usesInheritance: true, ngImport: i0, template: "@let item = data();\r\n@let _config = config();\r\n@let _measures = measures();\r\n@let renderer = itemRenderer();\r\n\r\n@if (item) {\r\n <div #listItem [part]=\"part\" [attr.index]=\"item.config.tabIndex\" tabindex=\"1\" class=\"ngvl-item__container\"\r\n [ngClass]=\"{'snapped': item.config.snapped, 'snapped-out': item.config.snappedOut, 'focus': focus()}\" (click)=\"onClickHandler()\">\r\n @if (renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\"\r\n [ngTemplateOutletContext]=\"{data: item.data, measures: _measures, config: _config}\" />\r\n }\r\n </div>\r\n}", styles: [":host{display:block;position:absolute;left:0;top:0;box-sizing:border-box;overflow:hidden}.ngvl-item__container{margin:0;padding:0;overflow:hidden;background-color:#fff;width:inherit;height:inherit;box-sizing:border-box}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
449
521
  }
450
522
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: NgVirtualListItemComponent, decorators: [{
451
523
  type: Component,
452
524
  args: [{ selector: 'ng-virtual-list-item', imports: [CommonModule], host: {
453
525
  'class': 'ngvl__item',
454
526
  'role': 'listitem',
455
- }, changeDetection: ChangeDetectionStrategy.OnPush, template: "@let item = data();\r\n@let _config = config();\r\n@let _measures = measures();\r\n@let renderer = itemRenderer();\r\n\r\n@if (item) {\r\n <div #listItem [part]=\"part\" class=\"ngvl-item__container\"\r\n [ngClass]=\"{'snapped': item.config.snapped, 'snapped-out': item.config.snappedOut}\" (click)=\"onClickHandler()\">\r\n @if (renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\"\r\n [ngTemplateOutletContext]=\"{data: item.data, measures: _measures, config: _config}\" />\r\n }\r\n </div>\r\n}", styles: [":host{display:block;position:absolute;left:0;top:0;box-sizing:border-box;overflow:hidden}.ngvl-item__container{margin:0;padding:0;overflow:hidden;background-color:#fff;width:inherit;height:inherit}\n"] }]
527
+ }, changeDetection: ChangeDetectionStrategy.OnPush, template: "@let item = data();\r\n@let _config = config();\r\n@let _measures = measures();\r\n@let renderer = itemRenderer();\r\n\r\n@if (item) {\r\n <div #listItem [part]=\"part\" [attr.index]=\"item.config.tabIndex\" tabindex=\"1\" class=\"ngvl-item__container\"\r\n [ngClass]=\"{'snapped': item.config.snapped, 'snapped-out': item.config.snappedOut, 'focus': focus()}\" (click)=\"onClickHandler()\">\r\n @if (renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\"\r\n [ngTemplateOutletContext]=\"{data: item.data, measures: _measures, config: _config}\" />\r\n }\r\n </div>\r\n}", styles: [":host{display:block;position:absolute;left:0;top:0;box-sizing:border-box;overflow:hidden}.ngvl-item__container{margin:0;padding:0;overflow:hidden;background-color:#fff;width:inherit;height:inherit;box-sizing:border-box}\n"] }]
456
528
  }], ctorParameters: () => [] });
457
529
 
458
530
  /**
@@ -1548,7 +1620,7 @@ class TrackBox extends CacheMap {
1548
1620
  const { width, height, normalizedItemWidth, normalizedItemHeight, dynamicSize, itemsOnDisplayLength, itemsFromStartToScrollEnd, isVertical, renderItems: renderItemsLength, scrollSize, sizeProperty, snap, snippedPos, startPosition, totalLength, startIndex, typicalItemSize, } = metrics, displayItems = [];
1549
1621
  if (items.length) {
1550
1622
  const actualSnippedPosition = snippedPos, isSnappingMethodAdvanced = this.isSnappingMethodAdvanced, boundsSize = isVertical ? height : width, actualEndSnippedPosition = boundsSize;
1551
- let pos = startPosition, renderItems = renderItemsLength, stickyItem, nextSticky, stickyItemIndex = -1, stickyItemSize = 0, endStickyItem, nextEndSticky, endStickyItemIndex = -1, endStickyItemSize = 0;
1623
+ let pos = startPosition, renderItems = renderItemsLength, stickyItem, nextSticky, stickyItemIndex = -1, stickyItemSize = 0, endStickyItem, nextEndSticky, endStickyItemIndex = -1, endStickyItemSize = 0, count = 1;
1552
1624
  if (snap) {
1553
1625
  for (let i = Math.min(itemsFromStartToScrollEnd > 0 ? itemsFromStartToScrollEnd : 0, totalLength - 1); i >= 0; i--) {
1554
1626
  if (!items[i]) {
@@ -1573,6 +1645,7 @@ class TrackBox extends CacheMap {
1573
1645
  snappedOut: false,
1574
1646
  dynamic: dynamicSize,
1575
1647
  isSnappingMethodAdvanced,
1648
+ tabIndex: count,
1576
1649
  zIndex: '1',
1577
1650
  };
1578
1651
  const itemData = items[i];
@@ -1580,6 +1653,7 @@ class TrackBox extends CacheMap {
1580
1653
  stickyItemIndex = i;
1581
1654
  stickyItemSize = size;
1582
1655
  displayItems.push(stickyItem);
1656
+ count++;
1583
1657
  break;
1584
1658
  }
1585
1659
  }
@@ -1611,6 +1685,7 @@ class TrackBox extends CacheMap {
1611
1685
  snappedOut: false,
1612
1686
  dynamic: dynamicSize,
1613
1687
  isSnappingMethodAdvanced,
1688
+ tabIndex: count,
1614
1689
  zIndex: '1',
1615
1690
  };
1616
1691
  const itemData = items[i];
@@ -1618,6 +1693,7 @@ class TrackBox extends CacheMap {
1618
1693
  endStickyItemIndex = i;
1619
1694
  endStickyItemSize = size;
1620
1695
  displayItems.push(endStickyItem);
1696
+ count++;
1621
1697
  break;
1622
1698
  }
1623
1699
  }
@@ -1649,6 +1725,7 @@ class TrackBox extends CacheMap {
1649
1725
  snappedOut: false,
1650
1726
  dynamic: dynamicSize,
1651
1727
  isSnappingMethodAdvanced,
1728
+ tabIndex: count,
1652
1729
  zIndex: '0',
1653
1730
  };
1654
1731
  if (snapped) {
@@ -1673,6 +1750,7 @@ class TrackBox extends CacheMap {
1673
1750
  nextEndSticky.measures.delta = isVertical ? (item.measures.y - scrollSize) : (item.measures.x - scrollSize);
1674
1751
  }
1675
1752
  displayItems.push(item);
1753
+ count++;
1676
1754
  }
1677
1755
  renderItems -= 1;
1678
1756
  pos += size;
@@ -2054,6 +2132,9 @@ class NgVirtualListComponent {
2054
2132
  this._initialized = signal(false);
2055
2133
  this.$initialized = toObservable(this._initialized);
2056
2134
  this._trackBox.displayComponents = this._displayComponents;
2135
+ toObservable(this._list).pipe(takeUntilDestroyed(), filter(v => !!v), tap(v => {
2136
+ this._service.listElement = v.nativeElement;
2137
+ })).subscribe();
2057
2138
  const $trackBy = toObservable(this.trackBy), $selectByClick = toObservable(this.selectByClick);
2058
2139
  $selectByClick.pipe(takeUntilDestroyed(), tap(v => {
2059
2140
  this._service.selectByClick = v;