Refactored portaling and fixed disconnected CSS vars

This commit is contained in:
Brian Vaughn
2019-03-18 09:11:28 -07:00
parent f3f4643a3a
commit 11573bf8d9
7 changed files with 224 additions and 130 deletions
+38 -39
View File
@@ -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);
}
});
});
+4 -9
View File
@@ -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);
}
+18 -14
View File
@@ -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;
}
+10 -3
View File
@@ -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;
}
+13 -3
View File
@@ -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).
+10 -3
View File
@@ -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;
}
+131 -59
View File
@@ -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 };