Bug 1265686 - Add PageUp/PageDown/Home/End navigation in AbstractTreeItem; r=gregtatum
authorLuke Chang <lchang@mozilla.com>
Wed, 13 Jul 2016 19:12:31 +0800
changeset 389861 b0b3b5145e8f3c82794c6078143f91a3b4dc5776
parent 389860 d3c71a72e5919852da96555b8d178790cf916f3e
child 389862 98a277292d0b8a76e068a17d23fa0defda4334d1
push id23554
push userpbrosset@mozilla.com
push dateWed, 20 Jul 2016 09:13:33 +0000
reviewersgregtatum
bugs1265686
milestone50.0a1
Bug 1265686 - Add PageUp/PageDown/Home/End navigation in AbstractTreeItem; r=gregtatum MozReview-Commit-ID: 1LkKblI5ghz
devtools/client/performance/test/browser.ini
devtools/client/performance/test/browser_perf-tree-abstract-05.js
devtools/client/shared/widgets/AbstractTreeItem.jsm
--- a/devtools/client/performance/test/browser.ini
+++ b/devtools/client/performance/test/browser.ini
@@ -97,16 +97,17 @@ support-files =
 [browser_perf-telemetry-02.js]
 [browser_perf-telemetry-03.js]
 [browser_perf-telemetry-04.js]
 # [browser_perf-theme-toggle.js] TODO bug 1256350
 [browser_perf-tree-abstract-01.js]
 [browser_perf-tree-abstract-02.js]
 [browser_perf-tree-abstract-03.js]
 [browser_perf-tree-abstract-04.js]
