@samitouri / QOS-React-2 / commits / ec421355bd

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