Bug 1324255 make webextension panels focused when opened, r?Gijs,rpl draft
authorShane Caraveo <scaraveo@mozilla.com>
Fri, 02 Feb 2018 13:46:49 -0700
changeset 750780 55e22fca4a4b19ee2e1719743dbc94347feaa075
parent 748899 fd995039d89708923b5673ecebc652967d40bd4e
push id97747
push usermixedpuppy@gmail.com
push dateFri, 02 Feb 2018 21:35:12 +0000
reviewersGijs, rpl
bugs1324255
milestone60.0a1
Bug 1324255 make webextension panels focused when opened, r?Gijs,rpl MozReview-Commit-ID: LhZSm4U8XxJ
browser/components/extensions/ExtensionPopups.jsm
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_popup_focus.js
toolkit/components/extensions/ext-browser-content.js
--- a/browser/components/extensions/ExtensionPopups.jsm
+++ b/browser/components/extensions/ExtensionPopups.jsm
@@ -75,16 +75,17 @@ class BasePopup {
 
     extension.callOnClose(this);
 
     this.contentReady = new Promise(resolve => {
       this._resolveContentReady = resolve;
     });
 
     this.viewNode.addEventListener(this.DESTROY_EVENT, this);
+    this.panel.addEventListener("popuppositioned", this, true);
 
     this.browser = null;
     this.browserLoaded = new Promise((resolve, reject) => {
       this.browserLoadedDeferred = {resolve, reject};
     });
     this.browserReady = this.createBrowser(viewNode, popupURL);
 
     BasePopup.instances.get(this.window).set(extension, this);
@@ -121,16 +122,17 @@ class BasePopup {
         this.viewNode.removeEventListener(this.DESTROY_EVENT, this);
         delete this.viewNode.customRectGetter;
       }
 
       let {panel} = this;
       if (panel) {
         panel.style.removeProperty("--arrowpanel-background");
         panel.removeAttribute("remote");
+        panel.removeEventListener("popuppositioned", this, true);
       }
 
       this.browser = null;
       this.stack = null;
       this.viewNode = null;
     });
   }
 
