Added selected and hovered guidelines to Components tree
Brian Vaughn committed
May 30, 2019 at 08:22 UTC
ec421355bd1680f3509bda00c5fe9d04dbffc06d
9 files changed
+265
-57
src/devtools/views/Components/Components.js
+15
-12
@@ -3,6 +3,7 @@
3
import React, { Suspense } from 'react';
4
import Tree from './Tree';
5
import SelectedElement from './SelectedElement';
6
+import { HoveredElementContextController } from './HoveredElementContext';
7
import { InspectedElementContextController } from './InspectedElementContext';
8
import { OwnersListContextController } from './OwnersListContext';
9
import portaledContent from '../portaledContent';
@@ -14,19 +15,21 @@ function Components(_: {||}) {
15
// TODO Flex wrappers below should be user resizable.
16
return (
17
<OwnersListContextController>
17
- <InspectedElementContextController>
18
- <div className={styles.Components}>
19
- <div className={styles.TreeWrapper}>
20
- <Tree />
18
+ <HoveredElementContextController>
19
+ <InspectedElementContextController>
20
+ <div className={styles.Components}>
21
+ <div className={styles.TreeWrapper}>
22
+ <Tree />
23
+ </div>
24
+ <div className={styles.SelectedElementWrapper}>
25
+ <Suspense fallback={<Loading />}>
26
+ <SelectedElement />
27
+ </Suspense>
28
+ </div>
29
+ <ModalDialog />
30
</div>
22
- <div className={styles.SelectedElementWrapper}>
23
- <Suspense fallback={<Loading />}>
24
- <SelectedElement />
25
- </Suspense>
26
- </div>
27
- <ModalDialog />
28
- </div>
29
- </InspectedElementContextController>
31
+ </InspectedElementContextController>
32
+ </HoveredElementContextController>
33
</OwnersListContextController>
34
);
35
}
src/devtools/views/Components/Element.js
+4
-1
@@ -19,6 +19,7 @@ import Store from 'src/devtools/store';
19
import ButtonIcon from '../ButtonIcon';
20
import { createRegExp } from '../utils';
21
import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
22
+import { HoveredElementSetIDContext } from './HoveredElementContext';
23
import { StoreContext } from '../context';
24
25
import type { ItemData } from './Tree';
@@ -38,6 +39,7 @@ export default function ElementView({ data, index, style }: Props) {
39
TreeStateContext
40
);
41
const dispatch = useContext(TreeDispatcherContext);
42
+ const setHoveredElementID = useContext(HoveredElementSetIDContext);
43
44
const element =
45
ownerFlatTree !== null
@@ -119,9 +121,10 @@ export default function ElementView({ data, index, style }: Props) {
121
const handleMouseEnter = useCallback(() => {
122
setIsHovered(true);
123
if (id !== null) {
124
+ setHoveredElementID(id);
125
onElementMouseEnter(id);
126
}
124
- }, [onElementMouseEnter, id]);
127
+ }, [id, onElementMouseEnter, setHoveredElementID]);
128
129
const handleMouseLeave = useCallback(() => {
130
setIsHovered(false);
src/devtools/views/Components/Guidelines.css
new
+18
@@ -0,0 +1,18 @@
1
+.GuidelineHovered,
2
+.GuidelineSelectedActive,
3
+.GuidelineSelectedInactive {
4
+ width: 1px;
5
+ z-index: 2;
6
+}
7
+
8
+.GuidelineHovered {
9
+ background-color: var(--color-guideline-hovered);
10
+}
11
+
12
+.GuidelineSelectedActive {
13
+ background-color: var(--color-background-selected);
14
+}
15
+
16
+.GuidelineSelectedInactive {
17
+ background-color: var(--color-background-inactive);
18
+}
src/devtools/views/Components/Guidelines.js
new
+126
@@ -0,0 +1,126 @@
1
+// @flow
2
+
3
+import React, { Fragment, useContext, useMemo } from 'react';
4
+import { HoveredElementIDContext } from './HoveredElementContext';
5
+import { TreeStateContext } from './TreeContext';
6
+import TreeFocusedContext from './TreeFocusedContext';
7
+import { SettingsContext } from '../Settings/SettingsContext';
8
+import { StoreContext } from '../context';
9
+import { useSubscription } from '../hooks';
10
+import Store from '../../store';
11
+
12
+import styles from './Guidelines.css';
13
+
14
+export default function Guidelines(_: {||}) {
15
+ const hoveredElementID = useContext(HoveredElementIDContext);
16
+ const { selectedElementID } = useContext(TreeStateContext);
17
+ const treeFocused = useContext(TreeFocusedContext);
18
+
19
+ return (
20
+ <Fragment>
21
+ {hoveredElementID !== selectedElementID && (
22
+ <Guideline
23
+ className={styles.GuidelineHovered}
24
+ elementID={hoveredElementID}
25
+ />
26
+ )}
27
+ <Guideline
28
+ className={
29
+ treeFocused
30
+ ? styles.GuidelineSelectedActive
31
+ : styles.GuidelineSelectedInactive
32
+ }
33
+ elementID={selectedElementID}
34
+ />
35
+ </Fragment>
36
+ );
37
+}
38
+
39
+type Data = {|
40
+ depth: number,
41
+ startIndex: number,
42
+ stopIndex: number,
43
+|};
44
+
45
+type Props = {|
46
+ className: string,
47
+ elementID: number | null,
48
+|};
49
+
50
+function Guideline({ className, elementID }: Props) {
51
+ const store = useContext(StoreContext);
52
+ const { lineHeight } = useContext(SettingsContext);
53
+
54
+ const subscription = useMemo(
55
+ () => ({
56
+ getCurrentValue: () => {
57
+ if (elementID === null) {
58
+ return null;
59
+ }
60
+
61
+ const element = store.getElementByID(elementID);
62
+ if (
63
+ element === null ||
64
+ element.isCollapsed ||
65
+ element.children.length === 0
66
+ ) {
67
+ return null;
68
+ }
69
+
70
+ const startIndex = store.getIndexOfElementID(element.children[0]);
71
+ if (startIndex === null) {
72
+ return null;
73
+ }
74
+
75
+ let stopIndex = null;
76
+ let current = element;
77
+ while (current !== null) {
78
+ if (current.isCollapsed || current.children.length === 0) {
79
+ // We've found the last/deepest descendant.
80
+ stopIndex = store.getIndexOfElementID(current.id);
81
+ current = null;
82
+ } else {
83
+ const lastChildID = current.children[current.children.length - 1];
84
+ current = store.getElementByID(lastChildID);
85
+ }
86
+ }
87
+
88
+ if (stopIndex === null) {
89
+ return null;
90
+ }
91
+
92
+ return {
93
+ depth: element.depth,
94
+ startIndex,
95
+ stopIndex,
96
+ };
97
+ },
98
+ subscribe: (callback: Function) => {
99
+ store.addListener('mutated', callback);
100
+ return () => {
101
+ store.removeListener('mutated', callback);
102
+ };
103
+ },
104
+ }),
105
+ [elementID, store]
106
+ );
107
+ const data = useSubscription<Data | null, Store>(subscription);
108
+
109
+ if (data === null) {
110
+ return null;
111
+ }
112
+
113
+ const { depth, startIndex, stopIndex } = data;
114
+
115
+ return (
116
+ <div
117
+ className={className}
118
+ style={{
119
+ position: 'absolute',
120
+ top: `${startIndex * lineHeight}px`,
121
+ left: `${depth * 0.75 + 0.75}rem`,
122
+ height: `${(stopIndex + 1 - startIndex) * lineHeight}px`,
123
+ }}
124
+ />
125
+ );
126
+}
src/devtools/views/Components/HoveredElementContext.js
new
+34
@@ -0,0 +1,34 @@
1
+// @flow
2
+
3
+import React, { createContext, useState } from 'react';
4
+
5
+type ID = number | null;
6
+type SetID = (id: ID) => void;
7
+
8
+const HoveredElementIDContext = createContext<ID>(null);
9
+HoveredElementIDContext.displayName = 'HoveredElementIDContext';
10
+
11
+const HoveredElementSetIDContext = createContext<SetID>(((null: any): SetID));
12
+HoveredElementSetIDContext.displayName = 'HoveredElementSetIDContext';
13
+
14
+type Props = {|
15
+ children: React$Node,
16
+|};
17
+
18
+function HoveredElementContextController({ children }: Props) {
19
+ const [hoveredElementID, setHoveredElementID] = useState<ID>(null);
20
+
21
+ return (
22
+ <HoveredElementIDContext.Provider value={hoveredElementID}>
23
+ <HoveredElementSetIDContext.Provider value={setHoveredElementID}>
24
+ {children}
25
+ </HoveredElementSetIDContext.Provider>
26
+ </HoveredElementIDContext.Provider>
27
+ );
28
+}
29
+
30
+export {
31
+ HoveredElementIDContext,
32
+ HoveredElementSetIDContext,
33
+ HoveredElementContextController,
34
+};
src/devtools/views/Components/Tree.js
+58
-44
@@ -22,6 +22,9 @@ import SearchInput from './SearchInput';
22
import { ComponentFiltersModalContextController } from './ComponentFiltersModalContext';
23
import ToggleComponentFiltersModalButton from './ToggleComponentFiltersModalButton';
24
import ComponentFiltersModal from './ComponentFiltersModal';
25
+import { HoveredElementSetIDContext } from './HoveredElementContext';
26
+import Guidelines from './Guidelines';
27
+import TreeFocusedContext from './TreeFocusedContext';
28
29
import styles from './Tree.css';
30
@@ -273,54 +276,57 @@ export default function Tree(props: Props) {
276
);
277
278
return (
276
- <ComponentFiltersModalContextController>
277
- <div className={styles.Tree} ref={treeRef}>
278
- <div className={styles.SearchInput}>
279
- <InspectHostNodesToggle />
280
- <div className={styles.VRule} />
281
- <Suspense fallback={<Loading />}>
282
- {ownerID !== null ? <OwnersStack /> : <SearchInput />}
283
- </Suspense>
284
- <div className={styles.VRule} />
285
- <ToggleComponentFiltersModalButton />
279
+ <TreeFocusedContext.Provider value={treeFocused}>
280
+ <ComponentFiltersModalContextController>
281
+ <div className={styles.Tree} ref={treeRef}>
282
+ <div className={styles.SearchInput}>
283
+ <InspectHostNodesToggle />
284
+ <div className={styles.VRule} />
285
+ <Suspense fallback={<Loading />}>
286
+ {ownerID !== null ? <OwnersStack /> : <SearchInput />}
287
+ </Suspense>
288
+ <div className={styles.VRule} />
289
+ <ToggleComponentFiltersModalButton />
290
+ </div>
291
+ <div
292
+ className={styles.AutoSizerWrapper}
293
+ onBlur={handleBlur}
294
+ onFocus={handleFocus}
295
+ onKeyPress={handleKeyPress}
296
+ onMouseMove={handleMouseMove}
297
+ onMouseLeave={handleMouseLeave}
298
+ ref={focusTargetRef}
299
+ tabIndex={0}
300
+ >
301
+ <AutoSizer>
302
+ {({ height, width }) => (
303
+ // $FlowFixMe https://github.com/facebook/flow/issues/7341
304
+ <FixedSizeList
305
+ className={styles.List}
306
+ height={height}
307
+ innerElementType={InnerElementType}
308
+ itemCount={numElements}
309
+ itemData={itemData}
310
+ itemSize={lineHeight}
311
+ overscanCount={3}
312
+ ref={listRef}
313
+ width={width}
314
+ >
315
+ {ElementView}
316
+ </FixedSizeList>
317
+ )}
318
+ </AutoSizer>
319
+ </div>
320
+ <ComponentFiltersModal />
321
</div>
287
- <div
288
- className={styles.AutoSizerWrapper}
289
- onBlur={handleBlur}
290
- onFocus={handleFocus}
291
- onKeyPress={handleKeyPress}
292
- onMouseMove={handleMouseMove}
293
- onMouseLeave={handleMouseLeave}
294
- ref={focusTargetRef}
295
- tabIndex={0}
296
- >
297
- <AutoSizer>
298
- {({ height, width }) => (
299
- // $FlowFixMe https://github.com/facebook/flow/issues/7341
300
- <FixedSizeList
301
- className={styles.List}
302
- height={height}
303
- innerElementType={InnerElementType}
304
- itemCount={numElements}
305
- itemData={itemData}
306
- itemSize={lineHeight}
307
- overscanCount={3}
308
- ref={listRef}
309
- width={width}
310
- >
311
- {ElementView}
312
- </FixedSizeList>
313
- )}
314
- </AutoSizer>
315
- </div>
316
- <ComponentFiltersModal />
317
- </div>
318
- </ComponentFiltersModalContextController>
322
+ </ComponentFiltersModalContextController>
323
+ </TreeFocusedContext.Provider>
324
);
325
}
326
322
-function InnerElementType({ style, ...rest }) {
327
+function InnerElementType({ children, style, ...rest }) {
328
const { ownerID } = useContext(TreeStateContext);
329
+ const setHoveredElementID = useContext(HoveredElementSetIDContext);
330
331
// The list may need to scroll horizontally due to deeply nested elements.
332
// We don't know the maximum scroll width up front, because we're windowing.
@@ -355,6 +361,10 @@ function InnerElementType({ style, ...rest }) {
361
setMinWidth(null);
362
}
363
364
+ const handleMouseLeave = useCallback(() => {
365
+ setHoveredElementID(null);
366
+ }, [setHoveredElementID]);
367
+
368
// This style override enables the background color to fill the full visible width,
369
// when combined with the CSS tweaks in Element.
370
// A lot of options were considered; this seemed the one that requires the least code.
@@ -362,6 +372,7 @@ function InnerElementType({ style, ...rest }) {
372
return (
373
<div
374
className={styles.InnerElementType}
375
+ onMouseLeave={handleMouseLeave}
376
style={{
377
...style,
378
display: 'inline-block',
@@ -370,7 +381,10 @@ function InnerElementType({ style, ...rest }) {
381
}}
382
ref={divRef}
383
{...rest}
373
- />
384
+ >
385
+ <Guidelines />
386
+ {children}
387
+ </div>
388
);
389
}
390
src/devtools/views/Components/TreeFocusedContext.js
new
+7
@@ -0,0 +1,7 @@
1
+// @flow
2
+
3
+import { createContext } from 'react';
4
+
5
+const TreeFocusedContext = createContext<boolean>(false);
6
+
7
+export default TreeFocusedContext;
src/devtools/views/Settings/SettingsContext.js
+1
@@ -261,6 +261,7 @@ function updateThemeVariables(
261
updateStyleHelper(theme, 'color-dimmer', documentElements);
262
updateStyleHelper(theme, 'color-dimmest', documentElements);
263
updateStyleHelper(theme, 'color-expand-collapse-toggle', documentElements);
264
+ updateStyleHelper(theme, 'color-guideline-hovered', documentElements);
265
updateStyleHelper(theme, 'color-jsx-arrow-brackets', documentElements);
266
updateStyleHelper(
267
theme,
src/devtools/views/root.css
+2
@@ -42,6 +42,7 @@
42
--light-color-dimmer: #cfd1d5;
43
--light-color-dimmest: #eff0f1;
44
--light-color-expand-collapse-toggle: #777d88;
45
+ --light-color-guideline-hovered: rgba(0, 0, 0, 0.1);
46
--light-color-jsx-arrow-brackets: #333333;
47
--light-color-jsx-arrow-brackets-inverted: rgba(255, 255, 255, 0.7);
48
--light-color-modal-background: rgba(255, 255, 255, 0.75);
@@ -99,6 +100,7 @@
100
--dark-color-dimmer: #777d88;
101
--dark-color-dimmest: #4f5766;
102
--dark-color-expand-collapse-toggle: #8f949d;
103
+ --dark-color-guideline-hovered: rgba(255, 255, 255, 0.1);
104
--dark-color-jsx-arrow-brackets: #777d88;
105
--dark-color-jsx-arrow-brackets-inverted: rgba(255, 255, 255, 0.7);
106
--dark-color-modal-background: rgba(0, 0, 0, 0.75);