Bug 1385548 - Part 1: Support tab modals in WebExtensions options_ui pages. draft
authorLuca Greco <lgreco@mozilla.com>
Mon, 18 Sep 2017 22:10:11 +0200
changeset 676714 7bfc3a5fc3a98e477a9ab8a66c3f4071e4d69ea2
parent 676713 613f64109bdef590b9748355441b3c620efa7be5
child 676715 0ef39325286a4dfebf32c094e269bc23cbb22567
push id83599
push userluca.greco@alcacoop.it
push dateMon, 09 Oct 2017 12:58:37 +0000
bugs1385548
milestone58.0a1
Bug 1385548 - Part 1: Support tab modals in WebExtensions options_ui pages. This patch introduces a stub gBrowser object which allow a WebExtensions options_ui page to open a tab modal using alert/prompt/confirm. The about:addons page is defined at toolkit level but the TabModalPromptBox is defined at browser level, and so to be able to provide a TabMobalPromptBox from the about:addons page this patch uses the implementation provided by the window that contains the about:addons tab, if any. MozReview-Commit-ID: m6khgJyMs
browser/base/content/tabbrowser.xml
toolkit/components/addoncompat/RemoteAddonsParent.jsm
toolkit/mozapps/extensions/content/extensions.js
toolkit/mozapps/extensions/test/browser/browser_inlinesettings_browser.js
toolkit/mozapps/extensions/test/browser/browser_webext_options.js
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -6094,18 +6094,20 @@
           // Focus window for beforeunload dialog so it is seen but don't
           // steal focus from other applications.
           if (event.detail &&
               event.detail.tabPrompt &&
               event.detail.inPermitUnload &&
               Services.focus.activeWindow)
             window.focus();
 
