@@ -13,9 +13,17 @@ import {
1313 Type,
1414 ViewChild,
1515 inject,
16+ signal,
1617} from '@angular/core';
1718import {By} from '@angular/platform-browser';
18- import {ComponentFixture, TestBed, fakeAsync, flush, waitForAsync} from '@angular/core/testing';
19+ import {
20+ ComponentFixture,
21+ TestBed,
22+ fakeAsync,
23+ flush,
24+ tick,
25+ waitForAsync,
26+ } from '@angular/core/testing';
1927import {BehaviorSubject, Observable, combineLatest, of as observableOf} from 'rxjs';
2028import {map} from 'rxjs/operators';
2129import {CdkColumnDef} from './cell';
@@ -36,6 +44,8 @@ import {
3644 getTableUnknownDataSourceError,
3745} from './table-errors';
3846import {NgClass} from '@angular/common';
47+ import {CdkVirtualScrollViewport, ScrollingModule} from '../scrolling';
48+ import {dispatchFakeEvent} from '../testing/private';
3949
4050describe('CdkTable', () => {
4151 let fixture: ComponentFixture<any>;
@@ -1995,6 +2005,107 @@ describe('CdkTable', () => {
19952005 expect(noDataRow).toBeTruthy();
19962006 expect(noDataRow.getAttribute('colspan')).toEqual('3');
19972007 });
2008+
2009+ describe('virtual scrolling', () => {
2010+ let fixture: ComponentFixture<TableWithVirtualScroll>;
2011+ let table: HTMLTableElement;
2012+
2013+ beforeEach(fakeAsync(() => {
2014+ fixture = TestBed.createComponent(TableWithVirtualScroll);
2015+
2016+ // Init logic copied from the virtual scroll tests.
2017+ fixture.detectChanges();
2018+ flush();
2019+ fixture.detectChanges();
2020+ flush();
2021+ tick(16);
2022+ flush();
2023+ fixture.detectChanges();
2024+ table = fixture.nativeElement.querySelector('table');
2025+ }));
2026+
2027+ function triggerScroll(offset: number) {
2028+ const viewport = fixture.componentInstance.viewport;
2029+ viewport.scrollToOffset(offset);
2030+ dispatchFakeEvent(viewport.scrollable!.getElementRef().nativeElement, 'scroll');
2031+ tick(16);
2032+ }
2033+
2034+ it('should not render the full data set when using virtual scrolling', fakeAsync(() => {
2035+ expect(fixture.componentInstance.dataSource.data.length).toBeGreaterThan(2000);
2036+ expect(getRows(table).length).toBe(10);
2037+ }));
2038+
2039+ it('should maintain a limited amount of data as the user is scrolling', fakeAsync(() => {
2040+ expect(getRows(table).length).toBe(10);
2041+
2042+ triggerScroll(500);
2043+ expect(getRows(table).length).toBe(13);
2044+
2045+ triggerScroll(500);
2046+ expect(getRows(table).length).toBe(13);
2047+
2048+ triggerScroll(1000);
2049+ expect(getRows(table).length).toBe(12);
2050+ }));
2051+
2052+ it('should update the table data as the user is scrolling', fakeAsync(() => {
2053+ expectTableToMatchContent(table, [
2054+ ['Column A', 'Column B', 'Column C'],
2055+ ['a_1', 'b_1', 'c_1'],
2056+ ['a_2', 'b_2', 'c_2'],
2057+ ['a_3', 'b_3', 'c_3'],
2058+ ['a_4', 'b_4', 'c_4'],
2059+ ['a_5', 'b_5', 'c_5'],
2060+ ['a_6', 'b_6', 'c_6'],
2061+ ['a_7', 'b_7', 'c_7'],
2062+ ['a_8', 'b_8', 'c_8'],
2063+ ['a_9', 'b_9', 'c_9'],
2064+ ['a_10', 'b_10', 'c_10'],
2065+ ['Footer A', 'Footer B', 'Footer C'],
2066+ ]);
2067+
2068+ triggerScroll(1000);
2069+
2070+ expectTableToMatchContent(table, [
2071+ ['Column A', 'Column B', 'Column C'],
2072+ ['a_18', 'b_18', 'c_18'],
2073+ ['a_19', 'b_19', 'c_19'],
2074+ ['a_20', 'b_20', 'c_20'],
2075+ ['a_21', 'b_21', 'c_21'],
2076+ ['a_22', 'b_22', 'c_22'],
2077+ ['a_23', 'b_23', 'c_23'],
2078+ ['a_24', 'b_24', 'c_24'],
2079+ ['a_25', 'b_25', 'c_25'],
2080+ ['a_26', 'b_26', 'c_26'],
2081+ ['a_27', 'b_27', 'c_27'],
2082+ ['a_28', 'b_28', 'c_28'],
2083+ ['a_29', 'b_29', 'c_29'],
2084+ ['Footer A', 'Footer B', 'Footer C'],
2085+ ]);
2086+ }));
2087+
2088+ it('should update the position of sticky cells as the user is scrolling', fakeAsync(() => {
2089+ const assertStickyOffsets = (position: number) => {
2090+ getHeaderCells(table).forEach(cell => expect(cell.style.top).toBe(`${position * -1}px`));
2091+ getFooterCells(table).forEach(cell => expect(cell.style.bottom).toBe(`${position}px`));
2092+ };
2093+
2094+ assertStickyOffsets(0);
2095+ triggerScroll(1000);
2096+ assertStickyOffsets(884);
2097+ }));
2098+
2099+ it('should force tables with virtual scrolling to have a fixed layout', fakeAsync(() => {
2100+ expect(fixture.componentInstance.isFixedLayout()).toBe(true);
2101+ expect(table.classList).toContain('cdk-table-fixed-layout');
2102+
2103+ fixture.componentInstance.isFixedLayout.set(false);
2104+ fixture.detectChanges();
2105+
2106+ expect(table.classList).toContain('cdk-table-fixed-layout');
2107+ }));
2108+ });
19982109});
19992110
20002111interface TestData {
@@ -2032,15 +2143,18 @@ class FakeDataSource extends DataSource<TestData> {
20322143 this.isConnected = false;
20332144 }
20342145
2035- addData() {
2036- const nextIndex = this.data.length + 1;
2037-
2146+ addData(amount = 1) {
20382147 let copiedData = this.data.slice();
2039- copiedData.push({
2040- a: `a_${nextIndex}`,
2041- b: `b_${nextIndex}`,
2042- c: `c_${nextIndex}`,
2043- });
2148+
2149+ for (let i = 0; i < amount; i++) {
2150+ const nextIndex = copiedData.length + 1;
2151+
2152+ copiedData.push({
2153+ a: `a_${nextIndex}`,
2154+ b: `b_${nextIndex}`,
2155+ c: `c_${nextIndex}`,
2156+ });
2157+ }
20442158
20452159 this.data = copiedData;
20462160 }
@@ -3176,6 +3290,54 @@ class WrapNativeHtmlTableAppOnPush {
31763290 dataSource = new FakeDataSource();
31773291}
31783292
3293+ @Component({
3294+ template: `
3295+ <cdk-virtual-scroll-viewport class="scroll-container" [itemSize]="52">
3296+ <table cdk-table [dataSource]="dataSource" [fixedLayout]="isFixedLayout()">
3297+ <ng-container cdkColumnDef="column_a">
3298+ <th cdk-header-cell *cdkHeaderCellDef>Column A</th>
3299+ <td cdk-cell *cdkCellDef="let row"> {{row.a}}</td>
3300+ <td cdk-footer-cell *cdkFooterCellDef>Footer A</td>
3301+ </ng-container>
3302+
3303+ <ng-container cdkColumnDef="column_b">
3304+ <th cdk-header-cell *cdkHeaderCellDef>Column B</th>
3305+ <td cdk-cell *cdkCellDef="let row"> {{row.b}}</td>
3306+ <td cdk-footer-cell *cdkFooterCellDef>Footer B</td>
3307+ </ng-container>
3308+
3309+ <ng-container cdkColumnDef="column_c">
3310+ <th cdk-header-cell *cdkHeaderCellDef>Column C</th>
3311+ <td cdk-cell *cdkCellDef="let row"> {{row.c}}</td>
3312+ <td cdk-footer-cell *cdkFooterCellDef>Footer C</td>
3313+ </ng-container>
3314+
3315+ <tr cdk-header-row *cdkHeaderRowDef="columnsToRender; sticky: true"></tr>
3316+ <tr cdk-row *cdkRowDef="let row; columns: columnsToRender"></tr>
3317+ <tr cdk-footer-row *cdkFooterRowDef="columnsToRender; sticky: true"></tr>
3318+ </table>
3319+ </cdk-virtual-scroll-viewport>
3320+ `,
3321+ imports: [CdkTableModule, ScrollingModule],
3322+ styles: `
3323+ .scroll-container {
3324+ height: 300px;
3325+ overflow: auto;
3326+ }
3327+ `,
3328+ })
3329+ class TableWithVirtualScroll {
3330+ @ViewChild(CdkTable) table: CdkTable<TestData>;
3331+ @ViewChild(CdkVirtualScrollViewport) viewport: CdkVirtualScrollViewport;
3332+ dataSource = new FakeDataSource();
3333+ columnsToRender = ['column_a', 'column_b', 'column_c'];
3334+ isFixedLayout = signal(true);
3335+
3336+ constructor() {
3337+ this.dataSource.addData(2000);
3338+ }
3339+ }
3340+
31793341function getElements(element: Element, query: string): HTMLElement[] {
31803342 return [].slice.call(element.querySelectorAll(query));
31813343}
0 commit comments