Bug 910022 - Allow websites to provide custom background colors and foreground colors for <select> popups. r?enndeakin
Portions of the patch were written by Jared Beach (beachjar@msu.edu)
MozReview-Commit-ID: 1SpUTJP8tPq
--- a/browser/base/content/test/general/browser_selectpopup.js
+++ b/browser/base/content/test/general/browser_selectpopup.js
@@ -75,16 +75,24 @@ const PAGECONTENT_SOMEHIDDEN =
const PAGECONTENT_TRANSLATED =
"<html><body>" +
"<div id='div'>" +
"<iframe id='frame' width='320' height='295' style='border: none;'" +
" src='data:text/html,<select id=select autofocus><option>he he he</option><option>boo boo</option><option>baz baz</option></select>'" +
"</iframe>" +
"</div></body></html>";
+const PAGECONTENT_COLORS =
+ "<html><head><style>.blue { color: #fff; background-color: #00f; } .green { color: #800080; background-color: green; }</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>' +
+ "</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") {
mousePromise = BrowserTestUtils.synthesizeMouseAtCenter(selector, { }, win.gBrowser.selectedBrowser);
} else {
@@ -724,8 +732,87 @@ add_task(function* test_somehidden() {
"Item " + (idx++) + " is visible");
child = child.nextSibling;
}
yield hideSelectPopup(selectPopup, "escape");
yield BrowserTestUtils.removeTab(tab);
});
+add_task(function* test_colors_applied_to_popup() {
+ function inverseRGBString(rgbString) {
+ let [, r, g, b] = rgbString.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
+ return `rgb(${255 - r}, ${255 - g}, ${255 - b})`;
+ }
+
+ const pageUrl = "data:text/html," + escape(PAGECONTENT_COLORS);
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+ let selectPopup = document.getElementById("ContentSelectDropdown").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, 3, "Correct number of items");
+ let child = selectPopup.firstChild;
+ let idx = 1;
+
+ ok(child.selected, "The first child should be selected");
+ while (child) {
+ let expectedColors = JSON.parse(child.label);
+
+ // We need to use Canvas here to get the actual pixel color
+ // because the computedStyle will only tell us the 'color' or
+ // 'backgroundColor' of the element, but not what the displayed
+ // color is due to composition of various CSS rules such as
+ // 'filter' which is applied when elements have custom background
+ // or foreground elements.
+ let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ canvas = document.documentElement.appendChild(canvas);
+ let rect = child.getBoundingClientRect();
+ canvas.setAttribute("width", rect.width);
+ canvas.setAttribute("height", rect.height);
+ canvas.mozOpaque = true;
+
+ let ctx = canvas.getContext("2d");
+ ctx.drawWindow(window, rect.x + rect.left, rect.y + rect.top, rect.width, rect.height, "#000", ctx.DRAWWINDOW_USE_WIDGET_LAYERS);
+ let frame = ctx.getImageData(0, 0, rect.width, rect.height);
+
+ let pixels = frame.data.length / 4;
+ // Assume the inverse backgroundColor is the color of the first pixel.
+ let [inverseBgR, inverseBgG, inverseBgB] = frame.data;
+ let inverseBackgroundColor = `rgb(${inverseBgR}, ${inverseBgG}, ${inverseBgB})`;
+ // Use the next different pixel color as the foreground color, assuming
+ // no anti-aliasing.
+ let inverseColor = inverseBackgroundColor;
+ for (let i = 0; i < pixels; i++) {
+ if (inverseBgR != frame.data[i * 4 + 0] &&
+ inverseBgG != frame.data[i * 4 + 1] &&
+ inverseBgB != frame.data[i * 4 + 2]) {
+ inverseColor = `rgb(${frame.data[i * 4 + 0]}, ${frame.data[i * 4 + 1]}, ${frame.data[i * 4 + 2]})`;
+ }
+ }
+ // The canvas code above isn't getting the right colors for the pixels,
+ // it always returns rgb(255,255,255).
+ todo_is(inverseColor, inverseRGBString(getComputedStyle(child).color),
+ "Item " + (idx) + " has correct inverse foreground color when selected");
+ todo_is(inverseBackgroundColor, inverseRGBString(getComputedStyle(child).backgroundColor),
+ "Item " + (idx) + " has correct inverse background color when selected");
+
+ canvas.remove();
+
+ // Press Down to move the selected item to the next item in the
+ // list and check the colors of this item when it's not selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { code: "ArrowDown" });
+
+ is(getComputedStyle(child).color, expectedColors.color,
+ "Item " + (idx) + " has correct foreground color");
+ is(getComputedStyle(child).backgroundColor, expectedColors.backgroundColor,
+ "Item " + (idx++) + " has correct background color");
+ child = child.nextSibling;
+ }
+
+ yield hideSelectPopup(selectPopup, "escape");
+ yield BrowserTestUtils.removeTab(tab);
+});
--- a/toolkit/modules/SelectContentHelper.jsm
+++ b/toolkit/modules/SelectContentHelper.jsm
@@ -220,34 +220,39 @@ function buildOptionListForChildren(node
let textContent =
tagName == "OPTGROUP" ? child.getAttribute("label")
: child.text;
if (textContent == null) {
textContent = "";
}
+ // Selected options have the :checked pseudo-class, which
+ // we want to disable before calculating the computed
+ // styles since the user agent styles alter the styling
+ // based on :checked.
+ DOMUtils.addPseudoClassLock(child, ":checked", false);
let cs = getComputedStyles(child);
let info = {
index: child.index,
tagName,
textContent,
disabled: child.disabled,
display: cs.display,
// We need to do this for every option element as each one can have
// an individual style set for direction
textDirection: cs.direction,
tooltip: child.title,
- // XXX this uses a highlight color when this is the selected element.
- // We need to suppress such highlighting in the content process to get
- // the option's correct unhighlighted color here.
- // We also need to detect default color vs. custom so that a standard
- // color does not override color: menutext in the parent.
- // backgroundColor: computedStyle.backgroundColor,
- // color: computedStyle.color,
+ backgroundColor: cs.backgroundColor,
+ color: cs.color,
children: tagName == "OPTGROUP" ? buildOptionListForChildren(child) : []
};
+
+ // We must wait until all computedStyles have been
+ // read before we clear the locks.
+ DOMUtils.clearPseudoClassLocks(child);
+
result.push(info);
}
}
return result;
}
--- a/toolkit/modules/SelectParentHelper.jsm
+++ b/toolkit/modules/SelectParentHelper.jsm
@@ -192,16 +192,31 @@ 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);
+ if (option.backgroundColor && option.backgroundColor != "transparent") {
+ item.style.backgroundColor = option.backgroundColor;
+ }
+
+ if (option.color && option.color != "transparent") {
+ item.style.color = option.color;
+ }
+
+ if ((option.backgroundColor && option.backgroundColor != "transparent") ||
+ (option.color && option.color != "rgb(0, 0, 0)")) {
+ item.setAttribute("customoptionstyling", "true");
+ } else {
+ item.removeAttribute("customoptionstyling");
+ }
+
element.appendChild(item);
// A disabled optgroup disables all of its child options.
let isDisabled = isGroupDisabled || option.disabled;
if (isDisabled) {
item.setAttribute("disabled", "true");
}
--- a/toolkit/themes/linux/global/menu.css
+++ b/toolkit/themes/linux/global/menu.css
@@ -27,16 +27,20 @@ menuitem[default="true"] {
}
menu[_moz-menuactive="true"],
menuitem[_moz-menuactive="true"] {
color: -moz-menuhovertext;
background-color: -moz-menuhover;
}
+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
@@ -130,16 +130,26 @@ menupopup > menucaption {
}
menu[_moz-menuactive="true"],
menuitem[_moz-menuactive="true"] {
color: -moz-mac-menutextselect;
background-color: Highlight;
}
+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,16 +199,20 @@ 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,