Bug 1476311 - Part 1: Backout the changes in Bug 1446316 in removing the new ColorWidget code. r=pbro draft
authorGabriel Luong <gabriel.luong@gmail.com>
Tue, 17 Jul 2018 11:50:05 -0400
changeset 819324 8ddac741f9c89a04f0e4756c9fce140bb89df62f
parent 819320 e91802969fb712eae9d2a5975406082bdbb64b95
push id116508
push userbmo:gl@mozilla.com
push dateTue, 17 Jul 2018 15:51:08 +0000
reviewerspbro
bugs1476311, 1446316
milestone63.0a1
Bug 1476311 - Part 1: Backout the changes in Bug 1446316 in removing the new ColorWidget code. r=pbro MozReview-Commit-ID: 7zIbTIpzGue
devtools/client/jar.mn
devtools/client/locales/en-US/inspector.properties
devtools/client/preferences/devtools-client.js
devtools/client/shared/widgets/ColorWidget.js
devtools/client/shared/widgets/color-widget.css
devtools/client/shared/widgets/moz.build
devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
devtools/client/themes/tooltips.css
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -81,16 +81,17 @@ devtools.jar:
     content/framework/toolbox-process-window.js (framework/toolbox-process-window.js)
     content/inspector/index.xhtml (inspector/index.xhtml)
     content/framework/connect/connect.xhtml (framework/connect/connect.xhtml)
     content/framework/connect/connect.css (framework/connect/connect.css)
     content/framework/connect/connect.js (framework/connect/connect.js)
     content/shared/widgets/graphs-frame.xhtml (shared/widgets/graphs-frame.xhtml)
     content/shared/widgets/cubic-bezier.css (shared/widgets/cubic-bezier.css)
     content/shared/widgets/filter-widget.css (shared/widgets/filter-widget.css)
+    content/shared/widgets/color-widget.css (shared/widgets/color-widget.css)
     content/shared/widgets/spectrum.css (shared/widgets/spectrum.css)
     content/aboutdebugging/aboutdebugging.xhtml (aboutdebugging/aboutdebugging.xhtml)
     content/aboutdebugging/aboutdebugging.css (aboutdebugging/aboutdebugging.css)
     content/responsive.html/index.xhtml (responsive.html/index.xhtml)
     content/dom/index.html (dom/index.html)
     content/dom/main.js (dom/main.js)
     content/accessibility/index.html (accessibility/index.html)
     content/accessibility/main.js (accessibility/main.js)
--- a/devtools/client/locales/en-US/inspector.properties
+++ b/devtools/client/locales/en-US/inspector.properties
@@ -415,16 +415,54 @@ inspector.sidebar.newBadge=new!
 # 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.colorNameLabel):
