@samitouri / QOS-React-2 / commits / 19ed98d0c2

Removed Settings panel in favor of new shared Settings modal

Brian Vaughn committed Jun 10, 2019 at 14:57 UTC 19ed98d0c25fc4aeb3d96656c3f171497d623e29
27 files changed +1004 -1168
shells/browser/shared/src/main.js
-17
@@ -269,23 +269,6 @@ function createPanelIfReactLoaded() {
269 });
270 });
271
272 - chrome.devtools.panels.create('⚛ Settings', '', 'panel.html', panel => {
273 - panel.onShown.addListener(panel => {
274 - if (currentPanel === panel) {
275 - return;
276 - }
277 -
278 - currentPanel = panel;
279 - settingsPortalContainer = panel.container;
280 -
281 - if (settingsPortalContainer != null) {
282 - ensureInitialHTMLIsCleared(settingsPortalContainer);
283 - render('settings');
284 - panel.injectStyles(cloneStyleTags);
285 - }
286 - });
287 - });
288 -
272 chrome.devtools.network.onNavigated.removeListener(checkPageForReact);
273
274 // Shutdown bridge before a new page is loaded.
src/devtools/views/Components/ComponentFiltersModal.js deleted
-445
@@ -1,445 +0,0 @@
1 -// @flow
2 -
3 -import React, {
4 - useCallback,
5 - useContext,
6 - useMemo,
7 - useRef,
8 - useState,
9 -} from 'react';
10 -import { useModalDismissSignal, useSubscription } from '../hooks';
11 -import { ComponentFiltersModalContext } from './ComponentFiltersModalContext';
12 -import { StoreContext } from '../context';
13 -import Button from '../Button';
14 -import ButtonIcon from '../ButtonIcon';
15 -import Toggle from '../Toggle';
16 -import Store from 'src/devtools/store';
17 -import {
18 - ComponentFilterDisplayName,
19 - ComponentFilterElementType,
20 - ComponentFilterHOC,
21 - ComponentFilterLocation,
22 - ElementTypeClass,
23 - ElementTypeContext,
24 - ElementTypeEventTarget,
25 - ElementTypeFunction,
26 - ElementTypeForwardRef,
27 - ElementTypeHostComponent,
28 - ElementTypeMemo,
29 - ElementTypeOtherOrUnknown,
30 - ElementTypeProfiler,
31 - ElementTypeSuspense,
32 -} from 'src/types';
33 -import styles from './ComponentFiltersModal.css';
34 -
35 -import type {
36 - BooleanComponentFilter,
37 - ComponentFilter,
38 - ComponentFilterType,
39 - ElementType,
40 - ElementTypeComponentFilter,
41 - RegExpComponentFilter,
42 -} from 'src/types';
43 -
44 -export default function ComponentFiltersModalWrapper(_: {||}) {
45 - const store = useContext(StoreContext);
46 - const { profilerStore } = store;
47 -
48 - const { isModalShowing, setIsModalShowing } = useContext(
49 - ComponentFiltersModalContext
50 - );
51 -
52 - // Re-mounting a tree while profiling is in progress might break a lot of assumptions.
53 - // If necessary, we could support this- but it doesn't seem like a necessary use case.
54 - const isProfilingSubscription = useMemo(
55 - () => ({
56 - getCurrentValue: () => profilerStore.isProfiling,
57 - subscribe: (callback: Function) => {
58 - profilerStore.addListener('isProfiling', callback);
59 - return () => profilerStore.removeListener('isProfiling', callback);
60 - },
61 - }),
62 - [profilerStore]
63 - );
64 - const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
65 - if (isProfiling && isModalShowing) {
66 - setIsModalShowing(false);
67 - }
68 -
69 - return isModalShowing ? (
70 - <ComponentFiltersModal
71 - store={store}
72 - setIsModalShowing={setIsModalShowing}
73 - />
74 - ) : null;
75 -}
76 -
77 -type Props = {|
78 - store: Store,
79 - setIsModalShowing: (value: boolean) => void,
80 -|};
81 -
82 -function ComponentFiltersModal({ store, setIsModalShowing }: Props) {
83 - const dismissModal = useCallback(() => setIsModalShowing(false), [
84 - setIsModalShowing,
85 - ]);
86 -
87 - const modalRef = useRef<HTMLDivElement | null>(null);
88 -
89 - useModalDismissSignal(modalRef, dismissModal);
90 -
91 - const {
92 - addFilter,
93 - changeFilterType,
94 - updateFilterValueElementType,
95 - updateFilterValueRegExp,
96 - componentFilters,
97 - removeFilter,
98 - saveFilters,
99 - toggleFilterIsEnabled,
100 - } = useComponentFilters();
101 -
102 - const saveAndClose = useCallback(() => {
103 - saveFilters();
104 - dismissModal();
105 - }, [dismissModal, saveFilters]);
106 -
107 - return (
108 - <div className={styles.Background}>
109 - <div className={styles.Modal} ref={modalRef}>
110 - <div className={styles.LeftRight}>
111 - <div className={styles.Left}>Hide components where...</div>
112 - <div className={styles.Right}>
113 - <Button onClick={addFilter}>
114 - <ButtonIcon className={styles.ButtonIcon} type="add" />
115 - Add filter
116 - </Button>
117 - </div>
118 - </div>
119 - <table className={styles.Table}>
120 - <tbody>
121 - {componentFilters.length === 0 && (
122 - <tr className={styles.TableRow}>
123 - <td className={styles.NoFiltersCell}>
124 - No filters have been added.
125 - </td>
126 - </tr>
127 - )}
128 - {componentFilters.map((componentFilter, index) => (
129 - <tr className={styles.TableRow} key={index}>
130 - <td className={styles.TableCell}>
131 - <Toggle
132 - className={
133 - componentFilter.isValid !== false
134 - ? ''
135 - : styles.InvalidRegExp
136 - }
137 - isChecked={componentFilter.isEnabled}
138 - onChange={isEnabled =>
139 - toggleFilterIsEnabled(componentFilter, isEnabled)
140 - }
141 - title={
142 - componentFilter.isValid === false
143 - ? 'Filter invalid'
144 - : componentFilter.isEnabled
145 - ? 'Filter enabled'
146 - : 'Filter disabled'
147 - }
148 - >
149 - <ToggleIcon
150 - isEnabled={componentFilter.isEnabled}
151 - isValid={
152 - componentFilter.isValid == null ||
153 - componentFilter.isValid === true
154 - }
155 - />
156 - </Toggle>
157 - </td>
158 - <td className={styles.TableCell}>
159 - <select
160 - className={styles.Select}
161 - value={componentFilter.type}
162 - onChange={({ currentTarget }) =>
163 - changeFilterType(
164 - componentFilter,
165 - ((parseInt(
166 - currentTarget.value,
167 - 10
168 - ): any): ComponentFilterType)
169 - )
170 - }
171 - >
172 - <option value={ComponentFilterLocation}>location</option>
173 - <option value={ComponentFilterDisplayName}>name</option>
174 - <option value={ComponentFilterElementType}>type</option>
175 - <option value={ComponentFilterHOC}>hoc</option>
176 - </select>
177 - </td>
178 - <td className={styles.TableCell}>
179 - {componentFilter.type === ComponentFilterElementType &&
180 - 'equals'}
181 - {(componentFilter.type === ComponentFilterLocation ||
182 - componentFilter.type === ComponentFilterDisplayName) &&
183 - 'matches'}
184 - </td>
185 - <td className={styles.TableCell}>
186 - {componentFilter.type === ComponentFilterElementType && (
187 - <select
188 - className={styles.Select}
189 - value={componentFilter.value}
190 - onChange={({ currentTarget }) =>
191 - updateFilterValueElementType(
192 - componentFilter,
193 - ((parseInt(
194 - currentTarget.value,
195 - 10
196 - ): any): ElementType)
197 - )
198 - }
199 - >
200 - <option value={ElementTypeClass}>class</option>
201 - <option value={ElementTypeContext}>context</option>
202 - <option value={ElementTypeEventTarget}>event</option>
203 - <option value={ElementTypeFunction}>function</option>
204 - <option value={ElementTypeForwardRef}>forward ref</option>
205 - <option value={ElementTypeHostComponent}>
206 - host (e.g. &lt;div&gt;)
207 - </option>
208 - <option value={ElementTypeMemo}>memo</option>
209 - <option value={ElementTypeOtherOrUnknown}>other</option>
210 - <option value={ElementTypeProfiler}>profiler</option>
211 - <option value={ElementTypeSuspense}>suspense</option>
212 - </select>
213 - )}
214 - {(componentFilter.type === ComponentFilterLocation ||
215 - componentFilter.type === ComponentFilterDisplayName) && (
216 - <input
217 - className={styles.Input}
218 - type="text"
219 - placeholder="Regular expression"
220 - onChange={({ currentTarget }) =>
221 - updateFilterValueRegExp(
222 - componentFilter,
223 - currentTarget.value
224 - )
225 - }
226 - value={componentFilter.value}
227 - />
228 - )}
229 - </td>
230 - <td className={styles.TableCell}>
231 - <Button
232 - onClick={() => removeFilter(index)}
233 - title="Delete filter"
234 - >
235 - <ButtonIcon type="delete" />
236 - </Button>
237 - </td>
238 - </tr>
239 - ))}
240 - </tbody>
241 - </table>
242 - <div className={styles.LeftRight}>
243 - <div className={styles.Right}>
244 - <Button className={styles.CancelButton} onClick={dismissModal}>
245 - Cancel
246 - </Button>
247 - <Button onClick={saveAndClose}>Save Changes</Button>
248 - </div>
249 - </div>
250 - </div>
251 - </div>
252 - );
253 -}
254 -
255 -type ToggleIconProps = {|
256 - isEnabled: boolean,
257 - isValid: boolean,
258 -|};
259 -function ToggleIcon({ isEnabled, isValid }: ToggleIconProps) {
260 - let className;
261 - if (isValid) {
262 - className = isEnabled ? styles.ToggleOn : styles.ToggleOff;
263 - } else {
264 - className = isEnabled ? styles.ToggleOnInvalid : styles.ToggleOffInvalid;
265 - }
266 - return (
267 - <div className={className}>
268 - <div
269 - className={isEnabled ? styles.ToggleInsideOn : styles.ToggleInsideOff}
270 - />
271 - </div>
272 - );
273 -}
274 -
275 -function useComponentFilters() {
276 - const store = useContext(StoreContext);
277 -
278 - const [componentFilters, setComponentFilters] = useState<
279 - Array<ComponentFilter>
280 - >(() => [...store.componentFilters]);
281 -
282 - const addFilter = useCallback(() => {
283 - setComponentFilters(componentFilters => {
284 - return [
285 - ...componentFilters,
286 - {
287 - type: ComponentFilterElementType,
288 - value: ElementTypeHostComponent,
289 - isEnabled: true,
290 - },
291 - ];
292 - });
293 - }, []);
294 -
295 - const changeFilterType = useCallback(
296 - (componentFilter: ComponentFilter, type: ComponentFilterType) => {
297 - setComponentFilters(componentFilters => {
298 - const cloned: Array<ComponentFilter> = [...componentFilters];
299 - const index = componentFilters.indexOf(componentFilter);
300 - if (index >= 0) {
301 - if (type === ComponentFilterElementType) {
302 - cloned[index] = {
303 - type: ComponentFilterElementType,
304 - isEnabled: componentFilter.isEnabled,
305 - value: ElementTypeHostComponent,
306 - };
307 - } else if (type === ComponentFilterDisplayName) {
308 - cloned[index] = {
309 - type: ComponentFilterDisplayName,
310 - isEnabled: componentFilter.isEnabled,
311 - isValid: true,
312 - value: '',
313 - };
314 - } else if (type === ComponentFilterLocation) {
315 - cloned[index] = {
316 - type: ComponentFilterLocation,
317 - isEnabled: componentFilter.isEnabled,
318 - isValid: true,
319 - value: '',
320 - };
321 - } else if (type === ComponentFilterHOC) {
322 - cloned[index] = {
323 - type: ComponentFilterHOC,
324 - isEnabled: componentFilter.isEnabled,
325 - isValid: true,
326 - };
327 - }
328 - }
329 - return cloned;
330 - });
331 - },
332 - []
333 - );
334 -
335 - const updateFilterValueElementType = useCallback(
336 - (componentFilter: ComponentFilter, value: ElementType) => {
337 - if (componentFilter.type !== ComponentFilterElementType) {
338 - throw Error('Invalid value for element type filter');
339 - }
340 -
341 - setComponentFilters(componentFilters => {
342 - const cloned: Array<ComponentFilter> = [...componentFilters];
343 - if (componentFilter.type === ComponentFilterElementType) {
344 - const index = componentFilters.indexOf(componentFilter);
345 - if (index >= 0) {
346 - cloned[index] = {
347 - ...componentFilter,
348 - value,
349 - };
350 - }
351 - }
352 - return cloned;
353 - });
354 - },
355 - []
356 - );
357 -
358 - const updateFilterValueRegExp = useCallback(
359 - (componentFilter: ComponentFilter, value: string) => {
360 - if (componentFilter.type === ComponentFilterElementType) {
361 - throw Error('Invalid value for element type filter');
362 - }
363 -
364 - setComponentFilters(componentFilters => {
365 - const cloned: Array<ComponentFilter> = [...componentFilters];
366 - if (
367 - componentFilter.type === ComponentFilterDisplayName ||
368 - componentFilter.type === ComponentFilterLocation
369 - ) {
370 - const index = componentFilters.indexOf(componentFilter);
371 - if (index >= 0) {
372 - let isValid = true;
373 - try {
374 - new RegExp(value);
375 - } catch (error) {
376 - isValid = false;
377 - }
378 - cloned[index] = {
379 - ...componentFilter,
380 - isValid,
381 - value,
382 - };
383 - }
384 - }
385 - return cloned;
386 - });
387 - },
388 - []
389 - );
390 -
391 - const removeFilter = useCallback((index: number) => {
392 - setComponentFilters(componentFilters => {
393 - const cloned: Array<ComponentFilter> = [...componentFilters];
394 - cloned.splice(index, 1);
395 - return cloned;
396 - });
397 - }, []);
398 -
399 - const saveFilters = useCallback(() => {
400 - store.componentFilters = [...componentFilters];
401 - }, [componentFilters, store]);
402 -
403 - const toggleFilterIsEnabled = useCallback(
404 - (componentFilter: ComponentFilter, isEnabled: boolean) => {
405 - setComponentFilters(componentFilters => {
406 - const cloned: Array<ComponentFilter> = [...componentFilters];
407 - const index = componentFilters.indexOf(componentFilter);
408 - if (index >= 0) {
409 - if (componentFilter.type === ComponentFilterElementType) {
410 - cloned[index] = {
411 - ...((cloned[index]: any): ElementTypeComponentFilter),
412 - isEnabled,
413 - };
414 - } else if (
415 - componentFilter.type === ComponentFilterDisplayName ||
416 - componentFilter.type === ComponentFilterLocation
417 - ) {
418 - cloned[index] = {
419 - ...((cloned[index]: any): RegExpComponentFilter),
420 - isEnabled,
421 - };
422 - } else if (componentFilter.type === ComponentFilterHOC) {
423 - cloned[index] = {
424 - ...((cloned[index]: any): BooleanComponentFilter),
425 - isEnabled,
426 - };
427 - }
428 - }
429 - return cloned;
430 - });
431 - },
432 - []
433 - );
434 -
435 - return {
436 - addFilter,
437 - changeFilterType,
438 - componentFilters,
439 - removeFilter,
440 - saveFilters,
441 - toggleFilterIsEnabled,
442 - updateFilterValueElementType,
443 - updateFilterValueRegExp,
444 - };
445 -}
src/devtools/views/Components/ComponentFiltersModalContext.js deleted
-37
@@ -1,37 +0,0 @@
1 -// @flow
2 -
3 -import React, { createContext, useMemo, useState } from 'react';
4 -
5 -type Context = {|
6 - isModalShowing: boolean,
7 - setIsModalShowing: (value: boolean) => void,
8 -|};
9 -
10 -const ComponentFiltersModalContext = createContext<Context>(
11 - ((null: any): Context)
12 -);
13 -ComponentFiltersModalContext.displayName = 'ComponentFiltersModalContext';
14 -
15 -type Props = {|
16 - children: React$Node,
17 -|};
18 -
19 -function ComponentFiltersModalContextController({ children }: Props) {
20 - const [isModalShowing, setIsModalShowing] = useState(false);
21 -
22 - const value = useMemo(
23 - () => ({
24 - isModalShowing,
25 - setIsModalShowing,
26 - }),
27 - [isModalShowing]
28 - );
29 -
30 - return (
31 - <ComponentFiltersModalContext.Provider value={value}>
32 - {children}
33 - </ComponentFiltersModalContext.Provider>
34 - );
35 -}
36 -
37 -export { ComponentFiltersModalContext, ComponentFiltersModalContextController };
src/devtools/views/Components/Components.js
+19 -14
@@ -7,27 +7,32 @@ import { InspectedElementContextController } from './InspectedElementContext';
7 import { OwnersListContextController } from './OwnersListContext';
8 import portaledContent from '../portaledContent';
9 import { ModalDialog } from '../ModalDialog';
10 +import SettingsModal from 'src/devtools/views/Settings/SettingsModal';
11 +import { SettingsModalContextController } from 'src/devtools/views/Settings/SettingsModalContext';
12
13 import styles from './Components.css';
14
15 function Components(_: {||}) {
16 // TODO Flex wrappers below should be user resizable.
17 return (
16 - <OwnersListContextController>
17 - <InspectedElementContextController>
18 - <div className={styles.Components}>
19 - <div className={styles.TreeWrapper}>
20 - <Tree />
18 + <SettingsModalContextController>
19 + <OwnersListContextController>
20 + <InspectedElementContextController>
21 + <div className={styles.Components}>
22 + <div className={styles.TreeWrapper}>
23 + <Tree />
24 + </div>
25 + <div className={styles.SelectedElementWrapper}>
26 + <Suspense fallback={<Loading />}>
27 + <SelectedElement />
28 + </Suspense>
29 + </div>
30 + <ModalDialog />
31 + <SettingsModal defaultTabID="components" />
32 </div>
22 - <div className={styles.SelectedElementWrapper}>
23 - <Suspense fallback={<Loading />}>
24 - <SelectedElement />
25 - </Suspense>
26 - </div>
27 - <ModalDialog />
28 - </div>
29 - </InspectedElementContextController>
30 - </OwnersListContextController>
33 + </InspectedElementContextController>
34 + </OwnersListContextController>
35 + </SettingsModalContextController>
36 );
37 }
38
src/devtools/views/Components/ToggleComponentFiltersModalButton.css deleted
-8
@@ -1,8 +0,0 @@
1 -.Wrapper {
2 - display: flex;
3 -}
4 -
5 -.Label {
6 - font-size: var(--font-size-sans-small);
7 - font-family: var(--font-family-sans);
8 -}
src/devtools/views/Components/ToggleComponentFiltersModalButton.js deleted
-80
@@ -1,80 +0,0 @@
1 -// @flow
2 -
3 -import React, { useContext, useMemo } from 'react';
4 -import { useSubscription } from '../hooks';
5 -import { ComponentFiltersModalContext } from './ComponentFiltersModalContext';
6 -import { StoreContext } from '../context';
7 -import Toggle from '../Toggle';
8 -import ButtonIcon from '../ButtonIcon';
9 -import Store from 'src/devtools/store';
10 -
11 -import styles from './ToggleComponentFiltersModalButton.css';
12 -
13 -import type { ComponentFilter } from 'src/types';
14 -
15 -export default function ToggleComponentFiltersModalButton() {
16 - const store = useContext(StoreContext);
17 - const { profilerStore } = store;
18 -
19 - const { isModalShowing, setIsModalShowing } = useContext(
20 - ComponentFiltersModalContext
21 - );
22 -
23 - // Re-mounting a tree while profiling is in progress might break a lot of assumptions.
24 - // If necessary, we could support this- but it doesn't seem like a necessary use case.
25 - const isProfilingSubscription = useMemo(
26 - () => ({
27 - getCurrentValue: () => profilerStore.isProfiling,
28 - subscribe: (callback: Function) => {
29 - profilerStore.addListener('isProfiling', callback);
30 - return () => profilerStore.removeListener('isProfiling', callback);
31 - },
32 - }),
33 - [profilerStore]
34 - );
35 - const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
36 -
37 - const componentFiltersSubscription = useMemo(
38 - () => ({
39 - getCurrentValue: () => store.componentFilters,
40 - subscribe: (callback: Function) => {
41 - store.addListener('componentFilters', callback);
42 - return () => store.removeListener('componentFilters', callback);
43 - },
44 - }),
45 - [store]
46 - );
47 - const componentFilters = useSubscription<Array<ComponentFilter>, Store>(
48 - componentFiltersSubscription
49 - );
50 -
51 - const enabledCount = useMemo(
52 - () =>
53 - componentFilters.reduce(
54 - (count, componentFilter) =>
55 - componentFilter.isEnabled ? count + 1 : count,
56 - 0
57 - ),
58 - [componentFilters]
59 - );
60 -
61 - return (
62 - <Toggle
63 - isChecked={isModalShowing}
64 - isDisabled={isProfiling}
65 - onChange={setIsModalShowing}
66 - title={
67 - enabledCount > 0
68 - ? `Filter preferences (${enabledCount} enabled)`
69 - : 'Filter preferences'
70 - }
71 - >
72 - <div className={styles.Wrapper}>
73 - <ButtonIcon type="filter" />
74 - {enabledCount > 0 && (
75 - <span className={styles.Label}>{enabledCount}</span>
76 - )}
77 - </div>
78 - </Toggle>
79 - );
80 -}
src/devtools/views/Components/Tree.js
+40 -45
@@ -19,9 +19,7 @@ import ElementView from './Element';
19 import InspectHostNodesToggle from './InspectHostNodesToggle';
20 import OwnersStack from './OwnersStack';
21 import SearchInput from './SearchInput';
22 -import { ComponentFiltersModalContextController } from './ComponentFiltersModalContext';
23 -import ToggleComponentFiltersModalButton from './ToggleComponentFiltersModalButton';
24 -import ComponentFiltersModal from './ComponentFiltersModal';
22 +import SettingsModalContextToggle from 'src/devtools/views/Settings/SettingsModalContextToggle';
23 import SelectedTreeHighlight from './SelectedTreeHighlight';
24 import TreeFocusedContext from './TreeFocusedContext';
25
@@ -284,49 +282,46 @@ export default function Tree(props: Props) {
282
283 return (
284 <TreeFocusedContext.Provider value={treeFocused}>
287 - <ComponentFiltersModalContextController>
288 - <div className={styles.Tree} ref={treeRef}>
289 - <div className={styles.SearchInput}>
290 - <InspectHostNodesToggle />
291 - <div className={styles.VRule} />
292 - <Suspense fallback={<Loading />}>
293 - {ownerID !== null ? <OwnersStack /> : <SearchInput />}
294 - </Suspense>
295 - <div className={styles.VRule} />
296 - <ToggleComponentFiltersModalButton />
297 - </div>
298 - <div
299 - className={styles.AutoSizerWrapper}
300 - onBlur={handleBlur}
301 - onFocus={handleFocus}
302 - onKeyPress={handleKeyPress}
303 - onMouseMove={handleMouseMove}
304 - onMouseLeave={handleMouseLeave}
305 - ref={focusTargetRef}
306 - tabIndex={0}
307 - >
308 - <AutoSizer>
309 - {({ height, width }) => (
310 - // $FlowFixMe https://github.com/facebook/flow/issues/7341
311 - <FixedSizeList
312 - className={styles.List}
313 - height={height}
314 - innerElementType={InnerElementType}
315 - itemCount={numElements}
316 - itemData={itemData}
317 - itemKey={itemKey}
318 - itemSize={lineHeight}
319 - ref={listRef}
320 - width={width}
321 - >
322 - {ElementView}
323 - </FixedSizeList>
324 - )}
325 - </AutoSizer>
326 - </div>
327 - <ComponentFiltersModal />
285 + <div className={styles.Tree} ref={treeRef}>
286 + <div className={styles.SearchInput}>
287 + <InspectHostNodesToggle />
288 + <div className={styles.VRule} />
289 + <Suspense fallback={<Loading />}>
290 + {ownerID !== null ? <OwnersStack /> : <SearchInput />}
291 + </Suspense>
292 + <div className={styles.VRule} />
293 + <SettingsModalContextToggle />
294 </div>
329 - </ComponentFiltersModalContextController>
295 + <div
296 + className={styles.AutoSizerWrapper}
297 + onBlur={handleBlur}
298 + onFocus={handleFocus}
299 + onKeyPress={handleKeyPress}
300 + onMouseMove={handleMouseMove}
301 + onMouseLeave={handleMouseLeave}
302 + ref={focusTargetRef}
303 + tabIndex={0}
304 + >
305 + <AutoSizer>
306 + {({ height, width }) => (
307 + // $FlowFixMe https://github.com/facebook/flow/issues/7341
308 + <FixedSizeList
309 + className={styles.List}
310 + height={height}
311 + innerElementType={InnerElementType}
312 + itemCount={numElements}
313 + itemData={itemData}
314 + itemKey={itemKey}
315 + itemSize={lineHeight}
316 + ref={listRef}
317 + width={width}
318 + >
319 + {ElementView}
320 + </FixedSizeList>
321 + )}
322 + </AutoSizer>
323 + </div>
324 + </div>
325 </TreeFocusedContext.Provider>
326 );
327 }
src/devtools/views/DevTools.js
+1 -14
@@ -11,7 +11,6 @@ import Store from '../store';
11 import { BridgeContext, StoreContext } from './context';
12 import Components from './Components/Components';
13 import Profiler from './Profiler/Profiler';
14 -import Settings from './Settings/Settings';
14 import TabBar from './TabBar';
15 import { SettingsContextController } from './Settings/SettingsContext';
16 import { TreeContextController } from './Components/TreeContext';
@@ -63,14 +62,8 @@ const profilerTab = {
62 label: 'Profiler',
63 title: 'React Profiler',
64 };
66 -const settingsTab = {
67 - id: ('settings': TabID),
68 - icon: 'settings',
69 - label: 'Settings',
70 - title: 'React Settings',
71 -};
65
73 -const tabs = [componentsTab, profilerTab, settingsTab];
66 +const tabs = [componentsTab, profilerTab];
67
68 export default function DevTools({
69 bridge,
@@ -132,12 +125,6 @@ export default function DevTools({
125 >
126 <Profiler portalContainer={profilerPortalContainer} />
127 </div>
135 - <div
136 - className={styles.TabContent}
137 - hidden={tab !== 'settings'}
138 - >
139 - <Settings portalContainer={settingsPortalContainer} />
140 - </div>
128 </div>
129 </ProfilerContextController>
130 </TreeContextController>
src/devtools/views/Profiler/CommitFilterModal.css deleted
-31
@@ -1,31 +0,0 @@
1 -.Background {
2 - position: absolute;
3 - width: 100%;
4 - height: 100%;
5 - background-color: var(--color-modal-background);
6 -}
7 -
8 -.Modal {
9 - position: absolute;
10 - top: 5px;
11 - right: 5px;
12 - display: inline-block;
13 - background-color: var(--color-background);
14 - padding: 1rem;
15 - border: 1px solid var(--color-border);
16 - border-radius: 0.25rem;
17 -}
18 -
19 -.Input {
20 - width: 40px;
21 - background: none;
22 - border: 1px solid transparent;
23 - color: var(--color-attribute-editable-value);
24 - border-radius: 0.125rem;
25 - font-family: var(--font-family-monospace);
26 - font-size: var(--font-size-monospace-normal);
27 -}
28 -.Input:focus {
29 - background-color: var(--color-button-background-focus);
30 - outline: none;
31 -}
src/devtools/views/Profiler/CommitFilterModal.js deleted
-89
@@ -1,89 +0,0 @@
1 -// @flow
2 -
3 -import React, { useCallback, useContext, useEffect, useRef } from 'react';
4 -import { CommitFilterModalContext } from './CommitFilterModalContext';
5 -import { ProfilerContext } from './ProfilerContext';
6 -import { useModalDismissSignal } from '../hooks';
7 -
8 -import styles from './CommitFilterModal.css';
9 -
10 -export default function FilterModal(_: {||}) {
11 - const { isModalShowing } = useContext(CommitFilterModalContext);
12 -
13 - if (!isModalShowing) {
14 - return null;
15 - }
16 -
17 - return <FilterModalImpl />;
18 -}
19 -
20 -function FilterModalImpl(_: {||}) {
21 - const {
22 - isCommitFilterEnabled,
23 - minCommitDuration,
24 - setIsCommitFilterEnabled,
25 - setMinCommitDuration,
26 - } = useContext(ProfilerContext);
27 -
28 - const { setIsModalShowing } = useContext(CommitFilterModalContext);
29 - const dismissModal = useCallback(() => setIsModalShowing(false), [
30 - setIsModalShowing,
31 - ]);
32 -
33 - const handleNumberChange = useCallback(
34 - (event: SyntheticEvent<HTMLInputElement>) => {
35 - const newValue = parseFloat(event.currentTarget.value);
36 - setMinCommitDuration(
37 - Number.isNaN(newValue) || newValue <= 0 ? 0 : newValue
38 - );
39 - },
40 - [setMinCommitDuration]
41 - );
42 -
43 - const handleEnabledChange = useCallback(
44 - (event: SyntheticEvent<HTMLInputElement>) => {
45 - const checked = event.currentTarget.checked;
46 - setIsCommitFilterEnabled(checked);
47 - if (checked) {
48 - if (inputRef.current !== null) {
49 - inputRef.current.focus();
50 - }
51 - }
52 - },
53 - [setIsCommitFilterEnabled]
54 - );
55 -
56 - const inputRef = useRef<HTMLInputElement | null>(null);
57 - const modalRef = useRef<HTMLDivElement | null>(null);
58 -
59 - useModalDismissSignal(modalRef, dismissModal);
60 -
61 - useEffect(() => {
62 - if (inputRef.current !== null) {
63 - inputRef.current.focus();
64 - }
65 - }, [inputRef]);
66 -
67 - return (
68 - <div className={styles.Background}>
69 - <div className={styles.Modal} ref={modalRef}>
70 - <label>
71 - <input
72 - checked={isCommitFilterEnabled}
73 - onChange={handleEnabledChange}
74 - type="checkbox"
75 - />{' '}
76 - Hide commits below
77 - </label>{' '}
78 - <input
79 - className={styles.Input}
80 - onChange={handleNumberChange}
81 - ref={inputRef}
82 - type="number"
83 - value={minCommitDuration}
84 - />{' '}
85 - (ms)
86 - </div>
87 - </div>
88 - );
89 -}
src/devtools/views/Profiler/CommitFilterModalContext.js deleted
-35
@@ -1,35 +0,0 @@
1 -// @flow
2 -
3 -import React, { createContext, useMemo, useState } from 'react';
4 -
5 -type Context = {|
6 - isModalShowing: boolean,
7 - setIsModalShowing: (value: boolean) => void,
8 -|};
9 -
10 -const CommitFilterModalContext = createContext<Context>(((null: any): Context));
11 -CommitFilterModalContext.displayName = 'CommitFilterModalContext';
12 -
13 -type Props = {|
14 - children: React$Node,
15 -|};
16 -
17 -function CommitFilterModalContextController({ children }: Props) {
18 - const [isModalShowing, setIsModalShowing] = useState(false);
19 -
20 - const value = useMemo(
21 - () => ({
22 - isModalShowing,
23 - setIsModalShowing,
24 - }),
25 - [isModalShowing]
26 - );
27 -
28 - return (
29 - <CommitFilterModalContext.Provider value={value}>
30 - {children}
31 - </CommitFilterModalContext.Provider>
32 - );
33 -}
34 -
35 -export { CommitFilterModalContext, CommitFilterModalContextController };
src/devtools/views/Profiler/NoCommitData.js
+1 -2
@@ -1,7 +1,6 @@
1 // @flow
2
3 import React from 'react';
4 -import ToggleCommitFilterModalButton from './ToggleCommitFilterModalButton';
4
5 import styles from './NoCommitData.css';
6
@@ -12,7 +11,7 @@ export default function NoCommitData(_: {||}) {
11 There is no data matching the current filter criteria.
12 </div>
13 <div className={styles.FilterMessage}>
15 - Try adjusting the commit filter <ToggleCommitFilterModalButton />
14 + Try adjusting the commit filter in Profiler settings.
15 </div>
16 </div>
17 );
src/devtools/views/Profiler/Profiler.css
+2 -1
@@ -1,6 +1,7 @@
1 .Profiler {
2 width: 100%;
3 height: 100%;
4 + position: relative;
5 display: flex;
6 flex-direction: row;
7 align-items: stretch;
@@ -57,7 +58,7 @@
58
59 .Toolbar {
60 height: 2.25rem;
60 - padding: 0 0.5rem;
61 + padding: 0 0.25rem;
62 flex: 0 0 auto;
63 display: flex;
64 align-items: center;
src/devtools/views/Profiler/Profiler.js
+7 -7
@@ -1,14 +1,12 @@
1 // @flow
2
3 import React, { Fragment, useContext } from 'react';
4 -import { CommitFilterModalContextController } from './CommitFilterModalContext';
4 import { ModalDialog } from '../ModalDialog';
5 import { ProfilerContext } from './ProfilerContext';
6 import TabBar from '../TabBar';
7 import ClearProfilingDataButton from './ClearProfilingDataButton';
8 import CommitFlamegraph from './CommitFlamegraph';
9 import CommitRanked from './CommitRanked';
11 -import CommitFilterModal from './CommitFilterModal';
10 import Interactions from './Interactions';
11 import RootSelector from './RootSelector';
12 import RecordToggle from './RecordToggle';
@@ -18,7 +16,9 @@ import SnapshotSelector from './SnapshotSelector';
16 import SidebarCommitInfo from './SidebarCommitInfo';
17 import SidebarInteractions from './SidebarInteractions';
18 import SidebarSelectedFiberInfo from './SidebarSelectedFiberInfo';
21 -import ToggleCommitFilterModalButton from './ToggleCommitFilterModalButton';
19 +import SettingsModal from 'src/devtools/views/Settings/SettingsModal';
20 +import SettingsModalContextToggle from 'src/devtools/views/Settings/SettingsModalContextToggle';
21 +import { SettingsModalContextController } from 'src/devtools/views/Settings/SettingsModalContext';
22 import portaledContent from '../portaledContent';
23
24 import styles from './Profiler.css';
@@ -79,7 +79,7 @@ function Profiler(_: {||}) {
79 }
80
81 return (
82 - <CommitFilterModalContextController>
82 + <SettingsModalContextController>
83 <div className={styles.Profiler}>
84 <div className={styles.LeftColumn}>
85 <div className={styles.Toolbar}>
@@ -97,7 +97,7 @@ function Profiler(_: {||}) {
97 />
98 <RootSelector />
99 <div className={styles.Spacer} />
100 - <ToggleCommitFilterModalButton />
100 + <SettingsModalContextToggle />
101 {didRecordCommits && (
102 <Fragment>
103 <div className={styles.VRule} />
@@ -107,13 +107,13 @@ function Profiler(_: {||}) {
107 </div>
108 <div className={styles.Content}>
109 {view}
110 - <CommitFilterModal />
110 <ModalDialog />
111 </div>
112 </div>
113 <div className={styles.RightColumn}>{sidebar}</div>
114 + <SettingsModal defaultTabID="profiler" />
115 </div>
116 - </CommitFilterModalContextController>
116 + </SettingsModalContextController>
117 );
118 }
119
src/devtools/views/Profiler/ToggleCommitFilterModalButton.js deleted
-20
@@ -1,20 +0,0 @@
1 -// @flow
2 -
3 -import React, { useCallback, useContext } from 'react';
4 -import { CommitFilterModalContext } from './CommitFilterModalContext';
5 -import Button from '../Button';
6 -import ButtonIcon from '../ButtonIcon';
7 -
8 -export default function ToggleCommitFilterModalButton() {
9 - const { setIsModalShowing } = useContext(CommitFilterModalContext);
10 -
11 - const showFilterModal = useCallback(() => setIsModalShowing(true), [
12 - setIsModalShowing,
13 - ]);
14 -
15 - return (
16 - <Button onClick={showFilterModal} title="Filter commits by duration">
17 - <ButtonIcon type="filter" />
18 - </Button>
19 - );
20 -}
src/devtools/views/Settings/ComponentsSettings.js new
+417
@@ -0,0 +1,417 @@
1 +// @flow
2 +
3 +import React, {
4 + useCallback,
5 + useContext,
6 + useEffect,
7 + useMemo,
8 + useRef,
9 + useState,
10 +} from 'react';
11 +import { useSubscription } from '../hooks';
12 +import { StoreContext } from '../context';
13 +import Store from 'src/devtools/store';
14 +import Button from '../Button';
15 +import ButtonIcon from '../ButtonIcon';
16 +import Toggle from '../Toggle';
17 +import { shallowDiffers } from 'src/utils';
18 +import {
19 + ComponentFilterDisplayName,
20 + ComponentFilterElementType,
21 + ComponentFilterHOC,
22 + ComponentFilterLocation,
23 + ElementTypeClass,
24 + ElementTypeContext,
25 + ElementTypeEventTarget,
26 + ElementTypeFunction,
27 + ElementTypeForwardRef,
28 + ElementTypeHostComponent,
29 + ElementTypeMemo,
30 + ElementTypeOtherOrUnknown,
31 + ElementTypeProfiler,
32 + ElementTypeSuspense,
33 +} from 'src/types';
34 +
35 +import styles from './SettingsShared.css';
36 +
37 +import type {
38 + BooleanComponentFilter,
39 + ComponentFilter,
40 + ComponentFilterType,
41 + ElementType,
42 + ElementTypeComponentFilter,
43 + RegExpComponentFilter,
44 +} from 'src/types';
45 +
46 +export default function ComponentsSettings(_: {||}) {
47 + const store = useContext(StoreContext);
48 +
49 + const collapseNodesByDefaultSubscription = useMemo(
50 + () => ({
51 + getCurrentValue: () => store.collapseNodesByDefault,
52 + subscribe: (callback: Function) => {
53 + store.addListener('collapseNodesByDefault', callback);
54 + return () => store.removeListener('collapseNodesByDefault', callback);
55 + },
56 + }),
57 + [store]
58 + );
59 + const collapseNodesByDefault = useSubscription<boolean, Store>(
60 + collapseNodesByDefaultSubscription
61 + );
62 +
63 + const updateCollapseNodesByDefault = useCallback(
64 + ({ currentTarget }) => {
65 + store.collapseNodesByDefault = currentTarget.checked;
66 + },
67 + [store]
68 + );
69 +
70 + const [componentFilters, setComponentFilters] = useState<
71 + Array<ComponentFilter>
72 + >(() => [...store.componentFilters]);
73 +
74 + const addFilter = useCallback(() => {
75 + setComponentFilters(componentFilters => {
76 + return [
77 + ...componentFilters,
78 + {
79 + type: ComponentFilterElementType,
80 + value: ElementTypeHostComponent,
81 + isEnabled: true,
82 + },
83 + ];
84 + });
85 + }, []);
86 +
87 + const changeFilterType = useCallback(
88 + (componentFilter: ComponentFilter, type: ComponentFilterType) => {
89 + setComponentFilters(componentFilters => {
90 + const cloned: Array<ComponentFilter> = [...componentFilters];
91 + const index = componentFilters.indexOf(componentFilter);
92 + if (index >= 0) {
93 + if (type === ComponentFilterElementType) {
94 + cloned[index] = {
95 + type: ComponentFilterElementType,
96 + isEnabled: componentFilter.isEnabled,
97 + value: ElementTypeHostComponent,
98 + };
99 + } else if (type === ComponentFilterDisplayName) {
100 + cloned[index] = {
101 + type: ComponentFilterDisplayName,
102 + isEnabled: componentFilter.isEnabled,
103 + isValid: true,
104 + value: '',
105 + };
106 + } else if (type === ComponentFilterLocation) {
107 + cloned[index] = {
108 + type: ComponentFilterLocation,
109 + isEnabled: componentFilter.isEnabled,
110 + isValid: true,
111 + value: '',
112 + };
113 + } else if (type === ComponentFilterHOC) {
114 + cloned[index] = {
115 + type: ComponentFilterHOC,
116 + isEnabled: componentFilter.isEnabled,
117 + isValid: true,
118 + };
119 + }
120 + }
121 + return cloned;
122 + });
123 + },
124 + []
125 + );
126 +
127 + const updateFilterValueElementType = useCallback(
128 + (componentFilter: ComponentFilter, value: ElementType) => {
129 + if (componentFilter.type !== ComponentFilterElementType) {
130 + throw Error('Invalid value for element type filter');
131 + }
132 +
133 + setComponentFilters(componentFilters => {
134 + const cloned: Array<ComponentFilter> = [...componentFilters];
135 + if (componentFilter.type === ComponentFilterElementType) {
136 + const index = componentFilters.indexOf(componentFilter);
137 + if (index >= 0) {
138 + cloned[index] = {
139 + ...componentFilter,
140 + value,
141 + };
142 + }
143 + }
144 + return cloned;
145 + });
146 + },
147 + []
148 + );
149 +
150 + const updateFilterValueRegExp = useCallback(
151 + (componentFilter: ComponentFilter, value: string) => {
152 + if (componentFilter.type === ComponentFilterElementType) {
153 + throw Error('Invalid value for element type filter');
154 + }
155 +
156 + setComponentFilters(componentFilters => {
157 + const cloned: Array<ComponentFilter> = [...componentFilters];
158 + if (
159 + componentFilter.type === ComponentFilterDisplayName ||
160 + componentFilter.type === ComponentFilterLocation
161 + ) {
162 + const index = componentFilters.indexOf(componentFilter);
163 + if (index >= 0) {
164 + let isValid = true;
165 + try {
166 + new RegExp(value);
167 + } catch (error) {
168 + isValid = false;
169 + }
170 + cloned[index] = {
171 + ...componentFilter,
172 + isValid,
173 + value,
174 + };
175 + }
176 + }
177 + return cloned;
178 + });
179 + },
180 + []
181 + );
182 +
183 + const removeFilter = useCallback((index: number) => {
184 + setComponentFilters(componentFilters => {
185 + const cloned: Array<ComponentFilter> = [...componentFilters];
186 + cloned.splice(index, 1);
187 + return cloned;
188 + });
189 + }, []);
190 +
191 + const toggleFilterIsEnabled = useCallback(
192 + (componentFilter: ComponentFilter, isEnabled: boolean) => {
193 + setComponentFilters(componentFilters => {
194 + const cloned: Array<ComponentFilter> = [...componentFilters];
195 + const index = componentFilters.indexOf(componentFilter);
196 + if (index >= 0) {
197 + if (componentFilter.type === ComponentFilterElementType) {
198 + cloned[index] = {
199 + ...((cloned[index]: any): ElementTypeComponentFilter),
200 + isEnabled,
201 + };
202 + } else if (
203 + componentFilter.type === ComponentFilterDisplayName ||
204 + componentFilter.type === ComponentFilterLocation
205 + ) {
206 + cloned[index] = {
207 + ...((cloned[index]: any): RegExpComponentFilter),
208 + isEnabled,
209 + };
210 + } else if (componentFilter.type === ComponentFilterHOC) {
211 + cloned[index] = {
212 + ...((cloned[index]: any): BooleanComponentFilter),
213 + isEnabled,
214 + };
215 + }
216 + }
217 + return cloned;
218 + });
219 + },
220 + []
221 + );
222 +
223 + // Filter updates are expensive to apply (since they impact the entire tree).
224 + // Only apply them on unmount, and only if they've actually changed.
225 + const componentFiltersRef = useRef<Array<ComponentFilter>>(componentFilters);
226 + useEffect(() => {
227 + console.log('update componentFilters ref:', componentFilters);
228 + componentFiltersRef.current = componentFilters;
229 + return () => {};
230 + }, [componentFilters]);
231 + useEffect(
232 + () => () => {
233 + const prevComponentFilters = store.componentFilters;
234 + const nextComponentFilters = componentFiltersRef.current;
235 +
236 + let haveFiltersChanged =
237 + prevComponentFilters.length !== nextComponentFilters.length;
238 + if (!haveFiltersChanged) {
239 + for (let i = 0; i < nextComponentFilters.length; i++) {
240 + if (
241 + shallowDiffers(prevComponentFilters[i], nextComponentFilters[i])
242 + ) {
243 + haveFiltersChanged = true;
244 + break;
245 + }
246 + }
247 + }
248 +
249 + console.log('haveFiltersChanged?', haveFiltersChanged);
250 + if (haveFiltersChanged) {
251 + store.componentFilters = [...nextComponentFilters];
252 + }
253 + },
254 + [store]
255 + );
256 +
257 + return (
258 + <div className={styles.Settings}>
259 + <label className={styles.Setting}>
260 + <input
261 + type="checkbox"
262 + checked={collapseNodesByDefault}
263 + onChange={updateCollapseNodesByDefault}
264 + />{' '}
265 + Collapse newly added components by default
266 + </label>
267 +
268 + <div className={styles.Header}>Hide components where...</div>
269 +
270 + <table className={styles.Table}>
271 + <tbody>
272 + {componentFilters.length === 0 && (
273 + <tr className={styles.TableRow}>
274 + <td className={styles.NoFiltersCell}>
275 + No filters have been added.
276 + </td>
277 + </tr>
278 + )}
279 + {componentFilters.map((componentFilter, index) => (
280 + <tr className={styles.TableRow} key={index}>
281 + <td className={styles.TableCell}>
282 + <Toggle
283 + className={
284 + componentFilter.isValid !== false
285 + ? ''
286 + : styles.InvalidRegExp
287 + }
288 + isChecked={componentFilter.isEnabled}
289 + onChange={isEnabled =>
290 + toggleFilterIsEnabled(componentFilter, isEnabled)
291 + }
292 + title={
293 + componentFilter.isValid === false
294 + ? 'Filter invalid'
295 + : componentFilter.isEnabled
296 + ? 'Filter enabled'
297 + : 'Filter disabled'
298 + }
299 + >
300 + <ToggleIcon
301 + isEnabled={componentFilter.isEnabled}
302 + isValid={
303 + componentFilter.isValid == null ||
304 + componentFilter.isValid === true
305 + }
306 + />
307 + </Toggle>
308 + </td>
309 + <td className={styles.TableCell}>
310 + <select
311 + className={styles.Select}
312 + value={componentFilter.type}
313 + onChange={({ currentTarget }) =>
314 + changeFilterType(
315 + componentFilter,
316 + ((parseInt(
317 + currentTarget.value,
318 + 10
319 + ): any): ComponentFilterType)
320 + )
321 + }
322 + >
323 + <option value={ComponentFilterLocation}>location</option>
324 + <option value={ComponentFilterDisplayName}>name</option>
325 + <option value={ComponentFilterElementType}>type</option>
326 + <option value={ComponentFilterHOC}>hoc</option>
327 + </select>
328 + </td>
329 + <td className={styles.TableCell}>
330 + {componentFilter.type === ComponentFilterElementType &&
331 + 'equals'}
332 + {(componentFilter.type === ComponentFilterLocation ||
333 + componentFilter.type === ComponentFilterDisplayName) &&
334 + 'matches'}
335 + </td>
336 + <td className={styles.TableCell}>
337 + {componentFilter.type === ComponentFilterElementType && (
338 + <select
339 + className={styles.Select}
340 + value={componentFilter.value}
341 + onChange={({ currentTarget }) =>
342 + updateFilterValueElementType(
343 + componentFilter,
344 + ((parseInt(currentTarget.value, 10): any): ElementType)
345 + )
346 + }
347 + >
348 + <option value={ElementTypeClass}>class</option>
349 + <option value={ElementTypeContext}>context</option>
350 + <option value={ElementTypeEventTarget}>event</option>
351 + <option value={ElementTypeFunction}>function</option>
352 + <option value={ElementTypeForwardRef}>forward ref</option>
353 + <option value={ElementTypeHostComponent}>
354 + host (e.g. &lt;div&gt;)
355 + </option>
356 + <option value={ElementTypeMemo}>memo</option>
357 + <option value={ElementTypeOtherOrUnknown}>other</option>
358 + <option value={ElementTypeProfiler}>profiler</option>
359 + <option value={ElementTypeSuspense}>suspense</option>
360 + </select>
361 + )}
362 + {(componentFilter.type === ComponentFilterLocation ||
363 + componentFilter.type === ComponentFilterDisplayName) && (
364 + <input
365 + className={styles.Input}
366 + type="text"
367 + placeholder="Regular expression"
368 + onChange={({ currentTarget }) =>
369 + updateFilterValueRegExp(
370 + componentFilter,
371 + currentTarget.value
372 + )
373 + }
374 + value={componentFilter.value}
375 + />
376 + )}
377 + </td>
378 + <td className={styles.TableCell}>
379 + <Button
380 + onClick={() => removeFilter(index)}
381 + title="Delete filter"
382 + >
383 + <ButtonIcon type="delete" />
384 + </Button>
385 + </td>
386 + </tr>
387 + ))}
388 + </tbody>
389 + </table>
390 +
391 + <Button onClick={addFilter}>
392 + <ButtonIcon className={styles.ButtonIcon} type="add" />
393 + Add filter
394 + </Button>
395 + </div>
396 + );
397 +}
398 +
399 +type ToggleIconProps = {|
400 + isEnabled: boolean,
401 + isValid: boolean,
402 +|};
403 +function ToggleIcon({ isEnabled, isValid }: ToggleIconProps) {
404 + let className;
405 + if (isValid) {
406 + className = isEnabled ? styles.ToggleOn : styles.ToggleOff;
407 + } else {
408 + className = isEnabled ? styles.ToggleOnInvalid : styles.ToggleOffInvalid;
409 + }
410 + return (
411 + <div className={className}>
412 + <div
413 + className={isEnabled ? styles.ToggleInsideOn : styles.ToggleInsideOff}
414 + />
415 + </div>
416 + );
417 +}
src/devtools/views/Settings/GeneralSettings.js new
+50
@@ -0,0 +1,50 @@
1 +// @flow
2 +
3 +import React, { useCallback, useContext } from 'react';
4 +import { SettingsContext } from './SettingsContext';
5 +
6 +import styles from './SettingsShared.css';
7 +
8 +export default function GeneralSettings(_: {||}) {
9 + const { displayDensity, setDisplayDensity, theme, setTheme } = useContext(
10 + SettingsContext
11 + );
12 +
13 + const updateDisplayDensity = useCallback(
14 + ({ currentTarget }) => {
15 + setDisplayDensity(currentTarget.value);
16 + },
17 + [setDisplayDensity]
18 + );
19 + const updateTheme = useCallback(
20 + ({ currentTarget }) => {
21 + setTheme(currentTarget.value);
22 + },
23 + [setTheme]
24 + );
25 +
26 + return (
27 + <div className={styles.Settings}>
28 + <div className={styles.Setting}>
29 + <div className={styles.RadioLabel}>Theme</div>
30 + <select className={styles.Select} value={theme} onChange={updateTheme}>
31 + <option value="auto">Auto</option>
32 + <option value="light">Light</option>
33 + <option value="dark">Dark</option>
34 + </select>
35 + </div>
36 +
37 + <div className={styles.Setting}>
38 + <div className={styles.RadioLabel}>Display density</div>
39 + <select
40 + className={styles.Select}
41 + value={displayDensity}
42 + onChange={updateDisplayDensity}
43 + >
44 + <option value="compact">Compact</option>
45 + <option value="comfortable">Comfortable</option>
46 + </select>
47 + </div>
48 + </div>
49 + );
50 +}
src/devtools/views/Settings/ProfilerSettings.js new
+144
@@ -0,0 +1,144 @@
1 +// @flow
2 +
3 +import React, {
4 + Fragment,
5 + useCallback,
6 + useContext,
7 + useMemo,
8 + useRef,
9 +} from 'react';
10 +import { useSubscription } from '../hooks';
11 +import { StoreContext } from '../context';
12 +import { ProfilerContext } from 'src/devtools/views/Profiler/ProfilerContext';
13 +import Store from 'src/devtools/store';
14 +
15 +import styles from './SettingsShared.css';
16 +
17 +export default function ProfilerSettings(_: {||}) {
18 + const {
19 + isCommitFilterEnabled,
20 + minCommitDuration,
21 + setIsCommitFilterEnabled,
22 + setMinCommitDuration,
23 + } = useContext(ProfilerContext);
24 + const store = useContext(StoreContext);
25 +
26 + const captureScreenshotsSubscription = useMemo(
27 + () => ({
28 + getCurrentValue: () => store.captureScreenshots,
29 + subscribe: (callback: Function) => {
30 + store.addListener('captureScreenshots', callback);
31 + return () => store.removeListener('captureScreenshots', callback);
32 + },
33 + }),
34 + [store]
35 + );
36 + const captureScreenshots = useSubscription<boolean, Store>(
37 + captureScreenshotsSubscription
38 + );
39 +
40 + const recordChangeDescriptionsSubscription = useMemo(
41 + () => ({
42 + getCurrentValue: () => store.recordChangeDescriptions,
43 + subscribe: (callback: Function) => {
44 + store.addListener('recordChangeDescriptions', callback);
45 + return () => store.removeListener('recordChangeDescriptions', callback);
46 + },
47 + }),
48 + [store]
49 + );
50 + const recordChangeDescriptions = useSubscription<boolean, Store>(
51 + recordChangeDescriptionsSubscription
52 + );
53 +
54 + const updateCaptureScreenshotsWhileProfiling = useCallback(
55 + ({ currentTarget }) => {
56 + store.captureScreenshots = currentTarget.checked;
57 + },
58 + [store]
59 + );
60 + const updateRecordChangeDescriptions = useCallback(
61 + ({ currentTarget }) => {
62 + store.recordChangeDescriptions = currentTarget.checked;
63 + },
64 + [store]
65 + );
66 + const updateMinCommitDuration = useCallback(
67 + (event: SyntheticEvent<HTMLInputElement>) => {
68 + const newValue = parseFloat(event.currentTarget.value);
69 + setMinCommitDuration(
70 + Number.isNaN(newValue) || newValue <= 0 ? 0 : newValue
71 + );
72 + },
73 + [setMinCommitDuration]
74 + );
75 + const updateIsCommitFilterEnabled = useCallback(
76 + (event: SyntheticEvent<HTMLInputElement>) => {
77 + const checked = event.currentTarget.checked;
78 + setIsCommitFilterEnabled(checked);
79 + if (checked) {
80 + if (minCommitDurationInputRef.current !== null) {
81 + minCommitDurationInputRef.current.focus();
82 + }
83 + }
84 + },
85 + [setIsCommitFilterEnabled]
86 + );
87 +
88 + const minCommitDurationInputRef = useRef<HTMLInputElement | null>(null);
89 +
90 + return (
91 + <div className={styles.Settings}>
92 + <div className={styles.Setting}>
93 + <label>
94 + <input
95 + type="checkbox"
96 + checked={recordChangeDescriptions}
97 + onChange={updateRecordChangeDescriptions}
98 + />{' '}
99 + Record why each component rendered while profiling.
100 + </label>
101 + </div>
102 +
103 + <div className={styles.Setting}>
104 + <label>
105 + <input
106 + checked={isCommitFilterEnabled}
107 + onChange={updateIsCommitFilterEnabled}
108 + type="checkbox"
109 + />{' '}
110 + Hide commits below
111 + </label>{' '}
112 + <input
113 + className={styles.Input}
114 + onChange={updateMinCommitDuration}
115 + ref={minCommitDurationInputRef}
116 + type="number"
117 + value={minCommitDuration}
118 + />{' '}
119 + (ms)
120 + </div>
121 +
122 + {store.supportsCaptureScreenshots && (
123 + <Fragment>
124 + <div className={styles.Setting}>
125 + <label>
126 + <input
127 + type="checkbox"
128 + checked={captureScreenshots}
129 + onChange={updateCaptureScreenshotsWhileProfiling}
130 + />{' '}
131 + Capture screenshots while profiling
132 + </label>
133 + </div>
134 + {captureScreenshots && (
135 + <div className={styles.ScreenshotThrottling}>
136 + Screenshots will be throttled in order to reduce the negative
137 + impact on performance.
138 + </div>
139 + )}
140 + </Fragment>
141 + )}
142 + </div>
143 + );
144 +}
src/devtools/views/Settings/Settings.css deleted
-86
@@ -1,86 +0,0 @@
1 -.Settings {
2 - width: 100%;
3 - padding: 0.5rem;
4 - display: flex;
5 - flex-direction: column;
6 - align-items: flex-start;
7 - justify-content: flex-start;
8 - border-top: 1px solid var(--color-border);
9 - font-family: var(--font-family-sans);
10 - font-size: var(--font-size-sans-normal);
11 - background-color: var(--color-background);
12 - color: var(--color-text);
13 -}
14 -
15 -.Section {
16 - width: 100%;
17 - padding: 0.5rem 0;
18 - border-top: 1px solid var(--color-border);
19 -}
20 -.Section:first-of-type {
21 - padding-top: 0;
22 - border-top: none;
23 -}
24 -
25 -.Header {
26 - margin-bottom: 0.5rem;
27 - font-size: var(--font-size-sans-large);
28 -}
29 -
30 -.OptionGroup {
31 - display: inline-flex;
32 - flex-direction: row;
33 - align-items: center;
34 - user-select: none;
35 - margin: 0 1rem 0.5rem 0;
36 -}
37 -.OptionGroup:last-of-type {
38 - margin-right: 0;
39 -}
40 -
41 -.OptionLabel {
42 - margin-right: 0.5rem;
43 - font-size: var(--font-size-sans-normal);
44 -}
45 -
46 -.RadioOption {
47 - cursor: pointer;
48 - padding: 0.5rem;
49 - border: 1px solid var(--color-border);
50 - border-right: none;
51 -}
52 -
53 -.RadioOption:hover {
54 - background-color: var(--color-background-hover);
55 -}
56 -
57 -.RadioOption:first-of-type {
58 - border-top-left-radius: 0.25rem;
59 - border-bottom-left-radius: 0.25rem;
60 -}
61 -
62 -.RadioOption:last-of-type {
63 - border-top-right-radius: 0.25rem;
64 - border-bottom-right-radius: 0.25rem;
65 - border-right: 1px solid var(--color-border);
66 -}
67 -
68 -.CheckboxOption {
69 - display: block;
70 - padding: 0 0 0.5rem;
71 -}
72 -
73 -.ScreenshotThrottling {
74 - display: inline-block;
75 - background-color: var(--color-background-hover);
76 - padding: 0.25rem 0.5rem;
77 - border-radius: 0.25rem;
78 -}
79 -
80 -.HRule {
81 - height: 1px;
82 - background-color: var(--color-border);
83 - width: 100%;
84 - border: none;
85 - margin: 0.5rem 0;
86 -}
src/devtools/views/Settings/Settings.js deleted
-203
@@ -1,203 +0,0 @@
1 -// @flow
2 -
3 -import React, { Fragment, useCallback, useContext, useMemo } from 'react';
4 -import { useSubscription } from '../hooks';
5 -import { StoreContext } from '../context';
6 -import { SettingsContext } from './SettingsContext';
7 -import Store from 'src/devtools/store';
8 -import portaledContent from '../portaledContent';
9 -
10 -import styles from './Settings.css';
11 -
12 -function Settings(_: {||}) {
13 - const store = useContext(StoreContext);
14 - const { displayDensity, setDisplayDensity, theme, setTheme } = useContext(
15 - SettingsContext
16 - );
17 -
18 - const captureScreenshotsSubscription = useMemo(
19 - () => ({
20 - getCurrentValue: () => store.captureScreenshots,
21 - subscribe: (callback: Function) => {
22 - store.addListener('captureScreenshots', callback);
23 - return () => store.removeListener('captureScreenshots', callback);
24 - },
25 - }),
26 - [store]
27 - );
28 - const captureScreenshots = useSubscription<boolean, Store>(
29 - captureScreenshotsSubscription
30 - );
31 -
32 - const collapseNodesByDefaultSubscription = useMemo(
33 - () => ({
34 - getCurrentValue: () => store.collapseNodesByDefault,
35 - subscribe: (callback: Function) => {
36 - store.addListener('collapseNodesByDefault', callback);
37 - return () => store.removeListener('collapseNodesByDefault', callback);
38 - },
39 - }),
40 - [store]
41 - );
42 - const collapseNodesByDefault = useSubscription<boolean, Store>(
43 - collapseNodesByDefaultSubscription
44 - );
45 -
46 - const recordChangeDescriptionsSubscription = useMemo(
47 - () => ({
48 - getCurrentValue: () => store.recordChangeDescriptions,
49 - subscribe: (callback: Function) => {
50 - store.addListener('recordChangeDescriptions', callback);
51 - return () => store.removeListener('recordChangeDescriptions', callback);
52 - },
53 - }),
54 - [store]
55 - );
56 - const recordChangeDescriptions = useSubscription<boolean, Store>(
57 - recordChangeDescriptionsSubscription
58 - );
59 -
60 - const updateDisplayDensity = useCallback(
61 - ({ currentTarget }) => {
62 - setDisplayDensity(currentTarget.value);
63 - },
64 - [setDisplayDensity]
65 - );
66 -
67 - const updateTheme = useCallback(
68 - ({ currentTarget }) => {
69 - setTheme(currentTarget.value);
70 - },
71 - [setTheme]
72 - );
73 -
74 - const updateCaptureScreenshotsWhileProfiling = useCallback(
75 - ({ currentTarget }) => {
76 - store.captureScreenshots = currentTarget.checked;
77 - },
78 - [store]
79 - );
80 - const updateCollapseNodesByDefault = useCallback(
81 - ({ currentTarget }) => {
82 - store.collapseNodesByDefault = currentTarget.checked;
83 - },
84 - [store]
85 - );
86 - const updateRecordChangeDescriptions = useCallback(
87 - ({ currentTarget }) => {
88 - store.recordChangeDescriptions = currentTarget.checked;
89 - },
90 - [store]
91 - );
92 -
93 - return (
94 - <div className={styles.Settings}>
95 - <div className={styles.Section}>
96 - <div className={styles.Header}>Display preferences</div>
97 - <div className={styles.OptionGroup}>
98 - <div className={styles.OptionLabel}>Theme</div>
99 - <label className={styles.RadioOption}>
100 - <input
101 - type="radio"
102 - name="Settings-theme"
103 - checked={theme === 'auto'}
104 - value="auto"
105 - onChange={updateTheme}
106 - />{' '}
107 - Auto
108 - </label>
109 - <label className={styles.RadioOption}>
110 - <input
111 - type="radio"
112 - name="Settings-theme"
113 - checked={theme === 'light'}
114 - value="light"
115 - onChange={updateTheme}
116 - />{' '}
117 - Light
118 - </label>
119 - <label className={styles.RadioOption}>
120 - <input
121 - type="radio"
122 - name="Settings-theme"
123 - checked={theme === 'dark'}
124 - value="dark"
125 - onChange={updateTheme}
126 - />{' '}
127 - Dark
128 - </label>
129 - </div>
130 - <div className={styles.OptionGroup}>
131 - <div className={styles.OptionLabel}>Display density</div>
132 - <label className={styles.RadioOption}>
133 - <input
134 - type="radio"
135 - name="Settings-displayDensity"
136 - checked={displayDensity === 'compact'}
137 - value="compact"
138 - onChange={updateDisplayDensity}
139 - />{' '}
140 - Compact
141 - </label>
142 - <label className={styles.RadioOption}>
143 - <input
144 - type="radio"
145 - name="Settings-displayDensity"
146 - checked={displayDensity === 'comfortable'}
147 - value="comfortable"
148 - onChange={updateDisplayDensity}
149 - />{' '}
150 - Comfortable
151 - </label>
152 - </div>
153 - </div>
154 -
155 - <div className={styles.Section}>
156 - <div className={styles.Header}>Components tree</div>
157 -
158 - <label className={styles.CheckboxOption}>
159 - <input
160 - type="checkbox"
161 - checked={collapseNodesByDefault}
162 - onChange={updateCollapseNodesByDefault}
163 - />{' '}
164 - Collapse newly added components by default
165 - </label>
166 - </div>
167 -
168 - <div className={styles.Section}>
169 - <div className={styles.Header}>Profiler</div>
170 -
171 - <label className={styles.CheckboxOption}>
172 - <input
173 - type="checkbox"
174 - checked={recordChangeDescriptions}
175 - onChange={updateRecordChangeDescriptions}
176 - />{' '}
177 - Record why each component rendered while profiling.
178 - </label>
179 -
180 - {store.supportsCaptureScreenshots && (
181 - <Fragment>
182 - <label className={styles.CheckboxOption}>
183 - <input
184 - type="checkbox"
185 - checked={captureScreenshots}
186 - onChange={updateCaptureScreenshotsWhileProfiling}
187 - />{' '}
188 - Capture screenshots while profiling
189 - </label>
190 - {captureScreenshots && (
191 - <div className={styles.ScreenshotThrottling}>
192 - Screenshots will be throttled in order to reduce the negative
193 - impact on performance.
194 - </div>
195 - )}
196 - </Fragment>
197 - )}
198 - </div>
199 - </div>
200 - );
201 -}
202 -
203 -export default portaledContent(Settings);
src/devtools/views/Settings/SettingsModal.css new
+43
@@ -0,0 +1,43 @@
1 +.Background {
2 + position: absolute;
3 + width: 100%;
4 + top: 0;
5 + bottom: 0;
6 + background-color: var(--color-modal-background);
7 + display: flex;
8 + align-items: flex-start;
9 + justify-content: center;
10 + font-size: var(--font-size-sans-normal);
11 + padding: 1rem;
12 +}
13 +
14 +.Modal {
15 + display: flex;
16 + flex-direction: column;
17 + flex: 0 1 auto;
18 + max-height: 100%;
19 + background-color: var(--color-background);
20 + border: 1px solid var(--color-border);
21 + border-radius: 0.25rem;
22 + overflow: auto;
23 + width: 400px;
24 + max-width: 100%;
25 +}
26 +
27 +.Spacer {
28 + flex: 1;
29 +}
30 +
31 +.Tabs {
32 + display: flex;
33 + flex-direction: row;
34 + border-bottom: 1px solid var(--color-border);
35 + padding-right: 0.25rem;
36 + flex: 0 0 auto;
37 +}
38 +
39 +.Content {
40 + padding: 0.5rem;
41 + flex: 0 1 auto;
42 + overflow: auto;
43 +}
src/devtools/views/Settings/SettingsModal.js new
+134
@@ -0,0 +1,134 @@
1 +// @flow
2 +
3 +import React, {
4 + useCallback,
5 + useContext,
6 + useEffect,
7 + useMemo,
8 + useRef,
9 + useState,
10 +} from 'react';
11 +import { SettingsModalContext } from './SettingsModalContext';
12 +import Store from 'src/devtools/store';
13 +import Button from '../Button';
14 +import ButtonIcon from '../ButtonIcon';
15 +import TabBar from '../TabBar';
16 +import { StoreContext } from '../context';
17 +import { useModalDismissSignal, useSubscription } from '../hooks';
18 +import ComponentsSettings from './ComponentsSettings';
19 +import GeneralSettings from './GeneralSettings';
20 +import ProfilerSettings from './ProfilerSettings';
21 +
22 +import styles from './SettingsModal.css';
23 +
24 +type TabID = 'general' | 'components' | 'profiler';
25 +
26 +type Props = {|
27 + defaultTabID: TabID,
28 +|};
29 +
30 +export default function SettingsModal({ defaultTabID = 'general' }: Props) {
31 + const { isModalShowing, setIsModalShowing } = useContext(
32 + SettingsModalContext
33 + );
34 + const store = useContext(StoreContext);
35 + const { profilerStore } = store;
36 +
37 + // Updating preferences while profiling is in progress could break things (e.g. filtering)
38 + // Explicitly disallow it for now.
39 + const isProfilingSubscription = useMemo(
40 + () => ({
41 + getCurrentValue: () => profilerStore.isProfiling,
42 + subscribe: (callback: Function) => {
43 + profilerStore.addListener('isProfiling', callback);
44 + return () => profilerStore.removeListener('isProfiling', callback);
45 + },
46 + }),
47 + [profilerStore]
48 + );
49 + const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
50 + if (isProfiling && isModalShowing) {
51 + setIsModalShowing(false);
52 + }
53 +
54 + if (!isModalShowing) {
55 + return null;
56 + }
57 +
58 + return <SettingsModalImpl defaultTabID={defaultTabID} />;
59 +}
60 +
61 +function SettingsModalImpl({ defaultTabID }: Props) {
62 + const { setIsModalShowing } = useContext(SettingsModalContext);
63 + const dismissModal = useCallback(() => setIsModalShowing(false), [
64 + setIsModalShowing,
65 + ]);
66 +
67 + const [selectedTabID, selectTab] = useState<TabID>(defaultTabID);
68 +
69 + const modalRef = useRef<HTMLDivElement | null>(null);
70 + useModalDismissSignal(modalRef, dismissModal);
71 +
72 + useEffect(() => {
73 + if (modalRef.current !== null) {
74 + modalRef.current.focus();
75 + }
76 + }, [modalRef]);
77 +
78 + let view = null;
79 + switch (selectedTabID) {
80 + case 'general':
81 + view = <GeneralSettings />;
82 + break;
83 + case 'profiler':
84 + view = <ProfilerSettings />;
85 + break;
86 + case 'components':
87 + view = <ComponentsSettings />;
88 + break;
89 + default:
90 + break;
91 + }
92 +
93 + return (
94 + <div className={styles.Background}>
95 + <div className={styles.Modal} ref={modalRef}>
96 + <div className={styles.Tabs}>
97 + <TabBar
98 + currentTab={selectedTabID}
99 + id="Settings"
100 + selectTab={selectTab}
101 + size="small"
102 + tabs={tabs}
103 + />
104 + <div className={styles.Spacer} />
105 + <Button onClick={dismissModal} title="Close settings dialog">
106 + <ButtonIcon type="close" />
107 + </Button>
108 + </div>
109 + <div className={styles.Content}>{view}</div>
110 + </div>
111 + </div>
112 + );
113 +}
114 +
115 +const tabs = [
116 + {
117 + id: 'general',
118 + icon: 'settings',
119 + label: 'General',
120 + title: 'General preferences',
121 + },
122 + {
123 + id: 'components',
124 + icon: 'components',
125 + label: 'Components',
126 + title: 'Components preferences',
127 + },
128 + {
129 + id: 'profiler',
130 + icon: 'profiler',
131 + label: 'Profiler',
132 + title: 'Profiler preferences',
133 + },
134 +];
src/devtools/views/Settings/SettingsModalContext.js new
+35
@@ -0,0 +1,35 @@
1 +// @flow
2 +
3 +import React, { createContext, useMemo, useState } from 'react';
4 +
5 +export type DisplayDensity = 'comfortable' | 'compact';
6 +export type Theme = 'auto' | 'light' | 'dark';
7 +
8 +type Context = {
9 + isModalShowing: boolean,
10 + setIsModalShowing: (value: boolean) => void,
11 +};
12 +
13 +const SettingsModalContext = createContext<Context>(((null: any): Context));
14 +SettingsModalContext.displayName = 'SettingsModalContext';
15 +
16 +function SettingsModalContextController({
17 + children,
18 +}: {|
19 + children: React$Node,
20 +|}) {
21 + const [isModalShowing, setIsModalShowing] = useState<boolean>(false);
22 +
23 + const value = useMemo(() => ({ isModalShowing, setIsModalShowing }), [
24 + isModalShowing,
25 + setIsModalShowing,
26 + ]);
27 +
28 + return (
29 + <SettingsModalContext.Provider value={value}>
30 + {children}
31 + </SettingsModalContext.Provider>
32 + );
33 +}
34 +
35 +export { SettingsModalContext, SettingsModalContextController };
src/devtools/views/Settings/SettingsModalContextToggle.js new
+43
@@ -0,0 +1,43 @@
1 +// @flow
2 +
3 +import React, { useCallback, useContext, useMemo } from 'react';
4 +import { SettingsModalContext } from './SettingsModalContext';
5 +import Button from '../Button';
6 +import ButtonIcon from '../ButtonIcon';
7 +import { StoreContext } from '../context';
8 +import { useSubscription } from '../hooks';
9 +import Store from 'src/devtools/store';
10 +
11 +export default function SettingsModalContextToggle() {
12 + const { setIsModalShowing } = useContext(SettingsModalContext);
13 + const store = useContext(StoreContext);
14 + const { profilerStore } = store;
15 +
16 + const showFilterModal = useCallback(() => setIsModalShowing(true), [
17 + setIsModalShowing,
18 + ]);
19 +
20 + // Updating preferences while profiling is in progress could break things (e.g. filtering)
21 + // Explicitly disallow it for now.
22 + const isProfilingSubscription = useMemo(
23 + () => ({
24 + getCurrentValue: () => profilerStore.isProfiling,
25 + subscribe: (callback: Function) => {
26 + profilerStore.addListener('isProfiling', callback);
27 + return () => profilerStore.removeListener('isProfiling', callback);
28 + },
29 + }),
30 + [profilerStore]
31 + );
32 + const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
33 +
34 + return (
35 + <Button
36 + disabled={isProfiling}
37 + onClick={showFilterModal}
38 + title="View settings"
39 + >
40 + <ButtonIcon type="settings" />
41 + </Button>
42 + );
43 +}
src/devtools/views/Settings/SettingsShared.css renamed
+49 -32
@@ -1,37 +1,61 @@
1 -.Background {
2 - position: absolute;
3 - width: 100%;
4 - height: 100%;
1 +.Settings {
2 display: flex;
3 + flex-direction: column;
4 align-items: flex-start;
7 - justify-content: center;
8 - padding: 1rem;
9 - background-color: var(--color-modal-background);
10 - overflow: auto;
5 + justify-content: flex-start;
6 + font-family: var(--font-family-sans);
7 + font-size: var(--font-size-sans-normal);
8 }
9
13 -.Modal {
14 - position: relative;
15 - z-index: 3;
16 - min-width: 20rem;
17 - max-width: 100%;
10 +.Setting {
11 + margin-bottom: 0.5rem;
12 +}
13 +.Setting:last-of-type {
14 + margin-bottom: 0;
15 +}
16 +
17 +.OptionGroup {
18 + display: inline-flex;
19 + flex-direction: row;
20 + align-items: center;
21 + user-select: none;
22 + margin: 0 1rem 0.5rem 0;
23 +}
24 +.OptionGroup:last-of-type {
25 + margin-right: 0;
26 +}
27 +
28 +.RadioLabel {
29 + display: inline;
30 + margin-right: 0.5rem;
31 +}
32 +
33 +.Select {
34 +}
35 +
36 +.CheckboxOption {
37 + display: block;
38 + padding: 0 0 0.5rem;
39 +}
40 +
41 +.ScreenshotThrottling {
42 display: inline-block;
19 - background-color: var(--color-background);
20 - padding: 0.5rem;
21 - border: 1px solid var(--color-border);
43 + background-color: var(--color-background-hover);
44 + padding: 0.25rem 0.5rem;
45 border-radius: 0.25rem;
46 }
47
25 -.LeftRight {
26 - display: flex;
27 -}
28 -.Left {
48 +.HRule {
49 + height: 1px;
50 + background-color: var(--color-border);
51 + width: 100%;
52 + border: none;
53 + margin: 0.5rem 0;
54 }
30 -.Right {
31 - flex: 1;
32 - display: flex;
33 - align-items: center;
34 - justify-content: flex-end;
55 +
56 +.Header {
57 + font-size: var(--font-size-sans-large);
58 + margin-top: 0.5rem;
59 }
60
61 .ButtonIcon {
@@ -62,19 +86,12 @@
86 padding-right: 0;
87 }
88
65 -.Select {
66 -}
67 -
89 .Input {
90 border: 1px solid var(--color-border);
91 border-radius: 0.125rem;
92 padding: 0.125rem;
93 }
94
74 -.CancelButton {
75 - margin-right: 0.25rem;
76 -}
77 -
95 .InvalidRegExp,
96 .InvalidRegExp:active,
97 .InvalidRegExp:focus,
src/devtools/views/TabBar.css
+3 -2
@@ -7,7 +7,6 @@
7 cursor: pointer;
8 border-top: 3px solid transparent;
9 border-bottom: 3px solid transparent;
10 - cursor: pointer;
10 user-select: none;
11 color: var(--color-text);
12 }
@@ -30,9 +29,11 @@
29 }
30
31 .TabSizeLarge {
32 + font-size: var(--font-size-sans-large);
33 padding: 0.5rem 1rem;
34 }
35 .TabSizeSmall {
36 + font-size: var(--font-size-sans-normal);
37 padding: 0.25rem 0.5rem;
38 }
39
@@ -76,7 +77,7 @@
77 }
78 }
79
79 -@media screen and (max-width: 600px) {
80 +@media screen and (max-width: 525px) {
81 .IconSizeLarge {
82 margin-right: 0;
83 }
src/utils.js
+16
@@ -229,3 +229,19 @@ export function separateDisplayNameAndHOCs(
229
230 return [displayName, hocDisplayNames];
231 }
232 +
233 +// Pulled from react-compat
234 +// https://github.com/developit/preact-compat/blob/7c5de00e7c85e2ffd011bf3af02899b63f699d3a/src/index.js#L349
235 +export function shallowDiffers(prev: Object, next: Object): boolean {
236 + for (let attribute in prev) {
237 + if (!(attribute in next)) {
238 + return true;
239 + }
240 + }
241 + for (let attribute in next) {
242 + if (prev[attribute] !== next[attribute]) {
243 + return true;
244 + }
245 + }
246 + return false;
247 +}