Bug 1400963 - Scroll selected row into view in JSON Viewer. draft
authorOriol Brufau <oriol-bugzilla@hotmail.com>
Wed, 01 Nov 2017 03:22:48 +0100
changeset 694312 c61fb8ecf6c2dcbe223bbe262f7647bf2ca883b6
parent 694164 7851d6768dfd9fe5568d1315a98f142d9bb9234f
child 739312 427e38d89234eee9ef825ec4882ae284fe2f46fb
push id88104
push userbmo:oriol-bugzilla@hotmail.com
push dateTue, 07 Nov 2017 17:21:59 +0000
bugs1400963
milestone58.0a1
Bug 1400963 - Scroll selected row into view in JSON Viewer. MozReview-Commit-ID: I7QuuGzHaiA
devtools/client/jsonview/test/browser.ini
devtools/client/jsonview/test/browser_jsonview_row_selection.js
devtools/client/jsonview/test/doc_frame_script.js
devtools/client/shared/components/tree/TreeView.js
--- a/devtools/client/jsonview/test/browser.ini
+++ b/devtools/client/jsonview/test/browser.ini
@@ -38,16 +38,17 @@ skip-if = (os == 'linux' && bits == 32 &
 [browser_jsonview_empty_object.js]
 [browser_jsonview_encoding.js]
 [browser_jsonview_filter.js]
 [browser_jsonview_invalid_json.js]
 [browser_jsonview_manifest.js]
 [browser_jsonview_nojs.js]
 [browser_jsonview_nul.js]
 [browser_jsonview_object-type.js]
+[browser_jsonview_row_selection.js]
 [browser_jsonview_save_json.js]
 support-files =
   !/toolkit/content/tests/browser/common/mockTransfer.js
 [browser_jsonview_theme.js]
 [browser_jsonview_slash.js]
 [browser_jsonview_valid_json.js]
 [browser_json_refresh.js]
 [browser_jsonview_serviceworker.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/jsonview/test/browser_jsonview_row_selection.js
@@ -0,0 +1,44 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function* () {
+  info("Test JSON row selection started");
+
+  // Create a tall JSON so that there is a scrollbar.
+  let numRows = 1e3;
+  let json = JSON.stringify(Array(numRows).fill().map((_, i) => i));
+  let tab = yield addJsonViewTab("data:application/json," + json);
+
+  is(yield getElementCount(".treeRow"), numRows, "Got the expected number of rows.");
+  yield assertRowSelected(null);
+  yield evalInContent("var scroller = $('.jsonPanelBox .panelContent')");
+  ok(yield evalInContent("scroller.clientHeight < scroller.scrollHeight"),
+     "There is a scrollbar.");
+  is(yield evalInContent("scroller.scrollTop"), 0, "Initially scrolled to the top.");
+
+  // Click to select last row.
+  yield evalInContent("$('.treeRow:last-child').click()");
+  yield assertRowSelected(numRows);
+  is(yield evalInContent("scroller.scrollTop + scroller.clientHeight"),
+     yield evalInContent("scroller.scrollHeight"), "Scrolled to the bottom.");
+
+  // Click to select 2nd row.
+  yield evalInContent("$('.treeRow:nth-child(2)').click()");
+  yield assertRowSelected(2);
+  ok(yield evalInContent("scroller.scrollTop > 0"), "Not scrolled to the top.");
+
+  // Synthetize up arrow key to select first row.
+  yield evalInContent("$('.treeTable').focus()");
+  yield BrowserTestUtils.synthesizeKey("VK_UP", {}, tab.linkedBrowser);
+  yield assertRowSelected(1);
+  is(yield evalInContent("scroller.scrollTop"), 0, "Scrolled to the top.");
+});
+
+async function assertRowSelected(rowNum) {
+  let idx = evalInContent("[].indexOf.call($$('.treeRow'), $('.treeRow.selected'))");
+  is(await idx + 1, +rowNum, `${rowNum ? "The row #" + rowNum : "No row"} is selected.`);
+}
--- a/devtools/client/jsonview/test/doc_frame_script.js
+++ b/devtools/client/jsonview/test/doc_frame_script.js
@@ -114,8 +114,13 @@ addMessageListener("Test:JsonView:WaitFo
 
   observer.observe(firstRow, { attributes: true });
 });
 
 addMessageListener("Test:JsonView:Eval", function (msg) {
   let result = content.eval(msg.data.code);
   sendAsyncMessage(msg.name, {result});
 });
+
+Components.utils.exportFunction(content.document.querySelector.bind(content.document),
+  content, {defineAs: "$"});
+Components.utils.exportFunction(content.document.querySelectorAll.bind(content.document),
+  content, {defineAs: "$$"});
--- a/devtools/client/shared/components/tree/TreeView.js
+++ b/devtools/client/shared/components/tree/TreeView.js
@@ -4,16 +4,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 // Make this available to both AMD and CJS environments
 define(function (require, exports, module) {
   const { cloneElement, Component, createFactory, DOM: dom, PropTypes } =
     require("devtools/client/shared/vendor/react");
+  const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
 
   // Reps
   const { ObjectProvider } = require("./ObjectProvider");
   const TreeRow = createFactory(require("./TreeRow"));
   const TreeHeader = createFactory(require("./TreeHeader"));
 
   const defaultProps = {
     object: null,
@@ -148,17 +149,17 @@ define(function (require, exports, modul
     }
 
     componentDidUpdate() {
       let selected = this.getSelectedRow(this.rows);
       if (!selected && this.rows.length > 0) {
         // TODO: Do better than just selecting the first row again. We want to
         // select (in order) previous, next or parent in case when selected
         // row is removed.
-        this.selectRow(this.rows[0].props.member.path);
+        this.selectRow(this.rows[0]);
       }
     }
 
     static subPath(path, subKey) {
       return path + "/" + String(subKey).replace(/[\\/]/g, "\\$&");
     }
 
     /**
@@ -250,52 +251,54 @@ define(function (require, exports, modul
         case "ArrowLeft":
           if (row && row.props.member.open) {
             this.toggle(this.state.selected);
           }
           break;
         case "ArrowDown":
           let nextRow = this.rows[index + 1];
           if (nextRow) {
-            this.selectRow(nextRow.props.member.path);
+            this.selectRow(nextRow);
           }
           break;
         case "ArrowUp":
           let previousRow = this.rows[index - 1];
           if (previousRow) {
-            this.selectRow(previousRow.props.member.path);
+            this.selectRow(previousRow);
           }
           break;
         default:
           return;
       }
 
       event.preventDefault();
     }
 
     onClickRow(nodePath, event) {
       event.stopPropagation();
       let cell = event.target.closest("td");
       if (cell && cell.classList.contains("treeLabelCell")) {
         this.toggle(nodePath);
       }
-      this.selectRow(nodePath);
+      this.selectRow(event.currentTarget);
     }
 
     getSelectedRow(rows) {
       if (!this.state.selected || rows.length === 0) {
         return null;
       }
       return rows.find(row => this.isSelected(row.props.member.path));
     }
 
-    selectRow(nodePath) {
+    selectRow(row) {
+      row = findDOMNode(row);
       this.setState(Object.assign({}, this.state, {
-        selected: nodePath
+        selected: row.id
       }));
+      row.scrollIntoView({block: "nearest"});
     }
 
     isSelected(nodePath) {
       return nodePath === this.state.selected;
     }
 
     // Filtering & Sorting