Bug 1332090 - Added a contrast ratio component to the Color Widget - Part 1; r?pbro draft
authorRahul Chaudhary <rahulch95@gmail.com>
Mon, 20 Feb 2017 22:27:22 -0500
changeset 553336 0b09bfb0888648f74ccf9f6e40a08cc44fae5cad
parent 502661 201231223cd4354a450c3e5d80959f35b8e4cf0c
child 622032 028f2be75b616a4ac807291690cce9b9ebe912c0
push id51597
push userbmo:rahulch95@gmail.com
push dateWed, 29 Mar 2017 20:40:39 +0000
reviewerspbro
bugs1332090
milestone55.0a1
Bug 1332090 - Added a contrast ratio component to the Color Widget - Part 1; r?pbro MozReview-Commit-ID: 9FA0h9ST62E
devtools/client/inspector/rules/views/text-property-editor.js
devtools/client/jar.mn
devtools/client/locales/en-US/inspector.properties
devtools/client/shared/widgets/ColorWidget.js
devtools/client/shared/widgets/color-widget.css
devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
devtools/client/themes/images/help.svg
devtools/server/actors/inspector.js
devtools/shared/css/color.js
devtools/shared/specs/node.js
--- 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;