CellTree: add API for keyboard-selected node

New public methods:
  CellTree.getKeyboardSelectedTreeNode()
  CellTree.setKeyboardSelectedTreeNode()

Change-Id: I00942611e485cc1459836065c7337a0a2d097f48
Review-Link: https://gwt-review.googlesource.com/#/c/9280/
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 1094903..5e66bc1 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTree.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellTree.java
@@ -820,6 +820,22 @@
     keyboardSelectedNode.setKeyboardSelected(true, true);
   }
 
+  /**
+   * Sets the node that will be selected when the CellTree gains keyboard focus.
+   *
+   * @param parentNode a node in the tree that is currently open
+   * @param childIndex the index of the child to select
+   * @param stealFocus if true, also change keyboard focus to this CellTree.
+   */
+  public void setKeyboardSelectedTreeNode(TreeNode parentNode, int childIndex, boolean stealFocus) {
+    CellTreeNodeView nodeView = getCellTreeNodeView(parentNode);
+    // Just to ensure necessary checks are done, e.g.
+    //   assertNotDestroyed();checkChildBounds(childIndex);flush();
+    nodeView.getTreeNode().getChildValue(childIndex);
+
+    keyboardSelect(nodeView.getChildNode(childIndex), stealFocus);
+  }
+
   public void setTabIndex(int index) {
     this.tabIndex = index;
     keyboardSelectedNode.setKeyboardSelected(true, false);
@@ -883,6 +899,13 @@
   }
 
   /**
+   * Returns the TreeNode that is selected when the CellTree has keyboard focus.
+   */
+  public TreeNode getKeyboardSelectedTreeNode() {
+    return keyboardSelectedNode == null ? null : keyboardSelectedNode.getTreeNode();
+  }
+
+  /**
    * Return the HTML to render the loading image.
    */
   SafeHtml getLoadingImageHtml() {
@@ -964,6 +987,19 @@
     chain.add(hElem);
   }
 
