Bug 1306054 - Use custom tooltip for inactive properties r=jdescottes,flod,rcaliman
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Fri, 10 May 2019 17:03:27 +0000
changeset 532252 362df4629f8f1fd5ed0eece429b77499a77bb955
parent 532251 85d5010b19abc7a58ce617aff2a39360fc54eee5
child 532265 2abcefb31ba7b2a1c573edc5695b772826c6a078
child 532266 192ba11153b4c29512a9722caf8260292601e6aa
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdescottes, flod, rcaliman
bugs1306054
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 1306054 - Use custom tooltip for inactive properties r=jdescottes,flod,rcaliman ### Changes Probably the most important change apart from the tooltips is that we now only support one property at a time. This allows us to short circuit at the first invalid property and improve performance. This was previously agreed with Razvan but there were some relics left in the code. `toolbox.xul` - Added tooltips.ftl `devtools/client/inspector/markup/test/helper_events_test_runner.js`: - Had to change to synthesizeMouseAtCenter because CSS changes caused the original to fail. `devtools/client/inspector/rules/rules.js`: - Added `VIEW_NODE_INACTIVE_CSS` to node types and sorted alphabetically. - Added new nodeInfo data for Inactive CSS icons. `devtools/client/inspector/rules/test/browser_rules_inactive_css_flexbox.js` & `devtools/client/inspector/rules/test/browser_rules_inactive_css_grid.js`: - removed some listeners that are no longer needed `devtools/client/inspector/rules/test/head.js`: - Refactored `getPropertiesForRuleIndex()` in order to pass along information needed for testing our Fluent strings. - Refactored `checkDeclarationIsInactive()` to check tooltip contnts using a new method. - Added `checkInteractiveTooltip()` for checking the tooltip contents themselves. - Simple changes to `runInactiveCSSTests()`. `devtools/client/inspector/rules/views/text-property-editor.js`: - We no longer create the tooltip by adding the title attribute. `devtools/client/inspector/shared/node-types.js`: - Changed the enum to use strings to simplify debugging. - Added `VIEW_NODE_INACTIVE_CSS`. - Sorted alphabetically. `devtools/client/inspector/shared/tooltips-overlay.js`: - Introduced a new tooltip type called `interactiveTooltip`. `devtools/client/locales/en-US/inspector.properties`: - Removed strings. `devtools/client/locales/en-US/tooltips.ftl`: - Added structured versions of the properties from `inspector.properties`. `devtools/client/shared/widgets/tooltip/HTMLTooltip.js`: - Made the tooltips obey the "prevent popup autohide" option in the browser debugger. `devtools/client/shared/widgets/tooltip/InactiveCSSTooltipHelper.js`: - Main file for handling InactiveCSS Tooltips. `devtools/client/themes/tooltips.css`: - Made arrow tooltips follow the Proton theme. `devtools/server/actors/utils/inactive-property-helper.js`: - General changes to support Fluent. - Bail on first inactive property found. ### Latest Try (expecting green) https://treeherder.mozilla.org/#/jobs?repo=try&revision=de28939206d444dc4b534a3e5cc7a84b8797bec3 Differential Revision: https://phabricator.services.mozilla.com/D29372
devtools/client/framework/test/browser_menu_api.js
devtools/client/framework/test/head.js
devtools/client/framework/toolbox.xul
devtools/client/inspector/rules/rules.js
devtools/client/inspector/rules/test/browser_rules_inactive_css_flexbox.js
devtools/client/inspector/rules/test/browser_rules_inactive_css_grid.js
devtools/client/inspector/rules/test/head.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/locales/en-US/inspector.properties
devtools/client/locales/en-US/tooltips.ftl
devtools/client/shared/widgets/tooltip/HTMLTooltip.js
devtools/client/shared/widgets/tooltip/inactive-css-tooltip-helper.js
devtools/client/shared/widgets/tooltip/moz.build
devtools/client/themes/tooltips.css
devtools/server/actors/utils/inactive-property-helper.js
--- a/devtools/client/framework/test/browser_menu_api.js
+++ b/devtools/client/framework/test/browser_menu_api.js
@@ -16,16 +16,18 @@ add_task(async function() {
   const tab = await addTab(URL);
   const target = await TargetFactory.forTab(tab);
   const toolbox = await gDevTools.showToolbox(target, "webconsole");
 
   // This test will involve localized strings, make sure the necessary FTL file is
   // available in the toolbox top window.
   toolbox.topWindow.MozXULElement.insertFTLIfNeeded("toolkit/main-window/editmenu.ftl");
 
+  loadFTL(toolbox, "toolkit/main-window/editmenu.ftl");
+
   await testMenuItems();
   await testMenuPopup(toolbox);
   await testSubmenu(toolbox);
 });
 
 function testMenuItems() {
   const menu = new Menu();
   const menuItem1 = new MenuItem();
--- a/devtools/client/framework/test/head.js
+++ b/devtools/client/framework/test/head.js
@@ -422,8 +422,24 @@ function setupPreferencesForBrowserToolb
     ["devtools.browser-toolbox.allow-unsafe-script", true],
     // On debug test runner, it takes more than the default time (20s)
     // to get a initialized console
     ["devtools.debugger.remote-timeout", 120000],
   ]};
 
   return SpecialPowers.pushPrefEnv(options);
 }
+
+/**
+ * Load FTL.
+ *
+ * @param {Toolbox} toolbox
+ *        Toolbox instance.
+ * @param {String} path
+ *        Path to the FTL file.
+ */
+function loadFTL(toolbox, path) {
+  const win = toolbox.doc.ownerGlobal;
+
+  if (win.MozXULElement) {
+    win.MozXULElement.insertFTLIfNeeded(path);
+  }
+}
--- a/devtools/client/framework/toolbox.xul
+++ b/devtools/client/framework/toolbox.xul
@@ -11,16 +11,19 @@
 <!ENTITY % toolboxDTD SYSTEM "chrome://devtools/locale/toolbox.dtd" >
 %toolboxDTD;
 <!ENTITY % globalKeysDTD SYSTEM "chrome://global/locale/globalKeys.dtd">
 %globalKeysDTD;
 ]>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:html="http://www.w3.org/1999/xhtml">
