Bug 1523049 - Implement the context menu for the new rules view. r=rcaliman
authorGabriel Luong <gabriel.luong@gmail.com>
Tue, 18 Feb 2020 11:27:18 +0000
changeset 514403 3b2e49d603db90472eac3d558231f2448e86fb4c
parent 514402 1e8b238dfef065d980cc8e52820d3362248534aa
child 514404 106776ac6c2056323c24fbc1e57c9eaee537e64c
push id37135
push useropoprus@mozilla.com
push dateTue, 18 Feb 2020 21:33:59 +0000
treeherdermozilla-central@df596657bebc [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrcaliman
bugs1523049
milestone75.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1523049 - Implement the context menu for the new rules view. r=rcaliman Differential Revision: https://phabricator.services.mozilla.com/D62530
devtools/client/inspector/computed/computed.js
devtools/client/inspector/rules/components/Declaration.js
devtools/client/inspector/rules/components/Rule.js
devtools/client/inspector/rules/components/RulesApp.js
devtools/client/inspector/rules/models/element-style.js
devtools/client/inspector/rules/models/rule.js
devtools/client/inspector/rules/new-rules.js
devtools/client/inspector/rules/rules.js
devtools/client/inspector/rules/utils/utils.js
devtools/client/inspector/rules/views/rule-editor.js
devtools/client/inspector/rules/views/text-property-editor.js
devtools/client/inspector/shared/style-inspector-menu.js
devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js
devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js
--- a/devtools/client/inspector/computed/computed.js
+++ b/devtools/client/inspector/computed/computed.js
@@ -286,17 +286,17 @@ CssComputedView.prototype = {
   // Toggle for zebra striping
   _darkStripe: true,
 
   // Number of visible properties
   numVisibleProperties: 0,
 
   get contextMenu() {
     if (!this._contextMenu) {
-      this._contextMenu = new StyleInspectorMenu(this, { isRuleView: false });
+      this._contextMenu = new StyleInspectorMenu(this);
     }
 
     return this._contextMenu;
   },
 
   // Get the highlighters overlay from the Inspector.
   get highlighters() {
     if (!this._highlighters) {
--- a/devtools/client/inspector/rules/components/Declaration.js
+++ b/devtools/client/inspector/rules/components/Declaration.js
@@ -209,16 +209,17 @@ class Declaration extends PureComponent 
     return dom.div({
       className: "ruleview-overridden-rule-filter",
       title: getStr("rule.filterProperty.title"),
     });
   }
 
   render() {
     const {
+      id,
       isEnabled,
       isKnownProperty,
       isOverridden,
       isPropertyChanged,
       name,
       value,
     } = this.props.declaration;
 
@@ -228,35 +229,38 @@ class Declaration extends PureComponent 
       declarationClassName += " ruleview-overridden";
     }
 
     if (isPropertyChanged) {
       declarationClassName += " ruleview-changed";
     }
 
     return dom.li(
-      { className: declarationClassName },
+      {
+        className: declarationClassName,
+        "data-declaration-id": id,
+      },
       dom.div(
         { className: "ruleview-propertycontainer" },
         dom.input({
-          type: "checkbox",
+          "aria-labelledby": id,
           className: "ruleview-enableproperty",
           checked: isEnabled,
           onChange: this.onToggleDeclarationChange,
-          "aria-labelledby": this.props.declaration.id,
-          tabindex: "-1",
+          tabIndex: "-1",
+          type: "checkbox",
         }),
         dom.span(
           { className: "ruleview-namecontainer" },
           dom.span(
             {
+              id,
               className: "ruleview-propertyname theme-fg-color3",
               ref: this.nameSpanRef,
               tabIndex: 0,
-              id: this.props.declaration.id,
             },
             name
           ),
           ": "
         ),
         dom.span({
           className:
             "ruleview-expander theme-twisty" +
--- a/devtools/client/inspector/rules/components/Rule.js
+++ b/devtools/client/inspector/rules/components/Rule.js
@@ -101,16 +101,17 @@ class Rule extends PureComponent {
     } = rule;
 
     return dom.div(
       {
         className:
           "ruleview-rule devtools-monospace" +
           (isUnmatched ? " unmatched" : "") +
           (isUserAgentStyle ? " uneditable" : ""),
+        "data-rule-id": id,
       },
       SourceLink({
         id,
         isUserAgentStyle,
         onOpenSourceLink,
         sourceLink,
         type,
       }),
--- a/devtools/client/inspector/rules/components/RulesApp.js
+++ b/devtools/client/inspector/rules/components/RulesApp.js
@@ -42,35 +42,56 @@ class RulesApp extends PureComponent {
       onSetClassState: PropTypes.func.isRequired,
       onToggleClassPanelExpanded: PropTypes.func.isRequired,
       onToggleDeclaration: PropTypes.func.isRequired,
       onTogglePrintSimulation: PropTypes.func.isRequired,
       onToggleColorSchemeSimulation: PropTypes.func.isRequired,
       onTogglePseudoClass: PropTypes.func.isRequired,
       onToggleSelectorHighlighter: PropTypes.func.isRequired,
       rules: PropTypes.arrayOf(PropTypes.shape(Types.rule)).isRequired,
+      showContextMenu: PropTypes.func.isRequired,
       showDeclarationNameEditor: PropTypes.func.isRequired,
       showDeclarationValueEditor: PropTypes.func.isRequired,
       showNewDeclarationEditor: PropTypes.func.isRequired,
       showSelectorEditor: PropTypes.func.isRequired,
     };
   }
 
+  constructor(props) {
+    super(props);
+    this.onContextMenu = this.onContextMenu.bind(this);
+  }
+
   getRuleProps() {
     return {
       onOpenSourceLink: this.props.onOpenSourceLink,
       onToggleDeclaration: this.props.onToggleDeclaration,
       onToggleSelectorHighlighter: this.props.onToggleSelectorHighlighter,
       showDeclarationNameEditor: this.props.showDeclarationNameEditor,
       showDeclarationValueEditor: this.props.showDeclarationValueEditor,
       showNewDeclarationEditor: this.props.showNewDeclarationEditor,
       showSelectorEditor: this.props.showSelectorEditor,
     };
   }
 
+  onContextMenu(event) {
+    if (
+      event.target.closest("input[type=text]") ||
+      event.target.closest("input:not([type])") ||
+      event.target.closest("textarea")
+    ) {
+      return;
+    }
+
+    event.stopPropagation();
+    event.preventDefault();
+
+    this.props.showContextMenu(event);
+  }
+
   renderInheritedRules(rules) {
     if (!rules.length) {
       return null;
     }
 
     const output = [];
     let lastInherited;
 
@@ -207,16 +228,17 @@ class RulesApp extends PureComponent {
       dom.div(
         {
           id: "ruleview-container",
           className: "ruleview",
         },
         dom.div(
           {
             id: "ruleview-container-focusable",
+            onContextMenu: this.onContextMenu,
             tabIndex: -1,
           },
           rules.length > 0
             ? createElement(
                 Fragment,
                 null,
                 this.renderPseudoElementRules(pseudoElementRules),
                 this.renderStyleRules(styleRules),
--- a/devtools/client/inspector/rules/models/element-style.js
+++ b/devtools/client/inspector/rules/models/element-style.js
@@ -207,22 +207,24 @@ class ElementStyle {
       });
     this.populated = populated;
     return this.populated;
   }
 
   /**
    * Returns the Rule object of the given rule id.
    *
-   * @param  {String} id
+   * @param  {String|null} id
    *         The id of the Rule object.
    * @return {Rule|undefined} of the given rule id or undefined if it cannot be found.
    */
   getRule(id) {
-    return this.rules.find(rule => rule.domRule.actorID === id);
+    return id
+      ? this.rules.find(rule => rule.domRule.actorID === id)
+      : undefined;
   }
 
   /**
    * Get the font families in use by the element.
    *
    * Returns a promise that will be resolved to a list of CSS family
    * names. The list might have duplicates.
    */
@@ -552,25 +554,25 @@ class ElementStyle {
       this.element.pseudoClassLocks
     );
   }
 
   /**
    * Given the id of the rule and the new declaration name, modifies the existing
    * declaration name to the new given value.
    *
-   * @param  {String} ruleID
+   * @param  {String} ruleId
    *         The Rule id of the given CSS declaration.
    * @param  {String} declarationId
    *         The TextProperty id for the CSS declaration.
    * @param  {String} name
    *         The new declaration name.
    */
-  async modifyDeclarationName(ruleID, declarationId, name) {
-    const rule = this.getRule(ruleID);
+  async modifyDeclarationName(ruleId, declarationId, name) {
+    const rule = this.getRule(ruleId);
     if (!rule) {
       return;
     }
 
     const declaration = rule.getDeclaration(declarationId);
     if (!declaration || declaration.name === name) {
       return;
     }
--- a/devtools/client/inspector/rules/models/rule.js
+++ b/devtools/client/inspector/rules/models/rule.js
@@ -234,23 +234,23 @@ class Rule {
    */
   get ruleColumn() {
     return this.domRule ? this.domRule.column : null;
   }
 
   /**
    * Returns the TextProperty with the given id or undefined if it cannot be found.
    *
-   * @param {String} id
+   * @param {String|null} id
    *        A TextProperty id.
    * @return {TextProperty|undefined} with the given id in the current Rule or undefined
    * if it cannot be found.
    */
   getDeclaration(id) {
-    return this.textProps.find(textProp => textProp.id === id);
+    return id ? this.textProps.find(textProp => textProp.id === id) : undefined;
   }
 
   /**
    * Returns a formatted source text of the given stylesheet URL with its source line
    * and @media text.
    *
    * @param  {String} url
    *         The stylesheet URL.
--- a/devtools/client/inspector/rules/new-rules.js
+++ b/devtools/client/inspector/rules/new-rules.js
@@ -49,16 +49,27 @@ loader.lazyRequireGetter(
 );
 loader.lazyRequireGetter(
   this,
   "ClassList",
   "devtools/client/inspector/rules/models/class-list"
 );
 loader.lazyRequireGetter(
   this,
+  "getNodeInfo",
+  "devtools/client/inspector/rules/utils/utils",
+  true
+);
+loader.lazyRequireGetter(
+  this,
+  "StyleInspectorMenu",
+  "devtools/client/inspector/shared/style-inspector-menu"
+);
+loader.lazyRequireGetter(
+  this,
   "advanceValidate",
   "devtools/client/inspector/shared/utils",
   true
 );
 loader.lazyRequireGetter(
   this,
   "AutocompletePopup",
   "devtools/client/shared/autocomplete-popup"
@@ -104,16 +115,17 @@ class RulesView {
     this.onToggleColorSchemeSimulation = this.onToggleColorSchemeSimulation.bind(
       this
     );
     this.onTogglePseudoClass = this.onTogglePseudoClass.bind(this);
     this.onToolChanged = this.onToolChanged.bind(this);
     this.onToggleSelectorHighlighter = this.onToggleSelectorHighlighter.bind(
       this
     );
+    this.showContextMenu = this.showContextMenu.bind(this);
     this.showDeclarationNameEditor = this.showDeclarationNameEditor.bind(this);
     this.showDeclarationValueEditor = this.showDeclarationValueEditor.bind(
       this
     );
     this.showNewDeclarationEditor = this.showNewDeclarationEditor.bind(this);
     this.showSelectorEditor = this.showSelectorEditor.bind(this);
     this.updateClassList = this.updateClassList.bind(this);
     this.updateRules = this.updateRules.bind(this);
@@ -140,16 +152,17 @@ class RulesView {
       onOpenSourceLink: this.onOpenSourceLink,
       onSetClassState: this.onSetClassState,
       onToggleClassPanelExpanded: this.onToggleClassPanelExpanded,
       onToggleColorSchemeSimulation: this.onToggleColorSchemeSimulation,
       onToggleDeclaration: this.onToggleDeclaration,
       onTogglePrintSimulation: this.onTogglePrintSimulation,
       onTogglePseudoClass: this.onTogglePseudoClass,
       onToggleSelectorHighlighter: this.onToggleSelectorHighlighter,
+      showContextMenu: this.showContextMenu,
       showDeclarationNameEditor: this.showDeclarationNameEditor,
       showDeclarationValueEditor: this.showDeclarationValueEditor,
       showNewDeclarationEditor: this.showNewDeclarationEditor,
       showSelectorEditor: this.showSelectorEditor,
     });
 
     this.initSimulationFeatures();
 
@@ -237,16 +250,21 @@ class RulesView {
     }
 
     if (this._classList) {
       this._classList.off("current-node-class-changed", this.refreshClassList);
       this._classList.destroy();
       this._classList = null;
     }
 
+    if (this._contextMenu) {
+      this._contextMenu.destroy();
+      this._contextMenu = null;
+    }
+
     if (this._selectHighlighter) {
       this._selectorHighlighter.finalize();
       this._selectorHighlighter = null;
     }
 
     if (this.elementStyle) {
       this.elementStyle.destroy();
       this.elementStyle = null;
@@ -294,16 +312,24 @@ class RulesView {
   get classList() {
     if (!this._classList) {
       this._classList = new ClassList(this.inspector);
     }
 
     return this._classList;
   }
 
+  get contextMenu() {
+    if (!this._contextMenu) {
+      this._contextMenu = new StyleInspectorMenu(this, { isRuleView: true });
+    }
+
+    return this._contextMenu;
+  }
+
   /**
    * Get the current target the toolbox is debugging.
    *
    * @return {Target}
    */
   get currentTarget() {
     return this.inspector.currentTarget;
   }
@@ -362,16 +388,33 @@ class RulesView {
     }
 
     // This event is emitted for testing purposes.
     this.inspector.emit("grid-line-names-updated");
     return gridLineNames;
   }
 
   /**
+   * Get the type of a given node in the Rules view.
+   *
+   * @param {DOMNode} node
+   *        The node which we want information about.
+   * @return {Object|null} containing the following props:
+   * - rule {Rule} The Rule object.
+   * - type {String} One of the VIEW_NODE_XXX_TYPE const in
+   *   client/inspector/shared/node-types.
+   * - value {Object} Depends on the type of the node.
+   * - view {String} Always "rule" to indicate the rule view.
+   * Otherwise, returns null if the node isn't anything we care about.
+   */
+  getNodeInfo(node) {
+    return getNodeInfo(node, this.elementStyle);
+  }
+
+  /**
    * Get an instance of SelectorHighlighter (used to highlight nodes that match
    * selectors in the rule-view).
    *
    * @return {Promise} resolves to the instance of the highlighter.
    */
   async getSelectorHighlighter() {
     if (!this.inspector) {
       return null;
@@ -601,16 +644,23 @@ class RulesView {
     const isSourceLinkEnabled = this.toolbox.isToolRegistered("styleeditor");
 
     if (prevIsSourceLinkEnabled !== isSourceLinkEnabled) {
       this.store.dispatch(updateSourceLinkEnabled(isSourceLinkEnabled));
     }
   }
 
   /**
+   * Handler for showing the context menu.
+   */
+  showContextMenu(event) {
+    this.contextMenu.show(event);
+  }
+
+  /**
    * Handler for showing the inplace editor when an editable property name is clicked in
    * the rules view.
    *
    * @param  {DOMNode} element
    *         The declaration name span element to be edited.
    * @param  {String} ruleId
    *         The id of the Rule object to be edited.
    * @param  {String} declarationId
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -9,28 +9,16 @@ const Services = require("Services");
 const flags = require("devtools/shared/flags");
 const { l10n } = require("devtools/shared/inspector/css-logic");
 const { PSEUDO_CLASSES } = require("devtools/shared/css/constants");
 const { ELEMENT_STYLE } = require("devtools/shared/specs/styles");
 const OutputParser = require("devtools/client/shared/output-parser");
 const { PrefObserver } = require("devtools/client/shared/prefs");
 const ElementStyle = require("devtools/client/inspector/rules/models/element-style");
 const RuleEditor = require("devtools/client/inspector/rules/views/rule-editor");
-const {
-  VIEW_NODE_FONT_TYPE,
-  VIEW_NODE_IMAGE_URL_TYPE,
-  VIEW_NODE_INACTIVE_CSS,
-  VIEW_NODE_LOCATION_TYPE,
-  VIEW_NODE_PROPERTY_TYPE,
-  VIEW_NODE_SELECTOR_TYPE,
-  VIEW_NODE_SHAPE_POINT_TYPE,
-  VIEW_NODE_SHAPE_SWATCH,
-  VIEW_NODE_VALUE_TYPE,
-  VIEW_NODE_VARIABLE_TYPE,
-} = require("devtools/client/inspector/shared/node-types");
 const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay");
 const {
   createChild,
   promiseWarn,
 } = require("devtools/client/inspector/shared/utils");
 const { debounce } = require("devtools/shared/debounce");
 const EventEmitter = require("devtools/shared/event-emitter");
 
@@ -48,16 +36,22 @@ loader.lazyRequireGetter(
 );
 loader.lazyRequireGetter(
   this,
   "ClassListPreviewer",
   "devtools/client/inspector/rules/views/class-list-previewer"
 );
 loader.lazyRequireGetter(
   this,
+  "getNodeInfo",
+  "devtools/client/inspector/rules/utils/utils",
+  true
+);
+loader.lazyRequireGetter(
+  this,
   "COLOR_SCHEMES",
   "devtools/client/inspector/rules/constants",
   true
 );
 loader.lazyRequireGetter(
   this,
   "StyleInspectorMenu",
   "devtools/client/inspector/shared/style-inspector-menu"
@@ -85,17 +79,16 @@ const FILTER_CHANGED_TIMEOUT = 150;
 // Removes the flash-out class from an element after 1 second.
 const PROPERTY_FLASHING_DURATION = 1000;
 
 // This is used to parse user input when filtering.
 const FILTER_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*;?$/;
 // This is used to parse the filter search value to see if the filter
 // should be strict or not
 const FILTER_STRICT_RE = /\s*`(.*?)`\s*$/;
-const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
 
 /**
  * Our model looks like this:
  *
  * ElementStyle:
  *   Responsible for keeping track of which properties are overridden.
  *   Maintains a list of Rule objects that apply to the element.
  * Rule:
@@ -460,155 +453,25 @@ CssRuleView.prototype = {
     }
   },
 
   /**
    * Get the type of a given node in the rule-view
    *
    * @param {DOMNode} node
    *        The node which we want information about
-   * @return {Object} The type information object contains the following props:
-   * - view {String} Always "rule" to indicate the rule view.
+   * @return {Object|null} containing the following props:
    * - type {String} One of the VIEW_NODE_XXX_TYPE const in
-   *   client/inspector/shared/node-types
-   * - value {Object} Depends on the type of the node
-   * returns null of the node isn't anything we care about
+   *   client/inspector/shared/node-types.
+   * - rule {Rule} The Rule object.
+   * - value {Object} Depends on the type of the node.
+   * Otherwise, returns null if the node isn't anything we care about.
    */
-  // eslint-disable-next-line complexity
   getNodeInfo: function(node) {
-    if (!node) {
-      return null;
-    }
-
-    let type, value;
-    const classes = node.classList;
-    const prop = getParentTextProperty(node);
-
-    if (classes.contains("ruleview-propertyname") && prop) {
-      type = VIEW_NODE_PROPERTY_TYPE;
-      value = {
-        property: node.textContent,
-        value: getPropertyNameAndValue(node).value,
-        enabled: prop.enabled,
-        overridden: prop.overridden,
-        pseudoElement: prop.rule.pseudoElement,
-        sheetHref: prop.rule.domRule.href,
-        textProperty: prop,
-      };
-    } else if (classes.contains("ruleview-propertyvalue") && prop) {
-      type = VIEW_NODE_VALUE_TYPE;
-      value = {
-        property: getPropertyNameAndValue(node).name,
-        value: node.textContent,
-        enabled: prop.enabled,
-        overridden: prop.overridden,
-        pseudoElement: prop.rule.pseudoElement,
-        sheetHref: prop.rule.domRule.href,
-        textProperty: prop,
-      };
-    } else if (classes.contains("ruleview-font-family") && prop) {
-      type = VIEW_NODE_FONT_TYPE;
-      value = {
-        property: getPropertyNameAndValue(node).name,
-        value: getPropertyNameAndValue(node).value,
-        enabled: prop.enabled,
-        overridden: prop.overridden,
-        pseudoElement: prop.rule.pseudoElement,
-        sheetHref: prop.rule.domRule.href,
-        textProperty: prop,
-      };
-    } else if (classes.contains("ruleview-shape-point") && prop) {
-      type = VIEW_NODE_SHAPE_POINT_TYPE;
-      value = {
-        property: getPropertyNameAndValue(node).name,
-        value: node.textContent,
-        enabled: prop.enabled,
-        overridden: prop.overridden,
-        pseudoElement: prop.rule.pseudoElement,
-        sheetHref: prop.rule.domRule.href,
-        textProperty: prop,
-        toggleActive: getShapeToggleActive(node),
-        point: getShapePoint(node),
-      };
-    } else if (classes.contains("ruleview-unused-warning") && prop) {
-      type = VIEW_NODE_INACTIVE_CSS;
-      value = prop.isUsed();
-    } else if (classes.contains("ruleview-shapeswatch") && prop) {
-      type = VIEW_NODE_SHAPE_SWATCH;
-      value = {
-        enabled: prop.enabled,
-        overridden: prop.overridden,
-        textProperty: prop,
-      };
-    } else if (
-      (classes.contains("ruleview-variable") ||
-        classes.contains("ruleview-unmatched-variable")) &&
-      prop
-    ) {
-      type = VIEW_NODE_VARIABLE_TYPE;
-      value = {
-        property: getPropertyNameAndValue(node).name,
-        value: node.textContent,
-        enabled: prop.enabled,
-        overridden: prop.overridden,
-        pseudoElement: prop.rule.pseudoElement,
-        sheetHref: prop.rule.domRule.href,
-        textProperty: prop,
-        variable: node.dataset.variable,
-      };
-    } else if (
-      classes.contains("theme-link") &&
-      !classes.contains("ruleview-rule-source") &&
-      prop
-    ) {
-      type = VIEW_NODE_IMAGE_URL_TYPE;
-      value = {
-        property: getPropertyNameAndValue(node).name,
-        value: node.parentNode.textContent,
-        url: node.href,
-        enabled: prop.enabled,
-        overridden: prop.overridden,
-        pseudoElement: prop.rule.pseudoElement,
-        sheetHref: prop.rule.domRule.href,
-        textProperty: prop,
-      };
-    } else if (
-      classes.contains("ruleview-selector-unmatched") ||
-      classes.contains("ruleview-selector-matched") ||
-      classes.contains("ruleview-selectorcontainer") ||
-      classes.contains("ruleview-selector") ||
-      classes.contains("ruleview-selector-attribute") ||
-      classes.contains("ruleview-selector-pseudo-class") ||
-      classes.contains("ruleview-selector-pseudo-class-lock")
-    ) {
-      type = VIEW_NODE_SELECTOR_TYPE;
-      value = this._getRuleEditorForNode(node).selectorText.textContent;
-    } else if (
-      classes.contains("ruleview-rule-source") ||
-      classes.contains("ruleview-rule-source-label")
-    ) {
-      type = VIEW_NODE_LOCATION_TYPE;
-      const rule = this._getRuleEditorForNode(node).rule;
-      value = rule.sheet && rule.sheet.href ? rule.sheet.href : rule.title;
-    } else {
-      return null;
-    }
-
-    return {
-      view: "rule",
-      type,
-      value,
-    };
-  },
-
-  /**
-   * Retrieve the RuleEditor instance.
-   */
-  _getRuleEditorForNode: function(node) {
-    return node.closest(".ruleview-rule")._ruleEditor;
+    return getNodeInfo(node, this._elementStyle);
   },
 
   /**
    * Context menu handler.
    */
   _onContextMenu: function(event) {
     if (
       event.originalTarget.closest("input[type=text]") ||
@@ -1976,138 +1839,16 @@ CssRuleView.prototype = {
         }
       }
     }
 
     return false;
   },
 };
 
-/**
- * Helper functions
- */
-
-/**
- * Walk up the DOM from a given node until a parent property holder is found.
- * For elements inside the computed property list, the non-computed parent
- * property holder will be returned
- *
- * @param {DOMNode} node
- *        The node to start from
- * @return {DOMNode} The parent property holder node, or null if not found
- */
-function getParentTextPropertyHolder(node) {
-  while (true) {
-    if (!node || !node.classList) {
-      return null;
-    }
-    if (node.classList.contains("ruleview-property")) {
-      return node;
-    }
-    node = node.parentNode;
-  }
-}
-
-/**
- * For any given node, find the TextProperty it is in if any
- * @param {DOMNode} node
- *        The node to start from
- * @return {TextProperty}
- */
-function getParentTextProperty(node) {
-  const parent = getParentTextPropertyHolder(node);
-  if (!parent) {
-    return null;
-  }
-
-  const propValue = parent.querySelector(".ruleview-propertyvalue");
-  if (!propValue) {
-    return null;
-  }
-
-  return propValue.textProperty;
-}
-
-/**
- * Walker up the DOM from a given node until a parent property holder is found,
- * and return the textContent for the name and value nodes.
- * Stops at the first property found, so if node is inside the computed property
- * list, the computed property will be returned
- *
- * @param {DOMNode} node
- *        The node to start from
- * @return {Object} {name, value}
- */
-function getPropertyNameAndValue(node) {
-  while (true) {
-    if (!node || !node.classList) {
-      return null;
-    }
-    // Check first for ruleview-computed since it's the deepest
-    if (
-      node.classList.contains("ruleview-computed") ||
-      node.classList.contains("ruleview-property")
-    ) {
-      return {
-        name: node.querySelector(".ruleview-propertyname").textContent,
-        value: node.querySelector(".ruleview-propertyvalue").textContent,
-      };
-    }
-    node = node.parentNode;
-  }
-}
-
-/**
- * Walk up the DOM from a given node until a parent property holder is found,
- * and return an active shape toggle if one exists.
- *
- * @param {DOMNode} node
- *        The node to start from
- * @returns {DOMNode} The active shape toggle node, if one exists.
- */
-function getShapeToggleActive(node) {
-  while (true) {
-    if (!node || !node.classList) {
-      return null;
-    }
-    // Check first for ruleview-computed since it's the deepest
-    if (
-      node.classList.contains("ruleview-computed") ||
-      node.classList.contains("ruleview-property")
-    ) {
-      return node.querySelector(".ruleview-shapeswatch.active");
-    }
-    node = node.parentNode;
-  }
-}
-
-/**
- * Get the point associated with a shape point node.
- *
- * @param {DOMNode} node
- *        A shape point node
- * @returns {String} The point associated with the given node.
- */
-function getShapePoint(node) {
-  const classList = node.classList;
-  let point = node.dataset.point;
-  // Inset points use classes instead of data because a single span can represent
-  // multiple points.
-  const insetClasses = [];
-  classList.forEach(className => {
-    if (INSET_POINT_TYPES.includes(className)) {
-      insetClasses.push(className);
-    }
-  });
-  if (insetClasses.length > 0) {
-    point = insetClasses.join(",");
-  }
-  return point;
-}
-
 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);
--- a/devtools/client/inspector/rules/utils/utils.js
+++ b/devtools/client/inspector/rules/utils/utils.js
@@ -1,22 +1,301 @@
 /* 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 {
+  VIEW_NODE_FONT_TYPE,
+  VIEW_NODE_IMAGE_URL_TYPE,
+  VIEW_NODE_INACTIVE_CSS,
+  VIEW_NODE_LOCATION_TYPE,
+  VIEW_NODE_PROPERTY_TYPE,
+  VIEW_NODE_SELECTOR_TYPE,
+  VIEW_NODE_SHAPE_POINT_TYPE,
+  VIEW_NODE_SHAPE_SWATCH,
+  VIEW_NODE_VALUE_TYPE,
+  VIEW_NODE_VARIABLE_TYPE,
+} = require("devtools/client/inspector/shared/node-types");
+const INSET_POINT_TYPES = ["top", "right", "bottom", "left"];
+
+/**
+ * Returns the [Rule] object associated with the given node.
+ *
+ * @param {DOMNode} node
+ *        The node which we want to find the [Rule] object for
+ * @param {ElementStyle} elementStyle
+ *        The [ElementStyle] associated with the selected element
+ * @return {Rule|null} associated with the given node
+ */
+function getRuleFromNode(node, elementStyle) {
+  const ruleEl = node.closest(".ruleview-rule[data-rule-id]");
+  const ruleId = ruleEl ? ruleEl.dataset.ruleId : null;
+  return ruleId ? elementStyle.getRule(ruleId) : null;
+}
+
+/**
+ * Returns the [TextProperty] object associated with the given node.
+ *
+ * @param {DOMNode} node
+ *        The node which we want to find the [TextProperty] object for
+ * @param {Rule|null} rule
+ *        The [Rule] associated with the given node
+ * @return {TextProperty|null} associated with the given node
+ */
+function getDeclarationFromNode(node, rule) {
+  if (!rule) {
+    return null;
+  }
+
+  const declarationEl = node.closest(".ruleview-property[data-declaration-id]");
+  const declarationId = declarationEl
+    ? declarationEl.dataset.declarationId
+    : null;
+  return rule ? rule.getDeclaration(declarationId) : null;
+}
+
+/**
+ * Get the type of a given node in the Rules view.
+ *
+ * @param {DOMNode} node
+ *        The node which we want information about
+ * @param {ElementStyle} elementStyle
+ *        The ElementStyle to which this rule belongs
+ * @return {Object|null} containing the following props:
+ * - rule {Rule} The Rule object.
+ * - type {String} One of the VIEW_NODE_XXX_TYPE const in
+ *   client/inspector/shared/node-types.
+ * - value {Object} Depends on the type of the node.
+ * - view {String} Always "rule" to indicate the rule view.
+ * Otherwise, returns null if the node isn't anything we care about.
+ */
+// eslint-disable-next-line complexity
+function getNodeInfo(node, elementStyle) {
+  if (!node) {
+    return null;
+  }
+
+  const rule = getRuleFromNode(node, elementStyle);
+  const declaration = getDeclarationFromNode(node, rule);
+  const classList = node.classList;
+
+  let type, value;
+
+  if (declaration && classList.contains("ruleview-propertyname")) {
+    type = VIEW_NODE_PROPERTY_TYPE;
+    value = {
+      property: node.textContent,
+      value: getPropertyNameAndValue(node).value,
+      enabled: declaration.enabled,
+      overridden: declaration.overridden,
+      pseudoElement: rule.pseudoElement,
+      sheetHref: rule.domRule.href,
+      textProperty: declaration,
+    };
+  } else if (declaration && classList.contains("ruleview-propertyvalue")) {
+    type = VIEW_NODE_VALUE_TYPE;
+    value = {
+      property: getPropertyNameAndValue(node).name,
+      value: node.textContent,
+      enabled: declaration.enabled,
+      overridden: declaration.overridden,
+      pseudoElement: rule.pseudoElement,
+      sheetHref: rule.domRule.href,
+      textProperty: declaration,
+    };
+  } else if (declaration && classList.contains("ruleview-font-family")) {
+    const {
+      name: propertyName,
+      value: propertyValue,
+    } = getPropertyNameAndValue(node);
+    type = VIEW_NODE_FONT_TYPE;
+    value = {
+      property: propertyName,
+      value: propertyValue,
+      enabled: declaration.enabled,
+      overridden: declaration.overridden,
+      pseudoElement: rule.pseudoElement,
+      sheetHref: rule.domRule.href,
+      textProperty: declaration,
+    };
+  } else if (declaration && classList.contains("ruleview-shape-point")) {
+    type = VIEW_NODE_SHAPE_POINT_TYPE;
+    value = {
+      property: getPropertyNameAndValue(node).name,
+      value: node.textContent,
+      enabled: declaration.enabled,
+      overridden: declaration.overridden,
+      pseudoElement: rule.pseudoElement,
+      sheetHref: rule.domRule.href,
+      textProperty: declaration,
+      toggleActive: getShapeToggleActive(node),
+      point: getShapePoint(node),
+    };
+  } else if (declaration && classList.contains("ruleview-unused-warning")) {
+    type = VIEW_NODE_INACTIVE_CSS;
+    value = declaration.isUsed();
+  } else if (declaration && classList.contains("ruleview-shapeswatch")) {
+    type = VIEW_NODE_SHAPE_SWATCH;
+    value = {
+      enabled: declaration.enabled,
+      overridden: declaration.overridden,
+      textProperty: declaration,
+    };
+  } else if (
+    declaration &&
+    (classList.contains("ruleview-variable") ||
+      classList.contains("ruleview-unmatched-variable"))
+  ) {
+    type = VIEW_NODE_VARIABLE_TYPE;
+    value = {
+      property: getPropertyNameAndValue(node).name,
+      value: node.textContent,
+      enabled: declaration.enabled,
+      overridden: declaration.overridden,
+      pseudoElement: rule.pseudoElement,
+      sheetHref: rule.domRule.href,
+      textProperty: declaration,
+      variable: node.dataset.variable,
+    };
+  } else if (
+    declaration &&
+    classList.contains("theme-link") &&
+    !classList.contains("ruleview-rule-source")
+  ) {
+    type = VIEW_NODE_IMAGE_URL_TYPE;
+    value = {
+      property: getPropertyNameAndValue(node).name,
+      value: node.parentNode.textContent,
+      url: node.href,
+      enabled: declaration.enabled,
+      overridden: declaration.overridden,
+      pseudoElement: rule.pseudoElement,
+      sheetHref: rule.domRule.href,
+      textProperty: declaration,
+    };
+  } else if (
+    classList.contains("ruleview-selector-unmatched") ||
+    classList.contains("ruleview-selector-matched") ||
+    classList.contains("ruleview-selectorcontainer") ||
+    classList.contains("ruleview-selector") ||
+    classList.contains("ruleview-selector-attribute") ||
+    classList.contains("ruleview-selector-pseudo-class") ||
+    classList.contains("ruleview-selector-pseudo-class-lock")
+  ) {
+    type = VIEW_NODE_SELECTOR_TYPE;
+    value = rule.selectorText;
+  } else if (
+    classList.contains("ruleview-rule-source") ||
+    classList.contains("ruleview-rule-source-label")
+  ) {
+    type = VIEW_NODE_LOCATION_TYPE;
+    value = rule.sheet && rule.sheet.href ? rule.sheet.href : rule.title;
+  } else {
+    return null;
+  }
+
+  return {
+    rule,
+    type,
+    value,
+    view: "rule",
+  };
+}
+
+/**
+ * Walk up the DOM from a given node until a parent property holder is found,
+ * and return the textContent for the name and value nodes.
+ * Stops at the first property found, so if node is inside the computed property
+ * list, the computed property will be returned
+ *
+ * @param {DOMNode} node
+ *        The node to start from
+ * @return {Object} {name, value}
+ */
+function getPropertyNameAndValue(node) {
+  while (node && node.classList) {
+    // Check first for ruleview-computed since it's the deepest
+    if (
+      node.classList.contains("ruleview-computed") ||
+      node.classList.contains("ruleview-property")
+    ) {
+      return {
+        name: node.querySelector(".ruleview-propertyname").textContent,
+        value: node.querySelector(".ruleview-propertyvalue").textContent,
+      };
+    }
+
+    node = node.parentNode;
+  }
+
+  return null;
+}
+
+/**
+ * Walk up the DOM from a given node until a parent property holder is found,
+ * and return an active shape toggle if one exists.
+ *
+ * @param {DOMNode} node
+ *        The node to start from
+ * @returns {DOMNode} The active shape toggle node, if one exists.
+ */
+function getShapeToggleActive(node) {
+  while (node && node.classList) {
+    // Check first for ruleview-computed since it's the deepest
+    if (
+      node.classList.contains("ruleview-computed") ||
+      node.classList.contains("ruleview-property")
+    ) {
+      return node.querySelector(".ruleview-shapeswatch.active");
+    }
+
+    node = node.parentNode;
+  }
+
+  return null;
+}
+
+/**
+ * Get the point associated with a shape point node.
+ *
+ * @param {DOMNode} node
+ *        A shape point node
+ * @returns {String} The point associated with the given node.
+ */
+function getShapePoint(node) {
+  const classList = node.classList;
+  let point = node.dataset.point;
+  // Inset points use classes instead of data because a single span can represent
+  // multiple points.
+  const insetClasses = [];
+  classList.forEach(className => {
+    if (INSET_POINT_TYPES.includes(className)) {
+      insetClasses.push(className);
+    }
+  });
+  if (insetClasses.length > 0) {
+    point = insetClasses.join(",");
+  }
+  return point;
+}
+
 /**
  * Returns true if the given property value is a CSS variables and contains the given
  * variable name, and false otherwise.
  *
- * @param {String} - propertyValue
+ * @param {String} propertyValue
  *        CSS property value (e.g. "var(--color)")
- * @param {String} - variableName
+ * @param {String} variableName
  *        CSS variable name (e.g. "--color")
  * @return {Boolean}
  */
 function hasCSSVariable(propertyValue, variableName) {
   const regex = new RegExp(`(^|\\W)var\\(${variableName}\\s*[,)]`);
   return regex.test(propertyValue);
 }
 
-module.exports = { hasCSSVariable };
+module.exports = {
+  getNodeInfo,
+  getRuleFromNode,
+  hasCSSVariable,
+};
--- a/devtools/client/inspector/rules/views/rule-editor.js
+++ b/devtools/client/inspector/rules/views/rule-editor.js
@@ -134,16 +134,17 @@ RuleEditor.prototype = {
     // Do not allow editing anonymousselectors until we can
     // detect mutations on  pseudo elements in Bug 1034110.
     return trait && !this.rule.elementStyle.element.isAnonymous;
   },
 
   _create: function() {
     this.element = this.doc.createElement("div");
     this.element.className = "ruleview-rule devtools-monospace";
+    this.element.dataset.ruleId = this.rule.domRule.actorID;
     this.element.setAttribute("uneditable", !this.isEditable);
     this.element.setAttribute("unmatched", this.rule.isUnmatched);
     this.element._ruleEditor = this;
 
     // Give a relative position for the inplace editor's measurement
     // span to be placed absolutely against.
     this.element.style.position = "relative";
 
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -148,16 +148,17 @@ TextPropertyEditor.prototype = {
   },
 
   /**
    * Create the property editor's DOM.
    */
   _create: function() {
     this.element = this.doc.createElementNS(HTML_NS, "li");
     this.element.classList.add("ruleview-property");
+    this.element.dataset.declarationId = this.prop.id;
     this.element._textPropertyEditor = this;
 
     this.container = createChild(this.element, "div", {
       class: "ruleview-propertycontainer inline-tooltip-container",
     });
 
     // The enable checkbox will disable or enable the rule.
     this.enable = createChild(this.container, "input", {
--- a/devtools/client/inspector/shared/style-inspector-menu.js
+++ b/devtools/client/inspector/shared/style-inspector-menu.js
@@ -16,16 +16,22 @@ const {
 loader.lazyRequireGetter(this, "Menu", "devtools/client/framework/menu");
 loader.lazyRequireGetter(
   this,
   "MenuItem",
   "devtools/client/framework/menu-item"
 );
 loader.lazyRequireGetter(
   this,
+  "getRuleFromNode",
+  "devtools/client/inspector/rules/utils/utils",
+  true
+);
+loader.lazyRequireGetter(
+  this,
   "clipboardHelper",
   "devtools/shared/platform/clipboard"
 );
 
 const STYLE_INSPECTOR_PROPERTIES =
   "devtools/shared/locales/styleinspector.properties";
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
@@ -35,23 +41,21 @@ const PREF_ORIG_SOURCES = "devtools.sour
 /**
  * Style inspector context menu
  *
  * @param {RuleView|ComputedView} view
  *        RuleView or ComputedView instance controlling this menu
  * @param {Object} options
  *        Option menu configuration
  */
-function StyleInspectorMenu(view, options) {
+function StyleInspectorMenu(view, { isRuleView = false } = {}) {
   this.view = view;
   this.inspector = this.view.inspector;
-  this.styleDocument = this.view.styleDocument;
-  this.styleWindow = this.view.styleWindow;
-
-  this.isRuleView = options.isRuleView;
+  this.styleWindow = this.view.styleWindow || this.view.doc.defaultView;
+  this.isRuleView = isRuleView;
 
   this._onAddNewRule = this._onAddNewRule.bind(this);
   this._onCopy = this._onCopy.bind(this);
   this._onCopyColor = this._onCopyColor.bind(this);
   this._onCopyImageDataUrl = this._onCopyImageDataUrl.bind(this);
   this._onCopyLocation = this._onCopyLocation.bind(this);
   this._onCopyDeclaration = this._onCopyDeclaration.bind(this);
   this._onCopyPropertyName = this._onCopyPropertyName.bind(this);
@@ -67,29 +71,27 @@ module.exports = StyleInspectorMenu;
 
 StyleInspectorMenu.prototype = {
   /**
    * Display the style inspector context menu
    */
   show: function(event) {
     try {
       this._openMenu({
-        target: event.explicitOriginalTarget,
+        target: event.target,
         screenX: event.screenX,
         screenY: event.screenY,
       });
     } catch (e) {
       console.error(e);
     }
   },
 
   _openMenu: function({ target, screenX = 0, screenY = 0 } = {}) {
-    // In the sidebar we do not have this.styleDocument.popupNode
-    // so we need to save the node ourselves.
-    this.styleDocument.popupNode = target;
+    this.currentTarget = target;
     this.styleWindow.focus();
 
     const menu = new Menu();
 
     const menuitemCopy = new MenuItem({
       label: STYLE_INSPECTOR_L10N.getStr("styleinspector.contextmenu.copy"),
       accesskey: STYLE_INSPECTOR_L10N.getStr(
         "styleinspector.contextmenu.copy.accessKey"
@@ -305,87 +307,80 @@ StyleInspectorMenu.prototype = {
    * value and saves the color to this._colorToCopy.
    *
    * @return {Boolean}
    *         true if click on color opened the popup, false otherwise.
    */
   _isColorPopup: function() {
     this._colorToCopy = "";
 
-    let container = this._getClickedNode();
+    const container = this._getClickedNode();
     if (!container) {
       return false;
     }
 
-    const isColorNode = el => el.dataset && "color" in el.dataset;
-
-    while (!isColorNode(container)) {
-      container = container.parentNode;
-      if (!container) {
-        return false;
-      }
+    const colorNode = container.closest("[data-color");
+    if (!colorNode) {
+      return false;
     }
 
-    this._colorToCopy = container.dataset.color;
+    this._colorToCopy = colorNode.dataset.color;
     return true;
   },
 
-  _isPropertyName: function() {
-    const nodeInfo = this._getClickedNodeInfo();
-    if (!nodeInfo) {
-      return false;
-    }
-    return nodeInfo.type == VIEW_NODE_PROPERTY_TYPE;
-  },
-
   /**
    * Check if the current node (clicked node) is an image URL
    *
    * @return {Boolean} true if the node is an image url
    */
   _isImageUrl: function() {
     const nodeInfo = this._getClickedNodeInfo();
     if (!nodeInfo) {
       return false;
     }
     return nodeInfo.type == VIEW_NODE_IMAGE_URL_TYPE;
   },
 
   /**
-   * Get the DOM Node container for the current popupNode.
-   * If popupNode is a textNode, return the parent node, otherwise return
-   * popupNode itself.
+   * Get the DOM Node container for the current target node.
+   * If the target node is a text node, return the parent node, otherwise return
+   * the target node itself.
    *
    * @return {DOMNode}
    */
   _getClickedNode: function() {
-    let container = null;
-    const node = this.styleDocument.popupNode;
+    const node = this.currentTarget;
 
-    if (node) {
-      const isTextNode = node.nodeType == node.TEXT_NODE;
-      container = isTextNode ? node.parentElement : node;
+    if (!node) {
+      return null;
     }
 
-    return container;
+    return node.nodeType === node.TEXT_NODE ? node.parentElement : node;
   },
 
   /**
    * Select all text.
    */
   _onSelectAll: function() {
     const selection = this.styleWindow.getSelection();
-    selection.selectAllChildren(this.view.element);
+
+    if (this.isRuleView) {
+      selection.selectAllChildren(
+        this.currentTarget.closest("#ruleview-container-focusable")
+      );
+    } else {
+      selection.selectAllChildren(this.view.element);
+    }
   },
 
   /**
    * Copy the most recently selected color value to clipboard.
    */
   _onCopy: function() {
-    this.view.copySelection(this.styleDocument.popupNode);
+    this.view.copySelection(this.currentTarget);
   },
 
   /**
    * Copy the most recently selected color value to clipboard.
    */
   _onCopyColor: function() {
     clipboardHelper.copyString(this._colorToCopy);
   },
@@ -423,18 +418,20 @@ StyleInspectorMenu.prototype = {
     }
 
     clipboardHelper.copyString(message);
   },
 
   /**
    * Add a new rule to the current element.
    */
-  _onAddNewRule: function() {
-    this.view._onAddRule();
+  async _onAddNewRule() {
+    this.view.isNewRulesView
+      ? await this.view.onAddRule()
+      : this.view._onAddRule();
   },
 
   /**
    * Copy the rule source location of the current clicked node.
    */
   _onCopyLocation: function() {
     if (!this._clickedNodeInfo) {
       return;
@@ -476,42 +473,42 @@ StyleInspectorMenu.prototype = {
 
     clipboardHelper.copyString(this._clickedNodeInfo.value.value);
   },
 
   /**
    * Copy the rule of the current clicked node.
    */
   _onCopyRule: function() {
-    const ruleEditor = this.styleDocument.popupNode.parentNode.offsetParent
-      ._ruleEditor;
-    const rule = ruleEditor.rule;
+    const node = this._getClickedNode();
+    const elementStyle = this.view.isNewRulesView
+      ? this.view.elementStyle
+      : this.view._elementStyle;
+    const rule = getRuleFromNode(node, elementStyle);
     clipboardHelper.copyString(rule.stringifyRule());
   },
 
   /**
    * Copy the rule selector of the current clicked node.
    */
   _onCopySelector: function() {
     if (!this._clickedNodeInfo) {
       return;
     }
 
     clipboardHelper.copyString(this._clickedNodeInfo.value);
   },
 
   /**
-   *  Toggle the original sources pref.
+   * Toggle the original sources pref.
    */
   _onToggleOrigSources: function() {
     const isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
     Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
   },
 
   destroy: function() {
-    this.popupNode = null;
-    this.styleDocument.popupNode = null;
+    this.currentTarget = null;
     this.view = null;
     this.inspector = null;
-    this.styleDocument = null;
     this.styleWindow = null;
   },
 };
--- a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js
@@ -23,17 +23,16 @@ async function testView(viewId, inspecto
   info("Testing " + viewId);
 
   await inspector.sidebar.select(viewId);
   const view =
     inspector.getPanel(viewId).view || inspector.getPanel(viewId).computedView;
   await selectNode("div", inspector);
 
   testIsColorValueNode(view);
-  testIsColorPopupOnAllNodes(view);
   await clearCurrentNodeSelection(inspector);
 }
 
 /**
  * A function testing that isColorValueNode correctly detects nodes part of
  * color values.
  */
 function testIsColorValueNode(view) {
@@ -43,50 +42,16 @@ function testIsColorValueNode(view) {
 
   ok(colorNode, "Color node found");
   for (const node of iterateNodes(colorNode)) {
     ok(isColorValueNode(node), "Node is part of color value.");
   }
 }
 
 /**
- * A function testing that _isColorPopup returns a correct value for all nodes
- * in the view.
- */
-function testIsColorPopupOnAllNodes(view) {
-  const root = rootElement(view);
-  for (const node of iterateNodes(root)) {
-    testIsColorPopupOnNode(view, node);
-  }
-}
-
-/**
- * Test result of _isColorPopup with given node.
- * @param object view
- *               A CSSRuleView or CssComputedView instance.
- * @param Node node
- *             A node to check.
- */
-function testIsColorPopupOnNode(view, node) {
-  info("Testing node " + node);
-  view.styleDocument.popupNode = node;
-  view.contextMenu._colorToCopy = "";
-
-  const result = view.contextMenu._isColorPopup();
-  const correct = isColorValueNode(node);
-
-  is(result, correct, "_isColorPopup returned the expected value " + correct);
-  is(
-    view.contextMenu._colorToCopy,
-    correct ? "rgb(18, 58, 188)" : "",
-    "_colorToCopy was set to the expected value"
-  );
-}
-
-/**
  * Check if a node is part of color value i.e. it has parent with a 'data-color'
  * attribute.
  */
 function isColorValueNode(node) {
   let container = node.nodeType == node.TEXT_NODE ? node.parentElement : node;
 
   const isColorNode = el => el.dataset && "color" in el.dataset;
 
--- a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_02.js
@@ -64,20 +64,20 @@ async function testManualEdit(inspector,
   EventUtils.sendString(newColor + ";", view.styleWindow);
   await onBlur;
   await wait(1);
 
   const colorValueElement = getRuleViewProperty(view, "div", "color").valueSpan
     .firstChild;
   is(colorValueElement.dataset.color, newColor, "data-color was updated");
 
-  view.styleDocument.popupNode = colorValueElement;
+  const contextMenu = view.contextMenu;
+  contextMenu.currentTarget = colorValueElement;
+  contextMenu._isColorPopup();
 
-  const contextMenu = view.contextMenu;
-  contextMenu._isColorPopup();
   is(contextMenu._colorToCopy, newColor, "_colorToCopy has the new value");
 }
 
 async function testColorPickerEdit(inspector, view) {
   info("Testing colors edited via color picker");
   await selectNode("div", inspector);
 
   const swatchElement = getRuleViewProperty(
@@ -96,14 +96,15 @@ async function testColorPickerEdit(inspe
   const rgbaColorText = "rgba(83, 183, 89, 1)";
   await simulateColorPickerChange(view, picker, rgbaColor);
 
   is(
     swatchElement.parentNode.dataset.color,
     rgbaColorText,
     "data-color was updated"
   );
-  view.styleDocument.popupNode = swatchElement;
 
   const contextMenu = view.contextMenu;
+  contextMenu.currentTarget = swatchElement;
   contextMenu._isColorPopup();
+
   is(contextMenu._colorToCopy, rgbaColorText, "_colorToCopy has the new value");
 }