Bug 1417883 - Allow theming autocomplete popups. r?ntim, jaws draft
authorDylan Stokes <stokesdy@msu.edu>
Sun, 11 Feb 2018 12:32:33 -0500
changeset 767271 65c6a54051e0ec7e194263bbb271990db2ca2e08
parent 767022 00ef5405cd7e707a89568ec00120e5acac32b169
push id102545
push userbmo:stokesdy@msu.edu
push dateWed, 14 Mar 2018 06:48:11 +0000
reviewersntim, jaws
bugs1417883
milestone61.0a1
Bug 1417883 - Allow theming autocomplete popups. r?ntim, jaws MozReview-Commit-ID: GITgsY3yYqN
browser/modules/ThemeVariableMap.jsm
browser/themes/linux/browser.css
browser/themes/shared/searchbar.inc.css
browser/themes/shared/urlbar-autocomplete.inc.css
browser/themes/windows/browser.css
toolkit/components/extensions/ext-theme.js
toolkit/components/extensions/schemas/theme.json
toolkit/components/extensions/test/browser/browser.ini
toolkit/components/extensions/test/browser/browser_ext_themes_autocomplete_popup.js
toolkit/modules/LightweightThemeConsumer.jsm
--- a/browser/modules/ThemeVariableMap.jsm
+++ b/browser/modules/ThemeVariableMap.jsm
@@ -20,9 +20,13 @@ const ThemeVariableMap = [
   ["--tabs-border-color", "toolbar_top_separator", "navigator-toolbox"],
   ["--lwt-toolbar-vertical-separator", "toolbar_vertical_separator"],
   ["--toolbox-border-bottom-color", "toolbar_bottom_separator"],
   ["--lwt-toolbarbutton-icon-fill", "icon_color"],
   ["--lwt-toolbarbutton-icon-fill-attention", "icon_attention_color"],
   ["--lwt-toolbarbutton-hover-background", "button_background_hover"],
   ["--lwt-toolbarbutton-active-background", "button_background_active"],
   ["--lwt-selected-tab-background-color", "tab_selected"],
+  ["--autocomplete-popup-background", "popup"],
+  ["--autocomplete-popup-color", "popup_text"],
+  ["--autocomplete-popup-highlight-background", "popup_highlight"],
+  ["--autocomplete-popup-highlight-color", "popup_highlight_text"],
 ];
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -40,16 +40,18 @@
   --tab-line-color: highlight;
 }
 
 :root:-moz-lwtheme {
   --toolbar-bgcolor: rgba(255,255,255,.4);
   --toolbar-bgimage: none;
 
   --toolbox-border-bottom-color: rgba(0,0,0,.3);
+
+  --panel-separator-color: hsla(210,4%,10%,.14);
 }
 
 #menubar-items {
   -moz-box-orient: vertical; /* for flex hack */
 }
 
 #main-menubar {
   -moz-box-flex: 1; /* make menu items expand to fill toolbar height */
