Bug 1343569 - Delay hiding the popup on blur if the search field gains focus. r=enndeakin+6102
authorJared Wein <jwein@mozilla.com>
Tue, 25 Apr 2017 20:19:42 -0400
changeset 356731 a38eaf316e46aa6aea5d3d2bad8c90c112afcbd8
parent 356730 31163f835c341bd9416a0e95b8ac2d4e8f86a44f
child 356732 4fa8134024ea8ebc35a20fbd9406f00a448b3ed3
push id89950
push usercbook@mozilla.com
push dateFri, 05 May 2017 13:26:37 +0000
treeherdermozilla-inbound@e95e5825fd40 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersenndeakin
bugs1343569
milestone55.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 1343569 - Delay hiding the popup on blur if the search field gains focus. r=enndeakin+6102 Since the order of events is 'blur' followed by 'focus', we send a message from the content process to the parent process and wait for a reply to give enough time for the parent process to signal that the 'blur' was related to the focusing of the search field. If the parent process hasn't signaled as such, then the content process will proceed with hiding the dropdown. MozReview-Commit-ID: 6ngoo9uHcsM
browser/base/content/test/forms/browser.ini
browser/base/content/test/forms/browser_selectpopup_searchfocus.js
toolkit/modules/SelectContentHelper.jsm
toolkit/modules/SelectParentHelper.jsm
--- a/browser/base/content/test/forms/browser.ini
+++ b/browser/base/content/test/forms/browser.ini
@@ -1,8 +1,9 @@
 [DEFAULT]
 support-files =
   head.js
 
 [browser_selectpopup.js]
 skip-if = os == "linux" # Bug 1329991 - test fails intermittently on Linux builds
 [browser_selectpopup_colors.js]
 skip-if = os == "linux" # Bug 1329991 - test fails intermittently on Linux builds
