From 1b6c93571c4615deb929f7b0e72c3e42478a31ec Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 27 Oct 2021 13:10:41 +0200 Subject: [PATCH] fix(material/table): set class and role on no data row (#23749) Automatically adds a class to the no data row so that it's easier to style. In the process of adding the class I also noticed that the no data row doesn't have the correct `role`. Fixes #23729. --- src/cdk/table/row.ts | 1 + src/cdk/table/table.spec.ts | 22 ++++++++------ src/cdk/table/table.ts | 30 +++++++++++++++---- src/material-experimental/mdc-table/row.ts | 4 ++- .../mdc-table/table.spec.ts | 18 ++++++----- src/material/table/row.ts | 4 ++- src/material/table/table.spec.ts | 18 ++++++----- tools/public_api_guard/cdk/table.md | 2 ++ tools/public_api_guard/material/table.md | 2 ++ 9 files changed, 70 insertions(+), 31 deletions(-) diff --git a/src/cdk/table/row.ts b/src/cdk/table/row.ts index 8e4e7b2f109a..1e2bd6bdd850 100644 --- a/src/cdk/table/row.ts +++ b/src/cdk/table/row.ts @@ -313,5 +313,6 @@ export class CdkRow {} selector: 'ng-template[cdkNoDataRow]', }) export class CdkNoDataRow { + _contentClassName = 'cdk-no-data-row'; constructor(public templateRef: TemplateRef) {} } diff --git a/src/cdk/table/table.spec.ts b/src/cdk/table/table.spec.ts index 61d558017a7f..3486abb232d1 100644 --- a/src/cdk/table/table.spec.ts +++ b/src/cdk/table/table.spec.ts @@ -298,13 +298,15 @@ describe('CdkTable', () => { }); it('should be able to show a message when no data is being displayed', () => { - expect(tableElement.textContent!.trim()).not.toContain('No data'); + expect(tableElement.querySelector('.cdk-no-data-row')).toBeFalsy(); const originalData = dataSource.data; dataSource.data = []; fixture.detectChanges(); - expect(tableElement.textContent!.trim()).toContain('No data'); + const noDataRow = tableElement.querySelector('.cdk-no-data-row')!; + expect(noDataRow).toBeTruthy(); + expect(noDataRow.getAttribute('role')).toBe('row'); dataSource.data = originalData; fixture.detectChanges(); @@ -312,7 +314,7 @@ describe('CdkTable', () => { // Expect it to have emitted once on init, once when empty, and again with original data. expect(component.contentChangedCount).toBe(3); - expect(tableElement.textContent!.trim()).not.toContain('No data'); + expect(tableElement.querySelector('.cdk-no-data-row')).toBeFalsy(); }); it('should show the no data row if there is no data on init', () => { @@ -321,7 +323,7 @@ describe('CdkTable', () => { fixture.componentInstance.dataSource.data = []; fixture.detectChanges(); tableElement = fixture.nativeElement.querySelector('.cdk-table'); - expect(tableElement.textContent!.trim()).toContain('No data'); + expect(tableElement.querySelector('.cdk-no-data-row')).toBeTruthy(); expect(component.contentChangedCount).toBe(1); }); }); @@ -558,17 +560,19 @@ describe('CdkTable', () => { const dataSource = thisFixture.componentInstance.dataSource!; const originalData = dataSource.data; - expect(tbody.textContent!.trim()).not.toContain('No data'); + expect(tbody.querySelector('.cdk-no-data-row')).toBeFalsy(); dataSource.data = []; thisFixture.detectChanges(); - expect(tbody.textContent!.trim()).toContain('No data'); + const noDataRow: HTMLElement = tbody.querySelector('.cdk-no-data-row'); + expect(noDataRow).toBeTruthy(); + expect(noDataRow.getAttribute('role')).toBe('row'); dataSource.data = originalData; thisFixture.detectChanges(); - expect(tbody.textContent!.trim()).not.toContain('No data'); + expect(tbody.querySelector('.cdk-no-data-row')).toBeFalsy(); }); it('should apply correct roles for native table elements', () => { @@ -743,7 +747,7 @@ describe('CdkTable', () => { fixture.componentInstance.dataSource.data = []; fixture.detectChanges(); - expect(tableElement.textContent).toContain('No data'); + expect(tableElement.querySelector('.cdk-no-data-row')).toBeTruthy(); }); describe('using when predicate', () => { @@ -2766,7 +2770,7 @@ class RowContextCdkTableApp { - No data +
No data
`, }) diff --git a/src/cdk/table/table.ts b/src/cdk/table/table.ts index c0d98f566fbf..114da2fbc634 100644 --- a/src/cdk/table/table.ts +++ b/src/cdk/table/table.ts @@ -1286,15 +1286,33 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes private _updateNoDataRow() { const noDataRow = this._customNoDataRow || this._noDataRow; - if (noDataRow) { - const shouldShow = this._rowOutlet.viewContainer.length === 0; + if (!noDataRow) { + return; + } + + const shouldShow = this._rowOutlet.viewContainer.length === 0; + + if (shouldShow === this._isShowingNoDataRow) { + return; + } + + const container = this._noDataRowOutlet.viewContainer; + + if (shouldShow) { + const view = container.createEmbeddedView(noDataRow.templateRef); + const rootNode: HTMLElement | undefined = view.rootNodes[0]; - if (shouldShow !== this._isShowingNoDataRow) { - const container = this._noDataRowOutlet.viewContainer; - shouldShow ? container.createEmbeddedView(noDataRow.templateRef) : container.clear(); - this._isShowingNoDataRow = shouldShow; + // Only add the attributes if we have a single root node since it's hard + // to figure out which one to add it to when there are multiple. + if (view.rootNodes.length === 1 && rootNode?.nodeType === this._document.ELEMENT_NODE) { + rootNode.setAttribute('role', 'row'); + rootNode.classList.add(noDataRow._contentClassName); } + } else { + container.clear(); } + + this._isShowingNoDataRow = shouldShow; } static ngAcceptInputType_multiTemplateDataRows: BooleanInput; diff --git a/src/material-experimental/mdc-table/row.ts b/src/material-experimental/mdc-table/row.ts index e2276b2d6d89..9b8e43163bc4 100644 --- a/src/material-experimental/mdc-table/row.ts +++ b/src/material-experimental/mdc-table/row.ts @@ -108,4 +108,6 @@ export class MatRow extends CdkRow {} selector: 'ng-template[matNoDataRow]', providers: [{provide: CdkNoDataRow, useExisting: MatNoDataRow}], }) -export class MatNoDataRow extends CdkNoDataRow {} +export class MatNoDataRow extends CdkNoDataRow { + override _contentClassName = 'mat-mdc-no-data-row'; +} diff --git a/src/material-experimental/mdc-table/table.spec.ts b/src/material-experimental/mdc-table/table.spec.ts index 045ddca64003..08d9f00adcee 100644 --- a/src/material-experimental/mdc-table/table.spec.ts +++ b/src/material-experimental/mdc-table/table.spec.ts @@ -123,17 +123,19 @@ describe('MDC-based MatTable', () => { const dataSource = fixture.componentInstance.dataSource!; const initialData = dataSource.data; - expect(tbody.textContent.trim()).not.toContain('No data'); + expect(tbody.querySelector('.mat-mdc-no-data-row')).toBeFalsy(); dataSource.data = []; fixture.detectChanges(); - expect(tbody.textContent.trim()).toContain('No data'); + const noDataRow: HTMLElement = tbody.querySelector('.mat-mdc-no-data-row'); + expect(noDataRow).toBeTruthy(); + expect(noDataRow.getAttribute('role')).toBe('row'); dataSource.data = initialData; fixture.detectChanges(); - expect(tbody.textContent.trim()).not.toContain('No data'); + expect(tbody.querySelector('.mat-mdc-no-data-row')).toBeFalsy(); }); it('should be able to show a message when no data is being displayed', () => { @@ -144,17 +146,19 @@ describe('MDC-based MatTable', () => { const tbody = fixture.nativeElement.querySelector('tbody')!; const initialData = fixture.componentInstance.dataSource!.data; - expect(tbody.textContent.trim()).not.toContain('No data'); + expect(tbody.querySelector('.mat-mdc-no-data-row')).toBeFalsy(); fixture.componentInstance.dataSource!.data = []; fixture.detectChanges(); - expect(tbody.textContent.trim()).toContain('No data'); + const noDataRow: HTMLElement = tbody.querySelector('.mat-mdc-no-data-row'); + expect(noDataRow).toBeTruthy(); + expect(noDataRow.getAttribute('role')).toBe('row'); fixture.componentInstance.dataSource!.data = initialData; fixture.detectChanges(); - expect(tbody.textContent.trim()).not.toContain('No data'); + expect(tbody.querySelector('.mat-mdc-no-data-row')).toBeFalsy(); }); it('should show the no data row if there is no data on init', () => { @@ -163,7 +167,7 @@ describe('MDC-based MatTable', () => { fixture.detectChanges(); const tbody = fixture.nativeElement.querySelector('tbody')!; - expect(tbody.textContent.trim()).toContain('No data'); + expect(tbody.querySelector('.mat-mdc-no-data-row')).toBeTruthy(); }); it('should set the content styling class on the tbody', () => { diff --git a/src/material/table/row.ts b/src/material/table/row.ts index 71624f8239e2..79490b1e7e1b 100644 --- a/src/material/table/row.ts +++ b/src/material/table/row.ts @@ -108,4 +108,6 @@ export class MatRow extends CdkRow {} selector: 'ng-template[matNoDataRow]', providers: [{provide: CdkNoDataRow, useExisting: MatNoDataRow}], }) -export class MatNoDataRow extends CdkNoDataRow {} +export class MatNoDataRow extends CdkNoDataRow { + override _contentClassName = 'mat-no-data-row'; +} diff --git a/src/material/table/table.spec.ts b/src/material/table/table.spec.ts index 6ae4f276c3f5..e47337e73a68 100644 --- a/src/material/table/table.spec.ts +++ b/src/material/table/table.spec.ts @@ -92,17 +92,19 @@ describe('MatTable', () => { const table = fixture.nativeElement.querySelector('.mat-table')!; const initialData = fixture.componentInstance.dataSource!.data; - expect(table.textContent.trim()).not.toContain('No data'); + expect(table.querySelector('.mat-no-data-row')).toBeFalsy(); fixture.componentInstance.dataSource!.data = []; fixture.detectChanges(); - expect(table.textContent.trim()).toContain('No data'); + const noDataRow: HTMLElement = table.querySelector('.mat-no-data-row'); + expect(noDataRow).toBeTruthy(); + expect(noDataRow.getAttribute('role')).toBe('row'); fixture.componentInstance.dataSource!.data = initialData; fixture.detectChanges(); - expect(table.textContent.trim()).not.toContain('No data'); + expect(table.querySelector('.mat-no-data-row')).toBeFalsy(); }); it('should show the no data row if there is no data on init', () => { @@ -111,7 +113,7 @@ describe('MatTable', () => { fixture.detectChanges(); const table = fixture.nativeElement.querySelector('.mat-table')!; - expect(table.textContent.trim()).toContain('No data'); + expect(table.querySelector('.mat-no-data-row')).toBeTruthy(); }); }); @@ -154,17 +156,19 @@ describe('MatTable', () => { const dataSource = fixture.componentInstance.dataSource!; const initialData = dataSource.data; - expect(tbody.textContent.trim()).not.toContain('No data'); + expect(tbody.querySelector('.mat-no-data-row')).toBeFalsy(); dataSource.data = []; fixture.detectChanges(); - expect(tbody.textContent.trim()).toContain('No data'); + const noDataRow: HTMLElement = tbody.querySelector('.mat-no-data-row'); + expect(noDataRow).toBeTruthy(); + expect(noDataRow.getAttribute('role')).toBe('row'); dataSource.data = initialData; fixture.detectChanges(); - expect(tbody.textContent.trim()).not.toContain('No data'); + expect(tbody.querySelector('.mat-no-data-row')).toBeFalsy(); }); it('should render with MatTableDataSource and sort', () => { diff --git a/tools/public_api_guard/cdk/table.md b/tools/public_api_guard/cdk/table.md index c2eca5fbc46d..8d459ff891f3 100644 --- a/tools/public_api_guard/cdk/table.md +++ b/tools/public_api_guard/cdk/table.md @@ -257,6 +257,8 @@ export class CdkHeaderRowDef extends _CdkHeaderRowDefBase implements CanStick, O export class CdkNoDataRow { constructor(templateRef: TemplateRef); // (undocumented) + _contentClassName: string; + // (undocumented) templateRef: TemplateRef; // (undocumented) static ɵdir: i0.ɵɵDirectiveDeclaration; diff --git a/tools/public_api_guard/material/table.md b/tools/public_api_guard/material/table.md index 7fc2eac4d0ea..90732fa69776 100644 --- a/tools/public_api_guard/material/table.md +++ b/tools/public_api_guard/material/table.md @@ -124,6 +124,8 @@ export class MatHeaderRowDef extends CdkHeaderRowDef { // @public export class MatNoDataRow extends CdkNoDataRow { + // (undocumented) + _contentClassName: string; // (undocumented) static ɵdir: i0.ɵɵDirectiveDeclaration; // (undocumented)