@@ -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