Revision: 8557
Author: [email protected]
Date: Tue Aug 17 11:18:17 2010
Log: First cut at keyboard navigation for CellTree

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

Review by: [email protected]
http://code.google.com/p/google-web-toolkit/source/detail?r=8557

Modified:
/trunk/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/ContactTreeViewModel.java
 /trunk/user/src/com/google/gwt/user/cellview/client/CellTree.css
 /trunk/user/src/com/google/gwt/user/cellview/client/CellTree.java
 /trunk/user/src/com/google/gwt/user/cellview/client/CellTreeClean.css
 /trunk/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java

=======================================
--- /trunk/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/ContactTreeViewModel.java Tue Aug 17 10:14:36 2010 +++ /trunk/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/ContactTreeViewModel.java Tue Aug 17 11:18:17 2010
@@ -21,8 +21,11 @@
 import com.google.gwt.cell.client.CompositeCell;
 import com.google.gwt.cell.client.FieldUpdater;
 import com.google.gwt.cell.client.HasCell;
+import com.google.gwt.cell.client.ValueUpdater;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.resources.client.ClientBundle;
 import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.sample.showcase.client.content.cell.ContactDatabase.Category;
@@ -99,6 +102,16 @@
     public int compareTo(LetterCount o) {
       return (o == null) ? -1 : (firstLetter - o.firstLetter);
     }
+
+    @Override
+    public boolean equals(Object o) {
+      return compareTo((LetterCount) o) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+      return firstLetter;
+    }

     /**
      * Increment the count.
@@ -185,6 +198,15 @@
       }
     });
     contactCell = new CompositeCell<ContactInfo>(hasCells) {
+      @Override
+ public void onBrowserEvent(Element parent, ContactInfo value, Object key,
+          NativeEvent event, ValueUpdater<ContactInfo> valueUpdater) {
+        if ("keyup".equals(event.getType())
+            && event.getKeyCode() == KeyCodes.KEY_ENTER) {
+ selectionModel.setSelected(value, !selectionModel.isSelected(value));
+        }
+      }
+
       @Override
       public void render(ContactInfo value, Object key, StringBuilder sb) {
         sb.append("<table><tbody><tr>");
=======================================
--- /trunk/user/src/com/google/gwt/user/cellview/client/CellTree.css Mon Jun 7 12:20:31 2010 +++ /trunk/user/src/com/google/gwt/user/cellview/client/CellTree.css Tue Aug 17 11:18:17 2010
@@ -23,9 +23,29 @@
   padding-right: 3px;
 }

+/*
+div:focus { outline: none; }
+*/
+
+.keyboardSelectedItem {
+  background-color: #ffff00;
+}
+
 .openItem {

 }
+
+.topItem {
+
+}
+
+.topItemImage {
+
+}
+
+.topItemImageValue {
+
+}

 @sprite .selectedItem {
   gwt-image: 'cellTreeSelectedBackground';
@@ -39,15 +59,3 @@
   padding-left: 16px;
   outline: none;
 }
-
-.topItem {
-
-}
-
-.topItemImage {
-
-}
-
-.topItemImageValue {
-
-}
=======================================
--- /trunk/user/src/com/google/gwt/user/cellview/client/CellTree.java Mon Aug 2 11:31:26 2010 +++ /trunk/user/src/com/google/gwt/user/cellview/client/CellTree.java Tue Aug 17 11:18:17 2010
@@ -21,6 +21,7 @@
 import com.google.gwt.dom.client.Style.Display;
 import com.google.gwt.dom.client.Style.Position;
 import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.resources.client.ClientBundle;
 import com.google.gwt.resources.client.CssResource;
 import com.google.gwt.resources.client.ImageResource;
