Bug 1461522 - Switch meatball menu to a doorhanger; r=jdescottes
authorBrian Birtles <birtles@gmail.com>
Thu, 28 Jun 2018 15:16:09 +0900
changeset 424955 cc6ec2789c9d50a686f4df5a70bf0f1173d1d32e
parent 424954 d6badfe124333064087ea2e878728faed94f8969
child 424956 2fafbc23c259ba61732ed064492271ec95248771
push id65903
push userbbirtles@mozilla.com
push dateWed, 04 Jul 2018 07:23:06 +0000
treeherderautoland@cc6ec2789c9d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdescottes
bugs1461522
milestone63.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 1461522 - Switch meatball menu to a doorhanger; r=jdescottes MozReview-Commit-ID: BHBJqiwOfHW
devtools/client/framework/components/MeatballMenu.js
devtools/client/framework/components/ToolboxToolbar.js
devtools/client/framework/components/moz.build
devtools/client/jar.mn
devtools/client/themes/images/command-console.svg
devtools/client/themes/images/command-noautohide.svg
devtools/client/themes/images/dock-bottom.svg
devtools/client/themes/images/dock-side-left.svg
devtools/client/themes/images/dock-side-right.svg
devtools/client/themes/images/dock-undock.svg
devtools/client/themes/images/tool-options-photon.svg
devtools/client/themes/toolbox.css
devtools/client/webconsole/test/mochitest/browser_webconsole_split.js
devtools/client/webconsole/test/mochitest/browser_webconsole_split_persist.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/components/MeatballMenu.js
@@ -0,0 +1,197 @@
+/* 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 PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const MenuItem = createFactory(
+  require("devtools/client/shared/components/menu/MenuItem")
+);
+const MenuList = createFactory(
+  require("devtools/client/shared/components/menu/MenuList")
+);
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { hr } = dom;
+const { openDocLink } = require("devtools/client/shared/link");
+
+class MeatballMenu extends PureComponent {
+  static get propTypes() {
+    return {
+      // The id of the currently selected tool, e.g. "inspector"
+      currentToolId: PropTypes.string,
+
+      // List of possible docking options.
+      hostTypes: PropTypes.arrayOf(
+        PropTypes.shape({
+          position: PropTypes.string.isRequired,
+          switchHost: PropTypes.func.isRequired,
+        })
+      ),
+
+      // Current docking type. Typically one of the position values in
+      // |hostTypes| but this is not always the case (e.g. when it is "custom").
+      currentHostType: PropTypes.string,
+
+      // Is the split console currently visible?
+      isSplitConsoleActive: PropTypes.bool,
+
+      // Are we disabling the behavior where pop-ups are automatically closed
+      // when clicking outside them?
+      //
+      // This is a tri-state value that may be true/false or undefined where
+      // undefined means that the option is not relevant in this context
+      // (i.e. we're not in a browser toolbox).
+      disableAutohide: PropTypes.bool,
+
+      // Function to turn the options panel on / off.
+      toggleOptions: PropTypes.func.isRequired,
+
+      // Function to turn the split console on / off.
+      toggleSplitConsole: PropTypes.func,
+
+      // Function to turn the disable pop-up autohide behavior on / off.
+      toggleNoAutohide: PropTypes.func,
+
+      // Localization interface.
+      L10N: PropTypes.object.isRequired,
+
+      // Callback function that will be invoked any time the component contents
+      // update in such a way that its bounding box might change.
+      onResize: PropTypes.func,
+    };
+  }
+
+  componentDidUpdate(prevProps) {
+    if (!this.props.onResize) {
+      return;
+    }
+
+    // We are only expecting the following kinds of dynamic changes when a popup
+    // is showing:
+    //
+    // - The "Disable pop-up autohide" menu item being added after the Browser
+    //   Toolbox is connected.
+    // - The split console label changing between "Show split console" and "Hide
+    //   split console".
+    // - The "Show/Hide split console" entry being added removed or removed.
+    //
+    // The latter two cases are only likely to be noticed when "Disable pop-up
+    // autohide" is active, but for completeness we handle them here.
+    const didChange =
+      typeof this.props.disableAutohide !== typeof prevProps.disableAutohide ||
+      this.props.currentToolId !== prevProps.currentToolId ||
+      this.props.isSplitConsoleActive !== prevProps.isSplitConsoleActive;
+
+    if (didChange) {
+      this.props.onResize();
+    }
+  }
+
+  render() {
+    const items = [];
+
+    // Dock options
+    for (const hostType of this.props.hostTypes) {
+      const l10nkey =
+        hostType.position === "window" ? "separateWindow" : hostType.position;
+      items.push(
+        MenuItem({
+          id: `toolbox-meatball-menu-dock-${hostType.position}`,
+          label: this.props.L10N.getStr(
+            `toolbox.meatballMenu.dock.${l10nkey}.label`
+          ),
+          onClick: () => hostType.switchHost(),
+          checked: hostType.position === this.props.currentHostType,
+          className: "iconic",
+        })
+      );
+    }
+
+    if (items.length) {
+      items.push(hr());
+    }
+
+    // Split console
+    if (this.props.currentToolId !== "webconsole") {
+      items.push(
+        MenuItem({
+          id: "toolbox-meatball-menu-splitconsole",
+          label: this.props.L10N.getStr(
+            `toolbox.meatballMenu.${
+              this.props.isSplitConsoleActive ? "hideconsole" : "splitconsole"
+            }.label`
+          ),
+          accelerator: "Esc",
+          onClick: this.props.toggleSplitConsole,
+          className: "iconic",
+        })
+      );
+    }
+
+    // Disable pop-up autohide
+    //
+    // If |disableAutohide| is undefined, it means this feature is not available
+    // in this context.
+    if (typeof this.props.disableAutohide !== "undefined") {
+      items.push(
+        MenuItem({
+          id: "toolbox-meatball-menu-noautohide",
+          label: this.props.L10N.getStr(
+            "toolbox.meatballMenu.noautohide.label"
+          ),
+          type: "checkbox",
+          checked: this.props.disableAutohide,
+          onClick: this.props.toggleNoAutohide,
+          className: "iconic",
+        })
+      );
+    }
+
+    // Settings
+    items.push(
+      MenuItem({
+        id: "toolbox-meatball-menu-settings",
+        label: this.props.L10N.getStr("toolbox.meatballMenu.settings.label"),
+        accelerator: this.props.L10N.getStr("toolbox.help.key"),
+        onClick: () => this.props.toggleOptions(),
+        className: "iconic",
+      })
+    );
+
+    items.push(hr());
+
+    // Getting started
+    items.push(
+      MenuItem({
+        id: "toolbox-meatball-menu-documentation",
+        label: this.props.L10N.getStr(
+          "toolbox.meatballMenu.documentation.label"
+        ),
+        onClick: () => {
+          openDocLink(
+            "https://developer.mozilla.org/docs/Tools?utm_source=devtools&utm_medium=tabbar-menu"
+          );
+        },
+      })
+    );
+
+    // Give feedback
+    items.push(
+      MenuItem({
+        id: "toolbox-meatball-menu-community",
+        label: this.props.L10N.getStr("toolbox.meatballMenu.community.label"),
+        onClick: () => {
+          openDocLink(
+            "https://discourse.mozilla.org/c/devtools?utm_source=devtools&utm_medium=tabbar-menu"
+          );
+        },
+      })
+    );
+
+    return MenuList({ id: "toolbox-meatball-menu" }, items);
+  }
+}
+
+module.exports = MeatballMenu;
--- a/devtools/client/framework/components/ToolboxToolbar.js
+++ b/devtools/client/framework/components/ToolboxToolbar.js
@@ -2,20 +2,19 @@
  * 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 { Component, 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 {div, button} = dom;
-const {openDocLink} = require("devtools/client/shared/link");
 
-const Menu = require("devtools/client/framework/menu");
-const MenuItem = require("devtools/client/framework/menu-item");
+const MeatballMenu = createFactory(require("devtools/client/framework/components/MeatballMenu"));
+const MenuButton = createFactory(require("devtools/client/shared/components/menu/MenuButton"));
 const ToolboxTabs = createFactory(require("devtools/client/framework/components/ToolboxTabs"));
 
 /**
  * This is the overall component for the toolbox toolbar. It is designed to not know how
  * the state is being managed, and attempts to be as pure as possible. The
  * ToolboxController component controls the changing state, and passes in everything as
  * props.
  */
