Files
Florian Quèze 6abfa5161b Bug 2070923 - Render the toolbox toolbar before Toolbox.open() resolves, r=devtools-reviewers,jdescottes.
ToolboxController renders no tab and no command button until its canRender state
is set, and open() only schedules that from an idle callback, which can be
starved past the end of open(). The tracer tests then hand a null
command-button-jstracer to synthesizeMouseAtCenter.

Bug 1399548 added the gate to stop the component rendering before the toolbar
buttons were built, when #buildButtons() was asynchronous; it is synchronous now,
so no intermediate render is left to prevent. Awaiting the idle callback instead
would leave open() pending once its window is gone.

Differential Revision: https://phabricator.services.mozilla.com/D325033
2026-09-11 11:29:57 +00:00

237 lines
6.8 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("resource://devtools/client/shared/vendor/react.mjs");
const ToolboxToolbar = createFactory(
require("resource://devtools/client/framework/components/ToolboxToolbar.js")
);
const ELEMENT_PICKER_ID = "command-button-pick";
/**
* This component serves as a state controller for the toolbox React component. It's a
* thin layer for translating events and state of the outside world into the React update
* cycle. This solution was used to keep the amount of code changes to a minimimum while
* adapting the existing codebase to start using React.
*/
class ToolboxController extends Component {
constructor(props, context) {
super(props, context);
// See the ToolboxToolbar propTypes for documentation on each of these items in
// state, and for the definitions of the props that are expected to be passed in.
this.state = {
focusedButton: ELEMENT_PICKER_ID,
toolboxButtons: [],
visibleToolboxButtonCount: 0,
currentToolId: null,
highlightedTools: new Set(),
panelDefinitions: [],
hostTypes: [],
currentHostType: undefined,
areDockOptionsEnabled: true,
canCloseToolbox: true,
isSplitConsoleActive: false,
disableAutohide: undefined,
alwaysOnTop: undefined,
pseudoLocale: undefined,
canRender: false,
buttonIds: [],
checkedButtonsUpdated: () => {
this.forceUpdate();
},
};
this.setFocusedButton = this.setFocusedButton.bind(this);
this.setToolboxButtons = this.setToolboxButtons.bind(this);
this.setCurrentToolId = this.setCurrentToolId.bind(this);
this.highlightTool = this.highlightTool.bind(this);
this.unhighlightTool = this.unhighlightTool.bind(this);
this.setHostTypes = this.setHostTypes.bind(this);
this.setCurrentHostType = this.setCurrentHostType.bind(this);
this.setDockOptionsEnabled = this.setDockOptionsEnabled.bind(this);
this.setCanCloseToolbox = this.setCanCloseToolbox.bind(this);
this.setIsSplitConsoleActive = this.setIsSplitConsoleActive.bind(this);
this.setDisableAutohide = this.setDisableAutohide.bind(this);
this.setCanRender = this.setCanRender.bind(this);
this.setPanelDefinitions = this.setPanelDefinitions.bind(this);
this.updateButtonIds = this.updateButtonIds.bind(this);
this.updateFocusedButton = this.updateFocusedButton.bind(this);
this.setDebugTargetData = this.setDebugTargetData.bind(this);
}
shouldComponentUpdate() {
return this.state.canRender;
}
componentWillUnmount() {
this.state.toolboxButtons.forEach(button => {
button.off("updatechecked", this.state.checkedButtonsUpdated);
});
}
/**
* The button and tab ids must be known in order to be able to focus left and right
* using the arrow keys.
*/
updateButtonIds() {
const { toolboxButtons, panelDefinitions, canCloseToolbox } = this.state;
// This is a little gnarly, but go through all of the state and extract the IDs.
this.setState({
buttonIds: [
...toolboxButtons
.filter(btn => btn.isInStartContainer)
.map(({ id }) => id),
...panelDefinitions.map(({ id }) => id),
...toolboxButtons
.filter(btn => !btn.isInStartContainer)
.map(({ id }) => id),
canCloseToolbox ? "toolbox-close" : null,
].filter(id => id),
});
this.updateFocusedButton();
}
updateFocusedButton() {
this.setFocusedButton(this.state.focusedButton);
}
setFocusedButton(focusedButton) {
const { buttonIds } = this.state;
focusedButton =
focusedButton && buttonIds.includes(focusedButton)
? focusedButton
: buttonIds[0];
if (this.state.focusedButton !== focusedButton) {
this.setState({
focusedButton,
});
}
}
setCurrentToolId(currentToolId) {
this.setState({ currentToolId }, () => {
// Also set the currently focused button to this tool.
this.setFocusedButton(currentToolId);
});
}
setCanRender() {
// Both callers in Toolbox.open() normally run, and a redundant call would
// re-render the whole toolbar on the toolbox opening path.
if (this.state.canRender) {
return;
}
this.setState({ canRender: true }, this.updateButtonIds);
}
highlightTool(highlightedTool) {
const { highlightedTools } = this.state;
highlightedTools.add(highlightedTool);
this.setState({ highlightedTools });
}
unhighlightTool(id) {
const { highlightedTools } = this.state;
if (highlightedTools.has(id)) {
highlightedTools.delete(id);
this.setState({ highlightedTools });
}
}
setDockOptionsEnabled(areDockOptionsEnabled) {
this.setState({ areDockOptionsEnabled });
}
setHostTypes(hostTypes) {
this.setState({ hostTypes });
}
setCurrentHostType(currentHostType) {
this.setState({ currentHostType });
}
setCanCloseToolbox(canCloseToolbox) {
this.setState({ canCloseToolbox }, this.updateButtonIds);
}
setIsSplitConsoleActive(isSplitConsoleActive) {
this.setState({ isSplitConsoleActive });
}
/**
* @param {bool | undefined} disableAutohide
*/
setDisableAutohide(disableAutohide) {
this.setState({ disableAutohide });
}
/**
* @param {bool | undefined} alwaysOnTop
*/
setAlwaysOnTop(alwaysOnTop) {
this.setState({ alwaysOnTop });
}
/**
* @param {bool} focusedState
*/
setFocusedState(focusedState) {
// We only care about the focused state when the toolbox is always on top
if (this.state.alwaysOnTop) {
this.setState({ focusedState });
}
}
/**
* @param {"bidi" | "accented" | "none" | undefined} pseudoLocale
*/
setPseudoLocale(pseudoLocale) {
this.setState({ pseudoLocale });
}
setPanelDefinitions(panelDefinitions) {
this.setState({ panelDefinitions }, this.updateButtonIds);
}
get panelDefinitions() {
return this.state.panelDefinitions;
}
setToolboxButtons(toolboxButtons) {
// Listen for updates of the checked attribute.
this.state.toolboxButtons.forEach(button => {
button.off("updatechecked", this.state.checkedButtonsUpdated);
});
toolboxButtons.forEach(button => {
button.on("updatechecked", this.state.checkedButtonsUpdated);
});
const visibleToolboxButtonCount = toolboxButtons.filter(
button => button.isVisible
).length;
this.setState(
{ toolboxButtons, visibleToolboxButtonCount },
this.updateButtonIds
);
}
setDebugTargetData(data) {
this.setState({ debugTargetData: data });
}
render() {
return ToolboxToolbar(Object.assign({}, this.props, this.state));
}
}
module.exports = ToolboxController;