Initial POC for compressing horizontal offset for wide/deep trees

This commit is contained in:
Brian Vaughn
2019-05-31 11:24:34 -07:00
parent c0f686eed9
commit 5a269e7ced
10 changed files with 102 additions and 197 deletions
+7 -18
View File
@@ -9,6 +9,7 @@
align-items: center;
cursor: default;
user-select: none;
color: var(--color-component-name);
}
.HoveredElement {
background-color: var(--color-background-hover);
@@ -17,6 +18,10 @@
background-color: var(--color-background-inactive);
}
.Bracket {
color: var(--color-jsx-arrow-brackets);
}
.ScrollAnchor {
height: 100%;
width: 0;
@@ -37,24 +42,6 @@
--color-expand-collapse-toggle: var(--color-component-name-inverted);
}
.DollarR {
color: var(--color-jsx-arrow-brackets);
}
.Component {
color: var(--color-component-name);
}
.Component:before {
white-space: nowrap;
content: '<';
color: var(--color-jsx-arrow-brackets);
}
.Component:after {
white-space: nowrap;
content: '>';
color: var(--color-jsx-arrow-brackets);
}
.AttributeName {
color: var(--color-attribute-name);
}
@@ -74,10 +61,12 @@
display: inline-flex;
width: 1rem;
height: 1rem;
flex: 0 0 1rem;
color: var(--color-expand-collapse-toggle);
}
.Badge {
color: var(--color-text);
background-color: var(--color-component-badge-background);
padding: 0.125rem 0.25rem;
line-height: normal;
+18 -95
View File
@@ -4,20 +4,13 @@ import React, {
Fragment,
useCallback,
useContext,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
ElementTypeClass,
ElementTypeFunction,
ElementTypeMemo,
ElementTypeForwardRef,
} from 'src/types';
import { ElementTypeMemo, ElementTypeForwardRef } from 'src/types';
import Store from 'src/devtools/store';
import ButtonIcon from '../ButtonIcon';
import { createRegExp } from '../utils';
import { createRegExp, truncateText } from '../utils';
import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
import { StoreContext } from '../context';
@@ -46,13 +39,7 @@ export default function ElementView({ data, index, style }: Props) {
const [isHovered, setIsHovered] = useState(false);
const {
lastScrolledIDRef,
isNavigatingWithKeyboard,
onElementMouseEnter,
showIndentLines,
treeFocused,
} = data;
const { isNavigatingWithKeyboard, onElementMouseEnter, treeFocused } = data;
const id = element === null ? null : element.id;
const isSelected = selectedElementID === id;
@@ -62,49 +49,6 @@ export default function ElementView({ data, index, style }: Props) {
}
}, [dispatch, id]);
const scrollAnchorStartRef = useRef<HTMLSpanElement | null>(null);
const scrollAnchorEndRef = useRef<HTMLSpanElement | null>(null);
// The tree above has its own autoscrolling, but it only works for rows.
// However, even when the row gets into the viewport, the component name
// might be too far left or right on the screen. Adjust it in this case.
useLayoutEffect(() => {
if (isSelected) {
// Don't select the same item twice.
// A row may appear and disappear just by scrolling:
// https://github.com/bvaughn/react-devtools-experimental/issues/67
// It doesn't necessarily indicate a user action.
// TODO: we might want to revamp the autoscroll logic
// to only happen explicitly for user-initiated events.
if (lastScrolledIDRef.current === id) {
return;
}
lastScrolledIDRef.current = id;
// We want to bring the whole <Component> name into view,
// including the expansion toggle and the "=== $r" hint.
// However, even calling scrollIntoView() on a wrapper parent node (e.g. <span>)
// wouldn't guarantee that it will be *fully* brought into view.
// As a workaround, we'll have two anchor spans, and scroll each into view.
if (scrollAnchorEndRef.current !== null) {
scrollAnchorEndRef.current.scrollIntoView({
behavior: 'auto',
block: 'nearest',
inline: 'nearest',
});
}
if (scrollAnchorStartRef.current !== null) {
// We scroll the start anchor last because it's
// more important for it to be in the view.
scrollAnchorStartRef.current.scrollIntoView({
behavior: 'auto',
block: 'nearest',
inline: 'nearest',
});
}
}
}, [id, isSelected, lastScrolledIDRef]);
const handleMouseDown = useCallback(
({ metaKey }) => {
if (id !== null) {
@@ -138,8 +82,6 @@ export default function ElementView({ data, index, style }: Props) {
const { depth, displayName, key, type } = ((element: any): Element);
const showDollarR =
isSelected && (type === ElementTypeClass || type === ElementTypeFunction);
const showBadge = type === ElementTypeMemo || type === ElementTypeForwardRef;
let className = styles.Element;
@@ -162,45 +104,26 @@ export default function ElementView({ data, index, style }: Props) {
...style, // "style" comes from react-window
// Left padding presents the appearance of a nested tree structure.
paddingLeft: '0.25rem',
// These style overrides enable the background color to fill the full visible width,
// when combined with the CSS tweaks in Tree.
// A lot of options were considered; this seemed the one that requires the least code.
// See https://github.com/bvaughn/react-devtools-experimental/issues/9
width: undefined,
minWidth: '100%',
position: 'relative',
marginBottom: `-${style.height}px`,
// We must use padding rather than margin/left because of the selected background color.
paddingLeft: `calc(${depth} * var(--indentation-size))`,
}}
>
<div
style={{
width: `${depth * 0.75}rem`,
height: '100%',
backgroundSize: '0.75rem 1rem',
backgroundColor: 'transparent',
backgroundImage: showIndentLines
? 'linear-gradient(to right, transparent 8px, var(--color-guideline) 8px, transparent 9px)'
: '',
backgroundRepeat: 'repeat',
}}
/>
<span className={styles.ScrollAnchor} ref={scrollAnchorStartRef} />
{ownerID === null ? (
<ExpandCollapseToggle element={element} store={store} />
) : null}
<span className={styles.Component}>
<DisplayName displayName={displayName} id={((id: any): number)} />
{key && (
<Fragment>
&nbsp;<span className={styles.AttributeName}>key</span>=
<span className={styles.AttributeValue}>"{key}"</span>
</Fragment>
)}
</span>
{showDollarR && <span className={styles.DollarR}>&nbsp;== $r</span>}
<span className={styles.ScrollAnchor} ref={scrollAnchorEndRef} />
<span className={styles.Bracket}>&lt;</span>
<DisplayName displayName={displayName} id={((id: any): number)} />
{key && (
<Fragment>
&nbsp;<span className={styles.AttributeName}>key</span>=
<span className={styles.AttributeValue} title={key}>
"{truncateText(`${key}`, 10)}"
</span>
</Fragment>
)}
<span className={styles.Bracket}>&gt;</span>
{showBadge && (
<span className={styles.Badge}>
{type === ElementTypeMemo ? 'Memo' : 'ForwardRef'}
@@ -1,6 +1,6 @@
// @flow
import React, { Fragment, useContext, useMemo } from 'react';
import React, { useContext, useMemo } from 'react';
import { TreeStateContext } from './TreeContext';
import TreeFocusedContext from './TreeFocusedContext';
import { SettingsContext } from '../Settings/SettingsContext';
@@ -8,23 +8,7 @@ import { StoreContext } from '../context';
import { useSubscription } from '../hooks';
import Store from '../../store';
import styles from './Guidelines.css';
export default function Guidelines(_: {||}) {
const { selectedElementID } = useContext(TreeStateContext);
const treeFocused = useContext(TreeFocusedContext);
return (
<Fragment>
<Guideline
className={
treeFocused ? styles.GuidelineActive : styles.GuidelineInactive
}
elementID={selectedElementID}
/>
</Fragment>
);
}
import styles from './Guideline.css';
type Data = {|
depth: number,
@@ -32,23 +16,20 @@ type Data = {|
stopIndex: number,
|};
type Props = {|
className: string,
elementID: number | null,
|};
function Guideline({ className, elementID }: Props) {
const store = useContext(StoreContext);
export default function Guideline(_: {||}) {
const { lineHeight } = useContext(SettingsContext);
const store = useContext(StoreContext);
const { selectedElementID } = useContext(TreeStateContext);
const treeFocused = useContext(TreeFocusedContext);
const subscription = useMemo(
() => ({
getCurrentValue: () => {
if (elementID === null) {
if (selectedElementID === null) {
return null;
}
const element = store.getElementByID(elementID);
const element = store.getElementByID(selectedElementID);
if (
element === null ||
element.isCollapsed ||
@@ -92,7 +73,7 @@ function Guideline({ className, elementID }: Props) {
};
},
}),
[elementID, store]
[selectedElementID, store]
);
const data = useSubscription<Data | null, Store>(subscription);
@@ -104,11 +85,13 @@ function Guideline({ className, elementID }: Props) {
return (
<div
className={className}
className={
treeFocused ? styles.GuidelineActive : styles.GuidelineInactive
}
style={{
position: 'absolute',
top: `${startIndex * lineHeight}px`,
left: `${depth * 0.75 + 0.75}rem`,
left: `calc(${depth} * var(--indentation-size) + 0.5rem)`,
height: `${(stopIndex + 1 - startIndex) * lineHeight}px`,
}}
/>
@@ -33,11 +33,13 @@
.Component,
.Owner {
overflow: hidden;
text-overflow: ellipsis;
color: var(--color-component-name);
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-normal);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.Component:before,
.Owner:before {
+8
View File
@@ -7,6 +7,14 @@
border-top: 1px solid var(--color-border);
}
.List {
overflow-x: hidden !important;
}
.InnerElementType {
overflow-x: hidden;
}
.SearchInput {
flex: 0 0 42px;
display: flex;
+36 -18
View File
@@ -22,7 +22,7 @@ import SearchInput from './SearchInput';
import { ComponentFiltersModalContextController } from './ComponentFiltersModalContext';
import ToggleComponentFiltersModalButton from './ToggleComponentFiltersModalButton';
import ComponentFiltersModal from './ComponentFiltersModal';
import Guidelines from './Guidelines';
import Guideline from './Guideline';
import TreeFocusedContext from './TreeFocusedContext';
import styles from './Tree.css';
@@ -32,7 +32,6 @@ export type ItemData = {|
isNavigatingWithKeyboard: boolean,
lastScrolledIDRef: { current: number | null },
onElementMouseEnter: (id: number) => void,
showIndentLines: boolean,
treeFocused: boolean,
|};
@@ -60,7 +59,7 @@ export default function Tree(props: Props) {
const [treeFocused, setTreeFocused] = useState<boolean>(false);
const { lineHeight, showIndentLines } = useContext(SettingsContext);
const { lineHeight } = useContext(SettingsContext);
// Make sure a newly selected element is visible in the list.
// This is helpful for things like the owners list and search.
@@ -264,7 +263,6 @@ export default function Tree(props: Props) {
isNavigatingWithKeyboard,
onElementMouseEnter: handleElementMouseEnter,
lastScrolledIDRef,
showIndentLines,
treeFocused,
}),
[
@@ -272,7 +270,6 @@ export default function Tree(props: Props) {
isNavigatingWithKeyboard,
handleElementMouseEnter,
lastScrolledIDRef,
showIndentLines,
treeFocused,
]
);
@@ -310,7 +307,6 @@ export default function Tree(props: Props) {
itemCount={numElements}
itemData={itemData}
itemSize={lineHeight}
overscanCount={3}
ref={listRef}
width={width}
>
@@ -326,6 +322,31 @@ export default function Tree(props: Props) {
);
}
function updateIndentationSizeVar(innerDiv: HTMLDivElement): void {
const list = ((innerDiv.parentElement: any): HTMLDivElement);
let indentationSize =
parseFloat(getComputedStyle(list).getPropertyValue('--indentation-size')) ||
12;
let maxChildWidth = 0;
for (let child of innerDiv.children) {
const { lastElementChild } = child;
// Skip over e.g. the guideline element
if (lastElementChild != null) {
const bounds = ((lastElementChild.getBoundingClientRect(): any): DOMRect);
maxChildWidth = Math.max(maxChildWidth, bounds.x + bounds.width);
}
}
indentationSize = Math.min(
12,
(list.clientWidth / maxChildWidth) * indentationSize
);
list.style.setProperty('--indentation-size', `${indentationSize}px`);
}
function InnerElementType({ children, style, ...rest }) {
const { ownerID } = useContext(TreeStateContext);
@@ -335,13 +356,12 @@ function InnerElementType({ children, style, ...rest }) {
// and ensure that once we've grown to a new max size, we don't shrink below it.
// This improves the user experience when scrolling between wide and narrow rows.
const divRef = useRef<HTMLDivElement | null>(null);
const [minWidth, setMinWidth] = useState(null);
// TODO This is a valid warning, but we're ignoring it for the time being.
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
if (divRef.current !== null) {
const measuredWidth = divRef.current.offsetWidth;
setMinWidth(w => Math.max(w || 0, measuredWidth));
updateIndentationSizeVar(divRef.current);
}
});
@@ -349,7 +369,11 @@ function InnerElementType({ children, style, ...rest }) {
// This will cause a render with 100% min-width, a measurement
// in an effect, and a second render where we know the width.
useEffect(() => {
const invalidateMinWidth = () => setMinWidth(null);
const invalidateMinWidth = () => {
if (divRef.current !== null) {
updateIndentationSizeVar(divRef.current);
}
};
window.addEventListener('resize', invalidateMinWidth);
return () => window.removeEventListener('resize', invalidateMinWidth);
}, []);
@@ -359,7 +383,6 @@ function InnerElementType({ children, style, ...rest }) {
const [prevOwnerID, setPrevOwnerID] = useState(ownerID);
if (ownerID !== prevOwnerID) {
setPrevOwnerID(ownerID);
setMinWidth(null);
}
// This style override enables the background color to fill the full visible width,
@@ -369,16 +392,11 @@ function InnerElementType({ children, style, ...rest }) {
return (
<div
className={styles.InnerElementType}
style={{
...style,
display: 'inline-block',
minWidth: minWidth || '100%',
width: undefined,
}}
style={style}
ref={divRef}
{...rest}
>
<Guidelines />
<Guideline />
{children}
</div>
);
+3 -23
View File
@@ -11,14 +11,9 @@ import styles from './Settings.css';
function Settings(_: {||}) {
const store = useContext(StoreContext);
const {
displayDensity,
setDisplayDensity,
showIndentLines,
setShowIndentLines,
theme,
setTheme,
} = useContext(SettingsContext);
const { displayDensity, setDisplayDensity, theme, setTheme } = useContext(
SettingsContext
);
const captureScreenshotsSubscription = useMemo(
() => ({
@@ -62,13 +57,6 @@ function Settings(_: {||}) {
[setTheme]
);
const updateShowIndentLines = useCallback(
({ currentTarget }) => {
setShowIndentLines(currentTarget.checked);
},
[setShowIndentLines]
);
const updateCaptureScreenshotsWhileProfiling = useCallback(
({ currentTarget }) => {
store.captureScreenshots = currentTarget.checked;
@@ -155,14 +143,6 @@ function Settings(_: {||}) {
/>{' '}
Collapse newly added components by default
</label>
<label className={styles.CheckboxOption}>
<input
type="checkbox"
checked={showIndentLines}
onChange={updateShowIndentLines}
/>{' '}
Show indent lines
</label>
</div>
{store.supportsCaptureScreenshots && (
@@ -16,9 +16,6 @@ type Context = {|
// Specified as a separate prop so it can trigger a re-render of FixedSizeList.
lineHeight: number,
showIndentLines: boolean,
setShowIndentLines: (value: boolean) => void,
theme: Theme,
setTheme(value: Theme): void,
|};
@@ -47,10 +44,6 @@ function SettingsContextController({
'React::DevTools::displayDensity',
'compact'
);
const [showIndentLines, setShowIndentLines] = useLocalStorage<boolean>(
'React::DevTools::showIndentLines',
true
);
const [theme, setTheme] = useLocalStorage<Theme>(
'React::DevTools::theme',
'auto'
@@ -133,8 +126,6 @@ function SettingsContextController({
setDisplayDensity,
theme,
setTheme,
showIndentLines,
setShowIndentLines,
lineHeight:
displayDensity === 'compact'
? compactLineHeight
@@ -144,9 +135,7 @@ function SettingsContextController({
comfortableLineHeight,
compactLineHeight,
displayDensity,
showIndentLines,
setDisplayDensity,
setShowIndentLines,
setTheme,
theme,
]
+13
View File
@@ -170,3 +170,16 @@ export function downloadFile(filename: string, text: string): void {
((document.body: any): HTMLBodyElement).removeChild(element);
}
export function truncateText(text: string, maxLength: number): string {
const { length } = text;
if (length > maxLength) {
return (
text.substr(0, Math.floor(maxLength / 2)) +
'…' +
text.substr(length - Math.ceil(maxLength / 2) + 1)
);
} else {
return text;
}
}