Bug 1336125 - Apply option styles using a scoped stylesheet to allow for disabling custom styling on the active item. r=mconley
authorJared Wein <jwein@mozilla.com>
Fri, 03 Feb 2017 12:46:45 -0500
changeset 388032 d229cbc1b3120cf53dbdbd12fbdce20a8d3bff2d
parent 388031 c474e07f9ad8f7f665cf3de1b5a13e35e8d34ee7
child 388033 43233b308b2d7e4007d7403dce7d947cc00a2909
push id7198
push userjlorenzo@mozilla.com
push dateTue, 18 Apr 2017 12:07:49 +0000
treeherdermozilla-beta@d57aa49c3948 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley
bugs1336125
milestone54.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1336125 - Apply option styles using a scoped stylesheet to allow for disabling custom styling on the active item. r=mconley MozReview-Commit-ID: 1dZ1rbKbNY9
browser/base/content/test/general/browser_selectpopup.js
toolkit/modules/SelectParentHelper.jsm
toolkit/themes/linux/global/menu.css
toolkit/themes/osx/global/menu.css
toolkit/themes/windows/global/menu.css
--- a/browser/base/content/test/general/browser_selectpopup.js
+++ b/browser/base/content/test/general/browser_selectpopup.js
@@ -89,16 +89,17 @@ const PAGECONTENT_COLORS =
   "</style>" +
   "<body><select id='one'>" +
   '  <option value="One" style="color: #fff; background-color: #f00;">{"color": "rgb(255, 255, 255)", "backgroundColor": "rgb(255, 0, 0)"}</option>' +
   '  <option value="Two" class="blue">{"color": "rgb(255, 255, 255)", "backgroundColor": "rgb(0, 0, 255)"}</option>' +
   '  <option value="Three" class="green">{"color": "rgb(128, 0, 128)", "backgroundColor": "rgb(0, 128, 0)"}</option>' +
   '  <option value="Four" class="defaultColor defaultBackground">{"color": "-moz-ComboboxText", "backgroundColor": "transparent", "unstyled": "true"}</option>' +
   '  <option value="Five" class="defaultColor">{"color": "-moz-ComboboxText", "backgroundColor": "transparent", "unstyled": "true"}</option>' +
   '  <option value="Six" class="defaultBackground">{"color": "-moz-ComboboxText", "backgroundColor": "transparent", "unstyled": "true"}</option>' +
