Bug 1467572 - Part 9: Use native context menu instead of select elements in the reload condition menu of RDM. r=Honza draft
authorGabriel Luong <gabriel.luong@gmail.com>
Tue, 14 Aug 2018 17:54:54 -0400 (2018-08-14)
changeset 829214 b70dfd53c9d911adcc7ccb55054fee34345e9b14
parent 829213 69a5f7dde37abbf7f03af246bdac173936faa2b2
child 829215 b3a1517ec227122c88b9f97248ee7146c542f557
push id118750
push userbmo:gl@mozilla.com
push dateTue, 14 Aug 2018 21:55:43 +0000 (2018-08-14)
reviewersHonza
bugs1467572
milestone63.0a1
Bug 1467572 - Part 9: Use native context menu instead of select elements in the reload condition menu of RDM. r=Honza - Converts the reload condition <select> to use native context menu - Converts the showMenu util to be reusable by both RDM and the Network Monitor - Renames getToplevelWindow to getTopLevelWindow - Renames the reload condition menu to be a setting menu in RDM according to the designs - Uses the new photon setting icon in RDM MozReview-Commit-ID: FBSQMhs7Sxx
devtools/client/jar.mn
devtools/client/locales/en-US/responsive.properties
devtools/client/netmonitor/src/components/Toolbar.js
devtools/client/netmonitor/src/utils/menu.js
devtools/client/netmonitor/src/utils/moz.build
devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
devtools/client/netmonitor/webpack.config.js
devtools/client/responsive.html/actions/screenshot.js
devtools/client/responsive.html/components/Browser.js
devtools/client/responsive.html/components/ReloadConditions.js
devtools/client/responsive.html/components/SettingsMenu.js
devtools/client/responsive.html/components/ToggleMenu.js
devtools/client/responsive.html/components/Toolbar.js
devtools/client/responsive.html/components/moz.build
devtools/client/responsive.html/index.css
devtools/client/responsive.html/utils/window.js
devtools/client/shared/components/menu/moz.build
devtools/client/shared/components/menu/utils.js
devtools/client/themes/images/settings.svg
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -259,16 +259,17 @@ devtools.jar:
     skin/images/security-state-weak.svg (themes/images/security-state-weak.svg)
     skin/images/diff.svg (themes/images/diff.svg)
     skin/images/import.svg (themes/images/import.svg)
     skin/images/pane-collapse.svg (themes/images/pane-collapse.svg)
     skin/images/pane-expand.svg (themes/images/pane-expand.svg)
     skin/images/help.svg (themes/images/help.svg)
     skin/images/read-only.svg (themes/images/read-only.svg)
     skin/images/reveal.svg (themes/images/reveal.svg)
+    skin/images/settings.svg (themes/images/settings.svg)
 
     # Debugger
     skin/images/debugger/angular.svg (themes/images/debugger/angular.svg)
     skin/images/debugger/arrow.svg (themes/images/debugger/arrow.svg)
     skin/images/debugger/back.svg (themes/images/debugger/back.svg)
     skin/images/debugger/blackBox.svg (themes/images/debugger/blackBox.svg)
     skin/images/debugger/breakpoint.svg (themes/images/debugger/breakpoint.svg)
     skin/images/debugger/close.svg (themes/images/debugger/close.svg)
--- a/devtools/client/locales/en-US/responsive.properties
+++ b/devtools/client/locales/en-US/responsive.properties
@@ -119,24 +119,16 @@ responsive.deviceAdderSave=Save
 # device.  %4$S is the user agent of the device.  %5$S is a boolean value
 # noting whether touch input is supported.
 responsive.deviceDetails=Size: %1$S x %2$S\nDPR: %3$S\nUA: %4$S\nTouch: %5$S
 
 # LOCALIZATION NOTE (responsive.devicePixelRatioOption): UI option in a menu to configure
 # the device pixel ratio. %1$S is the devicePixelRatio value of the device.
 responsive.devicePixelRatioOption=DPR: %1$S
 
-# LOCALIZATION NOTE (responsive.reloadConditions.label): Label on button to open a menu
-# used to choose whether to reload the page automatically when certain actions occur.
-responsive.reloadConditions.label=Reload when…
-
-# LOCALIZATION NOTE (responsive.reloadConditions.title): Title on button to open a menu
-# used to choose whether to reload the page automatically when certain actions occur.
-responsive.reloadConditions.title=Choose whether to reload the page automatically when certain actions occur
-
 # LOCALIZATION NOTE (responsive.reloadConditions.touchSimulation): Label on checkbox used
 # to select whether to reload when touch simulation is toggled.
 responsive.reloadConditions.touchSimulation=Reload when touch simulation is toggled
 
 # LOCALIZATION NOTE (responsive.reloadConditions.userAgent): Label on checkbox used
 # to select whether to reload when user agent is changed.
 responsive.reloadConditions.userAgent=Reload when user agent is changed
 
