@samitouri / QOS-React-2 / commits / fae15df40e

[DevTools] Add React Element pane to browser Elements panel (#35240)

Sebastian "Sebbie" Silbermann committed Jan 15, 2026 at 13:24 UTC fae15df40ef878fda68756176d501a4cb83cc9bc
7 files changed +131 -3
packages/react-devtools-extensions/src/main/elementSelection.js
+1 -1
@@ -32,7 +32,7 @@ export function setReactSelectionFromBrowser(bridge) {
32 return;
33 }
34
35 - // Remember to sync the selection next time we show Components tab.
35 + // Remember to sync the selection next time we show inspected element
36 bridge.send('syncSelectionFromBuiltinElementsPanel');
37 }
38 },
packages/react-devtools-extensions/src/main/index.js
+49
@@ -206,6 +206,7 @@ function createBridgeAndStore() {
206 bridge,
207 browserTheme: getBrowserTheme(),
208 componentsPortalContainer,
209 + inspectedElementPortalContainer,
210 profilerPortalContainer,
211 editorPortalContainer,
212 currentSelectedSource,
@@ -278,6 +279,51 @@ function createComponentsPanel() {
279 );
280 }
281
282 +function createElementsInspectPanel() {
283 + if (inspectedElementPortalContainer) {
284 + // Panel is created and user opened it at least once
285 + ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
286 + render();
287 +
288 + return;
289 + }
290 +
291 + if (inspectedElementPane) {
292 + // Panel is created, but wasn't opened yet, so no document is present for it
293 + return;
294 + }
295 +
296 + const elementsPanel = chrome.devtools.panels.elements;
297 + if (__IS_FIREFOX__ || !elementsPanel || !elementsPanel.createSidebarPane) {
298 + // Firefox will not pass the window to the onShown listener despite setPage
299 + // being called.
300 + // See https://bugzilla.mozilla.org/show_bug.cgi?id=2010549
301 +
302 + // May not be supported in some browsers.
303 + // See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/devtools/panels/ElementsPanel/createSidebarPane#browser_compatibility
304 + return;
305 + }
306 +
307 + elementsPanel.createSidebarPane('React Element ⚛', createdPane => {
308 + inspectedElementPane = createdPane;
309 +
310 + createdPane.setPage('panel.html');
311 + createdPane.setHeight('75px');
312 +
313 + createdPane.onShown.addListener(portal => {
314 + inspectedElementPortalContainer = portal.container;
315 + if (inspectedElementPortalContainer != null && render) {
316 + ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
317 +
318 + render();
319 + portal.injectStyles(cloneStyleTags);
320 +
321 + logEvent({event_name: 'selected-inspected-element-pane'});
322 + }
323 + });
324 + });
325 +}
326 +
327 function createProfilerPanel() {
328 if (profilerPortalContainer) {
329 // Panel is created and user opened it at least once
@@ -508,6 +554,7 @@ function mountReactDevTools() {
554 createComponentsPanel();
555 createProfilerPanel();
556 createSourcesEditorPanel();
557 + createElementsInspectPanel();
558 // Suspense Tab is created via the hook
559 // TODO(enableSuspenseTab): Create eagerly once Suspense tab is stable
560 }
@@ -556,10 +603,12 @@ let componentsPanel = null;
603 let profilerPanel = null;
604 let suspensePanel = null;
605 let editorPane = null;
606 +let inspectedElementPane = null;
607 let componentsPortalContainer = null;
608 let profilerPortalContainer = null;
609 let suspensePortalContainer = null;
610 let editorPortalContainer = null;
611 +let inspectedElementPortalContainer = null;
612
613 let mostRecentOverrideTab = null;
614 let render = null;
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css
+8
@@ -77,3 +77,11 @@
77 padding: 0.25rem;
78 color: var(--color-console-error-icon);
79 }
80 +
81 +.VRule {
82 + height: 20px;
83 + width: 1px;
84 + flex: 0 0 1px;
85 + margin: 0 0.5rem;
86 + background-color: var(--color-border);
87 +}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
+13 -2
@@ -34,13 +34,17 @@ import useEditorURL from '../useEditorURL';
34 import styles from './InspectedElement.css';
35 import Tooltip from './reach-ui/tooltip';
36
37 -export type Props = {};
37 +export type Props = {
38 + actionButtons?: React.Node,
39 +};
40
41 // TODO Make edits and deletes also use transition API!
42
43 const noSourcePromise = Promise.resolve(null);
44
43 -export default function InspectedElementWrapper(_: Props): React.Node {
45 +export default function InspectedElementWrapper({
46 + actionButtons,
47 +}: Props): React.Node {
48 const {inspectedElementID} = useContext(TreeStateContext);
49 const bridge = useContext(BridgeContext);
50 const store = useContext(StoreContext);
@@ -305,6 +309,13 @@ export default function InspectedElementWrapper(_: Props): React.Node {
309 symbolicatedSourcePromise={symbolicatedSourcePromise}
310 />
311 )}
312 +
313 + {actionButtons && (
314 + <>
315 + <div className={styles.VRule} />
316 + {actionButtons}
317 + </>
318 + )}
319 </div>
320
321 {inspectedElement === null && (
packages/react-devtools-shared/src/devtools/views/DevTools.js
+11
@@ -26,6 +26,7 @@ import Profiler from './Profiler/Profiler';
26 import SuspenseTab from './SuspenseTab/SuspenseTab';
27 import TabBar from './TabBar';
28 import EditorPane from './Editor/EditorPane';
29 +import InspectedElementPane from './InspectedElement/InspectedElementPane';
30 import {SettingsContextController} from './Settings/SettingsContext';
31 import {TreeContextController} from './Components/TreeContext';
32 import ViewElementSourceContext from './Components/ViewElementSourceContext';
@@ -100,6 +101,7 @@ export type Props = {
101 // The root <DevTools> app is rendered in the top-level extension window,
102 // but individual tabs (e.g. Components, Profiling) can be rendered into portals within their browser panels.
103 componentsPortalContainer?: Element,
104 + inspectedElementPortalContainer?: Element,
105 profilerPortalContainer?: Element,
106 suspensePortalContainer?: Element,
107 editorPortalContainer?: Element,
@@ -155,6 +157,7 @@ export default function DevTools({
157 canViewElementSourceFunction,
158 componentsPortalContainer,
159 editorPortalContainer,
160 + inspectedElementPortalContainer,
161 profilerPortalContainer,
162 suspensePortalContainer,
163 currentSelectedSource,
@@ -379,6 +382,14 @@ export default function DevTools({
382 portalContainer={editorPortalContainer}
383 />
384 ) : null}
385 + {inspectedElementPortalContainer ? (
386 + <InspectedElementPane
387 + selectedSource={currentSelectedSource}
388 + portalContainer={
389 + inspectedElementPortalContainer
390 + }
391 + />
392 + ) : null}
393 </ThemeProvider>
394 </SuspenseTreeContextController>
395 </InspectedElementContextController>
packages/react-devtools-shared/src/devtools/views/InspectedElement/InspectedElementPane.css new
+15
@@ -0,0 +1,15 @@
1 +.InspectedElementPane, .InspectedElementPane * {
2 + box-sizing: border-box;
3 + -webkit-font-smoothing: var(--font-smoothing);
4 +}
5 +
6 +.InspectedElementPane {
7 + position: relative;
8 + width: 100%;
9 + height: 100%;
10 + display: flex;
11 + flex-direction: row;
12 + background-color: var(--color-background);
13 + color: var(--color-text);
14 + font-family: var(--font-family-sans);
15 +}
packages/react-devtools-shared/src/devtools/views/InspectedElement/InspectedElementPane.js new
+34
@@ -0,0 +1,34 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import * as React from 'react';
11 +import {useContext} from 'react';
12 +
13 +import portaledContent from 'react-devtools-shared/src/devtools/views/portaledContent';
14 +import {OptionsContext} from 'react-devtools-shared/src/devtools/views/context';
15 +import InspectedElement from 'react-devtools-shared/src/devtools/views/Components/InspectedElement';
16 +import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
17 +import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
18 +import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
19 +import styles from './InspectedElementPane.css';
20 +
21 +function InspectedElementPane() {
22 + const {hideSettings} = useContext(OptionsContext);
23 + return (
24 + <SettingsModalContextController>
25 + <div className={styles.InspectedElementPane}>
26 + <InspectedElement
27 + actionButtons={!hideSettings && <SettingsModalContextToggle />}
28 + />
29 + <SettingsModal />
30 + </div>
31 + </SettingsModalContextController>
32 + );
33 +}
34 +export default (portaledContent(InspectedElementPane): component());