Bug 1311279, when the mouse is released, check if it should be retargetted at the select element in the content process, so that a click event is received, r=mconley
authorNeil Deakin <neil@mozilla.com>
Thu, 19 Jan 2017 10:03:56 -0500
changeset 377424 5a03c87a17259a2b78ecfb207246b7b04838545e
parent 377423 442d408c09b333bdc68c6c98d89e404515f7a826
child 377425 86f0daf456830dd8a4d60724d7f9821ecd7bbcf9
push id1419
push userjlund@mozilla.com
push dateMon, 10 Apr 2017 20:44:07 +0000
treeherdermozilla-release@5e6801b73ef6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley
bugs1311279
milestone53.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 1311279, when the mouse is released, check if it should be retargetted at the select element in the content process, so that a click event is received, r=mconley
browser/base/content/test/general/browser_selectpopup.js
toolkit/modules/SelectContentHelper.jsm
toolkit/modules/SelectParentHelper.jsm
--- a/browser/base/content/test/general/browser_selectpopup.js
+++ b/browser/base/content/test/general/browser_selectpopup.js
@@ -6,17 +6,18 @@
 // to implement the dropdown list.
 
 requestLongerTimeout(2);
 
 const XHTML_DTD = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">';
 
 const PAGECONTENT =
   "<html xmlns='http://www.w3.org/1999/xhtml'>" +
-  "<body onload='gChangeEvents = 0;gInputEvents = 0; document.body.firstChild.focus()'><select oninput='gInputEvents++' onchange='gChangeEvents++'>" +
+  "<body onload='gChangeEvents = 0;gInputEvents = 0; gClickEvents = 0; document.body.firstChild.focus()'>" +
+  "<select oninput='gInputEvents++' onchange='gChangeEvents++' onclick='if (event.target == this) gClickEvents++'>" +
   "  <optgroup label='First Group'>" +
   "    <option value='One'>One</option>" +
   "    <option value='Two'>Two</option>" +
   "  </optgroup>" +
   "  <option value='Three'>Three</option>" +
   "  <optgroup label='Second Group' disabled='true'>" +
   "    <option value='Four'>Four</option>" +
   "    <option value='Five'>Five</option>" +
@@ -122,16 +123,22 @@ function getInputEvents() {
 }
 
 function getChangeEvents() {
   return ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
     return content.wrappedJSObject.gChangeEvents;
   });
 }
 
