Bug 1068852 - Highlight search suggestions on hover/mouseover. r=MattN
authorDrew Willcoxon :adw <adw@mozilla.com>
Wed, 17 Sep 2014 13:37:00 +0200
changeset 230469 37f902fba2dedc79f4dc43ffbd77a7a165f58746
parent 230468 516ea3a530816f01da23fe80be4166d1e97d02c3
child 230470 79bf6ebacb09d14111055dddc445330da32e1517
push id611
push userraliiev@mozilla.com
push dateMon, 05 Jan 2015 23:23:16 +0000
treeherdermozilla-release@345cd3b9c445 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1068852
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1068852 - Highlight search suggestions on hover/mouseover. r=MattN
browser/base/content/searchSuggestionUI.js
browser/base/content/test/general/browser_searchSuggestionUI.js
browser/base/content/test/general/searchSuggestionUI.js
--- a/browser/base/content/searchSuggestionUI.js
+++ b/browser/base/content/searchSuggestionUI.js
@@ -91,46 +91,46 @@ SearchSuggestionUIController.prototype =
     // Update the table's rows, and the input when there is a selection.
     this._table.removeAttribute("aria-activedescendant");
     for (let i = 0; i < this._table.children.length; i++) {
       let row = this._table.children[i];
       if (i == idx) {
         row.classList.add("selected");
         row.firstChild.setAttribute("aria-selected", "true");
         this._table.setAttribute("aria-activedescendant", row.firstChild.id);
-        this.input.value = this.suggestionAtIndex(i);
       }
       else {
         row.classList.remove("selected");
         row.firstChild.setAttribute("aria-selected", "false");
       }
     }
-
-    // Update the input when there is no selection.
-    if (idx < 0) {
-      this.input.value = this._stickyInputValue;
-    }
   },
 
   get numSuggestions() {
     return this._table.children.length;
   },
 
+  selectAndUpdateInput: function (idx) {
+    this.selectedIndex = idx;
+    this.input.value = idx >= 0 ? this.suggestionAtIndex(idx) :
+                       this._stickyInputValue;
+  },
+
   suggestionAtIndex: function (idx) {
     let row = this._table.children[idx];
     return row ? row.textContent : null;
   },
 
   deleteSuggestionAtIndex: function (idx) {
     // Only form history suggestions can be deleted.
     if (this.isFormHistorySuggestionAtIndex(idx)) {
       let suggestionStr = this.suggestionAtIndex(idx);
       this._sendMsg("RemoveFormHistoryEntry", suggestionStr);
       this._table.children[idx].remove();
-      this.selectedIndex = -1;
+      this.selectAndUpdateInput(-1);
     }
   },
 
   isFormHistorySuggestionAtIndex: function (idx) {
     let row = this._table.children[idx];
     return row && row.classList.contains("formHistory");
   },
 
