Bug 1332090 - Add Contrast Ratio component to the Color Widget. r?gl draft
authorRahul Chaudhary <rahulch95@gmail.com>
Mon, 20 Feb 2017 22:27:22 -0500
changeset 495479 6cc3059b8d978bb295be2196914c769a8ef174ff
parent 494684 3d341b9ba5353b6b8ab45b6ca03dcb1b2d789faa
child 548389 8f23630b38d7b733c371c01182eaeb70cb94198d
push id48346
push userbmo:rahulch95@gmail.com
push dateWed, 08 Mar 2017 21:41:37 +0000
reviewersgl
bugs1332090, 1332086, 1332872
milestone55.0a1
Bug 1332090 - Add Contrast Ratio component to the Color Widget. r?gl Added: > A monospace font and displaying a fixed number of digits for contrast ratio and grade. > A (?) icon next to the contrast ratio that explains the contrast grading system briefly on hover (in it's title). > Addresed last review of code. Next Steps: > Finish code review on MVP of the contrast ratio. > Fix non-uniform styling of color widget. > Fix eyedropper bug mentioned below and an eyedropper to the color swatch. Discussion: Gabriel and I discussed > the non-uniform styling of the color widget and we decided to do it in a separate bug, since some of the other features in the new color widget themselves aren't uniform between themselves either, and we plan to move around the items color widget in bug 1332086. https://bugzilla.mozilla.org/show_bug.cgi?id=1332086 2. the feature to add an eyedropper on the swatch to select background color, which is blocked by the colorwidget closing when the eyepicker is used (currently being addressed in bug 1332872). https://bugzilla.mozilla.org/show_bug.cgi?id=1332872 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/firebug/help.svg
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
@@ -378,16 +378,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.name = 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)
@@ -306,8 +307,9 @@ devtools.jar:
     skin/images/firebug/command-frames.svg (themes/images/firebug/command-frames.svg)
     skin/images/firebug/command-paintflashing.svg (themes/images/firebug/command-paintflashing.svg)
     skin/images/firebug/command-responsivemode.svg (themes/images/firebug/command-responsivemode.svg)
     skin/images/firebug/command-scratchpad.svg (themes/images/firebug/command-scratchpad.svg)
     skin/images/firebug/command-screenshot.svg (themes/images/firebug/command-screenshot.svg)
     skin/images/firebug/command-measure.svg (themes/images/firebug/command-measure.svg)
     skin/images/firebug/command-rulers.svg (themes/images/firebug/command-rulers.svg)
     skin/images/firebug/command-noautohide.svg (themes/images/firebug/command-noautohide.svg)
+    skin/images/firebug/help.svg (themes/images/firebug/help.svg)
--- a/devtools/client/locales/en-US/inspector.properties
+++ b/devtools/client/locales/en-US/inspector.properties
@@ -349,16 +349,47 @@ 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.
+# The space at the end of the string is intentional.
+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.failInfo):
+# This string is used to indicate 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 indicate 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 indicate 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 indicate 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]);
@@ -82,28 +86,55 @@ function ColorWidget(parentEl, rgb) {
       </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.onSelectValueChange = this.onSelectValueChange.bind(this);
   this.onHexInputChange = this.onHexInputChange.bind(this);
   this.onRgbaInputChange = this.onRgbaInputChange.bind(this);
   this.onHslaInputChange = this.onHslaInputChange.bind(this);
+  this.updateContrast = this.updateContrast.bind(this);
 
   this.onElementClick = this.onElementClick.bind(this);
   this.element.addEventListener("click", this.onElementClick);
 
   this.parentEl.appendChild(this.element);
 
+  this.closestBackgroundColor = "transparent";
+
+  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.bind(this));
 
   this.dragger = this.element.querySelector(".colorwidget-color");
   this.dragHelper = this.element.querySelector(".colorwidget-dragger");
   ColorWidget.draggable(this.dragger, this.onDraggerMove.bind(this));
 
@@ -140,16 +171,46 @@ function ColorWidget(parentEl, rgb) {
   if (rgb) {
     this.rgb = rgb;
     this.updateUI();
   }
 }
 
 module.exports.ColorWidget = ColorWidget;
 
