fune/devtools/client/framework/components/ToolboxTabs.js
Michael Ratcliffe fcc5c87c5d Bug 1487385 - Add key prop to MenuItem call r=nchevobbe
Fixes this warning every time the toolbox is opened:

"Warning: Each child in an array or iterator should have a unique "key" prop.

Check the render method of `ToolboxTabs`. See https://fb.me/react-warning-keys for more information.
    in MenuItem (created by ToolboxTabs)
    in ToolboxTabs (created by ToolboxToolbar)
    in div (created by ToolboxToolbar)
    in ToolboxToolbar (created by ToolboxController)
    in ToolboxController"

Differential Revision: https://phabricator.services.mozilla.com/D4671

--HG--
extra : moz-landing-system : lando
2018-08-30 13:24:52 +00:00

286 lines
8.9 KiB
JavaScript

/* 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 { Component, createFactory } = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
const { div } = dom;
const MenuButton = createFactory(require("devtools/client/shared/components/menu/MenuButton"));
const MenuItem = createFactory(require("devtools/client/shared/components/menu/MenuItem"));
const MenuList = createFactory(require("devtools/client/shared/components/menu/MenuList"));
const ToolboxTab = createFactory(require("devtools/client/framework/components/ToolboxTab"));
const { ToolboxTabsOrderManager } = require("devtools/client/framework/toolbox-tabs-order-manager");
// 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,
panelDefinitions: PropTypes.array,
selectTool: PropTypes.func,
toolbox: PropTypes.object,
visibleToolboxButtonCount: PropTypes.number.isRequired,
L10N: PropTypes.object,
onTabsOrderUpdated: PropTypes.func.isRequired,
};
}
constructor(props) {
super(props);
this.state = {
// Array of overflowed tool id.
overflowedTabIds: [],
};
// 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);
this._tabsOrderManager = new ToolboxTabsOrderManager(props.onTabsOrderUpdated);
}
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();
}
}
componentWillUnmount() {
window.removeEventListener("resize", this.resizeHandler);
window.cancelIdleCallback(this._resizeTimerId);
this._tabsOrderManager.destroy();
}
/**
* 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("-");
}
/**
* Return true if we should update the overflowed tabs.
*/
shouldUpdateToolboxTabs(prevProps, nextProps) {
if (prevProps.currentToolId !== nextProps.currentToolId ||
prevProps.visibleToolboxButtonCount !== nextProps.visibleToolboxButtonCount) {
return true;
}
const prevPanels = prevProps.panelDefinitions.map(def => def.id);
const nextPanels = nextProps.panelDefinitions.map(def => def.id);
return !this.equalToolIdArray(prevPanels, nextPanels);
}
/**
* Update the Map of tool id and tool tab width.
*/
updateCachedToolTabsWidthMap() {
const thisNode = findDOMNode(this);
const utils = window.windowUtils;
// Force a reflow before calling getBoundingWithoutFlushing on each tab.
thisNode.clientWidth;
for (const tab of thisNode.querySelectorAll(".devtools-tab")) {
const tabId = tab.id.replace("toolbox-tab-", "");
if (!this._cachedToolTabsWidthMap.has(tabId)) {
const rect = utils.getBoundsWithoutFlushing(tab);
this._cachedToolTabsWidthMap.set(tabId, rect.width);
}
}
}
/**
* 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() {
const node = findDOMNode(this);
const toolboxWidth = parseInt(getComputedStyle(node).width, 10);
const { currentToolId } = this.props;
const enabledTabs = this.props.panelDefinitions.map(def => def.id);
let sumWidth = 0;
const visibleTabs = [];
for (const id of enabledTabs) {
const 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) {
const 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)) {
const selectedToolWidth = this._cachedToolTabsWidthMap.get(currentToolId);
while ((sumWidth + selectedToolWidth) > toolboxWidth &&
visibleTabs.length > 0) {
const removingToolId = visibleTabs.pop();
const removingToolWidth = this._cachedToolTabsWidthMap.get(removingToolId);
sumWidth -= removingToolWidth;
}
// If toolbox width is narrow, toolbox display only chevron menu.
// i.e. All tool tabs will overflow.
if ((sumWidth + selectedToolWidth) <= toolboxWidth) {
visibleTabs.push(currentToolId);
}
}
const willOverflowTabs = enabledTabs.filter(id => !visibleTabs.includes(id));
if (!this.equalToolIdArray(this.state.overflowedTabIds, willOverflowTabs)) {
this.setState({ overflowedTabIds: willOverflowTabs });
}
}
resizeHandler(evt) {
window.cancelIdleCallback(this._resizeTimerId);
this._resizeTimerId = window.requestIdleCallback(() => {
this.updateOverflowedTabs();
}, { timeout: 100 });
}
renderToolsChevronMenuList() {
const {
panelDefinitions,
selectTool,
} = this.props;
const items = [];
for (const { id, label, icon } of panelDefinitions) {
if (this.state.overflowedTabIds.includes(id)) {
items.push(MenuItem({
key: id,
id: "tools-chevron-menupopup-" + id,
label,
type: "checkbox",
onClick: () => {
selectTool(id, "tab_switch");
},
icon,
}));
}
}
return MenuList({ id: "tools-chevron-menupopup" }, items);
}
/**
* Render a button to access overflowed tools, displayed only when the toolbar
* presents an overflow.
*/
renderToolsChevronButton() {
const {
toolbox,
} = this.props;
return MenuButton(
{
id: "tools-chevron-menu-button",
menuId: "tools-chevron-menu-button-panel",
className: "devtools-button tools-chevron-menu",
doc: toolbox.doc,
},
this.renderToolsChevronMenuList()
);
}
/**
* Render all of the tabs, based on the panel definitions and builds out
* a toolbox tab for each of them. Will render the chevron button if the
* container has an overflow.
*/
render() {
const {
currentToolId,
focusButton,
focusedButton,
highlightedTools,
panelDefinitions,
selectTool,
} = this.props;
this._tabsOrderManager.setCurrentPanelDefinitions(panelDefinitions);
const 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;
});
return div(
{
className: "toolbox-tabs-wrapper"
},
div(
{
className: "toolbox-tabs",
onMouseDown: (e) => this._tabsOrderManager.onMouseDown(e),
},
tabs,
(this.state.overflowedTabIds.length > 0)
? this.renderToolsChevronButton() : null
)
);
}
}
module.exports = ToolboxTabs;