@@ -208,16 +210,26 @@ class BasePopup {
 
   handleEvent(event) {
     switch (event.type) {
       case this.DESTROY_EVENT:
         if (!this.destroyed) {
           this.destroy();
         }
         break;
+      case "popuppositioned":
+        if (!this.destroyed) {
+          this.browserLoaded.then(() => {
+            if (this.destroyed) {
+              return;
+            }
+            this.browser.messageManager.sendAsyncMessage("Extension:GrabFocus", {});
+          });
+        }
+        break;
     }
   }
 
   createBrowser(viewNode, popupURL = null) {
     let document = viewNode.ownerDocument;
 
     let stack = document.createElementNS(XUL_NS, "stack");
     stack.setAttribute("class", "webextension-popup-stack");
@@ -435,16 +447,17 @@ class ViewPopup extends BasePopup {
    *        browser was destroyed before it was fully loaded, and the popup
    *        should be closed, or `true` otherwise.
    */
   async attach(viewNode) {
     this.viewNode = viewNode;
     this.viewNode.addEventListener(this.DESTROY_EVENT, this);
     this.viewNode.setAttribute("closemenu", "none");
 
+    this.panel.addEventListener("popuppositioned", this, true);
     if (this.extension.remote) {
       this.panel.setAttribute("remote", "true");
     }
 
     // Wait until the browser element is fully initialized, and give it at least
     // a short grace period to finish loading its initial content, if necessary.
     //
     // In practice, the browser that was created by the mousdown handler should
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -108,16 +108,17 @@ skip-if = (os == 'win' && ccov) # Bug 14
 [browser_ext_pageAction_popup_resize.js]
 [browser_ext_pageAction_show_matches.js]
 [browser_ext_pageAction_simple.js]
 [browser_ext_pageAction_telemetry.js]
 [browser_ext_pageAction_title.js]
 [browser_ext_popup_api_injection.js]
 [browser_ext_popup_background.js]
 [browser_ext_popup_corners.js]
+[browser_ext_popup_focus.js]
 [browser_ext_popup_sendMessage.js]
 [browser_ext_popup_shutdown.js]
 [browser_ext_runtime_openOptionsPage.js]
 [browser_ext_runtime_openOptionsPage_uninstall.js]
 [browser_ext_runtime_setUninstallURL.js]
 [browser_ext_sessions_forgetClosedTab.js]
 [browser_ext_sessions_forgetClosedWindow.js]
 [browser_ext_sessions_getRecentlyClosed.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_popup_focus.js
@@ -0,0 +1,73 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const DUMMY_PAGE = "http://example.com/browser/browser/components/extensions/test/browser/file_dummy.html";
+
+add_task(async function testPageActionFocus() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "page_action": {
+        "default_popup": "popup.html",
+        "show_matches": ["<all_urls>"],
+      },
+    },
+    files: {
+      "popup.html": `<!DOCTYPE html><html><head><meta charset="utf-8">
+        <script src="popup.js"></script>
+        </head><body>
+        </body></html>
+      `,
+      "popup.js": function() {
+        window.addEventListener("focus", (event) => {
+          browser.test.assertEq(true, document.hasFocus(), "document should be focused");
+          browser.test.notifyPass("focused");
+        }, {once: true});
+      },
+    },
+  });
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, DUMMY_PAGE);
+
+  await extension.startup();
+  let finish = extension.awaitFinish("focused");
+  clickPageAction(extension);
+  await finish;
+
+  let panelId = `${makeWidgetId(extension.id)}-panel`;
+  let panel = document.getElementById(panelId);
+  panel.hidePopup();
+  await BrowserTestUtils.removeTab(tab);
+  await extension.unload();
+});
+
+add_task(async function testBrowserActionFocus() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "browser_action": {"default_popup": "popup.html"},
+    },
+    files: {
+      "popup.html": `<!DOCTYPE html><html><head><meta charset="utf-8">
+        <script src="popup.js"></script>
+        </head><body>
+        </body></html>
+      `,
+      "popup.js": function() {
+        window.addEventListener("focus", (event) => {
+          browser.test.assertEq(true, document.hasFocus(), "document should be focused");
+          browser.test.notifyPass("focused");
+        }, {once: true});
+      },
+    },
+  });
+  await extension.startup();
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, DUMMY_PAGE);
+  let finish = extension.awaitFinish("focused");
+  clickBrowserAction(extension);
+  await finish;
+
+  await closeBrowserAction(extension);
+
+  await BrowserTestUtils.removeTab(tab);
+  await extension.unload();
+});
--- a/toolkit/components/extensions/ext-browser-content.js
+++ b/toolkit/components/extensions/ext-browser-content.js
@@ -111,16 +111,20 @@ const BrowserListener = {
   receiveMessage({name, data}) {
     if (name === "Extension:InitBrowser") {
       this.init(data);
     } else if (name === "Extension:UnblockParser") {
       if (this.unblockParser) {
         this.unblockParser();
         this.blockingPromise = null;
       }
+    } else if (name === "Extension:GrabFocus") {
+      content.window.requestAnimationFrame(() => {
+        Services.focus.focusedWindow = content.window;
+      });
     }
   },
 
   loadStylesheets() {
     let winUtils = getWinUtils(content);
 
     for (let url of this.stylesheets) {
       winUtils.addSheet(ExtensionCommon.stylesheetMap.get(url), winUtils.AGENT_SHEET);
@@ -301,16 +305,17 @@ const BrowserListener = {
     }
 
     sendAsyncMessage("Extension:BrowserResized", result);
   },
 };
 
 addMessageListener("Extension:InitBrowser", BrowserListener);
 addMessageListener("Extension:UnblockParser", BrowserListener);
+addMessageListener("Extension:GrabFocus", BrowserListener);
 
 var WebBrowserChrome = {
   onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab) {
     // isAppTab is the value for the docShell that received the click.  We're
     // handling this in the top-level frame and want traversal behavior to
     // match the value for this frame rather than any subframe, so we pass
     // through the docShell.isAppTab value rather than what we were handed.
     return BrowserUtils.onBeforeLinkTraversal(originalTarget, linkURI, linkNode, docShell.isAppTab);