From f3f4643a3af686b1e4d8711df17f6465a855925c Mon Sep 17 00:00:00 2001
From: Brian Vaughn <bvaughn@fb.com>
Date: Sun, 17 Mar 2019 13:52:37 -0700
Subject: [PATCH 1/8] Experimenting with portals

---
 shells/browser/chrome/manifest.json           |   8 +-
 shells/browser/firefox/manifest.json          |   8 +-
 shells/browser/shared/build.js                |   9 +-
 .../shared/{elements.html => panel.html}      |   2 +-
 shells/browser/shared/profiler.html           |  32 -----
 shells/browser/shared/settings.html           |  32 -----
 shells/browser/shared/src/main.js             | 127 +++++++++++-------
 shells/browser/shared/src/panel.js            |  20 +++
 shells/browser/shared/src/panels/elements.js  |   3 -
 shells/browser/shared/src/panels/profiler.js  |   3 -
 shells/browser/shared/src/panels/settings.js  |   3 -
 shells/browser/shared/src/panels/utils.js     |  72 ----------
 shells/browser/shared/src/utils.js            |  21 +++
 shells/browser/shared/webpack.config.js       |   4 +-
 src/devtools/views/DevTools.js                |  15 ++-
 15 files changed, 136 insertions(+), 223 deletions(-)
 rename shells/browser/shared/{elements.html => panel.html} (93%)
 delete mode 100644 shells/browser/shared/profiler.html
 delete mode 100644 shells/browser/shared/settings.html
 create mode 100644 shells/browser/shared/src/panel.js
 delete mode 100644 shells/browser/shared/src/panels/elements.js
 delete mode 100644 shells/browser/shared/src/panels/profiler.js
 delete mode 100644 shells/browser/shared/src/panels/settings.js
 delete mode 100644 shells/browser/shared/src/panels/utils.js

diff --git a/shells/browser/chrome/manifest.json b/shells/browser/chrome/manifest.json
index ccbf29aa3c6cd..c22e4b38490c0 100644
--- a/shells/browser/chrome/manifest.json
+++ b/shells/browser/chrome/manifest.json
@@ -27,13 +27,7 @@
   "devtools_page": "main.html",
 
   "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
-  "web_accessible_resources": [
-    "elements.html",
-    "main.html",
-    "profiler.html",
-    "settings.html",
-    "build/backend.js"
-  ],
+  "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"],
 
   "background": {
     "scripts": ["build/background.js"],
diff --git a/shells/browser/firefox/manifest.json b/shells/browser/firefox/manifest.json
index a1653d50051fc..e0015a59ff7a6 100644
--- a/shells/browser/firefox/manifest.json
+++ b/shells/browser/firefox/manifest.json
@@ -33,13 +33,7 @@
   "devtools_page": "main.html",
 
   "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
-  "web_accessible_resources": [
-    "elements.html",
-    "main.html",
-    "profiler.html",
-    "settings.html",
-    "build/backend.js"
-  ],
+  "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"],
 
   "background": {
     "scripts": ["build/background.js"],
diff --git a/shells/browser/shared/build.js b/shells/browser/shared/build.js
index 3923c1f128d23..846f73967094e 100644
--- a/shells/browser/shared/build.js
+++ b/shells/browser/shared/build.js
@@ -7,14 +7,7 @@ const { join } = require('path');
 
 // These files are copied along with Webpack-bundled files
 // to produce the final web extension
-const STATIC_FILES = [
-  'icons',
-  'popups',
-  'elements.html',
-  'main.html',
-  'profiler.html',
-  'settings.html',
-];
+const STATIC_FILES = ['icons', 'popups', 'main.html', 'panel.html'];
 
 const preProcess = async (destinationPath, tempPath) => {
   await remove(destinationPath); // Clean up from previously completed builds
diff --git a/shells/browser/shared/elements.html b/shells/browser/shared/panel.html
similarity index 93%
rename from shells/browser/shared/elements.html
rename to shells/browser/shared/panel.html
index bbaa81a0f2f3a..60fd1bdf13ff2 100644
--- a/shells/browser/shared/elements.html
+++ b/shells/browser/shared/panel.html
@@ -27,6 +27,6 @@
     <body>
         <!-- main react mount point -->
         <div id="container">Unable to find React on the page.</div>
-        <script src="./build/elements.js"></script>
+        <script src="./build/panel.js"></script>
     </body>
 </html>
diff --git a/shells/browser/shared/profiler.html b/shells/browser/shared/profiler.html
deleted file mode 100644
index 0281d9d2cc22b..0000000000000
--- a/shells/browser/shared/profiler.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<!doctype html>
-<html style="display: flex">
-    <head>
-        <meta charset="utf8">
-        <style>
-            html {
-                display: flex;
-            }
-            body {
-                margin: 0;
-                padding: 0;
-                flex: 1;
-                display: flex;
-            }
-            #container {
-                display: flex;
-                flex: 1;
-                width: 100%;
-                position: fixed;
-                top: 0;
-                left: 0;
-                right: 0;
-                bottom: 0;
-            }
-        </style>
-    </head>
-    <body>
-        <!-- main react mount point -->
-        <div id="container">Unable to find React on the page.</div>
-        <script src="./build/profiler.js"></script>
-    </body>
-</html>
diff --git a/shells/browser/shared/settings.html b/shells/browser/shared/settings.html
deleted file mode 100644
index 16beaccceaca2..0000000000000
--- a/shells/browser/shared/settings.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<!doctype html>
-<html style="display: flex">
-    <head>
-        <meta charset="utf8">
-        <style>
-            html {
-                display: flex;
-            }
-            body {
-                margin: 0;
-                padding: 0;
-                flex: 1;
-                display: flex;
-            }
-            #container {
-                display: flex;
-                flex: 1;
-                width: 100%;
-                position: fixed;
-                top: 0;
-                left: 0;
-                right: 0;
-                bottom: 0;
-            }
-        </style>
-    </head>
-    <body>
-        <!-- main react mount point -->
-        <div id="container">Unable to find React on the page.</div>
-        <script src="./build/settings.js"></script>
-    </body>
-</html>
diff --git a/shells/browser/shared/src/main.js b/shells/browser/shared/src/main.js
index 9183a0c4ce6da..d09dddf1bd98d 100644
--- a/shells/browser/shared/src/main.js
+++ b/shells/browser/shared/src/main.js
@@ -1,8 +1,16 @@
 /* global chrome */
 
+import { createElement } from 'react';
+import { unstable_createRoot as createRoot } from 'react-dom';
 import Bridge from 'src/bridge';
 import Store from 'src/devtools/Store';
 import inject from './inject';
+import {
+  createViewElementSource,
+  getBrowserName,
+  getBrowserTheme,
+} from './utils';
+import DevTools from 'src/devtools/views/DevTools';
 
 let panelCreated = false;
 
