Bug 1335608 - add a button to select hidden tools when toolbox toolbar overflows;r=gregtatum
authorJulian Descottes <jdescottes@mozilla.com>
Wed, 01 Mar 2017 17:32:55 +0100
changeset 392057 c865b539abeab0daaa42083088adeaf35f8458e2
parent 392056 17a5205504383bc73553fe208749dc4b943c71d6
child 392058 10479537baf6bdc5d7168c586a17067b757b1023
push id7198
push userjlorenzo@mozilla.com
push dateTue, 18 Apr 2017 12:07:49 +0000
treeherdermozilla-beta@d57aa49c3948 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgregtatum
bugs1335608
milestone54.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 1335608 - add a button to select hidden tools when toolbox toolbar overflows;r=gregtatum MozReview-Commit-ID: HgfSteV6WXy
devtools/client/framework/components/moz.build
devtools/client/framework/components/toolbox-tabs.js
devtools/client/framework/components/toolbox-toolbar.js
devtools/client/framework/test/browser.ini
devtools/client/framework/test/browser_toolbox_toolbar_overflow.js
devtools/client/framework/toolbox.js
devtools/client/locales/en-US/toolbox.properties
devtools/client/shared/components/tabs/tabs.css
devtools/client/themes/common.css
devtools/client/themes/toolbox.css
--- a/devtools/client/framework/components/moz.build
+++ b/devtools/client/framework/components/moz.build
@@ -3,10 +3,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/.
 
 
 DevToolsModules(
   'toolbox-controller.js',
   'toolbox-tab.js',
+  'toolbox-tabs.js',
   'toolbox-toolbar.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/components/toolbox-tabs.js
@@ -0,0 +1,154 @@
+/* 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 {DOM, createClass, createFactory, PropTypes} = require("devtools/client/shared/vendor/react");
+
+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"));
+
+module.exports = createClass({
+  displayName: "ToolboxTabs",
+
+  // See toolbox-toolbar propTypes for details on the props used here.
+  propTypes: {
+    currentToolId: PropTypes.string,
+    focusButton: PropTypes.func,
+    focusedButton: PropTypes.string,
+    highlightedTool: PropTypes.string,
+    panelDefinitions: PropTypes.array,
+    selectTool: PropTypes.func,
+    toolbox: PropTypes.object,
+    L10N: PropTypes.object,
+  },
+
+  getInitialState() {
+    return {
+      overflow: false,
+    };
+  },
+
+  componentDidUpdate() {
+    this.addFlowEvents();
+  },
+
+  componentWillUnmount() {
+    this.removeFlowEvents();
+  },
+
+  addFlowEvents() {
+    this.removeFlowEvents();
+    let node = findDOMNode(this);
+    if (node) {
+      node.addEventListener("overflow", this.onOverflow);
+      node.addEventListener("underflow", this.onUnderflow);
+    }
+  },
+
+  removeFlowEvents() {
+    let node = findDOMNode(this);
+    if (node) {
+      node.removeEventListener("overflow", this.onOverflow);
+      node.removeEventListener("underflow", this.onUnderflow);
+    }
+  },
+
+  onOverflow() {
+    this.setState({
+      overflow: true
+    });
+  },
+
+  onUnderflow() {
+    this.setState({
+      overflow: false
+    });
+  },
+
+  /**
+   * 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.
+   */
+  render() {
+    let {
+      currentToolId,
+      focusButton,
+      focusedButton,
+      highlightedTool,
+      panelDefinitions,
+      selectTool,
+    } = this.props;
+
+    let tabs = panelDefinitions.map(panelDefinition => ToolboxTab({
+      currentToolId,
+      focusButton,
+      focusedButton,
+      highlightedTool,
+      panelDefinition,
+      selectTool,
+    }));
+
+    // 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
+    );
+  },
+});
+
+/**
+ * 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,
+        }));
+      });
+
+      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/components/toolbox-toolbar.js
+++ b/devtools/client/framework/components/toolbox-toolbar.js
@@ -1,16 +1,18 @@
 /* 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 {DOM, createClass, createFactory, PropTypes} = require("devtools/client/shared/vendor/react");
 const {div, button} = DOM;
+
 const ToolboxTab = createFactory(require("devtools/client/framework/components/toolbox-tab"));
+const ToolboxTabs = createFactory(require("devtools/client/framework/components/toolbox-tabs"));
 
 /**
  * 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.
  */
 module.exports = createClass({
@@ -34,69 +36,43 @@ module.exports = createClass({
     focusButton: PropTypes.func,
     // The options button definition.
     optionsPanel: PropTypes.object,
     // Hold off displaying the toolbar until enough information is ready for it to render
     // nicely.
     canRender: PropTypes.bool,
     // Localization interface.
     L10N: PropTypes.object,
+    // The devtools toolbox
+    toolbox: PropTypes.object,
   },
 
   /**
    * The render function is kept fairly short for maintainability. See the individual
    * render functions for how each of the sections is rendered.
    */
   render() {
     const containerProps = {className: "devtools-tabbar"};
     return this.props.canRender
       ? (
         div(
           containerProps,
           renderToolboxButtonsStart(this.props),
-          renderTabs(this.props),
+          ToolboxTabs(this.props),
           renderToolboxButtonsEnd(this.props),
           renderOptions(this.props),
           renderSeparator(),
           renderDockButtons(this.props)
         )
       )
       : div(containerProps);
   }
 });
 
 /**
- * Render all of the tabs, this takes in the panel definitions and builds out
- * the buttons for each of them.
- *
- * @param {Array}    panelDefinitions - Array of objects that define panels.
- * @param {String}   currentToolId - The currently selected tool's id; e.g. "inspector".
- * @param {String}   highlightedTool - If a tool is highlighted, this is it's id.
- * @param {Function} selectTool - Function to select a tool in the toolbox.
- * @param {String}   focusedButton - The id of the focused button.
- * @param {Function} focusButton - Keep a record of the currently focused button.
- */
-function renderTabs({panelDefinitions, currentToolId, highlightedTool, selectTool,
-                     focusedButton, focusButton}) {
-  // A wrapper is needed to get flex sizing correct in XUL.
-  return div({className: "toolbox-tabs-wrapper"},
-    div({className: "toolbox-tabs"},
-      ...panelDefinitions.map(panelDefinition => ToolboxTab({
-        panelDefinition,
-        currentToolId,
-        highlightedTool,
-        selectTool,
-        focusedButton,
-        focusButton,
-      }))
-    )
-  );
-}
-
-/**
  * A little helper function to call renderToolboxButtons for buttons at the start
  * of the toolbox.
  */
 function renderToolboxButtonsStart(props) {
   return renderToolboxButtons(props, true);
 }
 
 /**
@@ -145,17 +121,17 @@ function renderToolboxButtons({toolboxBu
         onFocus: () => focusButton(id),
         tabIndex: id === focusedButton ? "0" : "-1"
       });
     })
   );
 }
 
 /**
- * The options button is a ToolboxTab just like in the renderTabs() function. However
+ * The options button is a ToolboxTab just like in the ToolboxTabs component. However
  * it is separate from the normal tabs, so deal with it separately here.
  *
  * @param {Object}   optionsPanel - A single panel definition for the options panel.
  * @param {String}   currentToolId - The currently selected tool's id; e.g. "inspector".
  * @param {Function} selectTool - Function to select a tool in the toolbox.
  * @param {String}   focusedButton - The id of the focused button.
  * @param {Function} focusButton - Keep a record of the currently focused button.
  */
--- a/devtools/client/framework/test/browser.ini
+++ b/devtools/client/framework/test/browser.ini
@@ -75,16 +75,17 @@ skip-if = e10s # Bug 1069044 - destroyIn
 [browser_toolbox_target.js]
 [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_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]
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_toolbar_overflow.js
@@ -0,0 +1,87 @@
+/* -*- 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";
+
+// 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(function* () {
+  let tab = yield addTab("about:blank");
+
+  info("Open devtools on the Inspector in a separate window");
+  let toolbox = yield openToolboxForTab(tab, "inspector", Toolbox.HostType.WINDOW);
+
+  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);
+  yield onResize;
+
+  let allToolsButton = toolbox.doc.querySelector(".all-tools-menu");
+  ok(!allToolsButton, "The all tools button is not displayed");
+
+  info("Resize devtools window to a width that should trigger an overflow");
+  onResize = once(hostWindow, "resize");
+  hostWindow.resizeTo(300, 300);
+  yield onResize;
+
+  info("Wait until the all tools button is available");
+  yield waitUntil(() => toolbox.doc.querySelector(".all-tools-menu"));
+
+  allToolsButton = toolbox.doc.querySelector(".all-tools-menu");
+  ok(allToolsButton, "The all tools button is displayed");
+
+  info("Open the all-tools-menupopup and verify that the inspector button is checked");
+  let menuPopup = yield openAllToolsMenu(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 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");
+
+  info("Switch to the webconsole using the all-tools-menupopup popup");
+  let onSelected = toolbox.once("webconsole-selected");
+  consoleButton.click();
+  yield onSelected;
+
+  info("Closing the all-tools-menupopup popup");
+  let onPopupHidden = once(menuPopup, "popuphidden");
+  menuPopup.hidePopup();
+  yield onPopupHidden;
+
+  info("Re-open the all-tools-menupopup and verify that the console button is checked");
+  menuPopup = yield 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");
+  hostWindow.resizeTo(originalWidth, originalHeight);
+});
+
+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");
+  yield waitUntil(() => menuPopup && menuPopup.state === "open");
+
+  return menuPopup;
+}
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -999,16 +999,17 @@ Toolbox.prototype = {
     // Ensure the toolbar doesn't try to render until the tool is ready.
     const element = this.React.createElement(this.ToolboxController, {
       L10N,
       currentToolId: this.currentToolId,
       selectTool: this.selectTool,
       closeToolbox: this.destroy,
       focusButton: this._onToolbarFocus,
       toggleMinimizeMode: this._toggleMinimizeMode,
+      toolbox: this
     });
 
     this.component = this.ReactDOM.render(element, this._componentMount);
   },
 
   /**
    * Reset tabindex attributes across all focusable elements inside the toolbar.
    * Only have one element with tabindex=0 at a time to make sure that tabbing
--- a/devtools/client/locales/en-US/toolbox.properties
+++ b/devtools/client/locales/en-US/toolbox.properties
@@ -168,8 +168,12 @@ toolbox.frames.tooltip=Select an iframe 
 # the button to force the popups/panels to stay visible on blur.
 # This is only visible in the browser toolbox as it is meant for
 # addon developers and Firefox contributors.
 toolbox.noautohide.tooltip=Disable popup auto hide
 
 # LOCALIZATION NOTE (toolbox.closebutton.tooltip): This is the tooltip for
 # the close button the developer tools toolbox.
 toolbox.closebutton.tooltip=Close Developer Tools
+
+# LOCALIZATION NOTE (toolbox.allToolsButton.tooltip): This is the tooltip for the
+# "all tools" button displayed when some tools are hidden by overflow of the toolbar.
+toolbox.allToolsButton.tooltip=Select another tool
--- a/devtools/client/shared/components/tabs/tabs.css
+++ b/devtools/client/shared/components/tabs/tabs.css
@@ -46,29 +46,16 @@
 .tabs .panels {
   height: calc(100% - 24px);
 }
 
 .tabs .tab-panel {
   height: 100%;
 }
 
-.tabs .all-tabs-menu  {
-  position: absolute;
-  top: 0;
-  offset-inline-end: 0;
-  width: 15px;
-  height: 100%;
-  border-inline-start: 1px solid var(--theme-splitter-color);
-  background: var(--theme-tab-toolbar-background);
-  background-image: url("chrome://devtools/skin/images/dropmarker.svg");
-  background-repeat: no-repeat;
-  background-position: center;
-}
-
 .tabs .tabs-navigation,
 .tabs .tabs-navigation {
   position: relative;
   border-bottom: 1px solid var(--theme-splitter-color);
   background: var(--theme-tab-toolbar-background);
 }
 
 .theme-dark .tabs .tabs-menu-item,
--- a/devtools/client/themes/common.css
+++ b/devtools/client/themes/common.css
@@ -667,8 +667,26 @@ checkbox:-moz-focusring {
 @keyframes throbber-spin {
   from {
     transform: none;
   }
   to {
     transform: rotate(360deg);
   }
 }
+
+/* Common tabs styles */
+
+.all-tabs-menu {
+  position: absolute;
+
+  top: 0;
+  offset-inline-end: 0;
+  width: 15px;
+  height: 100%;
+
+  border-inline-start: 1px solid var(--theme-splitter-color);
+
+  background: var(--theme-tab-toolbar-background);
+  background-image: url("chrome://devtools/skin/images/dropmarker.svg");
+  background-repeat: no-repeat;
+  background-position: center;
+}
--- a/devtools/client/themes/toolbox.css
+++ b/devtools/client/themes/toolbox.css
@@ -50,24 +50,31 @@
   background: var(--theme-tab-toolbar-background);
   border-bottom-color: var(--theme-splitter-color);
   display: flex;
 }
 
 .toolbox-tabs {
   margin: 0;
   flex: 1;
+  overflow: hidden;
 }
 
 .toolbox-tabs-wrapper {
   position: relative;
   display: flex;
   flex: 1;
 }
 
+.toolbox-tabs-wrapper .all-tools-menu {
+  border-inline-end: 1px solid var(--theme-splitter-color);
+  border-top-width: 0;
+  border-bottom-width: 0;
+}
+
 .toolbox-tabs {
   position: absolute;
   top: 0;
   left: 0;
   right: 0;
   bottom: 0;
   display: flex;
 }