Refactored portaling and fixed disconnected CSS vars
Brian Vaughn committed
Mar 18, 2019 at 09:11 UTC
11573bf8d9d6346121492d97d2db8a443f4a22a3
7 files changed
+222
-128
shells/browser/shared/src/main.js
+36
-37
@@ -30,12 +30,13 @@ function createPanelIfReactLoaded() {
30
31
clearInterval(loadCheckInterval);
32
33
- let renderRootToPortal = null;
33
let bridge = null;
34
let store = null;
36
- let elementsPanel = null;
37
- let profilerPanel = null;
38
- let settingsPanel = null;
35
+ let elementsPortalContainer = null;
36
+ let profilerPortalContainer = null;
37
+ let settingsPortalContainer = null;
38
+ let cloneStyleTags = null;
39
+ let render = null;
40
41
function initBridgeAndStore() {
42
let hasPortBeenDisconnected = false;
@@ -68,51 +69,47 @@ function createPanelIfReactLoaded() {
69
const container = document.createElement('div');
70
const root = createRoot(container);
71
71
- renderRootToPortal = ({ overrideTab, portalContainer }) => {
72
+ render = overrideTab => {
73
root.render(
74
createElement(DevTools, {
75
bridge,
76
browserName: getBrowserName(),
77
browserTheme: getBrowserTheme(),
78
+ elementsPortalContainer,
79
overrideTab,
78
- portalContainer,
80
+ profilerPortalContainer,
81
+ settingsPortalContainer,
82
showTabBar: false,
83
store,
84
viewElementSource,
85
})
86
);
87
+ };
88
+ }
89
85
- const oldLinkTags = document.getElementsByTagName('link');
86
- const newLinkTags = [];
87
- for (let oldLinkTag of oldLinkTags) {
88
- if (oldLinkTag.rel === 'stylesheet') {
89
- const newLinkTag = document.createElement('link');
90
- for (let attribute of oldLinkTag.attributes) {
91
- newLinkTag.setAttribute(
92
- attribute.nodeName,
93
- attribute.nodeValue
94
- );
95
- }
96
- newLinkTags.push(newLinkTag);
90
+ cloneStyleTags = () => {
91
+ const linkTags = [];
92
+ for (let linkTag of document.getElementsByTagName('link')) {
93
+ if (linkTag.rel === 'stylesheet') {
94
+ const newLinkTag = document.createElement('link');
95
+ for (let attribute of linkTag.attributes) {
96
+ newLinkTag.setAttribute(attribute.nodeName, attribute.nodeValue);
97
}
98
+ linkTags.push(newLinkTag);
99
}
99
-
100
- return newLinkTags;
101
- };
102
-
103
- if (elementsPanel !== null) {
104
- elementsPanel.render(renderRootToPortal, 'elements');
100
}
106
- }
101
+ return linkTags;
102
+ };
103
104
initBridgeAndStore();
105
106
chrome.devtools.panels.create('⚛ Elements', '', 'panel.html', panel => {
107
panel.onShown.addListener(panel => {
112
- elementsPanel = panel;
113
-
114
- if (renderRootToPortal !== null) {
115
- elementsPanel.render(renderRootToPortal, 'elements');
108
+ elementsPortalContainer = panel.container;
109
+ if (elementsPortalContainer != null) {
110
+ elementsPortalContainer.innerHTML = '';
111
+ render('elements');
112
+ panel.injectStyles(cloneStyleTags);
113
}
114
115
// TODO: When the user switches to the panel, check for an Elements tab selection.
@@ -125,20 +122,22 @@ function createPanelIfReactLoaded() {
122
// TODO (profiling) Is there a way to detect profiling support and conditionally register this panel?
123
chrome.devtools.panels.create('⚛ Profiler', '', 'panel.html', panel => {
124
panel.onShown.addListener(panel => {
128
- profilerPanel = panel;
129
-
130
- if (renderRootToPortal !== null) {
131
- profilerPanel.render(renderRootToPortal, 'profiler');
125
+ profilerPortalContainer = panel.container;
126
+ if (profilerPortalContainer != null) {
127
+ profilerPortalContainer.innerHTML = '';
128
+ render('profiler');
129
+ panel.injectStyles(cloneStyleTags);
130
}
131
});
132
});
133
134
chrome.devtools.panels.create('⚛ Settings', '', 'panel.html', panel => {
135
panel.onShown.addListener(panel => {
138
- settingsPanel = panel;
139
-
140
- if (renderRootToPortal !== null) {
141
- settingsPanel.render(renderRootToPortal, 'settings');
136
+ settingsPortalContainer = panel.container;
137
+ if (settingsPortalContainer != null) {
138
+ settingsPortalContainer.innerHTML = '';
139
+ render('settings');
140
+ panel.injectStyles(cloneStyleTags);
141
}
142
});
143
});
shells/browser/shared/src/panel.js
+4
-9
@@ -1,18 +1,13 @@
1
-const container = document.getElementById('container');
1
+window.container = document.getElementById('container');
2
3
let hasInjectedStyles = false;
4
5
-window.render = (renderRootToPortal, tab) => {
6
- container.innerHTML = '';
7
-
8
- const linkTags = renderRootToPortal({
9
- overrideTab: tab,
10
- portalContainer: container,
11
- });
12
-
5
+window.injectStyles = getLinkTags => {
6
if (!hasInjectedStyles) {
7
hasInjectedStyles = true;
8
9
+ const linkTags = getLinkTags();
10
+
11
for (let linkTag of linkTags) {
12
document.head.appendChild(linkTag);
13
}
src/devtools/views/DevTools.js
+18
-14
@@ -1,7 +1,6 @@
1
// @flow
2
3
import React, { useEffect, useState } from 'react';
4
-import { createPortal } from 'react-dom';
4
import Store from '../store';
5
import { BridgeContext, StoreContext } from './context';
6
import Elements from './Elements/Elements';
@@ -26,10 +25,12 @@ export type TabID = 'elements' | 'profiler' | 'settings';
25
export type Props = {|
26
bridge: Bridge,
27
browserName: BrowserName,
29
- defaultTab?: TabID,
28
browserTheme: BrowserTheme,
29
+ defaultTab?: TabID,
30
+ elementsPortalContainer?: Element,
31
overrideTab?: TabID,
32
- portalContainer?: Element,
32
+ profilerPortalContainer?: Element,
33
+ settingsPortalContainer?: Element,
34
showTabBar?: boolean,
35
store: Store,
36
viewElementSource?: ?Function,
@@ -60,10 +61,12 @@ const tabsWithoutProfiler = [elementTab, settingsTab];
61
export default function DevTools({
62
bridge,
63
browserName,
63
- defaultTab = 'elements',
64
browserTheme = 'light',
65
+ defaultTab = 'elements',
66
+ elementsPortalContainer,
67
overrideTab,
66
- portalContainer,
68
+ profilerPortalContainer,
69
+ settingsPortalContainer,
70
showTabBar = false,
71
store,
72
viewElementSource = null,
@@ -98,21 +101,26 @@ export default function DevTools({
101
let tabElement;
102
switch (tab) {
103
case 'profiler':
101
- tabElement = <Profiler />;
104
+ tabElement = <Profiler portalContainer={profilerPortalContainer} />;
105
break;
106
case 'settings':
104
- tabElement = <Settings />;
107
+ tabElement = <Settings portalContainer={settingsPortalContainer} />;
108
break;
109
case 'elements':
110
default:
108
- tabElement = <Elements />;
111
+ tabElement = <Elements portalContainer={elementsPortalContainer} />;
112
break;
113
}
114
112
- const children = (
115
+ return (
116
<BridgeContext.Provider value={bridge}>
117
<StoreContext.Provider value={store}>
115
- <SettingsContextController browserTheme={browserTheme}>
118
+ <SettingsContextController
119
+ browserTheme={browserTheme}
120
+ elementsPortalContainer={elementsPortalContainer}
121
+ profilerPortalContainer={profilerPortalContainer}
122
+ settingsPortalContainer={settingsPortalContainer}
123
+ >
124
<TreeContextController viewElementSource={viewElementSource}>
125
<ProfilerContextController>
126
<div className={styles.DevTools}>
@@ -144,8 +152,4 @@ export default function DevTools({
152
</StoreContext.Provider>
153
</BridgeContext.Provider>
154
);
147
-
148
- return portalContainer != null
149
- ? createPortal(children, portalContainer)
150
- : children;
155
}
src/devtools/views/Elements/Elements.js
+10
-3
@@ -1,15 +1,18 @@
1
// @flow
2
3
import React from 'react';
4
+import { createPortal } from 'react-dom';
5
import Tree from './Tree';
6
import SelectedElement from './SelectedElement';
7
import styles from './Elements.css';
8
8
-export type Props = {||};
9
+export type Props = {|
10
+ portalContainer?: Element,
11
+|};
12
10
-export default function Elements(_: Props) {
13
+export default function Elements({ portalContainer }: Props) {
14
// TODO Flex wrappers below should be user resizable.
12
- return (
15
+ const children = (
16
<div className={styles.Elements}>
17
<div className={styles.TreeWrapper}>
18
<Tree />
@@ -19,4 +22,8 @@ export default function Elements(_: Props) {
22
</div>
23
</div>
24
);
25
+
26
+ return portalContainer != null
27
+ ? createPortal(children, portalContainer)
28
+ : children;
29
}
src/devtools/views/Profiler/Profiler.js
+13
-3
@@ -1,6 +1,7 @@
1
// @flow
2
3
import React, { Suspense, useCallback, useContext, useState } from 'react';
4
+import { createPortal } from 'react-dom';
5
import { ProfilerContext } from './ProfilerContext';
6
import Button from '../Button';
7
import ButtonIcon from '../ButtonIcon';
@@ -13,21 +14,30 @@ import SnapshotSelector from './SnapshotSelector';
14
15
import styles from './Profiler.css';
16
16
-export default function Profiler(_: {||}) {
17
+export type Props = {|
18
+ portalContainer?: Element,
19
+|};
20
+
21
+export default function Profiler({ portalContainer }: Props) {
22
const { hasProfilingData, isProfiling, rootHasProfilingData } = useContext(
23
ProfilerContext
24
);
25
26
+ let children = null;
27
if (isProfiling || !rootHasProfilingData) {
22
- return (
28
+ children = (
29
<NonSuspendingProfiler
30
hasProfilingData={hasProfilingData}
31
isProfiling={isProfiling}
32
/>
33
);
34
} else {
29
- return <SuspendingProfiler />;
35
+ children = <SuspendingProfiler />;
36
}
37
+
38
+ return portalContainer != null
39
+ ? createPortal(children, portalContainer)
40
+ : children;
41
}
42
43
// This view is rendered when there is no profiler data (either we haven't profiled yet or we're currently profiling).
src/devtools/views/Settings/Settings.js
+10
-3
@@ -1,13 +1,16 @@
1
// @flow
2
3
import React, { useCallback, useContext } from 'react';
4
+import { createPortal } from 'react-dom';
5
import { SettingsContext } from './SettingsContext';
6
7
import styles from './Settings.css';
8
8
-export type Props = {||};
9
+export type Props = {|
10
+ portalContainer?: Element,
11
+|};
12
10
-export default function Settings(_: Props) {
13
+export default function Settings({ portalContainer }: Props) {
14
const { displayDensity, setDisplayDensity, theme, setTheme } = useContext(
15
SettingsContext
16
);
@@ -26,7 +29,7 @@ export default function Settings(_: Props) {
29
[setTheme]
30
);
31
29
- return (
32
+ const children = (
33
<div className={styles.Settings}>
34
<div className={styles.Section}>
35
<div className={styles.Header}>Theme</div>
@@ -90,4 +93,8 @@ export default function Settings(_: Props) {
93
</div>
94
</div>
95
);
96
+
97
+ return portalContainer != null
98
+ ? createPortal(children, portalContainer)
99
+ : children;
100
}
src/devtools/views/Settings/SettingsContext.js
+131
-59
@@ -23,18 +23,58 @@ type Context = {|
23
const SettingsContext = createContext<Context>(((null: any): Context));
24
SettingsContext.displayName = 'SettingsContext';
25
26
+type DocumentElements = Array<HTMLElement>;
27
+
28
type Props = {|
29
browserTheme: BrowserTheme,
30
children: React$Node,
31
+ elementsPortalContainer?: Element,
32
+ profilerPortalContainer?: Element,
33
+ settingsPortalContainer?: Element,
34
|};
35
31
-function SettingsContextController({ browserTheme, children }: Props) {
36
+function SettingsContextController({
37
+ browserTheme,
38
+ children,
39
+ elementsPortalContainer,
40
+ profilerPortalContainer,
41
+ settingsPortalContainer,
42
+}: Props) {
43
const [displayDensity, setDisplayDensity] = useLocalStorage<DisplayDensity>(
44
'displayDensity',
45
'compact'
46
);
47
const [theme, setTheme] = useLocalStorage<Theme>('theme', 'auto');
48
49
+ const documentElements = useMemo<DocumentElements>(() => {
50
+ const array: Array<HTMLElement> = [
51
+ ((document.documentElement: any): HTMLElement),
52
+ ];
53
+ if (elementsPortalContainer != null) {
54
+ array.push(
55
+ ((elementsPortalContainer.ownerDocument
56
+ .documentElement: any): HTMLElement)
57
+ );
58
+ }
59
+ if (profilerPortalContainer != null) {
60
+ array.push(
61
+ ((profilerPortalContainer.ownerDocument
62
+ .documentElement: any): HTMLElement)
63
+ );
64
+ }
65
+ if (settingsPortalContainer != null) {
66
+ array.push(
67
+ ((settingsPortalContainer.ownerDocument
68
+ .documentElement: any): HTMLElement)
69
+ );
70
+ }
71
+ return array;
72
+ }, [
73
+ elementsPortalContainer,
74
+ profilerPortalContainer,
75
+ settingsPortalContainer,
76
+ ]);
77
+
78
const comfortableLineHeight = parseInt(
79
getComputedStyle((document.body: any)).getPropertyValue(
80
'--comfortable-line-height-data'
@@ -51,31 +91,31 @@ function SettingsContextController({ browserTheme, children }: Props) {
91
useLayoutEffect(() => {
92
switch (displayDensity) {
93
case 'compact':
54
- updateDisplayDensity('compact');
94
+ updateDisplayDensity('compact', documentElements);
95
break;
96
case 'comfortable':
57
- updateDisplayDensity('comfortable');
97
+ updateDisplayDensity('comfortable', documentElements);
98
break;
99
default:
100
throw Error(`Unsupported displayDensity value "${displayDensity}"`);
101
}
62
- }, [displayDensity]);
102
+ }, [displayDensity, documentElements]);
103
104
useLayoutEffect(() => {
105
switch (theme) {
106
case 'light':
67
- updateThemeVariables('light');
107
+ updateThemeVariables('light', documentElements);
108
break;
109
case 'dark':
70
- updateThemeVariables('dark');
110
+ updateThemeVariables('dark', documentElements);
111
break;
112
case 'auto':
73
- updateThemeVariables(browserTheme);
113
+ updateThemeVariables(browserTheme, documentElements);
114
break;
115
default:
116
throw Error(`Unsupported theme value "${theme}"`);
117
}
78
- }, [browserTheme, theme]);
118
+ }, [browserTheme, theme, documentElements]);
119
120
const value = useMemo(
121
() => ({
@@ -105,63 +145,95 @@ function SettingsContextController({ browserTheme, children }: Props) {
145
);
146
}
147
108
-function setStyleVariable(name: string, value: string) {
109
- (document.documentElement: any).style.setProperty(name, value);
148
+function setStyleVariable(
149
+ name: string,
150
+ value: string,
151
+ documentElements: DocumentElements
152
+) {
153
+ documentElements.forEach(documentElement =>
154
+ documentElement.style.setProperty(name, value)
155
+ );
156
}
157
112
-function updateStyleHelper(themeKey: string, style: string) {
113
- setStyleVariable(`--${style}`, `var(--${themeKey}-${style})`);
158
+function updateStyleHelper(
159
+ themeKey: string,
160
+ style: string,
161
+ documentElements: DocumentElements
162
+) {
163
+ setStyleVariable(
164
+ `--${style}`,
165
+ `var(--${themeKey}-${style})`,
166
+ documentElements
167
+ );
168
}
169
116
-function updateDisplayDensity(displayDensity: DisplayDensity): void {
117
- updateStyleHelper(displayDensity, 'font-size-monospace-normal');
118
- updateStyleHelper(displayDensity, 'font-size-monospace-large');
119
- updateStyleHelper(displayDensity, 'font-size-sans-normal');
120
- updateStyleHelper(displayDensity, 'font-size-sans-large');
121
- updateStyleHelper(displayDensity, 'line-height-data');
170
+function updateDisplayDensity(
171
+ displayDensity: DisplayDensity,
172
+ documentElements: DocumentElements
173
+): void {
174
+ updateStyleHelper(
175
+ displayDensity,
176
+ 'font-size-monospace-normal',
177
+ documentElements
178
+ );
179
+ updateStyleHelper(
180
+ displayDensity,
181
+ 'font-size-monospace-large',
182
+ documentElements
183
+ );
184
+ updateStyleHelper(displayDensity, 'font-size-sans-normal', documentElements);
185
+ updateStyleHelper(displayDensity, 'font-size-sans-large', documentElements);
186
+ updateStyleHelper(displayDensity, 'line-height-data', documentElements);
187
}
188
124
-function updateThemeVariables(theme: Theme): void {
125
- updateStyleHelper(theme, 'color-attribute-name');
126
- updateStyleHelper(theme, 'color-attribute-value');
127
- updateStyleHelper(theme, 'color-attribute-editable-value');
128
- updateStyleHelper(theme, 'color-background');
129
- updateStyleHelper(theme, 'color-border');
130
- updateStyleHelper(theme, 'color-button-background');
131
- updateStyleHelper(theme, 'color-button-background-focus');
132
- updateStyleHelper(theme, 'color-button-background-hover');
133
- updateStyleHelper(theme, 'color-button');
134
- updateStyleHelper(theme, 'color-button-disabled');
135
- updateStyleHelper(theme, 'color-button-focus');
136
- updateStyleHelper(theme, 'color-button-hover');
137
- updateStyleHelper(theme, 'color-commit-did-not-render');
138
- updateStyleHelper(theme, 'color-commit-gradient-0');
139
- updateStyleHelper(theme, 'color-commit-gradient-1');
140
- updateStyleHelper(theme, 'color-commit-gradient-2');
141
- updateStyleHelper(theme, 'color-commit-gradient-3');
142
- updateStyleHelper(theme, 'color-commit-gradient-4');
143
- updateStyleHelper(theme, 'color-commit-gradient-5');
144
- updateStyleHelper(theme, 'color-commit-gradient-6');
145
- updateStyleHelper(theme, 'color-commit-gradient-7');
146
- updateStyleHelper(theme, 'color-commit-gradient-8');
147
- updateStyleHelper(theme, 'color-commit-gradient-9');
148
- updateStyleHelper(theme, 'color-commit-gradient-text');
149
- updateStyleHelper(theme, 'color-component-name');
150
- updateStyleHelper(theme, 'color-component-name-inverted');
151
- updateStyleHelper(theme, 'color-dim');
152
- updateStyleHelper(theme, 'color-dimmer');
153
- updateStyleHelper(theme, 'color-dimmest');
154
- updateStyleHelper(theme, 'color-jsx-arrow-brackets');
155
- updateStyleHelper(theme, 'color-jsx-arrow-brackets-inverted');
156
- updateStyleHelper(theme, 'color-modal-background');
157
- updateStyleHelper(theme, 'color-record-active');
158
- updateStyleHelper(theme, 'color-record-hover');
159
- updateStyleHelper(theme, 'color-record-inactive');
160
- updateStyleHelper(theme, 'color-tree-node-selected');
161
- updateStyleHelper(theme, 'color-tree-node-hover');
162
- updateStyleHelper(theme, 'color-search-match');
163
- updateStyleHelper(theme, 'color-search-match-current');
164
- updateStyleHelper(theme, 'color-text-color');
189
+function updateThemeVariables(
190
+ theme: Theme,
191
+ documentElements: DocumentElements
192
+): void {
193
+ updateStyleHelper(theme, 'color-attribute-name', documentElements);
194
+ updateStyleHelper(theme, 'color-attribute-value', documentElements);
195
+ updateStyleHelper(theme, 'color-attribute-editable-value', documentElements);
196
+ updateStyleHelper(theme, 'color-background', documentElements);
197
+ updateStyleHelper(theme, 'color-border', documentElements);
198
+ updateStyleHelper(theme, 'color-button-background', documentElements);
199
+ updateStyleHelper(theme, 'color-button-background-focus', documentElements);
200
+ updateStyleHelper(theme, 'color-button-background-hover', documentElements);
201
+ updateStyleHelper(theme, 'color-button', documentElements);
202
+ updateStyleHelper(theme, 'color-button-disabled', documentElements);
203
+ updateStyleHelper(theme, 'color-button-focus', documentElements);
204
+ updateStyleHelper(theme, 'color-button-hover', documentElements);
205
+ updateStyleHelper(theme, 'color-commit-did-not-render', documentElements);
206
+ updateStyleHelper(theme, 'color-commit-gradient-0', documentElements);
207
+ updateStyleHelper(theme, 'color-commit-gradient-1', documentElements);
208
+ updateStyleHelper(theme, 'color-commit-gradient-2', documentElements);
209
+ updateStyleHelper(theme, 'color-commit-gradient-3', documentElements);
210
+ updateStyleHelper(theme, 'color-commit-gradient-4', documentElements);
211
+ updateStyleHelper(theme, 'color-commit-gradient-5', documentElements);
212
+ updateStyleHelper(theme, 'color-commit-gradient-6', documentElements);
213
+ updateStyleHelper(theme, 'color-commit-gradient-7', documentElements);
214
+ updateStyleHelper(theme, 'color-commit-gradient-8', documentElements);
215
+ updateStyleHelper(theme, 'color-commit-gradient-9', documentElements);
216
+ updateStyleHelper(theme, 'color-commit-gradient-text', documentElements);
217
+ updateStyleHelper(theme, 'color-component-name', documentElements);
218
+ updateStyleHelper(theme, 'color-component-name-inverted', documentElements);
219
+ updateStyleHelper(theme, 'color-dim', documentElements);
220
+ updateStyleHelper(theme, 'color-dimmer', documentElements);
221
+ updateStyleHelper(theme, 'color-dimmest', documentElements);
222
+ updateStyleHelper(theme, 'color-jsx-arrow-brackets', documentElements);
223
+ updateStyleHelper(
224
+ theme,
225
+ 'color-jsx-arrow-brackets-inverted',
226
+ documentElements
227
+ );
228
+ updateStyleHelper(theme, 'color-modal-background', documentElements);
229
+ updateStyleHelper(theme, 'color-record-active', documentElements);
230
+ updateStyleHelper(theme, 'color-record-hover', documentElements);
231
+ updateStyleHelper(theme, 'color-record-inactive', documentElements);
232
+ updateStyleHelper(theme, 'color-tree-node-selected', documentElements);
233
+ updateStyleHelper(theme, 'color-tree-node-hover', documentElements);
234
+ updateStyleHelper(theme, 'color-search-match', documentElements);
235
+ updateStyleHelper(theme, 'color-search-match-current', documentElements);
236
+ updateStyleHelper(theme, 'color-text-color', documentElements);
237
}
238
239
export { SettingsContext, SettingsContextController };