Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ public final void replaceAllRows(AbstractCellTable<?> table, TableSectionElement

// Render the html.
replaceAllRowsImpl(table, section, html);
table.onSectionRendered(section);

/*
* Reattach the section. If next section is null, the section will be
Expand Down Expand Up @@ -585,6 +586,7 @@ public final void replaceChildren(AbstractCellTable<?> table, TableSectionElemen

// Add new child elements.
TableSectionElement newSection = convertToSectionElement(table, section.getTagName(), html);
table.onSectionRendered(newSection);
Element newChild = newSection.getFirstChildElement();
while (newChild != null) {
Element next = newChild.getNextSiblingElement();
Expand Down Expand Up @@ -1706,6 +1708,18 @@ protected boolean dependsOnSelection() {
*/
protected abstract void doSetHeaderVisible(boolean isFooter, boolean isVisible);

/**
* Called after rows of a head, body or foot section are rendered, before
* they enter the document, so subclasses can post-process them. The default
* does nothing.
*
* @param section after a full render, the table's own section, detached;
* after a partial render of the body, a detached section of the
* same tag holding only the new rows
*/
protected void onSectionRendered(TableSectionElement section) {
}

@Override
protected Element getChildContainer() {
return getTableBodyElement();
Expand Down
78 changes: 76 additions & 2 deletions user/src/com/google/gwt/user/cellview/client/DataGrid.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.dom.client.Style.TableLayout;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.TableCellElement;
import com.google.gwt.dom.client.TableColElement;
import com.google.gwt.dom.client.TableElement;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.dom.client.TableSectionElement;
import com.google.gwt.event.dom.client.ScrollEvent;
import com.google.gwt.event.dom.client.ScrollHandler;
Expand Down Expand Up @@ -504,6 +507,58 @@ private static Resources getDefaultResources() {
return DEFAULT_RESOURCES;
}

/**
* Give the rows and cells of a rendered section explicit ARIA roles, which
* they lose to the presentation role on their table. Rows and cells that
* already have a role keep it, and a row with a role other than row (such
* as a layout row marked presentation) is left alone, cells included. An
* empty role counts as none.
*/
private static void applyTableRoles(TableSectionElement section) {
// Tag, not identity: a partial render passes a temporary tbody.
String thRole = "tbody".equalsIgnoreCase(section.getTagName()) ? "rowheader" : "columnheader";
NodeList<TableRowElement> rows = section.getRows();
for (int r = 0; r < rows.getLength(); r++) {
TableRowElement row = rows.getItem(r);
String rowRole = row.getAttribute("role");
if (rowRole.isEmpty()) {
row.setAttribute("role", "row");
} else if (!"row".equals(rowRole)) {
continue;
}
NodeList<TableCellElement> cells = row.getCells();
for (int c = 0; c < cells.getLength(); c++) {
TableCellElement cell = cells.getItem(c);
if (cell.getAttribute("role").isEmpty()) {
boolean isTh = "th".equalsIgnoreCase(cell.getTagName());
cell.setAttribute("role", isTh ? thRole : "cell");
}
}
}
}

/**
* Show a widget in a message container (loading or empty table), whose
* single cell reads as a row of the grid. A null widget disables the
* message, so its empty cell must not read as a blank row.
*/
private static void setMessageWidget(FlexTable container, Widget widget) {
container.setWidget(0, 0, widget);
TableElement table = container.getElement().cast();
Element[] parts = {
table.getTBodies().getItem(0),
container.getRowFormatter().getElement(0),
container.getCellFormatter().getElement(0, 0)};
String[] roles = {"rowgroup", "row", "cell"};
for (int i = 0; i < parts.length; i++) {
if (widget == null) {
parts[i].removeAttribute("role");
} else {
parts[i].setAttribute("role", roles[i]);
}
}
}

final TableWidget tableData;
final TableWidget tableFooter;
final TableWidget tableHeader;
Expand Down Expand Up @@ -649,6 +704,20 @@ public DataGrid(int pageSize, Resources resources, ProvidesKey<T> keyProvider,
loadingIndicatorContainer = new FlexTable();
loadingIndicatorContainer.getElement().setAttribute("align", "center");

/*
* The header, body and footer are separate tables so the header can stay
* fixed while the body scrolls. Presentation roles on those tables, plus
* explicit row and cell roles (see onSectionRendered), let assistive
* technology read them as one table.
*/
getElement().setAttribute("role", "table");
for (TableWidget part : new TableWidget[] {tableHeader, tableData, tableFooter}) {
part.tableElem.setAttribute("role", "presentation");
part.section.setAttribute("role", "rowgroup");
}
emptyTableWidgetContainer.getElement().setAttribute("role", "presentation");
loadingIndicatorContainer.getElement().setAttribute("role", "presentation");

// Set the loading indicator.
setLoadingIndicator(loadingIndicator); // Can be null.

Expand Down Expand Up @@ -724,13 +793,13 @@ public void setAlwaysShowScrollBars(boolean alwaysShowScrollBars) {

@Override
public void setEmptyTableWidget(Widget widget) {
emptyTableWidgetContainer.setWidget(0, 0, widget);
setMessageWidget(emptyTableWidgetContainer, widget);
super.setEmptyTableWidget(widget);
}

@Override
public void setLoadingIndicator(Widget widget) {
loadingIndicatorContainer.setWidget(0, 0, widget);
setMessageWidget(loadingIndicatorContainer, widget);
super.setLoadingIndicator(widget);
}

Expand Down Expand Up @@ -858,6 +927,11 @@ protected void onLoadingStateChanged(LoadingState state) {
super.onLoadingStateChanged(state);
}

@Override
protected void onSectionRendered(TableSectionElement section) {
applyTableRoles(section);
}

@Override
protected void refreshColumnWidths() {
super.refreshColumnWidths();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1224,7 +1224,7 @@ public void testSortableHeaderAria() {

// The sortable th keeps its columnheader role; the button is inside it.
TableCellElement sortableTh = getHeaderElement(table, 0);
assertFalse(sortableTh.hasAttribute("role"));
assertHeaderCellRole(sortableTh);
assertFalse(sortableTh.hasAttribute("aria-sort"));
Element sortButton = sortableTh.getFirstChildElement();
assertEquals("button", sortButton.getAttribute("role"));
Expand All @@ -1233,7 +1233,7 @@ public void testSortableHeaderAria() {

// A column that isn't sortable gets no button.
TableCellElement plainTh = getHeaderElement(table, 1);
assertFalse(plainTh.hasAttribute("role"));
assertHeaderCellRole(plainTh);
assertFalse(plainTh.hasAttribute("aria-sort"));
assertNull(plainTh.getFirstChildElement());
assertEquals("Column 1", plainTh.getInnerText());
Expand Down Expand Up @@ -1374,6 +1374,23 @@ public String getValue(String object) {
return table;
}

/**
* Get the role attribute a header th should carry: null for none (an
* implicit columnheader), or the explicit role the table stamps on it.
*/
protected String getHeaderCellRole() {
return null;
}

private void assertHeaderCellRole(TableCellElement th) {
String role = getHeaderCellRole();
if (role == null) {
assertFalse(th.hasAttribute("role"));
} else {
assertEquals(role, th.getAttribute("role"));
}
}

/**
* Get a td element from the table body.
*
Expand Down
Loading
Loading