-          // Don't need to act if the tab is already selected:
-          if (tabForEvent.selected)
+          // Don't need to act if the tab is already selected or if there isn't
+          // a tab for the event (e.g. for the webextensions options_ui remote
+          // browsers embedded in the "about:addons" page):
+          if (!tabForEvent || tabForEvent.selected)
             return;
 
           // We always switch tabs for beforeunload tab-modal prompts.
           if (event.detail &&
               event.detail.tabPrompt &&
               !event.detail.inPermitUnload) {
             let docPrincipal = targetIsWindow ? event.target.document.nodePrincipal : null;
             // At least one of these should/will be non-null:
--- a/toolkit/components/addoncompat/RemoteAddonsParent.jsm
+++ b/toolkit/components/addoncompat/RemoteAddonsParent.jsm
@@ -433,20 +433,25 @@ var EventTargetParent = {
 
     if (target instanceof Ci.nsIDOMXULElement) {
       if (target.localName == "browser") {
         return target;
       } else if (target.localName == "tab") {
         return target.linkedBrowser;
       }
 
-      // Check if |target| is somewhere on the patch from the
+      // Check if |target| is somewhere on the path from the
       // <tabbrowser> up to the root element.
       let window = target.ownerGlobal;
-      if (window && target.contains(window.gBrowser)) {
+
+      // Some non-browser windows define gBrowser globals which are not elements
+      // and can't be passed to target.contains().
+      if (window &&
+          window.gBrowser instanceof Ci.nsIDOMXULElement &&
+          target.contains(window.gBrowser)) {
         return window;
       }
     }
 
     return null;
   },
 
   // When a given event fires in the child, we fire it on the
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * 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";
 
 /* import-globals-from ../../../content/contentAreaUtils.js */
 /* globals XMLStylesheetProcessingInstruction */
-/* exported UPDATES_RELEASENOTES_TRANSFORMFILE, XMLURI_PARSE_ERROR, loadView */
+/* exported UPDATES_RELEASENOTES_TRANSFORMFILE, XMLURI_PARSE_ERROR, loadView, gBrowser */
 
 var Cc = Components.classes;
 var Ci = Components.interfaces;
 var Cu = Components.utils;
 var Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
@@ -3646,25 +3646,25 @@ var gDetailView = {
 
     var rows = document.getElementById("detail-downloads").parentNode;
 
     try {
       if (this._addon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_BROWSER) {
         whenViewLoaded(async () => {
           await this._addon.startupPromise;
 
-          let browser = await this.createOptionsBrowser(rows);
+          const browserContainer = await this.createOptionsBrowser(rows);
 
           // Make sure the browser is unloaded as soon as we change views,
           // rather than waiting for the next detail view to load.
           document.addEventListener("ViewChanged", function() {
-            browser.remove();
+            browserContainer.remove();
           }, {once: true});
 
-          finish(browser);
+          finish(browserContainer);
         });
 
         if (aCallback)
           aCallback();
       } else {
         var xhr = new XMLHttpRequest();
         xhr.open("GET", this._addon.optionsURL, true);
         xhr.responseType = "xml";
@@ -3729,16 +3729,19 @@ var gDetailView = {
       let detailViewBoxObject = gDetailView.node.boxObject;
       top -= detailViewBoxObject.y;
 
       detailViewBoxObject.scrollTo(0, top);
     }
   },
 
   async createOptionsBrowser(parentNode) {
+    let stack = document.createElement("stack");
+    stack.setAttribute("id", "addon-options-prompts-stack");
+
     let browser = document.createElement("browser");
     browser.setAttribute("type", "content");
     browser.setAttribute("disableglobalhistory", "true");
     browser.setAttribute("id", "addon-options");
     browser.setAttribute("class", "inline-options-browser");
     browser.setAttribute("transparent", "true");
     browser.setAttribute("forcemessagemanager", "true");
     browser.setAttribute("selectmenulist", "ContentSelectDropdown");
@@ -3757,31 +3760,32 @@ var gDetailView = {
     if (remote) {
       browser.setAttribute("remote", "true");
       browser.setAttribute("remoteType", E10SUtils.EXTENSION_REMOTE_TYPE);
       readyPromise = promiseEvent("XULFrameLoaderCreated", browser);
     } else {
       readyPromise = promiseEvent("load", browser, true);
     }
 
-    parentNode.appendChild(browser);
+    stack.appendChild(browser);
+    parentNode.appendChild(stack);
 
     // Force bindings to apply synchronously.
     browser.clientTop;
 
     await readyPromise;
     ExtensionParent.apiManager.emit("extension-browser-inserted", browser);
 
     return new Promise(resolve => {
       let messageListener = {
         receiveMessage({name, data}) {
           if (name === "Extension:BrowserResized")
             browser.style.height = `${data.height}px`;
           else if (name === "Extension:BrowserContentLoaded")
-            resolve(browser);
+            resolve(stack);
         },
       };
 
       let mm = browser.messageManager;
       mm.loadFrameScript("chrome://extensions/content/ext-browser-content.js",
                          false);
       mm.addMessageListener("Extension:BrowserContentLoaded", messageListener);
       mm.addMessageListener("Extension:BrowserResized", messageListener);
@@ -4127,8 +4131,23 @@ var gDragDrop = {
           AddonManager.installAddonFromAOM(browser, document.documentURIObject, install);
         }, "application/x-xpinstall");
       }
     }
 
     aEvent.preventDefault();
   }
 };
+
+// Stub tabbrowser implementation for use by the tab-modal alert code
+// when an alert/prompt/confirm method is called in a WebExtensions options_ui page
+// (See Bug 1385548 for rationale).
+var gBrowser = {
+  getTabModalPromptBox(browser) {
+    const parentWindow = document.docShell.chromeEventHandler.ownerGlobal;
+
+    if (parentWindow.gBrowser) {
+      return parentWindow.gBrowser.getTabModalPromptBox(browser);
+    }
+
+    return null;
+  }
+};
--- a/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_browser.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_browser.js
@@ -91,27 +91,29 @@ async function openDetailsBrowser(addonI
   await TestUtils.topicObserved(AddonManager.OPTIONS_NOTIFICATION_DISPLAYED,
                                 (subject, data) => data == addonId);
 
   is(gManagerWindow.gViewController.currentViewId,
      `addons://detail/${encodeURIComponent(addonId)}/preferences`,
      "Current view should scroll to preferences");
 
   var browser = gManagerWindow.document.querySelector(
-    "#detail-grid > rows > .inline-options-browser");
-  var rows = browser.parentNode;
+    "#detail-grid > rows > stack > .inline-options-browser");
+  var rows = browser.parentNode.parentNode;
 
   let url = await ContentTask.spawn(browser, {}, () => content.location.href);
 
-  ok(browser, "Grid should have a browser child");
-  is(browser.localName, "browser", "Grid should have a browser child");
+  ok(browser, "Grid should have a browser descendant");
+  is(browser.localName, "browser", "Grid should have a browser descendant");
   is(url, addon.mAddon.optionsURL, "Browser has the expected options URL loaded")
 
+  is(browser.clientWidth, browser.parentNode.clientWidth,
+     "Browser should be the same width as its direct parent");
   is(browser.clientWidth, rows.clientWidth,
-     "Browser should be the same width as its parent node");
+     "Browser should be the same width as its rows ancestor");
 
   button = gManagerWindow.document.getElementById("detail-prefs-btn");
   is_element_hidden(button, "Preferences button should not be visible");
 
   return browser;
 }
 
 
--- a/toolkit/mozapps/extensions/test/browser/browser_webext_options.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_webext_options.js
@@ -25,27 +25,29 @@ async function runTest(installer) {
 
   await TestUtils.topicObserved(AddonManager.OPTIONS_NOTIFICATION_DISPLAYED,
                                 (subject, data) => data == id);
 
   is(mgrWindow.gViewController.currentViewId,
      `addons://detail/${encodeURIComponent(id)}/preferences`,
      "Current view should scroll to preferences");
 
-  var browser = mgrWindow.document.querySelector("#detail-grid > rows > .inline-options-browser");
+  var browser = mgrWindow.document.querySelector("#detail-grid > rows > stack > .inline-options-browser");
   var rows = browser.parentNode;
 
   let url = await ContentTask.spawn(browser, {}, () => content.location.href);
 
-  ok(browser, "Grid should have a browser child");
-  is(browser.localName, "browser", "Grid should have a browser child");
+  ok(browser, "Grid should have a browser descendant");
+  is(browser.localName, "browser", "Grid should have a browser descendant");
   is(url, element.mAddon.optionsURL, "Browser has the expected options URL loaded")
 
+  is(browser.clientWidth, browser.parentNode.clientWidth,
+     "Browser should be the same width as its direct parent");
   is(browser.clientWidth, rows.clientWidth,
-     "Browser should be the same width as its parent node");
+     "Browser should be the same width as its rows ancestor");
 
   button = mgrWindow.document.getElementById("detail-prefs-btn");
   is_element_hidden(button, "Preferences button should not be visible");
 
   await close_manager(mgrWindow);
 
   addon.uninstall();
 }