Bug 1478152 - Style current color picker to match new design, r=mtigley
authorMaliha Islam <mislam@mozilla.com>
Fri, 17 May 2019 19:07:56 +0000
changeset 536285 67b8391890d08c4eabe23de9a77bd0a87cadfef6
parent 536284 70b0d5a417b826c34f76cc244a303200fc4de885
child 536286 9e5d3e59a35e2e50bf3b684368c636d2d833c920
push id2082
push userffxbld-merge
push dateMon, 01 Jul 2019 08:34:18 +0000
treeherdermozilla-release@2fb19d0466d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmtigley
bugs1478152
milestone68.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 1478152 - Style current color picker to match new design, r=mtigley Differential Revision: https://phabricator.services.mozilla.com/D31518
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
--- a/devtools/client/shared/test/browser_spectrum.js
+++ b/devtools/client/shared/test/browser_spectrum.js
@@ -15,20 +15,61 @@ add_task(async function() {
 
   const container = doc.getElementById("spectrum-container");
 
   await testCreateAndDestroyShouldAppendAndRemoveElements(container);
   await testPassingAColorAtInitShouldSetThatColor(container);
   await testSettingAndGettingANewColor(container);
   await testChangingColorShouldEmitEvents(container);
   await testSettingColorShoudUpdateTheUI(container);
+  await testChangingColorShouldUpdateColorPreview(container);
 
   host.destroy();
 });
 
+/**
+ * Helper method for extracting the rgba overlay value of the color preview's background
+ * image style.
+ *
+ * @param   {String} linearGradientStr
+ *          The linear gradient CSS string.
+ * @return  {String} Returns the rgba string for the color overlay.
+ */
+function extractRgbaOverlayString(linearGradientStr) {
+  const start = linearGradientStr.indexOf("(");
+  const end = linearGradientStr.indexOf(")");
+
+  return linearGradientStr.substring(start + 1, end + 1);
+}
+
+function testColorPreviewDisplay(spectrum, expectedRgbCssString, expectedBorderColor) {
+  const { colorPreview } = spectrum;
+  const colorPreviewStyle = window.getComputedStyle(colorPreview);
+  expectedBorderColor = expectedBorderColor === "transparent" ?
+                          "rgba(0, 0, 0, 0)"
+                          :
+                          expectedBorderColor;
+
+  spectrum.updateUI();
+
+  // Extract the first rgba value from the linear gradient
+  const linearGradientStr = colorPreviewStyle.getPropertyValue("background-image");
+  const colorPreviewValue = extractRgbaOverlayString(linearGradientStr);
+
+  is(colorPreviewValue, expectedRgbCssString,
+    `Color preview should be ${expectedRgbCssString}`);
+
+  info("Test if color preview has a border or not.");
+  // Since border-color is a shorthand CSS property, using getComputedStyle will return
+  // an empty string. Instead, use one of the border sides to find the border-color value
+  // since they will all be the same.
+  const borderColorTop = colorPreviewStyle.getPropertyValue("border-top-color");
+  is(borderColorTop, expectedBorderColor, "Color preview border color is correct.");
+}
+
 function testCreateAndDestroyShouldAppendAndRemoveElements(container) {
   ok(container, "We have the root node to append spectrum to");
   is(container.childElementCount, 0, "Root node is empty");
 
   const s = new Spectrum(container, [255, 126, 255, 1]);
   s.show();
   ok(container.childElementCount > 0, "Spectrum has appended elements");
 
@@ -90,23 +131,51 @@ function testChangingColorShouldEmitEven
   });
 }
 
 function testSettingColorShoudUpdateTheUI(container) {
   const s = new Spectrum(container, [255, 255, 255, 1]);
   s.show();
   const dragHelperOriginalPos = [s.dragHelper.style.top, s.dragHelper.style.left];
   const alphaHelperOriginalPos = s.alphaSliderHelper.style.left;
+  let hueHelperOriginalPos = s.hueSliderHelper.style.left;
 
   s.rgb = [50, 240, 234, .2];
   s.updateUI();
 
   ok(s.alphaSliderHelper.style.left != alphaHelperOriginalPos, "Alpha helper has moved");
   ok(s.dragHelper.style.top !== dragHelperOriginalPos[0], "Drag helper has moved");
   ok(s.dragHelper.style.left !== dragHelperOriginalPos[1], "Drag helper has moved");
+  ok(s.hueSliderHelper.style.left !== hueHelperOriginalPos, "Hue helper has moved");
+
+  hueHelperOriginalPos = s.hueSliderHelper.style.left;
 
   s.rgb = [240, 32, 124, 0];
   s.updateUI();
   is(s.alphaSliderHelper.style.left, -(s.alphaSliderHelper.offsetWidth / 2) + "px",
     "Alpha range UI has been updated again");
+  ok(hueHelperOriginalPos !== s.hueSliderHelper.style.left,
+    "Hue Helper slider should have move again");
 
   s.destroy();
 }
