Bug 1053898 - DevTools inspector support for shadowdom draft
authorJulian Descottes <jdescottes@mozilla.com>
Thu, 01 Feb 2018 22:03:24 +0100
changeset 760368 96242ede5d7b1e146ac05656b2eab923ea94e963
parent 760364 f4df3ec8c490fabc08686625895f49ceae89a151
child 760369 163a624966ca5369a9081afebf684f4c97d089cf
push id100613
push userjdescottes@mozilla.com
push dateTue, 27 Feb 2018 12:44:40 +0000
bugs1053898
milestone60.0a1
Bug 1053898 - DevTools inspector support for shadowdom MozReview-Commit-ID: L32QCenYeEK
devtools/client/framework/selection.js
devtools/client/inspector/inspector.js
devtools/client/inspector/markup/markup.js
devtools/client/inspector/markup/views/element-container.js
devtools/client/inspector/markup/views/markup-container.js
devtools/client/inspector/markup/views/moz.build
devtools/client/inspector/markup/views/read-only-editor.js
devtools/client/inspector/markup/views/root-container.js
devtools/client/inspector/markup/views/slotted-node-container.js
devtools/client/inspector/markup/views/slotted-node-editor.js
devtools/client/themes/markup.css
devtools/server/actors/highlighters/selector.js
devtools/server/actors/inspector/document-walker.js
devtools/server/actors/inspector/node.js
devtools/server/actors/inspector/walker.js
devtools/shared/fronts/inspector.js
devtools/shared/fronts/node.js
toolkit/modules/css-selector.js
--- a/devtools/client/framework/selection.js
+++ b/devtools/client/framework/selection.js
@@ -110,49 +110,50 @@ Selection.prototype = {
       this._walker.off("mutations", this._onMutations);
     }
     this._walker = walker;
     if (this._walker) {
       this._walker.on("mutations", this._onMutations);
     }
   },
 
