Bug 1293099: Fill panel arrow and arrow content with browser body's background color. r=bwinton ui-r=maritz
authorKris Maglione <maglione.k@gmail.com>
Mon, 08 Aug 2016 14:06:19 -0700
changeset 334848 e472c47e7912625a1a9cadf6dcfb7bcec2afc686
parent 334847 534dc4b44b1a7ec63697a8f2f802ed6e4f0b9381
child 334849 03b9c5797a8bb07431f552aaa1acb81d0312bb43
push id10033
push userraliiev@mozilla.com
push dateMon, 19 Sep 2016 13:50:26 +0000
treeherdermozilla-aurora@5dddbefdf759 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbwinton, maritz
bugs1293099
milestone51.0a1
Bug 1293099: Fill panel arrow and arrow content with browser body's background color. r=bwinton ui-r=maritz MozReview-Commit-ID: 3xf31UgUy2A
browser/components/extensions/ext-utils.js
browser/components/extensions/test/browser/browser.ini
browser/components/extensions/test/browser/browser_ext_popup_background.js
toolkit/themes/linux/global/global.css
toolkit/themes/linux/global/popup.css
toolkit/themes/osx/global/global.css
toolkit/themes/osx/global/popup.css
toolkit/themes/windows/global/global.css
toolkit/themes/windows/global/popup.css
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -8,16 +8,20 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
                                    "@mozilla.org/content/style-sheet-service;1",
                                    "nsIStyleSheetService");
 
+XPCOMUtils.defineLazyGetter(this, "colorUtils", () => {
+  return require("devtools/shared/css-color").colorUtils;
+});
+
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 Cu.import("resource://gre/modules/AppConstants.jsm");
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 // Minimum time between two resizes.
 const RESIZE_TIMEOUT = 100;
 
@@ -90,57 +94,67 @@ class BasePopup {
       Services.scriptSecurityManager.DISALLOW_SCRIPT);
 
     this.extension = extension;
     this.popupURI = popupURI;
     this.viewNode = viewNode;
     this.browserStyle = browserStyle;
     this.window = viewNode.ownerGlobal;
 
-    this.panel = this.viewNode;
-    while (this.panel.localName != "panel") {
-      this.panel = this.panel.parentNode;
-    }
-
     this.contentReady = new Promise(resolve => {
       this._resolveContentReady = resolve;
     });
 
     this.viewNode.addEventListener(this.DESTROY_EVENT, this);
 
+    let doc = viewNode.ownerDocument;
+    let arrowContent = doc.getAnonymousElementByAttribute(this.panel, "class", "panel-arrowcontent");
+    this.borderColor = doc.defaultView.getComputedStyle(arrowContent).borderTopColor;
+
     this.browser = null;
     this.browserReady = this.createBrowser(viewNode, popupURI);
   }
 
   destroy() {
     this.browserReady.then(() => {
       this.browser.removeEventListener("DOMWindowCreated", this, true);
       this.browser.removeEventListener("load", this, true);
       this.browser.removeEventListener("DOMTitleChanged", this, true);
       this.browser.removeEventListener("DOMWindowClose", this, true);
       this.browser.removeEventListener("MozScrolledAreaChanged", this, true);
       this.viewNode.removeEventListener(this.DESTROY_EVENT, this);
       this.viewNode.style.maxHeight = "";
       this.browser.remove();
 
+      this.panel.style.setProperty("--panel-arrowcontent-background", "");
+      this.panel.style.setProperty("--panel-arrow-image-vertical", "");
+
       this.browser = null;
       this.viewNode = null;
     });
   }
 
   // Returns the name of the event fired on `viewNode` when the popup is being
   // destroyed. This must be implemented by every subclass.
   get DESTROY_EVENT() {
     throw new Error("Not implemented");
   }
 
   get fixedWidth() {
     return false;
   }
 
