Added Aria roles and properties to the CellTree

Review at http://gwt-code-reviews.appspot.com/1776803


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@11211 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/cellview/client/CellTree.java b/user/src/com/google/gwt/user/cellview/client/CellTree.java
index da94e63..7b12a0a 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTree.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellTree.java
@@ -45,6 +45,7 @@
 import com.google.gwt.user.client.ui.HasAnimation;
 import com.google.gwt.user.client.ui.SimplePanel;
 import com.google.gwt.view.client.TreeViewModel;
+import com.google.gwt.aria.client.Roles;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -653,6 +654,8 @@
         getElement(), rootValue, messages);
     keyboardSelectedNode = rootNode = root;
     root.setOpen(true, false);
+
+    Roles.getTreeRole().set(getElement());
   }
 
   /**
diff --git a/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java b/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
index a7cd065..95d8573 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.user.cellview.client;
 
+import com.google.gwt.aria.client.Roles;
 import com.google.gwt.cell.client.Cell;
 import com.google.gwt.cell.client.Cell.Context;
 import com.google.gwt.core.client.GWT;
@@ -55,6 +56,7 @@
 import com.google.gwt.view.client.SelectionModel;
 import com.google.gwt.view.client.TreeViewModel;
 import com.google.gwt.view.client.TreeViewModel.NodeInfo;
+import com.google.gwt.aria.client.ExpandedValue;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -77,10 +79,10 @@
     SafeHtml innerDiv(SafeStyles cssString, String classes, SafeHtml image, String itemValueStyle,
         SafeHtml cellContents);
 
-    @Template("<div><div style=\"{0}\" class=\"{1}\">{2}</div></div>")
-    SafeHtml outerDiv(SafeStyles cssString, String classes, SafeHtml content);
+    @Template("<div aria-selected=\"{3}\">"
+        + "<div style=\"{0}\" class=\"{1}\">{2}</div></div>")
+    SafeHtml outerDiv(SafeStyles cssString, String classes, SafeHtml content, String ariaSelected);
   }
-
   /**
    * The {@link com.google.gwt.view.client.HasData} used to show children. This
    * class is intentionally static because we might move it to a new
@@ -153,7 +155,9 @@
           if (isRootNode) {
             outerClasses.append(topStyle);
           }
-          if (selectionModel != null && selectionModel.isSelected(value)) {
+          boolean isSelected = (selectionModel != null && selectionModel.isSelected(value));
+          String ariaSelected = String.valueOf(isSelected);
+          if (isSelected) {
             outerClasses.append(selectedStyle);
           }
 
@@ -182,11 +186,11 @@
           SafeHtml innerDiv =
               template.innerDiv(innerPadding, innerClasses.toString(), image, itemValueStyle,
                   cellBuilder.toSafeHtml());
-
           SafeStyles outerPadding =
               SafeStylesUtils.fromTrustedString("padding-" + paddingDirection + ": "
                   + paddingAmount + "px;");
-          sb.append(template.outerDiv(outerPadding, outerClasses.toString(), innerDiv));
+          sb.append(template.outerDiv(outerPadding, outerClasses.toString(), innerDiv,
+              ariaSelected));
         }
       }
 
@@ -282,6 +286,7 @@
         int len = values.size();
         int end = start + len;
         int childCount = nodeView.getChildCount();
+        int setSize = (childCount > len) ? childCount : end;
         ProvidesKey<C> keyProvider = nodeInfo.getProvidesKey();
 
         Element container = nodeView.ensureChildContainer();
@@ -344,6 +349,7 @@
           } else {
             nodeView.children.add(child);
           }
+          child.updateAriaAttributes(setSize);
           childElem = childElem.getNextSiblingElement();
         }
 
@@ -865,6 +871,8 @@
     this.value = value;
     this.messages = messages;
     setElement(elem);
+
+    Roles.getTreeitemRole().set(getElement());
   }
 
   public int getChildCount() {
@@ -1345,6 +1353,26 @@
     listView.setVisibleRange(range.getStart(), pageSize);
   }
 
+  private void updateAriaAttributes(int setSize) {
+    // Early out if this is a root node.
+    if (isRootNode()) {
+      return;
+    }
+
+    Roles.getTreeitemRole().setAriaSetsizeProperty(getElement(), setSize);
+    int selectionIndex = parentNode.indexOf(this);
+    Roles.getTreeitemRole().setAriaPosinsetProperty(getElement(), selectionIndex + 1);
+    // Set 'aria-expanded' state
+    // don't set aria-expanded on the leaf nodes
+    if (isLeaf()) {
+      Roles.getTreeitemRole().removeAriaExpandedState(getElement());
+    } else {
+      Roles.getTreeitemRole().setAriaExpandedState(getElement(),
+          ExpandedValue.of(open));
+    }
+    Roles.getTreeitemRole().setAriaLevelProperty(getElement(), this.depth);
+  }
+
   /**
    * Update the image based on the current state.
    * 
@@ -1371,5 +1399,14 @@
 
     Element oldImg = getImageElement();
     oldImg.getParentElement().replaceChild(imageElem, oldImg);
+
+    // Set 'aria-expanded' state
+    // don't set aria-expanded on the leaf nodes
+    if (isLeaf()) {
+      Roles.getTreeitemRole().removeAriaExpandedState(getElement());
+    } else {
+      Roles.getTreeitemRole().setAriaExpandedState(getElement(),
+          ExpandedValue.of(open));
+    }
   }
 }
diff --git a/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java b/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java
index 222888d..990abbf 100644
--- a/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java
+++ b/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java
@@ -159,6 +159,33 @@
     assertEquals("new", newNodeImpl.getCellParent().getInnerText());
   }
 
+  public void testAriaSelectedAndExpanded() {
+    CellTree cellTree = (CellTree) tree;
+    TreeNode root = cellTree.getRootTreeNode();
+
+    TreeNode newNode = root.setChildOpen(1, true);
+    cellTree.rootNode.getChildNode(1).setSelected(true);
+    model.getRootDataProvider().refresh();
+    model.getRootDataProvider().flush();
+    root.setChildOpen(1, true);
+    CellTreeNodeView<?> newNodeImpl = cellTree.rootNode.getChildNode(1);
+    assertEquals("true", newNodeImpl.getElement().getAttribute("aria-selected"));
+    // Check aria-expanded on open
+    assertEquals("true", newNodeImpl.getElement().getAttribute("aria-expanded"));
+
+    // Check aria-expanded on close
+    root.setChildOpen(1, false);
+    newNodeImpl = cellTree.rootNode.getChildNode(1);
+    assertEquals("false", newNodeImpl.getElement().getAttribute("aria-expanded"));
+
+    cellTree.rootNode.getChildNode(1).setSelected(false);
+    model.getRootDataProvider().refresh();
+    model.getRootDataProvider().flush();
+    root.setChildOpen(1, true);
+    newNodeImpl = cellTree.rootNode.getChildNode(1);
+    assertEquals("false", newNodeImpl.getElement().getAttribute("aria-selected"));
+  }
+
   public void testSetDefaultNodeSize() {
     CellTree cellTree = (CellTree) tree;
     TreeNode root = cellTree.getRootTreeNode();
@@ -175,6 +202,68 @@
     assertEquals(5, d.getChildCount());
   }
 
+  public void testAriaAttributes() {
+    CellTree cellTree = (CellTree) tree;
+    TreeNode rootNode = cellTree.getRootTreeNode();
+
+    // Open a child node.
+    TreeNode aNode = rootNode.setChildOpen(0, true);
+
+    // Check role="tree"
+    assertEquals("tree", cellTree.rootNode.getElement().getAttribute("role"));
+
+    // Check 1st level
+    CellTreeNodeView<?> aView = cellTree.rootNode.getChildNode(0);
+    CellTreeNodeView<?> bView = cellTree.rootNode.getChildNode(1);
+
+    // Check treeitem role, level, posinset, and setsize
+    assertEquals("treeitem", aView.getElement().getAttribute("role"));
+    assertEquals("1", aView.getElement().getAttribute("aria-level"));
+    assertEquals("1", aView.getElement().getAttribute("aria-posinset"));
+    assertEquals("10", aView.getElement().getAttribute("aria-setsize"));
+
+    assertEquals("treeitem", bView.getElement().getAttribute("role"));
+    assertEquals("1", bView.getElement().getAttribute("aria-level"));
+    assertEquals("2", bView.getElement().getAttribute("aria-posinset"));
+    assertEquals("10", bView.getElement().getAttribute("aria-setsize"));
+
+    // Check 2nd level
+    assertTrue(aNode.getChildCount() != 0);
+    assertEquals(aNode, aView.getTreeNode());
+    assertTrue(aView.getChildCount() != 0);
+    CellTreeNodeView<?> aViewChild = aView.getChildNode(0);
+
+    // Check treeitem role, level, posinset, and setsize
+    assertEquals("treeitem", aViewChild.getElement().getAttribute("role"));
+    assertEquals("2", aViewChild.getElement().getAttribute("aria-level"));
+    assertEquals("1", aViewChild.getElement().getAttribute("aria-posinset"));
+    assertEquals("10", aViewChild.getElement().getAttribute("aria-setsize"));
+
+    // Check aria-expanded
+    assertEquals("true", aView.getElement().getAttribute("aria-expanded"));
+    assertEquals("false", aViewChild.getElement().getAttribute("aria-expanded"));
+    while (!aNode.isChildLeaf(0)) {
+      aNode = aNode.setChildOpen(0, true);
+      aView = aView.getChildNode(0);
+    }
+    assertEquals(aNode, aView.getTreeNode());
+    assertEquals("", aView.getChildNode(0).getElement().getAttribute("aria-expanded"));
+
+    // Change default size and check aria-setsize and aria-posinset
+    cellTree.setDefaultNodeSize(5);
+    TreeNode cNode = rootNode.setChildOpen(3, true);
+    CellTreeNodeView<?> cView = cellTree.rootNode.getChildNode(3);
+    assertEquals(5, cNode.getChildCount());
+
+    CellTreeNodeView<?> cViewChildFirst = cView.getChildNode(0);
+    assertEquals("1", cViewChildFirst.getElement().getAttribute("aria-posinset"));
+    assertEquals("5", cViewChildFirst.getElement().getAttribute("aria-setsize"));
+
+    CellTreeNodeView<?> cViewChildLast = cView.getChildNode(cView.getChildCount() - 1);
+    assertEquals("5", cViewChildLast.getElement().getAttribute("aria-posinset"));
+    assertEquals("5", cViewChildLast.getElement().getAttribute("aria-setsize"));
+  }
+
   @Override
   protected <T> CellTree createAbstractCellTree(TreeViewModel model, T rootValue) {
     return new CellTree(model, rootValue);