Bug 1262332, fix select popup and invalid form popup position when inside a transformed frame, r=felipe, a=sylvestre
authorNeil Deakin <neil@mozilla.com>
Wed, 11 May 2016 08:57:03 -0400
changeset 333279 68357b1b2625a5ed509998a494c55a29b758efd9
parent 333278 4cb85604e7d71e3740fa0337da782bbbaf4eddb3
child 333280 4c6311b611e9262cf36613c2a61e55d1e7ac9d04
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfelipe, sylvestre
bugs1262332
milestone48.0a2
Bug 1262332, fix select popup and invalid form popup position when inside a transformed frame, r=felipe, a=sylvestre
browser/base/content/test/general/browser_selectpopup.js
browser/modules/FormSubmitObserver.jsm
toolkit/modules/BrowserUtils.jsm
--- a/browser/base/content/test/general/browser_selectpopup.js
+++ b/browser/base/content/test/general/browser_selectpopup.js
@@ -34,26 +34,34 @@ const PAGECONTENT_SMALL =
   "</select><select id='two'>" +
   "  <option value='Three'>Three</option>" +
   "  <option value='Four'>Four</option>" +
   "</select><select id='three'>" +
   "  <option value='Five'>Five</option>" +
   "  <option value='Six'>Six</option>" +
   "</select></body></html>";
 
+const PAGECONTENT_TRANSLATED =
+  "<html><body>" +
+  "<div id='div'>" +
+  "<iframe id='frame' width='320' height='295' style='border: none;'" +
+  "        src='data:text/html,<select id=select autofocus><option>he he he</option><option>boo boo</option><option>baz baz</option></select>'" +
+  "</iframe>" +
+  "</div></body></html>";
+
 function openSelectPopup(selectPopup, withMouse, selector = "select")
 {
   let popupShownPromise = BrowserTestUtils.waitForEvent(selectPopup, "popupshown");
 
   if (withMouse) {
     return Promise.all([popupShownPromise,
                         BrowserTestUtils.synthesizeMouseAtCenter(selector, { }, gBrowser.selectedBrowser)]);
   }
 
-  setTimeout(() => EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, code: "ArrowDown" }), 1500);
+  EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, code: "ArrowDown" });
   return popupShownPromise;
 }
 
 function hideSelectPopup(selectPopup, withEscape)
 {
   let popupHiddenPromise = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
 
   if (withEscape) {
@@ -197,8 +205,73 @@ add_task(function*() {
   yield openSelectPopup(selectPopup, true, "#one");
 
   popupHiddenPromise = BrowserTestUtils.waitForEvent(selectPopup, "popuphidden");
   yield BrowserTestUtils.removeTab(tab);
   yield popupHiddenPromise;
 
   ok(true, "Popup hidden when tab is closed");
 });
+
+// This test opens a select popup that is isn't a frame and has some translations applied.
+add_task(function*() {
+  const pageUrl = "data:text/html," + escape(PAGECONTENT_TRANSLATED);
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+  let menulist = document.getElementById("ContentSelectDropdown");
+  let selectPopup = menulist.menupopup;
+
+  // First, get the position of the select popup when no translations have been applied.
+  yield openSelectPopup(selectPopup, false);
+
+  let rect = selectPopup.getBoundingClientRect();
+  let expectedX = rect.left;
+  let expectedY = rect.top;
+
+  yield hideSelectPopup(selectPopup);
+
+  // Iterate through a set of steps which each add more translation to the select's expected position.
+  let steps = [
+    [ "div", "transform: translateX(7px) translateY(13px);", 7, 13 ],
+    [ "frame", "border-top: 5px solid green; border-left: 10px solid red; border-right: 35px solid blue;", 10, 5 ],
+    [ "frame", "border: none; padding-left: 6px; padding-right: 12px; padding-top: 2px;", -4, -3 ],
+    [ "select", "margin: 9px; transform: translateY(-3px);", 9, 6 ],
+  ];
+
+  for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
+    let step = steps[stepIndex];
+
+    yield ContentTask.spawn(gBrowser.selectedBrowser, step, function*(step) {
+      return new Promise(resolve => {
+        let changedWin = content;
+
+        let elem;
+        if (step[0] == "select") {
+          changedWin = content.document.getElementById("frame").contentWindow;
+          elem = changedWin.document.getElementById("select");
+        }
+        else {
+          elem = content.document.getElementById(step[0]);
+        }
+
+        changedWin.addEventListener("MozAfterPaint", function onPaint() {
+          changedWin.removeEventListener("MozAfterPaint", onPaint);
+          resolve();
+        });
+
+        elem.style = step[1];
+      });
+    });
+
+    yield openSelectPopup(selectPopup, false);
+
+    expectedX += step[2];
+    expectedY += step[3];
+
+    let rect = selectPopup.getBoundingClientRect();
+    is(rect.left, expectedX, "step " + (stepIndex + 1) + " x");
+    is(rect.top, expectedY, "step " + (stepIndex + 1) + " y");
+
+    yield hideSelectPopup(selectPopup);
+  }
+
+  yield BrowserTestUtils.removeTab(tab);
+});
--- a/browser/modules/FormSubmitObserver.jsm
+++ b/browser/modules/FormSubmitObserver.jsm
@@ -185,17 +185,17 @@ FormSubmitObserver.prototype =
   _showPopup: function (aElement) {
     // Collect positional information and show the popup
     let panelData = {};
 
     panelData.message = this._validationMessage;
 
     // Note, this is relative to the browser and needs to be translated
     // in chrome.
-    panelData.contentRect = this._msgRect(aElement);
+    panelData.contentRect = BrowserUtils.getElementBoundingRect(aElement);
 
     // We want to show the popup at the middle of checkbox and radio buttons
     // and where the content begin for the other elements.
     let offset = 0;
     let position = "";
 
     if (aElement.tagName == 'INPUT' &&
         (aElement.type == 'radio' || aElement.type == 'checkbox')) {
@@ -227,26 +227,10 @@ FormSubmitObserver.prototype =
     if (this._content == null) {
       return true;
     }
     let target = aEvent.originalTarget;
     return (target == this._content.document ||
             (target.ownerDocument && target.ownerDocument == this._content.document));
   },
 
-  /*
-   * Return a message manager rect for the element's bounding client rect
-   * in top level browser coords.
-   */
-  _msgRect: function (aElement) {
-    let domRect = aElement.getBoundingClientRect();
-    let zoomFactor = this._getWindowUtils().fullZoom;
-    let { offsetX, offsetY } = BrowserUtils.offsetToTopLevelWindow(this._content, aElement);
-    return {
-      left: (domRect.left + offsetX) * zoomFactor,
-      top: (domRect.top + offsetY) * zoomFactor,
-      width: domRect.width * zoomFactor,
-      height: domRect.height * zoomFactor
-    };
-  },
-
   QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver])
 };