--- a/browser/themes/shared/searchbar.inc.css
+++ b/browser/themes/shared/searchbar.inc.css
@@ -44,17 +44,17 @@
 }
 
 .search-panel-header {
   font-weight: normal;
   background-color: var(--arrowpanel-dimmed);
   border-top: 1px solid var(--panel-separator-color);
   margin: 0;
   padding: 3px 6px;
-  color: GrayText;
+  color: var(--autocomplete-popup-secondary-color);
 }
 
 .search-panel-header > label {
   margin-top: 2px !important;
   margin-bottom: 1px !important;
 }
 
 .search-panel-current-input > label {
@@ -79,17 +79,17 @@
   min-width: 48px;
   height: 32px;
   margin: 0;
   padding: 0;
   background: linear-gradient(transparent 15%, var(--panel-separator-color) 15%, var(--panel-separator-color) 85%, transparent 85%);
   background-size: 1px auto;
   background-repeat: no-repeat;
   background-position: right center;
-  color: GrayText;
+  color: var(--autocomplete-popup-secondary-color);
 }
 
 .searchbar-engine-one-off-item:-moz-locale-dir(rtl) {
   background-position-x: left;
 }
 
 .searchbar-engine-one-off-item:not(.last-row) {
   box-sizing: content-box;
--- a/browser/themes/shared/urlbar-autocomplete.inc.css
+++ b/browser/themes/shared/urlbar-autocomplete.inc.css
@@ -4,16 +4,30 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 %endif
 
 :root {
   --autocomplete-popup-background: -moz-field;
   --autocomplete-popup-color: -moz-fieldtext;
   --autocomplete-popup-highlight-background: Highlight;
   --autocomplete-popup-highlight-color: HighlightText;
+  --autocomplete-popup-secondary-color: GrayText;
+}
+
+:root:-moz-lwtheme {
+  --autocomplete-popup-background: #fff;
+  --autocomplete-popup-color: #0c0c0d;
+  --autocomplete-popup-secondary-color: #737373;
+  --urlbar-popup-url-color: hsl(210, 77%, 47%);
+  --urlbar-popup-action-color: hsl(178, 100%, 28%);
+}
+
+:root[lwt-popup-brighttext] {
+  --urlbar-popup-url-color: #0a84ff;
+  --urlbar-popup-action-color: #30e60b;
 }
 
 #PopupAutoCompleteRichResult,
 #PopupSearchAutoComplete {
   background: var(--autocomplete-popup-background);
   color: var(--autocomplete-popup-color);
 }
 
@@ -43,17 +57,17 @@
 
 :root[uidensity=touch] #PopupAutoCompleteRichResult .autocomplete-richlistitem {
   min-height: 40px;
 }
 
 /* Awesomebar popup items */
 
 .ac-separator:not([selected=true]) {
-  color: GrayText;
+  color: var(--autocomplete-popup-secondary-color);
 }
 
 .ac-url:not([selected=true]) {
   color: var(--urlbar-popup-url-color);
 }
 
 .ac-action:not([selected=true]) {
   color: var(--urlbar-popup-action-color);
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -62,16 +62,18 @@
   }
 }
 
 :root:-moz-lwtheme {
   --toolbar-bgcolor: rgba(255,255,255,.4);
   --toolbar-bgimage: none;
 
   --toolbox-border-bottom-color: rgba(0,0,0,.3);
+
+  --panel-separator-color: hsla(210,4%,10%,.14);
 }
 
 #navigator-toolbox:-moz-lwtheme {
   --tabs-border-color: rgba(0,0,0,.3);
 }
 
 #menubar-items {
   -moz-box-orient: vertical; /* for flex hack */