+function getClickEvents() {
+  return ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+    return content.wrappedJSObject.gClickEvents;
+  });
+}
+
 function* doSelectTests(contentType, dtd) {
   const pageUrl = "data:" + contentType + "," + escape(dtd + "\n" + PAGECONTENT);
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
 
   let menulist = document.getElementById("ContentSelectDropdown");
   let selectPopup = menulist.menupopup;
 
   yield openSelectPopup(selectPopup);
@@ -164,16 +171,17 @@ function* doSelectTests(contentType, dtd
   }
 
   EventUtils.synthesizeKey("KEY_ArrowUp", { code: "ArrowUp" });
   is(menulist.menuBoxObject.activeChild, menulist.getItemAtIndex(3), "Select item 3 again");
   is(menulist.selectedIndex, isWindows ? 3 : 1, "Select item 3 selectedIndex");
 
   is((yield getInputEvents()), 0, "Before closed - number of input events");
   is((yield getChangeEvents()), 0, "Before closed - number of change events");
+  is((yield getClickEvents()), 0, "Before closed - number of click events");
 
   EventUtils.synthesizeKey("a", { accelKey: true });
   yield ContentTask.spawn(gBrowser.selectedBrowser, { isWindows }, function(args) {
     Assert.equal(String(content.getSelection()), args.isWindows ? "Text" : "",
       "Select all while popup is open");
   });
 
   // Backspace should not go back
@@ -184,36 +192,41 @@ function* doSelectTests(contentType, dtd
   EventUtils.synthesizeKey("VK_BACK_SPACE", { });
   window.removeEventListener("keypress", handleKeyPress);
 
   yield hideSelectPopup(selectPopup);
 
   is(menulist.selectedIndex, 3, "Item 3 still selected");
   is((yield getInputEvents()), 1, "After closed - number of input events");
   is((yield getChangeEvents()), 1, "After closed - number of change events");
+  is((yield getClickEvents()), 0, "After closed - number of click events");
 
   // Opening and closing the popup without changing the value should not fire a change event.
   yield openSelectPopup(selectPopup, "click");
   yield hideSelectPopup(selectPopup, "escape");
   is((yield getInputEvents()), 1, "Open and close with no change - number of input events");
   is((yield getChangeEvents()), 1, "Open and close with no change - number of change events");
+  is((yield getClickEvents()), 1, "Open and close with no change - number of click events");
   EventUtils.synthesizeKey("VK_TAB", { });
   EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
   is((yield getInputEvents()), 1, "Tab away from select with no change - number of input events");
   is((yield getChangeEvents()), 1, "Tab away from select with no change - number of change events");
+  is((yield getClickEvents()), 1, "Tab away from select with no change - number of click events");
 
   yield openSelectPopup(selectPopup, "click");
   EventUtils.synthesizeKey("KEY_ArrowDown", { code: "ArrowDown" });
   yield hideSelectPopup(selectPopup, "escape");
   is((yield getInputEvents()), isWindows ? 2 : 1, "Open and close with change - number of input events");
   is((yield getChangeEvents()), isWindows ? 2 : 1, "Open and close with change - number of change events");
+  is((yield getClickEvents()), 2, "Open and close with change - number of click events");
   EventUtils.synthesizeKey("VK_TAB", { });
   EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
   is((yield getInputEvents()), isWindows ? 2 : 1, "Tab away from select with change - number of input events");
   is((yield getChangeEvents()), isWindows ? 2 : 1, "Tab away from select with change - number of change events");
+  is((yield getClickEvents()), 2, "Tab away from select with change - number of click events");
 
   is(selectPopup.lastChild.previousSibling.label, "Seven", "Spaces collapsed");
   is(selectPopup.lastChild.label, "\xA0\xA0Eight\xA0\xA0", "Non-breaking spaces not collapsed");
 
   yield BrowserTestUtils.removeTab(tab);
 }
 
 add_task(function* setup() {
--- a/toolkit/modules/SelectContentHelper.jsm
+++ b/toolkit/modules/SelectContentHelper.jsm
@@ -108,16 +108,25 @@ this.SelectContentHelper.prototype = {
     // The <select> was updated while the dropdown was open.
     // Let's send up a new list of options.
     this.global.sendAsyncMessage("Forms:UpdateDropDown", {
       options: this._buildOptionList(),
       selectedIndex: this.element.selectedIndex,
     });
   },
 
+  dispatchMouseEvent(win, target, eventName) {
+    let mouseEvent = new win.MouseEvent(eventName, {
+      view: win,
+      bubbles: true,
+      cancelable: true,
+    });
+    target.dispatchEvent(mouseEvent);
+  },
+
   receiveMessage(message) {
     switch (message.name) {
       case "Forms:SelectDropDownItem":
         this.element.selectedIndex = message.data.value;
         this.closedWithEnter = message.data.closedWithEnter;
         break;
 
       case "Forms:DismissedDropDown":
@@ -126,63 +135,57 @@ this.SelectContentHelper.prototype = {
           let win = this.element.ownerDocument.defaultView;
           // For ordering of events, we're using non-e10s as our guide here,
           // since the spec isn't exactly clear. In non-e10s, we fire:
           // mousedown, mouseup, input, change, click if the user clicks
           // on an element in the dropdown. If the user uses the keyboard
           // to select an element in the dropdown, we only fire input and
           // change events.
           if (!this.closedWithEnter) {
-            const MOUSE_EVENTS = ["mousedown", "mouseup"];
-            for (let eventName of MOUSE_EVENTS) {
-              let mouseEvent = new win.MouseEvent(eventName, {
-                view: win,
-                bubbles: true,
-                cancelable: true,
-              });
-              selectedOption.dispatchEvent(mouseEvent);
-            }
+            this.dispatchMouseEvent(win, selectedOption, "mousedown");
+            this.dispatchMouseEvent(win, selectedOption, "mouseup");
             DOMUtils.removeContentState(this.element, kStateActive);
           }
 
           let inputEvent = new win.UIEvent("input", {
             bubbles: true,
           });
           this.element.dispatchEvent(inputEvent);
 
           let changeEvent = new win.Event("change", {
             bubbles: true,
           });
           this.element.dispatchEvent(changeEvent);
 
           if (!this.closedWithEnter) {
-            let mouseEvent = new win.MouseEvent("click", {
-              view: win,
-              bubbles: true,
-              cancelable: true,
-            });
-            selectedOption.dispatchEvent(mouseEvent);
+            this.dispatchMouseEvent(win, selectedOption, "click");
           }
         }
 
         this.uninit();
         break;
 
       case "Forms:MouseOver":
         DOMUtils.setContentState(this.element, kStateHover);
         break;
 
       case "Forms:MouseOut":
         DOMUtils.removeContentState(this.element, kStateHover);
         break;
 
       case "Forms:MouseUp":
+        let win = this.element.ownerDocument.defaultView;
+        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;
-
     }
   },
 
   handleEvent(event) {
     switch (event.type) {
       case "pagehide":
         if (this.element.ownerDocument === event.target) {
           this.global.sendAsyncMessage("Forms:HideDropDown", {});
--- a/toolkit/modules/SelectParentHelper.jsm
+++ b/toolkit/modules/SelectParentHelper.jsm
@@ -20,16 +20,17 @@ const AUTOSCROLL_INTERVAL = 25;
 
 // Minimum elements required to show select search
 const SEARCH_MINIMUM_ELEMENTS = 40;
 
 var currentBrowser = null;
 var currentMenulist = null;
 var currentZoom = 1;
 var closedWithEnter = false;
+var selectRect;
 
 this.SelectParentHelper = {
   draggedOverPopup: false,
   scrollTimer: 0,
 
   populate(menulist, items, selectedIndex, zoom) {
     // Clear the current contents of the popup
     menulist.menupopup.textContent = "";
@@ -37,16 +38,17 @@ this.SelectParentHelper = {
     currentMenulist = menulist;
     populateChildren(menulist, items, selectedIndex, zoom);
   },
 
   open(browser, menulist, rect, isOpenedViaTouch) {
     menulist.hidden = false;
     currentBrowser = browser;
     closedWithEnter = false;
+    selectRect = rect;
     this._registerListeners(browser, menulist.menupopup);
 
     let win = browser.ownerDocument.defaultView;
 
     // Set the maximum height to show exactly MAX_ROWS items.
     let menupopup = menulist.menupopup;
     let firstItem = menupopup.firstChild;
     while (firstItem && firstItem.hidden) {
@@ -92,38 +94,56 @@ this.SelectParentHelper = {
     }
   },
 
   handleEvent(event) {
     switch (event.type) {
       case "mouseup":
         this.clearScrollTimer();
         currentMenulist.menupopup.removeEventListener("mousemove", this);
-        currentBrowser.messageManager.sendAsyncMessage("Forms:MouseUp", {});
+
+        function inRect(rect, x, y) {
+          return x >= rect.left && x <= rect.left + rect.width && y >= rect.top && y <= rect.top + rect.height;
+        }
+
+        let x = event.screenX, y = event.screenY;
+        let onAnchor = !inRect(currentMenulist.menupopup.getOuterScreenRect(), x, y) &&
+                        inRect(selectRect, x, y) && currentMenulist.menupopup.state == "open";
+        currentBrowser.messageManager.sendAsyncMessage("Forms:MouseUp", { onAnchor });
         break;
 
       case "mouseover":
         currentBrowser.messageManager.sendAsyncMessage("Forms:MouseOver", {});
         break;
 
       case "mouseout":
         currentBrowser.messageManager.sendAsyncMessage("Forms:MouseOut", {});
         break;
 
       case "mousemove":
         let menupopup = currentMenulist.menupopup;
-        let popupRect = menupopup.getOuterScreenRect();
 
         this.clearScrollTimer();
 
+        // If the user released the mouse before the popup opens, we will
+        // still be capturing, so check that the button is still pressed. If
+        // not, release the capture and do nothing else. This also handles if
+        // the dropdown was opened via the keyboard.
+        if (!(event.buttons & 1)) {
+          currentMenulist.menupopup.removeEventListener("mousemove", this);
+          menupopup.releaseCapture();
+          return;
+        }
+
         // If dragging outside the top or bottom edge of the popup, but within
         // the popup area horizontally, scroll the list in that direction. The
         // draggedOverPopup flag is used to ensure that scrolling does not start
         // until the mouse has moved over the popup first, preventing scrolling
         // while over the dropdown button.
+        let popupRect = menupopup.getOuterScreenRect();
         if (event.screenX >= popupRect.left && event.screenX <= popupRect.right) {
           if (!this.draggedOverPopup) {
             if (event.screenY > popupRect.top && event.screenY < popupRect.bottom) {
               this.draggedOverPopup = true;
             }
           }
 
           if (this.draggedOverPopup &&