ng-virtual-list 18.7.2 → 18.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.
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { Injectable, inject, ChangeDetectorRef, signal, ElementRef, Component, ChangeDetectionStrategy, viewChild, output, input, ViewContainerRef, ViewEncapsulation, ViewChild, NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
3
- import { Subject, tap, combineLatest, map, filter, distinctUntilChanged, switchMap, of } from 'rxjs';
3
+ import { Subject, tap, fromEvent, combineLatest, map, filter, distinctUntilChanged, switchMap, of } from 'rxjs';
4
4
  import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
5
5
  import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
6
6
  import * as i1 from '@angular/common';
@@ -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: "18.2.13", ngImpo
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/18.x/projects/ng-virtual-list/src/lib/components/ng-virtual-list-item.component.ts
@@ -282,6 +284,7 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
282
284
  _isSelected = false;
283
285
  config = signal({});
284
286
  measures = signal(undefined);
287
+ focus = signal(false);
285
288
  _part = PART_DEFAULT_ITEM;
286
289
  get part() { return this._part; }
287
290
  regular = false;
@@ -334,23 +337,71 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
334
337
  super();
335
338
  this._id = this._service.generateComponentId();
336
339
  const $data = toObservable(this.data);
340
+ fromEvent(this.element, 'focusin').pipe(takeUntilDestroyed(), tap(e => {
341
+ this.focus.set(true);
342
+ this.updateConfig(this._data);
343
+ this.updatePartStr(this._data, this._isSelected);
344
+ })).subscribe(),
345
+ fromEvent(this.element, 'focusout').pipe(takeUntilDestroyed(), tap(e => {
346
+ this.focus.set(false);
347
+ this.updateConfig(this._data);
348
+ this.updatePartStr(this._data, this._isSelected);
349
+ })).subscribe(),
350
+ fromEvent(this.element, 'keydown').pipe(takeUntilDestroyed(), tap(e => {
351
+ switch (e.key) {
352
+ case KEY_SPACE: {
353
+ e.stopImmediatePropagation();
354
+ e.preventDefault();
355
+ this._service.select(this._data);
356
+ break;
357
+ }
358
+ case KEY_ARR_LEFT:
359
+ if (!this.config().isVertical) {
360
+ e.stopImmediatePropagation();
361
+ e.preventDefault();
362
+ this.focusPrev();
363
+ }
364
+ break;
365
+ case KEY_ARR_UP:
366
+ if (this.config().isVertical) {
367
+ e.stopImmediatePropagation();
368
+ e.preventDefault();
369
+ this.focusPrev();
370
+ }
371
+ break;
372
+ case KEY_ARR_RIGHT:
373
+ if (!this.config().isVertical) {
374
+ e.stopImmediatePropagation();
375
+ e.preventDefault();
376
+ this.focusNext();
377
+ }
378
+ break;
379
+ case KEY_ARR_DOWN:
380
+ if (this.config().isVertical) {
381
+ e.stopImmediatePropagation();
382
+ e.preventDefault();
383
+ this.focusNext();
384
+ }
385
+ break;
386
+ }
387
+ })).subscribe();
337
388
  combineLatest([$data, this._service.$methodOfSelecting, this._service.$selectedIds]).pipe(takeUntilDestroyed(), map(([, m, ids]) => ({ method: m, ids })), tap(({ method, ids }) => {
338
389
  switch (method) {
339
390
  case MethodsForSelectingTypes.SELECT: {
340
391
  const id = ids, isSelected = id === this.itemId;
341
- this._elementRef.nativeElement.setAttribute(ATTR_AREA_SELECTED, String(isSelected));
392
+ this.element.setAttribute(ATTR_AREA_SELECTED, String(isSelected));
342
393
  this._isSelected = isSelected;
343
394
  break;
344
395
  }
345
396
  case MethodsForSelectingTypes.MULTI_SELECT: {
346
397
  const actualIds = ids, isSelected = this.itemId !== undefined && actualIds && actualIds.includes(this.itemId);
347
- this._elementRef.nativeElement.setAttribute(ATTR_AREA_SELECTED, String(isSelected));
398
+ this.element.setAttribute(ATTR_AREA_SELECTED, String(isSelected));
348
399
  this._isSelected = isSelected;
349
400
  break;
350
401
  }
351
402
  case MethodsForSelectingTypes.NONE:
352
403
  default: {
353
- this._elementRef.nativeElement.removeAttribute(ATTR_AREA_SELECTED);
404
+ this.element.removeAttribute(ATTR_AREA_SELECTED);
354
405
  this._isSelected = false;
355
406
  break;
356
407
  }
@@ -360,11 +411,29 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
360
411
  this.updateMeasures(this._data);
361
412
  })).subscribe();
362
413
  }
414
+ focusNext() {
415
+ const tabIndex = this.config()?.tabIndex ?? 0;
416
+ if (this._service.listElement && tabIndex > 0) {
417
+ const el = this._service.listElement.querySelector(`[${TABINDEX}="${tabIndex + 1}"]`);
418
+ if (el) {
419
+ el.focus();
420
+ }
421
+ }
422
+ }
423
+ focusPrev() {
424
+ const tabIndex = this.config()?.tabIndex ?? 0;
425
+ if (this._service.listElement && tabIndex > 1) {
426
+ const el = this._service.listElement.querySelector(`[${TABINDEX}="${tabIndex - 1}"]`);
427
+ if (el) {
428
+ el.focus();
429
+ }
430
+ }
431
+ }
363
432
  updateMeasures(v) {
364
433
  this.measures.set(v?.measures ? { ...v.measures } : undefined);
365
434
  }
366
435
  updateConfig(v) {
367
- this.config.set({ ...v?.config || {}, selected: this._isSelected, select: this._selectHandler(v) });
436
+ this.config.set({ ...v?.config || {}, selected: this._isSelected, select: this._selectHandler(v), focus: this.focus() });
368
437
  }
369
438
  update() {
370
439
  const data = this._data, regular = this.regular, length = this._regularLength;
@@ -405,6 +474,9 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
405
474
  if (isSelected) {
406
475
  part += PART_ITEM_SELECTED;
407
476
  }
477
+ if (this.focus()) {
478
+ part += PART_ITEM_FOCUSED;
479
+ }
408
480
  this._part = part;
409
481
  }
410
482
  getBounds() {
@@ -449,14 +521,14 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
449
521
  this._service.itemClick(this._data);
450
522
  }
451
523
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgVirtualListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
452
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: NgVirtualListItemComponent, selector: "ng-virtual-list-item", host: { attributes: { "role": "listitem" }, classAttribute: "ngvl__item" }, usesInheritance: true, ngImport: i0, template: "@if (data(); as item) {\r\n <div #listItem [part]=\"part\" class=\"ngvl-item__container\" [ngClass]=\"{'snapped': item.config.snapped,\r\n 'snapped-out': item.config.snappedOut}\" (click)=\"onClickHandler()\">\r\n @if (itemRenderer(); as renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\"\r\n [ngTemplateOutletContext]=\"{data: item.data || {}, measures: measures(), config: config()}\" />\r\n }\r\n </div>\r\n}\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: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
524
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: NgVirtualListItemComponent, selector: "ng-virtual-list-item", host: { attributes: { "role": "listitem" }, classAttribute: "ngvl__item" }, usesInheritance: true, ngImport: i0, template: "@if (data(); as item) {\r\n <div #listItem [part]=\"part\" class=\"ngvl-item__container\" [attr.index]=\"item.config.tabIndex\" tabindex=\"1\" [ngClass]=\"{'snapped': item.config.snapped,\r\n 'snapped-out': item.config.snappedOut, 'focus': focus()}\" (click)=\"onClickHandler()\">\r\n @if (itemRenderer(); as renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\"\r\n [ngTemplateOutletContext]=\"{data: item.data || {}, measures: measures(), config: config()}\" />\r\n }\r\n </div>\r\n}\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: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
453
525
  }