+
+function testChangingColorShouldUpdateColorPreview(container) {
+  const s = new Spectrum(container, [0, 0, 1, 1]);
+  s.show();
+
+  info("Test that color preview is black.");
+  testColorPreviewDisplay(s, "rgb(0, 0, 1)", "transparent");
+
+  info("Test that color preview is blue.");
+  s.rgb = [0, 0, 255, 1];
+  testColorPreviewDisplay(s, "rgb(0, 0, 255)", "transparent");
+
+  info("Test that color preview is red.");
+  s.rgb = [255, 0, 0, 1];
+  testColorPreviewDisplay(s, "rgb(255, 0, 0)", "transparent");
+
+  info("Test that color preview is white and also has a light grey border.");
+  s.rgb = [255, 255, 255, 1];
+  testColorPreviewDisplay(s, "rgb(255, 255, 255)", "rgb(204, 204, 204)");
+
+  s.destroy();
+}
--- a/devtools/client/shared/widgets/Spectrum.js
+++ b/devtools/client/shared/widgets/Spectrum.js
@@ -1,15 +1,17 @@
 /* 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 EventEmitter = require("devtools/shared/event-emitter");
+const { colorUtils } = require("devtools/shared/css/color.js");
+
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
 
 /**
  * Spectrum creates a color picker widget in any container you give it.
  *
  * Simple usage example:
  *
  * const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
@@ -28,56 +30,76 @@ const XHTML_NS = "http://www.w3.org/1999
  * visible will allow spectrum to correctly initialize its various parts.
  *
  * Fires the following events:
  * - changed : When the user changes the current color
  */
 function Spectrum(parentEl, rgb) {
   EventEmitter.decorate(this);
 
+  this.document = parentEl.ownerDocument;
   this.element = parentEl.ownerDocument.createElementNS(XHTML_NS, "div");
   this.parentEl = parentEl;
 
   this.element.className = "spectrum-container";
   this.element.innerHTML = `
-    <div class="spectrum-top">
-      <div class="spectrum-fill"></div>
-      <div class="spectrum-top-inner">
-        <div class="spectrum-color spectrum-box">
-          <div class="spectrum-sat">
-            <div class="spectrum-val">
-              <div class="spectrum-dragger"></div>
-            </div>
+    <section class="spectrum-color-picker">
+      <div class="spectrum-color spectrum-box">
+        <div class="spectrum-sat">
+          <div class="spectrum-val">
+            <div class="spectrum-dragger"></div>
           </div>
         </div>
+      </div>
+    </section>
+    <section class="spectrum-controls">
+      <div class="spectrum-color-preview"></div>
+      <div class="spectrum-slider-container">
         <div class="spectrum-hue spectrum-box">
-          <div class="spectrum-slider spectrum-slider-control"></div>
+          <div class="spectrum-hue-inner">
+            <div class="spectrum-hue-handle spectrum-slider-control"></div>
+          </div>
         </div>
-      </div>
-    </div>
-    <div class="spectrum-alpha spectrum-checker spectrum-box">
-      <div class="spectrum-alpha-inner">
-        <div class="spectrum-alpha-handle spectrum-slider-control"></div>
-      </div>
-    </div>
+        <div class="spectrum-alpha spectrum-checker spectrum-box">
+          <div class="spectrum-alpha-inner">
+            <div class="spectrum-alpha-handle spectrum-slider-control"></div>
+          </div>
+         </div>
+        </div>
+     </section>
   `;
 
   this.onElementClick = this.onElementClick.bind(this);
   this.element.addEventListener("click", this.onElementClick);
 
   this.parentEl.appendChild(this.element);
 
-  this.slider = this.element.querySelector(".spectrum-hue");
-  this.slideHelper = this.element.querySelector(".spectrum-slider");
-  Spectrum.draggable(this.slider, this.onSliderMove.bind(this));
-
+  // Color spectrum dragger.
   this.dragger = this.element.querySelector(".spectrum-color");
   this.dragHelper = this.element.querySelector(".spectrum-dragger");
   Spectrum.draggable(this.dragger, this.onDraggerMove.bind(this));
 
+  // Here we define the components for the "controls" section of the color picker.
+  this.controls = this.element.querySelector(".spectrum-controls");
+  this.colorPreview = this.element.querySelector(".spectrum-color-preview");
+
+  // Create the eyedropper.
+  const eyedropper = this.document.createElementNS(XHTML_NS, "button");
+  eyedropper.id = "eyedropper-button";
+  eyedropper.className = "devtools-button";
+  eyedropper.style.pointerEvents = "auto";
+  this.controls.insertBefore(eyedropper, this.colorPreview);
+
+  // Hue slider
+  this.hueSlider = this.element.querySelector(".spectrum-hue");
+  this.hueSliderInner = this.element.querySelector(".spectrum-hue-inner");
+  this.hueSliderHelper = this.element.querySelector(".spectrum-hue-handle");
+  Spectrum.draggable(this.hueSliderInner, this.onHueSliderMove.bind(this));
+
+  // Alpha slider
   this.alphaSlider = this.element.querySelector(".spectrum-alpha");
   this.alphaSliderInner = this.element.querySelector(".spectrum-alpha-inner");
   this.alphaSliderHelper = this.element.querySelector(".spectrum-alpha-handle");
   Spectrum.draggable(this.alphaSliderInner, this.onAlphaSliderMove.bind(this));
 
   if (rgb) {
     this.rgb = rgb;
     this.updateUI();
@@ -223,35 +245,35 @@ Spectrum.prototype = {
 
   get rgbCssString() {
     const rgb = this.rgb;
     return "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " +
       rgb[3] + ")";
   },
 
   show: function() {
-    this.element.classList.add("spectrum-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;
 
+    this.hueSliderWidth = this.hueSliderInner.offsetWidth;
+    this.hueSliderHelperWidth = this.hueSliderHelper.offsetWidth;
+
     this.updateUI();
   },
 
   onElementClick: function(e) {
     e.stopPropagation();
   },
 
-  onSliderMove: function(dragX, dragY) {
-    this.hsv[0] = (dragY / this.slideHeight);
+  onHueSliderMove: function(dragX, dragY) {
+    this.hsv[0] = (dragX / this.hueSliderWidth);
     this.updateUI();
     this.onChange();
   },
 
   onDraggerMove: function(dragX, dragY) {
     this.hsv[1] = dragX / this.dragWidth;
     this.hsv[2] = (this.dragHeight - dragY) / this.dragHeight;
     this.updateUI();
@@ -263,23 +285,44 @@ Spectrum.prototype = {
     this.updateUI();
     this.onChange();
   },
 
   onChange: function() {
     this.emit("changed", this.rgb, this.rgbCssString);
   },
 
+  updateAlphaSliderBackground: function() {
+    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 + ")";
+    this.alphaSliderInner.style.background = alphaGradient;
+  },
+
+  updateColorPreview: function() {
+    // Overlay the rgba color over a checkered image background.
+    this.colorPreview.style.setProperty("--overlay-color", this.rgbCssString);
+
+    // We should be able to distinguish the color preview on high luminance rgba values.
+    // Give the color preview a light grey border if the luminance of the current rgba
+    // tuple is great.
+    const colorLuminance = colorUtils.calculateLuminance(this.rgb);
+    this.colorPreview.classList.toggle("high-luminance", colorLuminance > 0.85);
+  },
+
+  updateDraggerBackgroundColor: function() {
+    const flatColor = "rgb(" + this.rgbNoSatVal[0] + ", " + this.rgbNoSatVal[1] + ", " +
+     this.rgbNoSatVal[2] + ")";
+    this.dragger.style.backgroundColor = flatColor;
+  },
+
   updateHelperLocations: function() {
-    // If the UI hasn't been shown yet then none of the dimensions will be
-    // correct
-    if (!this.element.classList.contains("spectrum-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;
@@ -292,47 +335,39 @@ Spectrum.prototype = {
       -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";
+    const hueSliderX = (h * this.hueSliderWidth) - (this.hueSliderHelperWidth / 2);
+    this.hueSliderHelper.style.left = hueSliderX + "px";
 
     // Placing the alpha slider
     const alphaSliderX = (this.hsv[3] * this.alphaSliderWidth) -
       (this.alphaSliderHelperWidth / 2);
     this.alphaSliderHelper.style.left = alphaSliderX + "px";
   },
 
   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;
+    this.updateColorPreview();
+    this.updateDraggerBackgroundColor();
+    this.updateAlphaSliderBackground();
   },
 
   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.colorPreview = null;
+    this.dragger = null;
+    this.element = null;
     this.parentEl = null;
-    this.element = null;
+    this.slider = null;
   },
 };
--- a/devtools/client/shared/widgets/spectrum.css
+++ b/devtools/client/shared/widgets/spectrum.css
@@ -1,14 +1,14 @@
 /* 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;
+  margin-inline-end: 5px;
   display: block;
 }
 
 #eyedropper-button::before {
   background-image: url(chrome://devtools/skin/images/command-eyedropper.svg);
 }
 
 /* Mix-in classes */
@@ -20,126 +20,139 @@
   background-size: 12px 12px;
   background-position: 0 0, 6px 6px;
 }
 
 .spectrum-slider-control {
   cursor: pointer;
   box-shadow: 0 0 2px rgba(0,0,0,.6);
   background: #fff;
-  border-radius: 10px;
-  opacity: .8;
+  border-radius: 50%;
+  opacity: .9;
 }
 
 .spectrum-box {
   border: 1px solid rgba(0,0,0,0.2);
   border-radius: 2px;
   background-clip: content-box;
 }
 
 /* Elements */
 
 #spectrum-tooltip {
-  padding: 4px;
+  padding: 5px;
+}
+
+/**
+ * Spectrum controls set the layout for the controls section of the color picker.
+ */
+.spectrum-controls {
+  display: flex;
+  justify-content: space-between;
+  margin-top: 10px;
+  margin-right: 5px;
+  width: 200px;
 }
 
 .spectrum-container {
-  position: relative;
-  display: none;
-  top: 0;
-  left: 0;
-  border-radius: 0;
-  width: 200px;
-  padding: 5px;
+  display: flex;
+  flex-direction: column;
+  margin: -1px;
 }
 
-.spectrum-show {
-  display: inline-block;
+/**
+ * This styles the color preview and adds a checkered background overlay inside of it. The overlay
+ * can be manipulated using the --overlay-color variable.
+ */
+.spectrum-color-preview {
+  --overlay-color: transparent;
+  border: 1px solid transparent;
+  border-radius: 50%;
+  width: 27px;
+  height: 27px;
+  background-color: #fff;
+  background-image: linear-gradient(var(--overlay-color), var(--overlay-color)),
+    linear-gradient(45deg,#ccc 25%,transparent 25%, transparent 75%, #ccc 75%),
+    linear-gradient(45deg,#ccc 25%, transparent 25%,transparent 75%,#ccc 75%);
+  background-size: 12px 12px;
+  background-position: 0 0, 6px 6px;
+}
+
+.spectrum-color-preview.high-luminance {
+  border-color: #ccc;
+}
+
+.spectrum-slider-container {
+  display: flex;
+  flex-direction: column;
+  justify-content: space-around;
+  width: 130px;
+  margin-left: 10px;
+  height: 30px;
 }
 
 /* Keep aspect ratio:
 http://www.briangrinstead.com/blog/keep-aspect-ratio-with-html-and-css */
-.spectrum-top {
+.spectrum-color-picker {
   position: relative;
-  width: 100%;
-  display: inline-block;
-}
-
-.spectrum-top-inner {
-  position: absolute;
-  top:0;
-  left:0;
-  bottom:0;
-  right:0;
+  width: 205px;
+  height: 120px;
 }
 
 .spectrum-color {
   position: absolute;
   top: 0;
   left: 0;
   bottom: 0;
-  right: 20%;
-}
-
-.spectrum-hue {
-  position: absolute;
-  top: 0;
-  right: 0;
-  bottom: 0;
-  left: 83%;
-}
-
-.spectrum-fill {
-  /* Same as spectrum-color width */
-  margin-top: 85%;
+  width: 100%;
 }
 
 .spectrum-sat, .spectrum-val {
   position: absolute;
   top: 0;
   left: 0;
   right: 0;
   bottom: 0;
 }
 
-.spectrum-dragger, .spectrum-slider {
-  -moz-user-select: none;
-}
-
-.spectrum-alpha {
+.spectrum-alpha,
+.spectrum-hue {
   position: relative;
   height: 8px;
   margin-top: 3px;
 }
 
-.spectrum-alpha-inner {
+.spectrum-alpha-inner,
+.spectrum-hue-inner {
   height: 100%;
 }
 
-.spectrum-alpha-handle {
+.spectrum-alpha-handle,
+.spectrum-hue-handle {
   position: absolute;
-  top: -3px;
-  bottom: -3px;
-  width: 5px;
-  left: 50%;
+  top: -2px;
+  bottom: -2px;
+  height: 12px;
+  width: 12px;
 }
 
 .spectrum-sat {
   background-image: linear-gradient(to right, #FFF, rgba(204, 154, 129, 0));
 }
 
 .spectrum-val {
   background-image: linear-gradient(to top, #000000, rgba(204, 154, 129, 0));
 }
 
 .spectrum-hue {
-  background: linear-gradient(to bottom, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
+  background: linear-gradient(to right, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
 }
 
 .spectrum-dragger {
+  -moz-user-select: none;
   position: absolute;
   top: 0px;
   left: 0px;
   cursor: pointer;
   border-radius: 50%;
   height: 8px;
   width: 8px;
   border: 1px solid white;
--- a/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
@@ -57,29 +57,20 @@ class SwatchColorPickerTooltip extends S
     container.id = "spectrum-tooltip";
 
     const node = doc.createElementNS(XHTML_NS, "div");
     node.id = "spectrum";
     container.appendChild(node);
 
     const widget = new Spectrum(node, color);
     this.tooltip.panel.appendChild(container);
-    this.tooltip.setContentSize({ width: 218, height: 224 });
+    this.tooltip.setContentSize({ width: 215, height: 175 });
 
     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.
-     */
-    eyedropper.style.pointerEvents = "auto";
-    container.appendChild(eyedropper);
-
     // Wait for the tooltip to be shown before calling widget.show
     // as it expect to be visible in order to compute DOM element sizes.
     this.tooltip.once("shown", () => {
       widget.show();
     });
 
     return widget;
   }