diff --git a/packages/react-devtools-extensions/src/main/elementSelection.js b/packages/react-devtools-extensions/src/main/elementSelection.js index 7f9b8ed208527..a315ebb8d58f3 100644 --- a/packages/react-devtools-extensions/src/main/elementSelection.js +++ b/packages/react-devtools-extensions/src/main/elementSelection.js @@ -34,7 +34,7 @@ export function setReactSelectionFromBrowser(bridge) { return; } - // Remember to sync the selection next time we show Components tab. + // Remember to sync the selection next time we show inspected element bridge.send('syncSelectionFromBuiltinElementsPanel'); } }, diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index bc948d1e6d6c9..48d6ed160e387 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -205,6 +205,7 @@ function createBridgeAndStore() { bridge, browserTheme: getBrowserTheme(), componentsPortalContainer, + inspectedElementPortalContainer, profilerPortalContainer, editorPortalContainer, currentSelectedSource, @@ -277,6 +278,46 @@ function createComponentsPanel() { ); } +function createElementsInspectPanel() { + if (inspectedElementPortalContainer) { + // Panel is created and user opened it at least once + ensureInitialHTMLIsCleared(inspectedElementPortalContainer); + render(); + + return; + } + + if (inspectedElementPane) { + // Panel is created, but wasn't opened yet, so no document is present for it + return; + } + + const elementsPanel = chrome.devtools.panels.elements; + if (!elementsPanel || !elementsPanel.createSidebarPane) { + // TODO: Does Firefox support elements panel extensions? + return; + } + + elementsPanel.createSidebarPane('React Element ⚛', createdPane => { + inspectedElementPane = createdPane; + + createdPane.setPage('panel.html'); + createdPane.setHeight('75px'); + + createdPane.onShown.addListener(portal => { + inspectedElementPortalContainer = portal.container; + if (inspectedElementPortalContainer != null && render) { + ensureInitialHTMLIsCleared(inspectedElementPortalContainer); + + render(); + portal.injectStyles(cloneStyleTags); + + logEvent({event_name: 'selected-inspected-element-pane'}); + } + }); + }); +} + function createProfilerPanel() { if (profilerPortalContainer) { // Panel is created and user opened it at least once @@ -507,6 +548,7 @@ function mountReactDevTools() { createComponentsPanel(); createProfilerPanel(); createSourcesEditorPanel(); + createElementsInspectPanel(); // Suspense Tab is created via the hook // TODO(enableSuspenseTab): Create eagerly once Suspense tab is stable } @@ -555,10 +597,12 @@ let componentsPanel = null; let profilerPanel = null; let suspensePanel = null; let editorPane = null; +let inspectedElementPane = null; let componentsPortalContainer = null; let profilerPortalContainer = null; let suspensePortalContainer = null; let editorPortalContainer = null; +let inspectedElementPortalContainer = null; let mostRecentOverrideTab = null; let render = null; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css index 156b28f2cadb6..7b00c0c0b3979 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css @@ -77,3 +77,11 @@ padding: 0.25rem; color: var(--color-console-error-icon); } + +.VRule { + height: 20px; + width: 1px; + flex: 0 0 1px; + margin: 0 0.5rem; + background-color: var(--color-border); +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js index 633a4d382eb76..7ba70678af058 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js @@ -34,13 +34,17 @@ import useEditorURL from '../useEditorURL'; import styles from './InspectedElement.css'; import Tooltip from './reach-ui/tooltip'; -export type Props = {}; +export type Props = { + actionButtons?: React.Node, +}; // TODO Make edits and deletes also use transition API! const noSourcePromise = Promise.resolve(null); -export default function InspectedElementWrapper(_: Props): React.Node { +export default function InspectedElementWrapper({ + actionButtons, +}: Props): React.Node { const {inspectedElementID} = useContext(TreeStateContext); const bridge = useContext(BridgeContext); const store = useContext(StoreContext); @@ -305,6 +309,13 @@ export default function InspectedElementWrapper(_: Props): React.Node { symbolicatedSourcePromise={symbolicatedSourcePromise} /> )} + + {actionButtons && ( + <> +
+ {actionButtons} + + )}
{inspectedElement === null && ( diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index 6ddacef563769..dc47157497820 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -26,6 +26,7 @@ import Profiler from './Profiler/Profiler'; import SuspenseTab from './SuspenseTab/SuspenseTab'; import TabBar from './TabBar'; import EditorPane from './Editor/EditorPane'; +import InspectedElementPane from './InspectedElement/InspectedElementPane'; import {SettingsContextController} from './Settings/SettingsContext'; import {TreeContextController} from './Components/TreeContext'; import ViewElementSourceContext from './Components/ViewElementSourceContext'; @@ -100,6 +101,7 @@ export type Props = { // The root app is rendered in the top-level extension window, // but individual tabs (e.g. Components, Profiling) can be rendered into portals within their browser panels. componentsPortalContainer?: Element, + inspectedElementPortalContainer?: Element, profilerPortalContainer?: Element, suspensePortalContainer?: Element, editorPortalContainer?: Element, @@ -155,6 +157,7 @@ export default function DevTools({ canViewElementSourceFunction, componentsPortalContainer, editorPortalContainer, + inspectedElementPortalContainer, profilerPortalContainer, suspensePortalContainer, currentSelectedSource, @@ -379,6 +382,14 @@ export default function DevTools({ portalContainer={editorPortalContainer} /> ) : null} + {inspectedElementPortalContainer ? ( + + ) : null} diff --git a/packages/react-devtools-shared/src/devtools/views/InspectedElement/InspectedElementPane.css b/packages/react-devtools-shared/src/devtools/views/InspectedElement/InspectedElementPane.css new file mode 100644 index 0000000000000..5d0cd2e40ae60 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/InspectedElement/InspectedElementPane.css @@ -0,0 +1,15 @@ +.InspectedElementPane, .InspectedElementPane * { + box-sizing: border-box; + -webkit-font-smoothing: var(--font-smoothing); +} + +.InspectedElementPane { + position: relative; + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + background-color: var(--color-background); + color: var(--color-text); + font-family: var(--font-family-sans); +} diff --git a/packages/react-devtools-shared/src/devtools/views/InspectedElement/InspectedElementPane.js b/packages/react-devtools-shared/src/devtools/views/InspectedElement/InspectedElementPane.js new file mode 100644 index 0000000000000..3e2fe1bd9d677 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/InspectedElement/InspectedElementPane.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; +import {useContext} from 'react'; + +import portaledContent from 'react-devtools-shared/src/devtools/views/portaledContent'; +import {OptionsContext} from 'react-devtools-shared/src/devtools/views/context'; +import InspectedElement from 'react-devtools-shared/src/devtools/views/Components/InspectedElement'; +import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal'; +import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle'; +import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext'; +import styles from './InspectedElementPane.css'; + +export type Props = {}; + +function InspectedElementPane({}: Props) { + const {hideSettings} = useContext(OptionsContext); + return ( + +
+ } + /> + +
+
+ ); +} +export default (portaledContent(InspectedElementPane): component());