--- a/toolkit/modules/BrowserUtils.jsm
+++ b/toolkit/modules/BrowserUtils.jsm
@@ -103,60 +103,61 @@ this.BrowserUtils = {
 
   /**
    * For a given DOM element, returns its position in "screen"
    * coordinates. In a content process, the coordinates returned will
    * be relative to the left/top of the tab. In the chrome process,
    * the coordinates are relative to the user's screen.
    */
   getElementBoundingScreenRect: function(aElement) {
+    return this.getElementBoundingRect(aElement, true);
+  },
+
+  /**
+   * For a given DOM element, returns its position as an offset from the topmost
+   * window. In a content process, the coordinates returned will be relative to
+   * the left/top of the topmost content area. If aInScreenCoords is true,
+   * screen coordinates will be returned instead.
+   */
+  getElementBoundingRect: function(aElement, aInScreenCoords) {
     let rect = aElement.getBoundingClientRect();
-    let window = aElement.ownerDocument.defaultView;
+    let win = aElement.ownerDocument.defaultView;
+
+    let x = rect.left, y = rect.top;
 
     // We need to compensate for any iframes that might shift things
     // over. We also need to compensate for zooming.
-    let fullZoom = window.getInterface(Ci.nsIDOMWindowUtils).fullZoom;
+    let parentFrame = win.frameElement;
+    while (parentFrame) {
+      win = parentFrame.ownerDocument.defaultView;
+      let cstyle = win.getComputedStyle(parentFrame, "");
+
+      let framerect = parentFrame.getBoundingClientRect();
+      x += framerect.left + parseFloat(cstyle.borderLeftWidth) + parseFloat(cstyle.paddingLeft);
+      y += framerect.top + parseFloat(cstyle.borderTopWidth) + parseFloat(cstyle.paddingTop);
+
+      parentFrame = win.frameElement;
+    }
+
+    if (aInScreenCoords) {
+      x += win.mozInnerScreenX;
+      y += win.mozInnerScreenY;
+    }
+
+    let fullZoom = win.getInterface(Ci.nsIDOMWindowUtils).fullZoom;
     rect = {
-      left: (rect.left + window.mozInnerScreenX) * fullZoom,
-      top: (rect.top + window.mozInnerScreenY) * fullZoom,
+      left: x * fullZoom,
+      top: y * fullZoom,
       width: rect.width * fullZoom,
       height: rect.height * fullZoom
     };
 
     return rect;
   },
 
-  /**
-   * Given an element potentially within a subframe, calculate the offsets
-   * up to the top level browser.
-   *
-   * @param aTopLevelWindow content window to calculate offsets to.
-   * @param aElement The element in question.
-   * @return [targetWindow, offsetX, offsetY]
-   */
-  offsetToTopLevelWindow: function (aTopLevelWindow, aElement) {
-    let offsetX = 0;
-    let offsetY = 0;
-    let element = aElement;
-    while (element &&
-           element.ownerDocument &&
-           element.ownerDocument.defaultView != aTopLevelWindow) {
-      element = element.ownerDocument.defaultView.frameElement;
-      let rect = element.getBoundingClientRect();
-      offsetX += rect.left;
-      offsetY += rect.top;
-    }
-    let win = null;
-    if (element == aElement)
-      win = aTopLevelWindow;
-    else
-      win = element.contentDocument.defaultView;
-    return { targetWindow: win, offsetX: offsetX, offsetY: offsetY };
-  },
-
   onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) {
     // Don't modify non-default targets or targets that aren't in top-level app
     // tab docshells (isAppTab will be false for app tab subframes).
     if (originalTarget != "" || !isAppTab)
       return originalTarget;
 
     // External links from within app tabs should always open in new tabs
     // instead of replacing the app tab's page (Bug 575561)