Bug 1336125 - Apply option styles using a scoped stylesheet to allow for disabling custom styling on the active item. r?mconley draft
authorJared Wein <jwein@mozilla.com>
Fri, 03 Feb 2017 12:46:45 -0500
changeset 480644 de4dd90e5656c5d08899a80e08d02ae013c9079e
parent 479958 e677ba018b22558fef1d07b74d416fd3a28a5dc3
child 480654 e7099be2bd22278e0c4ff2442b7a5adb97a23663
push id44616
push userbmo:jaws@mozilla.com
push dateWed, 08 Feb 2017 19:29:36 +0000
reviewersmconley
bugs1336125
milestone54.0a1
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,