Bug 1568053 - Redesign contrast info in color picker (front-end), r=yzen,gl
authorMaliha Islam <mislam@mozilla.com>
Fri, 16 Aug 2019 00:55:27 +0000
changeset 488397 c009e486b4c2e82a516e95cfb635523ea215e275
parent 488396 d9a83f0385d54763aabbadaea607a7451381beba
child 488398 1cca4f1af964c1214bc04bd9d5b34fee4023ed87
push id36443
push userccoroiu@mozilla.com
push dateFri, 16 Aug 2019 09:48:15 +0000
treeherdermozilla-central@5d4cbfe103bb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyzen, gl
bugs1568053
milestone70.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1568053 - Redesign contrast info in color picker (front-end), r=yzen,gl Differential Revision: https://phabricator.services.mozilla.com/D40925
devtools/client/accessibility/accessibility.css
devtools/client/locales/en-US/inspector.properties
devtools/client/shared/test/browser_spectrum.js
devtools/client/shared/widgets/Spectrum.js
devtools/client/shared/widgets/spectrum.css
devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
devtools/client/themes/accessibility-color-contrast.css
--- a/devtools/client/accessibility/accessibility.css
+++ b/devtools/client/accessibility/accessibility.css
@@ -658,17 +658,17 @@ body {
 
 .accessible .tree .objectBox-accessible .accessible-name,
 .accessible .tree .objectBox-node .attrName {
   overflow: hidden;
   text-overflow: ellipsis;
 }
 
 .accessible .tree .objectBox-accessible .open-accessibility-inspector,
-.accessible .tree .objectBox-node .open-inspector{
+.accessible .tree .objectBox-node .open-inspector {
   width: 17px;
   cursor: pointer;
   flex-shrink: 0;
 }
 
 .accessible .tree .objectBox-object,
 .accessible .tree .objectBox-string,
 .accessible .tree .objectBox-text,
@@ -800,13 +800,8 @@ body {
 .accessibility-color-contrast .accessibility-color-contrast-label:after {
   content: ":";
 }
 
 .accessibility-color-contrast .accessibility-color-contrast-label,
 .accessibility-color-contrast .accessibility-color-contrast-separator:before {
   margin-inline-end: 3px;
 }
-
-.accessibility-color-contrast .accessibility-color-contrast-separator:before {
-  content: "-";
-  margin-inline-start: 4px;
-}
--- a/devtools/client/locales/en-US/inspector.properties
+++ b/devtools/client/locales/en-US/inspector.properties
@@ -533,8 +533,14 @@ colorPickerTooltip.colorNameTitle=Closes
 
 # LOCALIZATION NOTE (colorPickerTooltip.hueSliderTitle): A title text for the
 # hue slider in the color picker tooltip.
 colorPickerTooltip.hueSliderTitle=Hue
 
 # LOCALIZATION NOTE (colorPickerTooltip.alphaSliderTitle): A title text for the
 # alpha slider in the color picker tooltip.
 colorPickerTooltip.alphaSliderTitle=Opacity
+
+# LOCALIZATION NOTE (colorPickerTooltip.contrast.large.title): A title text for the color
+# contrast ratio description in the color picker tooltip, used together with the specification
+# that the color contrast criteria used is for large text. %S in the content will be replaced by a
+# large text indicator span at run time.
+colorPickerTooltip.contrast.large.title=Contrast %S:
--- a/devtools/client/shared/test/browser_spectrum.js
+++ b/devtools/client/shared/test/browser_spectrum.js
@@ -4,31 +4,35 @@
 
 "use strict";
 
 // Tests that the spectrum color picker works correctly
 
 const { Spectrum } = require("devtools/client/shared/widgets/Spectrum");
 const {
   accessibility: {
-    SCORES: { FAIL, AAA },
+    SCORES: { FAIL, AAA, AA },
   },
 } = require("devtools/shared/constants");
 
 loader.lazyRequireGetter(
   this,
   "cssColors",
   "devtools/shared/css/color-db",
   true
 );
 
 const TEST_URI = CHROME_URL_ROOT + "doc_spectrum.html";
 const REGULAR_TEXT_PROPS = {
   "font-size": { value: "11px" },
   "font-weight": { value: "bold" },
+  opacity: { value: "1" },
+};
+const SINGLE_BG_COLOR = {
+  value: cssColors.white,
 };
 const ZERO_ALPHA_COLOR = [0, 255, 255, 0];
 
 add_task(async function() {
   const [host, , doc] = await createHost("bottom", TEST_URI);
 
   const container = doc.getElementById("spectrum-container");
 
@@ -38,16 +42,17 @@ add_task(async function() {
   await testChangingColorShouldEmitEvents(container, doc);
   await testSettingColorShoudUpdateTheUI(container);
   await testChangingColorShouldUpdateColorPreview(container);
   await testNotSettingTextPropsShouldNotShowContrastSection(container);
   await testSettingTextPropsAndColorShouldUpdateContrastValue(container);
   await testOnlySelectingLargeTextWithNonZeroAlphaShouldShowIndicator(
     container
   );
+  await testSettingMultiColoredBackgroundShouldShowContrastRange(container);
 
   host.destroy();
 });
 
 /**
  * Helper method for extracting the rgba overlay value of the color preview's background
  * image style.
  *
@@ -343,37 +348,38 @@ function testNotSettingTextPropsShouldNo
     "Contrast section is not shown."
   );
 
   s.destroy();
 }
 
 function testSpectrumContrast(
   spectrum,
+  contrastValueEl,
   rgb,
   expectedValue,
   expectedBadgeClass = "",
   expectLargeTextIndicator = false
 ) {
   setSpectrumProps(spectrum, { rgb });
 
   is(
-    spectrum.contrastValue.textContent,
+    contrastValueEl.textContent,
     expectedValue,
     "Contrast value has the correct text."
   );
   is(
-    spectrum.contrastValue.className,
+    contrastValueEl.className,
     `accessibility-contrast-value${
       expectedBadgeClass ? " " + expectedBadgeClass : ""
     }`,
     `Contrast value contains ${expectedBadgeClass || "base"} class.`
   );
   is(
-    spectrum.spectrumContrast.classList.contains("large-text"),
+    spectrum.contrastLabel.childNodes.length === 3,
     expectLargeTextIndicator,
     `Large text indicator is ${expectLargeTextIndicator ? "" : "not"} shown.`
   );
 }
 
 function testSettingTextPropsAndColorShouldUpdateContrastValue(container) {
   const s = new Spectrum(container, cssColors.white);
   s.show();
@@ -381,62 +387,119 @@ function testSettingTextPropsAndColorSho
   ok(
     !s.spectrumContrast.classList.contains("visible"),
     "Contrast value is not available yet."
   );
 
   info(
     "Test that contrast ratio is calculated on setting 'textProps' and 'rgb'."
   );
-  setSpectrumProps(s, { textProps: REGULAR_TEXT_PROPS }, false);
-  testSpectrumContrast(s, [50, 240, 234, 0.8], "1.35", FAIL);
+  setSpectrumProps(
+    s,
+    { textProps: REGULAR_TEXT_PROPS, backgroundColorData: SINGLE_BG_COLOR },
+    false
+  );
+  testSpectrumContrast(s, s.contrastValue, [50, 240, 234, 0.8], "1.35", FAIL);
 
   info("Test that contrast ratio is updated when color is changed.");
-  testSpectrumContrast(s, cssColors.black, "21.00", AAA);
+  testSpectrumContrast(s, s.contrastValue, cssColors.black, "21.00", AAA);
 
   info("Test that contrast ratio cannot be calculated with zero alpha.");
-  testSpectrumContrast(s, ZERO_ALPHA_COLOR, "Unable to calculate");
+  testSpectrumContrast(
+    s,
+    s.contrastValue,
+    ZERO_ALPHA_COLOR,
+    "Unable to calculate"
+  );
 
   s.destroy();
 }
 
 function testOnlySelectingLargeTextWithNonZeroAlphaShouldShowIndicator(
   container
 ) {
   let s = new Spectrum(container, cssColors.white);
   s.show();
 
   ok(
-    !s.spectrumContrast.classList.contains("large-text"),
+    s.contrastLabel.childNodes.length !== 3,
     "Large text indicator is initially hidden."
   );
 
   info(
     "Test that selecting large text with non-zero alpha shows large text indicator."
   );
   setSpectrumProps(
     s,
     {
       textProps: {
         "font-size": { value: "24px" },
         "font-weight": { value: "normal" },
+        opacity: { value: "1" },
       },
+      backgroundColorData: SINGLE_BG_COLOR,
     },
     false
   );
-  testSpectrumContrast(s, cssColors.black, "21.00", AAA, true);
+  testSpectrumContrast(s, s.contrastValue, cssColors.black, "21.00", AAA, true);
 
   info(
     "Test that selecting large text with zero alpha hides large text indicator."
   );
-  testSpectrumContrast(s, ZERO_ALPHA_COLOR, "Unable to calculate");
+  testSpectrumContrast(
+    s,
+    s.contrastValue,
+    ZERO_ALPHA_COLOR,
+    "Unable to calculate"
+  );
 
   // Spectrum should be closed and opened again to reflect changes in text size
   s.destroy();
   s = new Spectrum(container, cssColors.white);
   s.show();
 
   info("Test that selecting regular text does not show large text indicator.");
-  setSpectrumProps(s, { textProps: REGULAR_TEXT_PROPS }, false);
-  testSpectrumContrast(s, cssColors.black, "21.00", AAA);
+  setSpectrumProps(
+    s,
+    { textProps: REGULAR_TEXT_PROPS, backgroundColorData: SINGLE_BG_COLOR },
+    false
+  );
+  testSpectrumContrast(s, s.contrastValue, cssColors.black, "21.00", AAA);
 
   s.destroy();
 }
+
+function testSettingMultiColoredBackgroundShouldShowContrastRange(container) {
+  const s = new Spectrum(container, cssColors.white);
+  s.show();
+
+  info(
+    "Test setting text with non-zero alpha and multi-colored bg shows contrast range and empty single contrast."
+  );
+  setSpectrumProps(
+    s,
+    {
+      textProps: REGULAR_TEXT_PROPS,
+      backgroundColorData: {
+        min: cssColors.yellow,
+        max: cssColors.green,
+      },
+    },
+    false
+  );
+  testSpectrumContrast(s, s.contrastValueMin, cssColors.white, "1.07", FAIL);
+  testSpectrumContrast(s, s.contrastValueMax, cssColors.white, "5.14", AA);
+  testSpectrumContrast(s, s.contrastValue, cssColors.white, "");
+  ok(
+    s.spectrumContrast.classList.contains("range"),
+    "Contrast section contains range class."
+  );
+
+  info("Test setting text with zero alpha shows error in contrast min span.");
+  testSpectrumContrast(
+    s,
+    s.contrastValueMin,
+    ZERO_ALPHA_COLOR,
+    "Unable to calculate"
+  );
+
+  s.destroy();
+}
--- a/devtools/client/shared/widgets/Spectrum.js
+++ b/devtools/client/shared/widgets/Spectrum.js
@@ -9,52 +9,45 @@ const { MultiLocalizationHelper } = requ
 const L10N = new MultiLocalizationHelper(
   "devtools/shared/locales/en-US/accessibility.properties",
   "devtools/client/locales/en-US/accessibility.properties",
   "devtools/client/locales/en-US/inspector.properties"
 );
 const ARROW_KEYS = ["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft"];
 const [ArrowUp, ArrowRight, ArrowDown, ArrowLeft] = ARROW_KEYS;
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
-const COLOR_HEX_WHITE = "#ffffff";
 const SLIDER = {
   hue: {
     MIN: "0",
     MAX: "128",
     STEP: "1",
   },
   alpha: {
     MIN: "0",
     MAX: "1",
     STEP: "0.01",
   },
 };
 
 loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
 loader.lazyRequireGetter(
   this,
-  "cssColors",
-  "devtools/shared/css/color-db",
-  true
-);
-loader.lazyRequireGetter(
-  this,
   "labColors",
   "devtools/shared/css/color-db",
   true
 );
 loader.lazyRequireGetter(
   this,
-  "getContrastRatioScore",
+  "getTextProperties",
   "devtools/shared/accessibility",
   true
 );
 loader.lazyRequireGetter(
   this,
-  "getTextProperties",
+  "getContrastRatioAgainstBackground",
   "devtools/shared/accessibility",
   true
 );
 
 /**
  * Spectrum creates a color picker widget in any container you give it.
  *
  * Simple usage example:
@@ -105,26 +98,31 @@ function Spectrum(parentEl, rgb) {
     <section class="spectrum-controls">
       <div class="spectrum-color-preview"></div>
       <div class="spectrum-slider-container">
         <div class="spectrum-hue spectrum-box"></div>
         <div class="spectrum-alpha spectrum-checker spectrum-box"></div>
       </div>
     </section>
     <section class="spectrum-color-contrast accessibility-color-contrast">
-      <span class="contrast-ratio-label" role="presentation">
-        ${L10N.getStr("accessibility.contrast.ratio.label")}
-      </span>
-      <span class="accessibility-contrast-value"></span>
-      <span
-        class="accessibility-color-contrast-large-text"
-        title="${L10N.getStr("accessibility.contrast.large.title")}"
-      >
-        ${L10N.getStr("accessibility.contrast.large.text")}
-      </span>
+      <div class="contrast-ratio-header-and-single-ratio">
+        <span class="contrast-ratio-label" role="presentation"></span>
+        <span class="contrast-value-and-swatch contrast-ratio-single" role="presentation">
+          <span class="accessibility-contrast-value"></span>
+        </span>
+      </div>
+      <div class="contrast-ratio-range">
+        <span class="contrast-value-and-swatch contrast-ratio-min" role="presentation">
+          <span class="accessibility-contrast-value"></span>
+        </span>
+        <span class="accessibility-color-contrast-separator"></span>
+        <span class="contrast-value-and-swatch contrast-ratio-max" role="presentation">
+          <span class="accessibility-contrast-value"></span>
+        </span>
+      </div>
     </section>
   `;
 
   this.onElementClick = this.onElementClick.bind(this);
   this.element.addEventListener("click", this.onElementClick);
 
   this.parentEl.appendChild(this.element);
 
@@ -159,26 +157,31 @@ function Spectrum(parentEl, rgb) {
     "alpha",
     this.onAlphaSliderMove.bind(this)
   );
 
   // Color contrast
   this.spectrumContrast = this.element.querySelector(
     ".spectrum-color-contrast"
   );
-  this.contrastValue = this.element.querySelector(
-    ".accessibility-contrast-value"
-  );
+  this.contrastLabel = this.element.querySelector(".contrast-ratio-label");
+  [
+    this.contrastValue,
+    this.contrastValueMin,
+    this.contrastValueMax,
+  ] = this.element.querySelectorAll(".accessibility-contrast-value");
 
   // Create the learn more info button
   const learnMore = this.document.createElementNS(XHTML_NS, "button");
   learnMore.id = "learn-more-button";
   learnMore.className = "learn-more";
   learnMore.title = L10N.getStr("accessibility.learnMore");
-  this.spectrumContrast.appendChild(learnMore);
+  this.element
+    .querySelector(".contrast-ratio-header-and-single-ratio")
+    .appendChild(learnMore);
 
   if (rgb) {
     this.rgb = rgb;
     this.updateUI();
   }
 }
 
 module.exports.Spectrum = Spectrum;
@@ -356,57 +359,57 @@ Spectrum.draggable = function(element, d
 };
 
 /**
  * Calculates the contrast ratio for a DOM node's computed style against
  * a given background.
  *
  * @param  {Object} computedStyle
  *         The computed style for which we want to calculate the contrast ratio.
- * @param  {Array} background
- *         The rgba value array for background color (i.e. [255, 255, 255, 1]).
+ * @param  {Object} backgroundColor
+ *         Object with one or more of the following properties: value, min, max
  * @return {Object}
  *         An object that may contain one or more of the following fields: error,
  *         isLargeText, value, score for contrast.
  */
-function getContrastRatioAgainstSolidBg(
-  computedStyle,
-  background = cssColors.white
-) {
+function getContrastRatio(computedStyle, backgroundColor) {
   const props = getTextProperties(computedStyle);
+
   if (!props) {
     return {
       error: true,
     };
   }
 
-  const { color, isLargeText } = props;
-  const value = colorUtils.calculateContrastRatio(background, color);
-  return {
-    value,
-    color,
-    isLargeText,
-    score: getContrastRatioScore(value, isLargeText),
-  };
+  return getContrastRatioAgainstBackground(backgroundColor, props);
 }
 
 Spectrum.prototype = {
   set textProps(style) {
     this._textProps = style
       ? {
           fontSize: style["font-size"].value,
           fontWeight: style["font-weight"].value,
+          opacity: style.opacity.value,
         }
       : null;
   },
 
   set rgb(color) {
     this.hsv = Spectrum.rgbToHsv(color[0], color[1], color[2], color[3]);
   },
 
+  set backgroundColorData(colorData) {
+    this._backgroundColorData = colorData;
+  },
+
+  get backgroundColorData() {
+    return this._backgroundColorData;
+  },
+
   get textProps() {
     return this._textProps;
   },
 
   get rgb() {
     const rgb = Spectrum.hsvToRgb(
       this.hsv[0],
       this.hsv[1],
@@ -512,16 +515,92 @@ Spectrum.prototype = {
     slider.step = SLIDER[sliderType].STEP;
     slider.title = L10N.getStr(`colorPickerTooltip.${sliderType}SliderTitle`);
     slider.addEventListener("input", onSliderMove);
 
     container.appendChild(slider);
     return slider;
   },
 
+  /**
+   * Updates the contrast label with appropriate content (i.e. large text indicator
+   * if the contrast is calculated for large text, or a base label otherwise)
+   *
+   * @param  {Boolean} isLargeText
+   *         True if contrast is calculated for large text.
+   */
+  updateContrastLabel: function(isLargeText) {
+    if (!isLargeText) {
+      this.contrastLabel.textContent = L10N.getStr(
+        "accessibility.contrast.ratio.label"
+      );
+      return;
+    }
+
+    // Clear previously appended children before appending any new children
+    while (this.contrastLabel.firstChild) {
+      this.contrastLabel.firstChild.remove();
+    }
+
+    const largeTextStr = L10N.getStr("accessibility.contrast.large.text");
+    const contrastLabelStr = L10N.getFormatStr(
+      "colorPickerTooltip.contrast.large.title",
+      largeTextStr
+    );
+
+    // Build an array of children nodes for the contrast label element
+    const contents = contrastLabelStr
+      .split(new RegExp(largeTextStr), 2)
+      .map(content => this.document.createTextNode(content));
+    const largeTextIndicator = this.document.createElementNS(XHTML_NS, "span");
+    largeTextIndicator.className = "accessibility-color-contrast-large-text";
+    largeTextIndicator.textContent = largeTextStr;
+    largeTextIndicator.title = L10N.getStr(
+      "accessibility.contrast.large.title"
+    );
+    contents.splice(1, 0, largeTextIndicator);
+
+    // Append children to contrast label
+    for (const content of contents) {
+      this.contrastLabel.appendChild(content);
+    }
+  },
+
+  /**
+   * Updates a contrast value element with the given score, value and swatches.
+   *
+   * @param  {DOMNode} el
+   *         Contrast value element to update.
+   * @param  {String} score
+   *         Contrast ratio score.
+   * @param  {Number} value
+   *         Contrast ratio value.
+   * @param  {Array} backgroundColor
+   *         RGBA color array for the background color to show in the swatch.
+   */
+  updateContrastValueEl: function(el, score, value, backgroundColor) {
+    el.classList.toggle(score, true);
+    el.textContent = value.toFixed(2);
+    el.title = L10N.getFormatStr(
+      `accessibility.contrast.annotation.${score}`,
+      L10N.getFormatStr(
+        "colorPickerTooltip.contrastAgainstBgTitle",
+        `rgba(${backgroundColor})`
+      )
+    );
+    el.parentElement.style.setProperty(
+      "--accessibility-contrast-color",
+      this.rgbCssString
+    );
+    el.parentElement.style.setProperty(
+      "--accessibility-contrast-bg",
+      `rgba(${backgroundColor})`
+    );
+  },
+
   updateAlphaSlider: function() {
     // Set alpha slider background
     const rgb = this.rgb;
 
     const rgbNoAlpha = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
     const rgbAlpha0 = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ", 0)";
     const alphaGradient =
       "linear-gradient(to right, " + rgbAlpha0 + ", " + rgbNoAlpha + ")";
@@ -588,65 +667,106 @@ Spectrum.prototype = {
     // Placing the hue slider
     this.hueSlider.value = h * this.hueSlider.max;
 
     // Placing the alpha slider
     this.alphaSlider.value = this.hsv[3] * this.alphaSlider.max;
   },
 
   /* Calculates the contrast ratio for the currently selected
-   * color against white background and displays/hides contrast ratio span
+   * color against a single or range of background colors and displays contrast ratio section
    * components depending on the contrast ratio calculated.
    *
    * Contrast ratio components include:
    *    - contrastLargeTextIndicator: Hidden by default, shown when text has large font
    *                                  size if there is no error in calculation.
-   *    - contrastValue:              Set to calculated value and score. Set to error text
+   *    - contrastValue(s):           Set to calculated value(s), score(s) and text color on
+   *                                  background swatches. Set to error text
    *                                  if there is an error in calculation.
    */
   updateContrast: function() {
     // Remove additional classes on spectrum contrast, leaving behind only base classes
-    this.spectrumContrast.classList.toggle("large-text", false);
     this.spectrumContrast.classList.toggle("visible", false);
-    // Assign only base class to contrastValue, removing any score class
-    this.contrastValue.className = "accessibility-contrast-value";
+    this.spectrumContrast.classList.toggle("range", false);
+    this.spectrumContrast.classList.toggle("error", false);
+    // Assign only base class to all contrastValues, removing any score class
+    this.contrastValue.className = this.contrastValueMin.className = this.contrastValueMax.className =
+      "accessibility-contrast-value";
 
     if (!this.contrastEnabled) {
       return;
     }
 
+    const isRange = this.backgroundColorData.min !== undefined;
     this.spectrumContrast.classList.toggle("visible", true);
+    this.spectrumContrast.classList.toggle("range", isRange);
+
+    const colorContrast = getContrastRatio(
+      {
+        ...this.textProps,
+        color: this.rgbCssString,
+      },
+      this.backgroundColorData
+    );
 
-    const contrastRatio = getContrastRatioAgainstSolidBg({
-      ...this.textProps,
-      color: this.rgbCssString,
-    });
-    const { value, score, isLargeText, error } = contrastRatio;
+    const {
+      value,
+      min,
+      max,
+      score,
+      scoreMin,
+      scoreMax,
+      backgroundColor,
+      backgroundColorMin,
+      backgroundColorMax,
+      isLargeText,
+      error,
+    } = colorContrast;
 
     if (error) {
-      this.contrastValue.textContent = L10N.getStr(
-        "accessibility.contrast.error"
-      );
-      this.contrastValue.title = L10N.getStr(
+      this.updateContrastLabel(false);
+      this.spectrumContrast.classList.toggle("error", true);
+
+      // If current background color is a range, show the error text in the contrast range
+      // span. Otherwise, show it in the single contrast span.
+      const contrastValEl = isRange
+        ? this.contrastValueMin
+        : this.contrastValue;
+      contrastValEl.textContent = L10N.getStr("accessibility.contrast.error");
+      contrastValEl.title = L10N.getStr(
         "accessibility.contrast.annotation.transparent.error"
       );
-      this.spectrumContrast.classList.remove("large-text");
+
       return;
     }
 
-    this.contrastValue.classList.toggle(score, true);
-    this.contrastValue.textContent = value.toFixed(2);
-    this.contrastValue.title = L10N.getFormatStr(
-      `accessibility.contrast.annotation.${score}`,
-      L10N.getFormatStr(
-        "colorPickerTooltip.contrastAgainstBgTitle",
-        COLOR_HEX_WHITE
-      )
+    this.updateContrastLabel(isLargeText);
+    if (!isRange) {
+      this.updateContrastValueEl(
+        this.contrastValue,
+        score,
+        value,
+        backgroundColor
+      );
+
+      return;
+    }
+
+    this.updateContrastValueEl(
+      this.contrastValueMin,
+      scoreMin,
+      min,
+      backgroundColorMin
     );
-    this.spectrumContrast.classList.toggle("large-text", isLargeText);
+    this.updateContrastValueEl(
+      this.contrastValueMax,
+      scoreMax,
+      max,
+      backgroundColorMax
+    );
   },
 
   updateUI: function() {
     this.updateHelperLocations();
 
     this.updateColorPreview();
     this.updateDragger();
     this.updateHueSlider();
@@ -663,11 +783,12 @@ Spectrum.prototype = {
 
     this.dragger = this.dragHelper = null;
     this.alphaSlider = null;
     this.hueSlider = null;
     this.colorPreview = null;
     this.element = null;
     this.parentEl = null;
     this.spectrumContrast = null;
-    this.contrastValue = null;
+    this.contrastValue = this.contrastValueMin = this.contrastValueMax = null;
+    this.contrastLabel = null;
   },
 };
--- a/devtools/client/shared/widgets/spectrum.css
+++ b/devtools/client/shared/widgets/spectrum.css
@@ -203,65 +203,123 @@ http://www.briangrinstead.com/blog/keep-
   height: 8px;
   width: 8px;
   border: 1px solid white;
   box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
 }
 
 .spectrum-color-contrast {
   padding-block-start: 8px;
-  padding-inline-start: 3px;
-  align-items: stretch;
+  padding-inline-start: 4px;
+  padding-inline-end: 4px;
   line-height: 1.2em;
 }
 
-.spectrum-color-contrast.visible.large-text .accessibility-color-contrast-large-text {
+.contrast-ratio-header-and-single-ratio,
+.contrast-ratio-range {
+  display: flex;
+  align-items: stretch;
+}
+
+.contrast-ratio-range {
+  margin-block-start: 4px;
+  margin-inline-start: 1px;
+  margin-block-end: 2px;
+}
+
+.spectrum-color-contrast.visible {
   display: block;
 }
 
-.spectrum-color-contrast.visible {
+.spectrum-color-contrast.visible:not(.range) .contrast-ratio-single,
+.spectrum-color-contrast.visible.range .contrast-ratio-range {
   display: flex;
 }
 
 .spectrum-color-contrast,
-.spectrum-color-contrast .accessibility-color-contrast-large-text {
+.spectrum-color-contrast .contrast-ratio-range,
+.spectrum-color-contrast.range .contrast-ratio-single,
+.spectrum-color-contrast.error .accessibility-color-contrast-separator,
+.spectrum-color-contrast.error .contrast-ratio-max {
   display: none;
 }
 
 .contrast-ratio-label {
+  font-size: 10px;
   padding-inline-end: 4px;
-  padding-inline-start: 2px;
   color: var(--theme-toolbar-color);
 }
 
-.spectrum-color-contrast .accessibility-color-contrast {
-  align-items: stretch;
+.spectrum-color-contrast .accessibility-contrast-value {
+  font-size: 10px;
+  color: var(--theme-body-color);
+  border-bottom: 1px solid var(--learn-more-underline);
 }
 
-.spectrum-color-contrast .accessibility-contrast-value {
-  color: var(--theme-body-color);
-  border-bottom: 1px solid var(--learn-more-underline);
+.spectrum-color-contrast.visible:not(.error) .contrast-ratio-single .accessibility-contrast-value {
+  margin-inline-start: 10px;
+}
+
+.spectrum-color-contrast.visible:not(.error) .contrast-ratio-min .accessibility-contrast-value,
+.spectrum-color-contrast.visible:not(.error) .contrast-ratio-max .accessibility-contrast-value{
+  margin-inline-start: 7px;
 }
 
 .spectrum-color-contrast .accessibility-contrast-value:not(:empty)::before {
   width: auto;
   content: none;
   padding-inline-start: 2px;
 }
 
+.spectrum-color-contrast.visible:not(.error) .contrast-value-and-swatch:before {
+  display: inline-flex;
+  content: "";
+  height: 9px;
+  width: 9px;
+  background-color: var(--accessibility-contrast-color);
+}
+
+.spectrum-color-contrast.visible:not(.error):-moz-locale-dir(ltr) .contrast-value-and-swatch:before {
+  box-shadow: 0 0 0 1px var(--grey-40), 6px 5px var(--accessibility-contrast-bg),
+    6px 5px 0 1px var(--grey-40);
+}
+
+.spectrum-color-contrast.visible:not(.error):-moz-locale-dir(rtl) .contrast-value-and-swatch:before {
+  box-shadow: 0 0 0 1px var(--grey-40), -6px 5px var(--accessibility-contrast-bg),
+    -6px 5px 0 1px var(--grey-40);
+}
+
+.spectrum-color-contrast .accessibility-color-contrast-separator:before {
+  margin-inline-end: 4px;
+  color: var(--theme-body-color);
+}
+
+.spectrum-color-contrast .accessibility-color-contrast-large-text {
+  margin-inline-start: 1px;
+  margin-inline-end: 1px;
+  unicode-bidi: isolate;
+}
+
 .learn-more {
-  background-size: 12px 12px;
   background-repeat: no-repeat;
   -moz-context-properties: fill;
   background-image: url(chrome://devtools/skin/images/info-small.svg);
   background-color: transparent;
   fill: var(--theme-icon-dimmed-color);
   border: none;
   margin-inline-start: auto;
-  margin-block-start: 2px;
+  margin-block-start: 1px;
+}
+
+.learn-more:-moz-locale-dir(ltr) {
+  margin-inline-end: -5px;
+}
+
+.learn-more:-moz-locale-dir(rtl) {
+  margin-inline-end: -2px;
 }
 
 .learn-more:hover,
 .learn-more:focus {
   fill: var(--theme-icon-color);
   cursor: pointer;
   outline: none;
 }
--- a/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
@@ -105,30 +105,31 @@ class SwatchColorPickerTooltip extends S
   async show() {
     // set contrast enabled for the spectrum
     const name = this.activeSwatch.dataset.propertyName;
 
     if (this.isContrastCompatible === undefined) {
       const target = this.inspector.target;
       this.isContrastCompatible = await target.actorHasMethod(
         "domnode",
-        "getClosestBackgroundColor"
+        "getBackgroundColor"
       );
     }
 
-    // Only enable contrast and set spectrum text props if selected node is
+    // Only enable contrast and set text props and bg color if selected node is
     // contrast compatible and if the type of property is color.
     this.spectrum.contrastEnabled =
       name === "color" && this.isContrastCompatible;
-    this.spectrum.textProps = this.spectrum.contrastEnabled
-      ? await this.inspector.pageStyle.getComputed(
-          this.inspector.selection.nodeFront,
-          { filterProperties: ["font-size", "font-weight"] }
-        )
-      : null;
+    if (this.spectrum.contrastEnabled) {
+      this.spectrum.textProps = await this.inspector.pageStyle.getComputed(
+        this.inspector.selection.nodeFront,
+        { filterProperties: ["font-size", "font-weight", "opacity"] }
+      );
+      this.spectrum.backgroundColorData = await this.inspector.selection.nodeFront.getBackgroundColor();
+    }
 
     // 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;
       const color = this.activeSwatch.style.backgroundColor;
 
       this.spectrum.off("changed", this._onSpectrumColorChange);
--- a/devtools/client/themes/accessibility-color-contrast.css
+++ b/devtools/client/themes/accessibility-color-contrast.css
@@ -43,16 +43,21 @@
 }
 
 .accessibility-color-contrast
   .accessibility-contrast-value:not(:empty).AAA:after {
   content: "AAA\2713";
   unicode-bidi: isolate;
 }
 
+.accessibility-color-contrast .accessibility-color-contrast-separator:before {
+  content: "–";
+  margin-inline-start: 4px;
+}
+
 .accessibility-color-contrast-large-text {
   background-color: var(--badge-background-color);
   color: var(--badge-color);
   outline: 1px solid var(--badge-border-color);
   -moz-outline-radius: 3px;
   padding: 0px 2px;
   margin-inline-start: 6px;
   line-height: initial;