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);
}