From b354ddce49c832e9427daadae19e1b7dc497c02e Mon Sep 17 00:00:00 2001 From: Jeremy Elbourn Date: Tue, 12 Feb 2019 10:15:50 -0800 Subject: [PATCH] fix(table): add missing rowgroup roles The native cdk-table adds thead, tbody, and tfoot elements but was omitting the "rowgroup" role from these. This is necessary since we also specifically add roles to all of the other table elements. This also marks thead and tfoot as `display: none` when there are no corresponding rows. --- src/cdk/table/table.spec.ts | 75 ++++++++++++++++++++++++++++++++----- src/cdk/table/table.ts | 29 +++++++++++--- 2 files changed, 89 insertions(+), 15 deletions(-) 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); }