+  <linkset>
+    <html:link rel="localization" href="devtools/tooltips.ftl"/>
+  </linkset>
 
   <html:link href="chrome://browser/skin/window.svg" rel="shortcut icon"/>
   <script src="chrome://devtools/content/shared/theme-switching.js"/>
   <script src="chrome://global/content/viewSourceUtils.js"/>
 
   <script src="chrome://global/content/globalOverlay.js"/>
   <script src="chrome://devtools/content/framework/toolbox-init.js"/>
 
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -11,25 +11,26 @@ const Services = require("Services");
 const flags = require("devtools/shared/flags");
 const {l10n} = require("devtools/shared/inspector/css-logic");
 const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
 const OutputParser = require("devtools/client/shared/output-parser");
 const {PrefObserver} = require("devtools/client/shared/prefs");
 const ElementStyle = require("devtools/client/inspector/rules/models/element-style");
 const RuleEditor = require("devtools/client/inspector/rules/views/rule-editor");
 const {
-  VIEW_NODE_SELECTOR_TYPE,
+  VIEW_NODE_FONT_TYPE,
+  VIEW_NODE_IMAGE_URL_TYPE,
+  VIEW_NODE_INACTIVE_CSS,
+  VIEW_NODE_LOCATION_TYPE,
   VIEW_NODE_PROPERTY_TYPE,
-  VIEW_NODE_VALUE_TYPE,
-  VIEW_NODE_IMAGE_URL_TYPE,
-  VIEW_NODE_LOCATION_TYPE,
+  VIEW_NODE_SELECTOR_TYPE,
   VIEW_NODE_SHAPE_POINT_TYPE,
   VIEW_NODE_SHAPE_SWATCH,
+  VIEW_NODE_VALUE_TYPE,
   VIEW_NODE_VARIABLE_TYPE,
-  VIEW_NODE_FONT_TYPE,
 } = require("devtools/client/inspector/shared/node-types");
 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/event-emitter");
 
 loader.lazyRequireGetter(this, "flashElementOn", "devtools/client/inspector/markup/utils", true);
 loader.lazyRequireGetter(this, "flashElementOff", "devtools/client/inspector/markup/utils", true);
@@ -420,16 +421,19 @@ 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-unused-warning") && prop) {
+      type = VIEW_NODE_INACTIVE_CSS;
+      value = prop.isUsed();
     } else if (classes.contains("ruleview-shapeswatch") && prop) {
       type = VIEW_NODE_SHAPE_SWATCH;
       value = {
         enabled: prop.enabled,
         overridden: prop.overridden,
         textProperty: prop,
       };
     } else if ((classes.contains("ruleview-variable") ||
--- a/devtools/client/inspector/rules/test/browser_rules_inactive_css_flexbox.js
+++ b/devtools/client/inspector/rules/test/browser_rules_inactive_css_flexbox.js
@@ -39,17 +39,16 @@ const TEST_URI = `
     <div id="self-aligned"></div>
 </body>`;
 
 const BEFORE = [
   {
     selector: "#self-aligned",
     inactiveDeclarations: [
       {
-        l10n: "rule.inactive.css.not.grid.or.flex.item",
         declaration: {
           "align-self": "stretch",
         },
         ruleIndex: 1,
       },
     ],
   },
   {
@@ -67,17 +66,16 @@ const BEFORE = [
           "flex-grow": "1",
           "flex-shrink": "1",
         },
         ruleIndex: 1,
       },
     ],
     inactiveDeclarations: [
       {
-        l10n: "rule.inactive.css.not.flex.container",
         declaration: {
           "flex-direction": "row",
         },
         ruleIndex: 1,
       },
     ],
   },
   {
@@ -96,61 +94,54 @@ const BEFORE = [
           border: "1px solid #000",
           "align-content": "space-between",
         },
         ruleIndex: 1,
       },
     ],
     inactiveDeclarations: [
       {
-        l10n: "rule.inactive.css.not.flex.item",
         declaration: {
           "order": "1",
         },
         ruleIndex: 1,
       },
     ],
-    waitFor: "inspector-updated",
   },
 ];
 
 const AFTER = [
   {
     selector: ".item-2",
     inactiveDeclarations: [
       {
-        l10n: "rule.inactive.css.not.flex.item",
         declaration: {
           "order": "2",
         },
         ruleIndex: 0,
       },
       {
-        l10n: "rule.inactive.css.not.flex.item",
         declaration: {
           "flex-basis": "auto",
         },
         ruleIndex: 1,
       },
       {
-        l10n: "rule.inactive.css.not.flex.item",
         declaration: {
           "flex-grow": "1",
         },
         ruleIndex: 1,
       },
       {
-        l10n: "rule.inactive.css.not.flex.item",
         declaration: {
           "flex-shrink": "1",
         },
         ruleIndex: 1,
       },
       {
-        l10n: "rule.inactive.css.not.flex.container",
         declaration: {
           "flex-direction": "row",
         },
         ruleIndex: 1,
       },
     ],
   },
 ];
@@ -160,11 +151,10 @@ add_task(async function() {
   const {inspector, view} = await openRuleView();
 
   await runInactiveCSSTests(view, inspector, BEFORE);
 
   // Toggle `display:flex` to disabled.
   await toggleDeclaration(inspector, view, 0, {
     display: "flex",
   });
-  await view.once("ruleview-refreshed");
   await runInactiveCSSTests(view, inspector, AFTER);
 });
--- a/devtools/client/inspector/rules/test/browser_rules_inactive_css_grid.js
+++ b/devtools/client/inspector/rules/test/browser_rules_inactive_css_grid.js
@@ -42,17 +42,16 @@ const TEST_URI = `
     <div id="self-aligned"></div>
 </body>`;
 
 const BEFORE = [
   {
     selector: "#self-aligned",
     inactiveDeclarations: [
       {
-        l10n: "rule.inactive.css.not.grid.or.flex.item",
         declaration: {
           "align-self": "stretch",
         },
         ruleIndex: 1,
       },
     ],
   },
   {
@@ -65,17 +64,16 @@ const BEFORE = [
           "grid-row-start": "1",
           "grid-row-end": "auto",
         },
         ruleIndex: 1,
       },
     ],
     inactiveDeclarations: [
       {
-        l10n: "rule.inactive.css.not.flex.container",
         declaration: {
           "flex-direction": "row",
         },
         ruleIndex: 1,
       },
     ],
   },
   {
@@ -95,24 +93,22 @@ const BEFORE = [
           "column-gap": "10px",
           "row-gap": "10px",
         },
         ruleIndex: 1,
       },
     ],
     inactiveDeclarations: [
       {
-        l10n: "rule.inactive.css.not.grid.or.flex.item",
         declaration: {
           "align-self": "start",
         },
         ruleIndex: 1,
       },
     ],
