diff --git a/examples/treegrid/css/treegrid-row-nav-primary-1.css b/examples/treegrid/css/treegrid-row-nav-primary-1.css index 020a27d05c..2e5d4cc6fa 100644 --- a/examples/treegrid/css/treegrid-row-nav-primary-1.css +++ b/examples/treegrid/css/treegrid-row-nav-primary-1.css @@ -5,6 +5,10 @@ table-layout: fixed; } +#treegrid tr { + cursor: default; +} + #treegrid-col1, #treegrid-col3 { width: 30%; } @@ -67,6 +71,7 @@ } #treegrid tr[aria-expanded] > td: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; diff --git a/examples/treegrid/js/treegrid-row-nav-primary-1.js b/examples/treegrid/js/treegrid-row-nav-primary-1.js index e389c7f2fe..cb7d3472de 100644 --- a/examples/treegrid/js/treegrid-row-nav-primary-1.js +++ b/examples/treegrid/js/treegrid-row-nav-primary-1.js @@ -17,8 +17,8 @@ function onReady (treegrid, doAllowRowFocus, doStartRowFocus) { } else { setTabIndexForCellsInRow(rows[index], -1); + moveAriaExpandedToFirstCell(rows[index]); } - propagateExpandedToFirstCell(rows[index]); } if (doStartRowFocus) { @@ -149,7 +149,11 @@ function onReady (treegrid, doAllowRowFocus, doStartRowFocus) { // The row with focus is the row that either has focus or an element // inside of it has focus function getRowWithFocus () { - var possibleRow = document.activeElement; + return getContainingRow(document.activeElement); + } + + function getContainingRow (start) { + var possibleRow = start; if (treegrid.contains(possibleRow)) { while (possibleRow !== treegrid) { if (possibleRow.localName === 'tr') { @@ -304,12 +308,12 @@ function onReady (treegrid, doAllowRowFocus, doStartRowFocus) { var cols = getNavigableCols(currentRow); var currentCol = getColWithFocus(currentRow); if (currentCol === cols[0] && currentRow.hasAttribute('aria-expanded')) { - changeExpanded(currentRow.getAttribute('aria-expanded') === 'false'); + changeExpanded(isExpanded(currentRow)); } } - function changeExpanded (doExpand) { - var currentRow = getRowWithFocus(); + function changeExpanded (doExpand, row) { + var currentRow = row || getRowWithFocus(); if (!currentRow) { return; } @@ -340,8 +344,7 @@ function onReady (treegrid, doAllowRowFocus, doStartRowFocus) { } } if (didChange) { - currentRow.setAttribute('aria-expanded', doExpand); - propagateExpandedToFirstCell(currentRow); + setAriaExpanded(currentRow, doExpand); return true; } } @@ -349,15 +352,32 @@ function onReady (treegrid, doAllowRowFocus, doStartRowFocus) { // 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 propagateExpandedToFirstCell (row) { + 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'); } - else { - firstCell.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) { @@ -441,8 +461,46 @@ function onReady (treegrid, doAllowRowFocus, doStartRowFocus) { 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(); treegrid.addEventListener('keydown', onKeyDown); + treegrid.addEventListener('click', onClick); + treegrid.addEventListener('dblclick', onDoubleClick); // Polyfill for focusin necessary for Firefox < 52 window.addEventListener(window.onfocusin ? 'focusin' : 'focus', onFocusIn, true); diff --git a/examples/treegrid/treegrid-row-nav-primary-1.html b/examples/treegrid/treegrid-row-nav-primary-1.html index 2c202ff71b..eb7829a74a 100644 --- a/examples/treegrid/treegrid-row-nav-primary-1.html +++ b/examples/treegrid/treegrid-row-nav-primary-1.html @@ -132,7 +132,7 @@

Accessibility Features

  • aria-level: used to set the current level of an item, 1, 2, 3, etc.
  • aria-posinset, aria-setsize: used on a row to indicate the position of an item within it's local group, such as item 3 of 5. Unfortunately, aria-posinset and aria-setsize are not currently legal in the spec (bug filed). Therefore, aria-posinset and aria-setsize are not currently supported on role="row" in the Nu Validator (bug filed). We'll need to work that in as this is the most practical way of implementing ARIA in many existing treegrid libraries (as opposed to rearchitecting the DOM structure to contain rowgroups). Note that the ARIA spec's text for rowgroup states that it cannot be parented by a treegrid (bug filed).
  • -
  • aria-expanded (tristate): this attribute must be removed (not present) if the item cannot be expanded. For expandable items, the value is "true" or "false"
  • +
  • aria-expanded (tristate): this attribute must be removed (not present) if the item cannot be expanded. For expandable items, the value is "true" or "false".
    IMPORTANT: aria-expanded is set on the row unless rows cannot receive focus, because it is an indicator to the user that something is actionable. If only the cell can get focused, then pressing Enter on the cell is the only way to indicate this, so the attribute needs to go there so that screen readers will announce it. It is not added to the cell if rows can receive focus, as this provides a confusing/redundant experience where both the row and first cell announce the expanded state. In this example, JavaScript is used to move the aria-expanded attribute down to the first cell in the ?cell=force version.
  • aria-hidden: set to "true" for child items that are currently hidden because the parent is collapsed.
  • aria-readonly: in ARIA 1.0, a grid/treegrid is editable by default. However, there is no default in ARIA 1.1. Firefox currently implements the ARIA 1.0 concept for this, which means that "editable" is read for every cell unless aria-readonly="true" is used on the treegrid. There needs to be some follow up with user agent developers on this, as ARIA 1.1 seems to be treating it more a tristate (don't care, false, true). In addition, a bug was filed on the spec regarding aria-readonly but it is possibly invalid.
  • tabindex is set in the JS, as per the usual roving tabindex methodology. Specifically, we use tabindex="0" for the current item so that it is the subitem that gets focused if user tabs out and back in, and tabindex="-1" for all items where we want click-to-focus behavior enabled.