@@ -38,6 +39,33 @@
  * A view of a tree.
  */
 public class CellTree extends Composite implements HasAnimation {
+
+  /**
+   * A cleaner version of the table that uses less graphics.
+   */
+  public static interface CleanResources extends Resources {
+
+    @Source("cellTreeClosedArrow.png")
+    ImageResource cellTreeClosedItem();
+
+    @Source("cellTreeLoadingClean.gif")
+    ImageResource cellTreeLoading();
+
+    @Source("cellTreeOpenArrow.png")
+    ImageResource cellTreeOpenItem();
+
+    @Source("CellTreeClean.css")
+    CleanStyle cellTreeStyle();
+  }
+
+  /**
+   * A cleaner version of the table that uses less graphics.
+   */
+  public static interface CleanStyle extends Style {
+    String topItem();
+
+    String topItemImageValue();
+  }

   /**
    * A node animation.
@@ -310,6 +338,11 @@
      */
     String itemValue();

+    /**
+     * Applied to the keyboard selected item.
+     */
+    String keyboardSelectedItem();
+
     /**
      * Applied to open tree items.
      */
@@ -340,33 +373,6 @@
      */
     String topItemImageValue();
   }
-
-  /**
-   * A cleaner version of the table that uses less graphics.
-   */
-  public static interface CleanStyle extends Style {
-    String topItem();
-
-    String topItemImageValue();
-  }
-
-  /**
-   * A cleaner version of the table that uses less graphics.
-   */
-  public static interface CleanResources extends Resources {
-
-    @Source("cellTreeClosedArrow.png")
-    ImageResource cellTreeClosedItem();
-
-    @Source("cellTreeLoadingClean.gif")
-    ImageResource cellTreeLoading();
-
-    @Source("cellTreeOpenArrow.png")
-    ImageResource cellTreeOpenItem();
-
-    @Source("CellTreeClean.css")
-    CleanStyle cellTreeStyle();
-  }

   /**
    * The default number of children to show under a tree node.
@@ -387,6 +393,8 @@
    */
   private NodeAnimation animation;

+  private boolean cellIsEditing;
+
   /**
    * The HTML used to generate the closed image.
    */
@@ -412,6 +420,12 @@
    */
   private boolean isAnimationEnabled;

+  /**
+ * The {...@link CellTreeNodeView} whose children are currently being selected
+   * using the keyboard.
+   */
+  private CellTreeNodeView<?> keyboardSelectedNode;
+
   /**
    * The HTML used to generate the loading image.
    */
@@ -461,8 +475,8 @@
    * @param rootValue the hidden root value of the tree
    * @param resources the resources used to render the tree
    */