@@ -145,17 +145,17 @@ SearchSuggestionUIController.prototype =
   _onInput: function () {
     if (this.input.value) {
       this._getSuggestions();
     }
     else {
       this._stickyInputValue = "";
       this._hideSuggestions();
     }
-    this.selectedIndex = -1;
+    this.selectAndUpdateInput(-1);
   },
 
   _onKeypress: function (event) {
     let selectedIndexDelta = 0;
     switch (event.keyCode) {
     case event.DOM_VK_UP:
       if (this.numSuggestions) {
         selectedIndexDelta = -1;
@@ -203,31 +203,35 @@ SearchSuggestionUIController.prototype =
       // Update the selection.
       let newSelectedIndex = this.selectedIndex + selectedIndexDelta;
       if (newSelectedIndex < -1) {
         newSelectedIndex = this.numSuggestions - 1;
       }
       else if (this.numSuggestions <= newSelectedIndex) {
         newSelectedIndex = -1;
       }
-      this.selectedIndex = newSelectedIndex;
+      this.selectAndUpdateInput(newSelectedIndex);
 
       // Prevent the input's caret from moving.
       event.preventDefault();
     }
   },
 
   _onFocus: function () {
     this._speculativeConnect();
   },
 
   _onBlur: function () {
     this._hideSuggestions();
   },
 
+  _onMousemove: function (event) {
+    this.selectedIndex = this._indexOfTableRowOrDescendent(event.target);
+  },
+
   _onMousedown: function (event) {
     let idx = this._indexOfTableRowOrDescendent(event.target);
     let suggestion = this.suggestionAtIndex(idx);
     this._stickyInputValue = suggestion;
     this.input.value = suggestion;
     this.input.setAttribute("selection-index", idx);
     this.input.setAttribute("selection-kind", "mouse");
     this._hideSuggestions();
@@ -295,16 +299,17 @@ SearchSuggestionUIController.prototype =
     }
   },
 
   _makeTableRow: function (type, suggestionStr, currentRow, searchWords) {
     let row = document.createElementNS(HTML_NS, "tr");
     row.classList.add("searchSuggestionRow");
     row.classList.add(type);
     row.setAttribute("role", "presentation");
+    row.addEventListener("mousemove", this);
     row.addEventListener("mousedown", this);
 
     let entry = document.createElementNS(HTML_NS, "td");
     entry.classList.add("searchSuggestionEntry");
     entry.setAttribute("role", "option");
     entry.id = this._idPrefix + SUGGESTION_ID_PREFIX + currentRow;
     entry.setAttribute("aria-selected", "false");
 
@@ -338,17 +343,17 @@ SearchSuggestionUIController.prototype =
   },
 
   _hideSuggestions: function () {
     this.input.setAttribute("aria-expanded", "false");
     this._table.hidden = true;
     while (this._table.firstElementChild) {
       this._table.firstElementChild.remove();
     }
-    this.selectedIndex = -1;
+    this.selectAndUpdateInput(-1);
   },
 
   _indexOfTableRowOrDescendent: function (row) {
     while (row && row.localName != "tr") {
       row = row.parentNode;
     }
     if (!row) {
       throw new Error("Element is not a row");
--- a/browser/base/content/test/general/browser_searchSuggestionUI.js
+++ b/browser/base/content/test/general/browser_searchSuggestionUI.js
@@ -97,16 +97,24 @@ add_task(rightArrowOrReturn("VK_RIGHT"))
 add_task(rightArrowOrReturn("VK_RETURN"));
 
 add_task(function* mouse() {
   yield setUp();
 
   let state = yield msg("key", { key: "x", waitForSuggestions: true });
   checkState(state, "x", ["xfoo", "xbar"], -1);
 
+  // Mouse over the first suggestion.
+  state = yield msg("mousemove", 0);
+  checkState(state, "x", ["xfoo", "xbar"], 0);
+
+  // Mouse over the second suggestion.
+  state = yield msg("mousemove", 1);
+  checkState(state, "x", ["xfoo", "xbar"], 1);
+
   // Click the second suggestion.  This should make it sticky.  To make sure it
   // sticks, trigger suggestions again and cycle through them by pressing Down
   // until nothing is selected again.
   state = yield msg("mousedown", 1);
   checkState(state, "xbar", [], -1);
 
   state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
   checkState(state, "xbar", ["xbarfoo", "xbarbar"], -1);
--- a/browser/base/content/test/general/searchSuggestionUI.js
+++ b/browser/base/content/test/general/searchSuggestionUI.js
@@ -32,16 +32,43 @@ let messageHandlers = {
     ack();
   },
 
   blur: function () {
     gController.input.blur();
     ack();
   },
 
+  mousemove: function (suggestionIdx) {
+    // Copied from widget/tests/test_panel_mouse_coords.xul and
+    // browser/base/content/test/newtab/head.js
+    let row = gController._table.children[suggestionIdx];
+    let rect = row.getBoundingClientRect();
+    let left = content.mozInnerScreenX + rect.left;
+    let x = left + rect.width / 2;
+    let y = content.mozInnerScreenY + rect.top + rect.height / 2;
+
+    let utils = content.SpecialPowers.getDOMWindowUtils(content);
+    let scale = utils.screenPixelsPerCSSPixel;
+
+    let widgetToolkit = content.SpecialPowers.
+                        Cc["@mozilla.org/xre/app-info;1"].
+                        getService(content.SpecialPowers.Ci.nsIXULRuntime).
+                        widgetToolkit;
+    let nativeMsg = widgetToolkit == "cocoa" ? 5 : // NSMouseMoved
+                    widgetToolkit == "windows" ? 1 : // MOUSEEVENTF_MOVE
+                    3; // GDK_MOTION_NOTIFY
+
+    row.addEventListener("mousemove", function onMove() {
+      row.removeEventListener("mousemove", onMove);
+      ack();
+    });
+    utils.sendNativeMouseEvent(x * scale, y * scale, nativeMsg, 0, null);
+  },
+
   mousedown: function (suggestionIdx) {
     gController.onClick = () => {
       gController.onClick = null;
       ack();
     };
     let row = gController._table.children[suggestionIdx];
     content.sendMouseEvent({ type: "mousedown" }, row);
   },