+  private CellTreeNodeView getCellTreeNodeView(TreeNode treeNode) {
+    if (!(treeNode instanceof CellTreeNodeView.TreeNodeImpl)) {
+      throw new UnsupportedOperationException("Operation not supported for " + treeNode.getClass());
+    }
+
+    CellTreeNodeView nodeView = ((CellTreeNodeView.TreeNodeImpl) treeNode).getNodeView();
+    if (!nodeView.belongsToTree(this)) {
+      throw new IllegalArgumentException("The tree node does not belong to the tree.");
+    }
+
+    return nodeView;
+  }
+
   private CellTreeNodeView<?> findItemByChain(ArrayList<Element> chain,
       int idx, CellTreeNodeView<?> parent) {
     if (idx == chain.size()) {
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 8c26a8b..8a316a9 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
@@ -561,7 +561,7 @@
    * might move it to a new {@link CellTreeNodeView}, and we don't want
    * non-static references to the old {@link CellTreeNodeView}.
    */
-  private static class TreeNodeImpl implements TreeNode {
+  static class TreeNodeImpl implements TreeNode {
 
     private CellTreeNodeView<?> nodeView;
 
@@ -590,6 +590,10 @@
       return (nodeView.parentNode == null) ? 0 : nodeView.parentNode.children.indexOf(nodeView);
     }
 
+    final CellTreeNodeView<?> getNodeView() {
+      return nodeView;
+    }
+
     @Override
     public TreeNode getParent() {
       assertNotDestroyed();
@@ -669,7 +673,7 @@
     /**
      * Flush pending changes in the view.
      */
-    private void flush() {
+    void flush() {
       if (nodeView.listView != null) {
         nodeView.listView.presenter.flush();
       }
@@ -724,7 +728,7 @@
    * @param nodeElem the element that represents the node
    * @return the cell parent within the node
    */
-  private static Element getSelectionElement(Element nodeElem) {
+  static Element getSelectionElement(Element nodeElem) {
     return nodeElem.getFirstChildElement();
   }
 
@@ -1139,6 +1143,10 @@
     nodeInfo.setDataDisplay(view);
   }
 
+  boolean belongsToTree(final CellTree tree) {
+    return this.tree == tree;
+  }
+
   /**
    * Ensure that the animation frame exists and return it.
    *
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 990abbf..70c1630 100644
--- a/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java
+++ b/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java
@@ -264,6 +264,81 @@
     assertEquals("5", cViewChildLast.getElement().getAttribute("aria-setsize"));
   }
 
+  public void testExplicitKeyboardSelection() {
+    CellTree cellTree = (CellTree) tree;
+    TreeNode root = cellTree.getRootTreeNode();
+
+    final String cellTreeKeyboardSelectedItemStyleName =
+        cellTree.getStyle().cellTreeKeyboardSelectedItem();
+
+    // Navigate through tree to level 4 nodes.
+    TreeNode l1Node = root.setChildOpen(0, true); // a
+    CellTreeNodeView<?> l1View = cellTree.rootNode.getChildNode(0);
+    TreeNode l2Node = l1Node.setChildOpen(0, true); // aa
+    CellTreeNodeView<?> l2View = l1View.getChildNode(0);
+    TreeNode l3Node = l2Node.setChildOpen(0, true); // aaa
+    CellTreeNodeView<?> l3View = l2View.getChildNode(0);
+
+    ((CellTreeNodeView.TreeNodeImpl) l3Node).flush();
+    CellTreeNodeView<?> l4View = l3View.getChildNode(0);
+    // l4Node is leaf, cannot get a reference via setChildOpen except via non-public nodeView
+    TreeNode l4Node = l4View.getTreeNode(); // aaaa
+
+    // Set keyboard selected node to a subtree node, l3Node = first child of l2Node
+    cellTree.setKeyboardSelectedTreeNode(l2Node, 0, true);
+    assertEquals(l3View, cellTree.getKeyboardSelectedNode());
+    assertEquals(l3Node, cellTree.getKeyboardSelectedTreeNode());
+    assertTrue(CellTreeNodeView.getSelectionElement(l3View.getElement()).getClassName()
+        .indexOf(cellTreeKeyboardSelectedItemStyleName) >= 0);
+
+    // Set keyboard selected node to a leaf node.
+    cellTree.setKeyboardSelectedTreeNode(l3Node, 0, true);
+    assertEquals(l4View, cellTree.getKeyboardSelectedNode());
+    assertEquals(l4Node, cellTree.getKeyboardSelectedTreeNode());
+    assertTrue(CellTreeNodeView.getSelectionElement(l4View.getElement()).getClassName()
+        .indexOf(cellTreeKeyboardSelectedItemStyleName) >= 0);
+
+    l1Node.setChildOpen(0, false); // close l2node
+    ((CellTreeNodeView.TreeNodeImpl) l1Node).flush();
+
+    // Try to select a leaf node in closed subtree
+    try {
+      cellTree.setKeyboardSelectedTreeNode(l3Node, 0, true);
+      fail("should have thrown");
+    } catch (IllegalStateException e) {
+      assertEquals(e.getMessage(), "TreeNode no longer exists.");
+    }
+
+    // Try to select a subtree node in closed subtree
+    try {
+      cellTree.setKeyboardSelectedTreeNode(l2Node, 0, true);
+      fail("should have thrown");
+    } catch (IllegalStateException e) {
+      assertEquals(e.getMessage(), "TreeNode no longer exists.");
+    }
+
+    // Still ok to select closed subtree node
+    cellTree.setKeyboardSelectedTreeNode(l1Node, 0, true);
+
+    // Create another tree of same structure
+    CellTree anotherTree = createAbstractCellTree(model, root.getValue());
+    // Navigate to leaf
+    l1Node = anotherTree.getRootTreeNode().setChildOpen(0, true); // a
+    ((CellTreeNodeView.TreeNodeImpl) l1Node).flush();
+
+    // Now l1Node refers to a subtree node in anotherTree
+    // Select in the same cell tree is ok
+    anotherTree.setKeyboardSelectedTreeNode(l1Node, 0, true);
+
+    // Select in a different tree will throw exception.
+    try {
+      cellTree.setKeyboardSelectedTreeNode(l1Node, 0, true);
+      fail("should have thrown");
+    } catch (IllegalArgumentException e) {
+      assertEquals(e.getMessage(), "The tree node does not belong to the tree.");
+    }
+  }
+
   @Override
   protected <T> CellTree createAbstractCellTree(TreeViewModel model, T rootValue) {
     return new CellTree(model, rootValue);