Bug 1442531 - Part 1. Make chevron button of devtool to be exclusive and apply the photon design. r?jdescottes draft
authorMantaroh Yoshinaga <mantaroh@gmail.com>
Mon, 16 Apr 2018 16:48:11 +0900
changeset 782471 5542f08106c5f307243b6d20d382a9d939dd271b
parent 782432 7ff499dfcd51cf4a95ebf0db506b415bf7bb27c3
child 782472 e02b7c1bcf38a2e6e9708745907d6ec10884f2bc
push id106548
push userbmo:mantaroh@gmail.com
push dateMon, 16 Apr 2018 07:48:54 +0000
reviewersjdescottes
bugs1442531
milestone61.0a1
Bug 1442531 - Part 1. Make chevron button of devtool to be exclusive and apply the photon design. r?jdescottes This patch will: * change detecting overflow to using resize event. * make chevron menu and tab menu to be exclusive. * use photon design chevron menu. * modify the related tests. In this patch, the toolbox will create the cache of the displayed tool tab width after rendering an element since each width of toolbox tab is not fixed size. (i.e. each toolbox tab size is different from another size) MozReview-Commit-ID: EQ0nU6WzCg1
devtools/client/framework/components/toolbox-tabs.js
devtools/client/framework/test/browser.ini
devtools/client/framework/test/browser_toolbox_options.js
devtools/client/framework/test/browser_toolbox_toolbar_overflow.js
devtools/client/framework/test/browser_toolbox_toolbar_reorder_by_width.js
devtools/client/framework/test/head.js
devtools/client/jar.mn
devtools/client/themes/common.css
devtools/client/themes/images/command-chevron.svg
devtools/client/themes/toolbox.css
--- a/devtools/client/framework/components/toolbox-tabs.js
+++ b/devtools/client/framework/components/toolbox-tabs.js
@@ -8,16 +8,19 @@ const dom = require("devtools/client/sha
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const {findDOMNode} = require("devtools/client/shared/vendor/react-dom");
 const {button, div} = dom;
 
 const Menu = require("devtools/client/framework/menu");
 const MenuItem = require("devtools/client/framework/menu-item");
 const ToolboxTab = createFactory(require("devtools/client/framework/components/toolbox-tab"));
 
+// 26px is chevron devtools button width.(i.e. tools-chevronmenu)
+const CHEVRON_BUTTON_WIDTH = 26;
+
 class ToolboxTabs extends Component {
   // See toolbox-toolbar propTypes for details on the props used here.
   static get propTypes() {
     return {
       currentToolId: PropTypes.string,
       focusButton: PropTypes.func,
       focusedButton: PropTypes.string,
       highlightedTools: PropTypes.object,
@@ -27,140 +30,237 @@ class ToolboxTabs extends Component {
       L10N: PropTypes.object,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
-      overflow: false,
+      // Array of overflowed tool id.
+      overflowedTabIds: [],
     };
 
-    this.addFlowEvents = this.addFlowEvents.bind(this);
-    this.removeFlowEvents = this.removeFlowEvents.bind(this);
-    this.onOverflow = this.onOverflow.bind(this);
-    this.onUnderflow = this.onUnderflow.bind(this);
+    // Map with tool Id and its width size. This lifecycle is out of React's
+    // lifecycle. If a tool is registered, ToolboxTabs will add target tool id
+    // to this map. ToolboxTabs will never remove tool id from this cache.
+    this._cachedToolTabsWidthMap = new Map();
+
+    this._resizeTimerId = null;
+    this.resizeHandler = this.resizeHandler.bind(this);
+  }
+
+  componentDidMount() {
+    window.addEventListener("resize", this.resizeHandler);
+    this.updateCachedToolTabsWidthMap();
+    this.updateOverflowedTabs();
+  }
+
+  componentWillUpdate(nextProps, nextState) {
+    if (this.shouldUpdateToolboxTabs(this.props, nextProps)) {
+      // Force recalculate and render in this cycle if panel definition has
+      // changed or selected tool has changed.
+      nextState.overflowedTabIds = [];
+    }
+  }
+
+  componentDidUpdate(prevProps, prevState) {
+    if (this.shouldUpdateToolboxTabs(prevProps, this.props)) {
+      this.updateCachedToolTabsWidthMap();
+      this.updateOverflowedTabs();
+    }
   }
 
-  componentDidUpdate() {
-    this.addFlowEvents();
+  /**
+   * Check if two array of ids are the same or not.
+   */
+  equalToolIdArray(prevPanels, nextPanels) {
+    if (prevPanels.length !== nextPanels.length) {
+      return false;
+    }
+
+    // Check panel definitions even if both of array size is same.
+    // For example, the case of changing the tab's order.
+    return prevPanels.join("-") === nextPanels.join("-");
   }
 
-  componentWillUnmount() {
-    this.removeFlowEvents();
+  /**
+   * Return true if we should update the overflowed tabs.
+   */
+  shouldUpdateToolboxTabs(prevProps, nextProps) {
+    if (prevProps.currentToolId !== nextProps.currentToolId) {
+      return true;
+    }
+
+    let prevPanels = prevProps.panelDefinitions.map(def => def.id);
+    let nextPanels = nextProps.panelDefinitions.map(def => def.id);
+    return !this.equalToolIdArray(prevPanels, nextPanels);
   }
 
-  addFlowEvents() {
-    this.removeFlowEvents();
-    let node = findDOMNode(this);
-    if (node) {
-      node.addEventListener("overflow", this.onOverflow);
-      node.addEventListener("underflow", this.onUnderflow);
+  /**
+   * Update the Map of tool id and tool tab width.
+   */
+  updateCachedToolTabsWidthMap() {
+    let thisNode = findDOMNode(this);
+    for (let tab of thisNode.querySelectorAll(".devtools-tab")) {
+      let tabId = tab.id.replace("toolbox-tab-", "");
+      if (!this._cachedToolTabsWidthMap.has(tabId)) {
+        let cs = getComputedStyle(tab);
+        this._cachedToolTabsWidthMap.set(tabId, parseInt(cs.width, 10));
+      }
     }
   }
 
-  removeFlowEvents() {
+  /**
+   * Update the overflowed tab array from currently displayed tool tab.
+   * If calculated result is the same as the current overflowed tab array, this
+   * function will not update state.
+   */
+  updateOverflowedTabs() {
     let node = findDOMNode(this);
-    if (node) {
-      node.removeEventListener("overflow", this.onOverflow);
-      node.removeEventListener("underflow", this.onUnderflow);
+    const toolboxWidth = parseInt(getComputedStyle(node).width, 10);
+    let { currentToolId } = this.props;
+    let enabledTabs = this.props.panelDefinitions.map(def => def.id);
+    let sumWidth = 0;
+    let visibleTabs = [];
+
+    for (const id of enabledTabs) {
+      let width = this._cachedToolTabsWidthMap.get(id);
+      sumWidth += width;
+      if (sumWidth <= toolboxWidth) {
+        visibleTabs.push(id);
+      } else {
+        sumWidth = sumWidth - width + CHEVRON_BUTTON_WIDTH;
+
+        // If toolbox can't display the Chevron, remove the last tool tab.
+        if (sumWidth > toolboxWidth) {
+          let removeTabId = visibleTabs.pop();
+          sumWidth -= this._cachedToolTabsWidthMap.get(removeTabId);
+        }
+        break;
+      }
+    }
+
+    // If the selected tab is in overflowed tabs, insert it into a visible
+    // toolbox.
+    if (!visibleTabs.includes(currentToolId) &&
+        enabledTabs.includes(currentToolId)) {
+      let selectedToolWidth = this._cachedToolTabsWidthMap.get(currentToolId);
+      while ((sumWidth + selectedToolWidth) > toolboxWidth &&
+             visibleTabs.length > 0) {
+        let removingToolId  = visibleTabs.pop();
+        let removingToolWidth = this._cachedToolTabsWidthMap.get(removingToolId);
+        sumWidth -= removingToolWidth;
+      }
+      visibleTabs.push(currentToolId);
+    }
+
+    if (visibleTabs.length === 0) {
+      visibleTabs = [enabledTabs[0]];
+    }
+
+    let willOverflowTabs = enabledTabs.filter(id => !visibleTabs.includes(id));
+    if (!this.equalToolIdArray(this.state.overflowedTabIds, willOverflowTabs)) {
+      this.setState({ overflowedTabIds: willOverflowTabs });
     }
   }
 
-  onOverflow() {
-    this.setState({
-      overflow: true
-    });
+  resizeHandler(evt) {
+    window.cancelIdleCallback(this._resizeTimerId);
+    this._resizeTimerId = window.requestIdleCallback(() => {
+      this.updateOverflowedTabs();
+    }, { timeout: 300 });
   }
 
-  onUnderflow() {
-    this.setState({
-      overflow: false
+  /**
+   * Render a button to access overflowed tools, displayed only when the toolbar
+   * presents an overflow.
+   */
+  renderToolsChevronButton() {
+    let {
+      panelDefinitions,
+      selectTool,
+      toolbox,
+      L10N,
+    } = this.props;
+
+    return button({
+      className: "devtools-button tools-chevron-menu",
+      tabIndex: -1,
+      title: L10N.getStr("toolbox.allToolsButton.tooltip"),
+      id: "tools-chevron-menu-button",
+      onClick: ({ target }) => {
+        let menu = new Menu({
+          id: "tools-chevron-menupopup"
+        });
+
+        panelDefinitions.forEach(({id, label}) => {
+          if (this.state.overflowedTabIds.includes(id)) {
+            menu.append(new MenuItem({
+              click: () => {
+                selectTool(id);
+              },
+              id: "tools-chevron-menupopup-" + id,
+              label,
+              type: "checkbox",
+            }));
+          }
+        });
+
+        let rect = target.getBoundingClientRect();
+        let screenX = target.ownerDocument.defaultView.mozInnerScreenX;
+        let screenY = target.ownerDocument.defaultView.mozInnerScreenY;
+
+        // Display the popup below the button.
+        menu.popup(rect.left + screenX, rect.bottom + screenY, toolbox);
+        return menu;
+      },
     });
   }
 
   /**
    * Render all of the tabs, based on the panel definitions and builds out
-   * a toolbox tab for each of them. Will render an all-tabs button if the
+   * a toolbox tab for each of them. Will render the chevron button if the
    * container has an overflow.
    */
   render() {
     let {
       currentToolId,
       focusButton,
       focusedButton,
       highlightedTools,
       panelDefinitions,
       selectTool,
     } = this.props;
 
-    let tabs = panelDefinitions.map(panelDefinition => ToolboxTab({
-      key: panelDefinition.id,
-      currentToolId,
-      focusButton,
-      focusedButton,
-      highlightedTools,
-      panelDefinition,
-      selectTool,
-    }));
+    let tabs = panelDefinitions.map(panelDefinition => {
+      // Don't display overflowed tab.
+      if (!this.state.overflowedTabIds.includes(panelDefinition.id)) {
+        return ToolboxTab({
+          key: panelDefinition.id,
+          currentToolId,
+          focusButton,
+          focusedButton,
+          highlightedTools,
+          panelDefinition,
+          selectTool,
+        });
+      }
+      return null;
+    });
 
-    // A wrapper is needed to get flex sizing correct in XUL.
     return div(
       {
         className: "toolbox-tabs-wrapper"
       },
       div(
         {
           className: "toolbox-tabs"
         },
-        tabs
-      ),
-      this.state.overflow ? renderAllToolsButton(this.props) : null
+        tabs,
+        (this.state.overflowedTabIds.length > 0)
+          ? this.renderToolsChevronButton() : null
+      )
     );
   }
 }
 
 module.exports = ToolboxTabs;
-
-/**
- * Render a button to access all tools, displayed only when the toolbar presents an
- * overflow.
- */
-function renderAllToolsButton(props) {
-  let {
-    currentToolId,
-    panelDefinitions,
-    selectTool,
-    toolbox,
-    L10N,
-  } = props;
-
-  return button({
-    className: "all-tools-menu all-tabs-menu",
-    tabIndex: -1,
-    title: L10N.getStr("toolbox.allToolsButton.tooltip"),
-    onClick: ({ target }) => {
-      let menu = new Menu({
-        id: "all-tools-menupopup"
-      });
-      panelDefinitions.forEach(({id, label}) => {
-        menu.append(new MenuItem({
-          checked: currentToolId === id,
-          click: () => {
-            selectTool(id);
-          },
-          id: "all-tools-menupopup-" + id,
-          label,
-          type: "checkbox",
-        }));
-      });
-
-      let rect = target.getBoundingClientRect();
-      let screenX = target.ownerDocument.defaultView.mozInnerScreenX;
-      let screenY = target.ownerDocument.defaultView.mozInnerScreenY;
-
-      // Display the popup below the button.
-      menu.popup(rect.left + screenX, rect.bottom + screenY, toolbox);
-      return menu;
-    },
-  });
-}
--- a/devtools/client/framework/test/browser.ini
+++ b/devtools/client/framework/test/browser.ini
@@ -108,16 +108,17 @@ skip-if = e10s # Bug 1069044 - destroyIn
 [browser_toolbox_telemetry_close.js]
 [browser_toolbox_textbox_context_menu.js]
 [browser_toolbox_theme.js]
 [browser_toolbox_theme_registration.js]
 [browser_toolbox_toggle.js]
 [browser_toolbox_tool_ready.js]
 [browser_toolbox_tool_remote_reopen.js]
 [browser_toolbox_toolbar_overflow.js]
+[browser_toolbox_toolbar_reorder_by_width.js]
 [browser_toolbox_tools_per_toolbox_registration.js]
 [browser_toolbox_transport_events.js]
 [browser_toolbox_view_source_01.js]
 [browser_toolbox_view_source_02.js]
 [browser_toolbox_view_source_03.js]
 [browser_toolbox_view_source_04.js]
 [browser_toolbox_window_reload_target.js]
 [browser_toolbox_window_shortcuts.js]
--- a/devtools/client/framework/test/browser_toolbox_options.js
+++ b/devtools/client/framework/test/browser_toolbox_options.js
@@ -15,30 +15,39 @@ const {PrefObserver} = require("devtools
 
 add_task(async function() {
   const URL = "data:text/html;charset=utf8,test for dynamically registering " +
               "and unregistering tools";
   registerNewTool();
   let tab = await addTab(URL);
   let target = TargetFactory.forTab(tab);
   toolbox = await gDevTools.showToolbox(target);
+
+  info("In order to ensure display the chevron menu, increase the width of " +
+       "the toolbox");
+  let hostWindow = toolbox.win.parent;
+  let originalWidth = hostWindow.outerWidth;
+  let onResize = once(hostWindow, "resize");
+  hostWindow.resizeTo(1350, hostWindow.outerHeight);
+  await onResize;
+
   doc = toolbox.doc;
   await registerNewPerToolboxTool();
   await testSelectTool();
   await testOptionsShortcut();
   await testOptions();
   await testToggleTools();
 
   // Test that registered WebExtensions becomes entries in the
   // options panel and toggling their checkbox toggle the related
   // preference.
   await registerNewWebExtensions();
   await testToggleWebExtensions();
 
-  await cleanup();
+  await cleanup(hostWindow, originalWidth);
 });
 
 function registerNewTool() {
   let toolDefinition = {
     id: "test-tool",
     isTargetSupported: () => true,
     visibilityswitch: "devtools.test-tool.enabled",
     url: "about:blank",
@@ -450,17 +459,18 @@ function GetPref(name) {
       return Services.prefs.getIntPref(name);
     case Services.prefs.PREF_BOOL:
       return Services.prefs.getBoolPref(name);
     default:
       throw new Error("Unknown type");
   }
 }
 
-async function cleanup() {
+async function cleanup(win, winWidth) {
   gDevTools.unregisterTool("test-tool");
   await toolbox.destroy();
   gBrowser.removeCurrentTab();
   for (let pref of modifiedPrefs) {
     Services.prefs.clearUserPref(pref);
   }
   toolbox = doc = panelWin = modifiedPrefs = null;
+  win.resizeTo(winWidth, win.outerHeight);
 }
--- a/devtools/client/framework/test/browser_toolbox_toolbar_overflow.js
+++ b/devtools/client/framework/test/browser_toolbox_toolbar_overflow.js
@@ -7,80 +7,67 @@
 
 // Test that a button to access tools hidden by toolbar overflow is displayed when the
 // toolbar starts to present an overflow.
 let { Toolbox } = require("devtools/client/framework/toolbox");
 
 add_task(async function() {
   let tab = await addTab("about:blank");
 
-  info("Open devtools on the Inspector in a separate window");
-  let toolbox = await openToolboxForTab(tab, "inspector", Toolbox.HostType.WINDOW);
+  info("Open devtools on the Inspector in a bottom dock");
+  let toolbox = await openToolboxForTab(tab, "inspector", Toolbox.HostType.BOTTOM);
 
   let hostWindow = toolbox.win.parent;
   let originalWidth = hostWindow.outerWidth;
   let originalHeight = hostWindow.outerHeight;
 
   info("Resize devtools window to a width that should not trigger any overflow");
   let onResize = once(hostWindow, "resize");
-  hostWindow.resizeTo(640, 300);
+  hostWindow.resizeTo(1350, 300);
   await onResize;
+  waitUntil(() => {
+    // Wait for all buttons are displayed.
+    return toolbox.panelDefinitions.length !==
+      toolbox.doc.querySelectorAll(".devtools-tab").length;
+  });
 
-  let allToolsButton = toolbox.doc.querySelector(".all-tools-menu");
-  ok(!allToolsButton, "The all tools button is not displayed");
+  let chevronMenuButton = toolbox.doc.querySelector(".tools-chevron-menu");
+  ok(!chevronMenuButton, "The chevron menu button is not displayed");
 
   info("Resize devtools window to a width that should trigger an overflow");
   onResize = once(hostWindow, "resize");
-  hostWindow.resizeTo(300, 300);
+  hostWindow.resizeTo(800, 300);
   await onResize;
-
-  info("Wait until the all tools button is available");
-  await waitUntil(() => toolbox.doc.querySelector(".all-tools-menu"));
+  waitUntil(() => !toolbox.doc.querySelector(".tools-chevron-menu"));
 
-  allToolsButton = toolbox.doc.querySelector(".all-tools-menu");
-  ok(allToolsButton, "The all tools button is displayed");
+  info("Wait until the chevron menu button is available");
+  await waitUntil(() => toolbox.doc.querySelector(".tools-chevron-menu"));
 
-  info("Open the all-tools-menupopup and verify that the inspector button is checked");
-  let menuPopup = await openAllToolsMenu(toolbox);
+  chevronMenuButton = toolbox.doc.querySelector(".tools-chevron-menu");
+  ok(chevronMenuButton, "The chevron menu button is displayed");
+
+  info("Open the tools-chevron-menupopup and verify that the inspector button is checked");
+  let menuPopup = await openChevronMenu(toolbox);
 
-  let inspectorButton = toolbox.doc.querySelector("#all-tools-menupopup-inspector");
-  ok(inspectorButton, "The inspector button is available");
-  ok(inspectorButton.getAttribute("checked"), "The inspector button is checked");
+  let inspectorButton = toolbox.doc.querySelector("#tools-chevron-menupopup-inspector");
+  ok(!inspectorButton, "The chevron menu doesn't have the inspector button.");
+
+  let consoleButton = toolbox.doc.querySelector("#tools-chevron-menupopup-webconsole");
+  ok(!consoleButton, "The chevron menu doesn't have the console button.");
 
-  let consoleButton = toolbox.doc.querySelector("#all-tools-menupopup-webconsole");
-  ok(consoleButton, "The console button is available");
-  ok(!consoleButton.getAttribute("checked"), "The console button is not checked");
+  let storageButton = toolbox.doc.querySelector("#tools-chevron-menupopup-storage");
+  ok(storageButton, "The chevron menu has the storage button.");
 
-  info("Switch to the webconsole using the all-tools-menupopup popup");
-  let onSelected = toolbox.once("webconsole-selected");
-  consoleButton.click();
+  info("Switch to the performance using the tools-chevron-menupopup popup");
+  let onSelected = toolbox.once("storage-selected");
+  storageButton.click();
   await onSelected;
 
-  info("Closing the all-tools-menupopup popup");
+  info("Closing the tools-chevron-menupopup popup");
   let onPopupHidden = once(menuPopup, "popuphidden");
   menuPopup.hidePopup();
   await onPopupHidden;
 
-  info("Re-open the all-tools-menupopup and verify that the console button is checked");
-  menuPopup = await openAllToolsMenu(toolbox);
-
-  inspectorButton = toolbox.doc.querySelector("#all-tools-menupopup-inspector");
-  ok(!inspectorButton.getAttribute("checked"), "The inspector button is not checked");
-
-  consoleButton = toolbox.doc.querySelector("#all-tools-menupopup-webconsole");
-  ok(consoleButton.getAttribute("checked"), "The console button is checked");
-
   info("Restore the original window size");
+  onResize = once(hostWindow, "resize");
   hostWindow.resizeTo(originalWidth, originalHeight);
+  await onResize;
 });
-
-async function openAllToolsMenu(toolbox) {
-  let allToolsButton = toolbox.doc.querySelector(".all-tools-menu");
-  EventUtils.synthesizeMouseAtCenter(allToolsButton, {}, toolbox.win);
-
-  let menuPopup = toolbox.doc.querySelector("#all-tools-menupopup");
-  ok(menuPopup, "all-tools-menupopup is available");
-
-  info("Waiting for the menu popup to be displayed");
-  await waitUntil(() => menuPopup && menuPopup.state === "open");
-
-  return menuPopup;
-}
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_toolbar_reorder_by_width.js
@@ -0,0 +1,106 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// This test will:
+//
+// * Confirm that currently selected button to access tools will not hide due to overflow.
+//   In this case, a button which is located on the left of a currently selected will hide.
+// * Confirm that a button to access tool will hide when registering a new panel.
+//
+// Note that this test is based on the tab ordinal is fixed.
+// i.e. After changed by Bug 1226272, this test might fail.
+
+let { Toolbox } = require("devtools/client/framework/toolbox");
+
+add_task(async function() {
+  let tab = await addTab("about:blank");
+
+  info("Open devtools on the Storage in a sidebar.");
+  let toolbox = await openToolboxForTab(tab, "storage", Toolbox.HostType.BOTTOM);
+
+  info("Waiting for the window to be resized");
+  let {hostWin, originalWidth, originalHeight} = await resizeWindow(toolbox, 800);
+
+  info("Wait until the tools menu button is available");
+  await waitUntil(() => toolbox.doc.querySelector(".tools-chevron-menu"));
+
+  let toolsMenuButton = toolbox.doc.querySelector(".tools-chevron-menu");
+  ok(toolsMenuButton, "The tools menu button is displayed");
+
+  info("Confirm that selected tab is not hidden.");
+  let storageButton = toolbox.doc.querySelector("#toolbox-tab-storage");
+  ok(storageButton, "The storage tab is on toolbox.");
+
+  await resizeWindow(toolbox, originalWidth, originalHeight);
+});
+
+add_task(async function() {
+  let tab = await addTab("about:blank");
+
+  info("Open devtools on the Storage in a sidebar.");
+  let toolbox = await openToolboxForTab(tab, "storage", Toolbox.HostType.BOTTOM);
+
+  info("Resize devtools window to a width that should trigger an overflow");
+  let {hostWin, originalWidth, originalHeight} = await resizeWindow(toolbox, 800);
+
+  info("Regist a new tab");
+  let onRegistered = toolbox.once("tool-registered");
+  gDevTools.registerTool({
+    id: "test-tools",
+    label: "Test Tools",
+    isMenu: true,
+    isTargetSupported: () => true,
+    build: function() {},
+  });
+  await onRegistered;
+
+  info("Open the tools menu button.");
+  let popup = await openChevronMenu(toolbox);
+
+  info("The registered new tool tab should be in the tools menu.");
+  let testToolsButton = toolbox.doc.querySelector("#tools-chevron-menupopup-test-tools");
+  ok(testToolsButton, "The tools menu has a registered new tool button.");
+
+  info("Closing the tools-chevron-menupopup popup");
+  let onPopupHidden = once(popup, "popuphidden");
+  popup.hidePopup();
+  await onPopupHidden;
+
+  info("Unregistering test-tools");
+  let onUnregistered = toolbox.once("tool-unregistered");
+  gDevTools.unregisterTool("test-tools");
+  await onUnregistered;
+
+  info("Open the tools menu button.");
+  popup = await openChevronMenu(toolbox);
+
+  info("An unregistered new tool tab should not be in the tools menu.");
+  testToolsButton = toolbox.doc.querySelector("#tools-chevron-menupopup-test-tools");
+  ok(!testToolsButton, "The tools menu doesn't have a unregistered new tool button.");
+
+  info("Closing the tools-chevron-menupopup popup");
+  onPopupHidden = once(popup, "popuphidden");
+  popup.hidePopup();
+  await onPopupHidden;
+
+  await resizeWindow(toolbox, originalWidth, originalHeight);
+});
+
+async function resizeWindow(toolbox, width, height) {
+  let hostWindow = toolbox.win.parent;
+  let originalWidth = hostWindow.outerWidth;
+  let originalHeight = hostWindow.outerHeight;
+  let toWidth = width || originalWidth;
+  let toHeight = height || originalHeight;
+
+  info("Resize devtools window to a width that should trigger an overflow");
+  let onResize = once(hostWindow, "resize");
+  hostWindow.resizeTo(toWidth, toHeight);
+  await onResize;
+
+  return {hostWindow, originalWidth, originalHeight};
+}
--- a/devtools/client/framework/test/head.js
+++ b/devtools/client/framework/test/head.js
@@ -242,8 +242,21 @@ DevToolPanel.prototype = {
 
 /**
  * Create a simple devtools test panel that implements the minimum API needed to be
  * registered and opened in the toolbox.
  */
 function createTestPanel(iframeWindow, toolbox) {
   return new DevToolPanel(iframeWindow, toolbox);
 }
+
+async function openChevronMenu(toolbox) {
+  let chevronMenuButton = toolbox.doc.querySelector(".tools-chevron-menu");
+  EventUtils.synthesizeMouseAtCenter(chevronMenuButton, {}, toolbox.win);
+
+  let menuPopup = toolbox.doc.querySelector("#tools-chevron-menupopup");
+  ok(menuPopup, "tools-chevron-menupopup is available");
+
+  info("Waiting for the menu popup to be displayed");
+  await waitUntil(() => menuPopup && menuPopup.state === "open");
+
+  return menuPopup;
+}
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -145,16 +145,17 @@ devtools.jar:
     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-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-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)
     skin/animationinspector.css (themes/animationinspector.css)
--- a/devtools/client/themes/common.css
+++ b/devtools/client/themes/common.css
@@ -258,17 +258,16 @@ checkbox:-moz-focusring {
   color: var(--theme-toolbar-color);
   direction: ltr;
   font-size: 11px;
 }
 
 .devtools-button:empty::before {
   content: "";
   display: inline-block;
-  background-size: cover;
   background-repeat: no-repeat;
   vertical-align: middle;
 }
 
 .devtools-button.checked:empty::before {
   color: var(--theme-toolbar-checked-color);
 }
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/command-chevron.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 xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16">
+  <path fill="context-fill" d="M8.707 7.293l-5-5a1 1 0 0 0-1.414 1.414L6.586 8l-4.293 4.293a1 1 0 1 0 1.414 1.414l5-5a1 1 0 0 0 0-1.414zm6 0l-5-5a1 1 0 0 0-1.414 1.414L12.586 8l-4.293 4.293a1 1 0 1 0 1.414 1.414l5-5a1 1 0 0 0 0-1.414z"></path>
+</svg>
--- a/devtools/client/themes/toolbox.css
+++ b/devtools/client/themes/toolbox.css
@@ -11,16 +11,17 @@
   --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);
   --command-measure-image: url(images/command-measure.svg);
+  --command-chevron-image: url(images/command-chevron.svg);
 }
 
 /* Toolbox tabbar */
 
 .devtools-tabbar {
   -moz-appearance: none;
   display: flex;
   background: var(--theme-tab-toolbar-background);
@@ -30,18 +31,17 @@
 }
 
 .toolbox-tabs-wrapper {
   position: relative;
   display: flex;
   flex: 1;
 }
 
-.toolbox-tabs-wrapper .all-tools-menu {
-  border-inline-end: 1px solid var(--theme-splitter-color);
+.toolbox-tabs-wrapper .tools-chevron-menu {
   border-top-width: 0;
   border-bottom-width: 0;
 }
 
 .toolbox-tabs {
   position: absolute;
   top: 0;
   left: 0;
@@ -63,56 +63,39 @@
 }
 
 /* Toolbox tabs */
 
 .devtools-tab {
   position: relative;
   display: flex;
   align-items: center;
-  min-width: 32px;
   min-height: 24px;
   margin: 0;
   padding: 0;
   border: none;
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
   background-color: transparent;
+  flex-shrink: 0;
 }
 
 .devtools-tab-label {
   font-size: 12px;
   mask-image: linear-gradient(to left, transparent 0, black 6px);
   /* Set the end padding on the label to make sure the label gets faded out properly */
   padding-inline-end: 10px;
   min-width: 1px;
 }
 
 .devtools-tab-label:-moz-locale-dir(rtl) {
   mask-image: linear-gradient(to right, transparent 0, black 6px);
 }
 
-/* Hide tab icons when the viewport width is limited */
-@media (max-width: 700px) {
-  .devtools-tab-label {
-    /* Set the end padding on the label to make sure the label gets faded out properly */
-    padding-inline-end: 5px;
-  }
-
-  .devtools-tab:not(.devtools-tab-icon-only) {
-    padding-inline-start: 5px !important;
-  }
-
-  /* Hide the icons */
-  .devtools-tab:not(.devtools-tab-icon-only) > img {
-    display: none;
-  }
-}
-
 .devtools-tab-icon-only {
   min-width: 24px;
 }
 
 .devtools-tab {
   color: var(--theme-toolbar-color);
 }
 
@@ -139,31 +122,39 @@
   flex-shrink: 0;
 }
 
 .devtools-tab > label {
   white-space: nowrap;
   margin: 0 4px;
 }
 
-.devtools-tab > img {
+.devtools-tab > img,
+.tools-chevron-menu > img {
   -moz-context-properties: fill;
-  fill: var(--theme-toolbar-color);
+  fill: var(--theme-toolbar-photon-icon-color);
 }
 
 .devtools-tab.selected > img {
   fill: var(--theme-toolbar-selected-color);
 }
 
 .devtools-tab.highlighted > img {
   fill: var(--theme-toolbar-highlighted-color);
 }
 
 /* Toolbox controls */
 
+#tools-chevron-menu-button::before {
+  top: 0;
+  offset-inline-end: 0;
+  background-image: var(--command-chevron-image);
+  background-position: center;
+}
+
 #toolbox-controls {
   margin-right: 3px;
 }
 
 #toolbox-buttons-end > .devtools-separator {
   margin-inline-start: 5px;
   margin-inline-end: 5px;
 }