Bug 1335608 - add a button to select hidden tools when toolbox toolbar overflows;r=gregtatum
MozReview-Commit-ID: HgfSteV6WXy
--- 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;
}