@samitouri / QOS-React / commits / 998e69a10d

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 &nbsp;<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