Repository navigation
Expose DataGrid header and body tables as one ARIA table - #21
Conversation
DataGrid renders its header, body and footer as three separate <table> elements so the header can stay fixed while the body scrolls, which assistive technology reads as a headers-only table and a headerless data table. DataGrid now carries role="table", its three inner tables and the empty/loading containers are presentational, and each section is a rowgroup. A new protected hook, AbstractCellTable.onSectionRendered, runs after any head, body or foot rows are replaced (full or partial redraw); DataGrid uses it to give rows role="row" and cells columnheader, rowheader or cell. Rows that already carry a role other than row, such as layout rows marked presentation, are left alone. Addresses rstudio/rstudio#19173
replaceAllRows and replaceChildren detach the section while they render into it. Calling onSectionRendered before reattaching it keeps the role writes off the live document, so the accessibility tree is built once with the final roles. Addresses rstudio/rstudio#19173
Cover the rowheader role for a th in the body, and check the loading and empty-table containers by putting the grid in those states and reading the table that holds the displayed widget. The containers go back to private, since the test no longer reaches into them. Addresses rstudio/rstudio#19173
Code review findingsActionable items from an automated review. Important
Suggestions
|
replaceChildren passed the whole tbody to onSectionRendered, so updating one row re-checked the role of every row and cell in the body. It now passes the temporary section holding only the new rows, before they are moved into the table. The call in replaceAllRows moves above the reattach comment, which it had split from the line it describes. Addresses rstudio/rstudio#19173
A row with role="" was skipped along with its cells, and a cell with role="" kept the empty role, dropping them from the table. Each role is now read once and an empty one counts as absent. The th role is chosen once per section instead of inside the cell loop. Addresses rstudio/rstudio#19173
The loading and empty-table containers are presentational, so their message sat inside the grid's table but outside any row. Chrome exposed it as bare text, which screen readers that navigate Chrome's table structure have no row to reach. The container's tbody, row and cell now get rowgroup, row and cell roles, so the message reads as a row of the grid. Addresses rstudio/rstudio#19173
Cover the footer row and th roles. The sortable-header test compared the CellTable header's role to "", which also passed for role=""; it now requires the attribute to be absent. Addresses rstudio/rstudio#19173
Passing null to setLoadingIndicator or setEmptyTableWidget disables the message, but its container still has an empty cell, which the row and cell roles exposed as a blank row of the grid. The roles are now only set while the container holds a widget. Addresses rstudio/rstudio#19173
|
Addressed in 1a73a40..ea4d423:
|
|
Re-reviewed through ea4d423. All 8 findings from the earlier comment are addressed: 1 by the companion-change note in the description, the rest in code and tests. |
* Mark DataGrid layout-only rows presentational The GWT fork now exposes DataGrid rows and cells with explicit ARIA roles; rows that exist only for layout opt out. Addresses #19173 * Test that DataGrid headers and cells form one table Addresses #19173 * Expect the empty Environment grid message as a table row The GWT fork now gives the empty-table container row and cell roles, so the message reads as the grid's second row instead of bare text. Addresses #19173 * Use GWT 2.13.1-montauk-daisy-3 Picks up the fork's DataGrid table roles (rstudio/gwt#21). Addresses #19173 * Keep the Files table test's setup inside its cleanup The directory setup ran before the try block, so a timeout after the files were created skipped the cleanup, and it left d in the global environment. Setup now runs inside try, in local(), and the cleanup rebuilds the path itself. Addresses #19173 * Test that the Environment list view exposes no blank row The list view's width-setting row is marked presentational; check that the first row its table exposes has content. Addresses #19173 * Require the Values header as the list view's first row Only the list view has category header rows, so expecting "Values" as the first exposed row also confirms the switch from grid view finished before the check. Addresses #19173
DataGridkeeps its header fixed while the body scrolls by rendering three separate<table>elements (header, body, footer) inside aHeaderPanel. Nothing ties them together, so assistive technology sees a table of column headers with no data and a separate data table with no headers, and moving through body cells announces no column header.CellTablerenders a single<table>and is unaffected. Addresses rstudio/rstudio#19173.Changes
DataGridelement getsrole="table". Its three inner<table>elements getrole="presentation", and theirthead/tbody/tfootgetrole="rowgroup". Everything is a DOM descendant of therole="table"element (the divs in between are generic), so the browser builds one table and computes column headers by cell position. The DOM, CSS and layout are unchanged.protected void AbstractCellTable.onSectionRendered(TableSectionElement section), a no-op by default, is called with newly rendered rows before they enter the document: byImpl.replaceAllRows(full body redraws, and header/footer rendering) with the table's own detached section, and byImpl.replaceChildren(partial body redraws) with the temporary section that holds only the new rows.DataGridoverrides it to give each<tr>role="row", each<th>columnheader(in the head or foot) orrowheader(in the body), and each<td>cell. Doing this after rendering covers every table builder, including custom ones, without changing them.row, such as a layout-only row a builder markspresentation, is skipped along with its cells.FlexTables that replace the body table while loading or empty. Their table getsrole="presentation", and while they hold a widget theirtbody, row and cell getrowgroup,rowandcell, so the message reads as a row of the grid (Chrome:row > cell "Environment is empty") rather than a nested table or text outside any row. A null widget, which disables the message, gets no row roles, so the empty cell doesn't read as a blank row.Not changed: when a grid has a visible footer,
HeaderPanelputs the footer container before the content container in the DOM, so the footer rowgroup comes before the body rows in the accessibility tree. A<th>in the footer is acolumnheader, as Chrome maps a<th>in a native<tfoot>. No RStudio grid has a footer.RStudio needs a companion change shipped with the SDK update: rows that only exist for layout (the virtualized grid's padding rows, the Environment list's width-setting row) must carry
role="presentation", or they read as blank rows. That change and theGWT_VERSIONbump go in the same rstudio PR.Testing
New tests in
DataGridTest:testTableRoles: the container, table, rowgroup, row, columnheader and cell roles after a full render.testTableRolesAfterPartialRedraw: a row replaced bysetRowData(start, values)gets row and cell roles.testPartialRedrawOnlyStampsNewRows: a partial redraw leaves rows outside the replaced range untouched.testTableRolesKeepExplicitRoles: a builder row markedpresentationkeeps that role, and its cells get none.testTableRolesTreatEmptyRoleAsAbsent: a row and cell withrole=""getrowandcell.testTableRolesKeepAriaSort: a sorted header's<th>has bothcolumnheaderandaria-sort.testTableRolesBodyThIsRowHeader: a<th>rendered in the body is arowheader.testTableRolesFooter: a footer row is arowand its<th>acolumnheader.testLoadingAndEmptyMessagesArePresentational: in the loading and empty states, the displayed widget is in acellof arowof arowgroupin a presentational table inside the grid.testNullLoadingIndicatorAddsNoRow: with the loading indicator disabled, its empty cell and row have no role.testSortableHeaderAriainAbstractCellTableTestBaseasserted that a header<th>has no role. It now requires noroleattribute forCellTableandcolumnheaderforDataGrid.Each test except
testTableRolesFooterfailed before the change it covers, or, fortestTableRolesBodyThIsRowHeader, fails when the code it covers is broken (rowheaderswapped forcolumnheader).testTableRolesFootercovers footer rendering, which the role code already handled.Results:
CellTableTestandDataGridTestpass in HtmlUnit (96 tests).checkstyleandapicheckpass, andant clean dist-devbuilds.2.13.1-montauk-daisy-2, the Files and empty Environment tests fail with two tables.