-  setNodeFront: function (value, reason = "unknown") {
+  setNodeFront: function (value, reason = "unknown", slotted) {
     this.reason = reason;
 
     // If an inlineTextChild text node is being set, then set it's parent instead.
     let parentNode = value && value.parentNode();
     if (value && parentNode && parentNode.inlineTextChild === value) {
       value = parentNode;
     }
 
     this._nodeFront = value;
+    this._slotted = slotted;
     this.emit("new-node-front", value, this.reason);
   },
 
   get documentFront() {
     return this._walker.document(this._nodeFront);
   },
 
   get nodeFront() {
     return this._nodeFront;
   },
 
   isRoot: function () {
     return this.isNode() &&
            this.isConnected() &&
-           this._nodeFront.isDocumentElement;
+           this.nodeFront.isDocumentElement;
   },
 
   isNode: function () {
     return !!this._nodeFront;
   },
 
   isConnected: function () {
-    let node = this._nodeFront;
+    let node = this.nodeFront;
     if (!node || !node.actorID) {
       return false;
     }
 
     while (node) {
       if (node === this._walker.rootNode) {
         return true;
       }
@@ -240,9 +241,13 @@ Selection.prototype = {
   isDocumentFragmentNode: function () {
     return this.isNode() &&
       this.nodeFront.nodeType == nodeConstants.DOCUMENT_FRAGMENT_NODE;
   },
 
   isNotationNode: function () {
     return this.isNode() && this.nodeFront.nodeType == nodeConstants.NOTATION_NODE;
   },
+
+  isSlotted: function () {
+    return this._slotted;
+  }
 };
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -1148,40 +1148,40 @@ Inspector.prototype = {
    */
   onNewSelection: function (event, value, reason) {
     if (reason === "selection-destroy") {
       return;
     }
 
     // Wait for all the known tools to finish updating and then let the
     // client know.
-    let selection = this.selection.nodeFront;
+    let selectionNodeFront = this.selection.nodeFront;
 
     // Update the state of the add button in the toolbar depending on the
     // current selection.
     let btn = this.panelDoc.querySelector("#inspector-element-add-button");
     if (this.canAddHTMLChild()) {
       btn.removeAttribute("disabled");
     } else {
       btn.setAttribute("disabled", "true");
     }
 
     // On any new selection made by the user, store the unique css selector
     // of the selected node so it can be restored after reload of the same page
     if (this.canGetUniqueSelector &&
         this.selection.isElementNode()) {
-      selection.getUniqueSelector().then(selector => {
+      selectionNodeFront.getUniqueSelector().then(selector => {
         this.selectionCssSelector = selector;
       }, this._handleRejectionIfNotDestroyed);
     }
 
     let selfUpdate = this.updating("inspector-panel");
     executeSoon(() => {
       try {
-        selfUpdate(selection);
+        selfUpdate(selectionNodeFront);
       } catch (ex) {
         console.error(ex);
       }
     });
   },
 
   /**
    * Delay the "inspector-updated" notification while a tool
@@ -1359,16 +1359,21 @@ Inspector.prototype = {
     this._openMenu({
       screenX: e.screenX,
       screenY: e.screenY,
       target: e.target,
     });
   },
 
   _openMenu: function ({ target, screenX = 0, screenY = 0 } = { }) {
+    if (this.selection.isSlotted()) {
+      // There should not be any context menu for a slotted selection.
+      return null;
+    }
+
     let markupContainer = this.markup.getContainer(this.selection.nodeFront);
 
     this.contextMenuTarget = target;
     this.nodeMenuTriggerInfo = markupContainer &&
       markupContainer.editor.getInfoAtNode(target);
 
     let isSelectionElement = this.selection.isElementNode() &&
                              !this.selection.isPseudoElementNode();
--- a/devtools/client/inspector/markup/markup.js
+++ b/devtools/client/inspector/markup/markup.js
@@ -13,16 +13,17 @@ const EventEmitter = require("devtools/s
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const {PluralForm} = require("devtools/shared/plural-form");
 const AutocompletePopup = require("devtools/client/shared/autocomplete-popup");
 const KeyShortcuts = require("devtools/client/shared/key-shortcuts");
 const {scrollIntoViewIfNeeded} = require("devtools/client/shared/scroll");
 const {UndoStack} = require("devtools/client/shared/undo");
 const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
 const {PrefObserver} = require("devtools/client/shared/prefs");
+const SlottedNodeContainer = require("devtools/client/inspector/markup/views/slotted-node-container");
 const MarkupElementContainer = require("devtools/client/inspector/markup/views/element-container");
 const MarkupReadOnlyContainer = require("devtools/client/inspector/markup/views/read-only-container");
 const MarkupTextContainer = require("devtools/client/inspector/markup/views/text-container");
 const RootContainer = require("devtools/client/inspector/markup/views/root-container");
 
 const INSPECTOR_L10N =
       new LocalizationHelper("devtools/client/locales/inspector.properties");
 
@@ -83,16 +84,17 @@ function MarkupView(inspector, frame, co
     autoSelect: true,
     theme: "auto",
   });
 
   this.undo = new UndoStack();
   this.undo.installController(controllerWindow);
 
   this._containers = new Map();
+  this._slottedFronts = new WeakMap();
 
   // Binding functions that need to be called in scope.
   this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this);
   this._mutationObserver = this._mutationObserver.bind(this);
   this._onDisplayChange = this._onDisplayChange.bind(this);
   this._onMouseClick = this._onMouseClick.bind(this);
   this._onMouseUp = this._onMouseUp.bind(this);
   this._onNewSelection = this._onNewSelection.bind(this);
@@ -178,17 +180,17 @@ MarkupView.prototype = {
   },
 
   _disableImagePreviewTooltip: function () {
     this.imagePreviewTooltip.stopTogglingOnHover();
   },
 
   _onToolboxPickerHover: function (event, nodeFront) {
     this.showNode(nodeFront).then(() => {
-      this._showContainerAsHovered(nodeFront);
+      this._showNodeAsHovered(nodeFront);
     }, console.error);
   },
 
   /**
    * If the element picker gets canceled, make sure and re-center the view on the
    * current selected element.
    */
   _onToolboxPickerCanceled: function () {
@@ -218,20 +220,20 @@ MarkupView.prototype = {
     while (!target.container) {
       if (target.tagName.toLowerCase() === "body") {
         return;
       }
       target = target.parentNode;
     }
 
     let container = target.container;
-    if (this._hoveredNode !== container.node) {
+    if (this._hoveredContainer !== container) {
       this._showBoxModel(container.node);
     }
-    this._showContainerAsHovered(container.node);
+    this._showContainerAsHovered(container);
 
     this.emit("node-hover");
   },
 
   /**
    * If focus is moved outside of the markup view document and there is a
    * selected container, make its contents not focusable by a keyboard.
    */
@@ -320,21 +322,20 @@ MarkupView.prototype = {
     while (parentNode !== this.doc.body) {
       if (parentNode.container) {
         container = parentNode.container;
         break;
       }
       parentNode = parentNode.parentNode;
     }
 
-    if (container instanceof MarkupElementContainer) {
+    if (typeof container._onContainerClick === "function") {
       // With the newly found container, delegate the tooltip content creation
       // and decision to show or not the tooltip
-      container._buildEventTooltipContent(event.target,
-        this.eventDetailsTooltip);
+      container._onContainerClick(event, this.eventDetailsTooltip);
     }
   },
 
   _onMouseUp: function (event) {
     if (this._draggedContainer) {
       this._draggedContainer.onMouseUp(event);
     }
 
@@ -367,35 +368,40 @@ MarkupView.prototype = {
 
     this.indicateDropTarget(null);
     this.indicateDragTarget(null);
     if (this._autoScrollAnimationFrame) {
       this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
     }
   },
 
-  _hoveredNode: null,
+  _hoveredContainer: null,
+
+  _showNodeAsHovered: function (nodeFront) {
+    let container = this.getContainer(nodeFront);
+    this._showContainerAsHovered(container);
+  },
 
   /**
    * Show a NodeFront's container as being hovered
    *
    * @param  {NodeFront} nodeFront
    *         The node to show as hovered
    */
-  _showContainerAsHovered: function (nodeFront) {
-    if (this._hoveredNode === nodeFront) {
+  _showContainerAsHovered: function (container) {
+    if (this._hoveredContainer === container) {
       return;
     }
 
-    if (this._hoveredNode) {
-      this.getContainer(this._hoveredNode).hovered = false;
+    if (this._hoveredContainer) {
+      this._hoveredContainer.hovered = false;
     }
 
-    this.getContainer(nodeFront).hovered = true;
-    this._hoveredNode = nodeFront;
+    container.hovered = true;
+    this._hoveredContainer = container;
     // Emit an event that the container view is actually hovered now, as this function
     // can be called by an asynchronous caller.
     this.emit("showcontainerhovered");
   },
 
   _onMouseOut: function (event) {
     // Emulate mouseleave by skipping any relatedTarget inside the markup-view.
     if (this._elt.contains(event.relatedTarget)) {
@@ -405,20 +411,20 @@ MarkupView.prototype = {
     if (this._autoScrollAnimationFrame) {
       this.win.cancelAnimationFrame(this._autoScrollAnimationFrame);
     }
     if (this.isDragging) {
       return;
     }
 
     this._hideBoxModel(true);
-    if (this._hoveredNode) {
-      this.getContainer(this._hoveredNode).hovered = false;
+    if (this._hoveredContainer) {
+      this._hoveredContainer.hovered = false;
     }
-    this._hoveredNode = null;
+    this._hoveredContainer = null;
 
     this.emit("leave");
   },
 
   /**
    * Show the box model highlighter on a given node front
    *
    * @param  {NodeFront} nodeFront
@@ -468,22 +474,37 @@ MarkupView.prototype = {
         this._hideBoxModel().then(resolve, resolve);
       }, NEW_SELECTION_HIGHLIGHTER_TIMER);
     });
     this._briefBoxModelPromise.resolve = _resolve;
 
     return promise.all([onShown, this._briefBoxModelPromise]);
   },
 
+  getSelectedContainer: function () {
+    return this._selectedContainer;
+  },
+
   /**
    * Get the MarkupContainer object for a given node, or undefined if
    * none exists.
    */
-  getContainer: function (node) {
-    return this._containers.get(node);
+  getContainer: function (node, slotted) {
+    let key = this.getContainerKey(node, slotted);
+    return this._containers.get(key);
+  },
+
+  setContainer: function (node, container, slotted) {
+    let key = this.getContainerKey(node, slotted);
+    return this._containers.set(key, container);
+  },
+
+  hasContainer: function (node, slotted) {
+    let key = this.getContainerKey(node, slotted);
+    return this._containers.has(key);
   },
 
   update: function () {
     let updateChildren = (node) => {
       this.getContainer(node).update();
       for (let child of node.treeChildren()) {
         updateChildren(child);
       }
@@ -554,66 +575,76 @@ MarkupView.prototype = {
   _shouldNewSelectionBeHighlighted: function () {
     let reason = this.inspector.selection.reason;
     let unwantedReasons = [
       "inspector-open",
       "navigateaway",
       "nodeselected",
       "test"
     ];
-    let isHighlight = this._hoveredNode === this.inspector.selection.nodeFront;
+    let isHighlight = this._isContainerSelected(this._hoveredContainer);
     return !isHighlight && reason && !unwantedReasons.includes(reason);
   },
 
   /**
    * React to new-node-front selection events.
    * Highlights the node if needed, and make sure it is shown and selected in
    * the view.
    */
   _onNewSelection: function () {
-    let selection = this.inspector.selection;
-
     if (this.htmlEditor) {
       this.htmlEditor.hide();
     }
-    if (this._hoveredNode && this._hoveredNode !== selection.nodeFront) {
-      this.getContainer(this._hoveredNode).hovered = false;
-      this._hoveredNode = null;
+    let selection = this.inspector.selection;
+    if (this._hoveredContainer && !this._isContainerSelected(this._hoveredContainer)) {
+      this._hoveredContainer.hovered = false;
+      this._hoveredContainer = null;
     }
 
     if (!selection.isNode()) {
       this.unmarkSelectedNode();
       return;
     }
 
     let done = this.inspector.updating("markup-view");
     let onShowBoxModel, onShow;
 
     // Highlight the element briefly if needed.
     if (this._shouldNewSelectionBeHighlighted()) {
       onShowBoxModel = this._brieflyShowBoxModel(selection.nodeFront);
     }
 
-    onShow = this.showNode(selection.nodeFront).then(() => {
+    let slotted = this.inspector.selection.isSlotted();
+    onShow = this.showNode(selection.nodeFront, { centered: true, slotted }).then(() => {
       // We could be destroyed by now.
       if (this._destroyer) {
         return promise.reject("markupview destroyed");
       }
 
       // Mark the node as selected.
-      this.markNodeAsSelected(selection.nodeFront);
+      this.markNodeAsSelected(selection.nodeFront, { slotted });
 
       // Make sure the new selection is navigated to.
       this.maybeNavigateToNewSelection();
       return undefined;
     }).catch(this._handleRejectionIfNotDestroyed);
 
     promise.all([onShowBoxModel, onShow]).then(done);
   },
 
+  _isContainerSelected: function (container) {
+    if (!container) {
+      return false;
+    }
+
+    let selection = this.inspector.selection;
+    return container.node == selection.nodeFront &&
+           container.isSlotted() == selection.isSlotted();
+  },
+
   /**
    * Maybe make selected the current node selection's MarkupContainer depending
    * on why the current node got selected.
    */
   maybeNavigateToNewSelection: function () {
     let {reason, nodeFront} = this.inspector.selection;
 
     // The list of reasons that should lead to navigating to the node.
@@ -941,57 +972,84 @@ MarkupView.prototype = {
    *         The container we're navigating to.
    */
   navigate: function (container) {
     if (!container) {
       return;
     }
 
     let node = container.node;
-    this.markNodeAsSelected(node, "treepanel");
+    // Slotted containers do not display any children.
+    let slotted = container.isSlotted();
+    this.markNodeAsSelected(node, { reason: "treepanel", slotted });
+  },
+
+  getContainerKey: function (nodeFront, slotted) {
+    if (!slotted) {
+      return nodeFront;
+    }
+
+    if (!this._slottedFronts.has(nodeFront)) {
+      this._slottedFronts.set(nodeFront, {nodeFront});
+    }
+    return this._slottedFronts.get(nodeFront);
   },
 
   /**
    * Make sure a node is included in the markup tool.
    *
    * @param  {NodeFront} node
    *         The node in the content document.
-   * @param  {Boolean} flashNode
-   *         Whether the newly imported node should be flashed
+   * @param  {Object}
+   *         - {Boolean} flashNode: whether the newly imported node should be flashed
+   *         - {MarkupContainer} parentContainer: the parent container for the imported
+   *           node
    * @return {MarkupContainer} The MarkupContainer object for this element.
    */
-  importNode: function (node, flashNode) {
+  importNode: function (node, {flashNode, parentContainer} = {}) {
     if (!node) {
       return null;
     }
 
-    if (this._containers.has(node)) {
-      return this.getContainer(node);
+    let {nodeType, isPseudoElement} = node;
+
+    let slotted = false;
+    if (parentContainer &&
+        node.parentNode() &&
+        node.parentNode().actorID !== parentContainer.node.actorID &&
+        nodeType == nodeConstants.ELEMENT_NODE &&
+        parentContainer.node.nodeType == nodeConstants.ELEMENT_NODE) {
+      slotted = true;
+    }
+
+    if (this.hasContainer(node, slotted)) {
+      return this.getContainer(node, slotted);
     }
 
     let container;
-    let {nodeType, isPseudoElement} = node;
     if (node === this.walker.rootNode) {
       container = new RootContainer(this, node);
       this._elt.appendChild(container.elt);
       this._rootNode = node;
+    } else if (slotted) {
+      container = new SlottedNodeContainer(this, node, this.inspector);
     } else if (nodeType == nodeConstants.ELEMENT_NODE && !isPseudoElement) {
       container = new MarkupElementContainer(this, node, this.inspector);
     } else if (nodeType == nodeConstants.COMMENT_NODE ||
                nodeType == nodeConstants.TEXT_NODE) {
       container = new MarkupTextContainer(this, node, this.inspector);
     } else {
       container = new MarkupReadOnlyContainer(this, node, this.inspector);
     }
 
     if (flashNode) {
       container.flashMutation();
     }
 
-    this._containers.set(node, container);
+    this.setContainer(node, container, slotted);
     container.childrenDirty = true;
 
     this._updateChildren(container);
 
     this.inspector.emit("container-created", container);
 
     return container;
   },
@@ -1019,17 +1077,19 @@ MarkupView.prototype = {
         // Container might not exist if this came from a load event for a node
         // we're not viewing.
         continue;
       }
 
       if (type === "attributes" || type === "characterData"
         || type === "events" || type === "pseudoClassLock") {
         container.update();
-      } else if (type === "childList" || type === "nativeAnonymousChildList") {
+      } else if (type === "childList" ||
+                 type === "nativeAnonymousChildList" ||
+                 type === "slotchange") {
         container.childrenDirty = true;
         // Update the children to take care of changes in the markup view DOM
         // and update container (and its subtree) DOM tree depth level for
         // accessibility where necessary.
         this._updateChildren(container, {flash: true}).then(() =>
           container.updateLevel());
       } else if (type === "inlineTextChild") {
         container.childrenDirty = true;
@@ -1119,34 +1179,41 @@ MarkupView.prototype = {
     for (let container of addedOrEditedContainers) {
       container.flashMutation();
     }
   },
 
   /**
    * Make sure the given node's parents are expanded and the
    * node is scrolled on to screen.
+   *
+   * @param {NodeFront} node
+   *        The node to show
+   * @param {Object} options
+   *        - {Boolean} centered: should the node be centered
+   *        - {Boolean} slotted: show the slotted (shadowdom) container of the node
    */
-  showNode: function (node, centered = true) {
+  showNode: function (node, { centered = true, slotted } = {}) {
     let parent = node;
 
     this.importNode(node);
 
     while ((parent = parent.parentNode())) {
       this.importNode(parent);
       this.expandNode(parent);
     }
 
     return this._waitForChildren().then(() => {
       if (this._destroyer) {
         return promise.reject("markupview destroyed");
       }
       return this._ensureVisible(node);
     }).then(() => {
-      scrollIntoViewIfNeeded(this.getContainer(node).editor.elt, centered);
+      let container = this.getContainer(node, slotted);
+      scrollIntoViewIfNeeded(container.editor.elt, centered);
     }, this._handleRejectionIfNotDestroyed);
   },
 
   /**
    * Expand the container's children.
    */
   _expandContainer: function (container) {
     return this._updateChildren(container, {expand: true}).then(() => {
@@ -1302,17 +1369,17 @@ MarkupView.prototype = {
       this._removedNodeObserver = null;
 
       // Don't select the new node if the user has already changed the current
       // selection.
       if (this.inspector.selection.nodeFront === parentContainer.node ||
           (this.inspector.selection.nodeFront === removedNode && isHTMLTag)) {
         let childContainers = parentContainer.getChildContainers();
         if (childContainers && childContainers[childIndex]) {
-          this.markNodeAsSelected(childContainers[childIndex].node, reason);
+          this.markNodeAsSelected(childContainers[childIndex].node, { reason });
           if (childContainers[childIndex].hasChildren) {
             this.expandNode(childContainers[childIndex].node);
           }
           this.emit("reselectedonremoved");
         }
       }
     };
 
@@ -1480,23 +1547,24 @@ MarkupView.prototype = {
   },
 
   /**
    * Mark the given node selected, and update the inspector.selection
    * object's NodeFront to keep consistent state between UI and selection.
    *
    * @param  {NodeFront} aNode
    *         The NodeFront to mark as selected.
-   * @param  {String} reason
-   *         The reason for marking the node as selected.
+   * @param  {Object} options
+   *         - {String} reason: the reason for marking the node as selected.
+   *         - {Boolean} slotted: mark the slotted container for the node as selected.
    * @return {Boolean} False if the node is already marked as selected, true
    *         otherwise.
    */
-  markNodeAsSelected: function (node, reason) {
-    let container = this.getContainer(node);
+  markNodeAsSelected: function (node, { reason, slotted } = {}) {
+    let container = this.getContainer(node, slotted);
 
     if (this._selectedContainer === container) {
       return false;
     }
 
     // Un-select and remove focus from the previous container.
     if (this._selectedContainer) {
       this._selectedContainer.selected = false;
@@ -1506,17 +1574,17 @@ MarkupView.prototype = {
     // Select the new container.
     this._selectedContainer = container;
     if (node) {
       this._selectedContainer.selected = true;
     }
 
     // Change the current selection if needed.
     if (this.inspector.selection.nodeFront !== node) {
-      this.inspector.selection.setNodeFront(node, reason || "nodeselected");
+      this.inspector.selection.setNodeFront(node, reason || "nodeselected", slotted);
     }
 
     return true;
   },
 
   /**
    * Make sure that every ancestor of the selection are updated
    * and included in the list of visible children.
@@ -1586,16 +1654,22 @@ MarkupView.prototype = {
    * @param  {MarkupContainer} container
    *         The markup container whose children need updating
    * @param  {Object} options
    *         Options are {expand:boolean,flash:boolean}
    * @return {Promise} that will be resolved when the children are ready
    *         (which may be immediately).
    */
   _updateChildren: function (container, options) {
+    // Slotted containers do not display any children.
+    if (container.isSlotted()) {
+      container.hasChildren = false;
+      return promise.resolve(container);
+    }
+
     let expand = options && options.expand;
     let flash = options && options.flash;
 
     container.hasChildren = container.node.hasChildren;
     // Accessibility should either ignore empty children or semantically
     // consider them a group.
     container.setChildrenRole();
 
@@ -1673,17 +1747,20 @@ MarkupView.prototype = {
         // while the request was in progress, we need to do it again.
         if (container.childrenDirty) {
           return this._updateChildren(container, {expand: centered || expand});
         }
 
         let fragment = this.doc.createDocumentFragment();
 
         for (let child of children.nodes) {
-          let childContainer = this.importNode(child, flash);
+          let childContainer = this.importNode(child, {
+            flashNode: flash,
+            parentContainer: container
+          });
           fragment.appendChild(childContainer.elt);
         }
 
         while (container.children.firstChild) {
           container.children.firstChild.remove();
         }
 
         if (!children.hasFirst) {
@@ -1756,17 +1833,17 @@ MarkupView.prototype = {
     if (this._destroyer) {
       return this._destroyer;
     }
 
     this._destroyer = promise.resolve();
 
     this._clearBriefBoxModelTimer();
 
-    this._hoveredNode = null;
+    this._hoveredContainer = null;
 
     if (this.htmlEditor) {
       this.htmlEditor.destroy();
       this.htmlEditor = null;
     }
 
     this.undo.destroy();
     this.undo = null;
--- a/devtools/client/inspector/markup/views/element-container.js
+++ b/devtools/client/inspector/markup/views/element-container.js
@@ -12,17 +12,17 @@ const {Task} = require("devtools/shared/
 const nodeConstants = require("devtools/shared/dom-node-constants");
 const clipboardHelper = require("devtools/shared/platform/clipboard");
 const {setImageTooltip, setBrokenImageTooltip} =
       require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
 const MarkupContainer = require("devtools/client/inspector/markup/views/markup-container");
 const ElementEditor = require("devtools/client/inspector/markup/views/element-editor");
 const {extend} = require("devtools/shared/extend");
 
-// Lazy load this module as _buildEventTooltipContent is only called on click
+// Lazy load this module as _onContainerClick is only called on click
 loader.lazyRequireGetter(this, "setEventTooltip",
   "devtools/client/shared/widgets/tooltip/EventTooltipHelper", true);
 
 /**
  * An implementation of MarkupContainer for Elements that can contain
  * child nodes.
  * Allows editing of tag name, attributes, expanding / collapsing.
  *
@@ -40,17 +40,18 @@ function MarkupElementContainer(markupVi
   } else {
     throw new Error("Invalid node for MarkupElementContainer");
   }
 
   this.tagLine.appendChild(this.editor.elt);
 }
 
 MarkupElementContainer.prototype = extend(MarkupContainer.prototype, {
-  _buildEventTooltipContent: Task.async(function* (target, tooltip) {
+  _onContainerClick: Task.async(function* (event, tooltip) {
+    let target = event.target;
     if (target.dataset.event) {
       yield tooltip.hide();
 
       let listenerInfo = yield this.node.getEventListenerInfo();
 
       let toolbox = this.markup.toolbox;
 
       setEventTooltip(tooltip, listenerInfo, toolbox);
--- a/devtools/client/inspector/markup/views/markup-container.js
+++ b/devtools/client/inspector/markup/views/markup-container.js
@@ -213,17 +213,17 @@ MarkupContainer.prototype = {
       doc.activeElement.blur();
     }
   },
 
   /**
    * True if the current node can be expanded.
    */
   get canExpand() {
-    return this._hasChildren && !this.node.inlineTextChild;
+    return this.hasChildren && !this.node.inlineTextChild;
   },
 
   /**
    * True if this is the root <html> element and can't be collapsed.
    */
   get mustExpand() {
     return this.node._parent === this.markup.walker.rootNode;
   },
@@ -739,23 +739,38 @@ MarkupContainer.prototype = {
   _onToggle: function (event) {
     // Prevent the html tree from expanding when an event bubble or display node is
     // clicked.
     if (event.target.dataset.event || event.target.dataset.display) {
       event.stopPropagation();
       return;
     }
 
+    if (event.target.classList.contains("reveal-link")) {
+      event.stopPropagation();
+      return;
+    }
+
     this.markup.navigate(this);
     if (this.hasChildren) {
       this.markup.setNodeExpanded(this.node, !this.expanded, event.altKey);
     }
     event.stopPropagation();
   },
 
+  /*
+   * Check if this container represents a slotted elemented (ie. a DOM node inserted in a
+   * shadow DOM <slot> element).
+   *
+   * By default containers are not slotted and represent real elements.
+   */
+  isSlotted: function () {
+    return false;
+  },
+
   /**
    * Get rid of event listeners and references, when the container is no longer
    * needed
    */
   destroy: function () {
     // Remove event listeners
     this.elt.removeEventListener("mousedown", this._onMouseDown);
     this.elt.removeEventListener("dblclick", this._onToggle);
--- a/devtools/client/inspector/markup/views/moz.build
+++ b/devtools/client/inspector/markup/views/moz.build
@@ -7,11 +7,13 @@
 DevToolsModules(
     'element-container.js',
     'element-editor.js',
     'html-editor.js',
     'markup-container.js',
     'read-only-container.js',
     'read-only-editor.js',
     'root-container.js',
+    'slotted-node-container.js',
+    'slotted-node-editor.js',
     'text-container.js',
     'text-editor.js',
 )
--- a/devtools/client/inspector/markup/views/read-only-editor.js
+++ b/devtools/client/inspector/markup/views/read-only-editor.js
@@ -15,16 +15,18 @@ function ReadOnlyEditor(container, node)
   this.buildMarkup();
 
   if (node.isPseudoElement) {
     this.tag.classList.add("theme-fg-color5");
     this.tag.textContent = node.isBeforePseudoElement ? "::before" : "::after";
   } else if (node.nodeType == nodeConstants.DOCUMENT_TYPE_NODE) {
     this.elt.classList.add("comment", "doctype");
     this.tag.textContent = node.doctypeString;
+  } else if (node.isShadowRoot) {
+    this.tag.textContent = "#shadow-root";
   } else {
     this.tag.textContent = node.nodeName;
   }
 
   // Make the "tag" part of this editor focusable.
   this.tag.setAttribute("tabindex", "-1");
 }
 
--- a/devtools/client/inspector/markup/views/root-container.js
+++ b/devtools/client/inspector/markup/views/root-container.js
@@ -44,12 +44,16 @@ RootContainer.prototype = {
   /**
    * Set an appropriate role of the container's children node.
    */
   setChildrenRole: function () {},
 
   /**
    * Set an appropriate DOM tree depth level for a node and its subtree.
    */
-  updateLevel: function () {}
+  updateLevel: function () {},
+
+  isSlotted: function () {
+    return false;
+  }
 };
 
 module.exports = RootContainer;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/views/slotted-node-container.js
@@ -0,0 +1,55 @@
+/* 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 SlottedNodeEditor = require("devtools/client/inspector/markup/views/slotted-node-editor");
+const MarkupContainer = require("devtools/client/inspector/markup/views/markup-container");
+const {extend} = require("devtools/shared/extend");
+
+/**
+ * An implementation of MarkupContainer for Pseudo Elements,
+ * Doctype nodes, or any other type generic node that doesn't
+ * fit for other editors.
+ * Does not allow any editing, just viewing / selecting.
+ *
+ * @param  {MarkupView} markupView
+ *         The markup view that owns this container.
+ * @param  {NodeFront} node
+ *         The node to display.
+ */
+function SlottedNodeContainer(markupView, node) {
+  MarkupContainer.prototype.initialize.call(this, markupView, node,
+    "readonlycontainer");
+
+  this.editor = new SlottedNodeEditor(this, node);
+  this.tagLine.appendChild(this.editor.elt);
+}
+
+SlottedNodeContainer.prototype = extend(MarkupContainer.prototype, {
+  _onMouseDown: function (event) {
+    if (event.target.classList.contains("reveal-link")) {
+      event.stopPropagation();
+      event.preventDefault();
+      return;
+    }
+    MarkupContainer.prototype._onMouseDown.call(this, event);
+  },
+
+  _onContainerClick: async function (event) {
+    if (event.target.classList.contains("reveal-link")) {
+      this.markup.inspector.selection.setNodeFront(this.node);
+    }
+  },
+
+  isDraggable: function () {
+    return false;
+  },
+
+  isSlotted: function () {
+    return true;
+  }
+});
+
+module.exports = SlottedNodeContainer;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/markup/views/slotted-node-editor.js
@@ -0,0 +1,56 @@
+/* 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";
+
+/**
+ * Creates an editor for non-editable nodes.
+ */
+function ReadOnlyEditor(container, node) {
+  this.container = container;
+  this.markup = this.container.markup;
+  this.buildMarkup();
+  this.tag.textContent = "→ <" + node.nodeName.toLowerCase() + ">";
+
+  // Make the "tag" part of this editor focusable.
+  this.tag.setAttribute("tabindex", "-1");
+}
+
+ReadOnlyEditor.prototype = {
+  buildMarkup: function () {
+    let doc = this.markup.doc;
+
+    this.elt = doc.createElement("span");
+    this.elt.classList.add("editor");
+
+    this.tag = doc.createElement("span");
+    this.tag.classList.add("tag");
+    this.elt.appendChild(this.tag);
+
+    this.revealLink = doc.createElement("span");
+    this.revealLink.classList.add("reveal-link");
+    this.revealLink.textContent = "reveal";
+    this.elt.appendChild(this.revealLink);
+  },
+
+  destroy: function () {
+    // We might be already destroyed.
+    if (!this.elt) {
+      return;
+    }
+
+    this.elt.remove();
+    this.elt = null;
+    this.tag = null;
+  },
+
+  /**
+   * Stub method for consistency with ElementEditor.
+   */
+  getInfoAtNode: function () {
+    return null;
+  }
+};
+
+module.exports = ReadOnlyEditor;
--- a/devtools/client/themes/markup.css
+++ b/devtools/client/themes/markup.css
@@ -316,16 +316,30 @@ ul.children + .tag-line::before {
 .more-nodes {
   padding-left: 16px;
 }
 
 .styleinspector-propertyeditor {
   border: 1px solid #CCC;
 }
 
+.reveal-link {
+  margin-inline-start: 10px;
+  cursor: pointer;
+  display: none;
+}
+
+.reveal-link:hover {
+  text-decoration: underline
+}
+
+.tag-line:hover .reveal-link {
+  display: inline;
+}
+
 /* Draw a circle next to nodes that have a pseudo class lock.
    Center vertically with the 1.4em line height on .tag-line */
 .child.pseudoclass-locked::before {
   content: "";
   background: var(--theme-highlight-lightorange);
   border-radius: 50%;
   width: .8em;
   height: .8em;
--- a/devtools/server/actors/highlighters/selector.js
+++ b/devtools/server/actors/highlighters/selector.js
@@ -35,19 +35,21 @@ SelectorHighlighter.prototype = {
    */
   show: function (node, options = {}) {
     this.hide();
 
     if (!isNodeValid(node) || !options.selector) {
       return false;
     }
 
+    let document = node.getRootNode();
+
     let nodes = [];
     try {
-      nodes = [...node.ownerDocument.querySelectorAll(options.selector)];
+      nodes = [...document.querySelectorAll(options.selector)];
     } catch (e) {
       // It's fine if the provided selector is invalid, nodes will be an empty
       // array.
     }
 
     delete options.selector;
 
     let i = 0;
--- a/devtools/server/actors/inspector/document-walker.js
+++ b/devtools/server/actors/inspector/document-walker.js
@@ -44,21 +44,22 @@ function DocumentWalker(node, rootWin,
     showAnonymousContent = true
   }) {
   if (Cu.isDeadWrapper(rootWin) || !rootWin.location) {
     throw new Error("Got an invalid root window in DocumentWalker");
   }
 
   this.walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"]
     .createInstance(Ci.inIDeepTreeWalker);
-  this.walker.showAnonymousContent = true;
+  this.walker.showAnonymousContent = showAnonymousContent;
   this.walker.showSubDocuments = true;
   this.walker.showDocumentsAsNodes = true;
   this.walker.init(rootWin.document, whatToShow);
   this.filter = filter;
+  this.skipTo = skipTo;
 
   // Make sure that the walker knows about the initial node (which could
   // be skipped due to a filter).
   this.walker.currentNode = this.getStartingNode(node, skipTo);
 }
 
 DocumentWalker.prototype = {
 
@@ -67,16 +68,20 @@ DocumentWalker.prototype = {
   },
   get currentNode() {
     return this.walker.currentNode;
   },
   set currentNode(val) {
     this.walker.currentNode = val;
   },
 
+  setStartingNode: function (node) {
+    this.walker.currentNode = this.getStartingNode(node, this.skipTo);
+  },
+
   parentNode: function () {
     return this.walker.parentNode();
   },
 
   nextNode: function () {
     let node = this.walker.currentNode;
     if (!node) {
       return null;
--- a/devtools/server/actors/inspector/node.js
+++ b/devtools/server/actors/inspector/node.js
@@ -33,20 +33,24 @@ const EventEmitter = require("devtools/s
 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
 const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
 const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;
 
 /**
  * Server side of the node actor.
  */
 const NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
-  initialize: function (walker, node) {
+  initialize: function (walker, node, lightNodeActor) {
     protocol.Actor.prototype.initialize.call(this, null);
     this.walker = walker;
     this.rawNode = node;
+
+    this.isSlottedNode = !!lightNodeActor;
+    this.lightNodeActor = lightNodeActor;
+
     this._eventParsers = new EventParsers().parsers;
 
     // Store the original display type and whether or not the node is displayed to
     // track changes when reflows occur.
     this.currentDisplayType = this.displayType;
     this.wasDisplayed = this.isDisplayed;
   },
 
@@ -72,16 +76,23 @@ const NodeActor = protocol.ActorClassWit
     protocol.Actor.prototype.destroy.call(this);
 
     if (this.mutationObserver) {
       if (!Cu.isDeadWrapper(this.mutationObserver)) {
         this.mutationObserver.disconnect();
       }
       this.mutationObserver = null;
     }
+
+    if (this.slotchangeListener) {
+      if (!Cu.isDeadWrapper(this.rawNode)) {
+        this.rawNode.removeEventListener("slotchange", this.slotchangeListener);
+      }
+    }
+
     this.rawNode = null;
     this.walker = null;
   },
 
   // Returns the JSON representation of this object over the wire.
   form: function (detail) {
     if (detail === "actorid") {
       return this.actorID;
@@ -107,25 +118,29 @@ const NodeActor = protocol.ActorClassWit
       name: this.rawNode.name,
       publicId: this.rawNode.publicId,
       systemId: this.rawNode.systemId,
 
       attrs: this.writeAttrs(),
       isBeforePseudoElement: this.isBeforePseudoElement,
       isAfterPseudoElement: this.isAfterPseudoElement,
       isAnonymous: isAnonymous(this.rawNode),
+      isShadowRoot: this.isShadowRoot,
+      isSlottedNode: this.isSlottedNode,
       isNativeAnonymous: isNativeAnonymous(this.rawNode),
       isXBLAnonymous: isXBLAnonymous(this.rawNode),
       isShadowAnonymous: isShadowAnonymous(this.rawNode),
       pseudoClassLocks: this.writePseudoClassLocks(),
 
       isDisplayed: this.isDisplayed,
       isInHTMLDocument: this.rawNode.ownerDocument &&
         this.rawNode.ownerDocument.contentType === "text/html",
       hasEventListeners: this._hasEventListeners,
+
+      lightNodeActor: this.isSlottedNode ? this.lightNodeActor.actorID : null
     };
 
     if (this.isDocumentElement()) {
       form.isDocumentElement = true;
     }
 
     // Add an extra API for custom properties added by other
     // modules/extensions.
@@ -145,47 +160,66 @@ const NodeActor = protocol.ActorClassWit
 
     return form;
   },
 
   /**
    * Watch the given document node for mutations using the DOM observer
    * API.
    */
-  watchDocument: function (callback) {
+  watchDocument: function (doc, callback) {
     let node = this.rawNode;
     // Create the observer on the node's actor.  The node will make sure
     // the observer is cleaned up when the actor is released.
-    let observer = new node.defaultView.MutationObserver(callback);
+    let observer = new doc.defaultView.MutationObserver(callback);
     observer.mergeAttributeRecords = true;
     observer.observe(node, {
       nativeAnonymousChildList: true,
       attributes: true,
       characterData: true,
       characterDataOldValue: true,
       childList: true,
       subtree: true
     });
     this.mutationObserver = observer;
   },
 
+  watchSlotchange: function (callback) {
+    let node = this.rawNode;
+    node.addEventListener("slotchange", callback);
+    this.slotchangeListener = callback;
+  },
+
   get isBeforePseudoElement() {
     return this.rawNode.nodeName === "_moz_generated_content_before";
   },
 
   get isAfterPseudoElement() {
     return this.rawNode.nodeName === "_moz_generated_content_after";
   },
 
+  get isShadowRoot() {
+    let isFragment = this.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE;
+    return isFragment && this.rawNode.host;
+  },
+
+  get isLightDOMChild() {
+    let parentNode = this.rawNode.parentNode;
+    let parentIsShadowHost = parentNode && parentNode.shadowRoot;
+    // Pseudo elements are not slotted.
+    let isPseudo = this.isBeforePseudoElement || this.isAfterPseudoElement;
+    return parentIsShadowHost && !isPseudo;
+  },
+
   // Estimate the number of children that the walker will return without making
   // a call to children() if possible.
   get numChildren() {
     // For pseudo elements, childNodes.length returns 1, but the walker
     // will return 0.
-    if (this.isBeforePseudoElement || this.isAfterPseudoElement) {
+    if (this.isBeforePseudoElement || this.isAfterPseudoElement || this.isSlottedNode) {
       return 0;
     }
 
     let rawNode = this.rawNode;
     let numChildren = rawNode.childNodes.length;
     let hasAnonChildren = rawNode.nodeType === Ci.nsIDOMNode.ELEMENT_NODE &&
                           rawNode.ownerDocument.getAnonymousNodes(rawNode);
 
--- a/devtools/server/actors/inspector/walker.js
+++ b/devtools/server/actors/inspector/walker.js
@@ -134,16 +134,17 @@ var WalkerActor = protocol.ActorClassWit
     this._orphaned = new Set();
 
     // The client can tell the walker that it is interested in a node
     // even when it is orphaned with the `retainNode` method.  This
     // list contains orphaned nodes that were so retained.
     this._retainedOrphans = new Set();
 
     this.onMutations = this.onMutations.bind(this);
+    this.onSlotchange = this.onSlotchange.bind(this);
     this.onFrameLoad = this.onFrameLoad.bind(this);
     this.onFrameUnload = this.onFrameUnload.bind(this);
     this._throttledEmitNewMutations = throttle(this._emitNewMutations.bind(this),
       MUTATIONS_THROTTLING_DELAY);
 
     tabActor.on("will-navigate", this.onFrameUnload);
     tabActor.on("window-ready", this.onFrameLoad);
 
@@ -293,18 +294,24 @@ var WalkerActor = protocol.ActorClassWit
     actor = new NodeActor(this, node);
 
     // Add the node actor as a child of this walker actor, assigning
     // it an actorID.
     this.manage(actor);
     this._refMap.set(node, actor);
 
     if (node.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
-      actor.watchDocument(this.onMutations);
+      actor.watchDocument(node, this.onMutations);
     }
+
+    if (node.nodeType === Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE && node.host) {
+      actor.watchDocument(node.host.ownerDocument, this.onMutations);
+      actor.watchSlotchange(this.onSlotchange);
+    }
+
     return actor;
   },
 
   _onReflows: function (reflows) {
     // Going through the nodes the walker knows about, see which ones have
     // had their display changed and send a display-change event if any
     let changes = [];
     for (let [node, actor] of this._refMap) {
@@ -443,33 +450,39 @@ var WalkerActor = protocol.ActorClassWit
       }
 
       parents.push(this._ref(cur));
     }
     return parents;
   },
 
   parentNode: function (node) {
-    let walker = this.getDocumentWalker(node.rawNode);
+    // If the node is a slotted light DOM child, we can not use an anonymous walker to get
+    // the shadow host parent.
+    let showAnonymousContent = !node.isLightDOMChild;
+    let walker = this.getDocumentWalker(node.rawNode, { showAnonymousContent });
+
     let parent = walker.parentNode();
     if (parent) {
       return this._ref(parent);
     }
+
     return null;
   },
 
   /**
    * If the given NodeActor only has a single text node as a child with a text
    * content small enough to be inlined, return that child's NodeActor.
    *
    * @param NodeActor node
    */
   inlineTextChild: function (node) {
     // Quick checks to prevent creating a new walker if possible.
-    if (node.isBeforePseudoElement ||
+    if (node.isSlottedNode ||
+        node.isBeforePseudoElement ||
         node.isAfterPseudoElement ||
         node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE ||
         node.rawNode.children.length > 0) {
       return undefined;
     }
 
     let docWalker = this.getDocumentWalker(node.rawNode);
     let firstChild = docWalker.firstChild();
@@ -609,80 +622,134 @@ var WalkerActor = protocol.ActorClassWit
     if (options.center && options.start) {
       throw Error("Can't specify both 'center' and 'start' options.");
     }
     let maxNodes = options.maxNodes || -1;
     if (maxNodes == -1) {
       maxNodes = Number.MAX_VALUE;
     }
 
-    // We're going to create a few document walkers with the same filter,
-    // make it easier.
-    let getFilteredWalker = documentWalkerNode => {
-      let { whatToShow } = options;
-      // Use SKIP_TO_SIBLING to force the walker to use a sibling of the provided node
-      // in case this one is incompatible with the walker's filter function.
-      return this.getDocumentWalker(documentWalkerNode, {
-        whatToShow,
-        skipTo: SKIP_TO_SIBLING
-      });
-    };
+    let isShadowHost = !!node.rawNode.shadowRoot;
+    let isShadowRoot = !!node.rawNode.host;
+
+    let rawNode = node.rawNode;
+    let walker = this.getDocumentWalker(rawNode, {
+      whatToShow: options.whatToShow,
+      skipTo: SKIP_TO_SIBLING,
+      showAnonymousContent: !isShadowHost && !isShadowRoot
+    });
 
     // Need to know the first and last child.
-    let rawNode = node.rawNode;
-    let firstChild = getFilteredWalker(rawNode).firstChild();
-    let lastChild = getFilteredWalker(rawNode).lastChild();
+    let firstChild = walker.firstChild();
+
+    walker.setStartingNode(rawNode);
+    let lastChild = walker.lastChild();
+
+    let nodes = [];
+    if (firstChild) {
+      let start;
+      if (options.center) {
+        start = options.center.rawNode;
+      } else if (options.start) {
+        start = options.start.rawNode;
+      } else {
+        start = firstChild;
+      }
+
+      // Start by reading backward from the starting point if we're centering...
+      walker.setStartingNode(start);
 
-    if (!firstChild) {
+      let lastBackwardNode;
+      if (walker.currentNode != firstChild && options.center) {
+        walker.previousSibling();
+        let backwardCount = Math.floor(maxNodes / 2);
+        let backwardNodes = this._readBackward(walker, backwardCount);
+        nodes = backwardNodes;
+        lastBackwardNode = walker.currentNode;
+      }
+
+      // Then read forward by any slack left in the max children...
+      walker.setStartingNode(start);
+      let forwardCount = maxNodes - nodes.length;
+      nodes = nodes.concat(this._readForward(walker, forwardCount));
+
+      // If there's any room left, it means we've run all the way to the end.
+      // If we're centering, check if there are more items to read at the front.
+      let remaining = maxNodes - nodes.length;
+      if (options.center && remaining > 0 && nodes[0].rawNode != firstChild) {
+        walker.setStartingNode(lastBackwardNode);
+        let firstNodes = this._readBackward(walker, remaining);
+
+        // Then put it all back together.
+        nodes = firstNodes.concat(nodes);
+      }
+    } else if (!isShadowHost) {
       // No children, we're done.
       return { hasFirst: true, hasLast: true, nodes: [] };
     }
 
-    let start;
-    if (options.center) {
-      start = options.center.rawNode;
-    } else if (options.start) {
-      start = options.start.rawNode;
-    } else {
-      start = firstChild;
+    let hasFirst = true;
+    let hasLast = true;
+
+    if (nodes.length > 0) {
+      // Compare first/last with expected nodes before modifying the nodes array in case
+      // this is a shadow host.
+      hasFirst = nodes[0].rawNode == firstChild;
+      hasLast = nodes[nodes.length - 1].rawNode == lastChild;
     }
 
-    let nodes = [];
-
-    // Start by reading backward from the starting point if we're centering...
-    let backwardWalker = getFilteredWalker(start);
-    if (backwardWalker.currentNode != firstChild && options.center) {
-      backwardWalker.previousSibling();
-      let backwardCount = Math.floor(maxNodes / 2);
-      let backwardNodes = this._readBackward(backwardWalker, backwardCount);
-      nodes = backwardNodes;
+    if (isShadowHost) {
+      // We need a dedicated anonymous walker to fetch before / after pseudos.
+      let {before, after} = this._getBeforeAfterElements(rawNode);
+      nodes = [
+        // #shadow-root
+        this._ref(node.rawNode.shadowRoot),
+        // ::before
+        ...(before ? [before] : []),
+        // light dom nodes
+        ...nodes,
+        // ::after
+        ...(after ? [after] : []),
+      ];
     }
 
-    // Then read forward by any slack left in the max children...
-    let forwardWalker = getFilteredWalker(start);
-    let forwardCount = maxNodes - nodes.length;
-    nodes = nodes.concat(this._readForward(forwardWalker, forwardCount));
+    return {hasFirst, hasLast, nodes};
+  },
 
-    // If there's any room left, it means we've run all the way to the end.
-    // If we're centering, check if there are more items to read at the front.
-    let remaining = maxNodes - nodes.length;
-    if (options.center && remaining > 0 && nodes[0].rawNode != firstChild) {
-      let firstNodes = this._readBackward(backwardWalker, remaining);
+  _getBeforeAfterElements: function (node) {
+    let anonymousWalker = this.getDocumentWalker(node, {
+      showAnonymousContent: true
+    });
+    let before = this._ref(anonymousWalker.firstChild());
 
-      // Then put it all back together.
-      nodes = firstNodes.concat(nodes);
-    }
+    anonymousWalker.setStartingNode(node);
+    let after = this._ref(anonymousWalker.lastChild());
 
     return {
-      hasFirst: nodes[0].rawNode == firstChild,
-      hasLast: nodes[nodes.length - 1].rawNode == lastChild,
-      nodes: nodes
+      before: before.isBeforePseudoElement ? before : undefined,
+      after: after.isAfterPseudoElement ? after : undefined,
     };
   },
 
+  _convertNodesToSlottedNodes: function (parentActor, nodes) {
+    return nodes.map(nodeActor => {
+      if (nodeActor.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
+        return nodeActor;
+      }
+
+      // An unused slot will list its default children (if any) and those should not be
+      // converted to slotted node actors.
+      if (nodeActor.rawNode.parentNode === parentActor.rawNode) {
+        return nodeActor;
+      }
+
+      return this._slottedRef(nodeActor.rawNode);
+    });
+  },
+
   /**
    * Return siblings of the given node.  By default this method will return
    * all siblings of the node, but there are options that can restrict this
    * to a more manageable subset.
    *
    * If `start` or `center` are not specified, this method will center on the
    * node whose siblings are requested.
    *
@@ -1663,20 +1730,34 @@ var WalkerActor = protocol.ActorClassWit
         mutation.removed = removedActors;
         mutation.added = addedActors;
 
         let inlineTextChild = this.inlineTextChild(targetActor);
         if (inlineTextChild) {
           mutation.inlineTextChild = inlineTextChild.form();
         }
       }
+
       this.queueMutation(mutation);
     }
   },
 
+  onSlotchange: function (event) {
+    let target = event.target;
+    let targetActor = this.getNode(target);
+    if (!targetActor) {
+      return;
+    }
+
+    this.queueMutation({
+      type: "slotchange",
+      target: targetActor.actorID
+    });
+  },
+
   /**
    * Check if the provided mutation could change the way the target element is
    * inlined with its parent node. If it might, a custom mutation of type
    * "inlineTextChild" will be queued.
    *
    * @param {MutationRecord} mutation
    *        A characterData type mutation
    */
--- a/devtools/shared/fronts/inspector.js
+++ b/devtools/shared/fronts/inspector.js
@@ -302,17 +302,16 @@ const WalkerFront = FrontClassWithSpec(w
         } else {
           targetID = change.target;
           targetFront = this.get(targetID);
         }
 
         if (!targetFront) {
           console.warn("Got a mutation for an unexpected actor: " + targetID +
             ", please file a bug on bugzilla.mozilla.org!");
-          console.trace();
           continue;
         }
 
         let emittedMutation = Object.assign(change, { target: targetFront });
 
         if (change.type === "childList" ||
             change.type === "nativeAnonymousChildList") {
           // Update the ownership tree according to the mutation record.
--- a/devtools/shared/fronts/node.js
+++ b/devtools/shared/fronts/node.js
@@ -267,16 +267,22 @@ const NodeFront = FrontClassWithSpec(nod
   },
   get numChildren() {
     return this._form.numChildren;
   },
   get hasEventListeners() {
     return this._form.hasEventListeners;
   },
 
+  get isShadowRoot() {
+    return this._form.isShadowRoot;
+  },
+  get isSlottedNode() {
+    return this._form.isSlottedNode;
+  },
   get isBeforePseudoElement() {
     return this._form.isBeforePseudoElement;
   },
   get isAfterPseudoElement() {
     return this._form.isAfterPseudoElement;
   },
   get isPseudoElement() {
     return this.isBeforePseudoElement || this.isAfterPseudoElement;
--- a/toolkit/modules/css-selector.js
+++ b/toolkit/modules/css-selector.js
@@ -3,34 +3,49 @@
 /* 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";
 
 var EXPORTED_SYMBOLS = ["findCssSelector"];
 
+const DOCUMENT_FRAGMENT_NODETYPE = 11;
+
+function isDocumentFragment(node) {
+  return node.nodeType === DOCUMENT_FRAGMENT_NODETYPE;
+}
+
 /**
  * Traverse getBindingParent until arriving upon the bound element
  * responsible for the generation of the specified node.
  * See https://developer.mozilla.org/en-US/docs/XBL/XBL_1.0_Reference/DOM_Interfaces#getBindingParent.
  *
  * @param {DOMNode} node
  * @return {DOMNode}
  *         If node is not anonymous, this will return node. Otherwise,
  *         it will return the bound element
  *
  */
 function getRootBindingParent(node) {
   let parent;
-  let doc = node.ownerDocument;
-  if (!doc) {
+  let root = node.getRootNode();
+  if (!root) {
     return node;
   }
-  while ((parent = doc.getBindingParent(node))) {
+
+  // root might be a document-fragment, which doesn't implement getBindingParent.
+  let document = node.ownerDocument;
+
+  // For shadow dom elements, getBindingParent returns the host element.
+  // For ::before/::after pseudo-elements, getBindingParent returns the parent element.
+
+  // Climb up the getBindingParent chain as long as root.contains(parent), to avoid
+  // returning the host element for shadow dom elements.
+  while ((parent = document.getBindingParent(node)) && root.contains(parent)) {
     node = parent;
   }
   return node;
 }
 
 /**
  * Find the position of [element] in [nodeList].
  * @returns an index of the match, or -1 if there is no match
@@ -46,26 +61,27 @@ function positionInNodeList(element, nod
 
 /**
  * Find a unique CSS selector for a given element
  * @returns a string such that ele.ownerDocument.querySelector(reply) === ele
  * and ele.ownerDocument.querySelectorAll(reply).length === 1
  */
 const findCssSelector = function(ele) {
   ele = getRootBindingParent(ele);
-  let document = ele.ownerDocument;
-  if (!document || !document.contains(ele)) {
-    throw new Error("findCssSelector received element not inside document");
+  let root = ele.getRootNode();
+
+  if (!root || !root.contains(ele)) {
+    throw new Error("findCssSelector received element not inside the root");
   }
 
   let cssEscape = ele.ownerGlobal.CSS.escape;
 
-  // document.querySelectorAll("#id") returns multiple if elements share an ID
+  // root.querySelectorAll("#id") returns multiple if elements share an ID
   if (ele.id &&
-      document.querySelectorAll("#" + cssEscape(ele.id)).length === 1) {
+      root.querySelectorAll("#" + cssEscape(ele.id)).length === 1) {
     return "#" + cssEscape(ele.id);
   }
 
   // Inherently unique by tag name
   let tagName = ele.localName;
   if (tagName === "html") {
     return "html";
   }
@@ -77,38 +93,41 @@ const findCssSelector = function(ele) {
   }
 
   // We might be able to find a unique class name
   let selector, index, matches;
   if (ele.classList.length > 0) {
     for (let i = 0; i < ele.classList.length; i++) {
       // Is this className unique by itself?
       selector = "." + cssEscape(ele.classList.item(i));
-      matches = document.querySelectorAll(selector);
+      matches = root.querySelectorAll(selector);
       if (matches.length === 1) {
         return selector;
       }
       // Maybe it's unique with a tag name?
       selector = cssEscape(tagName) + selector;
-      matches = document.querySelectorAll(selector);
+      matches = root.querySelectorAll(selector);
       if (matches.length === 1) {
         return selector;
       }
       // Maybe it's unique using a tag name and nth-child
       index = positionInNodeList(ele, ele.parentNode.children) + 1;
       selector = selector + ":nth-child(" + index + ")";
-      matches = document.querySelectorAll(selector);
+      matches = root.querySelectorAll(selector);
       if (matches.length === 1) {
         return selector;
       }
     }
   }
 
   // Not unique enough yet.  As long as it's not a child of the document,
   // continue recursing up until it is unique enough.
-  if (ele.parentNode !== document) {
+  if (ele.parentNode !== root) {
     index = positionInNodeList(ele, ele.parentNode.children) + 1;
     selector = findCssSelector(ele.parentNode) + " > " +
       cssEscape(tagName) + ":nth-child(" + index + ")";
+  } else if (isDocumentFragment(root)) {
+    // In document fragments, we might have no better identifier than tagname + nth-child.
+    selector = cssEscape(tagName) + ":nth-child(" + index + ")";
   }
 
   return selector;
 };