+  '  <option value="Seven" selected="true">{"unstyled": "true"}</option>' +
   "</select></body></html>";
 
 function openSelectPopup(selectPopup, mode = "key", selector = "select", win = window) {
   let popupShownPromise = BrowserTestUtils.waitForEvent(selectPopup, "popupshown");
 
   if (mode == "click" || mode == "mousedown") {
     let mousePromise;
     if (mode == "click") {
@@ -744,29 +745,30 @@ add_task(function* test_somehidden() {
   yield hideSelectPopup(selectPopup, "escape");
   yield BrowserTestUtils.removeTab(tab);
 });
 
 add_task(function* test_colors_applied_to_popup() {
   const pageUrl = "data:text/html," + escape(PAGECONTENT_COLORS);
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
 
-  let selectPopup = document.getElementById("ContentSelectDropdown").menupopup;
+  let menulist = document.getElementById("ContentSelectDropdown");
+  let selectPopup = menulist.menupopup;
 
   let popupShownPromise = BrowserTestUtils.waitForEvent(selectPopup, "popupshown");
   yield BrowserTestUtils.synthesizeMouseAtCenter("#one", { type: "mousedown" }, gBrowser.selectedBrowser);
   yield popupShownPromise;
 
   // The label contains a JSON string of the expected colors for
   // `color` and `background-color`.
-  is(selectPopup.parentNode.itemCount, 6, "Correct number of items");
+  is(selectPopup.parentNode.itemCount, 7, "Correct number of items");
   let child = selectPopup.firstChild;
   let idx = 1;
 
-  ok(child.selected, "The first child should be selected");
+  ok(!child.selected, "The first child should not be selected");
   while (child) {
     let expected = JSON.parse(child.label);
 
     for (let color of Object.keys(expected)) {
       if (color.toLowerCase().includes("color") &&
           !expected[color].startsWith("rgb")) {
         // Need to convert system color to RGB color.
         let textarea = document.createElementNS("http://www.w3.org/1999/xhtml", "textarea");
--- a/toolkit/modules/SelectParentHelper.jsm
+++ b/toolkit/modules/SelectParentHelper.jsm
@@ -23,16 +23,21 @@ var currentMenulist = null;
 var currentZoom = 1;
 var closedWithEnter = false;
 var selectRect;
 
 this.SelectParentHelper = {
   populate(menulist, items, selectedIndex, zoom, uaBackgroundColor, uaColor) {
     // Clear the current contents of the popup
     menulist.menupopup.textContent = "";
+    let stylesheet = menulist.querySelector("#ContentSelectDropdownScopedStylesheet");
+    if (stylesheet) {
+      stylesheet.remove();
+    }
+
     currentZoom = zoom;
     currentMenulist = menulist;
     populateChildren(menulist, items, selectedIndex, zoom,
                      uaBackgroundColor, uaColor);
   },
 
   open(browser, menulist, rect, isOpenedViaTouch) {
     menulist.hidden = false;
@@ -169,19 +174,28 @@ this.SelectParentHelper = {
     browser.messageManager.removeMessageListener("Forms:UpdateDropDown", this);
   },
 
 };
 
 function populateChildren(menulist, options, selectedIndex, zoom,
                           uaBackgroundColor, uaColor,
                           parentElement = null, isGroupDisabled = false,
-                          adjustedTextSize = -1, addSearch = true) {
+                          adjustedTextSize = -1, addSearch = true, nthChildIndex = 1) {
   let element = menulist.menupopup;
   let win = element.ownerGlobal;
+  let scopedStyleSheet = menulist.querySelector("#ContentSelectDropdownScopedStylesheet");
+  if (!scopedStyleSheet) {
+    let doc = element.ownerDocument;
+    scopedStyleSheet = doc.createElementNS("http://www.w3.org/1999/xhtml", "style");
+    scopedStyleSheet.setAttribute("id", "ContentSelectDropdownScopedStylesheet");
+    scopedStyleSheet.scoped = true;
+    scopedStyleSheet.hidden = true;
+    scopedStyleSheet = menulist.appendChild(scopedStyleSheet);
+  }
 
   // -1 just means we haven't calculated it yet. When we recurse through this function
   // we will pass in adjustedTextSize to save on recalculations.
   if (adjustedTextSize == -1) {
     // Grab the computed text size and multiply it by the remote browser's fullZoom to ensure
     // the popup's text size is matched with the content's. We can't just apply a CSS transform
     // here as the popup's preferred size is calculated pre-transform.
     let textSize = win.getComputedStyle(element).getPropertyValue("font-size");
@@ -196,48 +210,53 @@ function populateChildren(menulist, opti
     item.style.direction = option.textDirection;
     item.style.fontSize = adjustedTextSize;
     item.hidden = option.display == "none" || (parentElement && parentElement.hidden);
     // Keep track of which options are hidden by page content, so we can avoid showing
     // them on search input
     item.hiddenByContent = item.hidden;
     item.setAttribute("tooltiptext", option.tooltip);
 
-    let customOptionStylingUsed = false;
+    let ruleBody = "";
     if (option.backgroundColor &&
         option.backgroundColor != "transparent" &&
         option.backgroundColor != uaBackgroundColor) {
-      item.style.backgroundColor = option.backgroundColor;
-      customOptionStylingUsed = true;
+      ruleBody = `background-color: ${option.backgroundColor};`;
     }
 
     if (option.color &&
         option.color != uaColor) {
-      item.style.color = option.color;
-      customOptionStylingUsed = true;
+      ruleBody += `color: ${option.color};`;
     }
 
-    if (customOptionStylingUsed) {
+    if (ruleBody) {
+      let sheet = scopedStyleSheet.sheet;
+      sheet.insertRule(`${item.localName}:nth-child(${nthChildIndex}):not([_moz-menuactive="true"]) {
+        ${ruleBody}
+      }`, 0);
+
       item.setAttribute("customoptionstyling", "true");
     } else {
       item.removeAttribute("customoptionstyling");
     }
 
     element.appendChild(item);
+    nthChildIndex++;
 
     // A disabled optgroup disables all of its child options.
     let isDisabled = isGroupDisabled || option.disabled;
     if (isDisabled) {
       item.setAttribute("disabled", "true");
     }
 
     if (isOptGroup) {
-      populateChildren(menulist, option.children, selectedIndex, zoom,
-                       uaBackgroundColor, uaColor,
-                       item, isDisabled, adjustedTextSize, false);
+      nthChildIndex =
+        populateChildren(menulist, option.children, selectedIndex, zoom,
+                         uaBackgroundColor, uaColor,
+                         item, isDisabled, adjustedTextSize, false);
     } else {
       if (option.index == selectedIndex) {
         // We expect the parent element of the popup to be a <xul:menulist> that
         // has the popuponly attribute set to "true". This is necessary in order
         // for a <xul:menupopup> to act like a proper <html:select> dropdown, as
         // the <xul:menulist> does things like remember state and set the
         // _moz-menuactive attribute on the selected <xul:menuitem>.
         menulist.selectedItem = item;
@@ -303,16 +322,17 @@ function populateChildren(menulist, opti
           return;
       }
       event.preventDefault();
     }, true);
 
     element.insertBefore(searchbox, element.childNodes[0]);
   }
 
+  return nthChildIndex;
 }
 
 function onSearchInput() {
   let searchObj = this;
 
   // Get input from search field, set to all lower case for comparison
   let input = searchObj.value.toLowerCase();
   // Get all items in dropdown (could be options or optgroups)
--- a/toolkit/themes/linux/global/menu.css
+++ b/toolkit/themes/linux/global/menu.css
@@ -31,20 +31,16 @@ menuitem[_moz-menuactive="true"] {
   color: -moz-menuhovertext;
   background-color: -moz-menuhover;
 }
 
 menuitem[customoptionstyling="true"] {
   -moz-appearance: none;
 }
 
-menuitem[_moz-menuactive="true"][customoptionstyling="true"] {
-  filter: invert(100%);
-}
-
 menu[disabled="true"],
 menuitem[disabled="true"],
 menucaption[disabled="true"] {
   color: GrayText;
 }
 
 menubar > menu {
   padding: 0px 4px;
--- a/toolkit/themes/osx/global/menu.css
+++ b/toolkit/themes/osx/global/menu.css
@@ -136,20 +136,16 @@ menuitem[_moz-menuactive="true"] {
 }
 
 menuitem[customoptionstyling="true"] {
   -moz-appearance: none;
   padding-top: 0;
   padding-bottom: 0;
 }
 
-menuitem[_moz-menuactive="true"][customoptionstyling="true"] {
-  filter: invert(100%);
-}
-
 /* ::::: menu/menuitems in menulist popups ::::: */
 
 menulist > menupopup > menuitem,
 menulist > menupopup > menucaption,
 menulist > menupopup > menu {
   max-width: none;
   font: inherit;
   color: -moz-FieldText;
--- a/toolkit/themes/windows/global/menu.css
+++ b/toolkit/themes/windows/global/menu.css
@@ -199,20 +199,16 @@ menulist > menupopup > menu {
 }
 
 menulist > menupopup > menuitem[_moz-menuactive="true"],
 menulist > menupopup > menu[_moz-menuactive="true"] {
   background-color: highlight;
   color: highlighttext;
 }
 
-menulist > menupopup > menuitem[_moz-menuactive="true"][customoptionstyling="true"] {
-  filter: invert(100%);
-}
-
 menulist > menupopup > menuitem > .menu-iconic-left,
 menulist > menupopup > menucaption > .menu-iconic-left,
 menulist > menupopup > menu > .menu-iconic-left {
   display: none;
 }
 
 menulist > menupopup > menuitem > label,
 menulist > menupopup > menucaption > label,