+[browser_perf-tree-abstract-05.js]
 [browser_perf-tree-view-01.js]
 [browser_perf-tree-view-02.js]
 [browser_perf-tree-view-03.js]
 [browser_perf-tree-view-04.js]
 [browser_perf-tree-view-05.js]
 [browser_perf-tree-view-06.js]
 [browser_perf-tree-view-07.js]
 [browser_perf-tree-view-08.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/test/browser_perf-tree-abstract-05.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the abstract tree base class for the profiler's tree view
+ * supports PageUp/PageDown/Home/End keys.
+ */
+
+const { appendAndWaitForPaint } = require("devtools/client/performance/test/helpers/dom-utils");
+const { synthesizeCustomTreeClass } = require("devtools/client/performance/test/helpers/synth-utils");
+const { once } = require("devtools/client/performance/test/helpers/event-utils");
+
+add_task(function* () {
+  let { MyCustomTreeItem } = synthesizeCustomTreeClass();
+
+  let container = document.createElement("vbox");
+  container.style.height = '100%';
+  container.style.overflow = 'scroll';
+  yield appendAndWaitForPaint(gBrowser.selectedBrowser.parentNode, container);
+
+  let myDataSrc = {
+    label: "root",
+    children: []
+  };
+
+  for (let i = 0; i < 1000; i++) {
+    myDataSrc.children.push({
+      label: "child-" + i,
+      children: []
+    });
+  }
+
+  let treeRoot = new MyCustomTreeItem(myDataSrc, { parent: null });
+  treeRoot.attachTo(container);
+  treeRoot.focus();
+  treeRoot.expand();
+
+  is(document.commandDispatcher.focusedElement, treeRoot.target,
+    "The root node is focused.");
+
+  // Test HOME and END
+
+  key("VK_END");
+  is(document.commandDispatcher.focusedElement,
+    treeRoot.getChild(myDataSrc.children.length - 1).target,
+    "The last node is focused.");
+
+  key("VK_HOME");
+  is(document.commandDispatcher.focusedElement, treeRoot.target,
+    "The first (root) node is focused.");
+
+  // Test PageUp and PageDown
+
+  let nodesPerPageSize = treeRoot._getNodesPerPageSize();
+
+  key("VK_PAGE_DOWN");
+  is(document.commandDispatcher.focusedElement,
+    treeRoot.getChild(nodesPerPageSize - 1).target,
+    "The first node in the second page is focused.");
+
+  key("VK_PAGE_DOWN");
+  is(document.commandDispatcher.focusedElement,
+    treeRoot.getChild(nodesPerPageSize * 2 - 1).target,
+    "The first node in the third page is focused.");
+
+  key("VK_PAGE_UP");
+  is(document.commandDispatcher.focusedElement,
+    treeRoot.getChild(nodesPerPageSize - 1).target,
+    "The first node in the second page is focused.");
+
+  key("VK_PAGE_UP");
+  is(document.commandDispatcher.focusedElement, treeRoot.target,
+    "The first (root) node is focused.");
+
+  // Test PageUp in the middle of the first page
+
+  let middleIndex = Math.floor(nodesPerPageSize / 2);
+
+  treeRoot.getChild(middleIndex).target.focus();
+  is(document.commandDispatcher.focusedElement,
+    treeRoot.getChild(middleIndex).target,
+    "The middle node in the first page is focused.");
+
+  key("VK_PAGE_UP");
+  is(document.commandDispatcher.focusedElement, treeRoot.target,
+    "The first (root) node is focused.");
+
+  // Test PageDown in the middle of the last page
+
+  middleIndex = Math.ceil(myDataSrc.children.length - middleIndex);
+
+  treeRoot.getChild(middleIndex).target.focus();
+  is(document.commandDispatcher.focusedElement,
+    treeRoot.getChild(middleIndex).target,
+    "The middle node in the last page is focused.");
+
+  key("VK_PAGE_DOWN");
+  is(document.commandDispatcher.focusedElement,
+    treeRoot.getChild(myDataSrc.children.length - 1).target,
+    "The last node is focused.");
+
+  container.remove();
+});
--- a/devtools/client/shared/widgets/AbstractTreeItem.jsm
+++ b/devtools/client/shared/widgets/AbstractTreeItem.jsm
@@ -473,17 +473,58 @@ AbstractTreeItem.prototype = {
    * @param number delta
    *        The offset from this item to the target item.
    * @return nsIDOMNode
    *         The element displaying the target item at the specified offset.
    */
   _getSiblingAtDelta: function (delta) {
     let childNodes = this._containerNode.childNodes;
     let indexOfSelf = Array.indexOf(childNodes, this._targetNode);
-    return childNodes[indexOfSelf + delta];
+    if (indexOfSelf + delta >= 0) {
+      return childNodes[indexOfSelf + delta];
+    }
+    return undefined;
+  },
+
+  _getNodesPerPageSize: function() {
+    let childNodes = this._containerNode.childNodes;
+    let nodeHeight = this._getHeight(childNodes[childNodes.length - 1]);
+    let containerHeight = this.bounds.height;
+    return Math.ceil(containerHeight / nodeHeight);
+  },
+
+  _getHeight: function(elem) {
+    let win = this.document.defaultView;
+    let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIDOMWindowUtils);
+    return utils.getBoundsWithoutFlushing(elem).height;
+  },
+
+  /**
+   * Focuses the first item in this tree.
+   */
+  _focusFirstNode: function () {
+    let childNodes = this._containerNode.childNodes;
+    // The root node of the tree may be hidden in practice, so uses for-loop
+    // here to find the next visible node.
+    for (let i = 0; i < childNodes.length; i++) {
+      // The height will be 0 if an element is invisible.
+      if (this._getHeight(childNodes[i])) {
+        childNodes[i].focus();
+        return;
+      }
+    }
+  },
+
+  /**
+   * Focuses the last item in this tree.
+   */
+  _focusLastNode: function () {
+    let childNodes = this._containerNode.childNodes;
+    childNodes[childNodes.length - 1].focus();
   },
 
   /**
    * Focuses the next item in this tree.
    */
   _focusNextNode: function () {
     let nextElement = this._getSiblingAtDelta(1);
     if (nextElement) nextElement.focus(); // nsIDOMNode
@@ -565,16 +606,46 @@ AbstractTreeItem.prototype = {
 
       case e.DOM_VK_RIGHT:
         if (!this._expanded) {
           this.expand();
         } else {
           this._focusNextNode();
         }
         return;
+
+      case e.DOM_VK_PAGE_UP:
+        let pageUpElement =
+          this._getSiblingAtDelta(-this._getNodesPerPageSize());
+        // There's a chance that the root node is hidden. In this case, its
+        // height will be 0.
+        if (pageUpElement && this._getHeight(pageUpElement)) {
+          pageUpElement.focus();
+        } else {
+          this._focusFirstNode();
+        }
+        return;
+
+      case e.DOM_VK_PAGE_DOWN:
+        let pageDownElement =
+          this._getSiblingAtDelta(this._getNodesPerPageSize());
+        if (pageDownElement) {
+          pageDownElement.focus();
+        } else {
+          this._focusLastNode();
+        }
+        return;
+
+      case e.DOM_VK_HOME:
+        this._focusFirstNode();
+        return;
+
+      case e.DOM_VK_END:
+        this._focusLastNode();
+        return;
     }
   },
 
   /**
    * Handler for the "focus" event on the element displaying this tree item.
    */
   _onFocus: function (e) {
     this._rootItem.emit("focus", this);