+  get panel() {
+    let panel = this.viewNode;
+    while (panel.localName != "panel") {
+      panel = panel.parentNode;
+    }
+    return panel;
+  }
+
   handleEvent(event) {
     switch (event.type) {
       case this.DESTROY_EVENT:
         this.destroy();
         break;
 
       case "DOMWindowCreated":
         if (event.target === this.browser.contentDocument) {
@@ -258,37 +272,38 @@ class BasePopup {
     }
   }
 
   _resizeBrowser(clearTimeout = true) {
     if (!this.browser) {
       return;
     }
 
+    let doc = this.browser.contentDocument;
+    if (!doc || !doc.documentElement) {
+      return;
+    }
+
+    let root = doc.documentElement;
+    let body = doc.body;
+    if (!body || doc.compatMode == "BackCompat") {
+      // In quirks mode, the root element is used as the scroll frame, and the
+      // body lies about its scroll geometry, and returns the values for the
+      // root instead.
+      body = root;
+    }
+
+
     if (this.fixedWidth) {
       // If we're in a fixed-width area (namely a slide-in subview of the main
       // menu panel), we need to calculate the view height based on the
       // preferred height of the content document's root scrollable element at the
       // current width, rather than the complete preferred dimensions of the
       // content window.
 
-      let doc = this.browser.contentDocument;
-      if (!doc || !doc.documentElement) {
-        return;
-      }
-
-      let root = doc.documentElement;
-      let body = doc.body;
-      if (!body || doc.compatMode == "BackCompat") {
-        // In quirks mode, the root element is used as the scroll frame, and the
-        // body lies about its scroll geometry, and returns the values for the
-        // root instead.
-        body = root;
-      }
-
       // Compensate for any offsets (margin, padding, ...) between the scroll
       // area of the body and the outer height of the document.
       let getHeight = elem => elem.getBoundingClientRect(elem).height;
       let bodyPadding = getHeight(root) - getHeight(body);
 
       let height = Math.ceil(body.scrollHeight + bodyPadding);
 
       // Figure out how much extra space we have on the side of the panel
@@ -302,16 +317,42 @@ class BasePopup {
       // Set a maximum height on the <panelview> element to our preferred
       // maximum height, so that the PanelUI resizing code can make an accurate
       // calculation. If we don't do this, the flex sizing logic will prevent us
       // from ever reporting a preferred size smaller than the height currently
       // available to us in the panel.
       height = Math.max(height, this.viewHeight);
       this.viewNode.style.maxHeight = `${height}px`;
     } else {
+      // Copy the background color of the document's body to the panel if it's
+      // fully opaque.
+      let panelBackground = "";
+      let panelArrow = "";
+
+      let background = doc.defaultView.getComputedStyle(body).backgroundColor;
+      if (background != "transparent") {
+        let bgColor = colorUtils.colorToRGBA(background);
+        if (bgColor.a == 1) {
+          panelBackground = background;
+          let borderColor = this.borderColor || background;
+
+          panelArrow = `url("data:image/svg+xml,${encodeURIComponent(`<?xml version="1.0" encoding="UTF-8"?>
+            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="10">
+              <path d="M 0,10 L 10,0 20,10 z" fill="${borderColor}"/>
+              <path d="M 1,10 L 10,1 19,10 z" fill="${background}"/>
+            </svg>
+          `)}")`;
+        }
+      }
+
+      this.panel.style.setProperty("--panel-arrowcontent-background", panelBackground);
+      this.panel.style.setProperty("--panel-arrow-image-vertical", panelArrow);
+
+
+      // Adjust the size of the browser based on its content's preferred size.
       let width, height;
       try {
         let w = {}, h = {};
         this.browser.docShell.contentViewer.getContentSize(w, h);
 
         width = w.value / this.window.devicePixelRatio;
         height = h.value / this.window.devicePixelRatio;
 
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -39,16 +39,17 @@ support-files =
 [browser_ext_incognito_popup.js]
 [browser_ext_lastError.js]
 [browser_ext_optionsPage_privileges.js]
 [browser_ext_pageAction_context.js]
 [browser_ext_pageAction_popup.js]
 [browser_ext_pageAction_popup_resize.js]
 [browser_ext_pageAction_simple.js]
 [browser_ext_popup_api_injection.js]
+[browser_ext_popup_background.js]
 [browser_ext_popup_corners.js]
 [browser_ext_runtime_openOptionsPage.js]
 [browser_ext_runtime_openOptionsPage_uninstall.js]
 [browser_ext_runtime_setUninstallURL.js]
 [browser_ext_simple.js]
 [browser_ext_tab_runtimeConnect.js]
 [browser_ext_tabs_audio.js]
 [browser_ext_tabs_captureVisibleTab.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_popup_background.js
@@ -0,0 +1,147 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function* awaitPanel(extension, win = window) {
+  let {target} = yield BrowserTestUtils.waitForEvent(win.document, "load", true, (event) => {
+    return event.target.location && event.target.location.href.endsWith("popup.html");
+  });
+
+  return target.defaultView
+               .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDocShell)
+               .chromeEventHandler;
+}
+
+function* awaitResize(browser) {
+  // Debouncing code makes this a bit racy.
+  // Try to skip the first, early resize, and catch the resize event we're
+  // looking for, but don't wait longer than a few seconds.
+
+  return Promise.race([
+    BrowserTestUtils.waitForEvent(browser, "WebExtPopupResized")
+      .then(() => BrowserTestUtils.waitForEvent(browser, "WebExtPopupResized")),
+    new Promise(resolve => setTimeout(resolve, 5000)),
+  ]);
+}
+
+add_task(function* testPopupBackground() {
+  let extension = ExtensionTestUtils.loadExtension({
+    background() {
+      browser.tabs.query({active: true, currentWindow: true}, tabs => {
+        browser.pageAction.show(tabs[0].id);
+      });
+    },
+
+    manifest: {
+      "browser_action": {
+        "default_popup": "popup.html",
+        "browser_style": false,
+      },
+
+      "page_action": {
+        "default_popup": "popup.html",
+        "browser_style": false,
+      },
+    },
+
+    files: {
+      "popup.html": `<!DOCTYPE html>
+        <html>
+          <head>
+            <meta charset="utf-8">
+          </head>
+          <body style="width: 100px; height: 100px; background-color: green;">
+          </body>
+        </html>`,
+    },
+  });
+
+  yield extension.startup();
+
+  function* testPanel(browser, standAlone) {
+    let panel = getPanelForNode(browser);
+    let arrowContent = document.getAnonymousElementByAttribute(panel, "class", "panel-arrowcontent");
+    let arrow = document.getAnonymousElementByAttribute(panel, "anonid", "arrow");
+
+    let borderColor = getComputedStyle(arrowContent).borderTopColor;
+
+    let checkArrow = (background = null) => {
+      let image = getComputedStyle(arrow).listStyleImage;
+
+      if (background == null || !standAlone) {
+        ok(image.startsWith('url("chrome://'), `We should have the built-in background image (got: ${image})`);
+        return;
+      }
+
+      if (AppConstants.platform == "mac") {
+        // Panels have a drop shadow rather than a border on OS-X, so we extend
+        // the background color through the border area instead.
+        borderColor = background;
+      }
+
+      image = decodeURIComponent(image);
+      let borderIndex = image.indexOf(`fill="${borderColor}"`);
+      let backgroundIndex = image.lastIndexOf(`fill="${background}"`);
+
+      ok(borderIndex >= 0, `Have border fill (index=${borderIndex})`);
+      ok(backgroundIndex >= 0, `Have background fill (index=${backgroundIndex})`);
+      is(getComputedStyle(arrowContent).backgroundColor, background, "Arrow content should have correct background");
+      isnot(borderIndex, backgroundIndex, "Border and background fills are separate elements");
+    };
+
+    let win = browser.contentWindow;
+    let body = win.document.body;
+
+    yield new Promise(resolve => setTimeout(resolve, 100));
+
+    info("Test that initial background color is applied");
+
+    checkArrow(win.getComputedStyle(body).backgroundColor);
+
+    info("Test that dynamically-changed background color is applied");
+
+    body.style.backgroundColor = "black";
+    yield awaitResize(browser);
+
+    checkArrow(win.getComputedStyle(body).backgroundColor);
+
+    info("Test that non-opaque background color results in default styling");
+
+    body.style.backgroundColor = "rgba(1, 2, 3, .9)";
+    yield awaitResize(browser);
+
+    checkArrow(null);
+  }
+
+  {
+    info("Test stand-alone browserAction popup");
+
+    clickBrowserAction(extension);
+    let browser = yield awaitPanel(extension);
+    yield testPanel(browser, true);
+    yield closeBrowserAction(extension);
+  }
+
+  {
+    info("Test menu panel browserAction popup");
+
+    let widget = getBrowserActionWidget(extension);
+    CustomizableUI.addWidgetToArea(widget.id, CustomizableUI.AREA_PANEL);
+
+    clickBrowserAction(extension);
+    let browser = yield awaitPanel(extension);
+    yield testPanel(browser, false);
+    yield closeBrowserAction(extension);
+  }
+
+  {
+    info("Test pageAction popup");
+
+    clickPageAction(extension);
+    let browser = yield awaitPanel(extension);
+    yield testPanel(browser, true);
+    yield closePageAction(extension);
+  }
+
+  yield extension.unload();
+});
--- a/toolkit/themes/linux/global/global.css
+++ b/toolkit/themes/linux/global/global.css
@@ -28,16 +28,24 @@ menulist > menupopup {
 progressmeter[mode="undetermined"] {
   -moz-binding: url("chrome://global/content/bindings/progressmeter.xml#progressmeter-undetermined");
 }
 
 toolbar[type="menubar"]:not([autohide="true"]):not(:-moz-lwtheme):-moz-system-metric(menubar-drag) {
   -moz-binding: url("chrome://global/content/bindings/toolbar.xml#toolbar-drag");
 }
 
+/* ::::: Variables ::::: */
+:root {
+  --panel-arrowcontent-padding: 10px;
+  --panel-arrowcontent-background: -moz-field;
+  --panel-arrowcontent-color: -moz-fieldText;
+  --panel-arrowcontent-border: 1px solid ThreeDShadow;
+}
+
 /* ::::: root elements ::::: */
 
 window,
 page,
 dialog,
 wizard,
 prefwindow {
   -moz-appearance: window;
--- a/toolkit/themes/linux/global/popup.css
+++ b/toolkit/themes/linux/global/popup.css
@@ -1,22 +1,14 @@
 /* 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/. */
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 
-/* ::::: Variables ::::: */
-.panel-arrowcontent {
-  --panel-arrowcontent-padding: 10px;
-  --panel-arrowcontent-background: -moz-field;
-  --panel-arrowcontent-color: -moz-fieldText;
-  --panel-arrowcontent-border: 1px solid ThreeDShadow;
-}
-
 /* ::::: menupopup ::::: */
 
 menupopup,
 panel {
   -moz-appearance: menupopup;
   min-width: 1px;
   color: MenuText;
 }
@@ -43,17 +35,18 @@ panel[type="arrow"][side="right"] {
   padding: var(--panel-arrowcontent-padding);
   color: var(--panel-arrowcontent-color);
   background: var(--panel-arrowcontent-background);
   border: var(--panel-arrowcontent-border);
 }
 
 .panel-arrow[side="top"],
 .panel-arrow[side="bottom"] {
-  list-style-image: url("chrome://global/skin/arrow/panelarrow-vertical-themed.svg");
+  list-style-image: var(--panel-arrow-image-vertical,
+                        url("chrome://global/skin/arrow/panelarrow-vertical-themed.svg"));
   position: relative;
   margin-left: 6px;
   margin-right: 6px;
 }
 
 .panel-arrow[side="top"] {
   margin-bottom: -1px;
 }
--- a/toolkit/themes/osx/global/global.css
+++ b/toolkit/themes/osx/global/global.css
@@ -9,16 +9,25 @@
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 
 /* ::::: XBL bindings ::::: */
 
 menulist > menupopup {
   -moz-binding: url("chrome://global/content/bindings/popup.xml#popup-scrollbars");
 }
 
+/* ::::: Variables ::::: */
+:root {
+  --panel-arrowcontent-padding: 16px;
+  --panel-arrowcontent-background: linear-gradient(hsla(0,0%,99%,1), hsla(0,0%,99%,.975) 10%, hsla(0,0%,98%,.975));
+  --panel-arrowcontent-color: hsl(0,0%,10%);
+  --panel-arrowcontent-border: none;
+  --panel-arrowcontent-border-radius: 3.5px;
+}
+
 /* ::::: root elements ::::: */
 
 window,
 page,
 dialog,
 wizard,
 prefwindow { 
   -moz-appearance: dialog;
--- a/toolkit/themes/osx/global/popup.css
+++ b/toolkit/themes/osx/global/popup.css
@@ -1,22 +1,14 @@
 /* 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/. */
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 
-.panel-arrowcontent {
-  --panel-arrowcontent-padding: 16px;
-  --panel-arrowcontent-background: linear-gradient(hsla(0,0%,99%,1), hsla(0,0%,99%,.975) 10%, hsla(0,0%,98%,.975));
-  --panel-arrowcontent-color: hsl(0,0%,10%);
-  --panel-arrowcontent-border: none;
-  --panel-arrowcontent-border-radius: 3.5px;
-}
-
 menupopup,
 panel {
   -moz-appearance: menupopup;
   background-color: Menu;
 }
 
 menupopup > menu > menupopup {
   margin-top: -4px;
@@ -54,17 +46,18 @@ panel[type="arrow"][side="right"] {
   box-shadow: 0 0 0 1px hsla(210,4%,10%,.05);
   color: var(--panel-arrowcontent-color);
   border: var(--panel-arrowcontent-border);
   padding: var(--panel-arrowcontent-padding);
   margin: 1px;
 }
 
 .panel-arrow[side="top"] {
-  list-style-image: url("chrome://global/skin/arrow/panelarrow-vertical.png");
+  list-style-image: var(--panel-arrow-image-vertical,
+                        url("chrome://global/skin/arrow/panelarrow-vertical.png"));
   margin-left: 16px;
   margin-right: 16px;
   margin-bottom: -1px;
 }
 
 .panel-arrow[side="bottom"] {
   list-style-image: url("chrome://global/skin/arrow/panelarrow-vertical.png");
   -moz-transform: scaleY(-1);
@@ -86,17 +79,18 @@ panel[type="arrow"][side="right"] {
   margin-top: 16px;
   margin-bottom: 16px;
   margin-left: -1px;
 }
 
 @media (min-resolution: 2dppx) {
   .panel-arrow[side="top"],
   .panel-arrow[side="bottom"] {
-    list-style-image: url("chrome://global/skin/arrow/panelarrow-vertical@2x.png");
+    list-style-image: var(--panel-arrow-image-vertical,
+                          url("chrome://global/skin/arrow/panelarrow-vertical@2x.png"));
     width: 18px;
     height: 10px;
   }
 
   .panel-arrow[side="left"],
   .panel-arrow[side="right"] {
     list-style-image: url("chrome://global/skin/arrow/panelarrow-horizontal@2x.png");
     width: 10px;
--- a/toolkit/themes/windows/global/global.css
+++ b/toolkit/themes/windows/global/global.css
@@ -16,16 +16,24 @@
 radio {
   -moz-binding: url("chrome://global/skin/globalBindings.xml#radio");
 }
 
 menulist > menupopup {
   -moz-binding: url("chrome://global/content/bindings/popup.xml#popup-scrollbars");
 }
 
+/* ::::: Variables ::::: */
+:root {
+  --panel-arrowcontent-padding: 10px;
+  --panel-arrowcontent-background: -moz-field;
+  --panel-arrowcontent-color: -moz-FieldText;
+  --panel-arrowcontent-border: 1px solid ThreeDShadow;
+}
+
 /* ::::: root elements ::::: */
 
 window,
 page,
 dialog,
 wizard,
 prefwindow {
   -moz-appearance: window;
--- a/toolkit/themes/windows/global/popup.css
+++ b/toolkit/themes/windows/global/popup.css
@@ -1,22 +1,14 @@
 /* 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/. */
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 
-/* ::::: Variables ::::: */
-.panel-arrowcontent {
-  --panel-arrowcontent-padding: 10px;
-  --panel-arrowcontent-background: -moz-field;
-  --panel-arrowcontent-color: -moz-FieldText;
-  --panel-arrowcontent-border: 1px solid ThreeDShadow;
-}
-
 /* ::::: menupopup ::::: */
 
 menupopup,
 panel {
   border: 3px solid transparent;
   -moz-border-top-colors   : ThreeDLightShadow ThreeDHighlight ThreeDFace;
   -moz-border-left-colors  : ThreeDLightShadow ThreeDHighlight ThreeDFace;
   -moz-border-right-colors : ThreeDDarkShadow  ThreeDShadow    ThreeDFace;
@@ -73,17 +65,18 @@ panel[type="arrow"][side="right"] {
   border-radius: 4px;
 }
 %ifdef XP_WIN
 }
 %endif
 
 .panel-arrow[side="top"],
 .panel-arrow[side="bottom"] {
-  list-style-image: url("chrome://global/skin/arrow/panelarrow-vertical-themed.svg");
+  list-style-image: var(--panel-arrow-image-vertical,
+                        url("chrome://global/skin/arrow/panelarrow-vertical-themed.svg"));
   position: relative;
   margin-left: 10px;
   margin-right: 10px;
 }
 
 .panel-arrow[side="top"] {
   margin-bottom: -5px;
 }
@@ -114,17 +107,18 @@ panel[type="arrow"][side="right"] {
 @media (-moz-windows-default-theme) {
   .panel-arrowcontent {
     --panel-arrowcontent-border: 1px solid hsla(210,4%,10%,.2);
     box-shadow: 0 0 4px hsla(210,4%,10%,.2);
   }
 
   .panel-arrow[side="top"],
   .panel-arrow[side="bottom"] {
-    list-style-image: url("chrome://global/skin/arrow/panelarrow-vertical.svg");
+    list-style-image: var(--panel-arrow-image-vertical,
+                          url("chrome://global/skin/arrow/panelarrow-vertical.svg"));
   }
 
   .panel-arrow[side="left"],
   .panel-arrow[side="right"] {
     list-style-image: url("chrome://global/skin/arrow/panelarrow-horizontal.svg");
   }
 }
 %endif