@samitouri / QOS-React-2 / commits / 5a269e7ced

Initial POC for compressing horizontal offset for wide/deep trees

Brian Vaughn committed May 31, 2019 at 11:24 UTC 5a269e7ced1011885e3622c2bde8d30fcd8ad3ab
10 files changed +102 -197
src/devtools/views/Components/Element.css
+7 -18
@@ -9,6 +9,7 @@
9 align-items: center;
10 cursor: default;
11 user-select: none;
12 + color: var(--color-component-name);
13 }
14 .HoveredElement {
15 background-color: var(--color-background-hover);
@@ -17,6 +18,10 @@
18 background-color: var(--color-background-inactive);
19 }
20
21 +.Bracket {
22 + color: var(--color-jsx-arrow-brackets);
23 +}
24 +
25 .ScrollAnchor {
26 height: 100%;
27 width: 0;
@@ -37,24 +42,6 @@
42 --color-expand-collapse-toggle: var(--color-component-name-inverted);
43 }
44
40 -.DollarR {
41 - color: var(--color-jsx-arrow-brackets);
42 -}
43 -
44 -.Component {
45 - color: var(--color-component-name);
46 -}
47 -.Component:before {
48 - white-space: nowrap;
49 - content: '<';
50 - color: var(--color-jsx-arrow-brackets);
51 -}
52 -.Component:after {
53 - white-space: nowrap;
54 - content: '>';
55 - color: var(--color-jsx-arrow-brackets);
56 -}
57 -
45 .AttributeName {
46 color: var(--color-attribute-name);
47 }
@@ -74,10 +61,12 @@
61 display: inline-flex;
62 width: 1rem;
63 height: 1rem;
64 + flex: 0 0 1rem;
65 color: var(--color-expand-collapse-toggle);
66 }
67
68 .Badge {
69 + color: var(--color-text);
70 background-color: var(--color-component-badge-background);
71 padding: 0.125rem 0.25rem;
72 line-height: normal;
src/devtools/views/Components/Element.js
+18 -95
@@ -4,20 +4,13 @@ import React, {
4 Fragment,
5 useCallback,
6 useContext,
7 - useLayoutEffect,
7 useMemo,
9 - useRef,
8 useState,
9 } from 'react';
12 -import {
13 - ElementTypeClass,
14 - ElementTypeFunction,
15 - ElementTypeMemo,
16 - ElementTypeForwardRef,
17 -} from 'src/types';
10 +import { ElementTypeMemo, ElementTypeForwardRef } from 'src/types';
11 import Store from 'src/devtools/store';
12 import ButtonIcon from '../ButtonIcon';
20 -import { createRegExp } from '../utils';
13 +import { createRegExp, truncateText } from '../utils';
14 import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
15 import { StoreContext } from '../context';
16
@@ -46,13 +39,7 @@ export default function ElementView({ data, index, style }: Props) {
39
40 const [isHovered, setIsHovered] = useState(false);
41
49 - const {
50 - lastScrolledIDRef,
51 - isNavigatingWithKeyboard,
52 - onElementMouseEnter,
53 - showIndentLines,
54 - treeFocused,
55 - } = data;
42 + const { isNavigatingWithKeyboard, onElementMouseEnter, treeFocused } = data;
43 const id = element === null ? null : element.id;
44 const isSelected = selectedElementID === id;
45
@@ -62,49 +49,6 @@ export default function ElementView({ data, index, style }: Props) {
49 }
50 }, [dispatch, id]);
51
65 - const scrollAnchorStartRef = useRef<HTMLSpanElement | null>(null);
66 - const scrollAnchorEndRef = useRef<HTMLSpanElement | null>(null);
67 -
68 - // The tree above has its own autoscrolling, but it only works for rows.
69 - // However, even when the row gets into the viewport, the component name
70 - // might be too far left or right on the screen. Adjust it in this case.
71 - useLayoutEffect(() => {
72 - if (isSelected) {
73 - // Don't select the same item twice.
74 - // A row may appear and disappear just by scrolling:
75 - // https://github.com/bvaughn/react-devtools-experimental/issues/67
76 - // It doesn't necessarily indicate a user action.
77 - // TODO: we might want to revamp the autoscroll logic
78 - // to only happen explicitly for user-initiated events.
79 - if (lastScrolledIDRef.current === id) {
80 - return;
81 - }
82 - lastScrolledIDRef.current = id;
83 -
84 - // We want to bring the whole <Component> name into view,
85 - // including the expansion toggle and the "=== $r" hint.
86 - // However, even calling scrollIntoView() on a wrapper parent node (e.g. <span>)
87 - // wouldn't guarantee that it will be *fully* brought into view.
88 - // As a workaround, we'll have two anchor spans, and scroll each into view.
89 - if (scrollAnchorEndRef.current !== null) {
90 - scrollAnchorEndRef.current.scrollIntoView({
91 - behavior: 'auto',
92 - block: 'nearest',
93 - inline: 'nearest',
94 - });
95 - }
96 - if (scrollAnchorStartRef.current !== null) {
97 - // We scroll the start anchor last because it's
98 - // more important for it to be in the view.
99 - scrollAnchorStartRef.current.scrollIntoView({
100 - behavior: 'auto',
101 - block: 'nearest',
102 - inline: 'nearest',
103 - });
104 - }
105 - }
106 - }, [id, isSelected, lastScrolledIDRef]);
107 -
52 const handleMouseDown = useCallback(
53 ({ metaKey }) => {
54 if (id !== null) {
@@ -138,8 +82,6 @@ export default function ElementView({ data, index, style }: Props) {
82
83 const { depth, displayName, key, type } = ((element: any): Element);
84
141 - const showDollarR =
142 - isSelected && (type === ElementTypeClass || type === ElementTypeFunction);
85 const showBadge = type === ElementTypeMemo || type === ElementTypeForwardRef;
86
87 let className = styles.Element;
@@ -162,45 +104,26 @@ export default function ElementView({ data, index, style }: Props) {
104 ...style, // "style" comes from react-window
105
106 // Left padding presents the appearance of a nested tree structure.
165 - paddingLeft: '0.25rem',
166 -
167 - // These style overrides enable the background color to fill the full visible width,
168 - // when combined with the CSS tweaks in Tree.
169 - // A lot of options were considered; this seemed the one that requires the least code.
170 - // See https://github.com/bvaughn/react-devtools-experimental/issues/9
171 - width: undefined,
172 - minWidth: '100%',
173 - position: 'relative',
174 - marginBottom: `-${style.height}px`,
107 + // We must use padding rather than margin/left because of the selected background color.
108 + paddingLeft: `calc(${depth} * var(--indentation-size))`,
109 }}
110 >
177 - <div
178 - style={{
179 - width: `${depth * 0.75}rem`,
180 - height: '100%',
181 - backgroundSize: '0.75rem 1rem',
182 - backgroundColor: 'transparent',
183 - backgroundImage: showIndentLines
184 - ? 'linear-gradient(to right, transparent 8px, var(--color-guideline) 8px, transparent 9px)'
185 - : '',
186 - backgroundRepeat: 'repeat',
187 - }}
188 - />
189 - <span className={styles.ScrollAnchor} ref={scrollAnchorStartRef} />
111 {ownerID === null ? (
112 <ExpandCollapseToggle element={element} store={store} />
113 ) : null}
193 - <span className={styles.Component}>
194 - <DisplayName displayName={displayName} id={((id: any): number)} />
195 - {key && (
196 - <Fragment>
197 - &nbsp;<span className={styles.AttributeName}>key</span>=
198 - <span className={styles.AttributeValue}>"{key}"</span>
199 - </Fragment>
200 - )}
201 - </span>
202 - {showDollarR && <span className={styles.DollarR}>&nbsp;== $r</span>}
203 - <span className={styles.ScrollAnchor} ref={scrollAnchorEndRef} />
114 +
115 + <span className={styles.Bracket}>&lt;</span>
116 + <DisplayName displayName={displayName} id={((id: any): number)} />
117 + {key && (
118 + <Fragment>
119 + &nbsp;<span className={styles.AttributeName}>key</span>=
120 + <span className={styles.AttributeValue} title={key}>
121 + "{truncateText(`${key}`, 10)}"
122 + </span>
123 + </Fragment>
124 + )}
125 + <span className={styles.Bracket}>&gt;</span>
126 +
127 {showBadge && (
128 <span className={styles.Badge}>
129 {type === ElementTypeMemo ? 'Memo' : 'ForwardRef'}
src/devtools/views/Components/Guideline.css renamed
src/devtools/views/Components/Guideline.js renamed
+13 -30
@@ -1,6 +1,6 @@
1 // @flow
2
3 -import React, { Fragment, useContext, useMemo } from 'react';
3 +import React, { useContext, useMemo } from 'react';
4 import { TreeStateContext } from './TreeContext';
5 import TreeFocusedContext from './TreeFocusedContext';
6 import { SettingsContext } from '../Settings/SettingsContext';
@@ -8,23 +8,7 @@ import { StoreContext } from '../context';
8 import { useSubscription } from '../hooks';
9 import Store from '../../store';
10
11 -import styles from './Guidelines.css';
12 -
13 -export default function Guidelines(_: {||}) {
14 - const { selectedElementID } = useContext(TreeStateContext);
15 - const treeFocused = useContext(TreeFocusedContext);
16 -
17 - return (
18 - <Fragment>
19 - <Guideline
20 - className={
21 - treeFocused ? styles.GuidelineActive : styles.GuidelineInactive
22 - }
23 - elementID={selectedElementID}
24 - />
25 - </Fragment>
26 - );
27 -}
11 +import styles from './Guideline.css';
12
13 type Data = {|
14 depth: number,
@@ -32,23 +16,20 @@ type Data = {|
16 stopIndex: number,
17 |};
18
35 -type Props = {|
36 - className: string,
37 - elementID: number | null,
38 -|};
39 -
40 -function Guideline({ className, elementID }: Props) {
41 - const store = useContext(StoreContext);
19 +export default function Guideline(_: {||}) {
20 const { lineHeight } = useContext(SettingsContext);
21 + const store = useContext(StoreContext);
22 + const { selectedElementID } = useContext(TreeStateContext);
23 + const treeFocused = useContext(TreeFocusedContext);
24
25 const subscription = useMemo(
26 () => ({
27 getCurrentValue: () => {
47 - if (elementID === null) {
28 + if (selectedElementID === null) {
29 return null;
30 }
31
51 - const element = store.getElementByID(elementID);
32 + const element = store.getElementByID(selectedElementID);
33 if (
34 element === null ||
35 element.isCollapsed ||
@@ -92,7 +73,7 @@ function Guideline({ className, elementID }: Props) {
73 };
74 },
75 }),
95 - [elementID, store]
76 + [selectedElementID, store]
77 );
78 const data = useSubscription<Data | null, Store>(subscription);
79
@@ -104,11 +85,13 @@ function Guideline({ className, elementID }: Props) {
85
86 return (
87 <div
107 - className={className}
88 + className={
89 + treeFocused ? styles.GuidelineActive : styles.GuidelineInactive
90 + }
91 style={{
92 position: 'absolute',
93 top: `${startIndex * lineHeight}px`,
111 - left: `${depth * 0.75 + 0.75}rem`,
94 + left: `calc(${depth} * var(--indentation-size) + 0.5rem)`,
95 height: `${(stopIndex + 1 - startIndex) * lineHeight}px`,
96 }}
97 />
src/devtools/views/Components/SelectedElement.css
+4 -2
@@ -33,11 +33,13 @@
33
34 .Component,
35 .Owner {
36 - overflow: hidden;
37 - text-overflow: ellipsis;
36 color: var(--color-component-name);
37 font-family: var(--font-family-monospace);
38 font-size: var(--font-size-monospace-normal);
39 + white-space: nowrap;
40 + overflow: hidden;
41 + text-overflow: ellipsis;
42 + max-width: 100%;
43 }
44 .Component:before,
45 .Owner:before {
src/devtools/views/Components/Tree.css
+8
@@ -7,6 +7,14 @@
7 border-top: 1px solid var(--color-border);
8 }
9
10 +.List {
11 + overflow-x: hidden !important;
12 +}
13 +
14 +.InnerElementType {
15 + overflow-x: hidden;
16 +}
17 +
18 .SearchInput {
19 flex: 0 0 42px;
20 display: flex;
src/devtools/views/Components/Tree.js
+36 -18
@@ -22,7 +22,7 @@ import SearchInput from './SearchInput';
22 import { ComponentFiltersModalContextController } from './ComponentFiltersModalContext';
23 import ToggleComponentFiltersModalButton from './ToggleComponentFiltersModalButton';
24 import ComponentFiltersModal from './ComponentFiltersModal';
25 -import Guidelines from './Guidelines';
25 +import Guideline from './Guideline';
26 import TreeFocusedContext from './TreeFocusedContext';
27
28 import styles from './Tree.css';
@@ -32,7 +32,6 @@ export type ItemData = {|
32 isNavigatingWithKeyboard: boolean,
33 lastScrolledIDRef: { current: number | null },
34 onElementMouseEnter: (id: number) => void,
35 - showIndentLines: boolean,
35 treeFocused: boolean,
36 |};
37
@@ -60,7 +59,7 @@ export default function Tree(props: Props) {
59
60 const [treeFocused, setTreeFocused] = useState<boolean>(false);
61
63 - const { lineHeight, showIndentLines } = useContext(SettingsContext);
62 + const { lineHeight } = useContext(SettingsContext);
63
64 // Make sure a newly selected element is visible in the list.
65 // This is helpful for things like the owners list and search.
@@ -264,7 +263,6 @@ export default function Tree(props: Props) {
263 isNavigatingWithKeyboard,
264 onElementMouseEnter: handleElementMouseEnter,
265 lastScrolledIDRef,
267 - showIndentLines,
266 treeFocused,
267 }),
268 [
@@ -272,7 +270,6 @@ export default function Tree(props: Props) {
270 isNavigatingWithKeyboard,
271 handleElementMouseEnter,
272 lastScrolledIDRef,
275 - showIndentLines,
273 treeFocused,
274 ]
275 );
@@ -310,7 +307,6 @@ export default function Tree(props: Props) {
307 itemCount={numElements}
308 itemData={itemData}
309 itemSize={lineHeight}
313 - overscanCount={3}
310 ref={listRef}
311 width={width}
312 >
@@ -326,6 +322,31 @@ export default function Tree(props: Props) {
322 );
323 }
324
325 +function updateIndentationSizeVar(innerDiv: HTMLDivElement): void {
326 + const list = ((innerDiv.parentElement: any): HTMLDivElement);
327 +
328 + let indentationSize =
329 + parseFloat(getComputedStyle(list).getPropertyValue('--indentation-size')) ||
330 + 12;
331 +
332 + let maxChildWidth = 0;
333 + for (let child of innerDiv.children) {
334 + const { lastElementChild } = child;
335 + // Skip over e.g. the guideline element
336 + if (lastElementChild != null) {
337 + const bounds = ((lastElementChild.getBoundingClientRect(): any): DOMRect);
338 + maxChildWidth = Math.max(maxChildWidth, bounds.x + bounds.width);
339 + }
340 + }
341 +
342 + indentationSize = Math.min(
343 + 12,
344 + (list.clientWidth / maxChildWidth) * indentationSize
345 + );
346 +
347 + list.style.setProperty('--indentation-size', `${indentationSize}px`);
348 +}
349 +
350 function InnerElementType({ children, style, ...rest }) {
351 const { ownerID } = useContext(TreeStateContext);
352
@@ -335,13 +356,12 @@ function InnerElementType({ children, style, ...rest }) {
356 // and ensure that once we've grown to a new max size, we don't shrink below it.
357 // This improves the user experience when scrolling between wide and narrow rows.
358 const divRef = useRef<HTMLDivElement | null>(null);
338 - const [minWidth, setMinWidth] = useState(null);
359 +
360 // TODO This is a valid warning, but we're ignoring it for the time being.
361 // eslint-disable-next-line react-hooks/exhaustive-deps
362 useEffect(() => {
363 if (divRef.current !== null) {
343 - const measuredWidth = divRef.current.offsetWidth;
344 - setMinWidth(w => Math.max(w || 0, measuredWidth));
364 + updateIndentationSizeVar(divRef.current);
365 }
366 });
367
@@ -349,7 +369,11 @@ function InnerElementType({ children, style, ...rest }) {
369 // This will cause a render with 100% min-width, a measurement
370 // in an effect, and a second render where we know the width.
371 useEffect(() => {
352 - const invalidateMinWidth = () => setMinWidth(null);
372 + const invalidateMinWidth = () => {
373 + if (divRef.current !== null) {
374 + updateIndentationSizeVar(divRef.current);
375 + }
376 + };
377 window.addEventListener('resize', invalidateMinWidth);
378 return () => window.removeEventListener('resize', invalidateMinWidth);
379 }, []);
@@ -359,7 +383,6 @@ function InnerElementType({ children, style, ...rest }) {
383 const [prevOwnerID, setPrevOwnerID] = useState(ownerID);
384 if (ownerID !== prevOwnerID) {
385 setPrevOwnerID(ownerID);
362 - setMinWidth(null);
386 }
387
388 // This style override enables the background color to fill the full visible width,
@@ -369,16 +392,11 @@ function InnerElementType({ children, style, ...rest }) {
392 return (
393 <div
394 className={styles.InnerElementType}
372 - style={{
373 - ...style,
374 - display: 'inline-block',
375 - minWidth: minWidth || '100%',
376 - width: undefined,
377 - }}
395 + style={style}
396 ref={divRef}
397 {...rest}
398 >
381 - <Guidelines />
399 + <Guideline />
400 {children}
401 </div>
402 );
src/devtools/views/Settings/Settings.js
+3 -23
@@ -11,14 +11,9 @@ import styles from './Settings.css';
11
12 function Settings(_: {||}) {
13 const store = useContext(StoreContext);
14 - const {
15 - displayDensity,
16 - setDisplayDensity,
17 - showIndentLines,
18 - setShowIndentLines,
19 - theme,
20 - setTheme,
21 - } = useContext(SettingsContext);
14 + const { displayDensity, setDisplayDensity, theme, setTheme } = useContext(
15 + SettingsContext
16 + );
17
18 const captureScreenshotsSubscription = useMemo(
19 () => ({
@@ -62,13 +57,6 @@ function Settings(_: {||}) {
57 [setTheme]
58 );
59
65 - const updateShowIndentLines = useCallback(
66 - ({ currentTarget }) => {
67 - setShowIndentLines(currentTarget.checked);
68 - },
69 - [setShowIndentLines]
70 - );
71 -
60 const updateCaptureScreenshotsWhileProfiling = useCallback(
61 ({ currentTarget }) => {
62 store.captureScreenshots = currentTarget.checked;
@@ -155,14 +143,6 @@ function Settings(_: {||}) {
143 />{' '}
144 Collapse newly added components by default
145 </label>
158 - <label className={styles.CheckboxOption}>
159 - <input
160 - type="checkbox"
161 - checked={showIndentLines}
162 - onChange={updateShowIndentLines}
163 - />{' '}
164 - Show indent lines
165 - </label>
146 </div>
147
148 {store.supportsCaptureScreenshots && (
src/devtools/views/Settings/SettingsContext.js
-11
@@ -16,9 +16,6 @@ type Context = {|
16 // Specified as a separate prop so it can trigger a re-render of FixedSizeList.
17 lineHeight: number,
18
19 - showIndentLines: boolean,
20 - setShowIndentLines: (value: boolean) => void,
21 -
19 theme: Theme,
20 setTheme(value: Theme): void,
21 |};
@@ -47,10 +44,6 @@ function SettingsContextController({
44 'React::DevTools::displayDensity',
45 'compact'
46 );
50 - const [showIndentLines, setShowIndentLines] = useLocalStorage<boolean>(
51 - 'React::DevTools::showIndentLines',
52 - true
53 - );
47 const [theme, setTheme] = useLocalStorage<Theme>(
48 'React::DevTools::theme',
49 'auto'
@@ -133,8 +126,6 @@ function SettingsContextController({
126 setDisplayDensity,
127 theme,
128 setTheme,
136 - showIndentLines,
137 - setShowIndentLines,
129 lineHeight:
130 displayDensity === 'compact'
131 ? compactLineHeight
@@ -144,9 +135,7 @@ function SettingsContextController({
135 comfortableLineHeight,
136 compactLineHeight,
137 displayDensity,
147 - showIndentLines,
138 setDisplayDensity,
149 - setShowIndentLines,
139 setTheme,
140 theme,
141 ]
src/devtools/views/utils.js
+13
@@ -170,3 +170,16 @@ export function downloadFile(filename: string, text: string): void {
170
171 ((document.body: any): HTMLBodyElement).removeChild(element);
172 }
173 +
174 +export function truncateText(text: string, maxLength: number): string {
175 + const { length } = text;
176 + if (length > maxLength) {
177 + return (
178 + text.substr(0, Math.floor(maxLength / 2)) +
179 + '…' +
180 + text.substr(length - Math.ceil(maxLength / 2) + 1)
181 + );
182 + } else {
183 + return text;
184 + }
185 +}