Files
Nicolas Chevobbe f957ab13f6 Bug 2010233 - [devtools] Don't change state in TabBar#select if function is called with activeTab. r=devtools-reviewers,ochameau.
Since we're now avoiding unnecessary work, we were missing events in the
openInspectorSidebarTab test helper.
To fix this, we make ToolSidebar#select return a Promise that resolves once
the select is done (or directly if the panel was already selected).
Callsites of ToolSidebar#select are updated accordingly.

We also add initialized promises to both the BoxModel and LayoutView so we
can wait on those instead of waiting for specific events.
We stop emitting the `grid-panel-updated` event as it's not listened for anymore.

Differential Revision: https://phabricator.services.mozilla.com/D278942
2026-01-21 18:54:15 +00:00

390 lines
9.3 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,
createRef,
} = require("resource://devtools/client/shared/vendor/react.mjs");
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
const Sidebar = createFactory(
require("resource://devtools/client/shared/components/Sidebar.js")
);
loader.lazyRequireGetter(
this,
"Menu",
"resource://devtools/client/framework/menu.js"
);
loader.lazyRequireGetter(
this,
"MenuItem",
"resource://devtools/client/framework/menu-item.js"
);
// Shortcuts
const { div } = dom;
/**
* Renders Tabbar component.
*/
class Tabbar extends Component {
static get propTypes() {
return {
children: PropTypes.array,
menuDocument: PropTypes.object,
onSelect: PropTypes.func,
showAllTabsMenu: PropTypes.bool,
allTabsMenuButtonTooltip: PropTypes.string,
activeTabId: PropTypes.string,
renderOnlySelected: PropTypes.bool,
sidebarToggleButton: PropTypes.shape({
// Set to true if collapsed.
collapsed: PropTypes.bool.isRequired,
// Tooltip text used when the button indicates expanded state.
collapsePaneTitle: PropTypes.string.isRequired,
// Tooltip text used when the button indicates collapsed state.
expandPaneTitle: PropTypes.string.isRequired,
// Click callback
onClick: PropTypes.func.isRequired,
// align toggle button to right
alignRight: PropTypes.bool,
// if set to true toggle-button rotate 90
canVerticalSplit: PropTypes.bool,
}),
};
}
static get defaultProps() {
return {
menuDocument: window.parent.document,
showAllTabsMenu: false,
};
}
constructor(props, context) {
super(props, context);
const { activeTabId, children = [] } = props;
const tabs = this.createTabs(children);
const activeTab = tabs.findIndex(tab => tab.id === activeTabId);
this.state = {
activeTab: activeTab === -1 ? 0 : activeTab,
tabs,
};
// Array of queued tabs to add to the Tabbar.
this.queuedTabs = [];
this.createTabs = this.createTabs.bind(this);
this.addTab = this.addTab.bind(this);
this.addAllQueuedTabs = this.addAllQueuedTabs.bind(this);
this.queueTab = this.queueTab.bind(this);
this.toggleTab = this.toggleTab.bind(this);
this.removeTab = this.removeTab.bind(this);
this.select = this.select.bind(this);
this.getTabIndex = this.getTabIndex.bind(this);
this.getTabId = this.getTabId.bind(this);
this.getCurrentTabId = this.getCurrentTabId.bind(this);
this.onTabChanged = this.onTabChanged.bind(this);
this.onAllTabsMenuClick = this.onAllTabsMenuClick.bind(this);
this.renderTab = this.renderTab.bind(this);
this.tabbarRef = createRef();
}
// FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
UNSAFE_componentWillReceiveProps(nextProps) {
const { activeTabId, children = [] } = nextProps;
const tabs = this.createTabs(children);
const activeTab = tabs.findIndex(tab => tab.id === activeTabId);
if (
activeTab !== this.state.activeTab ||
children !== this.props.children
) {
this.setState({
activeTab: activeTab === -1 ? 0 : activeTab,
tabs,
});
}
}
createTabs(children) {
return children
.filter(panel => panel)
.map((panel, index) =>
Object.assign({}, children[index], {
id: panel.props.id || index,
panel,
title: panel.props.title,
})
);
}
// Public API
addTab(id, title, selected = false, panel, url, index = -1) {
const tabs = this.state.tabs.slice();
if (index >= 0) {
tabs.splice(index, 0, { id, title, panel, url });
} else {
tabs.push({ id, title, panel, url });
}
const newState = Object.assign({}, this.state, {
tabs,
});
if (selected) {
newState.activeTab = index >= 0 ? index : tabs.length - 1;
}
this.setState(newState, () => {
if (this.props.onSelect && selected) {
this.props.onSelect(id);
}
});
}
addAllQueuedTabs() {
if (!this.queuedTabs.length) {
return;
}
const tabs = this.state.tabs.slice();
// Preselect the first sidebar tab if none was explicitly selected.
let activeTab = 0;
let activeId = this.queuedTabs[0].id;
for (const { id, index, panel, selected, title, url } of this.queuedTabs) {
if (index >= 0) {
tabs.splice(index, 0, { id, title, panel, url });
} else {
tabs.push({ id, title, panel, url });
}
if (selected) {
activeId = id;
activeTab = index >= 0 ? index : tabs.length - 1;
}
}
const newState = Object.assign({}, this.state, {
activeTab,
tabs,
});
this.setState(newState, () => {
if (this.props.onSelect) {
this.props.onSelect(activeId);
}
});
this.queuedTabs = [];
}
/**
* Queues a tab to be added. This is more performant than calling addTab for every
* single tab to be added since we will limit the number of renders happening when
* a new state is set. Once all the tabs to be added have been queued, call
* addAllQueuedTabs() to populate the TabBar with all the queued tabs.
*/
queueTab(id, title, selected = false, panel, url, index = -1) {
this.queuedTabs.push({
id,
index,
panel,
selected,
title,
url,
});
}
toggleTab(tabId, isVisible) {
const index = this.getTabIndex(tabId);
if (index < 0) {
return;
}
const tabs = this.state.tabs.slice();
tabs[index] = Object.assign({}, tabs[index], {
isVisible,
});
this.setState(
Object.assign({}, this.state, {
tabs,
})
);
}
removeTab(tabId) {
const index = this.getTabIndex(tabId);
if (index < 0) {
return;
}
const tabs = this.state.tabs.slice();
tabs.splice(index, 1);
let activeTab = this.state.activeTab - 1;
activeTab = activeTab === -1 ? 0 : activeTab;
this.setState(
Object.assign({}, this.state, {
activeTab,
tabs,
}),
() => {
// Select the next active tab and force the select event handler to initialize
// the panel if needed.
if (tabs.length && this.props.onSelect) {
this.props.onSelect(this.getTabId(activeTab));
}
}
);
}
/**
* Selects a given tab
*
* @param {string} tabId
* @returns {Promise}
*/
select(tabId) {
const docRef = this.tabbarRef.current.ownerDocument;
const index = this.getTabIndex(tabId);
if (index < 0) {
return Promise.resolve();
}
if (this.state.activeTab === index) {
return Promise.resolve();
}
const newState = Object.assign({}, this.state, {
activeTab: index,
});
const tabDomElement = docRef.querySelector(`[data-tab-index="${index}"]`);
if (tabDomElement) {
tabDomElement.scrollIntoView();
}
return new Promise(resolve => {
this.setState(newState, () => {
if (this.props.onSelect) {
this.props.onSelect(tabId);
}
resolve();
});
});
}
// Helpers
getTabIndex(tabId) {
let tabIndex = -1;
this.state.tabs.forEach((tab, index) => {
if (tab.id === tabId) {
tabIndex = index;
}
});
return tabIndex;
}
getTabId(index) {
return this.state.tabs[index].id;
}
getCurrentTabId() {
return this.state.tabs[this.state.activeTab].id;
}
// Event Handlers
onTabChanged(index) {
this.setState(
{
activeTab: index,
},
() => {
if (this.props.onSelect) {
this.props.onSelect(this.state.tabs[index].id);
}
}
);
}
onAllTabsMenuClick(event) {
const menu = new Menu();
const target = event.target;
// Generate list of menu items from the list of tabs.
this.state.tabs.forEach(tab => {
menu.append(
new MenuItem({
label: tab.title,
type: "checkbox",
checked: this.getCurrentTabId() === tab.id,
click: () => this.select(tab.id),
})
);
});
// Show a drop down menu with frames.
menu.popupAtTarget(target);
return menu;
}
// Rendering
renderTab(tab) {
if (typeof tab.panel === "function") {
return tab.panel({
key: tab.id,
title: tab.title,
id: tab.id,
url: tab.url,
});
}
return tab.panel;
}
render() {
const tabs = this.state.tabs.map(tab => this.renderTab(tab));
return div(
{
className: "devtools-sidebar-tabs",
ref: this.tabbarRef,
},
Sidebar(
{
onAllTabsMenuClick: this.onAllTabsMenuClick,
renderOnlySelected: this.props.renderOnlySelected,
showAllTabsMenu: this.props.showAllTabsMenu,
allTabsMenuButtonTooltip: this.props.allTabsMenuButtonTooltip,
sidebarToggleButton: this.props.sidebarToggleButton,
activeTab: this.state.activeTab,
onAfterChange: this.onTabChanged,
},
tabs
)
);
}
}
module.exports = Tabbar;