--- a/devtools/client/netmonitor/src/components/Toolbar.js
+++ b/devtools/client/netmonitor/src/components/Toolbar.js
@@ -51,17 +51,17 @@ const ENABLE_PERSISTENT_LOGS_LABEL =
   L10N.getStr("netmonitor.toolbar.enablePersistentLogs.label");
 const DISABLE_CACHE_TOOLTIP = L10N.getStr("netmonitor.toolbar.disableCache.tooltip");
 const DISABLE_CACHE_LABEL = L10N.getStr("netmonitor.toolbar.disableCache.label");
 const NO_THROTTLING_LABEL = new LocalizationHelper(
   "devtools/client/locales/network-throttling.properties"
   ).getStr("responsive.noThrottling");
 
 // Menu
-loader.lazyRequireGetter(this, "showMenu", "devtools/client/netmonitor/src/utils/menu", true);
+loader.lazyRequireGetter(this, "showMenu", "devtools/client/shared/components/menu/utils", true);
 loader.lazyRequireGetter(this, "HarMenuUtils", "devtools/client/netmonitor/src/har/har-menu-utils", true);
 
 // Throttling
 const Types = require("devtools/client/shared/components/throttling/types");
 const throttlingProfiles = require("devtools/client/shared/components/throttling/profiles");
 const { changeNetworkThrottling } = require("devtools/client/shared/components/throttling/actions");
 
 /**
--- a/devtools/client/netmonitor/src/utils/moz.build
+++ b/devtools/client/netmonitor/src/utils/moz.build
@@ -10,15 +10,14 @@ DIRS += [
 DevToolsModules(
     'filter-autocomplete-provider.js',
     'filter-predicates.js',
     'filter-text-utils.js',
     'format-utils.js',
     'headers-provider.js',
     'l10n.js',
     'mdn-utils.js',
-    'menu.js',
     'open-request-in-tab.js',
     'prefs.js',
     'request-utils.js',
     'sort-predicates.js',
     'sort-utils.js'
 )
--- a/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListContextMenu.js
@@ -12,17 +12,17 @@ const {
   getUrlQuery,
   getUrlBaseName,
   parseQueryString,
 } = require("../utils/request-utils");
 
 loader.lazyRequireGetter(this, "Curl", "devtools/client/shared/curl", true);
 loader.lazyRequireGetter(this, "saveAs", "devtools/client/shared/file-saver", true);
 loader.lazyRequireGetter(this, "copyString", "devtools/shared/platform/clipboard", true);
-loader.lazyRequireGetter(this, "showMenu", "devtools/client/netmonitor/src/utils/menu", true);
+loader.lazyRequireGetter(this, "showMenu", "devtools/client/shared/components/menu/utils", true);
 loader.lazyRequireGetter(this, "openRequestInTab", "devtools/client/netmonitor/src/utils/firefox/open-request-in-tab", true);
 loader.lazyRequireGetter(this, "HarMenuUtils", "devtools/client/netmonitor/src/har/har-menu-utils", true);
 
 class RequestListContextMenu {
   constructor(props) {
     this.props = props;
   }
 
--- a/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
+++ b/devtools/client/netmonitor/src/widgets/RequestListHeaderContextMenu.js
@@ -1,15 +1,15 @@
 /* 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 { showMenu } = require("devtools/client/netmonitor/src/utils/menu");
+const { showMenu } = require("devtools/client/shared/components/menu/utils");
 const { HEADERS } = require("../constants");
 const { L10N } = require("../utils/l10n");
 
 const stringMap = HEADERS
   .filter((header) => header.hasOwnProperty("label"))
   .reduce((acc, { name, label }) => Object.assign(acc, { [name]: label }), {});
 
 const subMenuMap = HEADERS
--- a/devtools/client/netmonitor/webpack.config.js
+++ b/devtools/client/netmonitor/webpack.config.js
@@ -68,17 +68,18 @@ const webpackConfig = {
       "node_modules",
     ],
     alias: {
       "Services": "devtools-modules/src/Services",
       "react": path.join(__dirname, "node_modules/react"),
 
       "devtools/client/framework/devtools": path.join(__dirname, "../../client/shared/webpack/shims/framework-devtools-shim"),
       "devtools/client/framework/menu": "devtools-modules/src/menu",
-      "devtools/client/netmonitor/src/utils/menu": "devtools-contextmenu",
+
+      "devtools/client/shared/components/menu/utils": "devtools-contextmenu",
 
       "devtools/client/shared/vendor/react": "react",
       "devtools/client/shared/vendor/react-dom": "react-dom",
       "devtools/client/shared/vendor/react-redux": "react-redux",
       "devtools/client/shared/vendor/redux": "redux",
       "devtools/client/shared/vendor/reselect": "reselect",
       "devtools/client/shared/vendor/jszip": "jszip",
 
--- a/devtools/client/responsive.html/actions/screenshot.js
+++ b/devtools/client/responsive.html/actions/screenshot.js
@@ -7,17 +7,17 @@
 "use strict";
 
 const {
   TAKE_SCREENSHOT_START,
   TAKE_SCREENSHOT_END,
 } = require("./index");
 
 const { getFormatStr } = require("../utils/l10n");
-const { getToplevelWindow } = require("../utils/window");
+const { getTopLevelWindow } = require("../utils/window");
 const e10s = require("../utils/e10s");
 const Services = require("Services");
 
 const CAMERA_AUDIO_URL = "resource://devtools/client/themes/audio/shutter.wav";
 
 const animationFrame = () => new Promise(resolve => {
   window.requestAnimationFrame(resolve);
 });
@@ -35,17 +35,17 @@ function getFileName() {
 
 function createScreenshotFor(node) {
   const mm = node.frameLoader.messageManager;
 
   return e10s.request(mm, "RequestScreenshot");
 }
 
 function saveToFile(data, filename) {
-  const chromeWindow = getToplevelWindow(window);
+  const chromeWindow = getTopLevelWindow(window);
   const chromeDocument = chromeWindow.document;
 
   // append .png extension to filename if it doesn't exist
   filename = filename.replace(/\.png$|$/i, ".png");
 
   chromeWindow.saveURL(data, filename, null,
                         true, true,
                         chromeDocument.documentURIObject, chromeDocument);
--- a/devtools/client/responsive.html/components/Browser.js
+++ b/devtools/client/responsive.html/components/Browser.js
@@ -9,17 +9,17 @@
 const Services = require("Services");
 const flags = require("devtools/shared/flags");
 const { PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const e10s = require("../utils/e10s");
 const message = require("../utils/message");
-const { getToplevelWindow } = require("../utils/window");
+const { getTopLevelWindow } = require("../utils/window");
 
 const FRAME_SCRIPT = "resource://devtools/client/responsive.html/browser/content.js";
 
 class Browser extends PureComponent {
   /**
    * This component is not allowed to depend directly on frequently changing data (width,
    * height). Any changes in props would cause the <iframe> to be removed and added again,
    * throwing away the current state of the page.
@@ -109,17 +109,17 @@ class Browser extends PureComponent {
     // since it still needs to do async work before the content is actually
     // resized to match.
     e10s.on(mm, "OnContentResize", onContentResize);
 
     const ready = e10s.once(mm, "ChildScriptReady");
     mm.loadFrameScript(FRAME_SCRIPT, true);
     await ready;
 
-    const browserWindow = getToplevelWindow(window);
+    const browserWindow = getTopLevelWindow(window);
     const requiresFloatingScrollbars =
       !browserWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
 
     await e10s.request(mm, "Start", {
       requiresFloatingScrollbars,
       // Tests expect events on resize to wait for various size changes
       notifyOnResize: flags.testing,
     });
rename from devtools/client/responsive.html/components/ReloadConditions.js
rename to devtools/client/responsive.html/components/SettingsMenu.js
--- a/devtools/client/responsive.html/components/ReloadConditions.js
+++ b/devtools/client/responsive.html/components/SettingsMenu.js
@@ -1,50 +1,73 @@
 /* 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 { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
-const ToggleMenu = createFactory(require("./ToggleMenu"));
-
 const { getStr } = require("../utils/l10n");
 const Types = require("../types");
 
-class ReloadConditions extends PureComponent {
+loader.lazyRequireGetter(this, "showMenu", "devtools/client/shared/components/menu/utils", true);
+
+class SettingsMenu extends PureComponent {
   static get propTypes() {
     return {
       reloadConditions: PropTypes.shape(Types.reloadConditions).isRequired,
       onChangeReloadCondition: PropTypes.func.isRequired,
     };
   }
 
-  render() {
+  constructor(props) {
+    super(props);
+    this.onToggleSettingMenu = this.onToggleSettingMenu.bind(this);
+  }
+
+  onToggleSettingMenu(event) {
     const {
       reloadConditions,
       onChangeReloadCondition,
     } = this.props;
 
-    return ToggleMenu({
-      id: "reload-conditions-menu",
-      items: [
-        {
-          id: "touchSimulation",
-          label: getStr("responsive.reloadConditions.touchSimulation"),
-          checked: reloadConditions.touchSimulation,
+    const menuItems = [
+      {
+        id: "touchSimulation",
+        checked: reloadConditions.touchSimulation,
+        label: getStr("responsive.reloadConditions.touchSimulation"),
+        type: "checkbox",
+        click: () => {
+          onChangeReloadCondition("touchSimulation", !reloadConditions.touchSimulation);
+        },
+      },
+      {
+        id: "userAgent",
+        checked: reloadConditions.userAgent,
+        label: getStr("responsive.reloadConditions.userAgent"),
+        type: "checkbox",
+        click: () => {
+          onChangeReloadCondition("userAgent", !reloadConditions.userAgent);
         },
-        {
-          id: "userAgent",
-          label: getStr("responsive.reloadConditions.userAgent"),
-          checked: reloadConditions.userAgent,
-        },
-      ],
-      label: getStr("responsive.reloadConditions.label"),
-      title: getStr("responsive.reloadConditions.title"),
-      onChange: onChangeReloadCondition,
+      },
+    ];
+
+    showMenu(menuItems, {
+      button: event.target,
+      useTopLevelWindow: true,
     });
   }
+
+  render() {
+    return (
+      dom.button({
+        id: "settings-button",
+        className: "devtools-button",
+        onClick: this.onToggleSettingMenu,
+      })
+    );
+  }
 }
 
-module.exports = ReloadConditions;
+module.exports = SettingsMenu;
deleted file mode 100644
--- a/devtools/client/responsive.html/components/ToggleMenu.js
+++ /dev/null
@@ -1,130 +0,0 @@
-/* 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 { PureComponent } = require("devtools/client/shared/vendor/react");
-const dom = require("devtools/client/shared/vendor/react-dom-factories");
-const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
-
-const MenuItem = {
-  id: PropTypes.string.isRequired,
-  label: PropTypes.string.isRequired,
-  checked: PropTypes.bool,
-};
-
-class ToggleMenu extends PureComponent {
-  static get propTypes() {
-    return {
-      id: PropTypes.string,
-      items: PropTypes.arrayOf(PropTypes.shape(MenuItem)).isRequired,
-      label: PropTypes.string,
-      title: PropTypes.string,
-      onChange: PropTypes.func.isRequired,
-    };
-  }
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      isOpen: false,
-    };
-
-    this.onItemChange = this.onItemChange.bind(this);
-    this.onToggleOpen = this.onToggleOpen.bind(this);
-  }
-
-  onItemChange({ target }) {
-    const {
-      onChange,
-    } = this.props;
-
-    // Close menu after changing an item
-    this.setState({
-      isOpen: false,
-    });
-
-    const id = target.name;
-    onChange(id, target.checked);
-  }
-
-  onToggleOpen() {
-    const {
-      isOpen,
-    } = this.state;
-
-    this.setState({
-      isOpen: !isOpen,
-    });
-  }
-
-  render() {
-    const {
-      id: menuID,
-      items,
-      label: toggleLabel,
-      title,
-    } = this.props;
-
-    const {
-      isOpen,
-    } = this.state;
-
-    const {
-      onItemChange,
-      onToggleOpen,
-    } = this;
-
-    const menuItems = items.map(({ id, label, checked }) => {
-      const inputID = `devtools-menu-item-${id}`;
-
-      return dom.div(
-        {
-          className: "devtools-menu-item",
-          key: id,
-        },
-        dom.input({
-          type: "checkbox",
-          id: inputID,
-          name: id,
-          checked,
-          onChange: onItemChange,
-        }),
-        dom.label({
-          htmlFor: inputID,
-        }, label)
-      );
-    });
-
-    let menuClass = "devtools-menu";
-    if (isOpen) {
-      menuClass += " opened";
-    }
-    const menu = dom.div(
-      {
-        className: menuClass,
-      },
-      menuItems
-    );
-
-    let buttonClass = "devtools-toggle-menu";
-    buttonClass += " toolbar-dropdown toolbar-button devtools-button";
-    if (isOpen || items.some(({ checked }) => checked)) {
-      buttonClass += " selected";
-    }
-    return dom.div(
-      {
-        id: menuID,
-        className: buttonClass,
-        title,
-        onClick: onToggleOpen,
-      },
-      toggleLabel,
-      menu
-    );
-  }
-}
-
-module.exports = ToggleMenu;
--- a/devtools/client/responsive.html/components/Toolbar.js
+++ b/devtools/client/responsive.html/components/Toolbar.js
@@ -6,17 +6,17 @@
 
 const { PureComponent, createFactory } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const DevicePixelRatioSelector = createFactory(require("./DevicePixelRatioSelector"));
 const DeviceSelector = createFactory(require("./DeviceSelector"));
 const NetworkThrottlingSelector = createFactory(require("devtools/client/shared/components/throttling/NetworkThrottlingSelector"));
-const ReloadConditions = createFactory(require("./ReloadConditions"));
+const SettingsMenu = createFactory(require("./SettingsMenu"));
 const ViewportDimension = createFactory(require("./ViewportDimension"));
 
 const { getStr } = require("../utils/l10n");
 const Types = require("../types");
 
 class Toolbar extends PureComponent {
   static get propTypes() {
     return {
@@ -121,17 +121,17 @@ class Toolbar extends PureComponent {
         { id: "toolbar-end-controls" },
         dom.button({
           id: "screenshot-button",
           className: "toolbar-button devtools-button",
           title: getStr("responsive.screenshot"),
           onClick: onScreenshot,
           disabled: screenshot.isCapturing,
         }),
-        ReloadConditions({
+        SettingsMenu({
           reloadConditions,
           onChangeReloadCondition,
         }),
         dom.div({ className: "devtools-separator" }),
         dom.button({
           id: "exit-button",
           className: "toolbar-button devtools-button",
           title: getStr("responsive.exit"),
--- a/devtools/client/responsive.html/components/moz.build
+++ b/devtools/client/responsive.html/components/moz.build
@@ -6,15 +6,14 @@
 
 DevToolsModules(
     'App.js',
     'Browser.js',
     'DeviceAdder.js',
     'DeviceModal.js',
     'DevicePixelRatioSelector.js',
     'DeviceSelector.js',
-    'ReloadConditions.js',
     'ResizableViewport.js',
-    'ToggleMenu.js',
+    'SettingsMenu.js',
     'Toolbar.js',
     'ViewportDimension.js',
     'Viewports.js',
 )
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -130,47 +130,16 @@ select > option:hover {
 select > option.divider {
   border-top: 1px solid var(--theme-splitter-color);
   height: 0px;
   padding: 0;
   font-size: 0px;
 }
 
 /**
- * Toggle Menu
- */
-
-.devtools-toggle-menu {
-  position: relative;
-}
-
-.devtools-toggle-menu .devtools-menu {
-  display: none;
-  flex-direction: column;
-  align-items: start;
-  position: absolute;
-  right: 0;
-  top: 100%;
-  z-index: 1;
-  padding: 5px;
-  border-radius: 2px;
-  background-color: var(--theme-toolbar-background);
-  box-shadow: var(--rdm-box-shadow);
-}
-
-.devtools-toggle-menu .devtools-menu.opened {
-  display: flex;
-}
-
-.devtools-toggle-menu .devtools-menu-item {
-  display: flex;
-  align-items: center;
-}
-
-/**
  * Common background for dropdowns like select and toggle menu
  */
 
 .toolbar-dropdown,
 .toolbar-dropdown.devtools-button,
 .toolbar-dropdown.devtools-button:hover:not(:empty):not(:disabled):not(.checked) {
   background-color: var(--theme-toolbar-background);
   background-image: var(--viewport-selection-arrow);
@@ -227,16 +196,20 @@ select > option.divider {
 #touch-simulation-button::before {
   background-image: url("./images/touch-events.svg");
 }
 
 #screenshot-button::before {
   background-image: url("./images/screenshot.svg");
 }
 