@@ -104,17 +103,17 @@ class ToolboxToolbar extends Component {
       ? (
         div(
           {
             className: classnames.join(" ")
           },
           startButtons,
           ToolboxTabs(this.props),
           endButtons,
-          renderToolboxControls(this.props)
+          renderToolboxControls(this.props, this.refs)
         )
       )
       : div({ className: classnames.join(" ") });
   }
 }
 
 module.exports = ToolboxToolbar;
 
@@ -219,80 +218,94 @@ function renderToolboxButtons({focusedBu
  */
 function renderSeparator() {
   return div({className: "devtools-separator"});
 }
 
 /**
  * Render the toolbox control buttons. The following props are expected:
  *
- * @param {string} focusedButton
+ * @param {string} props.focusedButton
  *        The id of the focused button.
- * @param {Object[]} hostTypes
+ * @param {string} props.currentToolId
+ *        The id of the currently selected tool, e.g. "inspector".
+ * @param {Object[]} props.hostTypes
  *        Array of host type objects.
- * @param {string} hostTypes[].position
+ * @param {string} props.hostTypes[].position
  *        Position name.
- * @param {Function} hostTypes[].switchHost
+ * @param {Function} props.hostTypes[].switchHost
  *        Function to switch the host.
- * @param {string} currentHostType
+ * @param {string} props.currentHostType
  *        The current docking configuration.
- * @param {boolean} areDockOptionsEnabled
+ * @param {boolean} props.areDockOptionsEnabled
  *        They are not enabled in certain situations like when they are in the
  *        WebIDE.
- * @param {boolean} canCloseToolbox
+ * @param {boolean} props.canCloseToolbox
  *        Do we need to add UI for closing the toolbox? We don't when the
  *        toolbox is undocked, for example.
- * @param {boolean} isSplitConsoleActive
+ * @param {boolean} props.isSplitConsoleActive
  *         Is the split console currently visible?
  *        toolbox is undocked, for example.
- * @param {boolean|undefined} disableAutohide
+ * @param {boolean|undefined} props.disableAutohide
  *        Are we disabling the behavior where pop-ups are automatically
  *        closed when clicking outside them?
  *        (Only defined for the browser toolbox.)
- * @param {Function} selectTool
+ * @param {Function} props.selectTool
  *        Function to select a tool based on its id.
- * @param {Function} toggleOptions
+ * @param {Function} props.toggleOptions
  *        Function to turn the options panel on / off.
- * @param {Function} toggleSplitConsole
+ * @param {Function} props.toggleSplitConsole
  *        Function to turn the split console on / off.
- * @param {Function} toggleNoAutohide
+ * @param {Function} props.toggleNoAutohide
  *        Function to turn the disable pop-up autohide behavior on / off.
- * @param {Function} closeToolbox
+ * @param {Function} props.closeToolbox
  *        Completely close the toolbox.
- * @param {Function} focusButton
+ * @param {Function} props.focusButton
  *        Keep a record of the currently focused button.
- * @param {Object} L10N
+ * @param {Object} props.L10N
  *        Localization interface.
+ * @param {Object} props.toolbox
+ *        The devtools toolbox. Used by the MenuButton component to display
+ *        the menu popup.
+ * @param {Object} refs
+ *        The components refs object. Used to keep a reference to the MenuButton
+ *        for the meatball menu so that we can tell it to resize its contents
+ *        when they change.
  */
-function renderToolboxControls(props) {
+function renderToolboxControls(props, refs) {
   const {
     focusedButton,
+    canCloseToolbox,
     closeToolbox,
-    hostTypes,
     focusButton,
     L10N,
-    areDockOptionsEnabled,
-    canCloseToolbox,
+    toolbox,
   } = props;
 
   const meatballMenuButtonId = "toolbox-meatball-menu-button";
 
-  const meatballMenuButton = button({
-    id: meatballMenuButtonId,
-    onFocus: () => focusButton(meatballMenuButtonId),
-    className: "devtools-button",
-    title: L10N.getStr("toolbox.meatballMenu.button.tooltip"),
-    onClick: evt => {
-      showMeatballMenu(evt.target, {
-        ...props,
-        hostTypes: areDockOptionsEnabled ? hostTypes : [],
-      });
+  const meatballMenuButton = MenuButton(
+    {
+      id: meatballMenuButtonId,
+      menuId: meatballMenuButtonId + "-panel",
+      doc: toolbox.doc,
+      onFocus: () => focusButton(meatballMenuButtonId),
+      className: "devtools-button",
+      title: L10N.getStr("toolbox.meatballMenu.button.tooltip"),
+      tabIndex: focusedButton === meatballMenuButtonId ? "0" : "-1",
+      ref: "meatballMenuButton",
     },
-    tabIndex: focusedButton === meatballMenuButtonId ? "0" : "-1",
-  });
+    MeatballMenu({
+      ...props,
+      hostTypes: props.areDockOptionsEnabled ? props.hostTypes : [],
+      onResize: () => {
+        refs.meatballMenuButton.resizeContent();
+      },
+    })
+  );
 
   const closeButtonId = "toolbox-close";
 
   const closeButton = canCloseToolbox
     ? button({
       id: closeButtonId,
       onFocus: () => focusButton(closeButtonId),
       className: "devtools-button",
@@ -304,154 +317,8 @@ function renderToolboxControls(props) {
     })
     : null;
 
   return div({id: "toolbox-controls"},
     meatballMenuButton,
     closeButton
   );
 }
-
-/**
- * Display the "..." menu (affectionately known as the meatball menu).
- *
- * @param {Object} menuButton
- *        The <button> element from which the menu should pop out. The geometry
- *        of this element is used to position the menu.
- * @param {Object} props
- *        Properties as described below.
- * @param {string} props.currentToolId
- *        The id of the currently selected tool.
- * @param {Object[]} props.hostTypes
- *        Array of host type objects.
- *        This array will be empty if we shouldn't shouldn't show any dock
- *        options.
- * @param {string} props.hostTypes[].position
- *        Position name.
- * @param {Function} props.hostTypes[].switchHost
- *        Function to switch the host.
- * @param {string} props.currentHostType
- *        The current docking configuration.
- * @param {boolean} isSplitConsoleActive
- *        Is the split console currently visible?
- * @param {boolean|undefined} disableAutohide
- *        Are we disabling the behavior where pop-ups are automatically
- *        closed when clicking outside them.
- *        (Only defined for the browser toolbox.)
- * @param {Function} selectTool
- *        Function to select a tool based on its id.
- * @param {Function} toggleOptions
- *        Function to turn the options panel on / off.
- * @param {Function} toggleSplitConsole
- *        Function to turn the split console on / off.
- * @param {Function} toggleNoAutohide
- *        Function to turn the disable pop-up autohide behavior on / off.
- * @param {Object} props.L10N
- *        Localization interface.
- * @param {Object} props.toolbox
- *        The devtools toolbox. Used by the Menu component to determine which
- *        document to use.
- */
-function showMeatballMenu(
-  menuButton,
-  {
-    currentToolId,
-    hostTypes,
-    currentHostType,
-    isSplitConsoleActive,
-    disableAutohide,
-    toggleOptions,
-    toggleSplitConsole,
-    toggleNoAutohide,
-    L10N,
-    toolbox,
-  }
-) {
-  const menu = new Menu({ id: "toolbox-meatball-menu" });
-
-  // Dock options
-  for (const hostType of hostTypes) {
-    const l10nkey =
-      hostType.position === "window"
-        ? "separateWindow"
-        : hostType.position;
-    menu.append(
-      new MenuItem({
-        id: `toolbox-meatball-menu-dock-${hostType.position}`,
-        label: L10N.getStr(`toolbox.meatballMenu.dock.${l10nkey}.label`),
-        click: () => hostType.switchHost(),
-        type: "checkbox",
-        checked: hostType.position === currentHostType,
-      })
-    );
-  }
-
-  if (menu.items.length) {
-    menu.append(new MenuItem({ type: "separator" }));
-  }
-
-  // Split console
-  if (currentToolId !== "webconsole") {
-    menu.append(new MenuItem({
-      id: "toolbox-meatball-menu-splitconsole",
-      label: L10N.getStr(
-        `toolbox.meatballMenu.${
-          isSplitConsoleActive ? "hideconsole" : "splitconsole"
-        }.label`
-      ),
-      accelerator: "Esc",
-      click: toggleSplitConsole,
-    }));
-  }
-
-  // Disable pop-up autohide
-  //
-  // If |disableAutohide| is undefined, it means this feature is not available
-  // in this context.
-  if (typeof disableAutohide !== "undefined") {
-    menu.append(new MenuItem({
-      id: "toolbox-meatball-menu-noautohide",
-      label: L10N.getStr("toolbox.meatballMenu.noautohide.label"),
-      type: "checkbox",
-      checked: disableAutohide,
-      click: toggleNoAutohide,
-    }));
-  }
-
-  // Settings
-  menu.append(new MenuItem({
-    id: "toolbox-meatball-menu-settings",
-    label: L10N.getStr("toolbox.meatballMenu.settings.label"),
-    accelerator: L10N.getStr("toolbox.help.key"),
-    click: () => toggleOptions(),
-  }));
-
-  if (menu.items.length) {
-    menu.append(new MenuItem({ type: "separator" }));
-  }
-
-  // Getting started
-  menu.append(new MenuItem({
-    id: "toolbox-meatball-menu-documentation",
-    label: L10N.getStr("toolbox.meatballMenu.documentation.label"),
-    click: () => {
-      openDocLink(
-        "https://developer.mozilla.org/docs/Tools?utm_source=devtools&utm_medium=tabbar-menu");
-    },
-  }));
-
-  // Give feedback
-  menu.append(new MenuItem({
-    id: "toolbox-meatball-menu-community",
-    label: L10N.getStr("toolbox.meatballMenu.community.label"),
-    click: () => {
-      openDocLink(
-        "https://discourse.mozilla.org/c/devtools?utm_source=devtools&utm_medium=tabbar-menu");
-    },
-  }));
-
-  const rect = menuButton.getBoundingClientRect();
-  const screenX = menuButton.ownerDocument.defaultView.mozInnerScreenX;
-  const screenY = menuButton.ownerDocument.defaultView.mozInnerScreenY;
-
-  // Display the popup below the button.
-  menu.popupWithZoom(rect.left + screenX, rect.bottom + screenY, toolbox);
-}
--- a/devtools/client/framework/components/moz.build
+++ b/devtools/client/framework/components/moz.build
@@ -1,13 +1,14 @@
 # -*- 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(
+  'MeatballMenu.js',
   'ToolboxController.js',
   'ToolboxTab.js',
   'ToolboxTabs.js',
   'ToolboxToolbar.js',
 )
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -129,19 +129,21 @@ devtools.jar:
     skin/images/alerticon-warning@2x.png (themes/images/alerticon-warning@2x.png)
     skin/rules.css (themes/rules.css)
     skin/images/command-paintflashing.svg (themes/images/command-paintflashing.svg)
     skin/images/command-screenshot.svg (themes/images/command-screenshot.svg)
     skin/images/command-responsivemode.svg (themes/images/command-responsivemode.svg)
     skin/images/command-pick.svg (themes/images/command-pick.svg)
     skin/images/command-pick-accessibility.svg (themes/images/command-pick-accessibility.svg)
     skin/images/command-frames.svg (themes/images/command-frames.svg)
+    skin/images/command-console.svg (themes/images/command-console.svg)
     skin/images/command-eyedropper.svg (themes/images/command-eyedropper.svg)
     skin/images/command-rulers.svg (themes/images/command-rulers.svg)
     skin/images/command-measure.svg (themes/images/command-measure.svg)
+    skin/images/command-noautohide.svg (themes/images/command-noautohide.svg)
     skin/images/command-chevron.svg (themes/images/command-chevron.svg)
     skin/markup.css (themes/markup.css)
     skin/images/editor-error.png (themes/images/editor-error.png)
     skin/images/breakpoint.svg (themes/images/breakpoint.svg)
     skin/webconsole.css (themes/webconsole.css)
     skin/images/webconsole.svg (themes/images/webconsole.svg)
     skin/images/breadcrumbs-scrollbutton.svg (themes/images/breadcrumbs-scrollbutton.svg)
     skin/animation.css (themes/animation.css)
@@ -173,30 +175,35 @@ devtools.jar:
     skin/images/more.svg (themes/images/more.svg)
     skin/images/pause.svg (themes/images/pause.svg)
     skin/images/play.svg (themes/images/play.svg)
     skin/images/rewind.svg (themes/images/rewind.svg)
     skin/images/debugger-step-in.svg (themes/images/debugger-step-in.svg)
     skin/images/debugger-step-out.svg (themes/images/debugger-step-out.svg)
     skin/images/debugger-step-over.svg (themes/images/debugger-step-over.svg)
     skin/images/debugger-toggleBreakpoints.svg (themes/images/debugger-toggleBreakpoints.svg)
+    skin/images/dock-bottom.svg (themes/images/dock-bottom.svg)
+    skin/images/dock-side-left.svg (themes/images/dock-side-left.svg)
+    skin/images/dock-side-right.svg (themes/images/dock-side-right.svg)
+    skin/images/dock-undock.svg (themes/images/dock-undock.svg)
     skin/images/jump-definition.svg (themes/images/jump-definition.svg)
     skin/images/tracer-icon.png (themes/images/tracer-icon.png)
     skin/images/tracer-icon@2x.png (themes/images/tracer-icon@2x.png)
     skin/floating-scrollbars-dark-theme.css (themes/floating-scrollbars-dark-theme.css)
     skin/floating-scrollbars-responsive-design.css (themes/floating-scrollbars-responsive-design.css)
     skin/inspector.css (themes/inspector.css)
     skin/images/profiler-stopwatch.svg (themes/images/profiler-stopwatch.svg)
     skin/images/debugging-addons.svg (themes/images/debugging-addons.svg)
     skin/images/debugging-tabs.svg (themes/images/debugging-tabs.svg)
     skin/images/debugging-workers.svg (themes/images/debugging-workers.svg)
     skin/images/globe.svg (themes/images/globe.svg)
     skin/images/sad-face.svg (themes/images/sad-face.svg)
     skin/images/shape-swatch.svg (themes/images/shape-swatch.svg)
     skin/images/tool-options.svg (themes/images/tool-options.svg)
+    skin/images/tool-options-photon.svg (themes/images/tool-options-photon.svg)
     skin/images/tool-webconsole.svg (themes/images/tool-webconsole.svg)
     skin/images/tool-canvas.svg (themes/images/tool-canvas.svg)
     skin/images/tool-debugger.svg (themes/images/tool-debugger.svg)
     skin/images/tool-inspector.svg (themes/images/tool-inspector.svg)
     skin/images/tool-shadereditor.svg (themes/images/tool-shadereditor.svg)
     skin/images/tool-styleeditor.svg (themes/images/tool-styleeditor.svg)
     skin/images/tool-storage.svg (themes/images/tool-storage.svg)
     skin/images/tool-profiler.svg (themes/images/tool-profiler.svg)
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/command-console.svg
@@ -0,0 +1,11 @@
+<!-- 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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="context-fill #0b0b0b">
+  <path d="
+    M13 1a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-10a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h10z
+    M13 3h-10a1 1 0 0 0-1 1v1h12v-1a1 1 0 0 0-1-1z
+    M14 6h-12v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1z"/>
+  <path d="M4.5 7.5l2 2l-2 2" stroke="context-fill" stroke-width="1"
+    stroke-linecap="round" stroke-linejoin="round" fill="none"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/command-noautohide.svg
@@ -0,0 +1,9 @@
+<!-- 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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="context-fill #0b0b0b">
+  <path d="M1 5a2 2 0 0 1 2-2h1l2-2 2 2h5a2 2 0 0 1 2 2v8
+    a2 2 0 0 1-2 2h-10a2 2 0 0 1-2-2z"
+    stroke-width="2" stroke="context-fill"
+    stroke-linejoin="round" stroke-linecap="round" fill="none"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/dock-bottom.svg
@@ -0,0 +1,10 @@
+<!-- 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 viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
+  fill="context-fill #0b0b0b">
+  <path d="
+    M12 0a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3h-8a3 3 0 0 1-3-3v-10a3 3 0 0 1 3-3h8z
+    M12 2h-8a1 1 0 0 0-1 1v7h10v-7a1 1 0 0 0-1-1z
+    M13 11h-10v2a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/dock-side-left.svg
@@ -0,0 +1,10 @@
+<!-- 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 viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
+  fill="context-fill #0b0b0b">
+  <path d="
+    M0 4a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-10a3 3 0 0 1-3-3z
+    M2 4v8a1 1 0 0 0 1 1h2v-10h-2a1 1 0 0 0-1 1z
+    M6 3v10h7a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/dock-side-right.svg
@@ -0,0 +1,10 @@
+<!-- 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 viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
+  fill="context-fill #0b0b0b">
+  <path d="
+    M0 4a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-10a3 3 0 0 1-3-3z
+    M2 4v8a1 1 0 0 0 1 1h7v-10h-7a1 1 0 0 0-1 1z
+    M11 3v10h2a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/dock-undock.svg
@@ -0,0 +1,12 @@
+<!-- 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 viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
+  fill="context-fill #0b0b0b">
+  <path d="
+    M13 0a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3h-5a3 3 0 0 1-3-3v-5a3 3 0 0 1 3-3z
+    M13 2h-5a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1z"/>
+  <path d="M9 13v-1h2v1a3 3 0 0 1-3 3
+    h-5a3 3 0 0 1-3-3v-5a3 3 0 0 1 3-3h1v2h-1a1 1 0 0 0-1 1v5
+    a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/tool-options-photon.svg
@@ -0,0 +1,9 @@
+<!-- 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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"
+  fill="context-fill #0b0b0b">
+  <path d="M7.45 15.95a1.86 1.86 0 0 1-1.86-1.86v-.89c-.3-.14-.6-.3-.89-.5l-.77.43a1.87 1.87 0 0 1-2.54-.68l-.55-.96c-.51-.88-.21-2.02.68-2.54l.77-.44a5.8 5.8 0 0 1 0-1.02l-.77-.45A1.87 1.87 0 0 1 .84 4.5l.55-.95a1.87 1.87 0 0 1 2.54-.69l.77.45c.28-.2.58-.37.89-.52v-.88C5.58.88 6.42.05 7.45.05h1.1c1.03 0 1.86.83 1.86 1.86v.89c.3.14.6.3.89.5l.77-.44a1.86 1.86 0 0 1 2.54.69l.55.95c.52.9.21 2.03-.68 2.55l-.77.44c.03.34.03.68 0 1.02l.77.44c.89.52 1.2 1.66.68 2.55l-.55.95a1.87 1.87 0 0 1-2.54.69l-.77-.45c-.29.2-.58.38-.88.52v.88c0 1.03-.84 1.86-1.87 1.86h-1.1zM5.49 11.4c.26.2.53.35.82.48l.78.34v1.87c0 .2.17.35.36.35h1.1c.2 0 .36-.16.36-.35v-1.87l.78-.34c.3-.13.57-.29.82-.48l.69-.5 1.62.93a.36.36 0 0 0 .48-.13l.56-.96a.36.36 0 0 0-.13-.48l-1.62-.94.1-.85c.03-.31.03-.63 0-.95l-.1-.85 1.62-.93c.17-.1.22-.32.13-.49l-.56-.95a.36.36 0 0 0-.48-.13l-1.62.94-.69-.51a4.27 4.27 0 0 0-.82-.48l-.78-.34V1.91c0-.2-.16-.35-.36-.35h-1.1c-.2 0-.36.16-.36.35v1.87l-.78.34c-.3.13-.57.29-.82.48l-.69.5-1.62-.93a.36.36 0 0 0-.48.13l-.56.96c-.1.16-.03.38.13.48l1.62.94-.1.85c-.03.31-.03.63 0 .95l.1.84-1.62.94a.36.36 0 0 0-.13.48l.56.96c.06.1.18.18.3.18.07 0 .13-.02.18-.05l1.62-.94.69.51z"/>
+  <circle cx="8" cy="8" r="1.5" stroke-width="1"
+    stroke="context-fill" fill="none"/>
+</svg>
--- a/devtools/client/themes/toolbox.css
+++ b/devtools/client/themes/toolbox.css
@@ -1,17 +1,24 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
 :root {
   --close-button-image: url(chrome://devtools/skin/images/close.svg);
+  --dock-bottom-image: url(chrome://devtools/skin/images/dock-bottom.svg);
+  --dock-side-right-image: url(chrome://devtools/skin/images/dock-side-right.svg);
+  --dock-side-left-image: url(chrome://devtools/skin/images/dock-side-left.svg);
+  --dock-undock-image: url(chrome://devtools/skin/images/dock-undock.svg);
   --more-button-image: url(chrome://devtools/skin/images/more.svg);
+  --settings-image: url(chrome://devtools/skin/images/tool-options-photon.svg);
 
+  --command-noautohide-image: url(images/command-noautohide.svg);
+  --command-console-image: url(images/command-console.svg);
   --command-paintflashing-image: url(images/command-paintflashing.svg);
   --command-screenshot-image: url(images/command-screenshot.svg);
   --command-responsive-image: url(images/command-responsivemode.svg);
   --command-scratchpad-image: url(images/tool-scratchpad.svg);
   --command-pick-image: url(images/command-pick.svg);
   --command-pick-accessibility-image: url(images/command-pick-accessibility.svg);
   --command-frames-image: url(images/command-frames.svg);
   --command-rulers-image: url(images/command-rulers.svg);
@@ -214,16 +221,38 @@
   min-width: 24px;
 }
 
 #toolbox-meatball-menu-button::before {
   fill: var(--theme-toolbar-photon-icon-color);
   background-image: var(--more-button-image);
 }
 
+#toolbox-meatball-menu-dock-bottom > .label::before {
+  background-image: var(--dock-bottom-image);
+}
+#toolbox-meatball-menu-dock-left > .label::before {
+  background-image: var(--dock-side-left-image);
+}
+#toolbox-meatball-menu-dock-right > .label::before {
+  background-image: var(--dock-side-right-image);
+}
+#toolbox-meatball-menu-dock-window > .label::before {
+  background-image: var(--dock-undock-image);
+}
+#toolbox-meatball-menu-splitconsole > .label::before {
+  background-image: var(--command-console-image);
+}
+#toolbox-meatball-menu-noautohide > .label::before {
+  background-image: var(--command-noautohide-image);
+}
+#toolbox-meatball-menu-settings > .label::before {
+  background-image: var(--settings-image);
+}
+
 /* Command buttons */
 
 .command-button,
 #toolbox-controls > button {
   /* !important is needed to override .devtools-button rules in common.css */
   padding: 0 !important;
   margin: 0 !important;
   border: none !important;
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_split.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_split.js
@@ -99,19 +99,19 @@ add_task(async function() {
       EventUtils.sendMouseEvent({ type: "click" }, button);
 
       toolbox.doc.addEventListener("popupshown", () => {
         const menuItem =
           toolbox.doc.getElementById("toolbox-meatball-menu-splitconsole");
 
         // Return undefined if the menu item is not available
         let label;
-        if (menuItem) {
+        if (menuItem && menuItem.querySelector(".label")) {
           label =
-            menuItem.label ===
+            menuItem.querySelector(".label").textContent ===
             L10N.getStr("toolbox.meatballMenu.hideconsole.label")
               ? "hide"
               : "split";
         }
 
         // Wait for menu to close
         toolbox.doc.addEventListener("popuphidden", () => {
           resolve(label);
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_split_persist.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_split_persist.js
@@ -92,17 +92,18 @@ function doesMenuSayHide(toolbox) {
     EventUtils.sendMouseEvent({ type: "click" }, button);
 
     toolbox.doc.addEventListener("popupshown", () => {
       const menuItem =
         toolbox.doc.getElementById("toolbox-meatball-menu-splitconsole");
 
       const result =
         menuItem &&
-        menuItem.label ===
+        menuItem.querySelector(".label") &&
+        menuItem.querySelector(".label").textContent ===
           L10N.getStr("toolbox.meatballMenu.hideconsole.label");
 
       toolbox.doc.addEventListener("popuphidden", () => {
         resolve(result);
       },
       { once: true });
       EventUtils.synthesizeKey("KEY_Escape");
     },