Added search support
Brian Vaughn committed
Feb 5, 2019 at 17:14 UTC
998e69a10defa81473cd50319a564ab485360186
15 files changed
+493
-57
src/devtools/store.js
+8
-2
@@ -265,7 +265,7 @@ export default class Store extends EventEmitter {
265
parentElement = ((this._idToElement.get(parentID): any): Element);
266
parentElement.children = parentElement.children.concat(id);
267
268
- this._idToElement.set(id, {
268
+ const element: Element = {
269
children: [],
270
depth: parentElement.depth + 1,
271
displayName,
@@ -274,7 +274,11 @@ export default class Store extends EventEmitter {
274
parentID: parentElement.id,
275
type,
276
weight: 1,
277
- });
277
+ };
278
+
279
+ this._idToElement.set(id, element);
280
+
281
+ this.emit('elementAdded', element);
282
283
weightDelta = 1;
284
}
@@ -303,6 +307,8 @@ export default class Store extends EventEmitter {
307
childID => childID !== id
308
);
309
}
310
+
311
+ this.emit('elementRemoved', element);
312
break;
313
case TREE_OPERATION_RESET_CHILDREN:
314
id = ((operations[i + 1]: any): number);
src/devtools/views/ButtonIcon.js
+17
-1
@@ -4,15 +4,24 @@ import React from 'react';
4
import styles from './ButtonIcon.css';
5
6
type Props = {|
7
- type: 'search' | 'view-dom' | 'view-source',
7
+ type: 'close' | 'down' | 'search' | 'up' | 'view-dom' | 'view-source',
8
|};
9
10
export default function ButtonIcon({ type }: Props) {
11
let pathData = null;
12
switch (type) {
13
+ case 'close':
14
+ pathData = PATH_CLOSE;
15
+ break;
16
+ case 'down':
17
+ pathData = PATH_DOWN;
18
+ break;
19
case 'search':
20
pathData = PATH_SEARCH;
21
break;
22
+ case 'up':
23
+ pathData = PATH_UP;
24
+ break;
25
case 'view-dom':
26
pathData = PATH_VIEW_DOM;
27
break;
@@ -38,12 +47,19 @@ export default function ButtonIcon({ type }: Props) {
47
);
48
}
49
50
+const PATH_CLOSE =
51
+ 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z';
52
+
53
+const PATH_DOWN = 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z';
54
+
55
const PATH_SEARCH = `
56
M20.94 11c-.46-4.17-3.77-7.48-7.94-7.94V1h-2v2.06C6.83 3.52 3.52 6.83 3.06 11H1v2h2.06c.46
57
4.17 3.77 7.48 7.94 7.94V23h2v-2.06c4.17-.46 7.48-3.77 7.94-7.94H23v-2h-2.06zM12 19c-3.87
58
0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z
59
`;
60
61
+const PATH_UP = 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z';
62
+
63
const PATH_VIEW_DOM = `
64
M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12
65
17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3
src/devtools/views/Element.css
+7
@@ -63,3 +63,10 @@
63
.AttributeValue {
64
color: var(--color-attribute-value);
65
}
66
+
67
+.Highlight {
68
+ background-color: var(--color-search-match);
69
+}
70
+.CurrentHighlight {
71
+ background-color: var(--color-search-match-current);
72
+}
src/devtools/views/Element.js
+48
-2
@@ -1,8 +1,9 @@
1
// @flow
2
3
-import React, { Fragment, useCallback, useContext } from 'react';
3
+import React, { Fragment, useCallback, useContext, useMemo } from 'react';
4
import { ElementTypeClassOrFunction } from 'src/devtools/types';
5
import { TreeContext } from './context';
6
+import { SearchContext } from './SearchContext';
7
import { SelectedElementContext } from './SelectedElementContext';
8
import Icon from './Icon';
9
@@ -56,7 +57,7 @@ export default function Element({ index, style }: Props) {
57
)}
58
59
<span className={styles.Component}>
59
- {displayName}
60
+ <DisplayName displayName={displayName} id={id} />
61
{key && (
62
<Fragment>
63
<span className={styles.AttributeName}>key</span>=
@@ -68,3 +69,48 @@ export default function Element({ index, style }: Props) {
69
</div>
70
);
71
}
72
+
73
+type DisplayNameProps = {|
74
+ displayName: string | null,
75
+ id: number,
76
+|};
77
+
78
+function DisplayName({ displayName, id }: DisplayNameProps) {
79
+ const {
80
+ currentIndex: currentSearchIndex,
81
+ ids: searchIDs,
82
+ text: searchText,
83
+ } = useContext(SearchContext);
84
+ const isSearchResult = useMemo(() => {
85
+ return searchIDs.includes(id);
86
+ }, [id, searchIDs]);
87
+ const isCurrentResult =
88
+ currentSearchIndex !== null && id === searchIDs[currentSearchIndex];
89
+
90
+ if (!isSearchResult || displayName === null) {
91
+ return displayName;
92
+ }
93
+
94
+ const startIndex = displayName
95
+ .toLowerCase()
96
+ .indexOf(searchText.toLowerCase());
97
+ const stopIndex = startIndex + searchText.length;
98
+
99
+ const children = [];
100
+ if (startIndex > 0) {
101
+ children.push(<span key="begin">{displayName.slice(0, startIndex)}</span>);
102
+ }
103
+ children.push(
104
+ <mark
105
+ key="middle"
106
+ className={isCurrentResult ? styles.CurrentHighlight : styles.Highlight}
107
+ >
108
+ {displayName.slice(startIndex, stopIndex)}
109
+ </mark>
110
+ );
111
+ if (stopIndex < displayName.length) {
112
+ children.push(<span key="end">{displayName.slice(stopIndex)}</span>);
113
+ }
114
+
115
+ return children;
116
+}
src/devtools/views/Elements.js
+10
-7
@@ -5,6 +5,7 @@ import Store from '../store';
5
import Tree from './Tree';
6
import { BridgeContext, StoreContext, TreeContext } from './context';
7
import SelectedElement from './SelectedElement';
8
+import { SearchController } from './SearchContext';
9
import { SelectedElementController } from './SelectedElementContext';
10
import styles from './Elements.css';
11
@@ -51,14 +52,16 @@ export default function Elements({ bridge, browserName, themeName }: Props) {
52
<StoreContext.Provider value={store}>
53
<TreeContext.Provider value={treeContext}>
54
<SelectedElementController>
54
- <div className={styles.Elements}>
55
- <div className={styles.TreeWrapper}>
56
- <Tree />
55
+ <SearchController>
56
+ <div className={styles.Elements}>
57
+ <div className={styles.TreeWrapper}>
58
+ <Tree />
59
+ </div>
60
+ <div className={styles.SelectedElementWrapper}>
61
+ <SelectedElement />
62
+ </div>
63
</div>
58
- <div className={styles.SelectedElementWrapper}>
59
- <SelectedElement />
60
- </div>
61
- </div>
64
+ </SearchController>
65
</SelectedElementController>
66
</TreeContext.Provider>
67
</StoreContext.Provider>
src/devtools/views/HooksTree.css
+1
-1
@@ -19,6 +19,6 @@
19
}
20
21
.None {
22
- color: var(--color-deemphasized);
22
+ color: var(--color-dimmer);
23
font-style: italic;
24
}
src/devtools/views/InspectedElementTree.css
+2
-2
@@ -18,12 +18,12 @@
18
}
19
20
.None {
21
- color: var(--color-deemphasized);
21
+ color: var(--color-dimmer);
22
font-style: italic;
23
}
24
25
.Empty {
26
- color: var(--color-deemphasized);
26
+ color: var(--color-dimmer);
27
font-style: italic;
28
padding-left: 1rem;
29
}
src/devtools/views/SearchContext.js
new
+194
@@ -0,0 +1,194 @@
1
+// @flow
2
+
3
+import React, {
4
+ createContext,
5
+ useContext,
6
+ useLayoutEffect,
7
+ useMemo,
8
+ useState,
9
+} from 'react';
10
+import { StoreContext } from './context';
11
+import { SelectedElementContext } from './SelectedElementContext';
12
+import Store from '../store';
13
+
14
+import type { Element } from 'src/devtools/types';
15
+
16
+export type SearchContextValue = {|
17
+ get currentIndex(): number | null,
18
+ get ids(): Array<number>,
19
+ get text(): string,
20
+ updateCurrentIndex(newCurrentIndex: number): void,
21
+ updateText(newText: string): void,
22
+|};
23
+
24
+const SearchContext = createContext<SearchContextValue>(
25
+ ((null: any): SearchContextValue)
26
+);
27
+// $FlowFixMe displayName is a valid attribute of React$Context
28
+SearchContext.displayName = 'SearchContext';
29
+
30
+type Props = {|
31
+ children: React$Node,
32
+|};
33
+
34
+// TODO Remove this wrapper element once global Context.write API exists.
35
+function SearchController({ children }: Props) {
36
+ const store = useContext(StoreContext);
37
+ const selectedElement = useContext(SelectedElementContext);
38
+
39
+ const [currentIndex, setCurrentIndex] = useState<number | null>(null);
40
+ const [ids, setIDs] = useState<Array<number>>([]);
41
+ const [text, setText] = useState<string>('');
42
+
43
+ const updateIndexHelper = (
44
+ newCurrentIndex: number | null,
45
+ newOrCurrentIDs: Array<number>
46
+ ) => {
47
+ setCurrentIndex(newCurrentIndex);
48
+
49
+ // Make sure a new search result is also selected.
50
+ selectedElement.id =
51
+ newCurrentIndex !== null ? newOrCurrentIDs[newCurrentIndex] : null;
52
+ };
53
+
54
+ const value = useMemo(
55
+ () => ({
56
+ get currentIndex(): number | null {
57
+ return currentIndex;
58
+ },
59
+
60
+ get ids(): Array<number> {
61
+ return ids;
62
+ },
63
+
64
+ get text(): string {
65
+ return text;
66
+ },
67
+
68
+ updateCurrentIndex(newCurrentIndex: number) {
69
+ updateIndexHelper(newCurrentIndex, ids);
70
+ },
71
+
72
+ updateText(newText: string) {
73
+ setText(newText);
74
+
75
+ // Find all matching elements.
76
+ const newIDs = [];
77
+ if (newText !== '') {
78
+ const regExp = new RegExp(newText, 'i');
79
+ store.roots.forEach(rootID => {
80
+ searchTree(store, rootID, regExp, newIDs);
81
+ });
82
+
83
+ setIDs(newIDs);
84
+
85
+ // If this is a refinement of a previous search, preserve the current index (unless it's no longer valid).
86
+ // If it's a new search, reset the index.
87
+ if (newIDs.length === 0) {
88
+ updateIndexHelper(null, newIDs);
89
+ } else if (currentIndex !== null && newText.startsWith(text)) {
90
+ updateIndexHelper(
91
+ Math.min(currentIndex, newIDs.length - 1),
92
+ newIDs
93
+ );
94
+ } else {
95
+ updateIndexHelper(0, newIDs);
96
+ }
97
+ } else {
98
+ setIDs([]);
99
+ updateIndexHelper(null, newIDs);
100
+ }
101
+ },
102
+ }),
103
+ [currentIndex, ids, text]
104
+ );
105
+
106
+ // Listen for changes to the tree and incrementally adjust the search results.
107
+ useLayoutEffect(() => {
108
+ const handleElementAdded = (element: Element) => {
109
+ if (!text) {
110
+ return;
111
+ }
112
+
113
+ const { displayName, id } = element;
114
+
115
+ // Add this item to the search results if it matches.
116
+ const regExp = new RegExp(text, 'i');
117
+ if (displayName !== null && regExp.test(displayName)) {
118
+ const newElementIndex = ((store.getIndexOfElementID(id): any): number);
119
+
120
+ let newIDs = null;
121
+ for (let index = 0; index < ids.length; index++) {
122
+ const id = ids[index];
123
+ if (
124
+ newElementIndex < ((store.getIndexOfElementID(id): any): number)
125
+ ) {
126
+ newIDs = ids
127
+ .slice(0, index)
128
+ .concat(id)
129
+ .concat(ids.slice(index));
130
+ break;
131
+ }
132
+ }
133
+ if (newIDs === null) {
134
+ newIDs = ids.concat(id);
135
+ }
136
+
137
+ setIDs(newIDs);
138
+ updateIndexHelper(currentIndex === null ? 0 : currentIndex, newIDs);
139
+ }
140
+ };
141
+ const handleElementRemoved = (element: Element) => {
142
+ if (!text) {
143
+ return;
144
+ }
145
+
146
+ // Prune this item from the search results.
147
+ const index = ids.indexOf(element.id);
148
+ if (index >= 0) {
149
+ const newIDs = ids.slice(0, index).concat(ids.slice(index + 1));
150
+ setIDs(newIDs);
151
+
152
+ // If the results are now empty, also deselect things.
153
+ if (newIDs.length === 0) {
154
+ updateIndexHelper(null, newIDs);
155
+ } else if (((currentIndex: any): number) < newIDs.length) {
156
+ updateIndexHelper(currentIndex, newIDs);
157
+ } else {
158
+ updateIndexHelper(newIDs.length - 1, newIDs);
159
+ }
160
+ }
161
+ };
162
+
163
+ store.addListener('elementAdded', handleElementAdded);
164
+ store.addListener('elementRemoved', handleElementRemoved);
165
+
166
+ return () => {
167
+ store.removeListener('elementAdded', handleElementAdded);
168
+ store.removeListener('elementRemoved', handleElementRemoved);
169
+ };
170
+ }, [currentIndex, ids, store, text]);
171
+
172
+ return (
173
+ <SearchContext.Provider value={value}>{children}</SearchContext.Provider>
174
+ );
175
+}
176
+
177
+function searchTree(
178
+ store: Store,
179
+ elementID: number,
180
+ regExp: RegExp,
181
+ ids: Array<number>
182
+): void {
183
+ const { children, displayName } = ((store.getElementByID(
184
+ elementID
185
+ ): any): Element);
186
+ if (displayName !== null) {
187
+ if (regExp.test(displayName)) {
188
+ ids.push(elementID);
189
+ }
190
+ }
191
+ children.forEach(childID => searchTree(store, childID, regExp, ids));
192
+}
193
+
194
+export { SearchContext, SearchController };
src/devtools/views/SearchInput.css
new
+53
@@ -0,0 +1,53 @@
1
+.SearchInput {
2
+ flex: 1;
3
+ display: flex;
4
+ align-items: center;
5
+}
6
+
7
+.Input {
8
+ flex: 1;
9
+ font-size: 1rem;
10
+ outline: none;
11
+ border: none;
12
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E");
13
+ background-repeat: no-repeat;
14
+ background-position: left center;
15
+ background-size: 16px 16px;
16
+ padding-left: 1.5rem;
17
+}
18
+
19
+.IndexLabel {
20
+ color: var(--color-dim);
21
+ font-size: var(--font-size-normal);
22
+}
23
+
24
+.IconButton {
25
+ border: none;
26
+ background: none;
27
+ display: inline-flex;
28
+ align-items: center;
29
+ padding: 0.25rem;
30
+ cursor: pointer;
31
+ color: var(--color-button);
32
+ flex: 0 0 auto;
33
+}
34
+.IconButton:hover {
35
+ color: var(--color-button-hover);
36
+}
37
+.IconButton:disabled {
38
+ color: var(--color-dimmer);
39
+ cursor: default;
40
+}
41
+
42
+.LeftVRule,
43
+.RightVRule {
44
+ height: 20px;
45
+ width: 1px;
46
+ background-color: var(--color-border);
47
+}
48
+.LeftVRule {
49
+ margin: 0 0.25rem 0 0.5rem;
50
+}
51
+.RightVRule {
52
+ margin: 0 0.5rem 0 0.25rem;
53
+}
src/devtools/views/SearchInput.js
new
+117
@@ -0,0 +1,117 @@
1
+// @flow
2
+
3
+import React, { useCallback, useContext, useEffect, useRef } from 'react';
4
+import { SearchContext } from './SearchContext';
5
+import ButtonIcon from './ButtonIcon';
6
+
7
+import styles from './SearchInput.css';
8
+
9
+type Props = {||};
10
+
11
+export default function SearchInput(props: Props) {
12
+ const {
13
+ currentIndex,
14
+ ids,
15
+ updateCurrentIndex,
16
+ updateText,
17
+ text,
18
+ } = useContext(SearchContext);
19
+
20
+ const inputRef = useRef();
21
+
22
+ const handleTextChange = useCallback(({ currentTarget }) => {
23
+ updateText(currentTarget.value);
24
+ });
25
+
26
+ const selectNext = useCallback(() => {
27
+ if (currentIndex !== null) {
28
+ updateCurrentIndex(currentIndex + 1);
29
+ }
30
+ }, [currentIndex]);
31
+
32
+ const selectPrevious = useCallback(() => {
33
+ if (currentIndex !== null) {
34
+ updateCurrentIndex(currentIndex - 1);
35
+ }
36
+ }, [currentIndex]);
37
+
38
+ const resetSearch = useCallback(() => {
39
+ updateText('');
40
+ }, [updateText]);
41
+
42
+ const handleInputKeyPress = useCallback(
43
+ ({ key }) => {
44
+ if (key === 'Enter') {
45
+ if (currentIndex !== null) {
46
+ if (currentIndex + 1 < ids.length) {
47
+ updateCurrentIndex(currentIndex + 1);
48
+ } else {
49
+ updateCurrentIndex(0);
50
+ }
51
+ }
52
+ }
53
+ },
54
+ [currentIndex, ids]
55
+ );
56
+
57
+ // Auto-focus search input
58
+ useEffect(() => {
59
+ const handleWindowKeyDown = event => {
60
+ const { key, metaKey } = event;
61
+ if (key === 'f' && metaKey) {
62
+ if (inputRef.current !== null) {
63
+ inputRef.current.focus();
64
+ event.preventDefault();
65
+ }
66
+ }
67
+ };
68
+
69
+ window.addEventListener('keydown', handleWindowKeyDown);
70
+
71
+ return () => window.removeEventListener('keydown', handleWindowKeyDown);
72
+ }, [inputRef]);
73
+
74
+ return (
75
+ <div className={styles.SearchInput}>
76
+ <input
77
+ className={styles.Input}
78
+ onKeyPress={handleInputKeyPress}
79
+ onChange={handleTextChange}
80
+ placeholder="Search (text or /regex/)"
81
+ ref={inputRef}
82
+ value={text}
83
+ />
84
+ {!!text && (
85
+ <span className={styles.IndexLabel}>
86
+ {Math.min(currentIndex + 1, ids.length)} | {ids.length}
87
+ </span>
88
+ )}
89
+ <div className={styles.LeftVRule} />
90
+ <button
91
+ className={styles.IconButton}
92
+ disabled={currentIndex === null || currentIndex === 0}
93
+ onClick={selectPrevious}
94
+ title="Scroll to previous search result"
95
+ >
96
+ <ButtonIcon type="up" />
97
+ </button>
98
+ <button
99
+ className={styles.IconButton}
100
+ disabled={currentIndex === null || currentIndex === ids.length - 1}
101
+ onClick={selectNext}
102
+ title="Scroll to next search result"
103
+ >
104
+ <ButtonIcon type="down" />
105
+ </button>
106
+ <button
107
+ className={styles.IconButton}
108
+ disabled={!text}
109
+ onClick={resetSearch}
110
+ title="Reset search"
111
+ >
112
+ <ButtonIcon type="close" />
113
+ </button>
114
+ <div className={styles.RightVRule} />
115
+ </div>
116
+ );
117
+}
src/devtools/views/SelectedElement.css
+1
-1
@@ -78,6 +78,6 @@
78
79
.Loading {
80
padding: 0.25rem;
81
- color: var(--color-deemphasized);
81
+ color: var(--color-dimmer);
82
font-style: italic;
83
}
src/devtools/views/SelectedElement.js
+10
-8
@@ -135,13 +135,13 @@ function useInspectedElement(id: number | null): InspectedElement | null {
135
idRef.current = id;
136
137
useLayoutEffect(() => {
138
+ // Hide previous/stale insepected element to avoid temporarily showing the wrong values.
139
+ setInspectedElement(null);
140
+
141
if (id === null) {
142
return () => {};
143
}
144
142
- // Hide previous/stale insepected element to avoid temporarily showing the wrong values.
143
- setInspectedElement(null);
144
-
145
let timeoutID = null;
146
147
// Update the $r variable.
@@ -152,15 +152,17 @@ function useInspectedElement(id: number | null): InspectedElement | null {
152
};
153
154
const onInspectedElement = (inspectedElement: InspectedElement) => {
155
- if (idRef.current !== inspectedElement.id) {
155
+ if (inspectedElement && inspectedElement.id !== idRef.current) {
156
// Ignore bridge updates about previously selected elements.
157
return;
158
}
159
160
- inspectedElement.context = hydrateHelper(inspectedElement.context);
161
- inspectedElement.hooks = hydrateHelper(inspectedElement.hooks);
162
- inspectedElement.props = hydrateHelper(inspectedElement.props);
163
- inspectedElement.state = hydrateHelper(inspectedElement.state);
160
+ if (inspectedElement !== null) {
161
+ inspectedElement.context = hydrateHelper(inspectedElement.context);
162
+ inspectedElement.hooks = hydrateHelper(inspectedElement.hooks);
163
+ inspectedElement.props = hydrateHelper(inspectedElement.props);
164
+ inspectedElement.state = hydrateHelper(inspectedElement.state);
165
+ }
166
167
setInspectedElement(inspectedElement);
168
src/devtools/views/Tree.css
+12
-24
@@ -6,7 +6,7 @@
6
border-top: 1px solid var(--color-border);
7
}
8
9
-.SearchRow {
9
+.SearchInput {
10
flex: 0 0 42px;
11
display: flex;
12
align-items: center;
@@ -15,16 +15,17 @@
15
padding: 0.5rem;
16
}
17
18
-.SearchInput {
19
- font-size: 1rem;
20
- flex: 1;
21
- outline: none;
22
- border: none;
23
- background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E");
24
- background-repeat: no-repeat;
25
- background-position: left center;
26
- background-size: 16px 16px;
27
- padding-left: 1.5rem;
18
+.AutoSizerWrapper {
19
+ width: 100%;
20
+ overflow: auto;
21
+ flex: 1 0 auto;
22
+ padding: 0.25rem;
23
+}
24
+
25
+.List {
26
+ font-family: var(--font-family-monospace);
27
+ font-size: var(--font-size-normal);
28
+ line-height: var(--line-height-data);
29
}
30
31
.IconButton {
@@ -40,16 +41,3 @@
41
.IconButton:hover {
42
color: var(--color-button-hover);
43
}
43
-
44
-.AutoSizerWrapper {
45
- width: 100%;
46
- overflow: auto;
47
- flex: 1 0 auto;
48
- padding: 0.25rem;
49
-}
50
-
51
-.List {
52
- font-family: var(--font-family-monospace);
53
- font-size: var(--font-size-normal);
54
- line-height: var(--line-height-data);
55
-}
src/devtools/views/Tree.js
+5
-7
@@ -4,8 +4,9 @@ import React, { useContext, useEffect, useLayoutEffect, useRef } from 'react';
4
import AutoSizer from 'react-virtualized-auto-sizer';
5
import { FixedSizeList } from 'react-window';
6
import { SelectedElementContext } from './SelectedElementContext';
7
-import Element from './Element';
7
import ButtonIcon from './ButtonIcon';
8
+import Element from './Element';
9
+import SearchInput from './SearchInput';
10
import { TreeContext } from './context';
11
12
import styles from './Tree.css';
@@ -15,7 +16,7 @@ type Props = {||};
16
export default function Tree(props: Props) {
17
const selectedElementContext = useContext(SelectedElementContext);
18
const treeContext = useContext(TreeContext);
18
- const listRef = useRef();
19
+ const listRef = useRef<FixedSizeList<any>>();
20
21
// Make sure a newly selected element is visible in the list.
22
// This is helpful for things like the owners list.
@@ -59,11 +60,8 @@ export default function Tree(props: Props) {
60
61
return (
62
<div className={styles.Tree}>
62
- <div className={styles.SearchRow}>
63
- <input
64
- className={styles.SearchInput}
65
- placeholder="Search (text or /regex/)"
66
- />
63
+ <div className={styles.SearchInput}>
64
+ <SearchInput />
65
<button
66
className={styles.IconButton}
67
title="Select an element in the page to inspect it"
src/devtools/views/root.css
+8
-2
@@ -10,9 +10,12 @@
10
--theme-light-color-border: #eeeeee;
11
--theme-light-component-name: #8155cb;
12
--theme-light-component-name-inverted: #ffffff;
13
- --theme-light-deemphasized: #cfd1d5;
13
+ --theme-light-dim: #777d88;
14
+ --theme-light-dimmer: #cfd1d5;
15
--theme-light-jsx-arrow-brackets: #333333;
16
--theme-light-jsx-arrow-brackets-inverted: rgba(255, 255, 255, 0.7);
17
+ --theme-light-search-match: yellow;
18
+ --theme-light-search-match-current: orange;
19
--theme-light-tree-node-selected: #0088fa;
20
--theme-light-tree-node-hover: #ebf1fb;
21
@@ -29,7 +32,10 @@
32
--color-button-hover: var(--theme-light-button-hover);
33
--color-component-name: var(--theme-light-component-name);
34
--color-component-name-inverted: var(--theme-light-component-name-inverted);
32
- --color-deemphasized: var(--theme-light-deemphasized);
35
+ --color-dim: var(--theme-light-dim);
36
+ --color-dimmer: var(--theme-light-dimmer);
37
+ --color-search-match: var(--theme-light-search-match);
38
+ --color-search-match-current: var(--theme-light-search-match-current);
39
--color-tree-jsx-arrow-brackets: var(--theme-light-jsx-arrow-brackets);
40
--color-tree-jsx-arrow-brackets-inverted: var(
41
--theme-light-jsx-arrow-brackets-inverted