diff --git a/shells/browser/shared/src/main.js b/shells/browser/shared/src/main.js index d09dddf1bd..63859497b1 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); - } - } - - return newLinkTags; }; - - if (elementsPanel !== null) { - elementsPanel.render(renderRootToPortal, 'elements'); - } } + 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 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 b7413918f5..1ce574dfcf 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 2db0e2dcfb..d80a0ecbe1 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 = ; + tabElement = ; break; case 'settings': - tabElement = ; + tabElement = ; break; case 'elements': default: - tabElement = ; + tabElement = ; break; } - const children = ( + return ( - +
@@ -144,8 +152,4 @@ export default function DevTools({ ); - - return portalContainer != null - ? createPortal(children, portalContainer) - : children; } diff --git a/src/devtools/views/Elements/Elements.js b/src/devtools/views/Elements/Elements.js index cb401ddfdb..596a5c754a 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 = (
@@ -19,4 +22,8 @@ export default function Elements(_: Props) {
); + + return portalContainer != null + ? createPortal(children, portalContainer) + : children; } diff --git a/src/devtools/views/Profiler/Profiler.js b/src/devtools/views/Profiler/Profiler.js index add7926922..e5c2db601a 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 = ( ); } else { - return ; + children = ; } + + 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 37e4dd9198..d3a99360a0 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 = (
Theme
@@ -90,4 +93,8 @@ export default function Settings(_: Props) {
); + + return portalContainer != null + ? createPortal(children, portalContainer) + : children; } diff --git a/src/devtools/views/Settings/SettingsContext.js b/src/devtools/views/Settings/SettingsContext.js index 0c5458cbad..f04af61f32 100644 --- a/src/devtools/views/Settings/SettingsContext.js +++ b/src/devtools/views/Settings/SettingsContext.js @@ -23,18 +23,58 @@ type Context = {| const SettingsContext = createContext(((null: any): Context)); SettingsContext.displayName = 'SettingsContext'; +type DocumentElements = Array; + 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', 'compact' ); const [theme, setTheme] = useLocalStorage('theme', 'auto'); + const documentElements = useMemo(() => { + const array: Array = [ + ((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 };