diff --git a/src/cdk/table/coalesced-style-scheduler.ts b/src/cdk/table/coalesced-style-scheduler.ts new file mode 100644 index 000000000000..5e8f985009ac --- /dev/null +++ b/src/cdk/table/coalesced-style-scheduler.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Injectable, NgZone, OnDestroy} from '@angular/core'; +import {Subject} from 'rxjs'; +import {take, takeUntil} from 'rxjs/operators'; + +/** + * @docs-private + */ +export class _Schedule { + tasks: (() => unknown)[] = []; + endTasks: (() => unknown)[] = []; +} + +/** + * Allows grouping up CSSDom mutations after the current execution context. + * This can significantly improve performance when separate consecutive functions are + * reading from the CSSDom and then mutating it. + * + * @docs-private + */ +@Injectable() +export class _CoalescedStyleScheduler implements OnDestroy { + private _currentSchedule: _Schedule|null = null; + private readonly _destroyed = new Subject(); + + constructor(private readonly _ngZone: NgZone) {} + + /** + * Schedules the specified task to run at the end of the current VM turn. + */ + schedule(task: () => unknown): void { + this._createScheduleIfNeeded(); + + this._currentSchedule!.tasks.push(task); + } + + /** + * Schedules the specified task to run after other scheduled tasks at the end of the current + * VM turn. + */ + scheduleEnd(task: () => unknown): void { + this._createScheduleIfNeeded(); + + this._currentSchedule!.endTasks.push(task); + } + + /** Prevent any further tasks from running. */ + ngOnDestroy() { + this._destroyed.next(); + this._destroyed.complete(); + } + + private _createScheduleIfNeeded() { + if (this._currentSchedule) { return; } + + this._currentSchedule = new _Schedule(); + + this._ngZone.onStable.pipe( + take(1), + takeUntil(this._destroyed), + ).subscribe(() => { + const schedule = this._currentSchedule!; + this._currentSchedule = null; + + for (const task of schedule.tasks) { + task(); + } + for (const task of schedule.endTasks) { + task(); + } + }); + } +} diff --git a/src/cdk/table/public-api.ts b/src/cdk/table/public-api.ts index 4095bf4082f5..ff3116bda891 100644 --- a/src/cdk/table/public-api.ts +++ b/src/cdk/table/public-api.ts @@ -8,6 +8,7 @@ export * from './table'; export * from './cell'; +export * from './coalesced-style-scheduler'; export * from './row'; export * from './table-module'; export * from './sticky-styler'; diff --git a/src/cdk/table/sticky-styler.ts b/src/cdk/table/sticky-styler.ts index 73ffced0c888..afa44787e417 100644 --- a/src/cdk/table/sticky-styler.ts +++ b/src/cdk/table/sticky-styler.ts @@ -11,6 +11,7 @@ * @docs-private */ import {Direction} from '@angular/cdk/bidi'; +import {_CoalescedStyleScheduler} from './coalesced-style-scheduler'; export type StickyDirection = 'top' | 'bottom' | 'left' | 'right'; @@ -37,6 +38,7 @@ export class StickyStyler { constructor(private _isNativeHtmlTable: boolean, private _stickCellCss: string, public direction: Direction, + private _coalescedStyleScheduler: _CoalescedStyleScheduler, private _isBrowser = true) { } /** @@ -46,6 +48,7 @@ export class StickyStyler { * @param stickyDirections The directions that should no longer be set as sticky on the rows. */ clearStickyPositioning(rows: HTMLElement[], stickyDirections: StickyDirection[]) { + const elementsToClear: HTMLElement[] = []; for (const row of rows) { // If the row isn't an element (e.g. if it's an `ng-container`), // it won't have inline styles or `children` so we skip it. @@ -53,13 +56,18 @@ export class StickyStyler { continue; } - this._removeStickyStyle(row, stickyDirections); - + elementsToClear.push(row); for (let i = 0; i < row.children.length; i++) { - const cell = row.children[i] as HTMLElement; - this._removeStickyStyle(cell, stickyDirections); + elementsToClear.push(row.children[i] as HTMLElement); } } + + // Coalesce with sticky row/column updates (and potentially other changes like column resize). + this._coalescedStyleScheduler.schedule(() => { + for (const element of elementsToClear) { + this._removeStickyStyle(element, stickyDirections); + } + }); } /** @@ -73,9 +81,8 @@ export class StickyStyler { */ updateStickyColumns( rows: HTMLElement[], stickyStartStates: boolean[], stickyEndStates: boolean[]) { - const hasStickyColumns = - stickyStartStates.some(state => state) || stickyEndStates.some(state => state); - if (!rows.length || !hasStickyColumns || !this._isBrowser) { + if (!rows.length || !this._isBrowser || !(stickyStartStates.some(state => state) || + stickyEndStates.some(state => state))) { return; } @@ -85,20 +92,26 @@ export class StickyStyler { const startPositions = this._getStickyStartColumnPositions(cellWidths, stickyStartStates); const endPositions = this._getStickyEndColumnPositions(cellWidths, stickyEndStates); - const isRtl = this.direction === 'rtl'; - - for (const row of rows) { - for (let i = 0; i < numCells; i++) { - const cell = row.children[i] as HTMLElement; - if (stickyStartStates[i]) { - this._addStickyStyle(cell, isRtl ? 'right' : 'left', startPositions[i]); - } - if (stickyEndStates[i]) { - this._addStickyStyle(cell, isRtl ? 'left' : 'right', endPositions[i]); + // Coalesce with sticky row updates (and potentially other changes like column resize). + this._coalescedStyleScheduler.schedule(() => { + const isRtl = this.direction === 'rtl'; + const start = isRtl ? 'right' : 'left'; + const end = isRtl ? 'left' : 'right'; + + for (const row of rows) { + for (let i = 0; i < numCells; i++) { + const cell = row.children[i] as HTMLElement; + if (stickyStartStates[i]) { + this._addStickyStyle(cell, start, startPositions[i]); + } + + if (stickyEndStates[i]) { + this._addStickyStyle(cell, end, endPositions[i]); + } } } - } + }); } /** @@ -124,30 +137,39 @@ export class StickyStyler { const rows = position === 'bottom' ? rowsToStick.slice().reverse() : rowsToStick; const states = position === 'bottom' ? stickyStates.slice().reverse() : stickyStates; - let stickyHeight = 0; - for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { + // Measure row heights all at once before adding sticky styles to reduce layout thrashing. + const stickyHeights: number[] = []; + const elementsToStick: HTMLElement[][] = []; + for (let rowIndex = 0, stickyHeight = 0; rowIndex < rows.length; rowIndex++) { + stickyHeights[rowIndex] = stickyHeight; + if (!states[rowIndex]) { continue; } const row = rows[rowIndex]; - if (this._isNativeHtmlTable) { - for (let j = 0; j < row.children.length; j++) { - const cell = row.children[j] as HTMLElement; - this._addStickyStyle(cell, position, stickyHeight); - } - } else { - // Flex does not respect the stick positioning on the cells, needs to be applied to the row. - // If this is applied on a native table, Safari causes the header to fly in wrong direction. - this._addStickyStyle(row, position, stickyHeight); - } + elementsToStick[rowIndex] = this._isNativeHtmlTable ? + Array.from(row.children) as HTMLElement[] : [row]; - if (rowIndex === rows.length - 1) { - // prevent unnecessary reflow from getBoundingClientRect() - return; + if (rowIndex !== rows.length - 1) { + stickyHeight += row.getBoundingClientRect().height; } - stickyHeight += row.getBoundingClientRect().height; } + + // Coalesce with other sticky row updates (top/bottom), sticky columns updates + // (and potentially other changes like column resize). + this._coalescedStyleScheduler.schedule(() => { + for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { + if (!states[rowIndex]) { + continue; + } + + const height = stickyHeights[rowIndex]; + for (const element of elementsToStick[rowIndex]) { + this._addStickyStyle(element, position, height); + } + } + }); } /** @@ -162,11 +184,15 @@ export class StickyStyler { } const tfoot = tableElement.querySelector('tfoot')!; - if (stickyStates.some(state => !state)) { - this._removeStickyStyle(tfoot, ['bottom']); - } else { - this._addStickyStyle(tfoot, 'bottom', 0); - } + + // Coalesce with other sticky updates (and potentially other changes like column resize). + this._coalescedStyleScheduler.schedule(() => { + if (stickyStates.some(state => !state)) { + this._removeStickyStyle(tfoot, ['bottom']); + } else { + this._addStickyStyle(tfoot, 'bottom', 0); + } + }); } /** diff --git a/src/cdk/table/table.spec.ts b/src/cdk/table/table.spec.ts index bc5a447ad723..51f331260fa5 100644 --- a/src/cdk/table/table.spec.ts +++ b/src/cdk/table/table.spec.ts @@ -10,7 +10,7 @@ import { ViewChild, AfterViewInit } from '@angular/core'; -import {ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing'; +import {ComponentFixture, fakeAsync, flush, flushMicrotasks, TestBed} from '@angular/core/testing'; import {BehaviorSubject, combineLatest, Observable, of as observableOf} from 'rxjs'; import {map} from 'rxjs/operators'; import {CdkColumnDef} from './cell'; @@ -870,9 +870,10 @@ describe('CdkTable', () => { dataRows = getRows(tableElement); }); - it('should stick and unstick headers', () => { + it('should stick and unstick headers', fakeAsync(() => { component.stickyHeaders = ['header-1', 'header-3']; fixture.detectChanges(); + flushMicrotasks(); expectStickyStyles(headerRows[0], '100', {top: '0px'}); expectNoStickyStyles([headerRows[1]]); @@ -881,12 +882,14 @@ describe('CdkTable', () => { component.stickyHeaders = []; fixture.detectChanges(); + flushMicrotasks(); expectNoStickyStyles(headerRows); - }); + })); - it('should stick and unstick footers', () => { + it('should stick and unstick footers', fakeAsync(() => { component.stickyFooters = ['footer-1', 'footer-3']; fixture.detectChanges(); + flushMicrotasks(); expectStickyStyles( footerRows[0], '10', {bottom: footerRows[1].getBoundingClientRect().height + 'px'}); @@ -895,20 +898,23 @@ describe('CdkTable', () => { component.stickyFooters = []; fixture.detectChanges(); + flushMicrotasks(); expectNoStickyStyles(footerRows); - }); + })); - it('should stick the correct footer row', () => { + it('should stick the correct footer row', fakeAsync(() => { component.stickyFooters = ['footer-3']; fixture.detectChanges(); + flushMicrotasks(); expectStickyStyles(footerRows[2], '10', {bottom: '0px'}); expectNoStickyStyles([footerRows[0], footerRows[1]]); - }); + })); - it('should stick and unstick left columns', () => { + it('should stick and unstick left columns', fakeAsync(() => { component.stickyStartColumns = ['column-1', 'column-3']; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => { let cells = getHeaderCells(row); @@ -931,14 +937,16 @@ describe('CdkTable', () => { component.stickyStartColumns = []; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row))); dataRows.forEach(row => expectNoStickyStyles(getCells(row))); footerRows.forEach(row => expectNoStickyStyles(getFooterCells(row))); - }); + })); - it('should stick and unstick right columns', () => { + it('should stick and unstick right columns', fakeAsync(() => { component.stickyEndColumns = ['column-4', 'column-6']; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => { let cells = getHeaderCells(row); @@ -961,16 +969,18 @@ describe('CdkTable', () => { component.stickyEndColumns = []; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row))); dataRows.forEach(row => expectNoStickyStyles(getCells(row))); footerRows.forEach(row => expectNoStickyStyles(getFooterCells(row))); - }); + })); - it('should reverse directions for sticky columns in rtl', () => { + it('should reverse directions for sticky columns in rtl', fakeAsync(() => { component.dir = 'rtl'; component.stickyStartColumns = ['column-1', 'column-2']; component.stickyEndColumns = ['column-5', 'column-6']; fixture.detectChanges(); + flushMicrotasks(); const firstColumnWidth = getHeaderCells(headerRows[0])[0].getBoundingClientRect().width; const lastColumnWidth = getHeaderCells(headerRows[0])[5].getBoundingClientRect().width; @@ -994,14 +1004,16 @@ describe('CdkTable', () => { expectStickyStyles(footerCells[1], '1', {right: `${firstColumnWidth}px`}); expectStickyStyles(footerCells[4], '1', {left: `${lastColumnWidth}px`}); expectStickyStyles(footerCells[5], '1', {left: '0px'}); - }); + })); - it('should stick and unstick combination of sticky header, footer, and columns', () => { + it('should stick and unstick combination of sticky header, footer, and columns', + fakeAsync(() => { component.stickyHeaders = ['header-1']; component.stickyFooters = ['footer-3']; component.stickyStartColumns = ['column-1']; component.stickyEndColumns = ['column-6']; fixture.detectChanges(); + flushMicrotasks(); let headerCells = getHeaderCells(headerRows[0]); expectStickyStyles(headerRows[0], '100', {top: '0px'}); @@ -1029,11 +1041,12 @@ describe('CdkTable', () => { component.stickyStartColumns = []; component.stickyEndColumns = []; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => expectNoStickyStyles([row, ...getHeaderCells(row)])); dataRows.forEach(row => expectNoStickyStyles([row, ...getCells(row)])); footerRows.forEach(row => expectNoStickyStyles([row, ...getFooterCells(row)])); - }); + })); }); describe('on native table layout', () => { @@ -1049,9 +1062,10 @@ describe('CdkTable', () => { dataRows = getRows(tableElement); }); - it('should stick and unstick headers', () => { + it('should stick and unstick headers', fakeAsync(() => { component.stickyHeaders = ['header-1', 'header-3']; fixture.detectChanges(); + flushMicrotasks(); getHeaderCells(headerRows[0]).forEach(cell => { expectStickyStyles(cell, '100', {top: '0px'}); @@ -1065,13 +1079,15 @@ describe('CdkTable', () => { component.stickyHeaders = []; fixture.detectChanges(); + flushMicrotasks(); expectNoStickyStyles(headerRows); // No sticky styles on rows for native table headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row))); - }); + })); - it('should stick and unstick footers', () => { + it('should stick and unstick footers', fakeAsync(() => { component.stickyFooters = ['footer-1', 'footer-3']; fixture.detectChanges(); + flushMicrotasks(); getFooterCells(footerRows[2]).forEach(cell => { expectStickyStyles(cell, '10', {bottom: '0px'}); @@ -1085,28 +1101,33 @@ describe('CdkTable', () => { component.stickyFooters = []; fixture.detectChanges(); + flushMicrotasks(); expectNoStickyStyles(footerRows); // No sticky styles on rows for native table footerRows.forEach(row => expectNoStickyStyles(getFooterCells(row))); - }); + })); - it('should stick tfoot when all rows are stuck', () => { + it('should stick tfoot when all rows are stuck', fakeAsync(() => { const tfoot = tableElement.querySelector('tfoot'); component.stickyFooters = ['footer-1']; fixture.detectChanges(); + flushMicrotasks(); expectNoStickyStyles([tfoot]); component.stickyFooters = ['footer-1', 'footer-2', 'footer-3']; fixture.detectChanges(); + flushMicrotasks(); expectStickyStyles(tfoot, '10', {bottom: '0px'}); component.stickyFooters = ['footer-1', 'footer-2']; fixture.detectChanges(); + flushMicrotasks(); expectNoStickyStyles([tfoot]); - }); + })); - it('should stick and unstick left columns', () => { + it('should stick and unstick left columns', fakeAsync(() => { component.stickyStartColumns = ['column-1', 'column-3']; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => { let cells = getHeaderCells(row); @@ -1129,14 +1150,16 @@ describe('CdkTable', () => { component.stickyStartColumns = []; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row))); dataRows.forEach(row => expectNoStickyStyles(getCells(row))); footerRows.forEach(row => expectNoStickyStyles(getFooterCells(row))); - }); + })); - it('should stick and unstick right columns', () => { + it('should stick and unstick right columns', fakeAsync(() => { component.stickyEndColumns = ['column-4', 'column-6']; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => { let cells = getHeaderCells(row); @@ -1159,17 +1182,20 @@ describe('CdkTable', () => { component.stickyEndColumns = []; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row))); dataRows.forEach(row => expectNoStickyStyles(getCells(row))); footerRows.forEach(row => expectNoStickyStyles(getFooterCells(row))); - }); + })); - it('should stick and unstick combination of sticky header, footer, and columns', () => { + it('should stick and unstick combination of sticky header, footer, and columns', + fakeAsync(() => { component.stickyHeaders = ['header-1']; component.stickyFooters = ['footer-3']; component.stickyStartColumns = ['column-1']; component.stickyEndColumns = ['column-6']; fixture.detectChanges(); + flushMicrotasks(); const headerCells = getHeaderCells(headerRows[0]); expectStickyStyles(headerCells[0], '101', {top: '0px', left: '0px'}); @@ -1201,11 +1227,12 @@ describe('CdkTable', () => { component.stickyStartColumns = []; component.stickyEndColumns = []; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => expectNoStickyStyles([row, ...getHeaderCells(row)])); dataRows.forEach(row => expectNoStickyStyles([row, ...getCells(row)])); footerRows.forEach(row => expectNoStickyStyles([row, ...getFooterCells(row)])); - }); + })); }); }); diff --git a/src/cdk/table/table.ts b/src/cdk/table/table.ts index 1dabd49ad286..df10a67bad15 100644 --- a/src/cdk/table/table.ts +++ b/src/cdk/table/table.ts @@ -48,6 +48,7 @@ import { } from 'rxjs'; import {takeUntil} from 'rxjs/operators'; import {CdkColumnDef} from './cell'; +import {_CoalescedStyleScheduler} from './coalesced-style-scheduler'; import { BaseRowDef, CdkCellOutlet, @@ -186,7 +187,10 @@ export interface RenderRow { // declared elsewhere, they are checked when their declaration points are checked. // tslint:disable-next-line:validate-decorators changeDetection: ChangeDetectionStrategy.Default, - providers: [{provide: CDK_TABLE, useExisting: CdkTable}] + providers: [ + {provide: CDK_TABLE, useExisting: CdkTable}, + _CoalescedStyleScheduler, + ] }) export class CdkTable implements AfterContentChecked, CollectionViewer, OnDestroy, OnInit { private _document: Document; @@ -376,6 +380,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes // this setter will be invoked before the row outlet has been defined hence the null check. if (this._rowOutlet && this._rowOutlet.viewContainer.length) { this._forceRenderDataRows(); + this.updateStickyColumnStyles(); } } _multiTemplateDataRows: boolean = false; @@ -422,6 +427,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes constructor( protected readonly _differs: IterableDiffers, protected readonly _changeDetectorRef: ChangeDetectorRef, + protected readonly _coalescedStyleScheduler: _CoalescedStyleScheduler, protected readonly _elementRef: ElementRef, @Attribute('role') role: string, @Optional() protected readonly _dir: Directionality, @Inject(DOCUMENT) _document: any, private _platform: Platform) { @@ -1080,7 +1086,8 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes private _setupStickyStyler() { const direction: Direction = this._dir ? this._dir.value : 'ltr'; this._stickyStyler = new StickyStyler( - this._isNativeHtmlTable, this.stickyCssClass, direction, this._platform.isBrowser); + this._isNativeHtmlTable, this.stickyCssClass, direction, this._coalescedStyleScheduler, + this._platform.isBrowser); (this._dir ? this._dir.change : observableOf()) .pipe(takeUntil(this._onDestroy)) .subscribe(value => { diff --git a/src/material-experimental/column-resize/column-resize.spec.ts b/src/material-experimental/column-resize/column-resize.spec.ts index 0cdbbffbd967..b6538be3d05f 100644 --- a/src/material-experimental/column-resize/column-resize.spec.ts +++ b/src/material-experimental/column-resize/column-resize.spec.ts @@ -6,7 +6,7 @@ import { ViewChild, ChangeDetectionStrategy, } from '@angular/core'; -import {ComponentFixture, TestBed, fakeAsync, inject} from '@angular/core/testing'; +import {ComponentFixture, TestBed, fakeAsync, flushMicrotasks, inject} from '@angular/core/testing'; import {BidiModule} from '@angular/cdk/bidi'; import {DataSource} from '@angular/cdk/collections'; import {dispatchKeyboardEvent} from '@angular/cdk/testing/private'; @@ -347,7 +347,7 @@ describe('Material Popover Edit', () => { let fixture: ComponentFixture; let overlayContainer: OverlayContainer; - beforeEach(() => { + beforeEach(fakeAsync(() => { jasmine.addMatchers(approximateMatcher); TestBed.configureTestingModule({ @@ -360,7 +360,8 @@ describe('Material Popover Edit', () => { fixture = TestBed.createComponent(componentClass); component = fixture.componentInstance; fixture.detectChanges(); - }); + flushMicrotasks(); + })); afterEach(() => { // The overlay container's `ngOnDestroy` won't be called between test runs so we need diff --git a/src/material-experimental/mdc-table/table.spec.ts b/src/material-experimental/mdc-table/table.spec.ts index 8a95ab53574c..d9d617fc703b 100644 --- a/src/material-experimental/mdc-table/table.spec.ts +++ b/src/material-experimental/mdc-table/table.spec.ts @@ -147,13 +147,14 @@ describe('MDC-based MatTable', () => { ]); }); - it('should apply custom sticky CSS class to sticky cells', () => { + it('should apply custom sticky CSS class to sticky cells', fakeAsync(() => { let fixture = TestBed.createComponent(StickyTableApp); fixture.detectChanges(); + flushMicrotasks(); const stuckCellElement = fixture.nativeElement.querySelector('table th')!; expect(stuckCellElement.classList).toContain('mat-mdc-table-sticky'); - }); + })); // Note: needs to be fakeAsync so it catches the error. it('should not throw when a row definition is on an ng-container', fakeAsync(() => { diff --git a/src/material-experimental/mdc-table/table.ts b/src/material-experimental/mdc-table/table.ts index daeb200e8e84..1e5ae437a5a8 100644 --- a/src/material-experimental/mdc-table/table.ts +++ b/src/material-experimental/mdc-table/table.ts @@ -7,7 +7,7 @@ */ import {ChangeDetectionStrategy, Component, OnInit, ViewEncapsulation} from '@angular/core'; -import {CDK_TABLE_TEMPLATE, CdkTable} from '@angular/cdk/table'; +import {CDK_TABLE_TEMPLATE, CdkTable, _CoalescedStyleScheduler} from '@angular/cdk/table'; @Component({ selector: 'table[mat-table]', @@ -17,7 +17,10 @@ import {CDK_TABLE_TEMPLATE, CdkTable} from '@angular/cdk/table'; host: { 'class': 'mat-mdc-table mdc-data-table__table', }, - providers: [{provide: CdkTable, useExisting: MatTable}], + providers: [ + {provide: CdkTable, useExisting: MatTable}, + _CoalescedStyleScheduler, + ], encapsulation: ViewEncapsulation.None, // See note on CdkTable for explanation on why this uses the default change detection strategy. // tslint:disable-next-line:validate-decorators diff --git a/src/material/table/table.spec.ts b/src/material/table/table.spec.ts index a64d1b7dc0d0..51a1a3a51555 100644 --- a/src/material/table/table.spec.ts +++ b/src/material/table/table.spec.ts @@ -186,13 +186,14 @@ describe('MatTable', () => { ]); }); - it('should apply custom sticky CSS class to sticky cells', () => { + it('should apply custom sticky CSS class to sticky cells', fakeAsync(() => { let fixture = TestBed.createComponent(StickyTableApp); fixture.detectChanges(); + flushMicrotasks(); const stuckCellElement = fixture.nativeElement.querySelector('.mat-table th')!; expect(stuckCellElement.classList).toContain('mat-table-sticky'); - }); + })); // Note: needs to be fakeAsync so it catches the error. it('should not throw when a row definition is on an ng-container', fakeAsync(() => { diff --git a/src/material/table/table.ts b/src/material/table/table.ts index 921b121f1c68..f86f0eae9ea8 100644 --- a/src/material/table/table.ts +++ b/src/material/table/table.ts @@ -6,7 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {CDK_TABLE_TEMPLATE, CdkTable, CDK_TABLE} from '@angular/cdk/table'; +import { + CDK_TABLE_TEMPLATE, + CdkTable, + CDK_TABLE, + _CoalescedStyleScheduler +} from '@angular/cdk/table'; import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; /** @@ -22,7 +27,8 @@ import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/co }, providers: [ {provide: CdkTable, useExisting: MatTable}, - {provide: CDK_TABLE, useExisting: MatTable} + {provide: CDK_TABLE, useExisting: MatTable}, + _CoalescedStyleScheduler, ], encapsulation: ViewEncapsulation.None, // See note on CdkTable for explanation on why this uses the default change detection strategy. diff --git a/tools/public_api_guard/cdk/table.d.ts b/tools/public_api_guard/cdk/table.d.ts index ab08f3d488ed..7d68d6eb4810 100644 --- a/tools/public_api_guard/cdk/table.d.ts +++ b/tools/public_api_guard/cdk/table.d.ts @@ -1,3 +1,17 @@ +export declare class _CoalescedStyleScheduler implements OnDestroy { + constructor(_ngZone: NgZone); + ngOnDestroy(): void; + schedule(task: () => unknown): void; + scheduleEnd(task: () => unknown): void; + static ɵfac: i0.ɵɵFactoryDef<_CoalescedStyleScheduler, never>; + static ɵprov: i0.ɵɵInjectableDef<_CoalescedStyleScheduler>; +} + +export declare class _Schedule { + endTasks: (() => unknown)[]; + tasks: (() => unknown)[]; +} + export declare class BaseCdkCell { constructor(columnDef: CdkColumnDef, elementRef: ElementRef); } @@ -169,6 +183,7 @@ export declare class CdkRowDef extends BaseRowDef { export declare class CdkTable implements AfterContentChecked, CollectionViewer, OnDestroy, OnInit { protected readonly _changeDetectorRef: ChangeDetectorRef; + protected readonly _coalescedStyleScheduler: _CoalescedStyleScheduler; _contentColumnDefs: QueryList; _contentFooterRowDefs: QueryList; _contentHeaderRowDefs: QueryList; @@ -194,7 +209,7 @@ export declare class CdkTable implements AfterContentChecked, CollectionViewe start: number; end: number; }>; - constructor(_differs: IterableDiffers, _changeDetectorRef: ChangeDetectorRef, _elementRef: ElementRef, role: string, _dir: Directionality, _document: any, _platform: Platform); + constructor(_differs: IterableDiffers, _changeDetectorRef: ChangeDetectorRef, _coalescedStyleScheduler: _CoalescedStyleScheduler, _elementRef: ElementRef, role: string, _dir: Directionality, _document: any, _platform: Platform); _getRenderedRows(rowOutlet: RowOutlet): HTMLElement[]; _getRowDefs(data: T, dataIndex: number): CdkRowDef[]; addColumnDef(columnDef: CdkColumnDef): void; @@ -214,7 +229,7 @@ export declare class CdkTable implements AfterContentChecked, CollectionViewe updateStickyHeaderRowStyles(): void; static ngAcceptInputType_multiTemplateDataRows: BooleanInput; static ɵcmp: i0.ɵɵComponentDefWithMeta, "cdk-table, table[cdk-table]", ["cdkTable"], { "trackBy": "trackBy"; "dataSource": "dataSource"; "multiTemplateDataRows": "multiTemplateDataRows"; }, {}, ["_noDataRow", "_contentColumnDefs", "_contentRowDefs", "_contentHeaderRowDefs", "_contentFooterRowDefs"], ["caption", "colgroup, col"]>; - static ɵfac: i0.ɵɵFactoryDef, [null, null, null, { attribute: "role"; }, { optional: true; }, null, null]>; + static ɵfac: i0.ɵɵFactoryDef, [null, null, null, null, { attribute: "role"; }, { optional: true; }, null, null]>; } export declare class CdkTableModule { @@ -299,7 +314,7 @@ export declare type StickyDirection = 'top' | 'bottom' | 'left' | 'right'; export declare class StickyStyler { direction: Direction; - constructor(_isNativeHtmlTable: boolean, _stickCellCss: string, direction: Direction, _isBrowser?: boolean); + constructor(_isNativeHtmlTable: boolean, _stickCellCss: string, direction: Direction, _coalescedStyleScheduler: _CoalescedStyleScheduler, _isBrowser?: boolean); _addStickyStyle(element: HTMLElement, dir: StickyDirection, dirValue: number): void; _getCalculatedZIndex(element: HTMLElement): string; _getCellWidths(row: HTMLElement): number[];