Bug 1302536 - Add a toggleable button next to the 'display: grid' declaration to toggle the grid highlighter. r=pbro, a=jcristau, l10n=flod
authorGabriel Luong <gabriel.luong@gmail.com>
Wed, 16 Nov 2016 22:35:20 +0900
changeset 352590 116d83d90ce4ca5cb214e3e33c5da6735d59b848
parent 352589 be41fbd836e36b4a85d3889fc87ac3382f5cdf43
child 352591 7ff711bec0281ba29c0736730a9a6e8d86f0e7b5
push id6795
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 14:19:46 +0000
treeherdermozilla-esr52@76101b503191 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro, jcristau
bugs1302536
milestone52.0a2
Bug 1302536 - Add a toggleable button next to the 'display: grid' declaration to toggle the grid highlighter. r=pbro, a=jcristau, l10n=flod
devtools/client/inspector/rules/rules.js
devtools/client/inspector/rules/test/browser.ini
devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js
devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js
devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js
devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js
devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js
devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js
devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js
devtools/client/inspector/rules/views/rule-editor.js
devtools/client/inspector/rules/views/text-property-editor.js
devtools/client/inspector/shared/highlighters-overlay.js
devtools/client/jar.mn
devtools/client/shared/output-parser.js
devtools/client/themes/images/grid.svg
devtools/client/themes/rules.css
devtools/server/actors/highlighters.css
devtools/server/actors/highlighters/box-model.js
devtools/server/actors/highlighters/css-grid.js
devtools/server/actors/highlighters/css-transform.js
devtools/shared/locales/en-US/styleinspector.properties
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -250,25 +250,25 @@ CssRuleView.prototype = {
    */
   toggleSelectorHighlighter: function (selectorIcon, selector) {
     if (this.lastSelectorIcon) {
       this.lastSelectorIcon.classList.remove("highlighted");
     }
     selectorIcon.classList.remove("highlighted");
 
     this.unhighlightSelector().then(() => {
-      if (selector !== this.highlightedSelector) {
-        this.highlightedSelector = selector;
+      if (selector !== this.highlighters.selectorHighlighterShown) {
+        this.highlighters.selectorHighlighterShown = selector;
         selectorIcon.classList.add("highlighted");
         this.lastSelectorIcon = selectorIcon;
         this.highlightSelector(selector).then(() => {
           this.emit("ruleview-selectorhighlighter-toggled", true);
         }, e => console.error(e));
       } else {
-        this.highlightedSelector = null;
+        this.highlighters.selectorHighlighterShown = null;
         this.emit("ruleview-selectorhighlighter-toggled", false);
       }
     }, e => console.error(e));
   },
 
   highlightSelector: Task.async(function* (selector) {
     let node = this.inspector.selection.nodeFront;
 
@@ -1500,25 +1500,25 @@ function getPropertyNameAndValue(node) {
 }
 
 function RuleViewTool(inspector, window) {
   this.inspector = inspector;
   this.document = window.document;
 
   this.view = new CssRuleView(this.inspector, this.document);
 
+  this.clearUserProperties = this.clearUserProperties.bind(this);
+  this.refresh = this.refresh.bind(this);
   this.onLinkClicked = this.onLinkClicked.bind(this);
-  this.onSelected = this.onSelected.bind(this);
-  this.refresh = this.refresh.bind(this);
-  this.clearUserProperties = this.clearUserProperties.bind(this);
+  this.onMutations = this.onMutations.bind(this);
+  this.onPanelSelected = this.onPanelSelected.bind(this);
   this.onPropertyChanged = this.onPropertyChanged.bind(this);
+  this.onResized = this.onResized.bind(this);
+  this.onSelected = this.onSelected.bind(this);
   this.onViewRefreshed = this.onViewRefreshed.bind(this);
-  this.onPanelSelected = this.onPanelSelected.bind(this);
-  this.onMutations = this.onMutations.bind(this);
-  this.onResized = this.onResized.bind(this);
 
   this.view.on("ruleview-changed", this.onPropertyChanged);
   this.view.on("ruleview-refreshed", this.onViewRefreshed);
   this.view.on("ruleview-linked-clicked", this.onLinkClicked);
 
   this.inspector.selection.on("detached-front", this.onSelected);
   this.inspector.selection.on("new-node-front", this.onSelected);
   this.inspector.selection.on("pseudoclass", this.refresh);
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -92,16 +92,17 @@ skip-if = e10s && debug # Bug 1250058 - 
 subsuite = clipboard
 [browser_rules_cssom.js]
 [browser_rules_cubicbezier-appears-on-swatch-click.js]
 [browser_rules_cubicbezier-commit-on-ENTER.js]
 [browser_rules_cubicbezier-revert-on-ESC.js]
 [browser_rules_custom.js]
 [browser_rules_cycle-angle.js]
 [browser_rules_cycle-color.js]
+[browser_rules_edit-display-grid-property.js]
 [browser_rules_edit-property-cancel.js]
 [browser_rules_edit-property-click.js]
 [browser_rules_edit-property-commit.js]
 [browser_rules_edit-property-computed.js]
 [browser_rules_edit-property-increments.js]
 [browser_rules_edit-property-order.js]
 [browser_rules_edit-property-remove_01.js]
 [browser_rules_edit-property-remove_02.js]
@@ -136,16 +137,20 @@ skip-if = os == "mac" # Bug 1245996 : cl
 [browser_rules_edit-value-after-name_04.js]
 [browser_rules_editable-field-focus_01.js]
 [browser_rules_editable-field-focus_02.js]
 [browser_rules_eyedropper.js]
 [browser_rules_filtereditor-appears-on-swatch-click.js]
 [browser_rules_filtereditor-commit-on-ENTER.js]
 [browser_rules_filtereditor-revert-on-ESC.js]
 skip-if = (os == "win" && debug) # bug 963492: win.
+[browser_rules_grid-highlighter-on-navigate.js]
+[browser_rules_grid-toggle_01.js]
+[browser_rules_grid-toggle_02.js]
+[browser_rules_grid-toggle_03.js]
 [browser_rules_guessIndentation.js]
 [browser_rules_inherited-properties_01.js]
 [browser_rules_inherited-properties_02.js]
 [browser_rules_inherited-properties_03.js]
 [browser_rules_inline-source-map.js]
 [browser_rules_invalid.js]
 [browser_rules_invalid-source-map.js]
 [browser_rules_keybindings.js]
@@ -193,16 +198,17 @@ skip-if = (os == "win" && debug) # bug 9
 [browser_rules_search-filter_08.js]
 [browser_rules_search-filter_09.js]
 [browser_rules_search-filter_10.js]
 [browser_rules_search-filter_context-menu.js]
 subsuite = clipboard
 [browser_rules_search-filter_escape-keypress.js]
 [browser_rules_select-and-copy-styles.js]
 subsuite = clipboard
+[browser_rules_selector-highlighter-on-navigate.js]
 [browser_rules_selector-highlighter_01.js]
 [browser_rules_selector-highlighter_02.js]
 [browser_rules_selector-highlighter_03.js]
 [browser_rules_selector-highlighter_04.js]
 [browser_rules_selector_highlight.js]
 [browser_rules_strict-search-filter-computed-list_01.js]
 [browser_rules_strict-search-filter_01.js]
 [browser_rules_strict-search-filter_02.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-display-grid-property.js
@@ -0,0 +1,49 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the grid highlighter in the rule view and modifying the 'display: grid'
+// declaration.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid">
+    <div id="cell1">cell1</div>
+    <div id="cell2">cell2</div>
+  </div>
+`;
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  let highlighters = view.highlighters;
+
+  yield selectNode("#grid", inspector);
+  let container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+  let gridToggle = container.querySelector(".ruleview-grid");
+
+  info("Toggling ON the CSS grid highlighter from the rule-view.");
+  let onHighlighterShown = highlighters.once("highlighter-shown");
+  gridToggle.click();
+  yield onHighlighterShown;
+
+  info("Edit the 'grid' property value to 'block'.");
+  let editor = yield focusEditableField(view, container);
+  let onHighlighterHidden = highlighters.once("highlighter-hidden");
+  let onDone = view.once("ruleview-changed");
+  editor.input.value = "block;";
+  EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+  yield onHighlighterHidden;
+  yield onDone;
+
+  info("Check the grid highlighter and grid toggle button are hidden.");
+  gridToggle = container.querySelector(".ruleview-grid");
+  ok(!gridToggle, "Grid highlighter toggle is not visible.");
+  ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+});
--- a/devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js
+++ b/devtools/client/inspector/rules/test/browser_rules_edit-selector_04.js
@@ -58,11 +58,12 @@ function* testEditSelector(view, name) {
   let onToggled = view.once("ruleview-selectorhighlighter-toggled");
 
   info("Entering a new selector name and committing");
   editor.input.value = name;
   EventUtils.synthesizeKey("VK_RETURN", {});
 
   let isVisible = yield onToggled;
 
-  ok(!view.highlightedSelector, "The selectorhighlighter instance was removed");
+  ok(!view.highlighters.selectorHighlighterShown,
+    "The selectorHighlighterShown instance was removed");
   ok(!isVisible, "The toggle event says the highlighter is not visible");
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-highlighter-on-navigate.js
@@ -0,0 +1,41 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that grid highlighter is hidden on page navigation.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid">
+    <div id="cell1">cell1</div>
+    <div id="cell2">cell2</div>
+  </div>
+`;
+
+const TEST_URI_2 = "data:text/html,<html><body>test</body></html>";
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  let highlighters = view.highlighters;
+
+  yield selectNode("#grid", inspector);
+  let container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+  let gridToggle = container.querySelector(".ruleview-grid");
+
+  info("Toggling ON the CSS grid highlighter from the rule-view.");
+  let onHighlighterShown = highlighters.once("highlighter-shown");
+  gridToggle.click();
+  yield onHighlighterShown;
+
+  ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+  yield navigateTo(inspector, TEST_URI_2);
+  ok(!highlighters.gridHighlighterShown, "CSS grid highlighter is hidden.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_01.js
@@ -0,0 +1,64 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the grid highlighter in the rule view and the display of the
+// grid highlighter.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid">
+    <div id="cell1">cell1</div>
+    <div id="cell2">cell2</div>
+  </div>
+`;
+
+const HIGHLIGHTER_TYPE = "CssGridHighlighter";
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  let highlighters = view.highlighters;
+
+  yield selectNode("#grid", inspector);
+  let container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+  let gridToggle = container.querySelector(".ruleview-grid");
+
+  info("Checking the initial state of the CSS grid toggle in the rule-view.");
+  ok(gridToggle, "Grid highlighter toggle is visible.");
+  ok(!gridToggle.classList.contains("active"),
+    "Grid highlighter toggle button is not active.");
+  ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "No CSS grid highlighter exists in the rule-view.");
+  ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+
+  info("Toggling ON the CSS grid highlighter from the rule-view.");
+  let onHighlighterShown = highlighters.once("highlighter-shown");
+  gridToggle.click();
+  yield onHighlighterShown;
+
+  info("Checking the CSS grid highlighter is created and toggle button is active in " +
+    "the rule-view.");
+  ok(gridToggle.classList.contains("active"),
+    "Grid highlighter toggle is active.");
+  ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "CSS grid highlighter created in the rule-view.");
+  ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+  info("Toggling OFF the CSS grid highlighter from the rule-view.");
+  let onHighlighterHidden = highlighters.once("highlighter-hidden");
+  gridToggle.click();
+  yield onHighlighterHidden;
+
+  info("Checking the CSS grid highlighter is not shown and toggle button is not active " +
+    "in the rule-view.");
+  ok(!gridToggle.classList.contains("active"),
+    "Grid highlighter toggle button is not active.");
+  ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_02.js
@@ -0,0 +1,73 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the grid highlighter in the rule view from an overridden 'display: grid'
+// declaration.
+
+const TEST_URI = `
+  <style type='text/css'>
+    #grid {
+      display: grid;
+    }
+    div, ul {
+      display: grid;
+    }
+  </style>
+  <ul id="grid">
+    <li id="cell1">cell1</li>
+    <li id="cell2">cell2</li>
+  </ul>
+`;
+
+const HIGHLIGHTER_TYPE = "CssGridHighlighter";
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  let highlighters = view.highlighters;
+
+  yield selectNode("#grid", inspector);
+  let container = getRuleViewProperty(view, "#grid", "display").valueSpan;
+  let gridToggle = container.querySelector(".ruleview-grid");
+  let overriddenContainer = getRuleViewProperty(view, "div, ul", "display").valueSpan;
+  let overriddenGridToggle = overriddenContainer.querySelector(".ruleview-grid");
+
+  info("Checking the initial state of the CSS grid toggle in the rule-view.");
+  ok(gridToggle && overriddenGridToggle, "Grid highlighter toggles are visible.");
+  ok(!gridToggle.classList.contains("active") &&
+    !overriddenGridToggle.classList.contains("active"),
+    "Grid highlighter toggle buttons are not active.");
+  ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "No CSS grid highlighter exists in the rule-view.");
+  ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+
+  info("Toggling ON the CSS grid highlighter from the overridden rule in the rule-view.");
+  let onHighlighterShown = highlighters.once("highlighter-shown");
+  overriddenGridToggle.click();
+  yield onHighlighterShown;
+
+  info("Checking the CSS grid highlighter is created and toggle buttons are active in " +
+    "the rule-view.");
+  ok(gridToggle.classList.contains("active") &&
+    overriddenGridToggle.classList.contains("active"),
+    "Grid highlighter toggle is active.");
+  ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "CSS grid highlighter created in the rule-view.");
+  ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+  info("Toggling off the CSS grid highlighter from the normal grid declaration in the " +
+    "rule-view.");
+  let onHighlighterHidden = highlighters.once("highlighter-hidden");
+  gridToggle.click();
+  yield onHighlighterHidden;
+
+  info("Checking the CSS grid highlighter is not shown and toggle buttons are not " +
+    "active in the rule-view.");
+  ok(!gridToggle.classList.contains("active") &&
+    !overriddenGridToggle.classList.contains("active"),
+    "Grid highlighter toggle buttons are not active.");
+  ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_grid-toggle_03.js
@@ -0,0 +1,96 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling the grid highlighter in the rule view with multiple grids in the page.
+
+const TEST_URI = `
+  <style type='text/css'>
+    .grid {
+      display: grid;
+    }
+  </style>
+  <div id="grid1" class="grid">
+    <div class="cell1">cell1</div>
+    <div class="cell2">cell2</div>
+  </div>
+  <div id="grid2" class="grid">
+    <div class="cell1">cell1</div>
+    <div class="cell2">cell2</div>
+  </div>
+`;
+
+const HIGHLIGHTER_TYPE = "CssGridHighlighter";
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  let highlighters = view.highlighters;
+
+  info("Selecting the first grid container.");
+  yield selectNode("#grid1", inspector);
+  let container = getRuleViewProperty(view, ".grid", "display").valueSpan;
+  let gridToggle = container.querySelector(".ruleview-grid");
+
+  info("Checking the state of the CSS grid toggle for the first grid container in the " +
+    "rule-view.");
+  ok(gridToggle, "Grid highlighter toggle is visible.");
+  ok(!gridToggle.classList.contains("active"),
+    "Grid highlighter toggle button is not active.");
+  ok(!highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "No CSS grid highlighter exists in the rule-view.");
+  ok(!highlighters.gridHighlighterShown, "No CSS grid highlighter is shown.");
+
+  info("Toggling ON the CSS grid highlighter for the first grid container from the " +
+    "rule-view.");
+  let onHighlighterShown = highlighters.once("highlighter-shown");
+  gridToggle.click();
+  yield onHighlighterShown;
+
+  info("Checking the CSS grid highlighter is created and toggle button is active in " +
+    "the rule-view.");
+  ok(gridToggle.classList.contains("active"),
+    "Grid highlighter toggle is active.");
+  ok(highlighters.highlighters[HIGHLIGHTER_TYPE],
+    "CSS grid highlighter created in the rule-view.");
+  ok(highlighters.gridHighlighterShown, "CSS grid highlighter is shown.");
+
+  info("Selecting the second grid container.");
+  yield selectNode("#grid2", inspector);
+  let firstGridHighterShown = highlighters.gridHighlighterShown;
+  container = getRuleViewProperty(view, ".grid", "display").valueSpan;
+  gridToggle = container.querySelector(".ruleview-grid");
+
+  info("Checking the state of the CSS grid toggle for the second grid container in the " +
+    "rule-view.");
+  ok(gridToggle, "Grid highlighter toggle is visible.");
+  ok(!gridToggle.classList.contains("active"),
+    "Grid highlighter toggle button is not active.");
+  ok(highlighters.gridHighlighterShown, "CSS grid highlighter is still shown.");
+
+  info("Toggling ON the CSS grid highlighter for the second grid container from the " +
+    "rule-view.");
+  onHighlighterShown = highlighters.once("highlighter-shown");
+  gridToggle.click();
+  yield onHighlighterShown;
+
+  info("Checking the CSS grid highlighter is created for the second grid container and " +
+    "toggle button is active in the rule-view.");
+  ok(gridToggle.classList.contains("active"),
+    "Grid highlighter toggle is active.");
+  ok(highlighters.gridHighlighterShown != firstGridHighterShown,
+    "Grid highlighter for the second grid container is shown.");
+
+  info("Selecting the first grid container.");
+  yield selectNode("#grid1", inspector);
+  container = getRuleViewProperty(view, ".grid", "display").valueSpan;
+  gridToggle = container.querySelector(".ruleview-grid");
+
+  info("Checking the state of the CSS grid toggle for the first grid container in the " +
+    "rule-view.");
+  ok(gridToggle, "Grid highlighter toggle is visible.");
+  ok(!gridToggle.classList.contains("active"),
+    "Grid highlighter toggle button is not active.");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_selector-highlighter-on-navigate.js
@@ -0,0 +1,38 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the selector highlighter is hidden on page navigation.
+
+const TEST_URI = `
+  <style type="text/css">
+    body, p, td {
+      background: red;
+    }
+  </style>
+  Test the selector highlighter
+`;
+
+const TEST_URI_2 = "data:text/html,<html><body>test</body></html>";
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  let highlighters = view.highlighters;
+
+  info("Clicking on a selector icon");
+  let icon = getRuleViewSelectorHighlighterIcon(view, "body, p, td");
+
+  let onToggled = view.once("ruleview-selectorhighlighter-toggled");
+  EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow);
+  let isVisible = yield onToggled;
+
+  ok(highlighters.selectorHighlighterShown, "The selectorHighlighterShown is set.");
+  ok(view.selectorHighlighter, "The selectorhighlighter instance was created");
+  ok(isVisible, "The toggle event says the highlighter is visible");
+
+  yield navigateTo(inspector, TEST_URI_2);
+  ok(!highlighters.selectorHighlighterShown, "The selectorHighlighterShown is unset.");
+});
--- a/devtools/client/inspector/rules/views/rule-editor.js
+++ b/devtools/client/inspector/rules/views/rule-editor.js
@@ -149,17 +149,17 @@ RuleEditor.prototype = {
 
     if (this.rule.domRule.type !== CSSRule.KEYFRAME_RULE) {
       let selector = this.rule.domRule.selectors
                ? this.rule.domRule.selectors.join(", ")
                : this.ruleView.inspector.selectionCssSelector;
 
       let selectorHighlighter = createChild(header, "span", {
         class: "ruleview-selectorhighlighter" +
-               (this.ruleView.highlightedSelector === selector ?
+               (this.ruleView.highlighters.selectorHighlighterShown === selector ?
                 " highlighted" : ""),
         title: l10n("rule.selectorHighlighter.tooltip")
       });
       selectorHighlighter.addEventListener("click", () => {
         this.ruleView.toggleSelectorHighlighter(selectorHighlighter, selector);
       });
     }
 
@@ -580,19 +580,19 @@ RuleEditor.prototype = {
 
       // We install the new editor in place of the old -- you might
       // think we would replicate the list-modification logic above,
       // but that is complicated due to the way the UI installs
       // pseudo-element rules and the like.
       this.element.parentNode.replaceChild(editor.element, this.element);
 
       // Remove highlight for modified selector
-      if (ruleView.highlightedSelector) {
+      if (ruleView.highlighters.selectorHighlighterShown) {
         ruleView.toggleSelectorHighlighter(ruleView.lastSelectorIcon,
-          ruleView.highlightedSelector);
+          ruleView.highlighters.selectorHighlighterShown);
       }
 
       editor._moveSelectorFocus(direction);
     } catch (err) {
       this.isEditing = false;
       promiseWarn(err);
     }
   }),
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -337,24 +337,25 @@ TextPropertyEditor.prototype = {
     if (propDirty) {
       this.element.setAttribute("dirty", "");
     } else {
       this.element.removeAttribute("dirty");
     }
 
     let outputParser = this.ruleView._outputParser;
     let parserOptions = {
-      colorSwatchClass: SHARED_SWATCH_CLASS + " " + COLOR_SWATCH_CLASS,
-      colorClass: "ruleview-color",
-      bezierSwatchClass: SHARED_SWATCH_CLASS + " " + BEZIER_SWATCH_CLASS,
+      angleClass: "ruleview-angle",
+      angleSwatchClass: SHARED_SWATCH_CLASS + " " + ANGLE_SWATCH_CLASS,
       bezierClass: "ruleview-bezier",
-      filterSwatchClass: SHARED_SWATCH_CLASS + " " + FILTER_SWATCH_CLASS,
+      bezierSwatchClass: SHARED_SWATCH_CLASS + " " + BEZIER_SWATCH_CLASS,
+      colorClass: "ruleview-color",
+      colorSwatchClass: SHARED_SWATCH_CLASS + " " + COLOR_SWATCH_CLASS,
       filterClass: "ruleview-filter",
-      angleSwatchClass: SHARED_SWATCH_CLASS + " " + ANGLE_SWATCH_CLASS,
-      angleClass: "ruleview-angle",
+      filterSwatchClass: SHARED_SWATCH_CLASS + " " + FILTER_SWATCH_CLASS,
+      gridClass: "ruleview-grid",
       defaultColorType: !propDirty,
       urlClass: "theme-link",
       baseURI: this.sheetHref
     };
     let frag = outputParser.parseCssProperty(name, val, parserOptions);
     this.valueSpan.innerHTML = "";
     this.valueSpan.appendChild(frag);
 
@@ -419,16 +420,25 @@ TextPropertyEditor.prototype = {
     if (this.ruleEditor.isEditable) {
       for (let angleSpan of this.angleSwatchSpans) {
         angleSpan.on("unit-change", this._onSwatchCommit);
         let title = l10n("rule.angleSwatch.tooltip");
         angleSpan.setAttribute("title", title);
       }
     }
 
+    let gridToggle = this.valueSpan.querySelector(".ruleview-grid");
+    if (gridToggle) {
+      gridToggle.setAttribute("title", l10n("rule.gridToggle.tooltip"));
+      if (this.ruleView.highlighters.gridHighlighterShown ===
+          this.ruleView.inspector.selection.nodeFront) {
+        gridToggle.classList.add("active");
+      }
+    }
+
     // Now that we have updated the property's value, we might have a pending
     // click on the value container. If we do, we have to trigger a click event
     // on the right element.
     if (this._hasPendingClick) {
       this._hasPendingClick = false;
       let elToClick;
 
       if (this._clickedElementOptions !== null) {
@@ -720,16 +730,20 @@ TextPropertyEditor.prototype = {
     // its original value and enabled or disabled state
     if (value.trim() && isValueUnchanged) {
       this.ruleEditor.rule.previewPropertyValue(this.prop, val.value,
                                                 val.priority);
       this.rule.setPropertyEnabled(this.prop, this.prop.enabled);
       return;
     }
 
+    if (this.isDisplayGrid) {
+      this.ruleView.highlighters._hideGridHighlighter();
+    }
+
     // First, set this property value (common case, only modified a property)
     this.prop.setValue(val.value, val.priority);
 
     if (!this.prop.enabled) {
       this.prop.setEnabled(true);
     }
 
     this.committed.value = this.prop.value;
@@ -846,12 +860,21 @@ TextPropertyEditor.prototype = {
   /**
    * Validate this property. Does it make sense for this value to be assigned
    * to this property name? This does not apply the property value
    *
    * @return {Boolean} true if the property value is valid, false otherwise.
    */
   isValid: function () {
     return this.prop.isValid();
+  },
+
+  /**
+   * Returns true if the property is a `display: grid` declaration.
+   *
+   * @return {Boolean} true if the property is a `display: grid` declaration.
+   */
+  isDisplayGrid: function () {
+    return this.prop.name === "display" && this.prop.value === "grid";
   }
 };
 
 exports.TextPropertyEditor = TextPropertyEditor;
--- a/devtools/client/inspector/shared/highlighters-overlay.js
+++ b/devtools/client/inspector/shared/highlighters-overlay.js
@@ -22,92 +22,143 @@ const { VIEW_NODE_VALUE_TYPE } = require
  *         Either the rule-view or computed-view panel
  */
 function HighlightersOverlay(view) {
   this.view = view;
 
   let {CssRuleView} = require("devtools/client/inspector/rules/rules");
   this.isRuleView = view instanceof CssRuleView;
 
+  this.highlighters = {};
+
+  // NodeFront of the grid container that is highlighted.
+  this.gridHighlighterShown = null;
+  // Name of the highlighter shown on mouse hover.
+  this.hoveredHighlighterShown = null;
+  // Name of the selector highlighter shown.
+  this.selectorHighlighterShown = null;
+
   this.highlighterUtils = this.view.inspector.toolbox.highlighterUtils;
 
+  // Only initialize the overlay if at least one of the highlighter types is
+  // supported.
+  this.supportsHighlighters =
+    this.highlighterUtils.supportsCustomHighlighters();
+
+  this._onClick = this._onClick.bind(this);
   this._onMouseMove = this._onMouseMove.bind(this);
   this._onMouseOut = this._onMouseOut.bind(this);
-
-  this.highlighters = {};
-
-  // Only initialize the overlay if at least one of the highlighter types is
-  // supported
-  this.supportsHighlighters =
-    this.highlighterUtils.supportsCustomHighlighters();
+  this._onWillNavigate = this._onWillNavigate.bind(this);
 
   EventEmitter.decorate(this);
 }
 
 HighlightersOverlay.prototype = {
   /**
    * Add the highlighters overlay to the view. This will start tracking mouse
-   * movements and display highlighters when needed
+   * movements and display highlighters when needed.
    */
   addToView: function () {
     if (!this.supportsHighlighters || this._isStarted || this._isDestroyed) {
       return;
     }
 
     let el = this.view.element;
+    el.addEventListener("click", this._onClick, true);
     el.addEventListener("mousemove", this._onMouseMove, false);
     el.addEventListener("mouseout", this._onMouseOut, false);
     el.ownerDocument.defaultView.addEventListener("mouseout", this._onMouseOut, false);
 
+    if (this.isRuleView) {
+      this.view.inspector.target.on("will-navigate", this._onWillNavigate);
+    }
+
     this._isStarted = true;
   },
 
   /**
    * Remove the overlay from the current view. This will stop tracking mouse
-   * movement and showing highlighters
+   * movement and showing highlighters.
    */
   removeFromView: function () {
     if (!this.supportsHighlighters || !this._isStarted || this._isDestroyed) {
       return;
     }
 
-    this._hideCurrent();
-
     let el = this.view.element;
+    el.removeEventListener("click", this._onClick, true);
     el.removeEventListener("mousemove", this._onMouseMove, false);
     el.removeEventListener("mouseout", this._onMouseOut, false);
 
+    if (this.isRuleView) {
+      this.view.inspector.target.off("will-navigate", this._onWillNavigate);
+    }
+
     this._isStarted = false;
   },
 
+  _onClick: function (event) {
+    // Bail out if the target is not a grid property value.
+    if (!this._isDisplayGridValue(event.target)) {
+      return;
+    }
+
+    event.stopPropagation();
+
+    this._getHighlighter("CssGridHighlighter").then(highlighter => {
+      let node = this.view.inspector.selection.nodeFront;
+
+      // Toggle off the grid highlighter if the grid highlighter toggle is clicked
+      // for the current highlighted grid.
+      if (node === this.gridHighlighterShown) {
+        return highlighter.hide();
+      }
+
+      return highlighter.show(node);
+    }).then(isGridShown => {
+      // Toggle all the grid icons in the current rule view.
+      for (let gridIcon of this.view.element.querySelectorAll(".ruleview-grid")) {
+        gridIcon.classList.toggle("active", isGridShown);
+      }
+
+      if (isGridShown) {
+        this.gridHighlighterShown = this.view.inspector.selection.nodeFront;
+        this.emit("highlighter-shown");
+      } else {
+        this.gridHighlighterShown = null;
+        this.emit("highlighter-hidden");
+      }
+    }).catch(e => console.error(e));
+  },
+
   _onMouseMove: function (event) {
-    // Bail out if the target is the same as for the last mousemove
+    // Bail out if the target is the same as for the last mousemove.
     if (event.target === this._lastHovered) {
       return;
     }
 
-    // Only one highlighter can be displayed at a time, hide the currently shown
-    this._hideCurrent();
+    // Only one highlighter can be displayed at a time, hide the currently shown.
+    this._hideHoveredHighlighter();
 
     this._lastHovered = event.target;
 
     let nodeInfo = this.view.getNodeInfo(event.target);
     if (!nodeInfo) {
       return;
     }
 
-    // Choose the type of highlighter required for the hovered node
+    // Choose the type of highlighter required for the hovered node.
     let type;
     if (this._isRuleViewTransform(nodeInfo) ||
         this._isComputedViewTransform(nodeInfo)) {
       type = "CssTransformHighlighter";
     }
 
     if (type) {
-      this.highlighterShown = type;
+      this.hoveredHighlighterShown = type;
       let node = this.view.inspector.selection.nodeFront;
       this._getHighlighter(type)
           .then(highlighter => highlighter.show(node))
           .then(shown => {
             if (shown) {
               this.emit("highlighter-shown");
             }
           });
@@ -118,72 +169,111 @@ HighlightersOverlay.prototype = {
     // Only hide the highlighter if the mouse leaves the currently hovered node.
     if (!this._lastHovered ||
         (event && this._lastHovered.contains(event.relatedTarget))) {
       return;
     }
 
     // Otherwise, hide the highlighter.
     this._lastHovered = null;
-    this._hideCurrent();
+    this._hideHoveredHighlighter();
   },
 
   /**
-   * Is the current hovered node a css transform property value in the rule-view
+   * Clear saved highlighter shown properties on will-navigate.
+   */
+  _onWillNavigate: function () {
+    this.gridHighlighterShown = null;
+    this.hoveredHighlighterShown = null;
+    this.selectorHighlighterShown = null;
+  },
+
+  /**
+   * Is the current hovered node a css transform property value in the rule-view.
    *
    * @param  {Object} nodeInfo
    * @return {Boolean}
    */
   _isRuleViewTransform: function (nodeInfo) {
     let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE &&
                       nodeInfo.value.property === "transform";
     let isEnabled = nodeInfo.value.enabled &&
                     !nodeInfo.value.overridden &&
                     !nodeInfo.value.pseudoElement;
     return this.isRuleView && isTransform && isEnabled;
   },
 
   /**
    * Is the current hovered node a css transform property value in the
-   * computed-view
+   * computed-view.
    *
    * @param  {Object} nodeInfo
    * @return {Boolean}
    */
   _isComputedViewTransform: function (nodeInfo) {
     let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE &&
                       nodeInfo.value.property === "transform";
     return !this.isRuleView && isTransform;
   },
 
   /**
-   * Hide the currently shown highlighter
+   * Is the current clicked node a grid display property value in the
+   * rule-view.
+   *
+   * @param  {DOMNode} node
+   * @return {Boolean}
+   */
+  _isDisplayGridValue: function (node) {
+    return this.isRuleView && node.classList.contains("ruleview-grid");
+  },
+
+  /**
+   * Hide the currently shown grid highlighter.
    */
-  _hideCurrent: function () {
-    if (!this.highlighterShown || !this.highlighters[this.highlighterShown]) {
+  _hideGridHighlighter: function () {
+    if (!this.gridHighlighterShown || !this.highlighters.CssGridHighlighter) {
+      return;
+    }
+
+    let onHidden = this.highlighters.CssGridHighlighter.hide();
+    if (onHidden) {
+      onHidden.then(null, e => console.error(e));
+    }
+
+    this.gridHighlighterShown = null;
+    this.emit("highlighter-hidden");
+  },
+
+  /**
+   * Hide the currently shown hovered highlighter.
+   */
+  _hideHoveredHighlighter: function () {
+    if (!this.hoveredHighlighterShown ||
+        !this.highlighters[this.hoveredHighlighterShown]) {
       return;
     }
 
     // For some reason, the call to highlighter.hide doesn't always return a
     // promise. This causes some tests to fail when trying to install a
     // rejection handler on the result of the call. To avoid this, check
     // whether the result is truthy before installing the handler.
-    let onHidden = this.highlighters[this.highlighterShown].hide();
+    let onHidden = this.highlighters[this.hoveredHighlighterShown].hide();
     if (onHidden) {
       onHidden.then(null, e => console.error(e));
     }
 
-    this.highlighterShown = null;
+    this.hoveredHighlighterShown = null;
     this.emit("highlighter-hidden");
   },
 
   /**
-   * Get a highlighter front given a type. It will only be initialized once
+   * Get a highlighter front given a type. It will only be initialized once.
    *
-   * @param  {String} type The highlighter type. One of this.highlighters
+   * @param  {String} type
+   *         The highlighter type. One of this.highlighters.
    * @return {Promise} that resolves to the highlighter
    */
   _getHighlighter: function (type) {
     let utils = this.highlighterUtils;
 
     if (this.highlighters[type]) {
       return promise.resolve(this.highlighters[type]);
     }
@@ -191,28 +281,35 @@ HighlightersOverlay.prototype = {
     return utils.getHighlighterByType(type).then(highlighter => {
       this.highlighters[type] = highlighter;
       return highlighter;
     });
   },
 
   /**
    * Destroy this overlay instance, removing it from the view and destroying
-   * all initialized highlighters
+   * all initialized highlighters.
    */
   destroy: function () {
     this.removeFromView();
 
     for (let type in this.highlighters) {
       if (this.highlighters[type]) {
         this.highlighters[type].finalize();
         this.highlighters[type] = null;
       }
     }
 
+    this.highlighters = null;
+
+    this.gridHighlighterShown = null;
+    this.hoveredHighlighterShown = null;
+    this.selectorHighlighterShown = null;
+
+    this.highlighterUtils = null;
+    this.isRuleView = null;
     this.view = null;
-    this.highlighterUtils = null;
 
     this._isDestroyed = true;
   }
 };
 
 module.exports = HighlightersOverlay;
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -136,16 +136,17 @@ devtools.jar:
     skin/light-theme.css (themes/light-theme.css)
     skin/firebug-theme.css (themes/firebug-theme.css)
     skin/toolbars.css (themes/toolbars.css)
     skin/toolbox.css (themes/toolbox.css)
     skin/tooltips.css (themes/tooltips.css)
     skin/images/add.svg (themes/images/add.svg)
     skin/images/filters.svg (themes/images/filters.svg)
     skin/images/filter-swatch.svg (themes/images/filter-swatch.svg)
+    skin/images/grid.svg (themes/images/grid.svg)
     skin/images/angle-swatch.svg (themes/images/angle-swatch.svg)
     skin/images/pseudo-class.svg (themes/images/pseudo-class.svg)
     skin/images/controls.png (themes/images/controls.png)
     skin/images/controls@2x.png (themes/images/controls@2x.png)
     skin/images/animation-fast-track.svg (themes/images/animation-fast-track.svg)
     skin/images/performance-icons.svg (themes/images/performance-icons.svg)
     skin/widgets.css (themes/widgets.css)
     skin/images/power.svg (themes/images/power.svg)
--- a/devtools/client/shared/output-parser.js
+++ b/devtools/client/shared/output-parser.js
@@ -9,18 +9,20 @@ const {colorUtils} = require("devtools/s
 const {getCSSLexer} = require("devtools/shared/css/lexer");
 const EventEmitter = require("devtools/shared/event-emitter");
 const {
   ANGLE_TAKING_FUNCTIONS,
   BEZIER_KEYWORDS,
   COLOR_TAKING_FUNCTIONS,
   CSS_TYPES
 } = require("devtools/shared/css/properties-db");
+const Services = require("Services");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
+const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled";
 
 /**
  * This module is used to process text for output by developer tools. This means
  * linking JS files with the debugger, CSS files with the style editor, JS
  * functions with the debugger, placing color swatches next to colors and
  * adding doorhanger previews where possible (images, angles, lengths,
  * border radius, cubic-bezier etc.).
  *
@@ -65,16 +67,17 @@ OutputParser.prototype = {
    *         _mergeOptions().
    * @return {DocumentFragment}
    *         A document fragment containing color swatches etc.
    */
   parseCssProperty: function (name, value, options = {}) {
     options = this._mergeOptions(options);
 
     options.expectCubicBezier = this.supportsType(name, CSS_TYPES.TIMING_FUNCTION);
+    options.expectDisplay = name === "display";
     options.expectFilter = name === "filter";
     options.supportsColor = this.supportsType(name, CSS_TYPES.COLOR) ||
                             this.supportsType(name, CSS_TYPES.GRADIENT);
 
     // The filter property is special in that we want to show the
     // swatch even if the value is invalid, because this way the user
     // can easily use the editor to fix it.
     if (options.expectFilter || this._cssPropertySupportsValue(name, value)) {
@@ -193,16 +196,20 @@ OutputParser.prototype = {
           }
           break;
         }
 
         case "ident":
           if (options.expectCubicBezier &&
               BEZIER_KEYWORDS.indexOf(token.text) >= 0) {
             this._appendCubicBezier(token.text, options);
+          } else if (Services.prefs.getBoolPref(CSS_GRID_ENABLED_PREF) &&
+                     options.expectDisplay && token.text === "grid" &&
+                     text === token.text) {
+            this._appendGrid(token.text, options);
           } else if (colorOK() && colorUtils.isValidCSSColor(token.text)) {
             this._appendColor(token.text, options);
           } else if (angleOK(token.text)) {
             this._appendAngle(token.text, options);
           } else {
             this._appendTextNode(text.substring(token.startOffset,
                                                 token.endOffset));
           }
@@ -283,16 +290,41 @@ OutputParser.prototype = {
       class: options.bezierClass
     }, bezier);
 
     container.appendChild(value);
     this.parsed.push(container);
   },
 
   /**
+   * Append a CSS Grid highlighter toggle icon next to the value in a
+   * 'display: grid' declaration
+   *
+   * @param {String} grid
+   *        The grid text value to append
+   * @param {Object} options
+   *        Options object. For valid options and default values see
+   *        _mergeOptions()
+   */
+  _appendGrid: function (grid, options) {
+    let container = this._createNode("span", {});
+
+    let toggle = this._createNode("span", {
+      class: options.gridClass
+    });
+
+    let value = this._createNode("span", {});
+    value.textContent = grid;
+
+    container.appendChild(toggle);
+    container.appendChild(value);
+    this.parsed.push(container);
+  },
+
+  /**
    * Append a angle value to the output
    *
    * @param {String} angle
    *        angle to append
    * @param {Object} options
    *        Options object. For valid options and default values see
    *        _mergeOptions()
    */
@@ -612,50 +644,52 @@ OutputParser.prototype = {
    * Merges options objects. Default values are set here.
    *
    * @param  {Object} overrides
    *         The option values to override e.g. _mergeOptions({colors: false})
    *
    *         Valid options are:
    *           - defaultColorType: true // Convert colors to the default type
    *                                    // selected in the options panel.
-   *           - colorSwatchClass: ""   // The class to use for color swatches.
-   *           - colorClass: ""         // The class to use for the color value
-   *                                    // that follows the swatch.
-   *           - bezierSwatchClass: ""  // The class to use for bezier swatches.
-   *           - bezierClass: ""        // The class to use for the bezier value
+   *           - angleClass: ""         // The class to use for the angle value
    *                                    // that follows the swatch.
    *           - angleSwatchClass: ""   // The class to use for angle swatches.
-   *           - angleClass: ""         // The class to use for the angle value
+   *           - bezierClass: ""        // The class to use for the bezier value
    *                                    // that follows the swatch.
-   *           - supportsColor: false   // Does the CSS property support colors?
-   *           - urlClass: ""           // The class to be used for url() links.
-   *           - baseURI: undefined     // A string used to resolve
-   *                                    // relative links.
+   *           - bezierSwatchClass: ""  // The class to use for bezier swatches.
+   *           - colorClass: ""         // The class to use for the color value
+   *                                    // that follows the swatch.
+   *           - colorSwatchClass: ""   // The class to use for color swatches.
    *           - filterSwatch: false    // A special case for parsing a
    *                                    // "filter" property, causing the
    *                                    // parser to skip the call to
    *                                    // _wrapFilter.  Used only for
    *                                    // previewing with the filter swatch.
+   *           - gridClass: ""          // The class to use for the grid icon.
+   *           - supportsColor: false   // Does the CSS property support colors?
+   *           - urlClass: ""           // The class to be used for url() links.
+   *           - baseURI: undefined     // A string used to resolve
+   *                                    // relative links.
    * @return {Object}
    *         Overridden options object
    */
   _mergeOptions: function (overrides) {
     let defaults = {
       defaultColorType: true,
-      colorSwatchClass: "",
-      colorClass: "",
+      angleClass: "",
+      angleSwatchClass: "",
+      bezierClass: "",
       bezierSwatchClass: "",
-      bezierClass: "",
-      angleSwatchClass: "",
-      angleClass: "",
+      colorClass: "",
+      colorSwatchClass: "",
+      filterSwatch: false,
+      gridClass: "",
       supportsColor: false,
       urlClass: "",
       baseURI: undefined,
-      filterSwatch: false
     };
 
     for (let item in overrides) {
       defaults[item] = overrides[item];
     }
     return defaults;
   }
 };
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/grid.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" stroke="#696969">
+  <path fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" d="M2 4h12M2 8h12M2 12h12M4 14V2M8 14V2M12 14V2"/>
+</svg>
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -362,29 +362,36 @@
   list-style: none;
   padding: 0;
 }
 
 .ruleview-computed {
   margin-inline-start: 35px;
 }
 
+.ruleview-grid,
 .ruleview-swatch {
   cursor: pointer;
   border-radius: 50%;
   width: 0.9em;
   height: 0.9em;
   vertical-align: middle;
   /* align the swatch with its value */
   margin-top: -1px;
   margin-inline-end: 5px;
   display: inline-block;
   position: relative;
 }
 
+.ruleview-grid {
+  background: url("chrome://devtools/skin/images/grid.svg");
+  background-size: 1em;
+  border-radius: 0;
+}
+
 .ruleview-colorswatch::before {
   content: '';
   background-color: #eee;
   background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
                     linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
   background-size: 12px 12px;
   background-position: 0 0, 6px 6px;
   position: absolute;
@@ -499,16 +506,17 @@
   margin-left: 5px;
   cursor: pointer;
 }
 
 .ruleview-selectorhighlighter:hover {
   filter: url(images/filters.svg#checked-icon-state);
 }
 
+.ruleview-grid.active,
 .ruleview-selectorhighlighter:active,
 .ruleview-selectorhighlighter.highlighted {
   filter: url(images/filters.svg#checked-icon-state) brightness(0.9);
 }
 
 #ruleview-add-rule-button::before {
   background-image: url("chrome://devtools/skin/images/add.svg");
   background-size: cover;
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -42,16 +42,22 @@
   /* The container for all highlighters doesn't react to pointer-events by
      default. This is because most highlighters cover the whole viewport but
      don't contain UIs that need to be accessed.
      If your highlighter has UI that needs to be interacted with, add
      'pointer-events:auto;' on its container element. */
   pointer-events: none;
 }
 
+:-moz-native-anonymous .highlighter-container.box-model {
+  /* Make the box-model container have a z-index other than auto so it always sits above
+     other highlighters. */
+  z-index: 1;
+}
+
 :-moz-native-anonymous .highlighter-container [hidden] {
   display: none;
 }
 
 :-moz-native-anonymous .highlighter-container [dragging] {
   cursor: grabbing;
 }
 
--- a/devtools/server/actors/highlighters/box-model.js
+++ b/devtools/server/actors/highlighters/box-model.js
@@ -95,28 +95,32 @@ function BoxModelHighlighter(highlighter
   this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
     this._buildMarkup.bind(this));
 
   /**
    * Optionally customize each region's fill color by adding an entry to the
    * regionFill property: `highlighter.regionFill.margin = "red";
    */
   this.regionFill = {};
+
+  this.onWillNavigate = this.onWillNavigate.bind(this);
+
+  this.highlighterEnv.on("will-navigate", this.onWillNavigate);
 }
 
 BoxModelHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
   typeName: "BoxModelHighlighter",
 
   ID_CLASS_PREFIX: "box-model-",
 
   _buildMarkup: function () {
     let doc = this.win.document;
 
     let highlighterContainer = doc.createElement("div");
-    highlighterContainer.className = "highlighter-container";
+    highlighterContainer.className = "highlighter-container box-model";
 
     // Build the root wrapper, used to adapt to the page zoom.
     let rootWrapper = createNode(this.win, {
       parent: highlighterContainer,
       attributes: {
         "id": "root",
         "class": "root"
       },
@@ -247,18 +251,19 @@ BoxModelHighlighter.prototype = extend(A
 
     return highlighterContainer;
   },
 
   /**
    * Destroy the nodes. Remove listeners.
    */
   destroy: function () {
+    this.highlighterEnv.off("will-navigate", this.onWillNavigate);
+    this.markup.destroy();
     AutoRefreshHighlighter.prototype.destroy.call(this);
-    this.markup.destroy();
   },
 
   getElement: function (id) {
     return this.markup.getElement(this.ID_CLASS_PREFIX + id);
   },
 
   /**
    * Override the AutoRefreshHighlighter's _isNodeValid method to also return true for
@@ -324,32 +329,32 @@ BoxModelHighlighter.prototype = extend(A
       }
       this._showBoxModel();
       shown = true;
     } else {
       // Nothing to highlight (0px rectangle like a <script> tag for instance)
       this._hide();
     }
 
-    setIgnoreLayoutChanges(false, this.currentNode.ownerDocument.documentElement);
+    setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
 
     return shown;
   },
 
   /**
    * Hide the highlighter, the outline and the infobar.
    */
   _hide: function () {
     setIgnoreLayoutChanges(true);
 
     this._untrackMutations();
     this._hideBoxModel();
     this._hideInfobar();
 
-    setIgnoreLayoutChanges(false, this.currentNode.ownerDocument.documentElement);
+    setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
   },
 
   /**
    * Hide the infobar
    */
   _hideInfobar: function () {
     this.getElement("infobar-container").setAttribute("hidden", "true");
   },
@@ -691,11 +696,17 @@ BoxModelHighlighter.prototype = extend(A
   /**
    * Move the Infobar to the right place in the highlighter.
    */
   _moveInfobar: function () {
     let bounds = this._getOuterBounds();
     let container = this.getElement("infobar-container");
 
     moveInfobar(container, bounds, this.win);
+  },
+
+  onWillNavigate: function ({ isTopLevel }) {
+    if (isTopLevel) {
+      this.hide();
+    }
   }
 });
 exports.BoxModelHighlighter = BoxModelHighlighter;
--- a/devtools/server/actors/highlighters/css-grid.js
+++ b/devtools/server/actors/highlighters/css-grid.js
@@ -95,17 +95,20 @@ const COLUMN_KEY = {};
  */
 function CssGridHighlighter(highlighterEnv) {
   AutoRefreshHighlighter.call(this, highlighterEnv);
 
   this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
     this._buildMarkup.bind(this));
 
   this.onNavigate = this.onNavigate.bind(this);
+  this.onWillNavigate = this.onWillNavigate.bind(this);
+
   this.highlighterEnv.on("navigate", this.onNavigate);
+  this.highlighterEnv.on("will-navigate", this.onWillNavigate);
 }
 
 CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
   typeName: "CssGridHighlighter",
 
   ID_CLASS_PREFIX: "css-grid-",
 
   _buildMarkup() {
@@ -207,16 +210,17 @@ CssGridHighlighter.prototype = extend(Au
       prefix: this.ID_CLASS_PREFIX
     });
 
     return container;
   },
 
   destroy() {
     this.highlighterEnv.off("navigate", this.onNavigate);
+    this.highlighterEnv.off("will-navigate", this.onWillNavigate);
     this.markup.destroy();
     AutoRefreshHighlighter.prototype.destroy.call(this);
   },
 
   getElement(id) {
     return this.markup.getElement(this.ID_CLASS_PREFIX + id);
   },
 
@@ -271,16 +275,22 @@ CssGridHighlighter.prototype = extend(Au
    * Called when the page navigates. Used to clear the cached gap patterns and avoid
    * using DeadWrapper objects as gap patterns the next time.
    */
   onNavigate() {
     gCachedGridPattern.delete(ROW_KEY);
     gCachedGridPattern.delete(COLUMN_KEY);
   },
 
+  onWillNavigate({ isTopLevel }) {
+    if (isTopLevel) {
+      this.hide();
+    }
+  },
+
   _show() {
     if (Services.prefs.getBoolPref(CSS_GRID_ENABLED_PREF) && !this.isGrid()) {
       this.hide();
       return false;
     }
 
     return this._update();
   },
@@ -360,17 +370,17 @@ CssGridHighlighter.prototype = extend(Au
     if (this.options.showAllGridAreas) {
       this.showAllGridAreas();
     } else if (this.options.showGridArea) {
       this.showGridArea(this.options.showGridArea);
     }
 
     this._showGrid();
 
-    setIgnoreLayoutChanges(false, this.currentNode.ownerDocument.documentElement);
+    setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
     return true;
   },
 
   /**
    * Update the grid information displayed in the grid info bar.
    *
    * @param  {GridArea} area
    *         The grid area object.
@@ -685,17 +695,17 @@ CssGridHighlighter.prototype = extend(Au
   /**
    * Hide the highlighter, the canvas and the infobar.
    */
   _hide() {
     setIgnoreLayoutChanges(true);
     this._hideGrid();
     this._hideGridArea();
     this._hideInfoBar();
-    setIgnoreLayoutChanges(false, this.currentNode.ownerDocument.documentElement);
+    setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
   },
 
   _hideGrid() {
     this.getElement("canvas").setAttribute("hidden", "true");
   },
 
   _showGrid() {
     this.getElement("canvas").removeAttribute("hidden");
--- a/devtools/server/actors/highlighters/css-transform.js
+++ b/devtools/server/actors/highlighters/css-transform.js
@@ -214,27 +214,27 @@ CssTransformHighlighter.prototype = exte
       this._setLinePoints(untransformedQuad["p" + nb], quad["p" + nb], "line" + nb);
     }
 
     // Adapt to the current zoom
     this.markup.scaleRootElement(this.currentNode, this.ID_CLASS_PREFIX + "root");
 
     this._showShapes();
 
-    setIgnoreLayoutChanges(false, this.currentNode.ownerDocument.documentElement);
+    setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
     return true;
   },
 
   /**
    * Hide the highlighter, the outline and the infobar.
    */
   _hide: function () {
     setIgnoreLayoutChanges(true);
     this._hideShapes();
-    setIgnoreLayoutChanges(false, this.currentNode.ownerDocument.documentElement);
+    setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
   },
 
   _hideShapes: function () {
     this.getElement("elements").setAttribute("hidden", "true");
   },
 
   _showShapes: function () {
     this.getElement("elements").removeAttribute("hidden");
--- a/devtools/shared/locales/en-US/styleinspector.properties
+++ b/devtools/shared/locales/en-US/styleinspector.properties
@@ -82,16 +82,20 @@ rule.bezierSwatch.tooltip=Click to open 
 # LOCALIZATION NOTE (rule.filterSwatch.tooltip): Text displayed in a tooltip
 # when the mouse is over a filter swatch in the rule view.
 rule.filterSwatch.tooltip=Click to open the filter editor
 
 # LOCALIZATION NOTE (rule.angleSwatch.tooltip): Text displayed in a tooltip
 # when the mouse is over a angle swatch in the rule view.
 rule.angleSwatch.tooltip=Shift+click to change the angle format
 
+# LOCALIZATION NOTE (rule.gridToggle.tooltip): Text displayed in a tooltip
+# when the mouse is over a CSS Grid toggle icon in the rule view.
+rule.gridToggle.tooltip=Click to toggle the CSS Grid highlighter
+
 # LOCALIZATION NOTE (styleinspector.contextmenu.copyColor): Text displayed in the rule
 # and computed view context menu when a color value was clicked.
 styleinspector.contextmenu.copyColor=Copy Color
 
 # LOCALIZATION NOTE (styleinspector.contextmenu.copyColor.accessKey): Access key for
 # the rule and computed view context menu "Copy Color" entry.
 styleinspector.contextmenu.copyColor.accessKey=L