@samitouri / QOS-React / commits / 11573bf8d9

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 };