Bug 1474110 - Choose browserAction text color among white and black, maximizing contrast
MozReview-Commit-ID: CykmiUc0BsO
--- a/browser/components/extensions/parent/ext-browserAction.js
+++ b/browser/components/extensions/parent/ext-browserAction.js
@@ -67,17 +67,18 @@ this.browserAction = class extends Exten
this.eventQueue = [];
this.tabManager = extension.tabManager;
this.defaults = {
enabled: true,
title: options.default_title || extension.name,
badgeText: "",
- badgeBackgroundColor: null,
+ badgeBackgroundColor: [0xd9, 0, 0, 255],
+ badgeDefaultColor: [255, 255, 255, 255],
badgeTextColor: null,
popup: options.default_popup || "",
area: browserAreas[options.default_area || "navbar"],
};
this.globals = Object.create(this.defaults);
this.browserStyle = options.browser_style;
@@ -437,31 +438,21 @@ this.browserAction = class extends Exten
}
if (tabData.enabled) {
node.removeAttribute("disabled");
} else {
node.setAttribute("disabled", "true");
}
- let {badgeBackgroundColor, badgeTextColor} = tabData;
- let badgeStyle = [];
- if (badgeBackgroundColor) {
- let [r, g, b, a] = badgeBackgroundColor;
- badgeStyle.push(`background-color: rgba(${r}, ${g}, ${b}, ${a / 255})`);
- }
- if (badgeTextColor) {
- let [r, g, b, a] = badgeTextColor;
- badgeStyle.push(`color: rgba(${r}, ${g}, ${b}, ${a / 255})`);
- }
- if (badgeStyle.length) {
- node.setAttribute("badgeStyle", badgeStyle.join("; "));
- } else {
- node.removeAttribute("badgeStyle");
- }
+ let serializeColor = ([r, g, b, a]) => `rgba(${r}, ${g}, ${b}, ${a / 255})`;
+ node.setAttribute("badgeStyle", [
+ `background-color: ${serializeColor(tabData.badgeBackgroundColor)}`,
+ `color: ${serializeColor(this.getTextColor(tabData))}`,
+ ].join("; "));
let style = this.iconData.get(tabData.icon);
node.setAttribute("style", style);
};
if (sync) {
callback();
} else {
node.ownerGlobal.requestAnimationFrame(callback);
@@ -564,30 +555,80 @@ this.browserAction = class extends Exten
}
/**
* Set a global, window specific or tab specific property.
*
* @param {Object} details
* An object with optional `tabId` or `windowId` properties.
* @param {string} prop
- * String property to set. Should should be one of "icon", "title", "badgeText"
+ * String property to set. Should should be one of "icon", "title", "badgeText",
* "popup", "badgeBackgroundColor", "badgeTextColor" or "enabled".
* @param {string} value
* Value for prop.
+ * @returns {Object}
+ * The object to which the property has been set.
*/
setProperty(details, prop, value) {
let {target, values} = this.getContextData(details);
if (value === null) {
delete values[prop];
} else {
values[prop] = value;
}
this.updateOnChange(target);
+ return values;
+ }
+
+ /**
+ * Determines the text badge color to be used in a tab, window, or globally.
+ *
+ * @param {Object} values
+ * The values associated with the tab or window, or global values.
+ * @returns {ColorArray}
+ */
+ getTextColor(values) {
+ // If a text color has been explicitly provided, use it.
+ let {badgeTextColor} = values;
+ if (badgeTextColor) {
+ return badgeTextColor;
+ }
+
+ // Otherwise, check if the default color to be used has been cached previously.
+ let {badgeDefaultColor} = values;
+ if (badgeDefaultColor) {
+ return badgeDefaultColor;
+ }
+
+ // Choose a color among white and black, maximizing contrast with background
+ // according to https://www.w3.org/TR/WCAG20-TECHS/G18.html#G18-procedure
+ let [r, g, b] = values.badgeBackgroundColor.slice(0, 3).map(function(channel) {
+ channel /= 255;
+ if (channel <= 0.03928) {
+ return channel / 12.92;
+ }
+ return ((channel + 0.055) / 1.055) ** 2.4;
+ });
+ let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
+
+ // The luminance is 0 for black, 1 for white, and `lum` for the background color.
+ // Since `0 <= lum`, the contrast ratio for black is `c0 = (lum + 0.05) / 0.05`.
+ // Since `lum <= 1`, the contrast ratio for white is `c1 = 1.05 / (lum + 0.05)`.
+ // We want to maximize contrast, so black is chosen if `c1 < c0`, that is, if
+ // `1.05 * 0.05 < (L + 0.05) ** 2`. Otherwise white is chosen.
+ let channel = 1.05 * 0.05 < (lum + 0.05) ** 2 ? 0 : 255;
+ let result = [channel, channel, channel, 255];
+
+ // Cache the result as high as possible in the prototype chain
+ while (!Object.getOwnPropertyDescriptor(values, "badgeDefaultColor")) {
+ values = Object.getPrototypeOf(values);
+ }
+ values.badgeDefaultColor = result;
+ return result;
}
/**
* Retrieve the value of a global, window specific or tab specific property.
*
* @param {Object} details
* An object with optional `tabId` or `windowId` properties.
* @param {string} prop
@@ -687,32 +728,38 @@ this.browserAction = class extends Exten
},
getPopup: function(details) {
return browserAction.getProperty(details, "popup");
},
setBadgeBackgroundColor: function(details) {
let color = parseColor(details.color, "background");
- browserAction.setProperty(details, "badgeBackgroundColor", color);
+ let values = browserAction.setProperty(details, "badgeBackgroundColor", color);
+ if (color === null) {
+ // Let the default text color inherit after removing background color
+ delete values.badgeDefaultColor;
+ } else {
+ // Invalidate a cached default color calculated with the old background
+ values.badgeDefaultColor = null;
+ }
},
getBadgeBackgroundColor: function(details, callback) {
- let color = browserAction.getProperty(details, "badgeBackgroundColor");
- return color || [0xd9, 0, 0, 255];
+ return browserAction.getProperty(details, "badgeBackgroundColor");
},
setBadgeTextColor: function(details) {
let color = parseColor(details.color, "text");
browserAction.setProperty(details, "badgeTextColor", color);
},
- getBadgeTextColor: function(details, callback) {
- let color = browserAction.getProperty(details, "badgeTextColor");
- return color || [255, 255, 255, 255];
+ getBadgeTextColor: function(details) {
+ let {values} = browserAction.getContextData(details);
+ return browserAction.getTextColor(values);
},
openPopup: function() {
let window = windowTracker.topWindow;
browserAction.triggerAction(window);
},
},
};
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -755,16 +755,99 @@ add_task(async function testMultipleWind
expect(null, details[1], null, details[0]);
},
];
},
});
});
+add_task(async function testDefaultBadgeTextColor() {
+ await runTests({
+ manifest: {
+ "browser_action": {
+ "default_icon": "default.png",
+ "default_popup": "default.html",
+ "default_title": "Default Title",
+ },
+ },
+
+ "files": {
+ "default.png": imageBuffer,
+ "window1.png": imageBuffer,
+ "window2.png": imageBuffer,
+ },
+
+ getTests: function(tabs, windows) {
+ let details = [
+ {"icon": browser.runtime.getURL("default.png"),
+ "popup": browser.runtime.getURL("default.html"),
+ "title": "Default Title",
+ "badge": "",
+ "badgeBackgroundColor": [0xd9, 0x00, 0x00, 0xFF],
+ "badgeTextColor": [0xff, 0xff, 0xff, 0xff],
+ "enabled": true},
+ {"badgeBackgroundColor": [0xff, 0xff, 0x00, 0xFF],
+ "badgeTextColor": [0x00, 0x00, 0x00, 0xff]},
+ {"badgeBackgroundColor": [0x00, 0x00, 0xff, 0xFF],
+ "badgeTextColor": [0xff, 0xff, 0xff, 0xff]},
+ {"badgeBackgroundColor": [0xff, 0xff, 0xff, 0x00],
+ "badgeTextColor": [0x00, 0x00, 0x00, 0xff]},
+ {"badgeBackgroundColor": [0x00, 0x00, 0xff, 0xFF],
+ "badgeTextColor": [0xff, 0x00, 0xff, 0xff]},
+ {"badgeBackgroundColor": [0xff, 0xff, 0xff, 0x00]},
+ {"badgeBackgroundColor": [0x00, 0x00, 0x00, 0x00],
+ "badgeTextColor": [0xff, 0xff, 0xff, 0xff]},
+ ];
+
+ return [
+ async expect => {
+ browser.test.log("Initial state, expect default properties.");
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Set a global light bgcolor, expect black text.");
+ browser.browserAction.setBadgeBackgroundColor({color: "#ff0"});
+ expect(null, null, details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Set a window-specific dark bgcolor, expect white text.");
+ let windowId = windows[0];
+ browser.browserAction.setBadgeBackgroundColor({windowId, color: "#00f"});
+ expect(null, details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Set a tab-specific transparent-white bgcolor, expect black text.");
+ let tabId = tabs[0];
+ browser.browserAction.setBadgeBackgroundColor({tabId, color: "#fff0"});
+ expect(details[3], details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Set a window-specific text color, expect it in the tab.");
+ let windowId = windows[0];
+ browser.browserAction.setBadgeTextColor({windowId, color: "#f0f"});
+ expect(details[5], details[4], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Remove the window-specific text color, expect black again.");
+ let windowId = windows[0];
+ browser.browserAction.setBadgeTextColor({windowId, color: null});
+ expect(details[3], details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Set a tab-specific transparent-black bgcolor, expect white text.");
+ let tabId = tabs[0];
+ browser.browserAction.setBadgeBackgroundColor({tabId, color: "#0000"});
+ expect(details[6], details[2], details[1], details[0]);
+ },
+ ];
+ },
+ });
+});
+
add_task(async function testNavigationClearsData() {
let url = "http://example.com/";
let default_title = "Default title";
let tab_title = "Tab title";
let {Management: {global: {tabTracker}}} = ChromeUtils.import("resource://gre/modules/Extension.jsm", {});
let extension, tabs = [];
async function addTab(...args) {