-    waitFor: "inspector-updated",
   },
 ];
 
 const AFTER = [
   {
     activeDeclarations: [
       {
         declarations: {
@@ -126,31 +122,28 @@ const AFTER = [
           height: "100px",
           border: "1px solid #000",
         },
         ruleIndex: 1,
       },
     ],
     inactiveDeclarations: [
       {
-        l10n: "rule.inactive.css.not.grid.container",
         declaration: {
           "column-gap": "10px",
         },
         ruleIndex: 1,
       },
       {
-        l10n: "rule.inactive.css.not.grid.container",
         declaration: {
           "row-gap": "10px",
         },
         ruleIndex: 1,
       },
       {
-        l10n: "rule.inactive.css.not.grid.or.flex.item",
         declaration: {
           "align-self": "start",
         },
         ruleIndex: 1,
       },
     ],
   },
 ];
--- a/devtools/client/inspector/rules/test/head.js
+++ b/devtools/client/inspector/rules/test/head.js
@@ -617,35 +617,38 @@ async function openEyedropper(view, swat
  *
  * @returns A map containing stringified property declarations e.g.
  *          [
  *            {
  *              "color:red":
  *                {
  *                  propertyName: "color",
  *                  propertyValue: "red",
- *                  warnings: "This won't work",
+ *                  warning: "This won't work",
  *                  used: true,
  *                }
  *            },
  *            ...
  *          ]
  */
 function getPropertiesForRuleIndex(view, ruleIndex) {
   const declaration = new Map();
   const ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
 
   for (const currProp of ruleEditor.rule.textProps) {
     const icon = currProp.editor.unusedState;
+    const unused = currProp.editor.element.classList.contains("unused");
 
     declaration.set(`${currProp.name}:${currProp.value}`, {
       propertyName: currProp.name,
       propertyValue: currProp.value,
-      warnings: icon.title ? icon.title.split("\n") : [],
-      used: !currProp.editor.element.classList.contains("unused"),
+      icon: icon,
+      data: currProp.isUsed(),
+      warning: unused,
+      used: !unused,
     });
   }
 
   return declaration;
 }
 
 /**
  * Toggle a declaration disabled or enabled.
@@ -674,62 +677,109 @@ async function toggleDeclaration(inspect
 
   const dec = `${name}:${value}`;
   ok(textProp, `Declaration "${dec}" found`);
 
   const newStatus = textProp.enabled ? "disabled" : "enabled";
   info(`Toggling declaration "${dec}" of rule ${ruleIndex} to ${newStatus}`);
 
   await togglePropStatus(view, textProp);
+  info("Toggled successfully.");
 }
 
 /**
  * Check that a declaration is marked inactive and that it has the expected
  * warning.
  *
  * @param {ruleView} view
  *        The rule-view instance.
  * @param {Number} ruleIndex
  *        The index we expect the rule to have in the rule-view.
  * @param {Object} declaration
  *        An object representing the declaration e.g. { color: "red" }.
- * @param {String} warningL10nString
- *        l10n string representing an expected warning.
  */
-function checkDeclarationIsInactive(view, ruleIndex, declaration, warningL10nString) {
+async function checkDeclarationIsInactive(view, ruleIndex, declaration) {
   const declarations = getPropertiesForRuleIndex(view, ruleIndex);
   const [[ name, value ]] = Object.entries(declaration);
   const dec = `${name}:${value}`;
-  const { used, warnings } = declarations.get(dec);
+  const { used, warning } = declarations.get(dec);
 
   ok(!used, `"${dec}" is inactive`);
-  is(warnings.length, 1, `"${dec}" has a warning`);
+  ok(warning, `"${dec}" has a warning`);
 
-  const warning = INSPECTOR_L10N.getFormatStr(warningL10nString, name);
-  is(warnings[0], warning, `The warning on "${dec}" is correct`);
+  await checkInteractiveTooltip(view, ruleIndex, declaration);
 }
 
 /**
  * Check that a declaration is marked active.
  *
  * @param {ruleView} view
  *        The rule-view instance.
  * @param {Number} ruleIndex
  *        The index we expect the rule to have in the rule-view.
  * @param {Object} declaration
  *        An object representing the declaration e.g. { color: "red" }.
  */
 function checkDeclarationIsActive(view, ruleIndex, declaration) {
   const declarations = getPropertiesForRuleIndex(view, ruleIndex);
   const [[ name, value ]] = Object.entries(declaration);
   const dec = `${name}:${value}`;
-  const { used, warnings } = declarations.get(dec);
+  const { used, warning } = declarations.get(dec);
 
   ok(used, `${dec} is active`);
-  is(warnings.length, 0, `${dec} has no warnings`);
+  ok(!warning, `${dec} has no warning`);
+}
+
+/**
+ * Check that a tooltip contains the correct value.
+ *
+ * @param {ruleView} view
+ *        The rule-view instance.
+ * @param {Number} ruleIndex
+ *        The index we expect the rule to have in the rule-view.
+ * @param {Object} declaration
+ *        An object representing the declaration e.g. { color: "red" }.
+ */
+async function checkInteractiveTooltip(view, ruleIndex, declaration) {
+  // Get the declaration
+  const declarations = getPropertiesForRuleIndex(view, ruleIndex);
+  const [[ name, value ]] = Object.entries(declaration);
+  const dec = `${name}:${value}`;
+  const { icon, data } = declarations.get(dec);
+
+  // Get the tooltip.
+  const tooltip = view.tooltips.getTooltip("interactiveTooltip");
+
+  // Get the HTML template.
+  const inactiveCssTooltipHelper = view.tooltips.inactiveCssTooltipHelper;
+  const template = inactiveCssTooltipHelper.getTemplate(data, tooltip);
+
+  // Translate the template using Fluent.
+  const { doc } = tooltip;
+  await doc.l10n.translateFragment(template);
+
+  // Get the expected HTML content of the now translated template.
+  const expected = template.firstElementChild.outerHTML;
+
+  // Show the tooltip for the correct icon.
+  const onTooltipReady = tooltip.once("shown");
+  await view.tooltips.onInteractiveTooltipTargetHover(icon);
+  tooltip.show(icon);
+  await onTooltipReady;
+
+  // Get the tooltip's actual HTML content.
+  const actual = tooltip.panel.firstElementChild.outerHTML;
+
+  // Hide the tooltip.
+  const onTooltipHidden = tooltip.once("hidden");
+  tooltip.hide();
+  await onTooltipHidden;
+
+  // Finally, check the values.
+  is(actual, expected, "Tooltip contains the correct value.");
 }
 
 /**
  * Inactive CSS test runner.
  *
  * @param {ruleView} view
  *        The rule-view instance.
  * @param {InspectorPanel} inspector
@@ -752,58 +802,47 @@ function checkDeclarationIsActive(view, 
  *                    "flex-grow": "1",
  *                    "flex-shrink": "1",
  *                  },
  *                  ruleIndex: 1,
  *                },
  *              ],
  *              inactiveDeclarations: [
  *                {
- *                  l10n: "rule.inactive.css.not.flex.container",
  *                  declaration: {
  *                    "flex-direction": "row",
  *                  },
  *                  ruleIndex: 1,
  *                },
  *              ],
- *              waitFor: "markupmutation",
  *            },
  *            ...
  *          ]
  */
 async function runInactiveCSSTests(view, inspector, tests) {
   for (const test of tests) {
-    let event = null;
-
-    if (test.waitFor) {
-      event = inspector.once(test.waitFor);
-    }
-
     if (test.selector) {
       await selectNode(test.selector, inspector);
     }
 
-    if (test.waitFor) {
-      await event;
-    }
+    if (test.activeDeclarations) {
+      info("Checking whether declarations are marked as used.");
 
-    if (test.activeDeclarations) {
-      // Check whether declarations are marked as used.
       for (const activeDeclarations of test.activeDeclarations) {
         for (const [name, value] of Object.entries(activeDeclarations.declarations)) {
           checkDeclarationIsActive(view, activeDeclarations.ruleIndex, {
             [name]: value,
           });
         }
       }
     }
 
     if (test.inactiveDeclarations) {
+      info("Checking that declarations are unused and have a warning.");
+
       for (const inactiveDeclaration of test.inactiveDeclarations) {
-        // Check that declaration is unused and has a warning.
-        checkDeclarationIsInactive(view,
-                                   inactiveDeclaration.ruleIndex,
-                                   inactiveDeclaration.declaration,
-                                   inactiveDeclaration.l10n);
+        await checkDeclarationIsInactive(view,
+                                         inactiveDeclaration.ruleIndex,
+                                         inactiveDeclaration.declaration);
       }
     }
   }
 }
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -629,24 +629,23 @@ TextPropertyEditor.prototype = {
     if (!this.editing &&
         (this.prop.overridden || !this.prop.enabled ||
          !this.prop.isKnownProperty)) {
       this.element.classList.add("ruleview-overridden");
     } else {
       this.element.classList.remove("ruleview-overridden");
     }
 
-    const { used, reasons } = this.prop.isUsed();
+    const { used } = this.prop.isUsed();
 
     if (this.editing || this.prop.overridden || !this.prop.enabled || used) {
       this.element.classList.remove("unused");
       this.unusedState.hidden = true;
     } else {
       this.element.classList.add("unused");
-      this.unusedState.title = reasons.join("\n");
       this.unusedState.hidden = false;
     }
   },
 
   /**
    * Update the indicator for computed styles. The computed styles themselves
    * are populated on demand, when they become visible.
    */
--- a/devtools/client/inspector/shared/node-types.js
+++ b/devtools/client/inspector/shared/node-types.js
@@ -5,17 +5,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 /**
  * Types of nodes used in the rule and computed view.
  */
 
-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;
-exports.VIEW_NODE_FONT_TYPE = 8;
-exports.VIEW_NODE_SHAPE_SWATCH = 9;
+exports.VIEW_NODE_FONT_TYPE = "font-type";
+exports.VIEW_NODE_IMAGE_URL_TYPE = "image-url-type";
+exports.VIEW_NODE_INACTIVE_CSS = "inactive-css";
+exports.VIEW_NODE_LOCATION_TYPE = "location-type";
+exports.VIEW_NODE_PROPERTY_TYPE = "property-type";
+exports.VIEW_NODE_SELECTOR_TYPE = "selector-type";
+exports.VIEW_NODE_SHAPE_POINT_TYPE = "shape-point-type";
+exports.VIEW_NODE_SHAPE_SWATCH = "shape-swatch";
+exports.VIEW_NODE_VALUE_TYPE = "value-type";
+exports.VIEW_NODE_VARIABLE_TYPE = "variable-type";
--- a/devtools/client/inspector/shared/tooltips-overlay.js
+++ b/devtools/client/inspector/shared/tooltips-overlay.js
@@ -12,37 +12,41 @@
  */
 
 const Services = require("Services");
 const flags = require("devtools/shared/flags");
 const {
   VIEW_NODE_VALUE_TYPE,
   VIEW_NODE_FONT_TYPE,
   VIEW_NODE_IMAGE_URL_TYPE,
+  VIEW_NODE_INACTIVE_CSS,
   VIEW_NODE_VARIABLE_TYPE,
 } = require("devtools/client/inspector/shared/node-types");
 
 loader.lazyRequireGetter(this, "getColor",
   "devtools/client/shared/theme", true);
 loader.lazyRequireGetter(this, "HTMLTooltip",
   "devtools/client/shared/widgets/tooltip/HTMLTooltip", 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);
+loader.lazyRequireGetter(this, "InactiveCssTooltipHelper",
+  "devtools/client/shared/widgets/tooltip/inactive-css-tooltip-helper", false);
 
 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_INACTIVE_CSS = "inactive-css";
 const TOOLTIP_VARIABLE_TYPE = "variable";
 
 /**
  * Manages all tooltips in the style-inspector.
  *
  * @param {CssRuleView|CssComputedView} view
  *        Either the rule-view or computed-view panel
  */
@@ -76,26 +80,31 @@ TooltipsOverlay.prototype = {
    */
   addToView: function() {
     if (this._isStarted || this._isDestroyed) {
       return;
     }
 
     this._isStarted = true;
 
-    // Instantiate the preview tooltip when the rule/computed view is hovered over in
-    // order to call tooltip.starTogglingOnHover. This will allow the preview tooltip
-    // to be shown when an appropriate element is hovered over.
-    if (flags.testing) {
-      this.getTooltip("previewTooltip");
-    } else {
-      // Lazily get the preview tooltip to avoid loading HTMLTooltip.
-      this.view.element.addEventListener("mousemove", () => {
-        this.getTooltip("previewTooltip");
-      }, { once: true });
+    this.inactiveCssTooltipHelper = new InactiveCssTooltipHelper();
+
+    // Instantiate the interactiveTooltip and preview tooltip when the
+    // rule/computed view is hovered over in order to call
+    // `tooltip.startTogglingOnHover`. This will allow the tooltip to be shown
+    // when an appropriate element is hovered over.
+    for (const type of ["interactiveTooltip", "previewTooltip"]) {
+      if (flags.testing) {
+        this.getTooltip(type);
+      } else {
+        // Lazily get the preview tooltip to avoid loading HTMLTooltip.
+        this.view.element.addEventListener("mousemove", () => {
+          this.getTooltip(type);
+        }, { once: true });
+      }
     }
   },
 
   /**
    * Lazily fetch and initialize the different tooltips that are used in the inspector.
    * These tooltips are attached to the toolbox document if they require a popup panel.
    * Otherwise, it is attached to the inspector panel document if it is an inline editor.
    *
@@ -121,16 +130,26 @@ TooltipsOverlay.prototype = {
         tooltip = new SwatchCubicBezierTooltip(doc);
         break;
       case "filterEditor":
         const SwatchFilterTooltip =
           require("devtools/client/shared/widgets/tooltip/SwatchFilterTooltip");
         tooltip = new SwatchFilterTooltip(doc,
           this._cssProperties.getValidityChecker(this.view.inspector.panelDoc));
         break;
+      case "interactiveTooltip":
+        tooltip = new HTMLTooltip(doc, {
+          type: "doorhanger",
+          useXulWrapper: true,
+        });
+        tooltip.startTogglingOnHover(this.view.element,
+          this.onInteractiveTooltipTargetHover.bind(this), {
+            interactive: true,
+          });
+        break;
       case "previewTooltip":
         tooltip = new HTMLTooltip(doc, {
           type: "arrow",
           useXulWrapper: true,
         });
         tooltip.startTogglingOnHover(this.view.element,
           this._onPreviewTooltipTargetHover.bind(this));
         break;
@@ -149,16 +168,18 @@ TooltipsOverlay.prototype = {
     if (!this._isStarted || this._isDestroyed) {
       return;
     }
 
     for (const [, tooltip] of this._instances) {
       tooltip.destroy();
     }
 
+    this.inactiveCssTooltipHelper.destroy();
+
     this._isStarted = false;
   },
 
   /**
    * Given a hovered node info, find out which type of tooltip should be shown,
    * if any
    *
    * @param {Object} nodeInfo
@@ -176,16 +197,21 @@ TooltipsOverlay.prototype = {
     if ((type === VIEW_NODE_VALUE_TYPE && prop.property === "font-family") ||
         (type === VIEW_NODE_FONT_TYPE)) {
       const value = prop.value.toLowerCase();
       if (value !== "inherit" && value !== "unset" && value !== "initial") {
         tooltipType = TOOLTIP_FONTFAMILY_TYPE;
       }
     }
 
+    // Inactive CSS tooltip
+    if (type === VIEW_NODE_INACTIVE_CSS) {
+      tooltipType = TOOLTIP_INACTIVE_CSS;
+    }
+
     // Variable preview tooltip
     if (type === VIEW_NODE_VARIABLE_TYPE) {
       tooltipType = TOOLTIP_VARIABLE_TYPE;
     }
 
     return tooltipType;
   },
 
@@ -248,16 +274,64 @@ TooltipsOverlay.prototype = {
       await this._setVariablePreviewTooltip(variable);
       return true;
     }
 
     return false;
   },
 
   /**
+   * 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.
+   *
+   * @param  {DOMNode} target
+   *         The currently hovered node
+   * @return {Boolean}
+   *         true if shown, false otherwise.
+   */
+  async onInteractiveTooltipTargetHover(target) {
+    const nodeInfo = this.view.getNodeInfo(target);
+    if (!nodeInfo) {
+      // The hovered node isn't something we care about.
+      return false;
+    }
+
+    const type = this._getTooltipType(nodeInfo);
+    if (!type) {
+      // There is no tooltip type defined for the hovered node.
+      return false;
+    }
+
+    // Remove previous tooltip instances.
+    for (const [, tooltip] of this._instances) {
+      if (tooltip.isVisible()) {
+        if (tooltip.revert) {
+          tooltip.revert();
+        }
+        tooltip.hide();
+      }
+    }
+
+    if (type === TOOLTIP_INACTIVE_CSS) {
+    // Ensure this is the correct node and not a parent.
+      if (!target.classList.contains("ruleview-unused-warning")) {
+        return false;
+      }
+
+      await this.inactiveCssTooltipHelper.setContent(
+        nodeInfo.value, this.getTooltip("interactiveTooltip"));
+      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.
    *
    * @param {String} imageUrl
    *        The image url value (may be relative or absolute).
    * @return {Promise} A promise that resolves when the preview tooltip content is ready
    */
--- a/devtools/client/locales/en-US/inspector.properties
+++ b/devtools/client/locales/en-US/inspector.properties
@@ -483,38 +483,8 @@ markupView.scrollableBadge.label=scroll
 
 # LOCALIZATION NOTE (markupView.scrollableBadge.tooltip): This is the tooltip that is displayed
 # when hovering over badges next to scrollable elements in the inspector.
 markupView.scrollableBadge.tooltip=This element has scrollable overflow.
 
 # LOCALIZATION NOTE (rulePreviewTooltip.noAssociatedRule): This is the text displayed inside
 # the RulePreviewTooltip when a rule cannot be found for a CSS property declaration.
 rulePreviewTooltip.noAssociatedRule=No associated rule
-
-# LOCALIZATION NOTE (rule.inactive.css.not.flex.container): These properties
-# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
-# property is not active. %S will be replaced with a property name.
-rule.inactive.css.not.flex.container=“%S” has no effect on this element since it’s not a flex container (try adding “display:flex” or “display:inline-flex”)
-
-# LOCALIZATION NOTE (rule.inactive.css.not.flex.item): These properties
-# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
-# property is not active. %S will be replaced with a property name.
-rule.inactive.css.not.flex.item=“%S” has no effect on this element since it’s not a flex item (try adding “display:flex” or “display:inline-flex” to the item’s parent)
-
-# LOCALIZATION NOTE (rule.inactive.css.not.grid.container): These properties
-# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
-# property is not active. %S will be replaced with a property name.
-rule.inactive.css.not.grid.container=“%S” has no effect on this element since it’s not a grid container (try adding “display:grid” or “display:inline-grid”)
-
-# LOCALIZATION NOTE (rule.inactive.css.not.grid.item): These properties
-# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
-# property is not active. %S will be replaced with a property name.
-rule.inactive.css.not.grid.item=“%S” has no effect on this element since it’s not a grid item (try adding “display:grid” or “display:inline-grid” to the item’s parent)
-
-# LOCALIZATION NOTE (rule.inactive.css.not.grid.or.flex.item): These properties
-# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
-# property is not active. %S will be replaced with a property name.
-rule.inactive.css.not.grid.or.flex.item=“%S” has no effect on this element since it’s not a grid or flex item (try adding “display:grid”, “display:flex”, “display:inline-grid” or “display:inline-flex” to the item’s parent)
-
-# LOCALIZATION NOTE (rule.inactive.css.not.grid.or.flex.container): These properties
-# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
-# property is not active. %S will be replaced with a property name.
-rule.inactive.css.not.grid.or.flex.container=“%S” has no effect on this element since it’s neither a flex container nor a grid container (try adding “display:grid” or “display:flex”)
new file mode 100644
--- /dev/null
+++ b/devtools/client/locales/en-US/tooltips.ftl
@@ -0,0 +1,41 @@
+# 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/.
+
+### Localization for Developer Tools tooltips.
+
+learn-more = <span data-l10n-name="link">Learn more</span>
+
+## In the Rule View when a CSS property cannot be successfully applied we display
+## an icon. When this icon is hovered this message is displayed to explain why
+## the property is not applied.
+## Variables:
+##   $property (string) - A CSS property name e.g. "color".
+
+inactive-css-not-grid-or-flex-container = <strong>{ $property }</strong> has no effect on this element since it’s neither a flex container nor a grid container.
+
+inactive-css-not-grid-or-flex-item = <strong>{ $property }</strong> has no effect on this element since it’s not a grid or flex item.
+
+inactive-css-not-grid-item = <strong>{ $property }</strong> has no effect on this element since it’s not a grid item.
+
+inactive-css-not-grid-container = <strong>{ $property }</strong> has no effect on this element since it’s not a grid container.
+
+inactive-css-not-flex-item = <strong>{ $property }</strong> has no effect on this element since it’s not a flex item.
+
+inactive-css-not-flex-container = <strong>{ $property }</strong> has no effect on this element since it’s not a flex container.
+
+## In the Rule View when a CSS property cannot be successfully applied we display
+## an icon. When this icon is hovered this message is displayed to explain how
+## the problem can be solved.
+
+inactive-css-not-grid-or-flex-container-fix = Try adding <strong>display:grid</strong> or <strong>display:flex</strong>. { learn-more }
+
+inactive-css-not-grid-or-flex-item-fix = Try adding <strong>display:grid</strong>, <strong>display:flex</strong>, <strong>display:inline-grid</strong> or <strong>display:inline-flex</strong>. { learn-more }
+
+inactive-css-not-grid-item-fix =Try adding <strong>display:grid</strong> or <strong>display:inline-grid</strong> to the item’s parent. { learn-more }
+
+inactive-css-not-grid-container-fix = Try adding <strong>display:grid</strong> or <strong>display:inline-grid</strong>. { learn-more }
+
+inactive-css-not-flex-item-fix = Try adding <strong>display:flex</strong> or <strong>display:inline-flex</strong> to the item’s parent. { learn-more }
+
+inactive-css-not-flex-container-fix = Try adding <strong>display:flex</strong> or <strong>display:inline-flex</strong>. { learn-more }
--- a/devtools/client/shared/widgets/tooltip/HTMLTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/HTMLTooltip.js
@@ -716,16 +716,21 @@ HTMLTooltip.prototype = {
     return { width, height };
   },
 
   /**
    * Hide the current tooltip. The event "hidden" will be fired when the tooltip
    * is hidden.
    */
   async hide({ fromMouseup = false } = {}) {
+    // Exit if the disable autohide setting is in effect.
+    if (Services.prefs.getBoolPref("ui.popup.disable_autohide", false)) {
+      return;
+    }
+
     this.doc.defaultView.clearTimeout(this.attachEventsTimer);
     if (!this.isVisible()) {
       this.emit("hidden");
       return;
     }
 
     // If the tooltip is hidden from a mouseup event, wait for a potential click event
     // to be consumed before removing event listeners.
@@ -821,21 +826,16 @@ HTMLTooltip.prototype = {
    * If the element that received the mousedown and the mouseup are different, click
    * will not be fired.
    */
   _onMouseup: function(e) {
     if (this._isInTooltipContainer(e.target)) {
       return;
     }
 
-    // If the disable autohide setting is in effect, ignore.
-    if (Services.prefs.getBoolPref("ui.popup.disable_autohide", false)) {
-      return;
-    }
-
     this.hide({ fromMouseup: true });
   },
 
   _isInTooltipContainer: function(node) {
     // Check if the target is the tooltip arrow.
     if (this.arrow && this.arrow === node) {
       return true;
     }
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/inactive-css-tooltip-helper.js
@@ -0,0 +1,128 @@
+/* -*- 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";
+
+loader.lazyRequireGetter(this, "openDocLink", "devtools/client/shared/link", true);
+
+class InactiveCssTooltipHelper {
+  constructor() {
+    this.addTab = this.addTab.bind(this);
+  }
+
+  /**
+   * Fill the tooltip with inactive CSS information.
+   *
+   * @param {String} propertyName
+   *        The property name to be displayed in bold.
+   * @param {String} text
+   *        The main text, which follows property name.
+   */
+  async setContent(data, tooltip) {
+    const fragment = this.getTemplate(data, tooltip);
+    const { doc } = tooltip;
+
+    tooltip.panel.innerHTML = "";
+
+    tooltip.panel.addEventListener("click", this.addTab);
+    tooltip.once("hidden", () => {
+      tooltip.panel.removeEventListener("click", this.addTab);
+    });
+
+    // Because Fluent is async we need to manually translate the fragment and
+    // then insert it into the tooltip. This is needed in order for the tooltip
+    // to size to the contents properly and for tests.
+    await doc.l10n.translateFragment(fragment);
+    doc.l10n.pauseObserving();
+    tooltip.panel.appendChild(fragment);
+    doc.l10n.resumeObserving();
+
+    // Size the content.
+    tooltip.setContentSize({width: 275, height: Infinity});
+  }
+/**
+ * Get the template that the Fluent string will be merged with. This template
+ * looks something like this but there is a variable amount of properties in the
+ * fix section:
+ *
+ * <div class="devtools-tooltip-inactive-css">
+ *   <p data-l10n-id="inactive-css-not-grid-or-flex-container"
+ *      data-l10n-args="{&quot;property&quot;:&quot;align-content&quot;}">
+ *     <strong></strong>
+ *   </p>
+ *   <p data-l10n-id="inactive-css-not-grid-or-flex-container-fix">
+ *     <strong></strong>
+ *     <strong></strong>
+ *     <span data-l10n-name="link" class="link"></span>
+ *   </p>
+ * </div>
+ *
+ * @param {Object} data
+ *        An object in the following format: {
+ *          fixId: "inactive-css-not-grid-item-fix", // Fluent id containing the
+ *                                                   // Inactive CSS fix.
+ *          msgId: "inactive-css-not-grid-item", // Fluent id containing the
+ *                                               // Inactive CSS message.
+ *          numFixProps: 2, // Number of properties in the fix section of the
+ *                          // tooltip.
+ *          property: "color", // Property name
+ *        }
+ * @param {HTMLTooltip} tooltip
+ *        The tooltip we are targetting.
+ */
+  getTemplate(data, tooltip) {
+    const XHTML_NS = "http://www.w3.org/1999/xhtml";
+    const { fixId, msgId, numFixProps, property } = data;
+    const { doc } = tooltip;
+
+    this._currentTooltip = tooltip;
+    this._currentUrl = `https://developer.mozilla.org/docs/Web/CSS/${property}`;
+
+    const templateNode = doc.createElementNS(XHTML_NS, "template");
+
+    // eslint-disable-next-line
+    templateNode.innerHTML = `
+    <div class="devtools-tooltip-inactive-css">
+      <p data-l10n-id="${msgId}"
+         data-l10n-args='${JSON.stringify({property})}'>
+        <strong></strong>
+      </p>
+      <p data-l10n-id="${fixId}">
+        ${"<strong></strong>".repeat(numFixProps)}
+        <span data-l10n-name="link" class="link"></span>
+      </p>
+    </div>`;
+
+    return doc.importNode(templateNode.content, true);
+  }
+
+  /**
+   * Hide the tooltip, open `this._currentUrl` in a new tab and focus it.
+   *
+   * @param {DOMEvent} event
+   *        The click event originating from the tooltip.
+   *
+   */
+  addTab(event) {
+    // The XUL panel swallows click events so handlers can't be added directly
+    // to the link span. As a workaround we listen to all click events in the
+    // panel and if a link span is clicked we proceed.
+    if (event.target.className !== "link") {
+      return;
+    }
+
+    const tooltip = this._currentTooltip;
+    tooltip.hide();
+    openDocLink(this._currentUrl);
+  }
+
+  destroy() {
+    this._currentTooltip = null;
+    this._currentUrl = null;
+  }
+}
+
+module.exports = InactiveCssTooltipHelper;
--- a/devtools/client/shared/widgets/tooltip/moz.build
+++ b/devtools/client/shared/widgets/tooltip/moz.build
@@ -3,16 +3,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/.
 
 DevToolsModules(
     'EventTooltipHelper.js',
     'HTMLTooltip.js',
     'ImageTooltipHelper.js',
+    'inactive-css-tooltip-helper.js',
     'InlineTooltip.js',
     'RulePreviewTooltip.js',
     'SwatchBasedEditorTooltip.js',
     'SwatchColorPickerTooltip.js',
     'SwatchCubicBezierTooltip.js',
     'SwatchFilterTooltip.js',
     'TooltipToggle.js',
     'VariableTooltipHelper.js'
--- a/devtools/client/themes/tooltips.css
+++ b/devtools/client/themes/tooltips.css
@@ -62,23 +62,45 @@
 :root[platform="mac"] {
   --theme-arrowpanel-border-radius: 3.5px;
 }
 
 :root[platform="mac"].theme-light {
   --theme-arrowpanel-separator: hsla(210,4%,10%,.14);
 }
 
+strong {
+  font-weight: bold;
+}
+
 /* Tooltip: CSS variables tooltip */
 
 .devtools-tooltip-css-variable {
   color: var(--theme-body-color);
   padding: 2px;
 }
 
+/* Tooltip: Inactive CSS tooltip */
+
+.devtools-tooltip-inactive-css {
+  color: var(--theme-body-color);
+  padding: 7px 10px;
+  margin: 0;
+}
+
+.devtools-tooltip-inactive-css p {
+  margin-block-start: 0;
+  margin-block-end: 0;
+}
+
+.devtools-tooltip-inactive-css .link {
+  color: var(--theme-highlight-blue);
+  cursor: pointer;
+}
+
 /* Tooltip: Tiles */
 
 .devtools-tooltip-tiles {
   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: 20px 20px;
   background-position: 0 0, 10px 10px;
--- a/devtools/server/actors/utils/inactive-property-helper.js
+++ b/devtools/server/actors/utils/inactive-property-helper.js
@@ -2,21 +2,18 @@
 /* vim: set 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 Services = require("Services");
-const { LocalizationHelper } = require("devtools/shared/l10n");
 
 const PREF_UNUSED_CSS_ENABLED = "devtools.inspector.inactive.css.enabled";
-const INSPECTOR_L10N =
-  new LocalizationHelper("devtools/client/locales/inspector.properties");
 
 class InactivePropertyHelper {
   /**
    * A list of rules for when CSS properties have no effect.
    *
    * In certain situations, CSS properties do not have any effect. A common
    * example is trying to set a width on an inline element like a <span>.
    *
@@ -31,21 +28,24 @@ class InactivePropertyHelper {
    * {
    *   invalidProperties (see note):
    *     Array of CSS property names that are inactive if the rule matches.
    *   validProperties (see note):
    *     Array of CSS property names that are active if the rule matches.
    *   when:
    *     The rule itself, a JS function used to identify the conditions
    *     indicating whether a property is valid or not.
-   *
-   *   error:
-   *     A JS function that returns a custom error message explaining why the
-   *     property is inactive in this situation. This function takes a single
-   *     argument: the property name.
+   *   fixId:
+   *     A Fluent id containing a suggested solution to the problem that is
+   *     causing a property to be inactive.
+   *   msgId:
+   *     A Fluent id containing an error message explaining why a property is
+   *     inactive in this situation.
+   *   numFixProps:
+   *     The number of properties we suggest in the fixId string.
    * }
    *
    * NOTE: validProperties and invalidProperties are mutually exclusive.
    *
    * The main export is `isPropertyUsed()`, which can be used to check if a
    * property is used or not, and why.
    */
   get VALIDATORS() {
@@ -53,77 +53,89 @@ class InactivePropertyHelper {
       // Flex container property used on non-flex container.
       {
         invalidProperties: [
           "flex-direction",
           "flex-flow",
           "flex-wrap",
         ],
         when: () => !this.flexContainer,
-        error: property => msg("rule.inactive.css.not.flex.container", property),
+        fixId: "inactive-css-not-flex-container-fix",
+        msgId: "inactive-css-not-flex-container",
+        numFixProps: 2,
       },
       // Flex item property used on non-flex item.
       {
         invalidProperties: [
           "flex",
           "flex-basis",
           "flex-grow",
           "flex-shrink",
           "order",
         ],
         when: () => !this.flexItem,
-        error: property => msg("rule.inactive.css.not.flex.item", property),
+        fixId: "inactive-css-not-flex-item-fix",
+        msgId: "inactive-css-not-flex-item",
+        numFixProps: 2,
       },
       // Grid container property used on non-grid container.
       {
         invalidProperties: [
           "grid-auto-columns",
           "grid-auto-flow",
           "grid-auto-rows",
           "grid-template",
           "grid-gap",
           "row-gap",
           "column-gap",
           "justify-items",
         ],
         when: () => !this.gridContainer,
-        error: property => msg("rule.inactive.css.not.grid.container", property),
+        fixId: "inactive-css-not-grid-container-fix",
+        msgId: "inactive-css-not-grid-container",
+        numFixProps: 2,
       },
       // Grid item property used on non-grid item.
       {
         invalidProperties: [
           "grid-area",
           "grid-column",
           "grid-column-end",
           "grid-column-start",
           "grid-row",
           "grid-row-end",
           "grid-row-start",
           "justify-self",
         ],
         when: () => !this.gridItem,
-        error: property => msg("rule.inactive.css.not.grid.item", property),
+        fixId: "inactive-css-not-grid-item-fix",
+        msgId: "inactive-css-not-grid-item",
+        numFixProps: 2,
       },
       // Grid and flex item properties used on non-grid or non-flex item.
       {
         invalidProperties: [
           "align-self",
         ],
         when: () => !this.gridItem && !this.flexItem,
-        error: property => msg("rule.inactive.css.not.grid.or.flex.item", property),
+        fixId: "inactive-css-not-grid-or-flex-item-fix",
+        msgId: "inactive-css-not-grid-or-flex-item",
+        numFixProps: 4,
       },
       // Grid and flex container properties used on non-grid or non-flex container.
       {
         invalidProperties: [
           "align-content",
           "align-items",
           "justify-content",
         ],
         when: () => !this.gridContainer && !this.flexContainer,
-        error: property => msg("rule.inactive.css.not.grid.or.flex.container", property),
+        fixId: "inactive-css-not-grid-or-flex-container-fix",
+        msgId: "inactive-css-not-grid-or-flex-container",
+        numFixProps: 2,
       },
     ];
   }
 
   get unusedCssEnabled() {
     if (!this._unusedCssEnabled) {
       this._unusedCssEnabled = Services.prefs.getBoolPref(PREF_UNUSED_CSS_ENABLED);
     }
@@ -138,59 +150,76 @@ class InactivePropertyHelper {
    * @param {Style} elStyle
    *        The computed style for this DOMNode.
    * @param {DOMRule} cssRule
    *        The CSS rule the property is defined in.
    * @param {String} property
    *        The CSS property name.
    *
    * @return {Object} object
+   * @return {Boolean} object.fixId
+   *         A Fluent id containing a suggested solution to the problem that is
+   *         causing a property to be inactive.
+   * @return {Boolean} object.msgId
+   *         A Fluent id containing an error message explaining why a property
+   *         is inactive in this situation.
+   * @return {Boolean} object.numFixProps
+   *         The number of properties we suggest in the fixId string.
+   * @return {Boolean} object.property
+   *         The inactive property name.
    * @return {Boolean} object.used
    *         true if the property is used.
-   * @return {Array} object.reasons
-   *         A string array listing the reasons a property isn't used.
    */
   isPropertyUsed(el, elStyle, cssRule, property) {
     if (!this.unusedCssEnabled) {
       return {used: true};
     }
 
-    const errors = [];
+    let fixId = "";
+    let msgId = "";
+    let numFixProps = 0;
+    let used = true;
 
-    this.VALIDATORS.forEach(validator => {
+    this.VALIDATORS.some(validator => {
       // First check if this rule cares about this property.
       let isRuleConcerned = false;
 
       if (validator.invalidProperties) {
         isRuleConcerned = validator.invalidProperties === "*" ||
                           validator.invalidProperties.includes(property);
       } else if (validator.validProperties) {
         isRuleConcerned = !validator.validProperties.includes(property);
       }
 
       if (!isRuleConcerned) {
-        return;
+        return false;
       }
 
       this.select(el, elStyle, cssRule, property);
 
       // And then run the validator, gathering the error message if the
       // validator passes.
       if (validator.when()) {
-        const error = validator.error(property);
+        fixId = validator.fixId;
+        msgId = validator.msgId;
+        numFixProps = validator.numFixProps;
+        used = false;
 
-        if (typeof error === "string") {
-          errors.push(validator.error(property));
-        }
+        return true;
       }
+
+      return false;
     });
 
     return {
-      used: !errors.length,
-      reasons: errors,
+      fixId,
+      msgId,
+      numFixProps,
+      property,
+      used,
     };
   }
 
   /**
    * Focus on a node.
    *
    * @param {DOMNode} node
    *        Node to focus on.
@@ -345,22 +374,9 @@ class InactivePropertyHelper {
         return null; // Not a grid item, for sure.
       }
       // display: contents, walk to the parent
     }
     return null;
   }
 }
 
-/**
- * Helper function that gets localized strings.
- *
- * @param  {String} propName
- *         The property name to use. This property name must exist in the
- *         `inspector.properties` file).
- * @param  {*} values
- *         Values to be used as replacement strings.
- */
-function msg(...args) {
-  return INSPECTOR_L10N.getFormatStr(...args);
-}
-
 exports.inactivePropertyHelper = new InactivePropertyHelper();