454
526
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: NgVirtualListItemComponent, decorators: [{
455
527
  type: Component,
456
528
  args: [{ selector: 'ng-virtual-list-item', standalone: false, host: {
457
529
  'class': 'ngvl__item',
458
530
  'role': 'listitem',
459
- }, changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (data(); as item) {\r\n <div #listItem [part]=\"part\" class=\"ngvl-item__container\" [ngClass]=\"{'snapped': item.config.snapped,\r\n 'snapped-out': item.config.snappedOut}\" (click)=\"onClickHandler()\">\r\n @if (itemRenderer(); as renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\"\r\n [ngTemplateOutletContext]=\"{data: item.data || {}, measures: measures(), config: config()}\" />\r\n }\r\n </div>\r\n}\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"] }]
531
+ }, changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (data(); as item) {\r\n <div #listItem [part]=\"part\" class=\"ngvl-item__container\" [attr.index]=\"item.config.tabIndex\" tabindex=\"1\" [ngClass]=\"{'snapped': item.config.snapped,\r\n 'snapped-out': item.config.snappedOut, 'focus': focus()}\" (click)=\"onClickHandler()\">\r\n @if (itemRenderer(); as renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\"\r\n [ngTemplateOutletContext]=\"{data: item.data || {}, measures: measures(), config: config()}\" />\r\n }\r\n </div>\r\n}\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"] }]
460
532
  }], ctorParameters: () => [] });
461
533
 
462
534
  /**
@@ -1552,7 +1624,7 @@ class TrackBox extends CacheMap {
1552
1624
  const { width, height, normalizedItemWidth, normalizedItemHeight, dynamicSize, itemsOnDisplayLength, itemsFromStartToScrollEnd, isVertical, renderItems: renderItemsLength, scrollSize, sizeProperty, snap, snippedPos, startPosition, totalLength, startIndex, typicalItemSize, } = metrics, displayItems = [];
1553
1625
  if (items.length) {
1554
1626
  const actualSnippedPosition = snippedPos, isSnappingMethodAdvanced = this.isSnappingMethodAdvanced, boundsSize = isVertical ? height : width, actualEndSnippedPosition = boundsSize;
1555
- let pos = startPosition, renderItems = renderItemsLength, stickyItem, nextSticky, stickyItemIndex = -1, stickyItemSize = 0, endStickyItem, nextEndSticky, endStickyItemIndex = -1, endStickyItemSize = 0;
1627
+ let pos = startPosition, renderItems = renderItemsLength, stickyItem, nextSticky, stickyItemIndex = -1, stickyItemSize = 0, endStickyItem, nextEndSticky, endStickyItemIndex = -1, endStickyItemSize = 0, count = 1;
1556
1628
  if (snap) {
1557
1629
  for (let i = Math.min(itemsFromStartToScrollEnd > 0 ? itemsFromStartToScrollEnd : 0, totalLength - 1); i >= 0; i--) {
1558
1630
  if (!items[i]) {
@@ -1577,6 +1649,7 @@ class TrackBox extends CacheMap {
1577
1649
  snappedOut: false,
1578
1650
  dynamic: dynamicSize,
1579
1651
  isSnappingMethodAdvanced,
1652
+ tabIndex: count,
1580
1653
  zIndex: '1',
1581
1654
  };
1582
1655
  const itemData = items[i];
@@ -1584,6 +1657,7 @@ class TrackBox extends CacheMap {
1584
1657
  stickyItemIndex = i;
1585
1658
  stickyItemSize = size;
1586
1659
  displayItems.push(stickyItem);
1660
+ count++;
1587
1661
  break;
1588
1662
  }
1589
1663
  }
@@ -1615,6 +1689,7 @@ class TrackBox extends CacheMap {
1615
1689
  snappedOut: false,
1616
1690
  dynamic: dynamicSize,
1617
1691
  isSnappingMethodAdvanced,
1692
+ tabIndex: count,
1618
1693
  zIndex: '1',
1619
1694
  };
1620
1695
  const itemData = items[i];
@@ -1622,6 +1697,7 @@ class TrackBox extends CacheMap {
1622
1697
  endStickyItemIndex = i;
1623
1698
  endStickyItemSize = size;
1624
1699
  displayItems.push(endStickyItem);
1700
+ count++;
1625
1701
  break;
1626
1702
  }
1627
1703
  }
@@ -1653,6 +1729,7 @@ class TrackBox extends CacheMap {
1653
1729
  snappedOut: false,
1654
1730
  dynamic: dynamicSize,
1655
1731
  isSnappingMethodAdvanced,
1732
+ tabIndex: count,
1656
1733
  zIndex: '0',
1657
1734
  };
1658
1735
  if (snapped) {
@@ -1677,6 +1754,7 @@ class TrackBox extends CacheMap {
1677
1754
  nextEndSticky.measures.delta = isVertical ? (item.measures.y - scrollSize) : (item.measures.x - scrollSize);
1678
1755
  }
1679
1756
  displayItems.push(item);
1757
+ count++;
1680
1758
  }
1681
1759
  renderItems -= 1;
1682
1760
  pos += size;
@@ -2058,6 +2136,9 @@ class NgVirtualListComponent {
2058
2136
  this._initialized = signal(false);
2059
2137
  this.$initialized = toObservable(this._initialized);
2060
2138
  this._trackBox.displayComponents = this._displayComponents;
2139
+ toObservable(this._list).pipe(takeUntilDestroyed(), filter(v => !!v), tap(v => {
2140
+ this._service.listElement = v.nativeElement;
2141
+ })).subscribe();
2061
2142
  const $trackBy = toObservable(this.trackBy), $selectByClick = toObservable(this.selectByClick);
2062
2143
  $selectByClick.pipe(takeUntilDestroyed(), tap(v => {
2063
2144
  this._service.selectByClick = v;