--- a/browser/base/content/test/general/browser_selectpopup.js
+++ b/browser/base/content/test/general/browser_selectpopup.js
@@ -76,21 +76,29 @@ 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>" +
+ "<html><head><style>" +
+ " .blue { color: #fff; background-color: #00f; }" +
+ " .green { color: #800080; background-color: green; }" +
+ " .defaultColor { color: -moz-ComboboxText; }" +
+ " .defaultBackground { background-color: -moz-Combobox; }" +
+ "</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>' +
"</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") {
@@ -733,86 +741,58 @@ add_task(function* test_somehidden() {
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");
+ is(selectPopup.parentNode.itemCount, 6, "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 expected = JSON.parse(child.label);
- 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]})`;
+ 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");
+ textarea.style.color = expected[color];
+ expected[color] = getComputedStyle(textarea).color;
}
}
- // 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");
+ if (expected.unstyled) {
+ ok(!child.hasAttribute("customoptionstyling"),
+ `Item ${idx} should not have any custom option styling`);
+ } else {
+ is(getComputedStyle(child).color, expected.color,
+ "Item " + (idx) + " has correct foreground color");
+ is(getComputedStyle(child).backgroundColor, expected.backgroundColor,
+ "Item " + (idx) + " has correct background color");
+ }
+
+ idx++;
child = child.nextSibling;
}
yield hideSelectPopup(selectPopup, "escape");
yield BrowserTestUtils.removeTab(tab);
});
--- a/toolkit/content/widgets/browser.xml
+++ b/toolkit/content/widgets/browser.xml
@@ -1063,17 +1063,18 @@
case "Forms:ShowDropDown": {
if (!this._selectParentHelper) {
this._selectParentHelper =
Cu.import("resource://gre/modules/SelectParentHelper.jsm", {}).SelectParentHelper;
}
let menulist = document.getElementById(this.getAttribute("selectmenulist"));
menulist.menupopup.style.direction = data.direction;
- this._selectParentHelper.populate(menulist, data.options, data.selectedIndex, this._fullZoom);
+ this._selectParentHelper.populate(menulist, data.options, data.selectedIndex, this._fullZoom,
+ data.uaBackgroundColor, data.uaColor);
this._selectParentHelper.open(this, menulist, data.rect, data.isOpenedViaTouch);
break;
}
case "Forms:HideDropDown": {
if (this._selectParentHelper) {
let menulist = document.getElementById(this.getAttribute("selectmenulist"));
this._selectParentHelper.hide(menulist, this);
--- a/toolkit/content/widgets/remote-browser.xml
+++ b/toolkit/content/widgets/remote-browser.xml
@@ -466,17 +466,18 @@
Cu.import("resource://gre/modules/SelectParentHelper.jsm", {}).SelectParentHelper;
}
let menulist = document.getElementById(this.getAttribute("selectmenulist"));
menulist.menupopup.style.direction = data.direction;
let zoom = Services.prefs.getBoolPref("browser.zoom.full") ||
this.isSyntheticDocument ? this._fullZoom : this._textZoom;
- this._selectParentHelper.populate(menulist, data.options, data.selectedIndex, zoom);
+ this._selectParentHelper.populate(menulist, data.options, data.selectedIndex,
+ zoom, data.uaBackgroundColor, data.uaColor);
this._selectParentHelper.open(this, menulist, data.rect, data.isOpenedViaTouch);
break;
}
case "FullZoomChange": {
this._fullZoom = data.value;
let event = document.createEvent("Events");
event.initEvent("FullZoomChange", true, false);
--- a/toolkit/modules/SelectContentHelper.jsm
+++ b/toolkit/modules/SelectContentHelper.jsm
@@ -31,16 +31,18 @@ this.EXPORTED_SYMBOLS = [
];
this.SelectContentHelper = function(aElement, aOptions, aGlobal) {
this.element = aElement;
this.initialSelection = aElement[aElement.selectedIndex] || null;
this.global = aGlobal;
this.closedWithEnter = false;
this.isOpenedViaTouch = aOptions.isOpenedViaTouch;
+ this._uaBackgroundColor = null;
+ this._uaColor = null;
this.init();
this.showDropDown();
this._updateTimer = new DeferredTask(this._update.bind(this), 0);
}
Object.defineProperty(SelectContentHelper, "open", {
get() {
return gOpen;
@@ -86,17 +88,19 @@ this.SelectContentHelper.prototype = {
showDropDown() {
this.element.openInParentProcess = true;
let rect = this._getBoundingContentRect();
this.global.sendAsyncMessage("Forms:ShowDropDown", {
rect,
options: this._buildOptionList(),
selectedIndex: this.element.selectedIndex,
direction: getComputedStyles(this.element).direction,
- isOpenedViaTouch: this.isOpenedViaTouch
+ isOpenedViaTouch: this.isOpenedViaTouch,
+ uaBackgroundColor: this.uaBackgroundColor,
+ uaColor: this.uaColor,
});
gOpen = true;
},
_getBoundingContentRect() {
return BrowserUtils.getElementBoundingScreenRect(this.element);
},
@@ -105,19 +109,47 @@ this.SelectContentHelper.prototype = {
},
_update() {
// The <select> was updated while the dropdown was open.
// Let's send up a new list of options.
this.global.sendAsyncMessage("Forms:UpdateDropDown", {
options: this._buildOptionList(),
selectedIndex: this.element.selectedIndex,
+ uaBackgroundColor: this.uaBackgroundColor,
+ uaColor: this.uaColor,
});
},
+ // Determine user agent background-color and color.
+ // This is used to skip applying the custom color if it matches
+ // the user agent values.
+ _calculateUAColors() {
+ let dummy = this.element.ownerDocument.createElement("option");
+ dummy.style.color = "-moz-comboboxtext";
+ dummy.style.backgroundColor = "-moz-combobox";
+ let dummyCS = this.element.ownerGlobal.getComputedStyle(dummy);
+ this._uaBackgroundColor = dummyCS.backgroundColor;
+ this._uaColor = dummyCS.color;
+ },
+
+ get uaBackgroundColor() {
+ if (!this._uaBackgroundColor) {
+ this._calculateUAColors();
+ }
+ return this._uaBackgroundColor;
+ },
+
+ get uaColor() {
+ if (!this._uaColor) {
+ this._calculateUAColors();
+ }
+ return this._uaColor;
+ },
+
dispatchMouseEvent(win, target, eventName) {
let mouseEvent = new win.MouseEvent(eventName, {
view: win,
bubbles: true,
cancelable: true,
});
target.dispatchEvent(mouseEvent);
},
--- a/toolkit/modules/SelectParentHelper.jsm
+++ b/toolkit/modules/SelectParentHelper.jsm
@@ -20,22 +20,23 @@ const SEARCH_MINIMUM_ELEMENTS = 40;
var currentBrowser = null;
var currentMenulist = null;
var currentZoom = 1;
var closedWithEnter = false;
var selectRect;
this.SelectParentHelper = {
- populate(menulist, items, selectedIndex, zoom) {
+ populate(menulist, items, selectedIndex, zoom, uaBackgroundColor, uaColor) {
// Clear the current contents of the popup
menulist.menupopup.textContent = "";
currentZoom = zoom;
currentMenulist = menulist;
- populateChildren(menulist, items, selectedIndex, zoom);
+ populateChildren(menulist, items, selectedIndex, zoom,
+ uaBackgroundColor, uaColor);
},
open(browser, menulist, rect, isOpenedViaTouch) {
menulist.hidden = false;
currentBrowser = browser;
closedWithEnter = false;
selectRect = rect;
this._registerListeners(browser, menulist.menupopup);
@@ -134,17 +135,20 @@ this.SelectParentHelper = {
// Sanity check - we'd better know what the currently
// opened menulist is, and what browser it belongs to...
if (!currentMenulist || !currentBrowser) {
return;
}
let options = msg.data.options;
let selectedIndex = msg.data.selectedIndex;
- this.populate(currentMenulist, options, selectedIndex, currentZoom);
+ let uaBackgroundColor = msg.data.uaBackgroundColor;
+ let uaColor = msg.data.uaColor;
+ this.populate(currentMenulist, options, selectedIndex,
+ currentZoom, uaBackgroundColor, uaColor);
}
},
_registerListeners(browser, popup) {
popup.addEventListener("command", this);
popup.addEventListener("popuphidden", this);
popup.addEventListener("mouseover", this);
popup.addEventListener("mouseout", this);
@@ -163,25 +167,25 @@ this.SelectParentHelper = {
browser.ownerGlobal.removeEventListener("keydown", this, true);
browser.ownerGlobal.removeEventListener("fullscreen", this, true);
browser.messageManager.removeMessageListener("Forms:UpdateDropDown", this);
},
};
function populateChildren(menulist, options, selectedIndex, zoom,
+ uaBackgroundColor, uaColor,
parentElement = null, isGroupDisabled = false,
adjustedTextSize = -1, addSearch = true) {
let element = menulist.menupopup;
+ let win = element.ownerGlobal;
// -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) {
- let win = element.ownerGlobal;
-
// 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");
adjustedTextSize = (zoom * parseFloat(textSize, 10)) + "px";
}
for (let option of options) {
@@ -192,41 +196,47 @@ 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") {
+ let customOptionStylingUsed = false;
+ if (option.backgroundColor &&
+ option.backgroundColor != "transparent" &&
+ option.backgroundColor != uaBackgroundColor) {
item.style.backgroundColor = option.backgroundColor;
+ customOptionStylingUsed = true;
}
- if (option.color && option.color != "transparent") {
+ if (option.color &&
+ option.color != uaColor) {
item.style.color = option.color;
+ customOptionStylingUsed = true;
}
- if ((option.backgroundColor && option.backgroundColor != "transparent") ||
- (option.color && option.color != "rgb(0, 0, 0)")) {
+ if (customOptionStylingUsed) {
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");
}
if (isOptGroup) {
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>.
--- 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[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;