diff --git a/src/cdk/table/table.spec.ts b/src/cdk/table/table.spec.ts index 00d04d17a60d..a5c32a33237d 100644 --- a/src/cdk/table/table.spec.ts +++ b/src/cdk/table/table.spec.ts @@ -505,6 +505,32 @@ describe('CdkTable', () => { ]); }); + it('should apply correct roles for native table elements', () => { + const thisFixture = createComponent(NativeHtmlTableApp); + const thisTableElement: HTMLTableElement = thisFixture.nativeElement.querySelector('table'); + thisFixture.detectChanges(); + + const rowGroups = Array.from(thisTableElement.querySelectorAll('thead, tbody, tfoot')); + expect(rowGroups.length).toBe(3, 'Expected table to have a thead, tbody, and tfoot'); + for (const group of rowGroups) { + expect(group.getAttribute('role')) + .toBe('rowgroup', 'Expected thead, tbody, and tfoot to have role="rowgroup"'); + } + }); + + it('should hide thead/tfoot when there are no header/footer rows', () => { + const thisFixture = createComponent(NativeTableWithNoHeaderOrFooterRows); + const thisTableElement: HTMLTableElement = thisFixture.nativeElement.querySelector('table'); + thisFixture.detectChanges(); + + const rowGroups: HTMLElement[] = Array.from(thisTableElement.querySelectorAll('thead, tfoot')); + expect(rowGroups.length).toBe(2, 'Expected table to have a thead and tfoot'); + for (const group of rowGroups) { + expect(group.style.display) + .toBe('none', 'Expected thead and tfoot to be `display: none`'); + } + }); + it('should render cells even if row data is falsy', () => { setupTableTestApp(BooleanRowCdkTableApp); expectTableToMatchContent(tableElement, [ @@ -1591,27 +1617,27 @@ class MultipleHeaderFooterRowsCdkTableApp { Column C - index_1_special_row + index_1_special_row Column C - c3_special_row + c3_special_row Index - {{index}} + {{index}} Data Index - {{dataIndex}} + {{dataIndex}} Render Index - {{renderIndex}} + {{renderIndex}} @@ -1662,12 +1688,12 @@ class WhenRowCdkTableApp { Column C - index_1_special_row + index_1_special_row Column C - c3_special_row + c3_special_row @@ -1705,12 +1731,12 @@ class WhenRowWithoutDefaultCdkTableApp { Column C - index_1_special_row + index_1_special_row Column C - c3_special_row + c3_special_row @@ -1790,7 +1816,7 @@ class TrackByCdkTableApp { [sticky]="isStuck(stickyStartColumns, column)" [stickyEnd]="isStuck(stickyEndColumns, column)"> Header {{column}} - {{column}} + {{column}} Footer {{column}} @@ -2226,6 +2252,35 @@ class NativeHtmlTableApp { @ViewChild(CdkTable) table: CdkTable; } +@Component({ + template: ` + + + + + + + + + + + + + + + + + +
Column A {{row.a}} Column B {{row.b}} Column C {{row.c}}
+ ` +}) +class NativeTableWithNoHeaderOrFooterRows { + dataSource: FakeDataSource | undefined = new FakeDataSource(); + columnsToRender = ['column_a', 'column_b', 'column_c']; + + @ViewChild(CdkTable) table: CdkTable; +} + @Component({ template: ` diff --git a/src/cdk/table/table.ts b/src/cdk/table/table.ts index 1411ecce8e6f..97f68382d843 100644 --- a/src/cdk/table/table.ts +++ b/src/cdk/table/table.ts @@ -579,11 +579,20 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes * sticky input changes. May be called manually for cases where the cell content changes outside * of these events. */ - updateStickyHeaderRowStyles() { + updateStickyHeaderRowStyles(): void { const headerRows = this._getRenderedRows(this._headerRowOutlet); - this._stickyStyler.clearStickyPositioning(headerRows, ['top']); + const tableElement = this._elementRef.nativeElement as HTMLElement; + + // Hide the thead element if there are no header rows. This is necessary to satisfy + // overzealous a11y checkers that fail because the `rowgroup` element does not contain + // required child `row`. + const thead = tableElement.querySelector('thead'); + if (thead) { + thead.style.display = headerRows.length ? '' : 'none'; + } const stickyStates = this._headerRowDefs.map(def => def.sticky); + this._stickyStyler.clearStickyPositioning(headerRows, ['top']); this._stickyStyler.stickRows(headerRows, stickyStates, 'top'); // Reset the dirty state of the sticky input change since it has been used. @@ -597,11 +606,20 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes * sticky input changes. May be called manually for cases where the cell content changes outside * of these events. */ - updateStickyFooterRowStyles() { + updateStickyFooterRowStyles(): void { const footerRows = this._getRenderedRows(this._footerRowOutlet); - this._stickyStyler.clearStickyPositioning(footerRows, ['bottom']); + const tableElement = this._elementRef.nativeElement as HTMLElement; + + // Hide the tfoot element if there are no footer rows. This is necessary to satisfy + // overzealous a11y checkers that fail because the `rowgroup` element does not contain + // required child `row`. + const tfoot = tableElement.querySelector('tfoot'); + if (tfoot) { + tfoot.style.display = footerRows.length ? '' : 'none'; + } const stickyStates = this._footerRowDefs.map(def => def.sticky); + this._stickyStyler.clearStickyPositioning(footerRows, ['bottom']); this._stickyStyler.stickRows(footerRows, stickyStates, 'bottom'); this._stickyStyler.updateStickyFooterContainer(this._elementRef.nativeElement, stickyStates); @@ -865,7 +883,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes } /** Gets the list of rows that have been rendered in the row outlet. */ - _getRenderedRows(rowOutlet: RowOutlet) { + _getRenderedRows(rowOutlet: RowOutlet): HTMLElement[] { const renderedRows: HTMLElement[] = []; for (let i = 0; i < rowOutlet.viewContainer.length; i++) { @@ -983,6 +1001,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes for (const section of sections) { const element = documentRef.createElement(section.tag); + element.setAttribute('role', 'rowgroup'); element.appendChild(section.outlet.elementRef.nativeElement); documentFragment.appendChild(element); }