Bug 1102464 - Implement CSS variable tooltip. r=pbro draft
authorRajdeep Nanua <rajdeep.nanua@mail.utoronto.ca>
Wed, 08 Nov 2017 00:22:24 -0500
changeset 705271 dd7eaf675c08f9c0b7b6df404db2f868cefcf855
parent 687940 d58424c244c38f88357a26fb61c333d3c6e552d7
child 742324 75a678b111c8a94b0a1f89f35b593fe7dc25fd81
push id91430
push userbmo:rajdeep.nanua@mail.utoronto.ca
push dateWed, 29 Nov 2017 20:05:49 +0000
reviewerspbro
bugs1102464
milestone58.0a1
Bug 1102464 - Implement CSS variable tooltip. r=pbro Initial support for CSS variable tooltip. Removed title attribute from variables and added a new tooltip displaying the same content. MozReview-Commit-ID: FeHmgiS7KQj
devtools/client/inspector/rules/rules.js
devtools/client/inspector/rules/test/browser_rules_variables_01.js
devtools/client/inspector/rules/test/browser_rules_variables_02.js
devtools/client/inspector/rules/views/text-property-editor.js
devtools/client/inspector/shared/node-types.js
devtools/client/inspector/shared/tooltips-overlay.js
devtools/client/shared/output-parser.js
devtools/client/shared/widgets/tooltip/VariableTooltipHelper.js
devtools/client/shared/widgets/tooltip/moz.build
devtools/client/themes/rules.css
devtools/client/themes/tooltips.css
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -20,16 +20,17 @@ const ClassListPreviewer = require("devt
 const {getCssProperties} = require("devtools/shared/fronts/css-properties");
 const {
   VIEW_NODE_SELECTOR_TYPE,
   VIEW_NODE_PROPERTY_TYPE,
   VIEW_NODE_VALUE_TYPE,
   VIEW_NODE_IMAGE_URL_TYPE,
   VIEW_NODE_LOCATION_TYPE,
   VIEW_NODE_SHAPE_POINT_TYPE,
+  VIEW_NODE_VARIABLE_TYPE,
 } = require("devtools/client/inspector/shared/node-types");
 const StyleInspectorMenu = require("devtools/client/inspector/shared/style-inspector-menu");
 const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay");
 const {createChild, promiseWarn} = require("devtools/client/inspector/shared/utils");
 const {debounce} = require("devtools/shared/debounce");
 const EventEmitter = require("devtools/shared/old-event-emitter");
 const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
 const clipboardHelper = require("devtools/shared/platform/clipboard");
@@ -333,16 +334,29 @@ CssRuleView.prototype = {
         enabled: prop.enabled,
         overridden: prop.overridden,
         pseudoElement: prop.rule.pseudoElement,
         sheetHref: prop.rule.domRule.href,
         textProperty: prop,
         toggleActive: getShapeToggleActive(node),
         point: getShapePoint(node)
       };
+    } else if ((classes.contains("ruleview-variable") ||
+                classes.contains("ruleview-unmatched-variable")) && prop) {
+      type = VIEW_NODE_VARIABLE_TYPE;
+      value = {
+        property: getPropertyNameAndValue(node).name,
+        value: node.textContent,
+        enabled: prop.enabled,
+        overridden: prop.overridden,
+        pseudoElement: prop.rule.pseudoElement,
+        sheetHref: prop.rule.domRule.href,
+        textProperty: prop,
+        variable: node.dataset.variable
+      };
     } else if (classes.contains("theme-link") &&
                !classes.contains("ruleview-rule-source") && prop) {
       type = VIEW_NODE_IMAGE_URL_TYPE;
       value = {
         property: getPropertyNameAndValue(node).name,
         value: node.parentNode.textContent,
         url: node.href,
         enabled: prop.enabled,
--- a/devtools/client/inspector/rules/test/browser_rules_variables_01.js
+++ b/devtools/client/inspector/rules/test/browser_rules_variables_01.js
@@ -12,24 +12,30 @@ add_task(function* () {
   yield addTab(TEST_URI);
   let {inspector, view} = yield openRuleView();
   yield selectNode("#target", inspector);
 
   info("Tests basic support for CSS Variables for both single variable " +
   "and double variable. Formats tested: var(x, constant), var(x, var(y))");
 
   let unsetColor = getRuleViewProperty(view, "div", "color").valueSpan
-    .querySelector(".ruleview-variable-unmatched");
+    .querySelector(".ruleview-unmatched-variable");
   let setColor = unsetColor.previousElementSibling;
   is(unsetColor.textContent, " red", "red is unmatched in color");
   is(setColor.textContent, "--color", "--color is not set correctly");
-  is(setColor.title, "--color = chartreuse", "--color's title is not set correctly");
+  is(setColor.dataset.variable, "--color = chartreuse",
+                                "--color's dataset.variable is not set correctly");
+  let previewTooltip = yield assertShowPreviewTooltip(view, setColor);
+  yield assertTooltipHiddenOnMouseOut(previewTooltip, setColor);
 
   let unsetVar = getRuleViewProperty(view, "div", "background-color").valueSpan
-    .querySelector(".ruleview-variable-unmatched");
+    .querySelector(".ruleview-unmatched-variable");
   let setVar = unsetVar.nextElementSibling;
   let setVarName = setVar.firstElementChild.firstElementChild;
   is(unsetVar.textContent, "--not-set",
      "--not-set is unmatched in background-color");
   is(setVar.textContent, " var(--bg)", "var(--bg) parsed incorrectly");
   is(setVarName.textContent, "--bg", "--bg is not set correctly");
-  is(setVarName.title, "--bg = seagreen", "--bg's title is not set correctly");
+  is(setVarName.dataset.variable, "--bg = seagreen",
+                                  "--bg's dataset.variable is not set correctly");
+  previewTooltip = yield assertShowPreviewTooltip(view, setVarName);
+  yield assertTooltipHiddenOnMouseOut(previewTooltip, setVarName);
 });
--- a/devtools/client/inspector/rules/test/browser_rules_variables_02.js
+++ b/devtools/client/inspector/rules/test/browser_rules_variables_02.js
@@ -21,60 +21,60 @@ add_task(function* () {
 });
 
 function* testBasic(inspector, view) {
   info("Test support for basic variable functionality for var() with 2 variables." +
        "Format: var(--var1, var(--var2))");
 
   yield selectNode("#a", inspector);
   let unsetVar = getRuleViewProperty(view, "#a", "font-size").valueSpan
-    .querySelector(".ruleview-variable-unmatched");
+    .querySelector(".ruleview-unmatched-variable");
   let setVarParent = unsetVar.nextElementSibling;
   let setVar = getVarFromParent(setVarParent);
   is(unsetVar.textContent, "--var-not-defined",
     "--var-not-defined is not set correctly");
-  is(unsetVar.title, "--var-not-defined is not set",
-    "--var-not-defined's title is not set correctly");
+  is(unsetVar.dataset.variable, "--var-not-defined is not set",
+    "--var-not-defined's dataset.variable is not set correctly");
   is(setVarParent.textContent, " var(--var-defined-font-size)",
     "var(--var-defined-font-size) parsed incorrectly");
   is(setVar.textContent, "--var-defined-font-size",
     "--var-defined-font-size is not set correctly");
-  is(setVar.title, "--var-defined-font-size = 60px",
-    "--bg's title is not set correctly");
+  is(setVar.dataset.variable, "--var-defined-font-size = 60px",
+    "--bg's dataset.variable is not set correctly");
 }
 
 function* testNestedCssFunctions(inspector, view) {
   info("Test support for variable functionality for a var() nested inside " +
   "another CSS function. Format: rgb(0, 0, var(--var1, var(--var2)))");
 
   yield selectNode("#b", inspector);
   let unsetVarParent = getRuleViewProperty(view, "#b", "color").valueSpan
-    .querySelector(".ruleview-variable-unmatched");
+    .querySelector(".ruleview-unmatched-variable");
   let unsetVar = getVarFromParent(unsetVarParent);
   let setVar = unsetVarParent.previousElementSibling;
   is(unsetVarParent.textContent, " var(--var-defined-r-2)",
     "var(--var-defined-r-2) not parsed correctly");
   is(unsetVar.textContent, "--var-defined-r-2",
     "--var-defined-r-2 is not set correctly");
-  is(unsetVar.title, "--var-defined-r-2 = 0",
-    "--var-defined-r-2's title is not set correctly");
+  is(unsetVar.dataset.variable, "--var-defined-r-2 = 0",
+    "--var-defined-r-2's dataset.variable is not set correctly");
   is(setVar.textContent, "--var-defined-r-1",
     "--var-defined-r-1 is not set correctly");
-  is(setVar.title, "--var-defined-r-1 = 255",
-    "--var-defined-r-1's title is not set correctly");
+  is(setVar.dataset.variable, "--var-defined-r-1 = 255",
+    "--var-defined-r-1's dataset.variable is not set correctly");
 }
 
 function* testBorderShorthandAndInheritance(inspector, view) {
   info("Test support for variable functionality for shorthands/CSS styles with spaces " +
   "like \"margin: w x y z\". Also tests functionality for inherticance of CSS" +
   " variables. Format: var(l, var(m)) var(x) rgb(var(r) var(g) var(b))");
 
   yield selectNode("#c", inspector);
   let unsetVarL = getRuleViewProperty(view, "#c", "border").valueSpan
-    .querySelector(".ruleview-variable-unmatched");
+    .querySelector(".ruleview-unmatched-variable");
   let setVarMParent = unsetVarL.nextElementSibling;
 
   // var(x) is the next sibling of the parent of M
   let setVarXParent = setVarMParent.parentNode.nextElementSibling;
 
   // var(r) is the next sibling of var(x), and var(g) is the next sibling of var(r), etc.
   let setVarRParent = setVarXParent.nextElementSibling;
   let setVarGParent = setVarRParent.nextElementSibling;
@@ -83,108 +83,108 @@ function* testBorderShorthandAndInherita
   let setVarM = getVarFromParent(setVarMParent);
   let setVarX = setVarXParent.firstElementChild;
   let setVarR = setVarRParent.firstElementChild;
   let setVarG = setVarGParent.firstElementChild;
   let setVarB = setVarBParent.firstElementChild;
 
   is(unsetVarL.textContent, "--var-undefined",
     "--var-undefined is not set correctly");
-  is(unsetVarL.title, "--var-undefined is not set",
-    "--var-undefined's title is not set correctly");
+  is(unsetVarL.dataset.variable, "--var-undefined is not set",
+    "--var-undefined's dataset.variable is not set correctly");
 
   is(setVarM.textContent, "--var-border-px",
     "--var-border-px is not set correctly");
-  is(setVarM.title, "--var-border-px = 10px",
-    "--var-border-px's title is not set correctly");
+  is(setVarM.dataset.variable, "--var-border-px = 10px",
+    "--var-border-px's dataset.variable is not set correctly");
 
   is(setVarX.textContent, "--var-border-style",
     "--var-border-style is not set correctly");
-  is(setVarX.title, "--var-border-style = solid",
-    "var-border-style's title is not set correctly");
+  is(setVarX.dataset.variable, "--var-border-style = solid",
+    "var-border-style's dataset.variable is not set correctly");
 
   is(setVarR.textContent, "--var-border-r",
     "--var-defined-r is not set correctly");
-  is(setVarR.title, "--var-border-r = 255",
-    "--var-defined-r's title is not set correctly");
+  is(setVarR.dataset.variable, "--var-border-r = 255",
+    "--var-defined-r's dataset.variable is not set correctly");
 
   is(setVarG.textContent, "--var-border-g",
     "--var-defined-g is not set correctly");
-  is(setVarG.title, "--var-border-g = 0",
-    "--var-defined-g's title is not set correctly");
+  is(setVarG.dataset.variable, "--var-border-g = 0",
+    "--var-defined-g's dataset.variable is not set correctly");
 
   is(setVarB.textContent, "--var-border-b",
     "--var-defined-b is not set correctly");
-  is(setVarB.title, "--var-border-b = 0",
-    "--var-defined-b's title is not set correctly");
+  is(setVarB.dataset.variable, "--var-border-b = 0",
+    "--var-defined-b's dataset.variable is not set correctly");
 }
 
 function* testSingleLevelVariable(inspector, view) {
   info("Test support for variable functionality of a single level of " +
   "undefined variables. Format: var(x, constant)");
 
   yield selectNode("#d", inspector);
   let unsetVar = getRuleViewProperty(view, "#d", "font-size").valueSpan
-    .querySelector(".ruleview-variable-unmatched");
+    .querySelector(".ruleview-unmatched-variable");
 
   is(unsetVar.textContent, "--var-undefined",
     "--var-undefined is not set correctly");
-  is(unsetVar.title, "--var-undefined is not set",
-    "--var-undefined's title is not set correctly");
+  is(unsetVar.dataset.variable, "--var-undefined is not set",
+    "--var-undefined's dataset.variable is not set correctly");
 }
 
 function* testDoubleLevelVariable(inspector, view) {
   info("Test support for variable functionality of double level of " +
   "undefined variables. Format: var(x, var(y, constant))");
 
   yield selectNode("#e", inspector);
   let allUnsetVars = getRuleViewProperty(view, "#e", "color").valueSpan
-    .querySelectorAll(".ruleview-variable-unmatched");
+    .querySelectorAll(".ruleview-unmatched-variable");
 
   is(allUnsetVars.length, 2, "The number of unset variables is mismatched.");
 
   let unsetVar1 = allUnsetVars[0];
   let unsetVar2 = allUnsetVars[1];
 
   is(unsetVar1.textContent, "--var-undefined",
     "--var-undefined is not set correctly");
-  is(unsetVar1.title, "--var-undefined is not set",
-    "--var-undefined's title is not set correctly");
+  is(unsetVar1.dataset.variable, "--var-undefined is not set",
+    "--var-undefined's dataset.variable is not set correctly");
 
   is(unsetVar2.textContent, "--var-undefined-2",
     "--var-undefined is not set correctly");
-  is(unsetVar2.title, "--var-undefined-2 is not set",
-    "--var-undefined-2's title is not set correctly");
+  is(unsetVar2.dataset.variable, "--var-undefined-2 is not set",
+    "--var-undefined-2's dataset.variable is not set correctly");
 }
 
 function* testTripleLevelVariable(inspector, view) {
   info("Test support for variable functionality of triple level of " +
   "undefined variables. Format: var(x, var(y, var(z, constant)))");
 
   yield selectNode("#f", inspector);
   let allUnsetVars = getRuleViewProperty(view, "#f", "border-style").valueSpan
-    .querySelectorAll(".ruleview-variable-unmatched");
+    .querySelectorAll(".ruleview-unmatched-variable");
 
   is(allUnsetVars.length, 3, "The number of unset variables is mismatched.");
 
   let unsetVar1 = allUnsetVars[0];
   let unsetVar2 = allUnsetVars[1];
   let unsetVar3 = allUnsetVars[2];
 
   is(unsetVar1.textContent, "--var-undefined",
     "--var-undefined is not set correctly");
-  is(unsetVar1.title, "--var-undefined is not set",
-    "--var-undefined's title is not set correctly");
+  is(unsetVar1.dataset.variable, "--var-undefined is not set",
+    "--var-undefined's dataset.variable is not set correctly");
 
   is(unsetVar2.textContent, "--var-undefined-2",
     "--var-undefined-2 is not set correctly");
-  is(unsetVar2.title, "--var-undefined-2 is not set",
-    "--var-defined-r-2's title is not set correctly");
+  is(unsetVar2.dataset.variable, "--var-undefined-2 is not set",
+    "--var-defined-r-2's dataset.variable is not set correctly");
 
   is(unsetVar3.textContent, "--var-undefined-3",
     "--var-undefined-3 is not set correctly");
-  is(unsetVar3.title, "--var-undefined-3 is not set",
-    "--var-defined-r-3's title is not set correctly");
+  is(unsetVar3.dataset.variable, "--var-undefined-3 is not set",
+    "--var-defined-r-3's dataset.variable is not set correctly");
 }
 
 function getVarFromParent(varParent) {
   return varParent.firstElementChild.firstElementChild;
 }
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -360,17 +360,18 @@ TextPropertyEditor.prototype = {
       colorSwatchClass: SHARED_SWATCH_CLASS + " " + COLOR_SWATCH_CLASS,
       filterClass: "ruleview-filter",
       filterSwatchClass: SHARED_SWATCH_CLASS + " " + FILTER_SWATCH_CLASS,
       gridClass: "ruleview-grid",
       shapeClass: "ruleview-shape",
       defaultColorType: !propDirty,
       urlClass: "theme-link",
       baseURI: this.sheetHref,
-      unmatchedVariableClass: "ruleview-variable-unmatched",
+      unmatchedVariableClass: "ruleview-unmatched-variable",
+      matchedVariableClass: "ruleview-variable",
       isVariableInUse: varName => this.rule.elementStyle.getVariable(varName),
     };
     let frag = outputParser.parseCssProperty(name, val, parserOptions);
     this.valueSpan.innerHTML = "";
     this.valueSpan.appendChild(frag);
 
     this.ruleView.emit("property-value-updated", this.valueSpan);
 
--- a/devtools/client/inspector/shared/node-types.js
+++ b/devtools/client/inspector/shared/node-types.js
@@ -11,8 +11,9 @@
  */
 
 exports.VIEW_NODE_SELECTOR_TYPE = 1;
 exports.VIEW_NODE_PROPERTY_TYPE = 2;
 exports.VIEW_NODE_VALUE_TYPE = 3;
 exports.VIEW_NODE_IMAGE_URL_TYPE = 4;
 exports.VIEW_NODE_LOCATION_TYPE = 5;
 exports.VIEW_NODE_SHAPE_POINT_TYPE = 6;
+exports.VIEW_NODE_VARIABLE_TYPE = 7;
--- a/devtools/client/inspector/shared/tooltips-overlay.js
+++ b/devtools/client/inspector/shared/tooltips-overlay.js
@@ -11,35 +11,39 @@
  * editor tooltips that appear when clicking swatch based editors.
  */
 
 const { Task } = require("devtools/shared/task");
 const Services = require("Services");
 const {
   VIEW_NODE_VALUE_TYPE,
   VIEW_NODE_IMAGE_URL_TYPE,
+  VIEW_NODE_VARIABLE_TYPE,
 } = require("devtools/client/inspector/shared/node-types");
 const { getColor } = require("devtools/client/shared/theme");
 const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
 
 loader.lazyRequireGetter(this, "getCssProperties",
   "devtools/shared/fronts/css-properties", true);
 
 loader.lazyRequireGetter(this, "getImageDimensions",
   "devtools/client/shared/widgets/tooltip/ImageTooltipHelper", true);
 loader.lazyRequireGetter(this, "setImageTooltip",
   "devtools/client/shared/widgets/tooltip/ImageTooltipHelper", true);
 loader.lazyRequireGetter(this, "setBrokenImageTooltip",
   "devtools/client/shared/widgets/tooltip/ImageTooltipHelper", true);
+loader.lazyRequireGetter(this, "setVariableTooltip",
+  "devtools/client/shared/widgets/tooltip/VariableTooltipHelper", true);
 
 const PREF_IMAGE_TOOLTIP_SIZE = "devtools.inspector.imagePreviewTooltipSize";
 
 // Types of existing tooltips
 const TOOLTIP_IMAGE_TYPE = "image";
 const TOOLTIP_FONTFAMILY_TYPE = "font-family";
+const TOOLTIP_VARIABLE_TYPE = "variable";
 
 /**
  * Manages all tooltips in the style-inspector.
  *
  * @param {CssRuleView|CssComputedView} view
  *        Either the rule-view or computed-view panel
  */
 function TooltipsOverlay(view) {
@@ -169,16 +173,21 @@ TooltipsOverlay.prototype = {
     // Font preview tooltip
     if (type === VIEW_NODE_VALUE_TYPE && prop.property === "font-family") {
       let value = prop.value.toLowerCase();
       if (value !== "inherit" && value !== "unset" && value !== "initial") {
         tooltipType = TOOLTIP_FONTFAMILY_TYPE;
       }
     }
 
+    // Variable preview tooltip
+    if (type === VIEW_NODE_VARIABLE_TYPE) {
+      tooltipType = TOOLTIP_VARIABLE_TYPE;
+    }
+
     return tooltipType;
   },
 
   /**
    * Executed by the tooltip when the pointer hovers over an element of the
    * view. Used to decide whether the tooltip should be shown or not and to
    * actually put content in it.
    * Checks if the hovered target is a css value we support tooltips for.
@@ -220,16 +229,22 @@ TooltipsOverlay.prototype = {
 
     if (type === TOOLTIP_FONTFAMILY_TYPE) {
       let font = nodeInfo.value.value;
       let nodeFront = inspector.selection.nodeFront;
       yield this._setFontPreviewTooltip(font, nodeFront);
       return true;
     }
 
+    if (type === TOOLTIP_VARIABLE_TYPE && nodeInfo.value.value.startsWith("--")) {
+      let variable = nodeInfo.value.variable;
+      yield this._setVariablePreviewTooltip(variable);
+      return true;
+    }
+
     return false;
   }),
 
   /**
    * Set the content of the preview tooltip to display an image preview. The image URL can
    * be relative, a call will be made to the debuggee to retrieve the image content as an
    * imageData URI.
    *
@@ -285,16 +300,28 @@ TooltipsOverlay.prototype = {
     let doc = this.view.inspector.panelDoc;
     let {naturalWidth, naturalHeight} = yield getImageDimensions(doc, imageUrl);
 
     yield setImageTooltip(this.getTooltip("previewTooltip"), doc, imageUrl,
       {hideDimensionLabel: true, hideCheckeredBackground: true,
        maxDim, naturalWidth, naturalHeight});
   }),
 
+  /**
+   * Set the content of the preview tooltip to display a variable preview.
+   *
+   * @param {String} text
+   *        The text to display for the variable tooltip
+   * @return {Promise} A promise that resolves when the preview tooltip content is ready
+   */
+  _setVariablePreviewTooltip: Task.async(function* (text) {
+    let doc = this.view.inspector.panelDoc;
+    yield setVariableTooltip(this.getTooltip("previewTooltip"), doc, text);
+  }),
+
   _onNewSelection: function () {
     for (let [, tooltip] of this._instances) {
       tooltip.hide();
     }
   },
 
   /**
    * Destroy this overlay instance, removing it from the view
--- a/devtools/client/shared/output-parser.js
+++ b/devtools/client/shared/output-parser.js
@@ -222,24 +222,25 @@ OutputParser.prototype = {
     }
 
     // Get the variable name.
     let varName = text.substring(tokens[0].startOffset, tokens[0].endOffset);
 
     if (typeof varValue === "string") {
       // The variable value is valid, set the variable name's title of the first argument
       // in var() to display the variable name and value.
-      firstOpts.title =
+      firstOpts["data-variable"] =
         STYLE_INSPECTOR_L10N.getFormatStr("rule.variableValue", varName, varValue);
+      firstOpts.class = options.matchedVariableClass;
       secondOpts.class = options.unmatchedVariableClass;
     } else {
       // The variable name is not valid, mark it unmatched.
       firstOpts.class = options.unmatchedVariableClass;
-      firstOpts.title = STYLE_INSPECTOR_L10N.getFormatStr("rule.variableUnset",
-                                                          varName);
+      firstOpts["data-variable"] = STYLE_INSPECTOR_L10N.getFormatStr("rule.variableUnset",
+                                                                      varName);
     }
 
     variableNode.appendChild(this._createNode("span", firstOpts, result));
 
     // If we saw a ",", then append it and show the remainder using
     // the correct highlighting.
     if (sawComma) {
       variableNode.appendChild(this.doc.createTextNode(","));
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/VariableTooltipHelper.js
@@ -0,0 +1,33 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+const PADDING = 2;
+
+/**
+ * Set the tooltip content of a provided HTMLTooltip instance to display a
+ * variable preview matching the provided text.
+ *
+ * @param {HTMLTooltip} tooltip
+ *        The tooltip instance on which the text preview content should be set
+ * @param {Document} doc
+ *        A document element to create the HTML elements needed for the tooltip
+ * @param {String} text
+ *        Text to display in tooltip
+ */
+function setVariableTooltip(tooltip, doc, text) {
+  // Create tooltip content
+  let div = doc.createElementNS(XHTML_NS, "div");
+  div.classList.add("devtools-monospace", "devtools-tooltip-css-variable");
+  div.textContent = text;
+
+  tooltip.setContent(div);
+}
+
+module.exports.setVariableTooltip = setVariableTooltip;
--- a/devtools/client/shared/widgets/tooltip/moz.build
+++ b/devtools/client/shared/widgets/tooltip/moz.build
@@ -11,9 +11,10 @@ DevToolsModules(
     'InlineTooltip.js',
     'SwatchBasedEditorTooltip.js',
     'SwatchColorPickerTooltip.js',
     'SwatchCubicBezierTooltip.js',
     'SwatchFilterTooltip.js',
     'Tooltip.js',
     'TooltipToggle.js',
     'VariableContentHelper.js',
+    'VariableTooltipHelper.js'
 )
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -567,17 +567,17 @@
 
 .ruleview-selectorcontainer {
   word-wrap: break-word;
   cursor: text;
 }
 
 .ruleview-selector-separator,
 .ruleview-selector-unmatched,
-.ruleview-variable-unmatched {
+.ruleview-unmatched-variable {
   color: #888;
 }
 
 .ruleview-selector-matched > .ruleview-selector-attribute {
   /* TODO: Bug 1178535 Awaiting UX feedback on highlight colors */
 }
 
 .ruleview-selector-matched > .ruleview-selector-pseudo-class {
--- a/devtools/client/themes/tooltips.css
+++ b/devtools/client/themes/tooltips.css
@@ -56,16 +56,23 @@
 .devtools-tooltip[clamped-dimensions-no-min-height] .panel-arrowcontent,
 .devtools-tooltip[clamped-dimensions-no-max-or-min-height] .panel-arrowcontent {
   overflow: hidden;
 }
 .devtools-tooltip[wide] {
   max-width: 600px;
 }
 
+/* Tooltip: CSS variables tooltip */
+
+.devtools-tooltip-css-variable {
+  color: var(--theme-body-color);
+  padding: 2px;
+}
+
 /* Tooltip: Simple Text */
 
 .devtools-tooltip-simple-text {
   max-width: 400px;
   margin: 0 -4px; /* Compensate for the .panel-arrowcontent padding. */
   padding: 8px 12px;
   white-space: pre-wrap;
 }