-  public <T> CellTree(
-      TreeViewModel viewModel, T rootValue, Resources resources) {
+  public <T> CellTree(TreeViewModel viewModel, T rootValue,
+      Resources resources) {
     this.viewModel = viewModel;
     this.style = resources.cellTreeStyle();
     this.style.ensureInjected();
@@ -485,13 +499,14 @@
     setAnimation(SlideAnimation.create());

     // Add event handlers.
-    sinkEvents(Event.ONCLICK);
+    sinkEvents(Event.ONCLICK | Event.ONKEYDOWN | Event.ONKEYUP);

     // Associate a view with the item.
-    CellTreeNodeView<T> root = new CellTreeNodeView<T>(
-        this, null, null, getElement(), rootValue);
-    rootNode = root;
+    CellTreeNodeView<T> root = new CellTreeNodeView<T>(this, null, null,
+        getElement(), rootValue);
+    keyboardSelectedNode = rootNode = root;
     root.setOpen(true);
+    keyboardSelectedNode.keyboardEnter(0, false);
   }

   /**
@@ -527,8 +542,32 @@
     CellBasedWidgetImpl.get().onBrowserEvent(this, event);
     super.onBrowserEvent(event);

-    Element target = event.getEventTarget().cast();
-
+    String eventType = event.getType();
+
+    // Keep track of whether the user has focused on the widget
+    if ("blur".equals(eventType)) {
+      keyboardSelectedNode.keyboardBlur();
+      return;
+    }
+    if ("focus".equals(eventType)) {
+      keyboardSelectedNode.keyboardFocus();
+      return;
+    }
+
+    boolean keyUp = "keyup".equals(eventType);
+    boolean keyDown = "keydown".equals(eventType);
+
+    // Ignore keydown events unless the cell is in edit mode
+    if (keyDown && !cellIsEditing) {
+      return;
+    }
+    if (keyUp && !cellIsEditing) {
+      if (handleKey(event)) {
+        return;
+      }
+    }
+
+    Element target = event.getEventTarget().cast();
     ArrayList<Element> chain = new ArrayList<Element>();
     collectElementChain(chain, getElement(), target);

@@ -544,10 +583,17 @@
           nodeView.showMore();
           return;
         }
+
+        // Move the keyboard focus to the clicked item
+        keyboardSelectedNode.keyboardExit();
+        keyboardSelectedNode = nodeView.getParentNode();
+        keyboardSelectedNode.keyboardEnter(target, true);
       }

-      // Forward the event to the cell.
-      if (nodeView.getCellParent().isOrHasChild(target)) {
+      // Forward the event to the cell
+      if (nodeView.getCellParent().isOrHasChild(target)
+          || (eventType.startsWith("key")
+              && nodeView.getCellParent().getParentElement() == target)) {
         nodeView.fireEventToCell(event);
       }
     }
@@ -634,16 +680,16 @@
    */
   void maybeAnimateTreeNode(CellTreeNodeView<?> node) {
     if (animation != null) {
-      animation.animate(node,
- node.consumeAnimate() && isAnimationEnabled() && !node.isRootNode());
+      animation.animate(node, node.consumeAnimate() && isAnimationEnabled()
+          && !node.isRootNode());
     }
   }

   /**
* Collects parents going up the element tree, terminated at the tree root.
    */
-  private void collectElementChain(
-      ArrayList<Element> chain, Element hRoot, Element hElem) {
+  private void collectElementChain(ArrayList<Element> chain, Element hRoot,
+      Element hElem) {
     if ((hElem == null) || (hElem == hRoot)) {
       return;
     }
@@ -652,8 +698,8 @@
     chain.add(hElem);
   }

-  private CellTreeNodeView<?> findItemByChain(
-      ArrayList<Element> chain, int idx, CellTreeNodeView<?> parent) {
+  private CellTreeNodeView<?> findItemByChain(ArrayList<Element> chain,
+      int idx, CellTreeNodeView<?> parent) {
     if (idx == chain.size()) {
       return parent;
     }
@@ -701,4 +747,80 @@
     sb.append("\"></div>");
     return sb.toString();
   }
-}
+
+  /**
+   * @return true if the key event was consumed by navigation, false if it
+   *         should be passed on to the underlying Cell.
+   */
+  private boolean handleKey(Event event) {
+    int keyCode = event.getKeyCode();
+
+    CellTreeNodeView<?> child = null;
+ int keyboardSelectedIndex = keyboardSelectedNode.getKeyboardSelectedIndex();
+    if (keyboardSelectedIndex != -1
+        && keyboardSelectedNode.getChildCount() > keyboardSelectedIndex) {
+      child = keyboardSelectedNode.getChildNode(keyboardSelectedIndex);
+    }
+
+    CellTreeNodeView<?> parent = keyboardSelectedNode.getParentNode();
+    switch (keyCode) {
+      case KeyCodes.KEY_UP:
+        if (keyboardSelectedNode.getKeyboardSelectedIndex() == 0) {
+          if (!keyboardSelectedNode.isRootNode()) {
+            if (parent != null) {
+              keyboardSelectedNode.keyboardExit();
+ parent.keyboardEnter(parent.indexOf(keyboardSelectedNode), true);
+              keyboardSelectedNode = parent;
+            }
+          }
+        } else {
+          keyboardSelectedNode.keyboardUp();
+          // Descend into open nodes, go to bottom of leaf node
+          int index = keyboardSelectedNode.getKeyboardSelectedIndex();
+ while ((child = keyboardSelectedNode.getChildNode(index)).isOpen()) {
+            keyboardSelectedNode.keyboardExit();
+            index = child.getChildCount() - 1;
+            child.keyboardEnter(index, true);
+            keyboardSelectedNode = child;
+          }
+        }
+        return true;
+
+      case KeyCodes.KEY_DOWN:
+        if (child != null && child.isOpen()) {
+          keyboardSelectedNode.keyboardExit();
+          child.keyboardEnter(0, true);
+          keyboardSelectedNode = child;
+        } else if (!keyboardSelectedNode.keyboardDown()) {
+          if (parent != null) {
+            keyboardSelectedNode.keyboardExit();
+ parent.keyboardEnter(parent.indexOf(keyboardSelectedNode), true);
+            // If already at last node of a given level, go up
+            while (!parent.keyboardDown()) {
+              CellTreeNodeView<?> newParent = parent.getParentNode();
+              if (newParent != null) {
+                parent.keyboardExit();
+ newParent.keyboardEnter(newParent.indexOf(parent) + 1, true);
+                parent = newParent;
+              }
+            }
+            keyboardSelectedNode = parent;
+          }
+        }
+        return true;
+
+      case KeyCodes.KEY_LEFT:
+      case KeyCodes.KEY_RIGHT:
+      case KeyCodes.KEY_ENTER:
+        // TODO(rice) - try different key bahavior mappings such as
+        // left=close, right=open, enter=toggle.
+        if (child != null && !child.isLeaf()) {
+          child.setOpen(!child.isOpen());
+          return true;
+        }
+        break;
+    }
+
+    return false;
+  }
+}
=======================================
--- /trunk/user/src/com/google/gwt/user/cellview/client/CellTreeClean.css Mon Jun 7 12:20:31 2010 +++ /trunk/user/src/com/google/gwt/user/cellview/client/CellTreeClean.css Tue Aug 17 11:18:17 2010
@@ -22,6 +22,14 @@
   padding-left: 3px;
   padding-right: 3px;
 }
