--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -379,16 +379,17 @@ TextPropertyEditor.prototype = {
onShow: this._onStartEditing,
onPreview: this._onSwatchPreview,
onCommit: this._onSwatchCommit,
onRevert: this._onSwatchRevert
});
span.on("unit-change", this._onSwatchCommit);
let title = l10n("rule.colorSwatch.tooltip");
span.setAttribute("title", title);
+ span.dataset.propertyName = this.nameSpan.textContent;
}
}
// Attach the cubic-bezier tooltip to the bezier swatches
this._bezierSwatchSpans =
this.valueSpan.querySelectorAll("." + BEZIER_SWATCH_CLASS);
if (this.ruleEditor.isEditable) {
for (let span of this._bezierSwatchSpans) {
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -266,16 +266,17 @@ devtools.jar:
skin/images/security-state-broken.svg (themes/images/security-state-broken.svg)
skin/images/security-state-insecure.svg (themes/images/security-state-insecure.svg)
skin/images/security-state-secure.svg (themes/images/security-state-secure.svg)
skin/images/security-state-weak.svg (themes/images/security-state-weak.svg)
skin/images/diff.svg (themes/images/diff.svg)
skin/images/import.svg (themes/images/import.svg)
skin/images/pane-collapse.svg (themes/images/pane-collapse.svg)
skin/images/pane-expand.svg (themes/images/pane-expand.svg)
+ skin/images/help.svg (themes/images/help.svg)
# Firebug Theme
skin/images/firebug/read-only.svg (themes/images/firebug/read-only.svg)
skin/images/firebug/spinner.png (themes/images/firebug/spinner.png)
skin/images/firebug/twisty-closed-firebug.svg (themes/images/firebug/twisty-closed-firebug.svg)
skin/images/firebug/twisty-open-firebug.svg (themes/images/firebug/twisty-open-firebug.svg)
skin/images/firebug/arrow-down.svg (themes/images/firebug/arrow-down.svg)
skin/images/firebug/arrow-up.svg (themes/images/firebug/arrow-up.svg)
--- a/devtools/client/locales/en-US/inspector.properties
+++ b/devtools/client/locales/en-US/inspector.properties
@@ -349,16 +349,50 @@ inspector.sidebar.layoutViewTitle2=Layou
# This is the title shown in a tab in the side panel of the Inspector panel
# that corresponds to the tool displaying animations defined in the page.
inspector.sidebar.animationInspectorTitle=Animations
# LOCALIZATION NOTE (inspector.eyedropper.label): A string displayed as the tooltip of
# a button in the inspector which toggles the Eyedropper tool
inspector.eyedropper.label=Grab a color from the page
+# LOCALIZATION NOTE (inspector.colorwidget.contrastRatio.header):
+# This string is used as a header to indicate the contrast section of the
+# color widget.
+inspector.colorwidget.contrastRatio.header=Contrast Ratio
+
+# LOCALIZATION NOTE (inspector.colorwidget.contrastRatio.invalidColor):
+# This string is used when an invalid color is passed as a background color
+# to the color widget.
+inspector.colorwidget.contrastRatio.invalidColor=Invalid color
+
+# LOCALIZATION NOTE (inspector.colorwidget.contrastRatio.info):
+# This string is used to explain the contrast ratio grading system when you hover over the help icon in the contrast info.
+inspector.colorwidget.contrastRatio.info=The contrast ratio grading system for text has the following grading: Fail, AA*, AAA* AAA from lowest to highest readability.\nIt was calculated based on the computed background color:
+
+# LOCALIZATION NOTE (inspector.colorwidget.contrastRatio.failGrade):
+# This string is used to indicate that the text fails for contrast ratio grading criteria.
+inspector.colorwidget.contrastRatio.failGrade=Fail
+
+# LOCALIZATION NOTE (inspector.colorwidget.contrastRatio.failInfo):
+# This string is used to explain that the text fails for contrast ratio grading criteria.
+inspector.colorwidget.contrastRatio.failInfo=This contrast ratio fails for all text sizes.
+
+# LOCALIZATION NOTE (inspector.colorwidget.contrastRatio.AABigInfo):
+# This string is used to explain that the text passes AA* grade for contrast ratio.
+inspector.colorwidget.contrastRatio.AABigInfo=This contrast ratio passes the AA grade for big text (at least 18 point or 14 point bold sized text).
+
+# LOCALIZATION NOTE (inspector.colorwidget.contrastRatio.AAABigInfo):
+# This string is used to explain that the text passes the AA grade and AAA* for contrast ratio.
+inspector.colorwidget.contrastRatio.AAABigInfo=This contrast ratio passes the AA grade for all text and AAA grade for big text (at least 18 point or 14 point bold sized text).
+
+# LOCALIZATION NOTE (inspector.colorwidget.contrastRatio.AAAInfo):
+# This string is used to explain that the text passes AAA grade for contrast ratio.
+inspector.colorwidget.contrastRatio.AAAInfo=This contrast ratio passes the AAA grade for all text sizes.
+
# LOCALIZATION NOTE (inspector.breadcrumbs.label): A string visible only to a screen reader and
# is used to label (using aria-label attribute) a container for inspector breadcrumbs
inspector.breadcrumbs.label=Breadcrumbs
# LOCALIZATION NOTE (inspector.browserStyles.label): This is the label for the checkbox
# that specifies whether the styles that are not from the user's stylesheet should be
# displayed or not.
inspector.browserStyles.label=Browser styles
--- a/devtools/client/shared/widgets/ColorWidget.js
+++ b/devtools/client/shared/widgets/ColorWidget.js
@@ -4,20 +4,24 @@
/**
* This file is a new working copy of Spectrum.js for the purposes of refreshing the color
* widget. It is hidden behind a pref("devtools.inspector.colorWidget.enabled").
*/
"use strict";
+const {Task} = require("devtools/shared/task");
const EventEmitter = require("devtools/shared/event-emitter");
const {colorUtils} = require("devtools/shared/css/color");
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
const XHTML_NS = "http://www.w3.org/1999/xhtml";
+const SAMPLE_TEXT = "Abc";
/**
* ColorWidget creates a color picker widget in any container you give it.
*
* Simple usage example:
*
* const {ColorWidget} = require("devtools/client/shared/widgets/ColorWidget");
* let s = new ColorWidget(containerElement, [255, 126, 255, 1]);
@@ -45,27 +49,75 @@ function ColorWidget(parentEl, rgb) {
this.onAlphaSliderMove = this.onAlphaSliderMove.bind(this);
this.onElementClick = this.onElementClick.bind(this);
this.onDraggerMove = this.onDraggerMove.bind(this);
this.onHexInputChange = this.onHexInputChange.bind(this);
this.onHslaInputChange = this.onHslaInputChange.bind(this);
this.onRgbaInputChange = this.onRgbaInputChange.bind(this);
this.onSelectValueChange = this.onSelectValueChange.bind(this);
this.onSliderMove = this.onSliderMove.bind(this);
+ this.updateContrast = this.updateContrast.bind(this);
this.initializeColorWidget();
if (rgb) {
this.rgb = rgb;
this.updateUI();
}
}
module.exports.ColorWidget = ColorWidget;
+/**
+ * Calculates the contrast grade and title for the given contrast
+ * ratio and background color.
+ * @param {Number} contrastRatio Contrast ratio to calculate grade.
+ * @param {String} backgroundColor A string of the form `rgba(r, g, b, a)`
+ * where r, g, b and a are floats.
+ * @return {Object} An object of the form {grade, title}.
+ * |grade| is a string containing the contrast grade.
+ * |title| is a string containing the title of the colorwidget.
+ */
+ColorWidget.calculateGradeAndTitle = function (contrastRatio, backgroundColor) {
+ let grade = "";
+ let title = "";
+
+ if (contrastRatio < 3.0) {
+ grade = L10N.getStr("inspector.colorwidget.contrastRatio.failGrade");
+ title = L10N.getStr("inspector.colorwidget.contrastRatio.failInfo");
+ } else if (contrastRatio < 4.5) {
+ grade = "AA*";
+ title = L10N.getStr("inspector.colorwidget.contrastRatio.AABigInfo");
+ } else if (contrastRatio < 7.0) {
+ grade = "AAA*";
+ title = L10N.getStr("inspector.colorwidget.contrastRatio.AAABigInfo");
+ } else if (contrastRatio < 22.0) {
+ grade = "AAA";
+ title = L10N.getStr("inspector.colorwidget.contrastRatio.AAAInfo");
+ }
+ title += "\n";
+ title += L10N.getStr("inspector.colorwidget.contrastRatio.info") + " ";
+ title += backgroundColor;
+
+ return { grade, title };
+};
+
+/**
+ * Converts the contrastRatio to a string of length 4 by rounding
+ * contrastRatio and padding the required number of 0s before or
+ * after.
+ * @param {Number} contrastRatio The contrast ratio to be formatted.
+ * @return {String} The formatted ratio.
+ */
+ColorWidget.ratioToString = function (contrastRatio) {
+ let formattedRatio = (contrastRatio < 10) ? "0" : "";
+ formattedRatio += contrastRatio.toFixed(2);
+ return formattedRatio;
+};
+
ColorWidget.hsvToRgb = function (h, s, v, a) {
let r, g, b;
let i = Math.floor(h * 6);
let f = h * 6 - i;
let p = v * (1 - s);
let q = v * (1 - f * s);
let t = v * (1 - (1 - f) * s);
@@ -250,22 +302,48 @@ ColorWidget.prototype = {
</div>
<div class="colorwidget-hsla colorwidget-hidden">
<input class="colorwidget-hsla-h" data-id="h" />
<input class="colorwidget-hsla-s" data-id="s" />
<input class="colorwidget-hsla-l" data-id="l" />
<input class="colorwidget-hsla-a" data-id="a" />
</div>
</div>
+ <div class="colorwidget-contrast">
+ <div class="colorwidget-contrast-info"></div>
+ <div class="colorwidget-contrast-inner">
+ <span class="colorwidget-colorswatch"></span>
+ <span class="colorwidget-contrast-ratio"></span>
+ <span class="colorwidget-contrast-grade"></span>
+ <button class="colorwidget-contrast-help devtools-button"></button>
+ </div>
+ </div>
`;
this.element.addEventListener("click", this.onElementClick);
this.parentEl.appendChild(this.element);
+ this.closestBackgroundColor = "rgba(255, 255, 255, 1)";
+
+ this.contrast = this.element.querySelector(".colorwidget-contrast");
+ this.contrastInfo = this.element.querySelector(".colorwidget-contrast-info");
+ this.contrastInfo.textContent = L10N.getStr(
+ "inspector.colorwidget.contrastRatio.header"
+ );
+
+ this.contrastInner = this.element.querySelector(".colorwidget-contrast-inner");
+ this.contrastSwatch = this.contrastInner.querySelector(".colorwidget-colorswatch");
+
+ this.contrastSwatch.textContent = SAMPLE_TEXT;
+
+ this.contrastRatio = this.contrastInner.querySelector(".colorwidget-contrast-ratio");
+ this.contrastGrade = this.contrastInner.querySelector(".colorwidget-contrast-grade");
+ this.contrastHelp = this.contrastInner.querySelector(".colorwidget-contrast-help");
+
this.slider = this.element.querySelector(".colorwidget-hue");
this.slideHelper = this.element.querySelector(".colorwidget-slider");
ColorWidget.draggable(this.slider, this.onSliderMove);
this.dragger = this.element.querySelector(".colorwidget-color");
this.dragHelper = this.element.querySelector(".colorwidget-dragger");
ColorWidget.draggable(this.dragger, this.onDraggerMove);
@@ -295,30 +373,36 @@ ColorWidget.prototype = {
h: this.element.querySelector(".colorwidget-hsla-h"),
s: this.element.querySelector(".colorwidget-hsla-s"),
l: this.element.querySelector(".colorwidget-hsla-l"),
a: this.element.querySelector(".colorwidget-hsla-a"),
};
this.hslaValue.addEventListener("input", this.onHslaInputChange);
},
- show: function () {
+ show: Task.async(function* () {
this.initializeColorWidget();
this.element.classList.add("colorwidget-show");
this.slideHeight = this.slider.offsetHeight;
this.dragWidth = this.dragger.offsetWidth;
this.dragHeight = this.dragger.offsetHeight;
this.dragHelperHeight = this.dragHelper.offsetHeight;
this.slideHelperHeight = this.slideHelper.offsetHeight;
this.alphaSliderWidth = this.alphaSliderInner.offsetWidth;
this.alphaSliderHelperWidth = this.alphaSliderHelper.offsetWidth;
+ if (this.inspector && this.inspector.selection.nodeFront && this.contrastEnabled) {
+ let node = this.inspector.selection.nodeFront;
+ this.closestBackgroundColor = yield node.getClosestBackgroundColor();
+ }
+ this.updateContrast();
+
this.updateUI();
- },
+ }),
onElementClick: function (e) {
e.stopPropagation();
},
onSliderMove: function (dragX, dragY) {
this.hsv[0] = (dragY / this.slideHeight);
this.hsl[0] = (dragY / this.slideHeight) * 360;
@@ -453,19 +537,60 @@ ColorWidget.prototype = {
this.hsl = hsl;
this.updateUI();
this.onChange();
},
onChange: function () {
+ this.updateContrast();
this.emit("changed", this.rgb, this.rgbCssString);
},
+ updateContrast: function () {
+ if (!this.contrastEnabled) {
+ this.contrast.style.display = "none";
+ return;
+ }
+
+ this.contrast.style.display = "initial";
+
+ if (!colorUtils.isValidCSSColor(this.closestBackgroundColor)) {
+ this.contrastRatio.textContent = L10N.getStr(
+ "inspector.colorwidget.contrastRatio.invalidColor"
+ );
+
+ this.contrastGrade.textContent = "";
+ this.contrastHelp.removeAttribute("title");
+ return;
+ }
+ if (!this.rgbaColor) {
+ this.rgbaColor = new colorUtils.CssColor(this.closestBackgroundColor);
+ }
+ this.rgbaColor.newColor(this.closestBackgroundColor);
+ let rgba = this.rgbaColor._getRGBATuple();
+ let backgroundColor = [rgba.r, rgba.g, rgba.b, rgba.a];
+
+ let textColor = this.rgb;
+
+ let ratio = colorUtils.calculateContrastRatio(backgroundColor, textColor);
+
+ let contrastDetails = ColorWidget.calculateGradeAndTitle(ratio,
+ this.rgbaColor.toString());
+
+ this.contrastRatio.textContent = ColorWidget.ratioToString(ratio);
+ this.contrastGrade.textContent = contrastDetails.grade;
+
+ this.contrastHelp.setAttribute("title", contrastDetails.title);
+
+ this.contrastSwatch.style.backgroundColor = this.rgbaColor.toString();
+ this.contrastSwatch.style.color = this.rgbCssString;
+ },
+
updateHelperLocations: function () {
// If the UI hasn't been shown yet then none of the dimensions will be
// correct
if (!this.element.classList.contains("colorwidget-show")) {
return;
}
let h = this.hsv[0];
--- a/devtools/client/shared/widgets/color-widget.css
+++ b/devtools/client/shared/widgets/color-widget.css
@@ -30,16 +30,22 @@
}
.colorwidget-box {
border: 1px solid rgba(0,0,0,0.2);
border-radius: 2px;
background-clip: content-box;
}
+.colorwidget-colorswatch {
+ background-color: transparent;
+ color: transparent;
+ border: 1px solid transparent;
+}
+
/* Elements */
#colorwidget-tooltip {
padding: 4px;
}
.colorwidget-container {
position: relative;
@@ -66,16 +72,53 @@ http://www.briangrinstead.com/blog/keep-
.colorwidget-top-inner {
position: absolute;
top:0;
left:0;
bottom:0;
right:0;
}
+.colorwidget-contrast {
+ color: var(--theme-content-color1);
+ padding-top: 4px;
+}
+
+.colorwidget-colorswatch, .colorwidget-contrast-ratio, .colorwidget-contrast-grade, .colorwidget-contrast-help {
+ display: inline-block;
+}
+
+.colorwidget-colorswatch {
+ width: 28%;
+}
+
+.colorwidget-contrast-ratio {
+ font-family: Courier New, Courier, monospace;
+ padding-left: 8px;
+ width: 26%;
+}
+
+.colorwidget-contrast-grade {
+ font-family: Courier New, Courier, monospace;
+ width: 18%;
+}
+
+.colorwidget-contrast-help {
+ margin-inline-start: 5px;
+}
+
+.colorwidget-contrast-help::before {
+ background-image: url(chrome://devtools/skin/images/help.svg);
+}
+
+.colorwidget-colorswatch {
+ text-align: center;
+ color: transparent;
+}
+
.colorwidget-color {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 20%;
}
--- a/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
@@ -67,56 +67,72 @@ SwatchColorPickerTooltip.prototype = Her
let widget;
let node = doc.createElementNS(XHTML_NS, "div");
if (NEW_COLOR_WIDGET) {
node.id = "colorwidget";
container.appendChild(node);
widget = new ColorWidget(node, color);
+ this.tooltip.setContent(container, { width: 218, height: 320 });
} else {
node.id = "spectrum";
container.appendChild(node);
widget = new Spectrum(node, color);
+ this.tooltip.setContent(container, { width: 218, height: 224 });
}
+ widget.inspector = this.inspector;
let eyedropper = doc.createElementNS(XHTML_NS, "button");
eyedropper.id = "eyedropper-button";
eyedropper.className = "devtools-button";
/* pointerEvents for eyedropper has to be set auto to display tooltip when
* eyedropper is disabled in non-HTML documents.
*/
eyedropper.style.pointerEvents = "auto";
container.appendChild(eyedropper);
- this.tooltip.setContent(container, { width: 218, height: 224 });
-
// Wait for the tooltip to be shown before calling widget.show
// as it expect to be visible in order to compute DOM element sizes.
this.tooltip.once("shown", () => {
widget.show();
});
return widget;
},
/**
* Overriding the SwatchBasedEditorTooltip.show function to set spectrum's
* color.
*/
show: Task.async(function* () {
+ // set contrast enabled for the spectrum
+ let name = this.activeSwatch.dataset.propertyName;
+
+ if (this.isContrastCompatible === undefined) {
+ let target = this.inspector.target;
+ this.isContrastCompatible = yield target.actorHasMethod(
+ "domnode",
+ "getClosestBackgroundColor"
+ );
+ }
+
+ // only enable contrast if it is compatible and if the type of property is color.
+ this.spectrum.contrastEnabled = (name === "color") && this.isContrastCompatible;
+
// Call then parent class' show function
yield SwatchBasedEditorTooltip.prototype.show.call(this);
// Then set spectrum's color and listen to color changes to preview them
if (this.activeSwatch) {
this.currentSwatchColor = this.activeSwatch.nextSibling;
this._originalColor = this.currentSwatchColor.textContent;
let color = this.activeSwatch.style.backgroundColor;
this.spectrum.off("changed", this._onSpectrumColorChange);
+
this.spectrum.rgb = this._colorToRgba(color);
this.spectrum.on("changed", this._onSpectrumColorChange);
this.spectrum.updateUI();
}
let eyeButton = this.tooltip.container.querySelector("#eyedropper-button");
let canShowEyeDropper = yield this.inspector.supportsEyeDropper();
if (canShowEyeDropper) {
@@ -159,17 +175,16 @@ SwatchColorPickerTooltip.prototype = Her
this.hide();
this.tooltip.emit("eyedropper-opened");
}, e => console.error(e));
inspector.once("color-picked", color => {
toolbox.win.focus();
this._selectColor(color);
- this._onEyeDropperDone();
});
inspector.once("color-pick-canceled", () => {
this._onEyeDropperDone();
});
},
_onEyeDropperDone: function () {
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/help.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+ <circle cx="12" cy="12" r="11" stroke-width="2" stroke="currentColor" fill="none"/>
+ <path d="M12.2,4.9c-1.6,0-2.9,0.4-3.8,0.8L9.2,8c0.6-0.4,1.5-0.6,2.2-0.6c1.1,0,1.6,0.5,1.6,1.2 c0,0.7-0.6,1.3-1.3,2.1c-1,1.1-1.4,2.1-1.3,3.2l0,0.5h3V14c0-0.9,0.3-1.7,1.2-2.5c0.9-0.9,1.9-1.9,1.9-3.4 C16.6,6.4,15.2,4.9,12.2,4.9z M12,16.1c-1.1,0-1.9,0.8-1.9,1.9c0,1.1,0.8,1.9,1.9,1.9c1.2,0,1.9-0.8,1.9-1.9 C13.9,16.9,13.1,16.1,12,16.1z"/>
+</svg>
--- a/devtools/server/actors/inspector.js
+++ b/devtools/server/actors/inspector.js
@@ -71,16 +71,17 @@ const {
isAnonymous,
isNativeAnonymous,
isXBLAnonymous,
isShadowAnonymous,
getFrameElement
} = require("devtools/shared/layout/utils");
const {getLayoutChangesObserver, releaseLayoutChangesObserver} = require("devtools/server/actors/reflow");
const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants");
+const {colorUtils} = require("devtools/shared/css/color");
const {EventParsers} = require("devtools/server/event-parsers");
const {nodeSpec, nodeListSpec, walkerSpec, inspectorSpec} = require("devtools/shared/specs/inspector");
const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;
const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
@@ -745,16 +746,39 @@ var NodeActor = exports.NodeActor = prot
let options = {
previewText: FONT_FAMILY_PREVIEW_TEXT,
previewFontSize: FONT_FAMILY_PREVIEW_TEXT_SIZE,
fillStyle: fillStyle
};
let { dataURL, size } = getFontPreviewData(font, doc, options);
return { data: LongStringActor(this.conn, dataURL), size: size };
+ },
+
+ /**
+ * Finds the computed background color of the closest parent with
+ * a set background color.
+ * Returns a string with the background color of the form
+ * rgba(r, g, b, a). Defaults to rgba(255, 255, 255, 1) if no
+ * background color is found.
+ */
+ getClosestBackgroundColor: function () {
+ let current = this.rawNode;
+ while (current) {
+ let computedStyle = CssLogic.getComputedStyle(current);
+ let currentStyle = computedStyle.getPropertyValue("background-color");
+ if (colorUtils.isValidCSSColor(currentStyle)) {
+ let currentCssColor = new colorUtils.CssColor(currentStyle);
+ if (!currentCssColor.isTransparent()) {
+ return currentCssColor.rgba;
+ }
+ }
+ current = current.parentNode;
+ }
+ return "rgba(255, 255, 255, 1)";
}
});
/**
* Server side of a node list as returned by querySelectorAll()
*/
var NodeListActor = exports.NodeListActor = protocol.ActorClassWithSpec(nodeListSpec, {
typeName: "domnodelist",
--- a/devtools/shared/css/color.js
+++ b/devtools/shared/css/color.js
@@ -70,16 +70,17 @@ function CssColor(colorValue, supportsCs
module.exports.colorUtils = {
CssColor: CssColor,
rgbToHsl: rgbToHsl,
setAlpha: setAlpha,
classifyColor: classifyColor,
rgbToColorName: rgbToColorName,
colorToRGBA: colorToRGBA,
isValidCSSColor: isValidCSSColor,
+ calculateContrastRatio: calculateContrastRatio,
};
/**
* Values used in COLOR_UNIT_PREF
*/
CssColor.COLORUNIT = {
"authored": "authored",
"hex": "hex",
@@ -440,16 +441,25 @@ CssColor.prototype = {
},
/**
* This method allows comparison of CssColor objects using ===.
*/
valueOf: function () {
return this.rgba;
},
+
+ /**
+ * Check whether the color is fully transparent (alpha === 0).
+ *
+ * @return {Boolean} True if the color is transparent and valid.
+ */
+ isTransparent: function () {
+ return this._getRGBATuple().a === 0;
+ },
};
/**
* Convert rgb value to hsl
*
* @param {array} rgb
* Array of rgb values
* @return {array}
@@ -1137,8 +1147,42 @@ function colorToRGBA(name, useCssColor4C
*
* @param {String} name The string to check
* @param {Boolean} useCssColor4ColorFunction use css-color-4 color function or not.
* @return {Boolean} True if the string is a CSS color name.
*/
function isValidCSSColor(name, useCssColor4ColorFunction = false) {
return colorToRGBA(name, useCssColor4ColorFunction) !== null;
}
+
+/**
+ * Calculates the luminance of a rgba tuple based on the formula given in
+ * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
+ *
+ * @param {Array} rgba An array with [r,g,b,a] values.
+ * @return {Number} The calculated luminance.
+ */
+function calculateLuminance(rgba) {
+ for (let i = 0; i < 3; i++) {
+ rgba[i] /= 255;
+ rgba[i] = (rgba[i] < 0.03928) ? (rgba[i] / 12.92) :
+ Math.pow(((rgba[i] + 0.055) / 1.055), 2.4);
+ }
+ return 0.2126 * rgba[0] + 0.7152 * rgba[1] + 0.0722 * rgba[2];
+}
+
+/**
+ * Calculates the contrast ratio of 2 rgba tuples based on the formula in
+ * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast7
+ *
+ * @param {Array} backgroundColor An array with [r,g,b,a] values containing
+ * the background color.
+ * @param {Array} textColor An array with [r,g,b,a] values containing
+ * the text color.
+ * @return {Number} The calculated luminance.
+ */
+function calculateContrastRatio(backgroundColor, textColor) {
+ let backgroundLuminance = calculateLuminance(backgroundColor);
+ let textLuminance = calculateLuminance(textColor);
+ let ratio = (textLuminance + 0.05) / (backgroundLuminance + 0.05);
+
+ return (ratio > 1.0) ? ratio : (1 / ratio);
+}
--- a/devtools/shared/specs/node.js
+++ b/devtools/shared/specs/node.js
@@ -61,13 +61,19 @@ const nodeSpec = generateActorSpec({
request: {
modifications: Arg(0, "array:json")
},
response: {}
},
getFontFamilyDataURL: {
request: {font: Arg(0, "string"), fillStyle: Arg(1, "nullable:string")},
response: RetVal("imageData")
- }
+ },
+ getClosestBackgroundColor: {
+ request: {},
+ response: {
+ value: RetVal("string")
+ }
+ },
}
});
exports.nodeSpec = nodeSpec;