+# The label for the current color widget's color name field.
+inspector.colorwidget.colorNameLabel=Color Name:
+
+# 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/preferences/devtools-client.js
+++ b/devtools/client/preferences/devtools-client.js
@@ -60,16 +60,18 @@ pref("devtools.inspector.showAllAnonymou
 // Enable the Flexbox highlighter
 pref("devtools.inspector.flexboxHighlighter.enabled", false);
 // Enable the CSS shapes highlighter
 pref("devtools.inspector.shapesHighlighter.enabled", true);
 // Enable the Flexbox Inspector panel
 pref("devtools.flexboxinspector.enabled", false);
 // Enable the new Animation Inspector
 pref("devtools.new-animationinspector.enabled", true);
+// Enable the new color widget
+pref("devtools.inspector.colorWidget.enabled", false);
 
 // Enable the Variable Fonts editor only in Nightly
 #if defined(NIGHTLY_BUILD)
 pref("devtools.inspector.fonteditor.enabled", true);
 #else
 pref("devtools.inspector.fonteditor.enabled", false);
 #endif
 // Enable the font highlight-on-hover feature
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/widgets/ColorWidget.js
@@ -0,0 +1,690 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * 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 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]);
+ * s.on("changed", (rgba, color) => {
+ *   console.log("rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ", " +
+ *     rgba[3] + ")");
+ * });
+ * s.show();
+ * s.destroy();
+ *
+ * Note that the color picker is hidden by default and you need to call show to
+ * make it appear. This 2 stages initialization helps in cases you are creating
+ * the color picker in a parent element that hasn't been appended anywhere yet
+ * or that is hidden. Calling show() when the parent element is appended and
+ * visible will allow the color widget to correctly initialize its various parts.
+ *
+ * Fires the following events:
+ * - changed : When the user changes the current color
+ */
+function ColorWidget(parentEl, rgb) {
+  EventEmitter.decorate(this);
+
+  this.parentEl = parentEl;
+
+  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();
+  }
+}
+
+/**
+ * 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;
+
+  const i = Math.floor(h * 6);
+  const f = h * 6 - i;
+  const p = v * (1 - s);
+  const q = v * (1 - f * s);
+  const t = v * (1 - (1 - f) * s);
+
+  switch (i % 6) {
+    case 0: r = v; g = t; b = p; break;
+    case 1: r = q; g = v; b = p; break;
+    case 2: r = p; g = v; b = t; break;
+    case 3: r = p; g = q; b = v; break;
+    case 4: r = t; g = p; b = v; break;
+    case 5: r = v; g = p; b = q; break;
+  }
+
+  return [r * 255, g * 255, b * 255, a];
+};
+
+ColorWidget.rgbToHsv = function(r, g, b, a) {
+  r = r / 255;
+  g = g / 255;
+  b = b / 255;
+
+  const max = Math.max(r, g, b);
+  const min = Math.min(r, g, b);
+
+  const v = max;
+  const d = max - min;
+  const s = max == 0 ? 0 : d / max;
+
+  let h;
+  if (max == min) {
+    // achromatic
+    h = 0;
+  } else {
+    switch (max) {
+      case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+      case g: h = (b - r) / d + 2; break;
+      case b: h = (r - g) / d + 4; break;
+    }
+    h /= 6;
+  }
+  return [h, s, v, a];
+};
+
+ColorWidget.hslToCssString = function(h, s, l, a) {
+  return `hsla(${h}, ${s}%, ${l}%, ${a})`;
+};
+
+ColorWidget.draggable = function(element, onmove, onstart, onstop) {
+  onmove = onmove || function() {};
+  onstart = onstart || function() {};
+  onstop = onstop || function() {};
+
+  const doc = element.ownerDocument;
+  let dragging = false;
+  let offset = {};
+  let maxHeight = 0;
+  let maxWidth = 0;
+
+  function prevent(e) {
+    e.stopPropagation();
+    e.preventDefault();
+  }
+
+  function move(e) {
+    if (dragging) {
+      if (e.buttons === 0) {
+        // The button is no longer pressed but we did not get a mouseup event.
+        stop();
+        return;
+      }
+      const pageX = e.pageX;
+      const pageY = e.pageY;
+
+      const dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth));
+      const dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight));
+
+      onmove.apply(element, [dragX, dragY]);
+    }
+  }
+
+  function start(e) {
+    const rightclick = e.which === 3;
+
+    if (!rightclick && !dragging) {
+      if (onstart.apply(element, arguments) !== false) {
+        dragging = true;
+        maxHeight = element.offsetHeight;
+        maxWidth = element.offsetWidth;
+
+        offset = element.getBoundingClientRect();
+
+        move(e);
+
+        doc.addEventListener("selectstart", prevent);
+        doc.addEventListener("dragstart", prevent);
+        doc.addEventListener("mousemove", move);
+        doc.addEventListener("mouseup", stop);
+
+        prevent(e);
+      }
+    }
+  }
+
+  function stop() {
+    if (dragging) {
+      doc.removeEventListener("selectstart", prevent);
+      doc.removeEventListener("dragstart", prevent);
+      doc.removeEventListener("mousemove", move);
+      doc.removeEventListener("mouseup", stop);
+      onstop.apply(element, arguments);
+    }
+    dragging = false;
+  }
+
+  element.addEventListener("mousedown", start);
+};
+
+ColorWidget.prototype = {
+  set rgb(color) {
+    this.hsv = ColorWidget.rgbToHsv(color[0], color[1], color[2], color[3]);
+
+    const { h, s, l } = new colorUtils.CssColor(this.rgbCssString)._getHSLATuple();
+    this.hsl = [h, s, l, color[3]];
+  },
+
+  get rgb() {
+    const rgb = ColorWidget.hsvToRgb(this.hsv[0], this.hsv[1], this.hsv[2],
+      this.hsv[3]);
+    return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]),
+            Math.round(rgb[3] * 100) / 100];
+  },
+
+  get rgbNoSatVal() {
+    const rgb = ColorWidget.hsvToRgb(this.hsv[0], 1, 1);
+    return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]), rgb[3]];
+  },
+
+  get rgbCssString() {
+    const rgb = this.rgb;
+    return "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")";
+  },
+
+  initializeColorWidget: function() {
+    const colorNameLabel = L10N.getStr("inspector.colorwidget.colorNameLabel");
+    this.parentEl.innerHTML = "";
+    this.element = this.parentEl.ownerDocument.createElementNS(XHTML_NS, "div");
+
+    this.element.className = "colorwidget-container";
+    // eslint-disable-next-line no-unsanitized/property
+    this.element.innerHTML = `
+      <div class="colorwidget-top">
+        <div class="colorwidget-fill"></div>
+        <div class="colorwidget-top-inner">
+          <div class="colorwidget-color colorwidget-box">
+            <div class="colorwidget-sat">
+              <div class="colorwidget-val">
+                <div class="colorwidget-dragger"></div>
+              </div>
+            </div>
+          </div>
+          <div class="colorwidget-hue colorwidget-box">
+            <div class="colorwidget-slider colorwidget-slider-control"></div>
+          </div>
+        </div>
+      </div>
+      <div class="colorwidget-alpha colorwidget-checker colorwidget-box">
+        <div class="colorwidget-alpha-inner">
+          <div class="colorwidget-alpha-handle colorwidget-slider-control"></div>
+        </div>
+      </div>
+      <div class="colorwidget-value">
+        <select class="colorwidget-select">
+          <option value="hex">Hex</option>
+          <option value="rgba">RGBA</option>
+          <option value="hsla">HSLA</option>
+        </select>
+        <div class="colorwidget-hex">
+          <input class="colorwidget-hex-input"/>
+        </div>
+        <div class="colorwidget-rgba colorwidget-hidden">
+          <input class="colorwidget-rgba-r" data-id="r" />
+          <input class="colorwidget-rgba-g" data-id="g" />
+          <input class="colorwidget-rgba-b" data-id="b" />
+          <input class="colorwidget-rgba-a" data-id="a" />
+        </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-colorname">
+        <label class="colorwidget-colorname-label">${colorNameLabel}</label>
+        <span class="colorwidget-colorname-value"></span>
+      </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.colorName = this.element.querySelector(".colorwidget-colorname-value");
+
+    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);
+
+    this.alphaSlider = this.element.querySelector(".colorwidget-alpha");
+    this.alphaSliderInner = this.element.querySelector(".colorwidget-alpha-inner");
+    this.alphaSliderHelper = this.element.querySelector(".colorwidget-alpha-handle");
+    ColorWidget.draggable(this.alphaSliderInner, this.onAlphaSliderMove);
+
+    this.colorSelect = this.element.querySelector(".colorwidget-select");
+    this.colorSelect.addEventListener("change", this.onSelectValueChange);
+
+    this.hexValue = this.element.querySelector(".colorwidget-hex");
+    this.hexValueInput = this.element.querySelector(".colorwidget-hex-input");
+    this.hexValueInput.addEventListener("input", this.onHexInputChange);
+
+    this.rgbaValue = this.element.querySelector(".colorwidget-rgba");
+    this.rgbaValueInputs = {
+      r: this.element.querySelector(".colorwidget-rgba-r"),
+      g: this.element.querySelector(".colorwidget-rgba-g"),
+      b: this.element.querySelector(".colorwidget-rgba-b"),
+      a: this.element.querySelector(".colorwidget-rgba-a"),
+    };
+    this.rgbaValue.addEventListener("input", this.onRgbaInputChange);
+
+    this.hslaValue = this.element.querySelector(".colorwidget-hsla");
+    this.hslaValueInputs = {
+      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);
+  },
+
+  async show() {
+    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) {
+      const node = this.inspector.selection.nodeFront;
+      this.closestBackgroundColor = await 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;
+    this.updateUI();
+    this.onChange();
+  },
+
+  onDraggerMove: function(dragX, dragY) {
+    this.hsv[1] = dragX / this.dragWidth;
+    this.hsv[2] = (this.dragHeight - dragY) / this.dragHeight;
+
+    this.hsl[2] = ((2 - this.hsv[1]) * this.hsv[2] / 2);
+    if (this.hsl[2] && this.hsl[2] < 1) {
+      this.hsl[1] = this.hsv[1] * this.hsv[2] /
+          (this.hsl[2] < 0.5 ? this.hsl[2] * 2 : 2 - this.hsl[2] * 2);
+      this.hsl[1] = this.hsl[1] * 100;
+    }
+    this.hsl[2] = this.hsl[2] * 100;
+
+    this.updateUI();
+    this.onChange();
+  },
+
+  onAlphaSliderMove: function(dragX, dragY) {
+    this.hsv[3] = dragX / this.alphaSliderWidth;
+    this.hsl[3] = dragX / this.alphaSliderWidth;
+    this.updateUI();
+    this.onChange();
+  },
+
+  onSelectValueChange: function(event) {
+    const selection = event.target.value;
+    this.colorSelect.classList.remove("colorwidget-select-spacing");
+    this.hexValue.classList.add("colorwidget-hidden");
+    this.rgbaValue.classList.add("colorwidget-hidden");
+    this.hslaValue.classList.add("colorwidget-hidden");
+
+    switch (selection) {
+      case "hex":
+        this.hexValue.classList.remove("colorwidget-hidden");
+        break;
+      case "rgba":
+        this.colorSelect.classList.add("colorwidget-select-spacing");
+        this.rgbaValue.classList.remove("colorwidget-hidden");
+        break;
+      case "hsla":
+        this.colorSelect.classList.add("colorwidget-select-spacing");
+        this.hslaValue.classList.remove("colorwidget-hidden");
+        break;
+    }
+  },
+
+  onHexInputChange: function(event) {
+    const hex = event.target.value;
+    const color = new colorUtils.CssColor(hex, true);
+    if (!color.rgba) {
+      return;
+    }
+
+    const { r, g, b, a } = color.getRGBATuple();
+    this.rgb = [r, g, b, a];
+    this.updateUI();
+    this.onChange();
+  },
+
+  onRgbaInputChange: function(event) {
+    const field = event.target.dataset.id;
+    const value = event.target.value.toString();
+    if (!value || isNaN(value) || value.endsWith(".")) {
+      return;
+    }
+
+    const rgb = this.rgb;
+
+    switch (field) {
+      case "r":
+        rgb[0] = value;
+        break;
+      case "g":
+        rgb[1] = value;
+        break;
+      case "b":
+        rgb[2] = value;
+        break;
+      case "a":
+        rgb[3] = Math.min(value, 1);
+        break;
+    }
+
+    this.rgb = rgb;
+
+    this.updateUI();
+    this.onChange();
+  },
+
+  onHslaInputChange: function(event) {
+    const field = event.target.dataset.id;
+    let value = event.target.value.toString();
+    if ((field === "s" || field === "l") && !value.endsWith("%")) {
+      return;
+    }
+
+    if (value.endsWith("%")) {
+      value = value.substring(0, value.length - 1);
+    }
+
+    if (!value || isNaN(value) || value.endsWith(".")) {
+      return;
+    }
+
+    const hsl = this.hsl;
+
+    switch (field) {
+      case "h":
+        hsl[0] = value;
+        break;
+      case "s":
+        hsl[1] = value;
+        break;
+      case "l":
+        hsl[2] = value;
+        break;
+      case "a":
+        hsl[3] = Math.min(value, 1);
+        break;
+    }
+
+    const cssString = ColorWidget.hslToCssString(hsl[0], hsl[1], hsl[2], hsl[3]);
+    const { r, g, b, a } = new colorUtils.CssColor(cssString).getRGBATuple();
+
+    this.rgb = [r, g, b, a];
+
+    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);
+    const rgba = this.rgbaColor.getRGBATuple();
+    const backgroundColor = [rgba.r, rgba.g, rgba.b, rgba.a];
+
+    const textColor = this.rgb;
+
+    const ratio = colorUtils.calculateContrastRatio(backgroundColor, textColor);
+
+    const 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;
+    }
+
+    const h = this.hsv[0];
+    const s = this.hsv[1];
+    const v = this.hsv[2];
+
+    // Placing the color dragger
+    let dragX = s * this.dragWidth;
+    let dragY = this.dragHeight - (v * this.dragHeight);
+    const helperDim = this.dragHelperHeight / 2;
+
+    dragX = Math.max(
+      -helperDim,
+      Math.min(this.dragWidth - helperDim, dragX - helperDim)
+    );
+    dragY = Math.max(
+      -helperDim,
+      Math.min(this.dragHeight - helperDim, dragY - helperDim)
+    );
+
+    this.dragHelper.style.top = dragY + "px";
+    this.dragHelper.style.left = dragX + "px";
+
+    // Placing the hue slider
+    const slideY = (h * this.slideHeight) - this.slideHelperHeight / 2;
+    this.slideHelper.style.top = slideY + "px";
+
+    // Placing the alpha slider
+    const alphaSliderX = (this.hsv[3] * this.alphaSliderWidth) -
+      (this.alphaSliderHelperWidth / 2);
+    this.alphaSliderHelper.style.left = alphaSliderX + "px";
+
+    const color = new colorUtils.CssColor(this.rgbCssString);
+
+    // Updating the hex field
+    this.hexValueInput.value = color.hex;
+
+    // Updating the RGBA fields
+    const rgb = this.rgb;
+    this.rgbaValueInputs.r.value = rgb[0];
+    this.rgbaValueInputs.g.value = rgb[1];
+    this.rgbaValueInputs.b.value = rgb[2];
+    this.rgbaValueInputs.a.value = parseFloat(rgb[3].toFixed(1));
+
+    // 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() {
+    this.updateHelperLocations();
+
+    const rgb = this.rgb;
+    const rgbNoSatVal = this.rgbNoSatVal;
+
+    const flatColor = "rgb(" + rgbNoSatVal[0] + ", " + rgbNoSatVal[1] + ", " +
+      rgbNoSatVal[2] + ")";
+
+    this.dragger.style.backgroundColor = flatColor;
+
+    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 + ")";
+    this.alphaSliderInner.style.background = alphaGradient;
+
+    const colorName = colorUtils.rgbToColorName(rgb[0], rgb[1], rgb[2]);
+    this.colorName.textContent = colorName || "---";
+  },
+
+  destroy: function() {
+    this.element.removeEventListener("click", this.onElementClick);
+
+    this.parentEl.removeChild(this.element);
+
+    this.slider = null;
+    this.dragger = null;
+    this.alphaSlider = this.alphaSliderInner = this.alphaSliderHelper = null;
+    this.parentEl = null;
+    this.element = null;
+    this.colorName = null;
+  }
+};
+
+module.exports = ColorWidget;
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/widgets/color-widget.css
@@ -0,0 +1,254 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#eyedropper-button {
+  margin-inline-start: 5px;
+  display: block;
+}
+
+#eyedropper-button::before {
+  background-image: url(chrome://devtools/skin/images/command-eyedropper.svg);
+}
+
+/* Mix-in classes */
+
+.colorwidget-checker {
+  background-color: #eee;
+  background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
+    linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
+  background-size: 12px 12px;
+  background-position: 0 0, 6px 6px;
+}
+
+.colorwidget-slider-control {
+  cursor: pointer;
+  box-shadow: 0 0 2px rgba(0,0,0,.6);
+  background: #fff;
+  border-radius: 10px;
+  opacity: .8;
+}
+
+.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;
+  display: none;
+  top: 0;
+  left: 0;
+  border-radius: 0;
+  width: 200px;
+  padding: 5px;
+}
+
+.colorwidget-show {
+  display: inline-block;
+}
+
+/* Keep aspect ratio:
+http://www.briangrinstead.com/blog/keep-aspect-ratio-with-html-and-css */
+.colorwidget-top {
+  position: relative;
+  width: 100%;
+  display: inline-block;
+}
+
+.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%;
+}
+
+.colorwidget-hue {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 83%;
+}
+
+.colorwidget-fill {
+  /* Same as colorwidget-color width */
+  margin-top: 85%;
+}
+
+.colorwidget-sat, .colorwidget-val {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+}
+
+.colorwidget-dragger, .colorwidget-slider {
+  -moz-user-select: none;
+}
+
+.colorwidget-alpha {
+  position: relative;
+  height: 8px;
+  margin-top: 3px;
+}
+
+.colorwidget-alpha-inner {
+  height: 100%;
+}
+
+.colorwidget-alpha-handle {
+  position: absolute;
+  top: -3px;
+  bottom: -3px;
+  width: 5px;
+  left: 50%;
+}
+
+.colorwidget-sat {
+  background-image: linear-gradient(to right, #FFF, rgba(204, 154, 129, 0));
+}
+
+.colorwidget-val {
+  background-image: linear-gradient(to top, #000000, rgba(204, 154, 129, 0));
+}
+
+.colorwidget-hue {
+  background: linear-gradient(to bottom, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
+}
+
+.colorwidget-dragger {
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  cursor: pointer;
+  border-radius: 50%;
+  height: 8px;
+  width: 8px;
+  border: 1px solid white;
+  box-shadow: 0 0 2px rgba(0,0,0,.6);
+}
+
+.colorwidget-slider {
+  position: absolute;
+  top: 0;
+  height: 5px;
+  left: -3px;
+  right: -3px;
+}
+
+/**
+ * Color Widget Editor
+ */
+
+.colorwidget-value {
+  position: relative;
+  margin-top: 8px;
+}
+
+/**
+ * Color Widget Select
+ */
+
+.colorwidget-select {
+  width: 100%;
+}
+
+.colorwidget-select-spacing {
+  letter-spacing: 40px;
+}
+
+.colorwidget-select-spacing option {
+  letter-spacing: initial;
+}
+
+/**
+ * Color Widget Inputs
+ */
+
+.colorwidget-hidden {
+  display: none;
+}
+
+.colorwidget-hex,
+.colorwidget-rgba,
+.colorwidget-hsla {
+  width: 200px;
+  font-size: 0;
+}
+
+.colorwidget-hex-input {
+  width: 192px;
+}
+
+.colorwidget-rgba-r,
+.colorwidget-rgba-g,
+.colorwidget-rgba-b,
+.colorwidget-rgba-a,
+.colorwidget-hsla-h,
+.colorwidget-hsla-s,
+.colorwidget-hsla-l,
+.colorwidget-hsla-a {
+  width: 42px;
+  margin: 0;
+}
\ No newline at end of file
--- a/devtools/client/shared/widgets/moz.build
+++ b/devtools/client/shared/widgets/moz.build
@@ -8,16 +8,17 @@ DIRS += [
     'tooltip',
 ]
 
 DevToolsModules(
     'AbstractTreeItem.jsm',
     'BarGraphWidget.js',
     'BreadcrumbsWidget.jsm',
     'Chart.js',
+    'ColorWidget.js',
     'CubicBezierPresets.js',
     'CubicBezierWidget.js',
     'FastListWidget.js',
     'FilterWidget.js',
     'FlameGraph.js',
     'Graphs.js',
     'GraphsWorker.js',
     'LineGraphWidget.js',
--- a/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
@@ -1,20 +1,26 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+const Services = require("Services");
 const {colorUtils} = require("devtools/shared/css/color");
 const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
 const SwatchBasedEditorTooltip = require("devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip");
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
 
+loader.lazyRequireGetter(this, "ColorWidget", "devtools/client/shared/widgets/ColorWidget");
+
+const COLOR_WIDGET_ENABLED_PREF = "devtools.inspector.colorWidget.enabled";
+const NEW_COLOR_WIDGET = Services.prefs.getBoolPref(COLOR_WIDGET_ENABLED_PREF);
+
 const TELEMETRY_PICKER_EYEDROPPER_OPEN_COUNT = "DEVTOOLS_PICKER_EYEDROPPER_OPENED_COUNT";
 
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
 
 /**
  * The swatch color picker tooltip class is a specific class meant to be used
  * along with output-parser's generated color swatches.
  * It extends the parent SwatchBasedEditorTooltip class.
@@ -51,21 +57,29 @@ class SwatchColorPickerTooltip extends S
 
   setColorPickerContent(color) {
     const { doc } = this.tooltip;
 
     const container = doc.createElementNS(XHTML_NS, "div");
     container.id = "spectrum-tooltip";
 
     const node = doc.createElementNS(XHTML_NS, "div");
-    node.id = "spectrum";
-    container.appendChild(node);
+    let widget;
 
-    const widget = new Spectrum(node, color);
-    this.tooltip.setContent(container, { width: 218, height: 224 });
+    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;
 
     const 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.
--- a/devtools/client/themes/tooltips.css
+++ b/devtools/client/themes/tooltips.css
@@ -1,14 +1,15 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Import stylesheets for specific tooltip widgets */
+@import url(chrome://devtools/content/shared/widgets/color-widget.css);
 @import url(chrome://devtools/content/shared/widgets/cubic-bezier.css);
 @import url(chrome://devtools/content/shared/widgets/filter-widget.css);
 @import url(chrome://devtools/content/shared/widgets/spectrum.css);
 
 /* Tooltip specific theme variables */
 
 .theme-dark {
   --bezier-diagonal-color: #eee;