+
+/*
+div:focus { outline: none; }
+*/
+
+.keyboardSelectedItem {
+  background-color: #ffff00;
+}

 .openItem {

=======================================
--- /trunk/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java Tue Aug 17 10:14:36 2010 +++ /trunk/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java Tue Aug 17 11:18:17 2010
@@ -53,72 +53,6 @@
  * @param <T> the type that this view contains
  */
 class CellTreeNodeView<T> extends UIObject {
-
-  /**
-   * The element used in place of an image when a node has no children.
-   */
-  private static final String LEAF_IMAGE =
-      "<div style='position:absolute;display:none;'></div>";
-
-  /**
-   * The temporary element used to render child items.
-   */
-  private static com.google.gwt.user.client.Element tmpElem;
-
-  /**
-   * Returns the element that parents the cell contents of the node.
-   *
-   * @param nodeElem the element that represents the node
-   * @return the cell parent within the node
-   */
-  private static Element getCellParent(Element nodeElem) {
-    return getSelectionElement(nodeElem).getFirstChildElement().getChild(
-        1).cast();
-  }
-
-  /**
-   * Returns the element that selection is applied to.
-   *
-   * @param nodeElem the element that represents the node
-   * @return the cell parent within the node
-   */
-  private static Element getImageElement(Element nodeElem) {
- return getSelectionElement(nodeElem).getFirstChildElement().getFirstChildElement();
-  }
-
-  /**
-   * Returns the element that selection is applied to.
-   *
-   * @param nodeElem the element that represents the node
-   * @return the cell parent within the node
-   */
-  private static Element getSelectionElement(Element nodeElem) {
-    return nodeElem.getFirstChildElement();
-  }
-
-  /**
-   * @return the temporary element used to create elements
-   */
-  private static com.google.gwt.user.client.Element getTmpElem() {
-    if (tmpElem == null) {
-      tmpElem = Document.get().createDivElement().cast();
-    }
-    return tmpElem;
-  }
-
-  /**
-   * Show or hide an element.
-   *
-   * @param element the element to show or hide
-   * @param show true to show, false to hide
-   */
-  private static void showOrHide(Element element, boolean show) {
-    if (show) {
-      element.getStyle().clearDisplay();
-    } else {
-      element.getStyle().setDisplay(Display.NONE);
-    }
-  }

   /**
    * The {...@link com.google.gwt.view.client.HasData} used to show children.
@@ -127,8 +61,6 @@
    */
   private static class NodeCellList<C> implements HasData<C> {

-    private HandlerManager handlerManger = new HandlerManager(this);
-
     /**
      * The view used by the NodeCellList.
      */
@@ -140,8 +72,8 @@
         this.childContainer = childContainer;
       }

-      public <H extends EventHandler> HandlerRegistration addHandler(
-          H handler, Type<H> type) {
+ public <H extends EventHandler> HandlerRegistration addHandler(H handler,
+          Type<H> type) {
         return handlerManger.addHandler(type, handler);
       }

@@ -154,8 +86,8 @@
       }

       public ElementIterator getChildIterator() {
-        return new HasDataPresenter.DefaultElementIterator(
-            this, childContainer.getFirstChildElement());
+        return new HasDataPresenter.DefaultElementIterator(this,
+            childContainer.getFirstChildElement());
       }

       public void onUpdateSelection() {
@@ -260,15 +192,19 @@
       public void replaceChildren(List<C> values, int start, String html) {
Map<Object, CellTreeNodeView<?>> savedViews = saveChildState(values, 0);

-        Element newChildren = AbstractHasData.convertToElements(
-            nodeView.tree, getTmpElem(), html);
-        AbstractHasData.replaceChildren(
-            nodeView.tree, childContainer, newChildren, start, html);
+ Element newChildren = AbstractHasData.convertToElements(nodeView.tree,
+            getTmpElem(), html);
+        AbstractHasData.replaceChildren(nodeView.tree, childContainer,
+            newChildren, start, html);

         loadChildState(values, 0, savedViews);
       }

       public void resetFocus() {
+        if (nodeView.keyboardSelectedIndex != -1) {
+          nodeView.keyboardEnter(nodeView.keyboardSelectedIndex,
+              nodeView.keyboardFocused);
+        }
       }

       public void setLoadingState(LoadingState state) {
@@ -294,13 +230,14 @@
         int end = start + len;
         int childCount = nodeView.getChildCount();
         ProvidesKey<C> providesKey = nodeInfo.getProvidesKey();
- Element childElem = nodeView.ensureChildContainer().getFirstChildElement();
+        Element childElem =
+          nodeView.ensureChildContainer().getFirstChildElement();
         for (int i = start; i < end; i++) {
           C childValue = values.get(i - start);
-          CellTreeNodeView<C> child = nodeView.createTreeNodeView(
-              nodeInfo, childElem, childValue, null);
-          CellTreeNodeView<?> savedChild = savedViews.remove(
-              providesKey.getKey(childValue));
+          CellTreeNodeView<C> child = nodeView.createTreeNodeView(nodeInfo,
+              childElem, childValue, null);
+          CellTreeNodeView<?> savedChild =
+            savedViews.remove(providesKey.getKey(childValue));
           // Copy the saved child's state into the new child
           if (savedChild != null) {
             child.animationFrame = savedChild.animationFrame;
@@ -344,8 +281,8 @@
        * @param start the start index
        * @return the map of open nodes
        */
-      private Map<Object, CellTreeNodeView<?>> saveChildState(
-          List<C> values, int start) {
+ private Map<Object, CellTreeNodeView<?>> saveChildState(List<C> values,
+          int start) {
         // Ensure that we have a children array.
         if (nodeView.children == null) {
           nodeView.children = new ArrayList<CellTreeNodeView<?>>();
@@ -355,8 +292,8 @@
         int len = values.size();
         int end = start + len;
         int childCount = nodeView.getChildCount();
-        Map<Object, CellTreeNodeView<?>> openNodes = new HashMap<
-            Object, CellTreeNodeView<?>>();
+        Map<Object, CellTreeNodeView<?>> openNodes =
+          new HashMap<Object, CellTreeNodeView<?>>();
         for (int i = start; i < end && i < childCount; i++) {
           CellTreeNodeView<?> child = nodeView.getChildNode(i);
           // Ignore child nodes that are closed.
@@ -367,8 +304,8 @@

         // Trim the saved views down to the children that still exists.
         ProvidesKey<C> providesKey = nodeInfo.getProvidesKey();
-        Map<Object, CellTreeNodeView<?>> savedViews = new HashMap<
-            Object, CellTreeNodeView<?>>();
+        Map<Object, CellTreeNodeView<?>> savedViews =
+          new HashMap<Object, CellTreeNodeView<?>>();
         for (C childValue : values) {
           // Remove any child elements that correspond to prior children
           // so the call to setInnerHtml will not destroy them
@@ -384,7 +321,9 @@
     }

     private final Cell<C> cell;
+
     private final int defaultPageSize;
+    private HandlerManager handlerManger = new HandlerManager(this);
     private final NodeInfo<C> nodeInfo;
     private CellTreeNodeView<?> nodeView;
     private final HasDataPresenter<C> presenter;
@@ -396,8 +335,8 @@
       this.nodeView = nodeView;
       cell = nodeInfo.getCell();

-      presenter = new HasDataPresenter<C>(
-          this, new View(nodeView.ensureChildContainer()), pageSize);
+      presenter = new HasDataPresenter<C>(this, new View(
+          nodeView.ensureChildContainer()), pageSize);

       // Use a pager to update buttons.
presenter.addRowCountChangeHandler(new RowCountChangeEvent.Handler() {
@@ -492,6 +431,70 @@
       nodeView.listView = this;
     }
   }
+
+  /**
+   * The element used in place of an image when a node has no children.
+   */
+ private static final String LEAF_IMAGE = "<div style='position:absolute;display:none;'></div>";
+
+  /**
+   * The temporary element used to render child items.
+   */
+  private static com.google.gwt.user.client.Element tmpElem;
+
+  /**
+   * Returns the element that parents the cell contents of the node.
+   *
+   * @param nodeElem the element that represents the node
+   * @return the cell parent within the node
+   */
+  private static Element getCellParent(Element nodeElem) {
+ return getSelectionElement(nodeElem).getFirstChildElement().getChild(1).cast();
+  }
+
+  /**
+   * Returns the element that selection is applied to.
+   *
+   * @param nodeElem the element that represents the node
+   * @return the cell parent within the node
+   */
+  private static Element getImageElement(Element nodeElem) {
+ return getSelectionElement(nodeElem).getFirstChildElement().getFirstChildElement();
+  }
+
+  /**
+   * Returns the element that selection is applied to.
+   *
+   * @param nodeElem the element that represents the node
+   * @return the cell parent within the node
+   */
+  private static Element getSelectionElement(Element nodeElem) {
+    return nodeElem.getFirstChildElement();
+  }
+
+  /**
+   * @return the temporary element used to create elements
+   */
+  private static com.google.gwt.user.client.Element getTmpElem() {
+    if (tmpElem == null) {
+      tmpElem = Document.get().createDivElement().cast();
+    }
+    return tmpElem;
+  }
+
+  /**
+   * Show or hide an element.
+   *
+   * @param element the element to show or hide
+   * @param show true to show, false to hide
+   */
+  private static void showOrHide(Element element, boolean show) {
+    if (show) {
+      element.getStyle().clearDisplay();
+    } else {
+      element.getStyle().setDisplay(Display.NONE);
+    }
+  }

   /**
    * True during the time a node should be animated.
@@ -531,6 +534,21 @@
    */
   private Element emptyMessageElem;

+  /**
+   * True if the keyboard selection has focus.
+   */
+  private boolean keyboardFocused;
+
+  /**
+   * The index of the keyboard selection within this node's children.
+   */
+  private int keyboardSelectedIndex = -1;
+
+  /**
+   * The parent element of the current keyboard selection, or null.
+   */
+  private Element keyboardSelection;
+
   /**
    * The list view used to display the nodes.
    */
@@ -602,6 +620,10 @@
   public CellTreeNodeView<?> getChildNode(int childIndex) {
     return children.get(childIndex);
   }
+
+  public boolean isLeaf() {
+    return tree.getTreeViewModel().isLeaf(value);
+  }

   /**
    * Check whether or not this node is open.
@@ -632,10 +654,16 @@

         // Sink events for the new node.
         if (nodeInfo != null) {
+          Set<String> eventsToSink = new HashSet<String>();
+          // Listen for focus and blur for keyboard navigation
+          eventsToSink.add("focus");
+          eventsToSink.add("blur");
+
Set<String> consumedEvents = nodeInfo.getCell().getConsumedEvents();
           if (consumedEvents != null) {
-            CellBasedWidgetImpl.get().sinkEvents(tree, consumedEvents);
-          }
+            eventsToSink.addAll(consumedEvents);
+          }
+          CellBasedWidgetImpl.get().sinkEvents(tree, eventsToSink);
         }
       }

@@ -658,6 +686,7 @@
       cleanup();
       tree.maybeAnimateTreeNode(this);
       updateImage(false);
+      keyboardExit();
     }
   }

@@ -698,8 +727,8 @@
    * @param viewData view data associated with the node
    * @return a TreeNodeView of suitable type
    */
-  protected <C> CellTreeNodeView<C> createTreeNodeView(
- NodeInfo<C> nodeInfo, Element childElem, C childValue, Object viewData) { + protected <C> CellTreeNodeView<C> createTreeNodeView(NodeInfo<C> nodeInfo,
+      Element childElem, C childValue, Object viewData) {
return new CellTreeNodeView<C>(tree, this, nodeInfo, childElem, childValue);
   }

@@ -712,7 +741,8 @@
     if (parentNodeInfo != null) {
       Cell<T> parentCell = parentNodeInfo.getCell();
       String eventType = event.getType();
- SelectionModel<? super T> selectionModel = parentNodeInfo.getSelectionModel();
+      SelectionModel<? super T> selectionModel =
+        parentNodeInfo.getSelectionModel();

       // Update selection.
       if (selectionModel != null && "click".equals(eventType)
@@ -726,8 +756,8 @@
       Object key = getValueKey();
       Set<String> consumedEvents = parentCell.getConsumedEvents();
       if (consumedEvents != null && consumedEvents.contains(eventType)) {
-        parentCell.onBrowserEvent(
- cellParent, value, key, event, parentNodeInfo.getValueUpdater());
+        parentCell.onBrowserEvent(cellParent, value, key, event,
+            parentNodeInfo.getValueUpdater());
       }
     }
   }
@@ -764,8 +794,8 @@
    * @param <C> the child data type of the node
    */
   protected <C> void onOpen(final NodeInfo<C> nodeInfo) {
-    NodeCellList<C> view = new NodeCellList<C>(
-        nodeInfo, this, tree.getDefaultNodeSize());
+    NodeCellList<C> view = new NodeCellList<C>(nodeInfo, this,
+        tree.getDefaultNodeSize());
     listView = view;
     view.setSelectionModel(nodeInfo.getSelectionModel());
     nodeInfo.setDataDisplay(view);
@@ -826,10 +856,25 @@
     }
     return contentContainer;
   }
+
+  int getKeyboardSelectedIndex() {
+    return keyboardSelectedIndex;
+  }
+
+  /**
+   * Return the parent node, or null if this node is the root.
+   */
+  CellTreeNodeView<?> getParentNode() {
+    return parentNode;
+  }

   Element getShowMoreElement() {
     return showMoreElem;
   }
+
+  int indexOf(CellTreeNodeView<?> child) {
+    return children.indexOf(child);
+  }

   /**
    * Check if this node is a root node.
@@ -839,12 +884,122 @@
   boolean isRootNode() {
     return parentNode == null;
   }
+
+  /**
+   * The user has "tabbed" away from the node -- don't force refocus when
+   * re-rendering.
+   */
+  void keyboardBlur() {
+    keyboardFocused = false;
+  }
+
+  /**
+ * Handle a keyboard navigation event to move down one item. Returns true if
+   * movement was possible (the current item was not the last child of its
+   * parent).
+   *
+   * @return true if the selection moved
+   */
+  boolean keyboardDown() {
+    Element next = keyboardSelection.getNextSiblingElement();
+    if (next != null) {
+      keyboardExit();
+      keyboardEnterAtElement(next, true);
+      keyboardSelectedIndex++;
+      return true;
+    }
+    return false;
+  }
+
+  /**
+ * Handle a keyboard event to move focus into the current item list at the
+   * child that contains the given Element.
+   *
+   * @param focus if true, focus on the element
+   */
+  void keyboardEnter(Element element, boolean focus) {
+    Element item = ensureChildContainer().getFirstChildElement();
+    int index = 0;
+    boolean found = false;
+    for (int i = 0; i < getChildCount(); i++) {
+      if (item.isOrHasChild(element)) {
+        found = true;
+        break;
+      }
+      item = item.getNextSiblingElement();
+      index++;
+    }
+    if (found) {
+      keyboardEnterAtElement(item, focus);
+      keyboardSelectedIndex = index;
+    }
+  }
+
+  /**
+ * Handle a keyboard event to move focus into the current item list at the
+   * given child index.
+   *
+   * @param focus if true, focus on the element
+   */
+  void keyboardEnter(int index, boolean focus) {
+    if (index < 0 || index >= getChildCount()) {
+      throw new IllegalArgumentException("Index out of range: " + index);
+    }
+    Element item = ensureChildContainer().getChild(index).cast();
+    keyboardEnterAtElement(item, focus);
+    keyboardSelectedIndex = index;
+  }
+
+  /**
+   * Handle a keyboard event to move focus away from the current item.
+   */
+  void keyboardExit() {
+    if (keyboardSelection == null) {
+      return;
+    }
+    Element child =
+      keyboardSelection.getFirstChildElement().getFirstChildElement();
+    child.removeAttribute("tabIndex");
+    child.removeClassName(tree.getStyle().keyboardSelectedItem());
+    keyboardSelection = null;
+    keyboardSelectedIndex = -1;
+    keyboardFocused = false;
+  }
+
+  void keyboardFocus() {
+    keyboardFocused = true;
+  }
+
+  /**
+ * Handle a keyboard navigation event to move up one item. Returns true if
+   * movement was possible (the current item was not the first child of its
+   * parent).
+   *
+   * @return true if the selection moved
+   */
+  boolean keyboardUp() {
+    Element prev =
+      keyboardSelection.getParentElement().getFirstChildElement();
+    Element next = prev.getNextSiblingElement();
+    while (next != null && next != keyboardSelection) {
+      prev = next;
+      next = next.getNextSiblingElement();
+    }
+    if (next == keyboardSelection) {
+      int index = keyboardSelectedIndex;
+      keyboardExit();
+      keyboardEnterAtElement(prev, true);
+      keyboardSelectedIndex = index - 1;
+      return true;
+    }
+    return false;
+  }

   void showFewer() {
     Range range = listView.getVisibleRange();
     int defaultPageSize = listView.getDefaultPageSize();
-    int maxSize = Math.max(
-        defaultPageSize, range.getLength() - defaultPageSize);
+    int maxSize = Math.max(defaultPageSize,
+        range.getLength() - defaultPageSize);
     listView.setVisibleRange(range.getStart(), maxSize);
   }

@@ -853,6 +1008,19 @@
     int pageSize = range.getLength() + listView.getDefaultPageSize();
     listView.setVisibleRange(range.getStart(), pageSize);
   }
+
+  private void keyboardEnterAtElement(Element item, boolean focus) {
+    if (item != null) {
+      Element child = item.getFirstChildElement().getFirstChildElement();
+      child.addClassName(tree.getStyle().keyboardSelectedItem());
+      child.setTabIndex(0);
+      if (focus) {
+        child.focus();
+      }
+      keyboardSelection = item;
+      keyboardFocused = focus;
+    }
+  }

   /**
    * Update the image based on the current state.
@@ -869,8 +1037,8 @@
     boolean isTopLevel = parentNode.isRootNode();
     String html = tree.getClosedImageHtml(isTopLevel);
     if (open) {
- html = isLoading ? tree.getLoadingImageHtml() : tree.getOpenImageHtml(
-          isTopLevel);
+      html = isLoading ? tree.getLoadingImageHtml()
+          : tree.getOpenImageHtml(isTopLevel);
     }
     if (nodeInfoLoaded && nodeInfo == null) {
       html = LEAF_IMAGE;

--
http://groups.google.com/group/Google-Web-Toolkit-Contributors

Reply via email to