ng-virtual-list 20.7.2 → 20.7.3

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
@@ -44,6 +44,10 @@ As each item may contain images, nested components, or interactions, virtual ren
44
44
 
45
45
  Single and multiple selection of elements
46
46
 
47
+ Navigating with the keyboard
48
+
49
+ Support for element animation
50
+
47
51
  <br/>
48
52
 
49
53
  ## Installation
@@ -575,12 +579,12 @@ Methods
575
579
 
576
580
  | Angular version | ng-virtual-list version | git | npm |
577
581
  |--|--|--|--|
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) |
582
+ | 19.x | 19.7.4 | [19.x](https://github.com/DjonnyX/ng-virtual-list/tree/19.x) | [19.7.4](https://www.npmjs.com/package/ng-virtual-list/v/19.7.4) |
583
+ | 18.x | 18.7.3 | [18.x](https://github.com/DjonnyX/ng-virtual-list/tree/18.x) | [18.7.3](https://www.npmjs.com/package/ng-virtual-list/v/18.7.3) |
584
+ | 17.x | 17.7.3 | [17.x](https://github.com/DjonnyX/ng-virtual-list/tree/17.x) | [17.7.3](https://www.npmjs.com/package/ng-virtual-list/v/17.7.3) |
585
+ | 16.x | 16.7.3 | [16.x](https://github.com/DjonnyX/ng-virtual-list/tree/16.x) | [16.7.3](https://www.npmjs.com/package/ng-virtual-list/v/16.7.3) |
586
+ | 15.x | 15.7.3 | [15.x](https://github.com/DjonnyX/ng-virtual-list/tree/15.x) | [15.7.3](https://www.npmjs.com/package/ng-virtual-list/v/15.7.3) |
587
+ | 14.x | 14.7.3 | [14.x](https://github.com/DjonnyX/ng-virtual-list/tree/14.x) | [14.7.3](https://www.npmjs.com/package/ng-virtual-list/v/14.7.3) |
584
588
 
585
589
  <br/>
586
590
 
@@ -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;