@@ -22,6 +30,7 @@ function createPanelIfReactLoaded() {
 
       clearInterval(loadCheckInterval);
 
+      let renderRootToPortal = null;
       let bridge = null;
       let store = null;
       let elementsPanel = null;
@@ -54,69 +63,85 @@ function createPanelIfReactLoaded() {
         // Otherwise the Store may miss important initial tree op codes.
         inject(chrome.runtime.getURL('build/backend.js'));
 
+        const viewElementSource = createViewElementSource(bridge, store);
+
+        const container = document.createElement('div');
+        const root = createRoot(container);
+
+        renderRootToPortal = ({ overrideTab, portalContainer }) => {
+          root.render(
+            createElement(DevTools, {
+              bridge,
+              browserName: getBrowserName(),
+              browserTheme: getBrowserTheme(),
+              overrideTab,
+              portalContainer,
+              showTabBar: false,
+              store,
+              viewElementSource,
+            })
+          );
+
+          const oldLinkTags = document.getElementsByTagName('link');
+          const newLinkTags = [];
+          for (let oldLinkTag of oldLinkTags) {
+            if (oldLinkTag.rel === 'stylesheet') {
+              const newLinkTag = document.createElement('link');
+              for (let attribute of oldLinkTag.attributes) {
+                newLinkTag.setAttribute(
+                  attribute.nodeName,
+                  attribute.nodeValue
+                );
+              }
+              newLinkTags.push(newLinkTag);
+            }
+          }
+
+          return newLinkTags;
+        };
+
         if (elementsPanel !== null) {
-          elementsPanel.injectBridgeAndStore(bridge, store);
-        }
-        if (profilerPanel !== null) {
-          profilerPanel.injectBridgeAndStore(bridge, store);
-        }
-        if (settingsPanel !== null) {
-          settingsPanel.injectBridgeAndStore(bridge, store);
+          elementsPanel.render(renderRootToPortal, 'elements');
         }
       }
 
       initBridgeAndStore();
 
-      chrome.devtools.panels.create(
-        '⚛ Elements',
-        '',
-        'elements.html',
-        panel => {
-          panel.onShown.addListener(panel => {
-            if (elementsPanel === null) {
-              panel.injectBridgeAndStore(bridge, store);
-            }
+      chrome.devtools.panels.create('⚛ Elements', '', 'panel.html', panel => {
+        panel.onShown.addListener(panel => {
+          elementsPanel = panel;
 
-            elementsPanel = panel;
+          if (renderRootToPortal !== null) {
+            elementsPanel.render(renderRootToPortal, 'elements');
+          }
 
-            // TODO: When the user switches to the panel, check for an Elements tab selection.
-          });
-          panel.onHidden.addListener(() => {
-            // TODO: Stop highlighting and stuff.
-          });
-        }
-      );
+          // TODO: When the user switches to the panel, check for an Elements tab selection.
+        });
+        panel.onHidden.addListener(() => {
+          // TODO: Stop highlighting and stuff.
+        });
+      });
 
       // TODO (profiling) Is there a way to detect profiling support and conditionally register this panel?
-      chrome.devtools.panels.create(
-        '⚛ Profiler',
-        '',
-        'profiler.html',
-        panel => {
-          panel.onShown.addListener(panel => {
-            if (settingsPanel === null) {
-              panel.injectBridgeAndStore(bridge, store);
-            }
+      chrome.devtools.panels.create('⚛ Profiler', '', 'panel.html', panel => {
+        panel.onShown.addListener(panel => {
+          profilerPanel = panel;
 
-            profilerPanel = panel;
-          });
-        }
-      );
-
-      chrome.devtools.panels.create(
-        '⚛ Settings',
-        '',
-        'settings.html',
-        panel => {
-          panel.onShown.addListener(panel => {
-            if (settingsPanel === null) {
-              panel.injectBridgeAndStore(bridge, store);
-            }
+          if (renderRootToPortal !== null) {
+            profilerPanel.render(renderRootToPortal, 'profiler');
+          }
+        });
+      });
 
-            settingsPanel = panel;
-          });
-        }
-      );
+      chrome.devtools.panels.create('⚛ Settings', '', 'panel.html', panel => {
+        panel.onShown.addListener(panel => {
+          settingsPanel = panel;
+
+          if (renderRootToPortal !== null) {
+            settingsPanel.render(renderRootToPortal, 'settings');
+          }
+        });
+      });
 
       chrome.devtools.network.onNavigated.removeListener(checkPageForReact);
 
diff --git a/shells/browser/shared/src/panel.js b/shells/browser/shared/src/panel.js
new file mode 100644
index 0000000000000..b7413918f5c2d
--- /dev/null
+++ b/shells/browser/shared/src/panel.js
@@ -0,0 +1,20 @@
+const container = document.getElementById('container');
+
+let hasInjectedStyles = false;
+
+window.render = (renderRootToPortal, tab) => {
+  container.innerHTML = '';
+
+  const linkTags = renderRootToPortal({
+    overrideTab: tab,
+    portalContainer: container,
+  });
+
+  if (!hasInjectedStyles) {
+    hasInjectedStyles = true;
+
+    for (let linkTag of linkTags) {
+      document.head.appendChild(linkTag);
+    }
+  }
+};
diff --git a/shells/browser/shared/src/panels/elements.js b/shells/browser/shared/src/panels/elements.js
deleted file mode 100644
index ef7f5e80df618..0000000000000
--- a/shells/browser/shared/src/panels/elements.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { createPanel } from './utils';
-
-createPanel('elements');
diff --git a/shells/browser/shared/src/panels/profiler.js b/shells/browser/shared/src/panels/profiler.js
deleted file mode 100644
index 23f028455b7e1..0000000000000
--- a/shells/browser/shared/src/panels/profiler.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { createPanel } from './utils';
-
-createPanel('profiler');
diff --git a/shells/browser/shared/src/panels/settings.js b/shells/browser/shared/src/panels/settings.js
deleted file mode 100644
index a8dd78b0504ce..0000000000000
--- a/shells/browser/shared/src/panels/settings.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { createPanel } from './utils';
-
-createPanel('settings');
diff --git a/shells/browser/shared/src/panels/utils.js b/shells/browser/shared/src/panels/utils.js
deleted file mode 100644
index 1f807fc922f19..0000000000000
--- a/shells/browser/shared/src/panels/utils.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/* global chrome */
-
-import { createElement } from 'react';
-import { unstable_createRoot as createRoot, flushSync } from 'react-dom';
-import DevTools from 'src/devtools/views/DevTools';
-import { getBrowserName, getBrowserTheme } from '../utils';
-
-export function createPanel(defaultTab) {
-  let injectedBridge = null;
-  let injectedStore = null;
-  let root = null;
-
-  // All DevTools panel share a single Bridge and Store instance.
-  // The main script will inject those shared instances using this method.
-  window.injectBridgeAndStore = (bridge, store) => {
-    injectedBridge = bridge;
-    injectedStore = store;
-
-    if (root === null) {
-      injectAndInit();
-    } else {
-      // It's easiest to recreate the DevTools panel (to clean up potential stale state).
-      // We can revisit this in the future as a small optimization.
-      flushSync(() => root.unmount(injectAndInit));
-    }
-  };
-
-  function viewElementSource(id) {
-    if (injectedBridge == null || injectedStore == null) {
-      return;
-    }
-
-    const rendererID = injectedStore.getRendererIDForElement(id);
-    if (rendererID != null) {
-      // Ask the renderer interface to determine the component function,
-      // and store it as a global variable on the window
-      injectedBridge.send('viewElementSource', { id, rendererID });
-
-      setTimeout(() => {
-        // Ask Chrome to display the location of the component function,
-        // assuming the renderer found one.
-        chrome.devtools.inspectedWindow.eval(`
-          if (window.$type != null) {
-            inspect(window.$type);
-          }
-        `);
-      }, 100);
-    }
-  }
-
-  function injectAndInit() {
-    const container = ((document.getElementById(
-      'container'
-    ): any): HTMLElement);
-
-    // Clear the "React not found" initial message before rendering.
-    container.innerHTML = '';
-
-    root = createRoot(container);
-    root.render(
-      createElement(DevTools, {
-        bridge: injectedBridge,
-        browserName: getBrowserName(),
-        browserTheme: getBrowserTheme(),
-        defaultTab,
-        showTabBar: false,
-        store: injectedStore,
-        viewElementSource,
-      })
-    );
-  }
-}
diff --git a/shells/browser/shared/src/utils.js b/shells/browser/shared/src/utils.js
index 7f4c2eaa068b2..0446ff9daf9a7 100644
--- a/shells/browser/shared/src/utils.js
+++ b/shells/browser/shared/src/utils.js
@@ -2,6 +2,27 @@
 
 const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0;
 
+export function createViewElementSource(bridge: Bridge, store: Store) {
+  return function viewElementSource(id) {
+    const rendererID = store.getRendererIDForElement(id);
+    if (rendererID != null) {
+      // Ask the renderer interface to determine the component function,
+      // and store it as a global variable on the window
+      bridge.send('viewElementSource', { id, rendererID });
+
+      setTimeout(() => {
+        // Ask Chrome to display the location of the component function,
+        // assuming the renderer found one.
+        chrome.devtools.inspectedWindow.eval(`
+          if (window.$type != null) {
+            inspect(window.$type);
+          }
+        `);
+      }, 100);
+    }
+  };
+}
+
 export function getBrowserName() {
   return IS_CHROME ? 'Chrome' : 'Firefox';
 }
diff --git a/shells/browser/shared/webpack.config.js b/shells/browser/shared/webpack.config.js
index e95c5cf418eee..44e3d5b5c28f4 100644
--- a/shells/browser/shared/webpack.config.js
+++ b/shells/browser/shared/webpack.config.js
@@ -16,9 +16,7 @@ module.exports = {
     contentScript: './src/contentScript.js',
     inject: './src/GlobalHook.js',
     main: './src/main.js',
-    elements: './src/panels/elements.js',
-    profiler: './src/panels/profiler.js',
-    settings: './src/panels/settings.js',
+    panel: './src/panel.js',
   },
   output: {
     path: __dirname + '/build',
diff --git a/src/devtools/views/DevTools.js b/src/devtools/views/DevTools.js
index 3473582f04cba..2db0e2dcfb3ac 100644
--- a/src/devtools/views/DevTools.js
+++ b/src/devtools/views/DevTools.js
@@ -1,6 +1,7 @@
 // @flow
 
 import React, { useEffect, useState } from 'react';
+import { createPortal } from 'react-dom';
 import Store from '../store';
 import { BridgeContext, StoreContext } from './context';
 import Elements from './Elements/Elements';
@@ -27,6 +28,8 @@ export type Props = {|
   browserName: BrowserName,
   defaultTab?: TabID,
   browserTheme: BrowserTheme,
+  overrideTab?: TabID,
+  portalContainer?: Element,
   showTabBar?: boolean,
   store: Store,
   viewElementSource?: ?Function,
@@ -59,11 +62,17 @@ export default function DevTools({
   browserName,
   defaultTab = 'elements',
   browserTheme = 'light',
+  overrideTab,
+  portalContainer,
   showTabBar = false,
   store,
   viewElementSource = null,
 }: Props) {
   const [tab, setTab] = useState(defaultTab);
+  if (overrideTab != null && overrideTab !== tab) {
+    setTab(overrideTab);
+  }
+
   const [supportsProfiling, setSupportsProfiling] = useState(
     store.supportsProfiling
   );
@@ -100,7 +109,7 @@ export default function DevTools({
       break;
   }
 
-  return (
+  const children = (
     <BridgeContext.Provider value={bridge}>
       <StoreContext.Provider value={store}>
         <SettingsContextController browserTheme={browserTheme}>
@@ -135,4 +144,8 @@ export default function DevTools({
       </StoreContext.Provider>
     </BridgeContext.Provider>
   );
+
+  return portalContainer != null
+    ? createPortal(children, portalContainer)
+    : children;
 }

From 11573bf8d9d6346121492d97d2db8a443f4a22a3 Mon Sep 17 00:00:00 2001
From: Brian Vaughn <bvaughn@fb.com>
Date: Mon, 18 Mar 2019 09:11:28 -0700
Subject: [PATCH 2/8] Refactored portaling and fixed disconnected CSS vars

---
 shells/browser/shared/src/main.js             |  73 ++++---
 shells/browser/shared/src/panel.js            |  13 +-
 src/devtools/views/DevTools.js                |  32 +--
 src/devtools/views/Elements/Elements.js       |  13 +-
 src/devtools/views/Profiler/Profiler.js       |  16 +-
 src/devtools/views/Settings/Settings.js       |  13 +-
 .../views/Settings/SettingsContext.js         | 190 ++++++++++++------
 7 files changed, 222 insertions(+), 128 deletions(-)

diff --git a/shells/browser/shared/src/main.js b/shells/browser/shared/src/main.js
index d09dddf1bd98d..63859497b1d09 100644
--- a/shells/browser/shared/src/main.js
+++ b/shells/browser/shared/src/main.js
@@ -30,12 +30,13 @@ function createPanelIfReactLoaded() {
 
       clearInterval(loadCheckInterval);
 
-      let renderRootToPortal = null;
       let bridge = null;
       let store = null;
-      let elementsPanel = null;
-      let profilerPanel = null;
-      let settingsPanel = null;
+      let elementsPortalContainer = null;
+      let profilerPortalContainer = null;
+      let settingsPortalContainer = null;
+      let cloneStyleTags = null;
+      let render = null;
 
       function initBridgeAndStore() {
         let hasPortBeenDisconnected = false;
@@ -68,51 +69,47 @@ function createPanelIfReactLoaded() {
         const container = document.createElement('div');
         const root = createRoot(container);
 
-        renderRootToPortal = ({ overrideTab, portalContainer }) => {
+        render = overrideTab => {
           root.render(
             createElement(DevTools, {
               bridge,
               browserName: getBrowserName(),
               browserTheme: getBrowserTheme(),
+              elementsPortalContainer,
               overrideTab,
-              portalContainer,
+              profilerPortalContainer,
+              settingsPortalContainer,
               showTabBar: false,
               store,
               viewElementSource,
             })
           );
+        };
+      }
 
-          const oldLinkTags = document.getElementsByTagName('link');
-          const newLinkTags = [];
-          for (let oldLinkTag of oldLinkTags) {
-            if (oldLinkTag.rel === 'stylesheet') {
-              const newLinkTag = document.createElement('link');
-              for (let attribute of oldLinkTag.attributes) {
-                newLinkTag.setAttribute(
-                  attribute.nodeName,
-                  attribute.nodeValue
-                );
-              }
-              newLinkTags.push(newLinkTag);
+      cloneStyleTags = () => {
+        const linkTags = [];
+        for (let linkTag of document.getElementsByTagName('link')) {
+          if (linkTag.rel === 'stylesheet') {
+            const newLinkTag = document.createElement('link');
+            for (let attribute of linkTag.attributes) {
+              newLinkTag.setAttribute(attribute.nodeName, attribute.nodeValue);
             }
+            linkTags.push(newLinkTag);
           }
-
-          return newLinkTags;
-        };
-
-        if (elementsPanel !== null) {
-          elementsPanel.render(renderRootToPortal, 'elements');
         }
-      }
+        return linkTags;
+      };
 
       initBridgeAndStore();
 
       chrome.devtools.panels.create('⚛ Elements', '', 'panel.html', panel => {
         panel.onShown.addListener(panel => {
-          elementsPanel = panel;
-
-          if (renderRootToPortal !== null) {
-            elementsPanel.render(renderRootToPortal, 'elements');
+          elementsPortalContainer = panel.container;
+          if (elementsPortalContainer != null) {
+            elementsPortalContainer.innerHTML = '';
+            render('elements');
+            panel.injectStyles(cloneStyleTags);
           }
 
           // TODO: When the user switches to the panel, check for an Elements tab selection.
@@ -125,20 +122,22 @@ function createPanelIfReactLoaded() {
       // TODO (profiling) Is there a way to detect profiling support and conditionally register this panel?
       chrome.devtools.panels.create('⚛ Profiler', '', 'panel.html', panel => {
         panel.onShown.addListener(panel => {
-          profilerPanel = panel;
-
-          if (renderRootToPortal !== null) {
-            profilerPanel.render(renderRootToPortal, 'profiler');
+          profilerPortalContainer = panel.container;
+          if (profilerPortalContainer != null) {
+            profilerPortalContainer.innerHTML = '';
+            render('profiler');
+            panel.injectStyles(cloneStyleTags);
           }
         });
       });
 
       chrome.devtools.panels.create('⚛ Settings', '', 'panel.html', panel => {
         panel.onShown.addListener(panel => {
-          settingsPanel = panel;
-
-          if (renderRootToPortal !== null) {
-            settingsPanel.render(renderRootToPortal, 'settings');
+          settingsPortalContainer = panel.container;
+          if (settingsPortalContainer != null) {
+            settingsPortalContainer.innerHTML = '';
+            render('settings');
+            panel.injectStyles(cloneStyleTags);
           }
         });
       });
diff --git a/shells/browser/shared/src/panel.js b/shells/browser/shared/src/panel.js
index b7413918f5c2d..1ce574dfcf420 100644
--- a/shells/browser/shared/src/panel.js
+++ b/shells/browser/shared/src/panel.js
@@ -1,18 +1,13 @@
-const container = document.getElementById('container');
+window.container = document.getElementById('container');
 
 let hasInjectedStyles = false;
 
-window.render = (renderRootToPortal, tab) => {
-  container.innerHTML = '';
-
-  const linkTags = renderRootToPortal({
-    overrideTab: tab,
-    portalContainer: container,
-  });
-
+window.injectStyles = getLinkTags => {
   if (!hasInjectedStyles) {
     hasInjectedStyles = true;
 
+    const linkTags = getLinkTags();
+
     for (let linkTag of linkTags) {
       document.head.appendChild(linkTag);
     }
diff --git a/src/devtools/views/DevTools.js b/src/devtools/views/DevTools.js
index 2db0e2dcfb3ac..d80a0ecbe1197 100644
--- a/src/devtools/views/DevTools.js
+++ b/src/devtools/views/DevTools.js
@@ -1,7 +1,6 @@
 // @flow
 
 import React, { useEffect, useState } from 'react';
-import { createPortal } from 'react-dom';
 import Store from '../store';
 import { BridgeContext, StoreContext } from './context';
 import Elements from './Elements/Elements';
@@ -26,10 +25,12 @@ export type TabID = 'elements' | 'profiler' | 'settings';
 export type Props = {|
   bridge: Bridge,
   browserName: BrowserName,
-  defaultTab?: TabID,
   browserTheme: BrowserTheme,
+  defaultTab?: TabID,
+  elementsPortalContainer?: Element,
   overrideTab?: TabID,
-  portalContainer?: Element,
+  profilerPortalContainer?: Element,
+  settingsPortalContainer?: Element,
   showTabBar?: boolean,
   store: Store,
   viewElementSource?: ?Function,
@@ -60,10 +61,12 @@ const tabsWithoutProfiler = [elementTab, settingsTab];
 export default function DevTools({
   bridge,
   browserName,
-  defaultTab = 'elements',
   browserTheme = 'light',
+  defaultTab = 'elements',
+  elementsPortalContainer,
   overrideTab,
-  portalContainer,
+  profilerPortalContainer,
+  settingsPortalContainer,
   showTabBar = false,
   store,
   viewElementSource = null,
@@ -98,21 +101,26 @@ export default function DevTools({
   let tabElement;
   switch (tab) {
     case 'profiler':
-      tabElement = <Profiler />;
+      tabElement = <Profiler portalContainer={profilerPortalContainer} />;
       break;
     case 'settings':
-      tabElement = <Settings />;
+      tabElement = <Settings portalContainer={settingsPortalContainer} />;
       break;
     case 'elements':
     default:
-      tabElement = <Elements />;
+      tabElement = <Elements portalContainer={elementsPortalContainer} />;
       break;
   }
 
-  const children = (
+  return (
     <BridgeContext.Provider value={bridge}>
       <StoreContext.Provider value={store}>
-        <SettingsContextController browserTheme={browserTheme}>
+        <SettingsContextController
+          browserTheme={browserTheme}
+          elementsPortalContainer={elementsPortalContainer}
+          profilerPortalContainer={profilerPortalContainer}
+          settingsPortalContainer={settingsPortalContainer}
+        >
           <TreeContextController viewElementSource={viewElementSource}>
             <ProfilerContextController>
               <div className={styles.DevTools}>
@@ -144,8 +152,4 @@ export default function DevTools({
       </StoreContext.Provider>
     </BridgeContext.Provider>
   );
-
-  return portalContainer != null
-    ? createPortal(children, portalContainer)
-    : children;
 }
diff --git a/src/devtools/views/Elements/Elements.js b/src/devtools/views/Elements/Elements.js
index cb401ddfdba99..596a5c754a4a2 100644
--- a/src/devtools/views/Elements/Elements.js
+++ b/src/devtools/views/Elements/Elements.js
@@ -1,15 +1,18 @@
 // @flow
 
 import React from 'react';
+import { createPortal } from 'react-dom';
 import Tree from './Tree';
 import SelectedElement from './SelectedElement';
 import styles from './Elements.css';
 
-export type Props = {||};
+export type Props = {|
+  portalContainer?: Element,
+|};
 
-export default function Elements(_: Props) {
+export default function Elements({ portalContainer }: Props) {
   // TODO Flex wrappers below should be user resizable.
-  return (
+  const children = (
     <div className={styles.Elements}>
       <div className={styles.TreeWrapper}>
         <Tree />
@@ -19,4 +22,8 @@ export default function Elements(_: Props) {
       </div>
     </div>
   );
+
+  return portalContainer != null
+    ? createPortal(children, portalContainer)
+    : children;
 }
diff --git a/src/devtools/views/Profiler/Profiler.js b/src/devtools/views/Profiler/Profiler.js
index add792692297f..e5c2db601a9e0 100644
--- a/src/devtools/views/Profiler/Profiler.js
+++ b/src/devtools/views/Profiler/Profiler.js
@@ -1,6 +1,7 @@
 // @flow
 
 import React, { Suspense, useCallback, useContext, useState } from 'react';
+import { createPortal } from 'react-dom';
 import { ProfilerContext } from './ProfilerContext';
 import Button from '../Button';
 import ButtonIcon from '../ButtonIcon';
@@ -13,21 +14,30 @@ import SnapshotSelector from './SnapshotSelector';
 
 import styles from './Profiler.css';
 
-export default function Profiler(_: {||}) {
+export type Props = {|
+  portalContainer?: Element,
+|};
+
+export default function Profiler({ portalContainer }: Props) {
   const { hasProfilingData, isProfiling, rootHasProfilingData } = useContext(
     ProfilerContext
   );
 
+  let children = null;
   if (isProfiling || !rootHasProfilingData) {
-    return (
+    children = (
       <NonSuspendingProfiler
         hasProfilingData={hasProfilingData}
         isProfiling={isProfiling}
       />
     );
   } else {
-    return <SuspendingProfiler />;
+    children = <SuspendingProfiler />;
   }
+
+  return portalContainer != null
+    ? createPortal(children, portalContainer)
+    : children;
 }
 
 // This view is rendered when there is no profiler data (either we haven't profiled yet or we're currently profiling).
diff --git a/src/devtools/views/Settings/Settings.js b/src/devtools/views/Settings/Settings.js
index 37e4dd9198bfa..d3a99360a0b53 100644
--- a/src/devtools/views/Settings/Settings.js
+++ b/src/devtools/views/Settings/Settings.js
@@ -1,13 +1,16 @@
 // @flow
 
 import React, { useCallback, useContext } from 'react';
+import { createPortal } from 'react-dom';
 import { SettingsContext } from './SettingsContext';
 
 import styles from './Settings.css';
 
-export type Props = {||};
+export type Props = {|
+  portalContainer?: Element,
+|};
 
-export default function Settings(_: Props) {
+export default function Settings({ portalContainer }: Props) {
   const { displayDensity, setDisplayDensity, theme, setTheme } = useContext(
     SettingsContext
   );
@@ -26,7 +29,7 @@ export default function Settings(_: Props) {
     [setTheme]
   );
 
-  return (
+  const children = (
     <div className={styles.Settings}>
       <div className={styles.Section}>
         <div className={styles.Header}>Theme</div>
@@ -90,4 +93,8 @@ export default function Settings(_: Props) {
       </div>
     </div>
   );
+
+  return portalContainer != null
+    ? createPortal(children, portalContainer)
+    : children;
 }
diff --git a/src/devtools/views/Settings/SettingsContext.js b/src/devtools/views/Settings/SettingsContext.js
index 0c5458cbade74..f04af61f32128 100644
--- a/src/devtools/views/Settings/SettingsContext.js
+++ b/src/devtools/views/Settings/SettingsContext.js
@@ -23,18 +23,58 @@ type Context = {|
 const SettingsContext = createContext<Context>(((null: any): Context));
 SettingsContext.displayName = 'SettingsContext';
 
+type DocumentElements = Array<HTMLElement>;
+
 type Props = {|
   browserTheme: BrowserTheme,
   children: React$Node,
+  elementsPortalContainer?: Element,
+  profilerPortalContainer?: Element,
+  settingsPortalContainer?: Element,
 |};
 
-function SettingsContextController({ browserTheme, children }: Props) {
+function SettingsContextController({
+  browserTheme,
+  children,
+  elementsPortalContainer,
+  profilerPortalContainer,
+  settingsPortalContainer,
+}: Props) {
   const [displayDensity, setDisplayDensity] = useLocalStorage<DisplayDensity>(
     'displayDensity',
     'compact'
   );
   const [theme, setTheme] = useLocalStorage<Theme>('theme', 'auto');
 
+  const documentElements = useMemo<DocumentElements>(() => {
+    const array: Array<HTMLElement> = [
+      ((document.documentElement: any): HTMLElement),
+    ];
+    if (elementsPortalContainer != null) {
+      array.push(
+        ((elementsPortalContainer.ownerDocument
+          .documentElement: any): HTMLElement)
+      );
+    }
+    if (profilerPortalContainer != null) {
+      array.push(
+        ((profilerPortalContainer.ownerDocument
+          .documentElement: any): HTMLElement)
+      );
+    }
+    if (settingsPortalContainer != null) {
+      array.push(
+        ((settingsPortalContainer.ownerDocument
+          .documentElement: any): HTMLElement)
+      );
+    }
+    return array;
+  }, [
+    elementsPortalContainer,
+    profilerPortalContainer,
+    settingsPortalContainer,
+  ]);
+
   const comfortableLineHeight = parseInt(
     getComputedStyle((document.body: any)).getPropertyValue(
       '--comfortable-line-height-data'
@@ -51,31 +91,31 @@ function SettingsContextController({ browserTheme, children }: Props) {
   useLayoutEffect(() => {
     switch (displayDensity) {
       case 'compact':
-        updateDisplayDensity('compact');
+        updateDisplayDensity('compact', documentElements);
         break;
       case 'comfortable':
-        updateDisplayDensity('comfortable');
+        updateDisplayDensity('comfortable', documentElements);
         break;
       default:
         throw Error(`Unsupported displayDensity value "${displayDensity}"`);
     }
-  }, [displayDensity]);
+  }, [displayDensity, documentElements]);
 
   useLayoutEffect(() => {
     switch (theme) {
       case 'light':
-        updateThemeVariables('light');
+        updateThemeVariables('light', documentElements);
         break;
       case 'dark':
-        updateThemeVariables('dark');
+        updateThemeVariables('dark', documentElements);
         break;
       case 'auto':
-        updateThemeVariables(browserTheme);
+        updateThemeVariables(browserTheme, documentElements);
         break;
       default:
         throw Error(`Unsupported theme value "${theme}"`);
     }
-  }, [browserTheme, theme]);
+  }, [browserTheme, theme, documentElements]);
 
   const value = useMemo(
     () => ({
@@ -105,63 +145,95 @@ function SettingsContextController({ browserTheme, children }: Props) {
   );
 }
 
-function setStyleVariable(name: string, value: string) {
-  (document.documentElement: any).style.setProperty(name, value);
+function setStyleVariable(
+  name: string,
+  value: string,
+  documentElements: DocumentElements
+) {
+  documentElements.forEach(documentElement =>
+    documentElement.style.setProperty(name, value)
+  );
 }
 
-function updateStyleHelper(themeKey: string, style: string) {
-  setStyleVariable(`--${style}`, `var(--${themeKey}-${style})`);
+function updateStyleHelper(
+  themeKey: string,
+  style: string,
+  documentElements: DocumentElements
+) {
+  setStyleVariable(
+    `--${style}`,
+    `var(--${themeKey}-${style})`,
+    documentElements
+  );
 }
 
-function updateDisplayDensity(displayDensity: DisplayDensity): void {
-  updateStyleHelper(displayDensity, 'font-size-monospace-normal');
-  updateStyleHelper(displayDensity, 'font-size-monospace-large');
-  updateStyleHelper(displayDensity, 'font-size-sans-normal');
-  updateStyleHelper(displayDensity, 'font-size-sans-large');
-  updateStyleHelper(displayDensity, 'line-height-data');
+function updateDisplayDensity(
+  displayDensity: DisplayDensity,
+  documentElements: DocumentElements
+): void {
+  updateStyleHelper(
+    displayDensity,
+    'font-size-monospace-normal',
+    documentElements
+  );
+  updateStyleHelper(
+    displayDensity,
+    'font-size-monospace-large',
+    documentElements
+  );
+  updateStyleHelper(displayDensity, 'font-size-sans-normal', documentElements);
+  updateStyleHelper(displayDensity, 'font-size-sans-large', documentElements);
+  updateStyleHelper(displayDensity, 'line-height-data', documentElements);
 }
 
-function updateThemeVariables(theme: Theme): void {
-  updateStyleHelper(theme, 'color-attribute-name');
-  updateStyleHelper(theme, 'color-attribute-value');
-  updateStyleHelper(theme, 'color-attribute-editable-value');
-  updateStyleHelper(theme, 'color-background');
-  updateStyleHelper(theme, 'color-border');
-  updateStyleHelper(theme, 'color-button-background');
-  updateStyleHelper(theme, 'color-button-background-focus');
-  updateStyleHelper(theme, 'color-button-background-hover');
-  updateStyleHelper(theme, 'color-button');
-  updateStyleHelper(theme, 'color-button-disabled');
-  updateStyleHelper(theme, 'color-button-focus');
-  updateStyleHelper(theme, 'color-button-hover');
-  updateStyleHelper(theme, 'color-commit-did-not-render');
-  updateStyleHelper(theme, 'color-commit-gradient-0');
-  updateStyleHelper(theme, 'color-commit-gradient-1');
-  updateStyleHelper(theme, 'color-commit-gradient-2');
-  updateStyleHelper(theme, 'color-commit-gradient-3');
-  updateStyleHelper(theme, 'color-commit-gradient-4');
-  updateStyleHelper(theme, 'color-commit-gradient-5');
-  updateStyleHelper(theme, 'color-commit-gradient-6');
-  updateStyleHelper(theme, 'color-commit-gradient-7');
-  updateStyleHelper(theme, 'color-commit-gradient-8');
-  updateStyleHelper(theme, 'color-commit-gradient-9');
-  updateStyleHelper(theme, 'color-commit-gradient-text');
-  updateStyleHelper(theme, 'color-component-name');
-  updateStyleHelper(theme, 'color-component-name-inverted');
-  updateStyleHelper(theme, 'color-dim');
-  updateStyleHelper(theme, 'color-dimmer');
-  updateStyleHelper(theme, 'color-dimmest');
-  updateStyleHelper(theme, 'color-jsx-arrow-brackets');
-  updateStyleHelper(theme, 'color-jsx-arrow-brackets-inverted');
-  updateStyleHelper(theme, 'color-modal-background');
-  updateStyleHelper(theme, 'color-record-active');
-  updateStyleHelper(theme, 'color-record-hover');
-  updateStyleHelper(theme, 'color-record-inactive');
-  updateStyleHelper(theme, 'color-tree-node-selected');
-  updateStyleHelper(theme, 'color-tree-node-hover');
-  updateStyleHelper(theme, 'color-search-match');
-  updateStyleHelper(theme, 'color-search-match-current');
-  updateStyleHelper(theme, 'color-text-color');
+function updateThemeVariables(
+  theme: Theme,
+  documentElements: DocumentElements
+): void {
+  updateStyleHelper(theme, 'color-attribute-name', documentElements);
+  updateStyleHelper(theme, 'color-attribute-value', documentElements);
+  updateStyleHelper(theme, 'color-attribute-editable-value', documentElements);
+  updateStyleHelper(theme, 'color-background', documentElements);
+  updateStyleHelper(theme, 'color-border', documentElements);
+  updateStyleHelper(theme, 'color-button-background', documentElements);
+  updateStyleHelper(theme, 'color-button-background-focus', documentElements);
+  updateStyleHelper(theme, 'color-button-background-hover', documentElements);
+  updateStyleHelper(theme, 'color-button', documentElements);
+  updateStyleHelper(theme, 'color-button-disabled', documentElements);
+  updateStyleHelper(theme, 'color-button-focus', documentElements);
+  updateStyleHelper(theme, 'color-button-hover', documentElements);
+  updateStyleHelper(theme, 'color-commit-did-not-render', documentElements);
+  updateStyleHelper(theme, 'color-commit-gradient-0', documentElements);
+  updateStyleHelper(theme, 'color-commit-gradient-1', documentElements);
+  updateStyleHelper(theme, 'color-commit-gradient-2', documentElements);
+  updateStyleHelper(theme, 'color-commit-gradient-3', documentElements);
+  updateStyleHelper(theme, 'color-commit-gradient-4', documentElements);
+  updateStyleHelper(theme, 'color-commit-gradient-5', documentElements);
+  updateStyleHelper(theme, 'color-commit-gradient-6', documentElements);
+  updateStyleHelper(theme, 'color-commit-gradient-7', documentElements);
+  updateStyleHelper(theme, 'color-commit-gradient-8', documentElements);
+  updateStyleHelper(theme, 'color-commit-gradient-9', documentElements);
+  updateStyleHelper(theme, 'color-commit-gradient-text', documentElements);
+  updateStyleHelper(theme, 'color-component-name', documentElements);
+  updateStyleHelper(theme, 'color-component-name-inverted', documentElements);
+  updateStyleHelper(theme, 'color-dim', documentElements);
+  updateStyleHelper(theme, 'color-dimmer', documentElements);
+  updateStyleHelper(theme, 'color-dimmest', documentElements);
+  updateStyleHelper(theme, 'color-jsx-arrow-brackets', documentElements);
+  updateStyleHelper(
+    theme,
+    'color-jsx-arrow-brackets-inverted',
+    documentElements
+  );
+  updateStyleHelper(theme, 'color-modal-background', documentElements);
+  updateStyleHelper(theme, 'color-record-active', documentElements);
+  updateStyleHelper(theme, 'color-record-hover', documentElements);
+  updateStyleHelper(theme, 'color-record-inactive', documentElements);
+  updateStyleHelper(theme, 'color-tree-node-selected', documentElements);
+  updateStyleHelper(theme, 'color-tree-node-hover', documentElements);
+  updateStyleHelper(theme, 'color-search-match', documentElements);
+  updateStyleHelper(theme, 'color-search-match-current', documentElements);
+  updateStyleHelper(theme, 'color-text-color', documentElements);
 }
 
 export { SettingsContext, SettingsContextController };

From 2664036dbe42a137ecf0a345a0c15ab068982151 Mon Sep 17 00:00:00 2001
From: Brian Vaughn <bvaughn@fb.com>
Date: Mon, 18 Mar 2019 09:18:22 -0700
Subject: [PATCH 3/8] Tweaked profiling did-not-render color to be dimmer

---
 src/devtools/views/root.css | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/devtools/views/root.css b/src/devtools/views/root.css
index 65eb630d24221..a8d562ee16928 100644
--- a/src/devtools/views/root.css
+++ b/src/devtools/views/root.css
@@ -16,7 +16,7 @@
   --light-color-button-focus: #3578e5;
   --light-color-button-hover: #3578e5;
   --light-color-border: #eeeeee;
-  --light-color-commit-did-not-render: #777d88;
+  --light-color-commit-did-not-render: #cfd1d5;
   --light-color-commit-gradient-0: #37afa9;
   --light-color-commit-gradient-1: #63b19e;
   --light-color-commit-gradient-2: #80b393;
@@ -58,7 +58,7 @@
   --dark-color-button-focus: #a2e9fc;
   --dark-color-button-hover: #a2e9fc;
   --dark-color-border: #3d424a;
-  --dark-color-commit-did-not-render: #8f949d;
+  --dark-color-commit-did-not-render: #777d88;
   --dark-color-commit-gradient-0: #37afa9;
   --dark-color-commit-gradient-1: #63b19e;
   --dark-color-commit-gradient-2: #80b393;

From c9920f095457e39c4c8f060c5a6d14c7a8f0cd6f Mon Sep 17 00:00:00 2001
From: Brian Vaughn <bvaughn@fb.com>
Date: Mon, 18 Mar 2019 09:20:00 -0700
Subject: [PATCH 4/8] Added a few inline comments

---
 shells/browser/shared/src/main.js  | 2 ++
 shells/browser/shared/src/panel.js | 3 +++
 2 files changed, 5 insertions(+)

diff --git a/shells/browser/shared/src/main.js b/shells/browser/shared/src/main.js
index 63859497b1d09..bf6726141ea3b 100644
--- a/shells/browser/shared/src/main.js
+++ b/shells/browser/shared/src/main.js
@@ -32,9 +32,11 @@ function createPanelIfReactLoaded() {
 
       let bridge = null;
       let store = null;
+
       let elementsPortalContainer = null;
       let profilerPortalContainer = null;
       let settingsPortalContainer = null;
+
       let cloneStyleTags = null;
       let render = null;
 
diff --git a/shells/browser/shared/src/panel.js b/shells/browser/shared/src/panel.js
index 1ce574dfcf420..f84ac11beb1e7 100644
--- a/shells/browser/shared/src/panel.js
+++ b/shells/browser/shared/src/panel.js
@@ -1,7 +1,10 @@
+// Portal target container.
 window.container = document.getElementById('container');
 
 let hasInjectedStyles = false;
 
+// DevTools styles are injected into the top-level document head (where the main React app is rendered).
+// This method copies those styles to the child window where each panel (e.g. Elements, Profiler) is portaled.
 window.injectStyles = getLinkTags => {
   if (!hasInjectedStyles) {
     hasInjectedStyles = true;

From e728ebc7b9c38770761fbd5f791c9334518f0131 Mon Sep 17 00:00:00 2001
From: Brian Vaughn <bvaughn@fb.com>
Date: Mon, 18 Mar 2019 09:36:47 -0700
Subject: [PATCH 5/8] Unmount and remount when main URL changes to avoid
 staleness problems

---
 shells/browser/shared/src/main.js | 17 ++++++++++++-----
 1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/shells/browser/shared/src/main.js b/shells/browser/shared/src/main.js
index bf6726141ea3b..0a596e42caa27 100644
--- a/shells/browser/shared/src/main.js
+++ b/shells/browser/shared/src/main.js
@@ -1,7 +1,7 @@
 /* global chrome */
 
 import { createElement } from 'react';
-import { unstable_createRoot as createRoot } from 'react-dom';
+import { unstable_createRoot as createRoot, flushSync } from 'react-dom';
 import Bridge from 'src/bridge';
 import Store from 'src/devtools/Store';
 import inject from './inject';
@@ -38,7 +38,9 @@ function createPanelIfReactLoaded() {
       let settingsPortalContainer = null;
 
       let cloneStyleTags = null;
+      let mostRecentOverrideTab = null;
       let render = null;
+      let root = null;
 
       function initBridgeAndStore() {
         let hasPortBeenDisconnected = false;
@@ -68,10 +70,11 @@ function createPanelIfReactLoaded() {
 
         const viewElementSource = createViewElementSource(bridge, store);
 
-        const container = document.createElement('div');
-        const root = createRoot(container);
+        root = createRoot(document.createElement('div'));
+
+        render = (overrideTab = mostRecentOverrideTab) => {
+          mostRecentOverrideTab = overrideTab;
 
-        render = overrideTab => {
           root.render(
             createElement(DevTools, {
               bridge,
@@ -87,6 +90,8 @@ function createPanelIfReactLoaded() {
             })
           );
         };
+
+        render();
       }
 
       cloneStyleTags = () => {
@@ -150,7 +155,9 @@ function createPanelIfReactLoaded() {
       chrome.devtools.network.onNavigated.addListener(function onNavigated() {
         bridge.send('shutdown');
 
-        initBridgeAndStore();
+        // It's easiest to recreate the DevTools panel (to clean up potential stale state).
+        // We can revisit this in the future as a small optimization.
+        flushSync(() => root.unmount(initBridgeAndStore));
       });
     }
   );

From 50b6b1d5f9e8a3284971ca0b464fb88ce3f925ba Mon Sep 17 00:00:00 2001
From: Brian Vaughn <bvaughn@fb.com>
Date: Mon, 18 Mar 2019 09:57:08 -0700
Subject: [PATCH 6/8] Added some inline comments about portal props

---
 src/devtools/views/DevTools.js | 17 +++++++++++++----
 1 file changed, 13 insertions(+), 4 deletions(-)

diff --git a/src/devtools/views/DevTools.js b/src/devtools/views/DevTools.js
index d80a0ecbe1197..4f839c368009b 100644
--- a/src/devtools/views/DevTools.js
+++ b/src/devtools/views/DevTools.js
@@ -27,13 +27,22 @@ export type Props = {|
   browserName: BrowserName,
   browserTheme: BrowserTheme,
   defaultTab?: TabID,
-  elementsPortalContainer?: Element,
-  overrideTab?: TabID,
-  profilerPortalContainer?: Element,
-  settingsPortalContainer?: Element,
   showTabBar?: boolean,
   store: Store,
   viewElementSource?: ?Function,
+
+  // This property is used only by the web extension target.
+  // The built-in tab UI is hidden in that case, in favor of the browser's own panel tabs.
+  // This is done to save space within the app.
+  // Because of this, the extension needs to be able to change which tab is active/rendered.
+  overrideTab?: TabID,
+
+  // To avoid potential multi-root trickiness, the web extension uses portals to render tabs.
+  // The root <DevTools> app is rendered in the top-level extension window,
+  // but individual tabs (e.g. Elements, Profiling) can be rendered into portals within their browser panels.
+  elementsPortalContainer?: Element,
+  profilerPortalContainer?: Element,
+  settingsPortalContainer?: Element,
 |};
 
 const elementTab = {

From be48150fc69a078e4b51cdaef635b6eacaa1b4db Mon Sep 17 00:00:00 2001
From: Brian Vaughn <bvaughn@fb.com>
Date: Mon, 18 Mar 2019 10:09:09 -0700
Subject: [PATCH 7/8] innerTagName -> innerElementType

---
 src/devtools/views/Profiler/CommitRanked.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/devtools/views/Profiler/CommitRanked.js b/src/devtools/views/Profiler/CommitRanked.js
index 1fb4ded3ee415..acdd0cb8841e6 100644
--- a/src/devtools/views/Profiler/CommitRanked.js
+++ b/src/devtools/views/Profiler/CommitRanked.js
@@ -104,7 +104,7 @@ function CommitRanked({ height, width }: {| height: number, width: number |}) {
   return (
     <FixedSizeList
       height={height}
-      innerTagName="svg"
+      innerElementType="svg"
       itemCount={chartData.nodes.length}
       itemData={itemData}
       itemSize={barHeight}

From 5f154b376e9658a6b7b7477dead472063be1541e Mon Sep 17 00:00:00 2001
From: Brian Vaughn <bvaughn@fb.com>
Date: Mon, 18 Mar 2019 13:37:37 -0700
Subject: [PATCH 8/8] Added profiling-not-supported message for browser
 extension

---
 src/devtools/views/Button.css               |  3 +-
 src/devtools/views/DevTools.js              |  7 +++-
 src/devtools/views/Profiler/Profiler.js     | 38 +++++++++++++++++++--
 src/devtools/views/Profiler/RecordToggle.js |  7 ++--
 4 files changed, 48 insertions(+), 7 deletions(-)

diff --git a/src/devtools/views/Button.css b/src/devtools/views/Button.css
index 31291d7f3bde1..a37af0ee30cc0 100644
--- a/src/devtools/views/Button.css
+++ b/src/devtools/views/Button.css
@@ -22,7 +22,8 @@
   outline: none;
   box-shadow: 0 0 0 2px var(--color-button-background-focus) inset;
 }
-.Button:disabled {
+.Button:disabled,
+.Button:disabled:active {
   background: var(--color-button-background);
   color: var(--color-button-disabled);
   cursor: default;
diff --git a/src/devtools/views/DevTools.js b/src/devtools/views/DevTools.js
index 4f839c368009b..56eee3b56edce 100644
--- a/src/devtools/views/DevTools.js
+++ b/src/devtools/views/DevTools.js
@@ -110,7 +110,12 @@ export default function DevTools({
   let tabElement;
   switch (tab) {
     case 'profiler':
-      tabElement = <Profiler portalContainer={profilerPortalContainer} />;
+      tabElement = (
+        <Profiler
+          portalContainer={profilerPortalContainer}
+          supportsProfiling={supportsProfiling}
+        />
+      );
       break;
     case 'settings':
       tabElement = <Settings portalContainer={settingsPortalContainer} />;
diff --git a/src/devtools/views/Profiler/Profiler.js b/src/devtools/views/Profiler/Profiler.js
index e5c2db601a9e0..4199efe1c25d6 100644
--- a/src/devtools/views/Profiler/Profiler.js
+++ b/src/devtools/views/Profiler/Profiler.js
@@ -16,9 +16,13 @@ import styles from './Profiler.css';
 
 export type Props = {|
   portalContainer?: Element,
+  supportsProfiling: boolean,
 |};
 
-export default function Profiler({ portalContainer }: Props) {
+export default function Profiler({
+  portalContainer,
+  supportsProfiling,
+}: Props) {
   const { hasProfilingData, isProfiling, rootHasProfilingData } = useContext(
     ProfilerContext
   );
@@ -29,6 +33,7 @@ export default function Profiler({ portalContainer }: Props) {
       <NonSuspendingProfiler
         hasProfilingData={hasProfilingData}
         isProfiling={isProfiling}
+        supportsProfiling={supportsProfiling}
       />
     );
   } else {
@@ -47,12 +52,16 @@ export default function Profiler({ portalContainer }: Props) {
 function NonSuspendingProfiler({
   hasProfilingData,
   isProfiling,
+  supportsProfiling,
 }: {|
   hasProfilingData: boolean,
   isProfiling: boolean,
+  supportsProfiling: boolean,
 |}) {
   let view = null;
-  if (isProfiling) {
+  if (!supportsProfiling) {
+    view = <ProfilingNotSupported />;
+  } else if (isProfiling) {
     view = <RecortdingInProgress />;
   } else if (!hasProfilingData) {
     view = <NoProfilingData />;
@@ -64,7 +73,7 @@ function NonSuspendingProfiler({
     <div className={styles.Profiler}>
       <div className={styles.LeftColumn}>
         <div className={styles.Toolbar}>
-          <RecordToggle />
+          <RecordToggle disabled={!supportsProfiling} />
           <Button disabled title="Reload and start profiling">
             {/* TODO (profiling) Wire up reload button */}
             <ButtonIcon type="reload" />
@@ -200,6 +209,29 @@ const NoProfilingDataForRoot = () => (
   </div>
 );
 
+const ProfilingNotSupported = () => (
+  <div className={styles.Column}>
+    <div className={styles.Header}>Profiling not supported.</div>
+    <div className={styles.Column}>
+      <p>
+        Profiling support requires either a development or production-profiling
+        build of React v16.5+.
+      </p>
+      <p>
+        Learn more at{' '}
+        <a
+          href="https://fb.me/react-profiling"
+          rel="noopener noreferrer"
+          target="_blank"
+        >
+          fb.me/react-profiling
+        </a>
+        .
+      </p>
+    </div>
+  </div>
+);
+
 const RecortdingInProgress = () => (
   <div className={styles.Column}>
     <div className={styles.Header}>Profiling is in progress...</div>
diff --git a/src/devtools/views/Profiler/RecordToggle.js b/src/devtools/views/Profiler/RecordToggle.js
index 4d1bb591d92f2..94074002ca1db 100644
--- a/src/devtools/views/Profiler/RecordToggle.js
+++ b/src/devtools/views/Profiler/RecordToggle.js
@@ -7,9 +7,11 @@ import { ProfilerContext } from './ProfilerContext';
 
 import styles from './RecordToggle.css';
 
-export type Props = {||};
+export type Props = {|
+  disabled?: boolean,
+|};
 
-export default function RecordToggle(_: Props) {
+export default function RecordToggle({ disabled }: Props) {
   const { isProfiling, startProfiling, stopProfiling } = useContext(
     ProfilerContext
   );
@@ -19,6 +21,7 @@ export default function RecordToggle(_: Props) {
       className={
         isProfiling ? styles.ActiveRecordToggle : styles.InactiveRecordToggle
       }
+      disabled={disabled}
       onClick={isProfiling ? stopProfiling : startProfiling}
       title={isProfiling ? 'Stop profiling' : 'Start profiling'}
     >