mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #33 from bvaughn/portals
Experimenting with portals [WIP]
This commit is contained in:
@@ -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"],
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,8 +1,16 @@
|
||||
/* global chrome */
|
||||
|
||||
import { createElement } from 'react';
|
||||
import { unstable_createRoot as createRoot, flushSync } 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;
|
||||
|
||||
@@ -24,9 +32,15 @@ function createPanelIfReactLoaded() {
|
||||
|
||||
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 mostRecentOverrideTab = null;
|
||||
let render = null;
|
||||
let root = null;
|
||||
|
||||
function initBridgeAndStore() {
|
||||
let hasPortBeenDisconnected = false;
|
||||
@@ -54,69 +68,86 @@ function createPanelIfReactLoaded() {
|
||||
// Otherwise the Store may miss important initial tree op codes.
|
||||
inject(chrome.runtime.getURL('build/backend.js'));
|
||||
|
||||
if (elementsPanel !== null) {
|
||||
elementsPanel.injectBridgeAndStore(bridge, store);
|
||||
}
|
||||
if (profilerPanel !== null) {
|
||||
profilerPanel.injectBridgeAndStore(bridge, store);
|
||||
}
|
||||
if (settingsPanel !== null) {
|
||||
settingsPanel.injectBridgeAndStore(bridge, store);
|
||||
}
|
||||
const viewElementSource = createViewElementSource(bridge, store);
|
||||
|
||||
root = createRoot(document.createElement('div'));
|
||||
|
||||
render = (overrideTab = mostRecentOverrideTab) => {
|
||||
mostRecentOverrideTab = overrideTab;
|
||||
|
||||
root.render(
|
||||
createElement(DevTools, {
|
||||
bridge,
|
||||
browserName: getBrowserName(),
|
||||
browserTheme: getBrowserTheme(),
|
||||
elementsPortalContainer,
|
||||
overrideTab,
|
||||
profilerPortalContainer,
|
||||
settingsPortalContainer,
|
||||
showTabBar: false,
|
||||
store,
|
||||
viewElementSource,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
render();
|
||||
}
|
||||
|
||||
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',
|
||||
'',
|
||||
'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 => {
|
||||
elementsPortalContainer = panel.container;
|
||||
if (elementsPortalContainer != null) {
|
||||
elementsPortalContainer.innerHTML = '';
|
||||
render('elements');
|
||||
panel.injectStyles(cloneStyleTags);
|
||||
}
|
||||
|
||||
elementsPanel = panel;
|
||||
|
||||
// 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 => {
|
||||
profilerPortalContainer = panel.container;
|
||||
if (profilerPortalContainer != null) {
|
||||
profilerPortalContainer.innerHTML = '';
|
||||
render('profiler');
|
||||
panel.injectStyles(cloneStyleTags);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
profilerPanel = panel;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
chrome.devtools.panels.create(
|
||||
'⚛ Settings',
|
||||
'',
|
||||
'settings.html',
|
||||
panel => {
|
||||
panel.onShown.addListener(panel => {
|
||||
if (settingsPanel === null) {
|
||||
panel.injectBridgeAndStore(bridge, store);
|
||||
}
|
||||
|
||||
settingsPanel = panel;
|
||||
});
|
||||
}
|
||||
);
|
||||
chrome.devtools.panels.create('⚛ Settings', '', 'panel.html', panel => {
|
||||
panel.onShown.addListener(panel => {
|
||||
settingsPortalContainer = panel.container;
|
||||
if (settingsPortalContainer != null) {
|
||||
settingsPortalContainer.innerHTML = '';
|
||||
render('settings');
|
||||
panel.injectStyles(cloneStyleTags);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
chrome.devtools.network.onNavigated.removeListener(checkPageForReact);
|
||||
|
||||
@@ -124,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));
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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;
|
||||
|
||||
const linkTags = getLinkTags();
|
||||
|
||||
for (let linkTag of linkTags) {
|
||||
document.head.appendChild(linkTag);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import { createPanel } from './utils';
|
||||
|
||||
createPanel('elements');
|
||||
@@ -1,3 +0,0 @@
|
||||
import { createPanel } from './utils';
|
||||
|
||||
createPanel('profiler');
|
||||
@@ -1,3 +0,0 @@
|
||||
import { createPanel } from './utils';
|
||||
|
||||
createPanel('settings');
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -25,11 +25,24 @@ export type TabID = 'elements' | 'profiler' | 'settings';
|
||||
export type Props = {|
|
||||
bridge: Bridge,
|
||||
browserName: BrowserName,
|
||||
defaultTab?: TabID,
|
||||
browserTheme: BrowserTheme,
|
||||
defaultTab?: TabID,
|
||||
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 = {
|
||||
@@ -57,13 +70,21 @@ const tabsWithoutProfiler = [elementTab, settingsTab];
|
||||
export default function DevTools({
|
||||
bridge,
|
||||
browserName,
|
||||
defaultTab = 'elements',
|
||||
browserTheme = 'light',
|
||||
defaultTab = 'elements',
|
||||
elementsPortalContainer,
|
||||
overrideTab,
|
||||
profilerPortalContainer,
|
||||
settingsPortalContainer,
|
||||
showTabBar = false,
|
||||
store,
|
||||
viewElementSource = null,
|
||||
}: Props) {
|
||||
const [tab, setTab] = useState(defaultTab);
|
||||
if (overrideTab != null && overrideTab !== tab) {
|
||||
setTab(overrideTab);
|
||||
}
|
||||
|
||||
const [supportsProfiling, setSupportsProfiling] = useState(
|
||||
store.supportsProfiling
|
||||
);
|
||||
@@ -89,21 +110,31 @@ export default function DevTools({
|
||||
let tabElement;
|
||||
switch (tab) {
|
||||
case 'profiler':
|
||||
tabElement = <Profiler />;
|
||||
tabElement = (
|
||||
<Profiler
|
||||
portalContainer={profilerPortalContainer}
|
||||
supportsProfiling={supportsProfiling}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'settings':
|
||||
tabElement = <Settings />;
|
||||
tabElement = <Settings portalContainer={settingsPortalContainer} />;
|
||||
break;
|
||||
case 'elements':
|
||||
default:
|
||||
tabElement = <Elements />;
|
||||
tabElement = <Elements portalContainer={elementsPortalContainer} />;
|
||||
break;
|
||||
}
|
||||
|
||||
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}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,35 @@ import SnapshotSelector from './SnapshotSelector';
|
||||
|
||||
import styles from './Profiler.css';
|
||||
|
||||
export default function Profiler(_: {||}) {
|
||||
export type Props = {|
|
||||
portalContainer?: Element,
|
||||
supportsProfiling: boolean,
|
||||
|};
|
||||
|
||||
export default function Profiler({
|
||||
portalContainer,
|
||||
supportsProfiling,
|
||||
}: Props) {
|
||||
const { hasProfilingData, isProfiling, rootHasProfilingData } = useContext(
|
||||
ProfilerContext
|
||||
);
|
||||
|
||||
let children = null;
|
||||
if (isProfiling || !rootHasProfilingData) {
|
||||
return (
|
||||
children = (
|
||||
<NonSuspendingProfiler
|
||||
hasProfilingData={hasProfilingData}
|
||||
isProfiling={isProfiling}
|
||||
supportsProfiling={supportsProfiling}
|
||||
/>
|
||||
);
|
||||
} 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).
|
||||
@@ -37,12 +52,16 @@ export default function Profiler(_: {||}) {
|
||||
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 />;
|
||||
@@ -54,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" />
|
||||
@@ -190,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>
|
||||
|
||||
@@ -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'}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user