+[browser_selectpopup_searchfocus.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/forms/browser_selectpopup_searchfocus.js
@@ -0,0 +1,42 @@
+let SELECT =
+  "<html><body><select id='one'>";
+for (let i = 0; i < 75; i++) {
+  SELECT +=
+    `  <option>${i}${i}${i}${i}${i}</option>`;
+}
+SELECT +=
+    '  <option selected="true">{"end": "true"}</option>' +
+  "</select></body></html>";
+
+add_task(function* setup() {
+  yield SpecialPowers.pushPrefEnv({
+    "set": [
+      ["dom.select_popup_in_parent.enabled", true],
+      ["dom.forms.selectSearch", true]
+    ]
+  });
+});
+
+add_task(function* test_focus_on_search_shouldnt_close_popup() {
+  const pageUrl = "data:text/html," + escape(SELECT);
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+  let menulist = document.getElementById("ContentSelectDropdown");
+  let selectPopup = menulist.menupopup;
+
+  let popupShownPromise = BrowserTestUtils.waitForEvent(selectPopup, "popupshown");
+  yield BrowserTestUtils.synthesizeMouseAtCenter("#one", { type: "mousedown" }, gBrowser.selectedBrowser);
+  yield popupShownPromise;
+
+  let searchInput = selectPopup.querySelector("textbox[type='search']");
+  searchInput.scrollIntoView();
+  let searchFocused = BrowserTestUtils.waitForEvent(searchInput, "focus");
+  yield EventUtils.synthesizeMouseAtCenter(searchInput, {}, window);
+  yield searchFocused;
+
+  is(selectPopup.state, "open", "select popup should still be open after clicking on the search field");
+
+  yield hideSelectPopup(selectPopup, "escape");
+  yield BrowserTestUtils.removeTab(tab);
+});
+
--- a/toolkit/modules/SelectContentHelper.jsm
+++ b/toolkit/modules/SelectContentHelper.jsm
@@ -37,16 +37,17 @@ this.SelectContentHelper = function(aEle
   this.closedWithEnter = false;
   this.isOpenedViaTouch = aOptions.isOpenedViaTouch;
   this._selectBackgroundColor = null;
   this._selectColor = null;
   this._uaBackgroundColor = null;
   this._uaColor = null;
   this._uaSelectBackgroundColor = null;
   this._uaSelectColor = null;
+  this._closeAfterBlur = true;
   this.init();
   this.showDropDown();
   this._updateTimer = new DeferredTask(this._update.bind(this), 0);
 }
 
 Object.defineProperty(SelectContentHelper, "open", {
   get() {
     return gOpen;
@@ -55,16 +56,18 @@ Object.defineProperty(SelectContentHelpe
 
 this.SelectContentHelper.prototype = {
   init() {
     this.global.addMessageListener("Forms:SelectDropDownItem", this);
     this.global.addMessageListener("Forms:DismissedDropDown", this);
     this.global.addMessageListener("Forms:MouseOver", this);
     this.global.addMessageListener("Forms:MouseOut", this);
     this.global.addMessageListener("Forms:MouseUp", this);
+    this.global.addMessageListener("Forms:SearchFocused", this);
+    this.global.addMessageListener("Forms:BlurDropDown-Pong", this);
     this.global.addEventListener("pagehide", this, { mozSystemGroup: true });
     this.global.addEventListener("mozhidedropdown", this, { mozSystemGroup: true });
     this.element.addEventListener("blur", this, { mozSystemGroup: true });
     this.element.addEventListener("transitionend", this, { mozSystemGroup: true });
     let MutationObserver = this.element.ownerGlobal.MutationObserver;
     this.mut = new MutationObserver(mutations => {
       // Something changed the <select> while it was open, so
       // we'll poke a DeferredTask to update the parent sometime
@@ -76,16 +79,18 @@ this.SelectContentHelper.prototype = {
 
   uninit() {
     this.element.openInParentProcess = false;
     this.global.removeMessageListener("Forms:SelectDropDownItem", this);
     this.global.removeMessageListener("Forms:DismissedDropDown", this);
     this.global.removeMessageListener("Forms:MouseOver", this);
     this.global.removeMessageListener("Forms:MouseOut", this);
     this.global.removeMessageListener("Forms:MouseUp", this);
+    this.global.removeMessageListener("Forms:SearchFocused", this);
+    this.global.removeMessageListener("Forms:BlurDropDown-Pong", this);
     this.global.removeEventListener("pagehide", this, { mozSystemGroup: true });
     this.global.removeEventListener("mozhidedropdown", this, { mozSystemGroup: true });
     this.element.removeEventListener("blur", this, { mozSystemGroup: true });
     this.element.removeEventListener("transitionend", this, { mozSystemGroup: true });
     this.element = null;
     this.global = null;
     this.mut.disconnect();
     this._updateTimer.disarm();
@@ -264,28 +269,50 @@ this.SelectContentHelper.prototype = {
         if (message.data.onAnchor) {
           this.dispatchMouseEvent(win, this.element, "mouseup");
         }
         DOMUtils.removeContentState(this.element, kStateActive);
         if (message.data.onAnchor) {
           this.dispatchMouseEvent(win, this.element, "click");
         }
         break;
+
+      case "Forms:SearchFocused":
+        this._closeAfterBlur = false;
+        break;
+
+      case "Forms:BlurDropDown-Pong":
+        if (!this._closeAfterBlur || !gOpen) {
+          return;
+        }
+        this.global.sendAsyncMessage("Forms:HideDropDown", {});
+        this.uninit();
+        break;
     }
   },
 
   handleEvent(event) {
     switch (event.type) {
       case "pagehide":
         if (this.element.ownerDocument === event.target) {
           this.global.sendAsyncMessage("Forms:HideDropDown", {});
           this.uninit();
         }
         break;
-      case "blur":
+      case "blur": {
+        if (this.element !== event.target) {
+          break;
+        }
+        this._closeAfterBlur = true;
+        // Send a ping-pong message to make sure that we wait for
+        // enough cycles to pass from the potential focusing of the
+        // search box to disable closing-after-blur.
+        this.global.sendAsyncMessage("Forms:BlurDropDown-Ping", {});
+        break;
+      }
       case "mozhidedropdown":
         if (this.element === event.target) {
           this.global.sendAsyncMessage("Forms:HideDropDown", {});
           this.uninit();
         }
         break;
       case "transitionend":
         this._update();
--- a/toolkit/modules/SelectParentHelper.jsm
+++ b/toolkit/modules/SelectParentHelper.jsm
@@ -181,20 +181,24 @@ this.SelectParentHelper = {
         currentBrowser = null;
         currentMenulist = null;
         currentZoom = 1;
         break;
     }
   },
 
   receiveMessage(msg) {
+    if (!currentBrowser) {
+      return;
+    }
+
     if (msg.name == "Forms:UpdateDropDown") {
       // Sanity check - we'd better know what the currently
       // opened menulist is, and what browser it belongs to...
-      if (!currentMenulist || !currentBrowser) {
+      if (!currentMenulist) {
         return;
       }
 
       let scrollBox = currentMenulist.menupopup.scrollBox;
       let scrollTop = scrollBox.scrollTop;
 
       let options = msg.data.options;
       let selectedIndex = msg.data.selectedIndex;
@@ -207,39 +211,43 @@ this.SelectParentHelper = {
       let selectTextShadow = msg.data.selectTextShadow;
       this.populate(currentMenulist, options, selectedIndex,
                     currentZoom, uaBackgroundColor, uaColor,
                     uaSelectBackgroundColor, uaSelectColor,
                     selectBackgroundColor, selectColor, selectTextShadow);
 
       // Restore scroll position to what it was prior to the update.
       scrollBox.scrollTop = scrollTop;
+    } else if (msg.name == "Forms:BlurDropDown-Ping") {
+      currentBrowser.messageManager.sendAsyncMessage("Forms:BlurDropDown-Pong", {});
     }
   },
 
   _registerListeners(browser, popup) {
     popup.addEventListener("command", this);
     popup.addEventListener("popuphidden", this);
     popup.addEventListener("mouseover", this);
     popup.addEventListener("mouseout", this);
     browser.ownerGlobal.addEventListener("mouseup", this, true);
     browser.ownerGlobal.addEventListener("keydown", this, true);
     browser.ownerGlobal.addEventListener("fullscreen", this, true);
     browser.messageManager.addMessageListener("Forms:UpdateDropDown", this);
+    browser.messageManager.addMessageListener("Forms:BlurDropDown-Ping", this);
   },
 
   _unregisterListeners(browser, popup) {
     popup.removeEventListener("command", this);
     popup.removeEventListener("popuphidden", this);
     popup.removeEventListener("mouseover", this);
     popup.removeEventListener("mouseout", this);
     browser.ownerGlobal.removeEventListener("mouseup", this, true);
     browser.ownerGlobal.removeEventListener("keydown", this, true);
     browser.ownerGlobal.removeEventListener("fullscreen", this, true);
     browser.messageManager.removeMessageListener("Forms:UpdateDropDown", this);
+    browser.messageManager.removeMessageListener("Forms:BlurDropDown-Ping", this);
   },
 
 };
 
 function populateChildren(menulist, options, selectedIndex, zoom,
                           uaBackgroundColor, uaColor, sheet,
                           parentElement = null, isGroupDisabled = false,
                           adjustedTextSize = -1, addSearch = true, nthChildIndex = 1) {
@@ -456,15 +464,16 @@ function onSearchInput() {
   }
 }
 
 function onSearchFocus() {
   let searchObj = this;
   let menupopup = searchObj.parentElement;
   menupopup.parentElement.menuBoxObject.activeChild = null;
   menupopup.setAttribute("ignorekeys", "true");
+  currentBrowser.messageManager.sendAsyncMessage("Forms:SearchFocused", {});
 }
 
 function onSearchBlur() {
   let searchObj = this;
   let menupopup = searchObj.parentElement;
   menupopup.setAttribute("ignorekeys", "false");
 }