--- a/toolkit/components/extensions/ext-theme.js
+++ b/toolkit/components/extensions/ext-theme.js
@@ -159,16 +159,18 @@ class Theme {
         case "toolbar_top_separator":
         case "toolbar_bottom_separator":
         case "toolbar_vertical_separator":
         case "button_background_hover":
         case "button_background_active":
         case "popup":
         case "popup_text":
         case "popup_border":
+        case "popup_highlight":
+        case "popup_highlight_text":
           this.lwtStyles[color] = cssColor;
           break;
       }
     }
   }
 
   /**
    * Helper method for loading images found in the extension's manifest.
--- a/toolkit/components/extensions/schemas/theme.json
+++ b/toolkit/components/extensions/schemas/theme.json
@@ -167,16 +167,24 @@
               },
               "popup_text": {
                 "$ref": "ThemeColor",
                 "optional": true
               },
               "popup_border": {
                 "$ref": "ThemeColor",
                 "optional": true
+              },
+              "popup_highlight": {
+                "$ref": "ThemeColor",
+                "optional": true
+              },
+              "popup_highlight_text": {
+                "$ref": "ThemeColor",
+                "optional": true
               }
             },
             "additionalProperties": { "$ref": "UnrecognizedProperty" }
           },
           "icons": {
             "type": "object",
             "optional": true,
             "properties": {
--- a/toolkit/components/extensions/test/browser/browser.ini
+++ b/toolkit/components/extensions/test/browser/browser.ini
@@ -18,8 +18,9 @@ support-files =
 [browser_ext_themes_tab_loading.js]
 [browser_ext_themes_tab_text.js]
 [browser_ext_themes_toolbar_fields.js]
 [browser_ext_themes_toolbars.js]
 [browser_ext_themes_toolbarbutton_icons.js]
 [browser_ext_themes_toolbarbutton_colors.js]
 [browser_ext_themes_arrowpanels.js]
 [browser_ext_themes_tab_selected.js]
+[browser_ext_themes_autocomplete_popup.js]
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_autocomplete_popup.js
@@ -0,0 +1,233 @@
+"use strict";
+
+// This test checks whether applied WebExtension themes that attempt to change
+// popup properties are applied correctly to the autocomplete bar.
+const POPUP_COLOR = "#85A400";
+const POPUP_TEXT_COLOR_DARK = "#000000";
+const POPUP_TEXT_COLOR_BRIGHT = "#ffffff";
+const POPUP_SELECTED_COLOR = "#9400ff";
+const POPUP_SELECTED_TEXT_COLOR = "#09b9a6";
+
+const POPUP_URL_COLOR_DARK = "#1c78d4";
+const POPUP_ACTION_COLOR_DARK = "#008f8a";
+const POPUP_URL_COLOR_BRIGHT = "#0a84ff";
+const POPUP_ACTION_COLOR_BRIGHT = "#30e60b";
+
+const SEARCH_TERM = "urlbar-reflows-" + Date.now();
+const ONEOFF_URLBAR_PREF = "browser.urlbar.oneOffSearches";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm",
+});
+
+function promisePopupShown(popup) {
+  return new Promise(resolve => {
+    if (popup.state == "open") {
+      resolve();
+    } else {
+      popup.addEventListener("popupshown", function(event) {
+        resolve();
+      }, {once: true});
+    }
+  });
+}
+
+async function promiseAutocompleteResultPopup(inputText) {
+  gURLBar.focus();
+  gURLBar.value = inputText;
+  gURLBar.controller.startSearch(inputText);
+  await promisePopupShown(gURLBar.popup);
+  await BrowserTestUtils.waitForCondition(() => {
+    return gURLBar.controller.searchStatus >=
+      Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH;
+  });
+}
+
+async function waitForAutocompleteResultAt(index) {
+  let searchString = gURLBar.controller.searchString;
+  await BrowserTestUtils.waitForCondition(
+    () => gURLBar.popup.richlistbox.children.length > index &&
+          gURLBar.popup.richlistbox.children[index].getAttribute("ac-text") == searchString,
+    `Waiting for the autocomplete result for "${searchString}" at [${index}] to appear`);
+  // Ensure the addition is complete, for proper mouse events on the entries.
+  await new Promise(resolve => window.requestIdleCallback(resolve, {timeout: 1000}));
+  return gURLBar.popup.richlistbox.children[index];
+}
+
+add_task(async function setup() {
+  await PlacesUtils.history.clear();
+  const NUM_VISITS = 10;
+  let visits = [];
+
+  for (let i = 0; i < NUM_VISITS; ++i) {
+    visits.push({
+      uri: `http://example.com/urlbar-reflows-${i}`,
+      title: `Reflow test for URL bar entry #${i} - ${SEARCH_TERM}`,
+    });
+  }
+
+  await PlacesTestUtils.addVisits(visits);
+
+  registerCleanupFunction(async function() {
+    await PlacesUtils.history.clear();
+  });
+});
+
+add_task(async function test_popup_url() {
+  // Load extension with brighttext not set
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "theme": {
+        "images": {
+          "headerURL": "image1.png",
+        },
+        "colors": {
+          "accentcolor": ACCENT_COLOR,
+          "textcolor": TEXT_COLOR,
+          "popup": POPUP_COLOR,
+          "popup_text": POPUP_TEXT_COLOR_DARK,
+          "popup_highlight": POPUP_SELECTED_COLOR,
+          "popup_highlight_text": POPUP_SELECTED_TEXT_COLOR,
+        },
+      },
+    },
+    files: {
+      "image1.png": BACKGROUND,
+    },
+  });
+
+  await extension.startup();
+
+  let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults");
+  Services.prefs.setBoolPref(ONEOFF_URLBAR_PREF, true);
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+  registerCleanupFunction(async function() {
+    await PlacesUtils.history.clear();
+    Services.prefs.clearUserPref(ONEOFF_URLBAR_PREF);
+    await BrowserTestUtils.removeTab(tab);
+  });
+
+  let visits = [];
+
+  for (let i = 0; i < maxResults; i++) {
+    visits.push({uri: makeURI("http://example.com/autocomplete/?" + i)});
+  }
+
+  await PlacesTestUtils.addVisits(visits);
+  await promiseAutocompleteResultPopup("example.com/autocomplete");
+  await waitForAutocompleteResultAt(maxResults - 1);
+
+  let popup = gURLBar.popup;
+  let results = popup.richlistbox.children;
+  is(results.length, maxResults,
+     "Should get maxResults=" + maxResults + " results");
+
+  let popupCS = window.getComputedStyle(popup);
+
+  Assert.equal(popupCS.backgroundColor,
+               `rgb(${hexToRGB(POPUP_COLOR).join(", ")})`,
+               `Popup background color should be set to ${POPUP_COLOR}`);
+
+  Assert.equal(popupCS.color,
+               `rgb(${hexToRGB(POPUP_TEXT_COLOR_DARK).join(", ")})`,
+               `Popup color should be set to ${POPUP_TEXT_COLOR_DARK}`);
+
+  // Set the selected attribute to true to test the highlight popup properties
+  results[1].setAttribute("selected", "true");
+  let resultCS = window.getComputedStyle(results[1]);
+
+  Assert.equal(resultCS.backgroundColor,
+               `rgb(${hexToRGB(POPUP_SELECTED_COLOR).join(", ")})`,
+               `Popup highlight background color should be set to ${POPUP_SELECTED_COLOR}`);
+
+  Assert.equal(resultCS.color,
+               `rgb(${hexToRGB(POPUP_SELECTED_TEXT_COLOR).join(", ")})`,
+               `Popup highlight color should be set to ${POPUP_SELECTED_TEXT_COLOR}`);
+
+  results[1].removeAttribute("selected");
+
+  let urlText = document.getAnonymousElementByAttribute(results[1], "anonid", "url-text");
+  Assert.equal(window.getComputedStyle(urlText).color,
+               `rgb(${hexToRGB(POPUP_URL_COLOR_DARK).join(", ")})`,
+               `Urlbar popup url color should be set to ${POPUP_URL_COLOR_DARK}`);
+
+  let actionText = document.getAnonymousElementByAttribute(results[1], "anonid", "action-text");
+  Assert.equal(window.getComputedStyle(actionText).color,
+               `rgb(${hexToRGB(POPUP_ACTION_COLOR_DARK).join(", ")})`,
+               `Urlbar popup action color should be set to ${POPUP_ACTION_COLOR_DARK}`);
+
+  let root = document.documentElement;
+  Assert.equal(root.getAttribute("lwt-popup-brighttext"),
+               "",
+               "brighttext should not be set!");
+
+  await extension.unload();
+
+  // Load a manifest with popup_text being bright. Test for bright text properties.
+  extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "theme": {
+        "images": {
+          "headerURL": "image1.png",
+        },
+        "colors": {
+          "accentcolor": ACCENT_COLOR,
+          "textcolor": TEXT_COLOR,
+          "popup": POPUP_COLOR,
+          "popup_text": POPUP_TEXT_COLOR_BRIGHT,
+          "popup_highlight": POPUP_SELECTED_COLOR,
+          "popup_highlight_text": POPUP_SELECTED_TEXT_COLOR,
+        },
+      },
+    },
+    files: {
+      "image1.png": BACKGROUND,
+    },
+  });
+
+  await extension.startup();
+
+  popupCS = window.getComputedStyle(popup);
+  Assert.equal(popupCS.color,
+               `rgb(${hexToRGB(POPUP_TEXT_COLOR_BRIGHT).join(", ")})`,
+               `Popup color should be set to ${POPUP_TEXT_COLOR_BRIGHT}`);
+
+  urlText = document.getAnonymousElementByAttribute(results[1], "anonid", "url-text");
+  Assert.equal(window.getComputedStyle(urlText).color,
+               `rgb(${hexToRGB(POPUP_URL_COLOR_BRIGHT).join(", ")})`,
+               `Urlbar popup url color should be set to ${POPUP_URL_COLOR_BRIGHT}`);
+
+  actionText = document.getAnonymousElementByAttribute(results[1], "anonid", "action-text");
+  Assert.equal(window.getComputedStyle(actionText).color,
+               `rgb(${hexToRGB(POPUP_ACTION_COLOR_BRIGHT).join(", ")})`,
+               `Urlbar popup action color should be set to ${POPUP_ACTION_COLOR_BRIGHT}`);
+
+  // Since brighttext is enabled, the seperator color should be
+  // POPUP_TEXT_COLOR_BRIGHT with added alpha.
+  let separator = document.getAnonymousElementByAttribute(results[1], "anonid", "separator");
+  Assert.equal(window.getComputedStyle(separator).color,
+               `rgba(${hexToRGB(POPUP_TEXT_COLOR_BRIGHT).join(", ")}, 0.5)`,
+               `Urlbar popup separator color should be set to ${POPUP_TEXT_COLOR_BRIGHT} with alpha`);
+
+  Assert.equal(root.getAttribute("lwt-popup-brighttext"),
+               "true",
+               "brighttext should be set to true!");
+
+  await extension.unload();
+
+  // Check to see if popup-brighttext and secondary color are not set after
+  // unload of theme
+  Assert.equal(root.getAttribute("lwt-popup-brighttext"),
+               "",
+               "brighttext should not be set!");
+
+  // Calculate what GrayText should be. May differ between platforms.
+  let span = document.createElement("span");
+  span.style.color = "GrayText";
+  let GRAY_TEXT = window.getComputedStyle(span).color;
+
+  separator = document.getAnonymousElementByAttribute(results[1], "anonid", "separator");
+  Assert.equal(window.getComputedStyle(separator).color,
+               GRAY_TEXT,
+               `Urlbar popup separator color should be set to ${GRAY_TEXT}`);
+});
--- a/toolkit/modules/LightweightThemeConsumer.jsm
+++ b/toolkit/modules/LightweightThemeConsumer.jsm
@@ -99,16 +99,18 @@ LightweightThemeConsumer.prototype = {
     // or because we are applying a new theme and the data might be bogus CSS,
     // so if we don't reset first, it'll keep the old value.
     root.style.removeProperty("--lwt-text-color");
     root.style.removeProperty("--lwt-accent-color");
     let textcolor = aData.textcolor || "black";
     _setProperty(root, active, "--lwt-text-color", textcolor);
     _setProperty(root, active, "--lwt-accent-color", this._sanitizeCSSColor(aData.accentcolor) || "white");
 
+    _inferPopupColorsFromText(root, active, this._sanitizeCSSColor(aData.popup_text));
+
     if (active) {
       let dummy = this._doc.createElement("dummy");
       dummy.style.color = textcolor;
       let [r, g, b] = _parseRGB(this._win.getComputedStyle(dummy).color);
       let luminance = 0.2125 * r + 0.7154 * g + 0.0721 * b;
       root.setAttribute("lwthemetextcolor", luminance <= 110 ? "dark" : "bright");
       root.setAttribute("lwtheme", "true");
     } else {
@@ -188,8 +190,27 @@ function _setProperties(root, active, va
   }
 }
 
 function _parseRGB(aColorString) {
   var rgb = aColorString.match(/^rgba?\((\d+), (\d+), (\d+)/);
   rgb.shift();
   return rgb.map(x => parseInt(x));
 }
+
+function _inferPopupColorsFromText(element, active, color) {
+  if (!color) {
+    element.removeAttribute("lwt-popup-brighttext");
+    _setProperty(element, active, "--autocomplete-popup-secondary-color");
+    return;
+  }
+
+  let [r, g, b] = _parseRGB(color);
+  let luminance = 0.2125 * r + 0.7154 * g + 0.0721 * b;
+
+  if (luminance <= 110) {
+    element.removeAttribute("lwt-popup-brighttext");
+  } else {
+    element.setAttribute("lwt-popup-brighttext", "true");
+  }
+
+  _setProperty(element, active, "--autocomplete-popup-secondary-color", `rgba(${r}, ${g}, ${b}, 0.5)`);
+}