+#settings-button::before {
+  background-image: url("chrome://devtools/skin/images/settings.svg");
+}
+
 #exit-button::before {
   background-image: url("chrome://devtools/skin/images/close.svg");
 }
 
 #screenshot-button:disabled {
   filter: var(--theme-icon-checked-filter);
   opacity: 1 !important;
 }
--- a/devtools/client/responsive.html/utils/window.js
+++ b/devtools/client/responsive.html/utils/window.js
@@ -4,20 +4,20 @@
 
 "use strict";
 
 const Services = require("Services");
 
 /**
  * Returns the `nsIDOMWindow` toplevel window for any child/inner window
  */
-function getToplevelWindow(window) {
+function getTopLevelWindow(window) {
   return window.docShell.rootTreeItem.domWindow;
 }
-exports.getToplevelWindow = getToplevelWindow;
+exports.getTopLevelWindow = getTopLevelWindow;
 
 function getDOMWindowUtils(window) {
   return window.windowUtils;
 }
 exports.getDOMWindowUtils = getDOMWindowUtils;
 
 /**
  * Check if the given browser window has finished the startup.
--- a/devtools/client/shared/components/menu/moz.build
+++ b/devtools/client/shared/components/menu/moz.build
@@ -1,11 +1,12 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
-  'MenuButton.js',
-  'MenuItem.js',
-  'MenuList.js',
+    'MenuButton.js',
+    'MenuItem.js',
+    'MenuList.js',
+    'utils.js',
 )
rename from devtools/client/netmonitor/src/utils/menu.js
rename to devtools/client/shared/components/menu/utils.js
--- a/devtools/client/netmonitor/src/utils/menu.js
+++ b/devtools/client/shared/components/menu/utils.js
@@ -2,24 +2,33 @@
  * 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 Menu = require("devtools/client/framework/menu");
 const MenuItem = require("devtools/client/framework/menu-item");
 
+loader.lazyRequireGetter(this, "getTopLevelWindow", "devtools/client/responsive.html/utils/window", true);
+
 /**
  * Helper function for opening context menu.
  *
- * @param {Array} items List of menu items.
+ * @param {Array} items
+ *        List of menu items.
  * @param {Object} options:
- * @property {Number} screenX coordinate of the menu on the screen
- * @property {Number} screenY coordinate of the menu on the screen
- * @property {Object} button parent used to open the menu
+ * @property {Element} button
+ *           Button element used to open the menu.
+ * @property {Number} screenX
+ *           Screen x coordinate of the menu on the screen.
+ * @property {Number} screenY
+ *           Screen y coordinate of the menu on the screen.
+ * @property {Boolean} useTopLevelWindow
+ *           Whether or not the top level window needs to be fetched. This option is used
+ *           by RDM.
  */
 function showMenu(items, options) {
   if (items.length === 0) {
     return;
   }
 
   // Build the menu object from provided menu items.
   const menu = new Menu();
@@ -50,14 +59,21 @@ function showMenu(items, options) {
   if (options.button) {
     const button = options.button;
     const rect = button.getBoundingClientRect();
     const defaultView = button.ownerDocument.defaultView;
     screenX = rect.left + defaultView.mozInnerScreenX;
     screenY = rect.bottom + defaultView.mozInnerScreenY;
   }
 
-  menu.popup(screenX, screenY, { doc: window.parent.document });
+  let doc;
+  if (options.useTopLevelWindow) {
+    doc = getTopLevelWindow(window).document;
+  } else {
+    doc = window.parent.document;
+  }
+
+  menu.popup(screenX, screenY, { doc });
 }
 
 module.exports = {
   showMenu,
 };
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/settings.svg
@@ -0,0 +1,6 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg width="16" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+  <path fill="context-fill" fill-rule="evenodd" clip-rule="evenodd" d="M12 22a1 1 0 0 1-1-1v-3.083a5.966 5.966 0 0 1-2.477-1.026l-2.18 2.18a1 1 0 0 1-1.414-1.414l2.18-2.18A5.967 5.967 0 0 1 6.083 13H3a1 1 0 1 1 0-2h3.083a5.968 5.968 0 0 1 1.026-2.477l-2.18-2.18A1 1 0 0 1 6.343 4.93l2.18 2.18A5.968 5.968 0 0 1 11 6.083V3a1 1 0 1 1 2 0v3.083a5.967 5.967 0 0 1 2.476 1.026l2.18-2.18a1 1 0 1 1 1.415 1.414l-2.18 2.18A5.966 5.966 0 0 1 17.917 11H21a1 1 0 1 1 0 2h-3.083a5.966 5.966 0 0 1-1.026 2.476l2.18 2.18a1 1 0 0 1-1.414 1.415l-2.18-2.18A5.966 5.966 0 0 1 13 17.917V21a1 1 0 0 1-1 1zM8 12a4 4 0 1 1 8 0 4 4 0 0 1-8 0z"></path>
+</svg>