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 374442 c865b539abeab0daaa42083088adeaf35f8458e2
parent 374441 17a5205504383bc73553fe208749dc4b943c71d6
child 374443 10479537baf6bdc5d7168c586a17067b757b1023
push id10863
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 23:02:23 +0000
treeherdermozilla-aurora@0931190cd725 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgregtatum
bugs1335608
milestone54.0a1
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;
 }