Bug 1442531 - Make chevron button of devtool to be exclusive and apply the photon design. r?jdescottes draft
authorMantaroh Yoshinaga <mantaroh@gmail.com>
Thu, 05 Apr 2018 10:20:51 +0900
changeset 777579 bb28a4c90c25d4bffd3ed9cfc60b225d3c65307c
parent 777562 071ee904485e21e19ca08456d32bce6825b77a26
push id105250
push userbmo:mantaroh@gmail.com
push dateThu, 05 Apr 2018 01:23:32 +0000
reviewersjdescottes
bugs1442531
milestone61.0a1
Bug 1442531 - 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 calculate the displayed tab button size after rendering 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/jar.mn
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
@@ -27,59 +27,285 @@ 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 toolId and its width size. This lifecycle is out of
+    // React's lifecycle. If tool is registered, ToolboxTabs will add/remove
+    // target tool id to/from this map.
+    this._registeredToolTabsWidthMap = new Map();
+
+    // This is to store the sum of currently displayed tool tab size due to
+    // the performance problem.
+    this._sumToolTabsWidth = 0;
+    this._nextToolTabSize = 0;
+    this._resizeTimerId = null;
+
+    this.resizeHandler = this.resizeHandler.bind(this);
   }
 
-  componentDidUpdate() {
-    this.addFlowEvents();
+  componentDidMount() {
+    window.removeEventListener("resize", this.resizeHandler);
+    window.addEventListener("resize", this.resizeHandler);
+
+    this.updateRegisteredToolTabsWidthMap();
+    let overflowedTabs = this.calculateOverflowedTabs();
+    if (overflowedTabs) {
+      // eslint-disable-next-line
+      this.setState({
+        overflowedTabIds: overflowedTabs
+      });
+    }
   }
 
-  componentWillUnmount() {
-    this.removeFlowEvents();
+  componentWillUpdate(nextProps, nextState) {
+    if (this.shouldRecalculateAndRender(this.props, nextProps)) {
+      // Force recalculate and render in this cycle if panel definition has
+      // changed or selected tool has changed.
+      nextState.overflowedTabIds = [];
+    }
   }
 
-  addFlowEvents() {
-    this.removeFlowEvents();
-    let node = findDOMNode(this);
-    if (node) {
-      node.addEventListener("overflow", this.onOverflow);
-      node.addEventListener("underflow", this.onUnderflow);
+  componentDidUpdate(prevProps, prevState) {
+    if (this.shouldRecalculateAndRender(prevProps, this.props)) {
+      this.updateRegisteredToolTabsWidthMap();
+      let overflowedTabs = this.calculateOverflowedTabs();
+      if (overflowedTabs) {
+        // eslint-disable-next-line
+        this.setState({
+          overflowedTabIds: overflowedTabs
+        });
+      }
     }
   }
 
-  removeFlowEvents() {
+  /**
+   * Return a specified two panel id array is 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("-");
+  }
+
+  /**
+   * Return true if we should re-calculate the overflowed tabs.
+   */
+  shouldRecalculateAndRender(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);
+  }
+
+  /**
+   * Updatae the Map of tool id and tool tab width.
+   */
+  updateRegisteredToolTabsWidthMap() {
+    let thisNode = findDOMNode(this);
+    let tabs = new Map();
+    let cachedTabs = this._registeredToolTabsWidthMap;
+
+    for (let child of thisNode.querySelectorAll(".devtools-tab")) {
+      let childId = child.id.replace("toolbox-tab-", "");
+      if (cachedTabs && cachedTabs.has(childId)) {
+        tabs.set(childId, cachedTabs.get(childId));
+      } else {
+        let cs = getComputedStyle(child);
+        tabs.set(childId, parseInt(cs.width, 10));
+      }
+    }
+    this._registeredToolTabsWidthMap = tabs;
+  }
+
+  /**
+   * Insert specified tooltab into current toolbox.
+   * If inserted toolbox width is overflowed, remove least tool id of rightmost
+   * from toolbox.
+   */
+  insertToolTabToToolbox(maxWidth,
+                         toolId,
+                         toolIdWidth,
+                         currentVisibleTabs,
+                         currentSumWidth) {
+    if ((currentSumWidth + toolIdWidth) < maxWidth) {
+      currentVisibleTabs.push(toolId);
+      return currentVisibleTabs;
+    }
+
+    let removedToolId = currentVisibleTabs.pop();
+    currentSumWidth -= this._registeredToolTabsWidthMap.get(removedToolId);
+    return this.insertToolTabToToolbox(maxWidth,
+                                       toolId,
+                                       toolIdWidth,
+                                       currentVisibleTabs,
+                                       currentSumWidth);
+  }
+
+  /**
+   * Return the overflowed tab array from currently displayed tab buttons.
+   * If calculated result is same to a current overflowed tab array, this
+   * function will return undefined.
+   */
+  calculateOverflowedTabs() {
     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 willOverflowTabs = [];
+
+    // 26px is chevron button width
+    let sumWidth = this.state.overflowedTabIds.length > 0 ? 26 : 0;
+    let visibleTabs = [];
+
+    for (const id of enabledTabs) {
+      let width = this._registeredToolTabsWidthMap.get(id);
+      sumWidth += width;
+      if (sumWidth <= toolboxWidth) {
+        visibleTabs.push(id);
+      } else {
+        sumWidth -= width;
+        break;
+      }
+    }
+
+    if (!visibleTabs.includes(currentToolId) &&
+        enabledTabs.includes(currentToolId)) {
+      let selectedToolWidth = this._registeredToolTabsWidthMap.get(currentToolId);
+      visibleTabs = this.insertToolTabToToolbox(toolboxWidth,
+                                                currentToolId,
+                                                selectedToolWidth,
+                                                visibleTabs,
+                                                sumWidth);
+    }
+
+    for (const id of enabledTabs) {
+      if (!visibleTabs.includes(id)) {
+        willOverflowTabs.push(id);
+      }
+    }
+
+    if (!this.equalToolIdArray(this.state.overflowedTabIds, willOverflowTabs)) {
+      return willOverflowTabs;
+    }
+
+    return undefined;
+  }
+
+  resizeHandler(evt) {
+    window.cancelIdleCallback(this._resizeTimerId);
+    this._resizeTimerId = window.requestIdleCallback(() => {
+      let node = findDOMNode(this);
+      let tabWidth = parseInt(getComputedStyle(node).width, 10);
+      this.updateToolboxTabs(tabWidth);
+    }, { timeout: 100 });
+  }
+
+  shouldUpdateToolboxTabs(toolboxWidth) {
+    // Use the cache for detecting overflow/underflow.
+    if (this._sumToolTabsWidth != 0 &&
+        this._nextToolTabSize != 0) {
+      let space = toolboxWidth - this._sumToolTabsWidth;
+      if (space > 0 && space < this._nextToolTabSize) {
+        return false;
+      }
+      let displayedTabNum =
+          this.props.panelDefinitions.length -
+          this.state.overflowedTabIds.length;
+      // Skip underflow calculation if the tab which is displayed
+      // is current tool only.
+      if (space < 0 && displayedTabNum == 1) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  updateToolboxTabs(toolboxWidth) {
+    if (!this.shouldUpdateToolboxTabs(toolboxWidth)) {
+      return;
+    }
+    let overflowtabs = this.calculateOverflowedTabs();
+    if (overflowtabs) {
+      this.setState({
+        overflowedTabIds: overflowtabs
+      });
+
+      // Refresh the tab width caches.
+      let displayedWidth;
+      let _nextToolTabSize;
+      this._registeredToolTabsWidthMap.forEach((val, key, map) => {
+        if (!overflowtabs.includes(key)) {
+          displayedWidth += val;
+        } else if (!_nextToolTabSize) {
+          _nextToolTabSize = val;
+        }
+      });
+      this._sumToolTabsWidth = displayedWidth;
+      this._nextToolTabSize = _nextToolTabSize;
     }
   }
 
-  onOverflow() {
-    this.setState({
-      overflow: true
-    });
-  }
+  /**
+   * Render a button to access all tools, displayed only when the toolbar presents an
+   * overflow.
+   */
+  renderToolsChevronButton() {
+    let {
+      panelDefinitions,
+      selectTool,
+      toolbox,
+      L10N,
+    } = this.props;
+
+    return button({
+      className: "devtools-button tools-chevronmenu",
+      tabIndex: -1,
+      title: L10N.getStr("toolbox.allToolsButton.tooltip"),
+      onClick: ({ target }) => {
+        let menu = new Menu({
+          id: "tools-chevron-menupopup"
+        });
 
-  onUnderflow() {
-    this.setState({
-      overflow: false
+        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
    * container has an overflow.
    */
@@ -88,79 +314,41 @@ class ToolboxTabs extends Component {
       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
@@ -106,16 +106,17 @@ skip-if = e10s # Bug 1069044 - destroyIn
 [browser_toolbox_tabsswitch_shortcuts.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
@@ -16,16 +16,23 @@ 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 all tab menu, increase the width of devtool.");
+  let hostWindow = toolbox.win.parent;
+  let onResize = once(hostWindow, "resize");
+  hostWindow.resizeTo(1300, hostWindow.outerHeight);
+  await onResize;
+
   doc = toolbox.doc;
   await registerNewPerToolboxTool();
   await testSelectTool();
   await testOptionsShortcut();
   await testOptions();
   await testToggleTools();
   await cleanup();
 });
--- a/devtools/client/framework/test/browser_toolbox_toolbar_overflow.js
+++ b/devtools/client/framework/test/browser_toolbox_toolbar_overflow.js
@@ -15,73 +15,80 @@ add_task(async function () {
 
   info("Open devtools on the Inspector in a separate window");
   let toolbox = await openToolboxForTab(tab, "inspector", Toolbox.HostType.WINDOW);
 
   let hostWindow = toolbox.win.parent;
   let originalWidth = hostWindow.outerWidth;
   let originalHeight = hostWindow.outerHeight;
 
+  // After resizing the devtools window, toolbox needs one frame to restyle the
+  // chevron menu button.
+  let raf = (function() {
+    let resolve = null;
+    const promise = new Promise(r => resolve = r);
+    requestIdleCallback(() => {
+      requestAnimationFrame(resolve);
+    });
+    return promise;
+  });
+
   info("Resize devtools window to a width that should not trigger any overflow");
   let onResize = once(hostWindow, "resize");
-  hostWindow.resizeTo(640, 300);
+  hostWindow.resizeTo(1300, 300);
   await onResize;
+  await raf();
 
-  let allToolsButton = toolbox.doc.querySelector(".all-tools-menu");
-  ok(!allToolsButton, "The all tools button is not displayed");
+  let chevronMenuButton = toolbox.doc.querySelector(".tools-chevronmenu");
+  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"));
+  await raf();
 
-  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-chevronmenu"));
 
-  info("Open the all-tools-menupopup and verify that the inspector button is checked");
-  let menuPopup = await openAllToolsMenu(toolbox);
+  chevronMenuButton = toolbox.doc.querySelector(".tools-chevronmenu");
+  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, "A chevron menu doesn't have the inspector button.");
+
+  let consoleButton = toolbox.doc.querySelector("#tools-chevron-menupopup-webconsole");
+  ok(!consoleButton, "A 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, "A 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);
+async function openChevronMenu(toolbox) {
+  let chevronMenuButton = toolbox.doc.querySelector(".tools-chevronmenu");
+  EventUtils.synthesizeMouseAtCenter(chevronMenuButton, {}, toolbox.win);
 
-  let menuPopup = toolbox.doc.querySelector("#all-tools-menupopup");
-  ok(menuPopup, "all-tools-menupopup is available");
+  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;
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_toolbar_reorder_by_width.js
@@ -0,0 +1,102 @@
+/* -*- 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/ */
+
+/* import-globals-from shared-head.js */
+"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.SIDE);
+
+  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(800, originalHeight);
+  await onResize;
+
+  info("Wait until the chevron menu button is available");
+  await waitUntil(() => toolbox.doc.querySelector(".tools-chevronmenu"));
+
+  let chevronMenuButton = toolbox.doc.querySelector(".tools-chevronmenu");
+  ok(chevronMenuButton, "The chevron menu button is displayed");
+
+  info("Confirm that selected menu doesn't hidden.");
+  let storageButton = toolbox.doc.querySelector("#toolbox-tab-storage");
+  ok(storageButton, "A chevron menu has the storage button.");
+
+  info("Regist a new menu");
+  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 chevron menu button.");
+  let popup = await openChevronMenu(toolbox);
+
+  info("A registered new tool menu should be in the chevron menu.");
+  let testToolsButton = toolbox.doc.querySelector("#tools-chevron-menupopup-test-tools");
+  ok(testToolsButton, "A chevron 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 chevron menu button.");
+  popup = await openChevronMenu(toolbox);
+
+  info("A unregistered new tool menu should not be in the chevron menu.");
+  testToolsButton = toolbox.doc.querySelector("#tools-chevron-menupopup-test-tools");
+  ok(!testToolsButton, "A chevron menu doesn't have a unregistered new tool button.");
+
+  info("Closing the tools-chevron-menupopup popup");
+  onPopupHidden = once(popup, "popuphidden");
+  popup.hidePopup();
+  await onPopupHidden;
+
+  info("Restore the original window size");
+  onResize = once(hostWindow, "resize");
+  hostWindow.resizeTo(originalWidth, originalHeight);
+  await onResize;
+});
+
+async function openChevronMenu(toolbox) {
+  let chevronMenuButton = toolbox.doc.querySelector(".tools-chevronmenu");
+  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
@@ -150,16 +150,17 @@ devtools.jar:
     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)
     skin/animationinspector.css (themes/animationinspector.css)
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
@@ -15,16 +15,17 @@
   --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-splitconsole-image: url(images/command-console.svg);
   --command-noautohide-image: url(images/command-noautohide.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);
 }
 
 .theme-firebug {
   --close-button-image: url(chrome://devtools/skin/images/firebug/close.svg);
   --dock-bottom-image: url(chrome://devtools/skin/images/firebug/dock-bottom.svg);
   --dock-side-image: url(chrome://devtools/skin/images/firebug/dock-side.svg);
   --dock-undock-image: url(chrome://devtools/skin/images/firebug/dock-undock.svg);
 
@@ -52,22 +53,28 @@
 }
 
 .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-chevronmenu {
   border-top-width: 0;
   border-bottom-width: 0;
 }
 
+.tools-chevronmenu::before {
+  top: 0;
+  offset-inline-end: 0;
+  width: 12px;
+  background-image: var(--command-chevron-image);
+}
+
 .toolbox-tabs {
   position: absolute;
   top: 0;
   left: 0;
   right: 0;
   bottom: 0;
   display: flex;
   margin: 0;
@@ -87,56 +94,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;
 }
 
 /* Save space on the tab-strip in Firebug theme */
 .theme-firebug .devtools-tab {
   -moz-box-flex: initial;
 }
@@ -178,17 +168,18 @@
   flex-shrink: 0;
 }
 
 .devtools-tab > label {
   white-space: nowrap;
   margin: 0 4px;
 }
 
-.devtools-tab > img {
+.devtools-tab > img,
+.tools-chevronmenu > img {
   -moz-context-properties: fill;
   fill: var(--theme-toolbar-color);
 }
 
 .devtools-tab.selected > img {
   fill: var(--theme-toolbar-selected-color);
 }