+ NOTE: This section is not complete.
+ Development of content for this pattern is tracked by
+ issue 91.
+
A treegrid widget presents a hierarchical data grid consisting of tabular information that is editable or interactive.
Any row in the hierarchy may have child rows, and rows with children may be expanded or collapsed to show or hide the children.
For example, a hierarchical e-mail discussion list uses a treegrid to display messages and responses to that message, a message row with responses can be expanded to reveal the response messages.
-
- In a treegrid both rows and cells are focusable. Every row and cell contains a focusable element or is itself focusable, regardless of whether individual cell content is editable or interactive.
- There is one exception: if column cells do not provide functions, such as sort or filter, they do not need to be focusable.
- One reason it is important for all cells to be able to receive or contain keyboard focus is that screen readers will typically be in their application reading mode, rather than their document reading mode, when users are interacting with the grid.
- While in application mode, a screen reader user hears only focusable elements and content that labels focusable elements.
- So, screen reader users may unknowingly overlook elements contained in a treegrid that are either not focusable or not used to label a column or row.
-
+
+ In a treegrid both rows and cells are focusable. Every row and cell contains a focusable element or is itself focusable, regardless of whether individual cell content is editable or interactive.
+ There is one exception: if column cells do not provide functions, such as sort or filter, they do not need to be focusable.
+ One reason it is important for all cells to be able to receive or contain keyboard focus is that screen readers will typically be in their application reading mode, rather than their document reading mode, when users are interacting with the grid.
+ While in application mode, a screen reader user hears only focusable elements and content that labels focusable elements.
+ So, screen reader users may unknowingly overlook elements contained in a treegrid that are either not focusable or not used to label a column or row.
+
When using a keyboard to navigate a treegrid, a visual keyboard indicator informs the user which row or cell is focused.
If the treegrid allows the user to choose just one item for an action, then it is known as a single-select treegrid, and the item with focus also has a selected state.
@@ -2771,152 +2776,152 @@
- The following keys provide treegrid navigation by moving focus among rows and cells of the grid.
- Implementations of treegrid make these key commands available when an element in the grid has received focus, e.g., after a user has moved focus to the grid with Tab. Moving focus into the grid may result in the first cell or the first row being focused. Whether focus goes to a cell or the row depends on author preferences and whether row focus is supported, since some treegrids may not provide focus to rows.
-
-
-
- Enter: If cell only focus is enabled and focus is on the first cell with the aria-expanded property it will will open or close the child rows, otherwise performs the default action for the cell.
-
- Tab: If there are focusable elements in the current row (e.g. inputs, buttons, links, ..) focus moves to the next input in the row. If on the last input in the row, focus moves out of the treegrid widget to the next focusable control on the page.
-
- Right Arrow:
-
-
If focus is on a row and the row is expandable, but not expanded, the row is expanded.
-
If focus is on a row and the row is not expandable or is expanded, focus moves to the first cell in the row.
-
If focus is on the right-most cell in the row, focus does not move.
-
If focus is on any other cell, focus one cell to the right.
-
-
-
Left Arrow:
-
-
If focus is on a row and the row is expanded, the row is collapsed.
-
If focus is on a row and the row is not expandable or is collapsed, focus does not move.
-
If focus is on a first cell in the row and row focus is supported, focus moves to the row.
-
If focus is on a first cell in the row and row focus is not supported,focus does not move.
-
If focus is on a any other cell, focus one cell to the left.
-
-
-
- Down Arrow:
-
-
If focus is on a row, moves focus one row down. If focus is on the last row, focus does not move.
-
If focus is on a cell, moves focus one cell down. If focus is on the bottom cell in the column, focus does not move.
-
-
- Up Arrow:
-
-
If focus is on a row, moves focus one row up. If focus is on the first row, focus does not move.
-
If focus is on a cell, moves focus one cell up. If focus is on the top cell in the column, focus does not move.
-
-
- Page Down:
-
-
If focus is on a row, moves focus down an author-determined number of rows, typically scrolling so the bottom row in the currently visible set of rows becomes one of the first visible rows. If focus is in the last row of the grid, focus does not move.
-
If focus is on a cell, moves focus down an author-determined number of cells, typically scrolling so the bottom row in the currently visible set of rows becomes one of the first visible rows. If focus is in the last row of the grid, focus does not move.
-
-
-
- Page Up:
-
-
If focus is on a row, moves focus up an author-determined number of rows, typically scrolling so the top row in the currently visible set of rows becomes one of the last visible rows. If focus is in the first row of the grid, focus does not move.
-
If focus is on a cell, moves focus up an author-determined number of cells, typically scrolling so the top row in the currently visible set of rows becomes one of the last visible rows. If focus is in the first row of the grid, focus does not move.
-
-
-
- Home:
-
-
If focus is on a row, moves focus up to the first row. If focus is in the first row of the grid, focus does not move.
-
If focus is on a cell, moves focus to the first cell in the row. If focus is in the first cell of the row, focus does not move.
-
-
-
- End:
-
-
If focus is on a row, moves focus to the last row. If focus is in the last row of the grid, focus does not move.
-
If focus is on a cell, moves focus to the last cell in the row. If focus is in the last cell of the row, focus does not move.
-
-
-
- Control + Home:
-
-
If focus is on a row, moves focus up to the first row. If focus is in the first row of the grid, focus does not move.
-
If focus is on a cell, moves focus to the first cell in the column. If focus is in the first row of the grid, focus does not move.
-
-
-
- Control + End:
-
-
If focus is on a row, moves focus to the last row. If focus is in the last row of the grid, focus does not move.
-
If focus is on a cell, moves focus to the last cell in the column. If focus is in the last row of the grid, focus does not move.
-
-
-
-
-
- When the above treegrid navigation keys move focus, whether the focus is set on an element inside the cell or the grid cell depends on cell content.
- See Whether to Focus on a Cell or an Element Inside It.
-
-
- While navigation keys, such as arrow keys, are moving focus from cell to cell, they are not available to do something like operate a combobox or move an editing caret inside of a cell.
- If this functionality is needed, see Editing and Navigating Inside a Cell.
-
-
If navigation functions can dynamically add more rows or columns to the DOM, key events that move focus to the beginning or end of the grid, such as control + End, may move focus to the last row in the DOM rather than the last available row in the back-end data.
-
-
If a grid supports selection of cells, rows, or columns, the following keys are commonly used for these functions.
-
-
- Control + Space:
-
-
If focus on a row, selects all cells.
-
If focus is on a call, selects the column that contains the focus.
-
-
-
- Shift + Space:
-
-
If focus on a row, no change in selection.
-
If focus on a cell, selects the row that contains the focus. If the grid includes a column with checkboxes for selecting rows, this key can serve as a shortcut for checking the box when focus is not on the checkbox.
+
+
Keyboard Interaction For Data Grids
+
+ The following keys provide treegrid navigation by moving focus among rows and cells of the grid.
+ Implementations of treegrid make these key commands available when an element in the grid has received focus, e.g., after a user has moved focus to the grid with Tab. Moving focus into the grid may result in the first cell or the first row being focused. Whether focus goes to a cell or the row depends on author preferences and whether row focus is supported, since some treegrids may not provide focus to rows.
+
+
+
+ Enter: If cell only focus is enabled and focus is on the first cell with the aria-expanded property it will will open or close the child rows, otherwise performs the default action for the cell.
+
+ Tab: If there are focusable elements in the current row (e.g. inputs, buttons, links, ..) focus moves to the next input in the row. If on the last input in the row, focus moves out of the treegrid widget to the next focusable control on the page.
+
+ Right Arrow:
+
+
If focus is on a row and the row is expandable, but not expanded, the row is expanded.
+
If focus is on a row and the row is not expandable or is expanded, focus moves to the first cell in the row.
+
If focus is on the right-most cell in the row, focus does not move.
+
If focus is on any other cell, focus one cell to the right.
+
+
+
Left Arrow:
+
+
If focus is on a row and the row is expanded, the row is collapsed.
+
If focus is on a row and the row is not expandable or is collapsed, focus does not move.
+
If focus is on a first cell in the row and row focus is supported, focus moves to the row.
+
If focus is on a first cell in the row and row focus is not supported,focus does not move.
+
If focus is on a any other cell, focus one cell to the left.
+
+
+
+ Down Arrow:
+
+
If focus is on a row, moves focus one row down. If focus is on the last row, focus does not move.
+
If focus is on a cell, moves focus one cell down. If focus is on the bottom cell in the column, focus does not move.
+
+
+ Up Arrow:
+
+
If focus is on a row, moves focus one row up. If focus is on the first row, focus does not move.
+
If focus is on a cell, moves focus one cell up. If focus is on the top cell in the column, focus does not move.
+
+
+ Page Down:
+
+
If focus is on a row, moves focus down an author-determined number of rows, typically scrolling so the bottom row in the currently visible set of rows becomes one of the first visible rows. If focus is in the last row of the grid, focus does not move.
+
If focus is on a cell, moves focus down an author-determined number of cells, typically scrolling so the bottom row in the currently visible set of rows becomes one of the first visible rows. If focus is in the last row of the grid, focus does not move.
+
+
+
+ Page Up:
+
+
If focus is on a row, moves focus up an author-determined number of rows, typically scrolling so the top row in the currently visible set of rows becomes one of the last visible rows. If focus is in the first row of the grid, focus does not move.
+
If focus is on a cell, moves focus up an author-determined number of cells, typically scrolling so the top row in the currently visible set of rows becomes one of the last visible rows. If focus is in the first row of the grid, focus does not move.
-
-
Control + A: Selects all cells.
-
- Shift + Right Arrow:
-
-
If focus on a row, no change in selection.
-
if focus on a cell, extends selection one cell to the right.
+
+
+ Home:
+
+
If focus is on a row, moves focus up to the first row. If focus is in the first row of the grid, focus does not move.
+
If focus is on a cell, moves focus to the first cell in the row. If focus is in the first cell of the row, focus does not move.
-
-
- Shift + Left Arrow:
-
-
If focus on a row, no change in selection.
-
if focus on a cell, extends selection one cell to the left.
+
+
+ End:
+
+
If focus is on a row, moves focus to the last row. If focus is in the last row of the grid, focus does not move.
+
If focus is on a cell, moves focus to the last cell in the row. If focus is in the last cell of the row, focus does not move.
-
-
- Shift + Down Arrow:
-
-
If focus on a row, extends selection to all the cells in the next row.
-
If focus on a cell, extends selection one cell down.
+
+
+ Control + Home:
+
+
If focus is on a row, moves focus up to the first row. If focus is in the first row of the grid, focus does not move.
+
If focus is on a cell, moves focus to the first cell in the column. If focus is in the first row of the grid, focus does not move.
-
-
- Shift + Up Arrow:
-
-
If focus on a row, extends selection to all the cells in the previous row.
-
If focus on a cell, extends selection one cell up.
+
+
+ Control + End:
+
+
If focus is on a row, moves focus to the last row. If focus is in the last row of the grid, focus does not move.
+
If focus is on a cell, moves focus to the last cell in the column. If focus is in the last row of the grid, focus does not move.
-
-
-
See for cut, copy, and paste key assignments.
-
+
+
+
+
+ When the above treegrid navigation keys move focus, whether the focus is set on an element inside the cell or the grid cell depends on cell content.
+ See Whether to Focus on a Cell or an Element Inside It.
+
+
+ While navigation keys, such as arrow keys, are moving focus from cell to cell, they are not available to do something like operate a combobox or move an editing caret inside of a cell.
+ If this functionality is needed, see Editing and Navigating Inside a Cell.
+
+
If navigation functions can dynamically add more rows or columns to the DOM, key events that move focus to the beginning or end of the grid, such as control + End, may move focus to the last row in the DOM rather than the last available row in the back-end data.
+
+
If a grid supports selection of cells, rows, or columns, the following keys are commonly used for these functions.
+
+
+ Control + Space:
+
+
If focus on a row, selects all cells.
+
If focus is on a call, selects the column that contains the focus.
+
+
+
+ Shift + Space:
+
+
If focus on a row, no change in selection.
+
If focus on a cell, selects the row that contains the focus. If the grid includes a column with checkboxes for selecting rows, this key can serve as a shortcut for checking the box when focus is not on the checkbox.
+
+
+
Control + A: Selects all cells.
+
+ Shift + Right Arrow:
+
+
If focus on a row, no change in selection.
+
if focus on a cell, extends selection one cell to the right.
+
+
+
+ Shift + Left Arrow:
+
+
If focus on a row, no change in selection.
+
if focus on a cell, extends selection one cell to the left.
+
+
+
+ Shift + Down Arrow:
+
+
If focus on a row, extends selection to all the cells in the next row.
+
If focus on a cell, extends selection one cell down.
+
+
+
+ Shift + Up Arrow:
+
+
If focus on a row, extends selection to all the cells in the previous row.
+
If focus on a cell, extends selection one cell up.
+
+
+
+
See for cut, copy, and paste key assignments.
-
+
WAI-ARIA Roles, States, and Properties
@@ -2976,7 +2981,6 @@
WAI-ARIA Roles, States, and Properties
-
diff --git a/examples/treegrid/css/expand-icon-highlighted.svg b/examples/treegrid/css/expand-icon-highlighted.svg
new file mode 100644
index 0000000000..4ee2b031f4
--- /dev/null
+++ b/examples/treegrid/css/expand-icon-highlighted.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/treegrid/css/expand-icon.svg b/examples/treegrid/css/expand-icon.svg
new file mode 100644
index 0000000000..f3ad2c5bec
--- /dev/null
+++ b/examples/treegrid/css/expand-icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/examples/treegrid/css/treegrid-1.css b/examples/treegrid/css/treegrid-1.css
new file mode 100644
index 0000000000..7be628de02
--- /dev/null
+++ b/examples/treegrid/css/treegrid-1.css
@@ -0,0 +1,103 @@
+#treegrid {
+ width: 100%;
+ white-space: nowrap;
+ border-collapse: collapse;
+ table-layout: fixed;
+}
+
+#treegrid tr {
+ display: table-row;
+ cursor: default;
+}
+
+#treegrid tbody td {
+ cursor: default;
+}
+
+#treegrid-col1, #treegrid-col3 {
+ width: 30%;
+}
+
+#treegrid th {
+ text-align: left;
+ background-color: #eee;
+}
+
+/* Extra space between columns for readability */
+#treegrid th, #treegrid td {
+ padding-bottom: 3px;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+}
+
+#treegrid tr > td:not(:first-child),
+#treegrid tr > th:not(:first-child) {
+ padding-left: 3ch;
+}
+
+#treegrid a {
+ padding-left: .25ch;
+ padding-right: .25ch;
+}
+
+#treegrid tr:focus,
+#treegrid td:focus,
+#treegrid a:focus {
+ outline: 2px solid hsl(216, 94%, 70%);
+ background-color: hsl(216, 80%, 97%);
+}
+
+#treegrid a:focus {
+ border-bottom: none;
+}
+
+/* Hide collapsed rows */
+#treegrid tr.hidden {
+ display: none;
+}
+
+/* Indents */
+#treegrid tr[aria-level="2"] > td:first-child {
+ padding-left: 2.5ch;
+}
+#treegrid tr[aria-level="3"] > td:first-child {
+ padding-left: 5ch;
+}
+#treegrid tr[aria-level="4"] > td:first-child {
+ padding-left: 7.5ch;
+}
+#treegrid tr[aria-level="5"] > td:first-child {
+ padding-left: 10ch;
+}
+
+
+/* Collapse/expand icons */
+#treegrid tr > td:first-child::before {
+ font-family: monospace;
+ content: " ";
+ display: inline-block;
+ width: 2ch;
+ height: 11px;
+ transition: transform .3s;
+ transform-origin: 5px 5px;
+}
+
+#treegrid tr[aria-expanded] > td:first-child::before,
+#treegrid td[aria-expanded]:first-child::before {
+ cursor: pointer;
+ /* Load both right away so there is no lag when we need the other */
+ background-image: url("expand-icon.svg"), url("expand-icon-highlighted.svg");
+ background-repeat: no-repeat;
+}
+
+#treegrid tr[aria-expanded]:focus > td:first-child::before,
+#treegrid tr[aria-expanded] > td:focus:first-child::before,
+#treegrid tr:focus > td[aria-expanded]:first-child::before,
+#treegrid tr > td[aria-expanded]:focus:first-child::before {
+ background-image: url("expand-icon-highlighted.svg");
+}
+
+#treegrid tr[aria-expanded="true"] > td:first-child::before,
+#treegrid td[aria-expanded="true"]:first-child::before {
+ transform: rotate(90deg);
+}
diff --git a/examples/treegrid/js/treegrid-1.js b/examples/treegrid/js/treegrid-1.js
new file mode 100644
index 0000000000..6519a779ac
--- /dev/null
+++ b/examples/treegrid/js/treegrid-1.js
@@ -0,0 +1,518 @@
+/* exported TreeGrid */
+function TreeGrid (treegridElem, doAllowRowFocus, doStartRowFocus) {
+ function initAttributes () {
+ // Make sure focusable elements are not in the tab order
+ // They will be added back in for the active row
+ setTabIndexOfFocusableElems(treegridElem, -1);
+
+ // Add tabindex="0" to first row, "-1" to other rows
+ // We will use the roving tabindex method since aria-activedescendant
+ // does not work in IE
+ var rows = getAllRows();
+ var index = rows.length;
+ var startRowIndex = doStartRowFocus ? 0 : -1;
+
+ while (index--) {
+ if (doAllowRowFocus) {
+ rows[index].tabIndex = index === startRowIndex ? 0 : -1;
+ }
+ else {
+ setTabIndexForCellsInRow(rows[index], -1);
+ moveAriaExpandedToFirstCell(rows[index]);
+ }
+ }
+
+ if (doStartRowFocus) {
+ return;
+ }
+
+ // Start with cell focus
+ var firstCell = getNavigableCols(rows[0])[0];
+ setTabIndexForCell(firstCell);
+ }
+
+ function setTabIndexForCell (cell, tabIndex) {
+ var focusable = getFocusableElems(cell)[0] || cell;
+ focusable.tabIndex = tabIndex;
+ }
+
+ function setTabIndexForCellsInRow (row, tabIndex) {
+ var cells = getNavigableCols(row);
+ var cellIndex = cells.length;
+ while (cellIndex--) {
+ setTabIndexForCell(cells[cellIndex], tabIndex);
+ }
+ }
+
+ function getAllRows () {
+ var nodeList = treegridElem.querySelectorAll('tbody > tr');
+ return Array.prototype.slice.call(nodeList);
+ }
+
+ function getFocusableElems (root) {
+ // textarea not supported as a cell widget as it's multiple lines
+ // and needs up/down keys
+ // These should all be descendants of a cell
+ var nodeList = root.querySelectorAll('a,button,input,td>[tabindex]');
+ return Array.prototype.slice.call(nodeList);
+ }
+
+ function setTabIndexOfFocusableElems (root, tabIndex) {
+ var focusableElems = getFocusableElems(root);
+ var index = focusableElems.length;
+ while (index--) {
+ focusableElems[index].tabIndex = tabIndex;
+ }
+ }
+
+ function getAllNavigableRows () {
+ var nodeList = treegridElem.querySelectorAll('tbody > tr:not([class~="hidden"])');
+ // Convert to array so that we can use array methods on it
+ return Array.prototype.slice.call(nodeList);
+ }
+
+ function getNavigableCols (currentRow) {
+ var nodeList = currentRow.getElementsByTagName('td');
+ return Array.prototype.slice.call(nodeList);
+ }
+
+ function restrictIndex (index, numItems) {
+ if (index < 0) {
+ return 0;
+ }
+ return index >= numItems ? index - 1 : index;
+ }
+
+ function focus (elem) {
+ elem.tabIndex = 0; // Ensure focusable
+ elem.focus();
+ }
+
+ function focusCell (cell) {
+ // Check for focusable child such as link or textbox
+ // and use that if available
+ var focusableChildren = getFocusableElems(cell);
+ focus(focusableChildren[0] || cell);
+ }
+
+ // Restore tabIndex to what it should be when focus switches from
+ // one treegrid item to another
+ function onFocusIn (event) {
+ var newTreeGridFocus =
+ event.target !== window && treegridElem.contains(event.target) &&
+ event.target;
+
+ // The last row we considered focused
+ var oldCurrentRow = enableTabbingInActiveRowDescendants.tabbingRow;
+ if (oldCurrentRow) {
+ enableTabbingInActiveRowDescendants(false, oldCurrentRow);
+ }
+ if (doAllowRowFocus && onFocusIn.prevTreeGridFocus &&
+ onFocusIn.prevTreeGridFocus.localName === 'td') {
+ // Was focused on td, remove tabIndex so that it's not focused on click
+ onFocusIn.prevTreeGridFocus.removeAttribute('tabindex');
+ }
+
+ if (newTreeGridFocus) {
+ // Stayed in treegrid
+ if (oldCurrentRow) {
+ // There will be a different current row that will be
+ // the tabbable one
+ oldCurrentRow.tabIndex = -1;
+ }
+
+ // The new row
+ var currentRow = getRowWithFocus();
+ if (currentRow) {
+ currentRow.tabIndex = 0;
+ // Items within current row are also tabbable
+ enableTabbingInActiveRowDescendants(true, currentRow);
+ }
+ }
+
+ onFocusIn.prevTreeGridFocus = newTreeGridFocus;
+ }
+
+ // Set whether interactive elements within a row are tabbable
+ function enableTabbingInActiveRowDescendants (isTabbingOn, row) {
+ if (row) {
+ setTabIndexOfFocusableElems(row, isTabbingOn ? 0 : -1);
+ if (isTabbingOn) {
+ enableTabbingInActiveRowDescendants.tabbingRow = row;
+ }
+ else {
+ if (enableTabbingInActiveRowDescendants.tabbingRow === row) {
+ enableTabbingInActiveRowDescendants.tabbingRow = null;
+ }
+ }
+ }
+ }
+
+ // The row with focus is the row that either has focus or an element
+ // inside of it has focus
+ function getRowWithFocus () {
+ return getContainingRow(document.activeElement);
+ }
+
+ function getContainingRow (start) {
+ var possibleRow = start;
+ if (treegridElem.contains(possibleRow)) {
+ while (possibleRow !== treegridElem) {
+ if (possibleRow.localName === 'tr') {
+ return possibleRow;
+ }
+ possibleRow = possibleRow.parentElement;
+ }
+ }
+ }
+
+ function isRowFocused () {
+ return getRowWithFocus() === document.activeElement;
+ }
+
+ // Note: contenteditable not currently supported
+ function isEditableFocused () {
+ var focusedElem = document.activeElement;
+ return focusedElem.localName === 'input';
+ }
+
+ function getColWithFocus (currentRow) {
+ if (currentRow) {
+ var possibleCol = document.activeElement;
+ if (currentRow.contains(possibleCol)) {
+ while (possibleCol !== currentRow) {
+ if (possibleCol.localName === 'td') {
+ return possibleCol;
+ }
+ possibleCol = possibleCol.parentElement;
+ }
+ }
+ }
+ }
+
+ function getLevel (row) {
+ return row && parseInt(row.getAttribute('aria-level'));
+ }
+
+ // Move backwards (direction = -1) or forwards (direction = 1)
+ // If we also need to move down/up a level, requireLevelChange = true
+ // When
+ function moveByRow (direction, requireLevelChange) {
+ var currentRow = getRowWithFocus();
+ var requiredLevel = requireLevelChange && currentRow &&
+ getLevel(currentRow) + direction;
+ var rows = getAllNavigableRows();
+ var numRows = rows.length;
+ var rowIndex = currentRow ? rows.indexOf(currentRow) : -1;
+ // When moving down a level, only allow moving to next row as the
+ // first child will never be farther than that
+ var maxDistance = requireLevelChange && direction === 1 ? 1 : NaN;
+
+ // Move in direction until required level is found
+ do {
+ if (maxDistance-- === 0) {
+ return; // Failed to find required level, return without focus change
+ }
+ rowIndex = restrictIndex(rowIndex + direction, numRows);
+ }
+ while (requiredLevel && requiredLevel !== getLevel(rows[rowIndex]));
+
+ if (!focusSameColInDifferentRow(currentRow, rows[rowIndex])) {
+ focus(rows[rowIndex]);
+ }
+ }
+
+ function focusSameColInDifferentRow (fromRow, toRow) {
+ var currentCol = getColWithFocus(fromRow);
+ if (!currentCol) {
+ return;
+ }
+
+ var fromCols = getNavigableCols(fromRow);
+ var currentColIndex = fromCols.indexOf(currentCol);
+
+ if (currentColIndex < 0) {
+ return;
+ }
+
+ var toCols = getNavigableCols(toRow);
+ // Focus the first focusable element inside the
+ focusCell(toCols[currentColIndex]);
+ return true;
+ }
+
+ function moveToExtreme (direction) {
+ var currentRow = getRowWithFocus();
+ if (!currentRow) {
+ return;
+ }
+ var currentCol = getColWithFocus(currentRow);
+ if (currentCol) {
+ moveToExtremeCol(direction, currentRow);
+ }
+ else {
+ // Move to first/last row
+ moveToExtremeRow(direction);
+ }
+ }
+
+ function moveByCol (direction) {
+ var currentRow = getRowWithFocus();
+ if (!currentRow) {
+ return;
+ }
+ var cols = getNavigableCols(currentRow);
+ var numCols = cols.length;
+ var currentCol = getColWithFocus(currentRow);
+ var currentColIndex = cols.indexOf(currentCol);
+ // First right arrow moves to first column
+ var newColIndex = (currentCol || direction < 0) ? currentColIndex +
+ direction : 0;
+ // Moving past beginning focuses row
+ if (doAllowRowFocus && newColIndex < 0) {
+ focus(currentRow);
+ return;
+ }
+ newColIndex = restrictIndex(newColIndex, numCols);
+ focusCell(cols[newColIndex]);
+ }
+
+ function moveToExtremeCol (direction, currentRow) {
+ // Move to first/last col
+ var cols = getNavigableCols(currentRow);
+ var desiredColIndex = direction < 0 ? 0 : cols.length - 1;
+ focusCell(cols[desiredColIndex]);
+ }
+
+ function moveToExtremeRow (direction) {
+ var rows = getAllNavigableRows();
+ var newRow = rows[direction > 0 ? rows.length - 1 : 0];
+ if (!focusSameColInDifferentRow(getRowWithFocus(), newRow)) {
+ focus(newRow);
+ }
+ }
+
+ function doPrimaryAction () {
+ var currentRow = getRowWithFocus();
+ if (!currentRow) {
+ return;
+ }
+
+ // If row has focus, open message
+ if (currentRow === document.activeElement) {
+ alert('Message from ' + currentRow.children[2].innerText + ':\n\n' +
+ currentRow.children[1].innerText);
+ return;
+ }
+
+ // If first col has focused, toggle expand/collapse
+ toggleExpanded(currentRow);
+ }
+
+ function toggleExpanded (row) {
+ var cols = getNavigableCols(row);
+ var currentCol = getColWithFocus(row);
+ if (currentCol === cols[0] && isExpandable(row)) {
+ changeExpanded(!isExpanded(row), row);
+ }
+ }
+
+ function changeExpanded (doExpand, row) {
+ var currentRow = row || getRowWithFocus();
+ if (!currentRow) {
+ return;
+ }
+ var currentLevel = getLevel(currentRow);
+ var rows = getAllRows();
+ var currentRowIndex = rows.indexOf(currentRow);
+ var didChange;
+ var doExpandLevel = [];
+ doExpandLevel[currentLevel + 1] = doExpand;
+
+ while (++currentRowIndex < rows.length) {
+ var nextRow = rows[currentRowIndex];
+ var rowLevel = getLevel(nextRow);
+ if (rowLevel <= currentLevel) {
+ break; // Next row is not a level down from current row
+ }
+ // Only expand the next level if this level is expanded
+ // and previous level is expanded
+ doExpandLevel[rowLevel + 1] =
+ doExpandLevel[rowLevel] &&
+ isExpanded(nextRow);
+ var willHideRow = !doExpandLevel[rowLevel];
+ var isRowHidden = nextRow.classList.contains('hidden');
+
+ if (willHideRow !== isRowHidden) {
+ if (willHideRow) {
+ nextRow.classList.add('hidden');
+ }
+ else {
+ nextRow.classList.remove('hidden');
+ }
+ didChange = true;
+ }
+ }
+ if (didChange) {
+ setAriaExpanded(currentRow, doExpand);
+ return true;
+ }
+ }
+
+ // Mirror aria-expanded from the row to the first cell in that row
+ // (TBD is this a good idea? How else will screen reader user hear
+ // that the cell represents the opportunity to collapse/expand rows?)
+ function moveAriaExpandedToFirstCell (row) {
+ var expandedValue = row.getAttribute('aria-expanded');
+ var firstCell = getNavigableCols(row)[0];
+ if (expandedValue) {
+ firstCell.setAttribute('aria-expanded', expandedValue);
+ row.removeAttribute('aria-expanded');
+ }
+ }
+
+ function getAriaExpandedElem (row) {
+ return doAllowRowFocus ? row : getNavigableCols(row)[0];
+ }
+
+ function setAriaExpanded (row, doExpand) {
+ var elem = getAriaExpandedElem(row);
+ elem.setAttribute('aria-expanded', doExpand);
+ }
+
+ function isExpandable (row) {
+ var elem = getAriaExpandedElem(row);
+ return elem.hasAttribute('aria-expanded');
+ }
+
+ function isExpanded (row) {
+ var elem = getAriaExpandedElem(row);
+ return elem.getAttribute('aria-expanded') === 'true';
+ }
+
+ function onKeyDown (event) {
+ var ENTER = 13;
+ var UP = 38;
+ var DOWN = 40;
+ var LEFT = 37;
+ var RIGHT = 39;
+ var HOME = 36;
+ var END = 35;
+ var CTRL_HOME = -HOME;
+ var CTRL_END = -END;
+
+ var numModifiersPressed = event.ctrlKey + event.altKey + event.shiftKey +
+ event.metaKey;
+
+ var key = event.keyCode;
+
+ if (numModifiersPressed === 1 && event.ctrlKey) {
+ key = -key; // Treat as negative key value when ctrl pressed
+ }
+ else if (numModifiersPressed) {
+ return;
+ }
+
+ switch (key) {
+ case DOWN:
+ moveByRow(1);
+ break;
+ case UP:
+ moveByRow(-1);
+ break;
+ case LEFT:
+ if (isEditableFocused()) {
+ return; // Leave key for editable area
+ }
+ if (isRowFocused()) {
+ changeExpanded(false) || moveByRow(-1, true);
+ }
+ else {
+ moveByCol(-1);
+ }
+ break;
+ case RIGHT:
+ if (isEditableFocused()) {
+ return; // Leave key for editable area
+ }
+
+ // If row: try to expand
+ // If col or can't expand, move column to right
+ if (!isRowFocused() || !changeExpanded(true)) {
+ moveByCol(1);
+ }
+ break;
+ case CTRL_HOME:
+ moveToExtremeRow(-1);
+ break;
+ case HOME:
+ if (isEditableFocused()) {
+ return; // Leave key for editable area
+ }
+ moveToExtreme(-1);
+ break;
+ case CTRL_END:
+ moveToExtremeRow(1);
+ break;
+ case END:
+ if (isEditableFocused()) {
+ return; // Leave key for editable area
+ }
+ moveToExtreme(1);
+ break;
+ case ENTER:
+ doPrimaryAction();
+ break;
+ default:
+ return;
+ }
+
+ // Important: don't use key for anything else, such as scrolling
+ event.preventDefault();
+ }
+
+ // Toggle row expansion if the click is over the expando triangle
+ // Since the triangle is a pseudo element we can't bind an event listener
+ // to it. Another option is to have an actual element with role="presentation"
+ function onClick (event) {
+ var target = event.target;
+ if (target.localName !== 'td') {
+ return;
+ }
+
+ var row = getContainingRow(event.target);
+ if (!isExpandable(row)) {
+ return;
+ }
+
+ // Determine if mouse coordinate is just to the left of the start of text
+ var range = document.createRange();
+ range.selectNodeContents(target.firstChild);
+ var left = range.getBoundingClientRect().left;
+ var EXPANDO_WIDTH = 20;
+
+ if (event.clientX < left && event.clientX > left - EXPANDO_WIDTH) {
+ changeExpanded(!isExpanded(row), row);
+ }
+ }
+
+ // Double click on row toggles expansion
+ function onDoubleClick (event) {
+ var row = getContainingRow(event.target);
+ if (row) {
+ if (isExpandable(row)) {
+ changeExpanded(!isExpanded(row), row);
+ }
+ event.preventDefault();
+ }
+ }
+
+ initAttributes();
+ treegridElem.addEventListener('keydown', onKeyDown);
+ treegridElem.addEventListener('click', onClick);
+ treegridElem.addEventListener('dblclick', onDoubleClick);
+ // Polyfill for focusin necessary for Firefox < 52
+ window.addEventListener(window.onfocusin ? 'focusin' : 'focus',
+ onFocusIn, true);
+}
+
diff --git a/examples/treegrid/treegrid-1.html b/examples/treegrid/treegrid-1.html
new file mode 100644
index 0000000000..5a7c20f333
--- /dev/null
+++ b/examples/treegrid/treegrid-1.html
@@ -0,0 +1,526 @@
+
+
+
+
+
+Treegrid Email Inbox Example | WAI-ARIA Authoring Practices 1.1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Treegrid Email Inbox Example
+
+ NOTE: This example page is not yet complete.
+ Development of this example is tracked by
+ issue 132.
+
+
+ The following example demonstrates how the
+ treegrid design pattern
+ can be used to make an interactive tree that enables users to both navigate the hierarchical structure of email conversations
+ and also navigate elements that describe each email, such as subject and sender.
+
+
Similar examples include:
+
+
Data Grid Examples: Three example implementations of grid that include features relevant to presenting tabular information, such as content editing, sort, and column hiding.
+ This example demonstrates three different ways of implementing the keyboard navigation specified in the treegrid pattern.
+ The following links change the behavior of the navigation keys :
+
+
+
+ Rows are focused first, but cells can be focused:
+ Useful when the desired experience is for the treegrid to act primarily like a tree where each row is treated like a node in a tree,
+ but it is still possible for users to navigate across the cells in a row.
+
+
+ Cells are focused first, but rows can be focused:
+ Useful when the desired experience is for the treegrid to act primarily like a grid where arrow keys move among cells,
+ but it is still possible for users to focus a row and then start navigating the structure like a tree.
+
+
+ Only cells can be focused:
+ Useful when the desired experience is that the treegrid act primarily like a grid and there is no need to focus complete rows.
+
+
+
+ Note: A row-only option is not provided.
+ A treegrid where cells cannot be focused would be implemented as a tree view.
+ A treeview that has columns in its visual presentation may be appropriate when all the following conditions are present:
+
+
+
Columns are guaranteed to fit horizontally (no horizontal scrolling necessary).
+
Columns are merely for attractive presentation; there is no need to navigate them individually.
+
A screen reader user can easily understand the UI when all information in a row is announced as a single string without any separation.
If a row is focused, and it is collapsed, expands the current row.
+
If a row is focused, and it is expanded, focuses the first cell in the row.
+
If a cell is focused, moves one cell to the right.
+
If focus is on the right most cell, focus does not move.
+
+
+
+
+
+ Left Arrow
+
+
+
+
If a row is focused, and it is expanded, collapses the current row.
+
If a row is focused, and it is collapsed, moves to the parent row (if there is one).
+
If a cell in the first column is focused, focuses the row.
+
If a cell in a different column is focused, moves focus one cell to the left.
+
+
+
+
+
+ Down Arrow
+
+
+
+
Moves focus one row or one cell down, depending on whether a row or cell is currently focused.
+
If focus is on the bottom row, focus does not move.
+
+
+
+
+
+ Up Arrow
+
+
+
+
Moves focus one row or one cell up, depending on whether a row or cell is currently focused.
+
If focus is on the top row, focus does not move.
+
+
+
+
+
+ Tab
+
+
+
+
Moves focus to the next interactive cell in the current row.
+
If there are no more interactive cells in the current row,
+ moves out of the treegrid.
+
+
+
+
+
+ Shift + Tab
+
+
+
+
If a cell is focused, moves focus to the previous interactive cell in the current row.
+
If a row is focused, moves out of the treegrid.
+
+
+
+
+
+ Home
+
+
+
+
If a row is focused, moves to the first row.
+
If a cell is focused, moves focus to the row's first cell.
+
+
+
+
+
+ End
+
+
+
+
If a row is focused, moves to the last row.
+
If a cell is focused, moves focus to the row's last cell.
+
+
+
+
+
+ Control + Home
+
+
+
+
Moves focus to the first row.
+
If a cell was focused, focus is kept in the same column,
+ otherwise the entire first row is focused.
+
+
+
+
+
+ Control + End
+
+
+
+
Moves focus to the last row.
+
If a cell was focused, focus is kept in the same column,
+ otherwise the entire last row is focused.
+
+
+
+
+
+ Enter
+
+
+
+
Performs default action on row or cell, e.g. opens message or navigate to link.
+
If focus is on the cell with the expand/collapse button, and there is no other action,
+ will toggle expansion of the current row.
+
+
+
+
+
+
+
+
+
Role, Property, State, and Tabindex Attributes
+
+
+
+
Role
+
Attribute
+
Element
+
Usage
+
+
+
+
+
treegrid
+
+
table
+
Identifies the element as a treegrid.
+
+
+
+
+
aria-label="Inbox"
+
table
+
Provides an accessible name for the treegrid.
+
+
+
row
+
+
tr
+
+
+
Identifies the element as a row.
+
The row role is not an implicit semantic for the tr element when in a treegrid.
+
+
+
+
+
+
tabindex="-1"
+
tr or td
+
+
+
Makes the element focusable without including it in the tab sequence of the page.
+
All row and gridcell elements are focusable, but only one is included in the tab sequence.
+
+
+
+
+
+
tabindex="0"
+
tr or td
+
+
+
Includes the element in the tab sequence.
+
Only one row or gridcell in the treegrid has tabindex="0".
+
In this implementation, the first row in the treegrid is included in the tab sequence when the page loads.
+
+ When the user moves focus in the treegrid, the element included in the tab sequence changes to the element with focus as described in the section on
+ roving tabindex.
+
+
+
+
+
+
+
+
aria-expanded="false"
+
tr or td
+
+
+
Applied only to parent row or first cell of parent row, i.e., next set of rows are responses to the message in this row
+
Indicates the parent row is closed, i.e., the descendant rows are not visible.
+
The visual indication of the collapsed state is synchronized by a CSS attribute selector.
+
When the treegrid is configured to support focus on rows aria-expanded is on the tr elements, but when the treegrid is configured to support focus on cells only, aria-expanded is on the first td element contained in each row.
+
+
+
+
+
+
+
aria-expanded="true"
+
tr or td
+
+
+
Applied only to parent rows or the first cell in parent rows, i.e., next set of rows are responses to the message in this row
+
Indicates the parent row is open, i.e., the descendant rows are visible.
+
The visual indication of the open state is synchronized by a CSS attribute selector.
+
When the treegrid is configured to support focus on rows aria-expanded is on the tr elements, but when the treegrid is configured to support focus on cells only, aria-expanded is on the first td element contained in each row.
+
+
+
+
+
+
+
aria-level="number"
+
tr
+
+
+
Defines the level of the row in the hierarchical treegrid structure.
+
Counting is one-based.
+
Root rows have aria-level=“1”.
+
+
+
+
+
+
+
aria-setsize="number"
+
tr
+
+ Defines the number of rows in the set of rows that are in the same branch and at the same level within the hierarchy.
+
+
+
+
+
+
aria-posinset="number"
+
tr
+
+
+
Defines the position of the row within the set of other rows that are in the same branch and at the same level within the hierarchy.
+
Counting is one-based, not zero-based.
+
+
+
+
+
gridcell
+
+
td
+
+
+
Identifies the element as a gridcell.
+
The gridcell role is not an implicit semantic for the td element when in a treegrid.
+
+
+
+
+
+
+ NOTE: Due to an error in the ARIA 1.1 specification, the aria-posinset and aria-setsize properties are not supported on row elements.
+ This is being corrected in ARIA 1.2.
+ Consequently, when validating the HTML in this example, errors are generated.
+