+ColorWidget.calculateGradeAndTitle = function (contrastRatio, backgroundColor) {
+  let grade = "";
+  let title = "";
+
+  if (contrastRatio < 3.0) {
+    grade = "Fail";
+    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 };
+};
+
+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);
@@ -446,16 +507,54 @@ ColorWidget.prototype = {
     this.updateUI();
     this.onChange();
   },
 
   onChange: function () {
     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;
+    }
+
+    let rgbaColor = new colorUtils.CssColor(this.closestBackgroundColor);
+    let rgba = 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,
+                        rgbaColor.toString());
+
+    this.contrastRatio.textContent = ColorWidget.ratioToString(ratio);
+    this.contrastGrade.textContent = contrastDetails.grade;
+
+    this.contrastHelp.setAttribute("title", contrastDetails.title);
+
+    this.contrastSwatch.style.backgroundColor = 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];
@@ -502,33 +601,39 @@ ColorWidget.prototype = {
 
     // Updating the HSLA fields
     this.hslaValueInputs.h.value = this.hsl[0];
     this.hslaValueInputs.s.value = this.hsl[1] + "%";
     this.hslaValueInputs.l.value = this.hsl[2] + "%";
     this.hslaValueInputs.a.value = parseFloat(this.hsl[3].toFixed(1));
   },
 
-  updateUI: function () {
+  updateUI: Task.async(function* () {
     this.updateHelperLocations();
 
     let rgb = this.rgb;
     let rgbNoSatVal = this.rgbNoSatVal;
 
     let flatColor = "rgb(" + rgbNoSatVal[0] + ", " + rgbNoSatVal[1] + ", " +
       rgbNoSatVal[2] + ")";
 
     this.dragger.style.backgroundColor = flatColor;
 
     let rgbNoAlpha = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
     let rgbAlpha0 = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ", 0)";
     let alphaGradient = "linear-gradient(to right, " + rgbAlpha0 + ", " +
       rgbNoAlpha + ")";
     this.alphaSliderInner.style.background = alphaGradient;
-  },
+
+    if (this.inspector && this.inspector.selection.nodeFront && this.contrastEnabled) {
+      let node = this.inspector.selection.nodeFront;
+      this.closestBackgroundColor = yield (node.getClosestBackgroundColor());
+    }
+    this.updateContrast();
+  }),
 
   destroy: function () {
     this.element.removeEventListener("click", this.onElementClick);
 
     this.parentEl.removeChild(this.element);
 
     this.slider = null;
     this.dragger = null;
--- 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: 24%;
+}
+
+.colorwidget-contrast-ratio {
+  font-family: Courier New, Courier, monospace;
+  padding-left: 8px;
+  width: 20%;
+}
+
+.colorwidget-contrast-grade {
+  font-family: Courier New, Courier, monospace;
+  width: 14%;
+}
+
+.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
@@ -71,19 +71,20 @@ SwatchColorPickerTooltip.prototype = Her
     /* 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);
 
     let spectrum;
     if (NEW_COLOR_WIDGET) {
-      this.tooltip.setContent(container, { width: 218, height: 271 });
+      this.tooltip.setContent(container, { width: 218, height: 320 });
       const {ColorWidget} = require("devtools/client/shared/widgets/ColorWidget");
       spectrum = new ColorWidget(spectrumNode, color);
+      spectrum.inspector = this.inspector;
     } else {
       this.tooltip.setContent(container, { width: 218, height: 224 });
       spectrum = new Spectrum(spectrumNode, color);
     }
 
     // Wait for the tooltip to be shown before calling spectrum.show
     // as it expect to be visible in order to compute DOM element sizes.
     this.tooltip.once("shown", () => {
@@ -95,22 +96,33 @@ SwatchColorPickerTooltip.prototype = Her
 
   /**
    * Overriding the SwatchBasedEditorTooltip.show function to set spectrum's
    * color.
    */
   show: Task.async(function* () {
     // 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);
+      let name = this.activeSwatch.name;
+
+      let target = this.inspector.target;
+      let 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") && isContrastCompatible;
+
       this.spectrum.rgb = this._colorToRgba(color);
       this.spectrum.on("changed", this._onSpectrumColorChange);
       this.spectrum.updateUI();
     }
 
     let tooltipDoc = this.tooltip.doc;
     let eyeButton = tooltipDoc.querySelector("#eyedropper-button");
     let canShowEyeDropper = yield this.inspector.supportsEyeDropper();
@@ -154,17 +166,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/firebug/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>
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,38 @@ 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(0, 0, 0, 0) if no
+   * background color is found.
+   */
+  getClosestBackgroundColor: function () {
+    let current = this;
+    while (current) {
+      let currentStyle = current.computedStyle.getPropertyValue("background-color");
+      if (colorUtils.isValidCSSColor(currentStyle)) {
+        let currentCssColor = new colorUtils.CssColor(currentStyle);
+        if (!currentCssColor.isTransparent()) {
+          return currentCssColor.rgba;
+        }
+      }
+      current = current.walker.parentNode(current);
+    }
+    return "rgba(0, 0, 0, 0)";
   }
 });
 
 /**
  * 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,35 @@ 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 here
+ * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
+ * Return the luminance of the rgba tuple.
+ */
+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 given
+ * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast7
+ * Return the contrast ratio between the 2 rgba tuples.
+ */
+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;