29
import OwnersStack from './OwnersStack';
30
import ComponentSearchInput from './ComponentSearchInput';
31
import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
32
-import SelectedTreeHighlight from './SelectedTreeHighlight';
32
import TreeFocusedContext from './TreeFocusedContext';
33
import {useHighlightHostInstance, useSubscription} from '../hooks';
34
import {clearErrorsAndWarnings as clearErrorsAndWarningsAPI} from 'react-devtools-shared/src/backendAPI';
39
import {useExtensionComponentsPanelVisibility} from 'react-devtools-shared/src/frontend/hooks/useExtensionComponentsPanelVisibility';
40
import {useChangeOwnerAction} from './OwnersListContext';
41
43
-// Never indent more than this number of pixels (even if we have the room).
44
-const MAX_INDENTATION_SIZE = 12;
45
-const MIN_INDENTATION_SIZE = 4;
42
+// Indent for each node at level N, compared to node at level N - 1.
43
+const INDENTATION_SIZE = 10;
44
+
45
+function calculateElementOffset(elementDepth: number): number {
46
+ return elementDepth * INDENTATION_SIZE;
47
+}
48
49
export type ItemData = {
50
isNavigatingWithKeyboard: boolean,
51
onElementMouseEnter: (id: number) => void,
52
treeFocused: boolean,
53
+ calculateElementOffset: (depth: number) => number,
54
};
55
56
function calculateInitialScrollOffset(
94
const treeRef = useRef<HTMLDivElement | null>(null);
95
const focusTargetRef = useRef<HTMLDivElement | null>(null);
96
const listRef = useRef(null);
97
+ const listDOMElementRef = useRef(null);
98
99
useEffect(() => {
96
- if (!componentsPanelVisible) {
100
+ if (!componentsPanelVisible || inspectedElementIndex == null) {
101
+ return;
102
+ }
103
+
104
+ const listDOMElement = listDOMElementRef.current;
105
+ if (listDOMElement == null) {
106
return;
107
}
108
100
- if (listRef.current != null && inspectedElementIndex !== null) {
101
- listRef.current.scrollToItem(inspectedElementIndex, 'smart');
109
+ const viewportHeight = listDOMElement.clientHeight;
110
+ const viewportLeft = listDOMElement.scrollLeft;
111
+ const viewportRight = viewportLeft + listDOMElement.clientWidth;
112
+ const viewportTop = listDOMElement.scrollTop;
113
+ const viewportBottom = viewportTop + viewportHeight;
114
+
115
+ const element = store.getElementAtIndex(inspectedElementIndex);
116
+ if (element == null) {
117
+ return;
118
+ }
119
+ const elementLeft = calculateElementOffset(element.depth);
120
+ // Because of virtualization, this element might not be rendered yet; we can't look up its width.
121
+ // Assuming that it may take up to the half of the vieport.
122
+ const elementRight = elementLeft + listDOMElement.clientWidth / 2;
123
+ const elementTop = inspectedElementIndex * lineHeight;
124
+ const elementBottom = elementTop + lineHeight;
125
+
126
+ const isElementFullyVisible =
127
+ elementTop >= viewportTop &&
128
+ elementBottom <= viewportBottom &&
129
+ elementLeft >= viewportLeft &&
130
+ elementRight <= viewportRight;
131
+
132
+ if (!isElementFullyVisible) {
133
+ const verticalDelta =
134
+ Math.min(0, elementTop - viewportTop) +
135
+ Math.max(0, elementBottom - viewportBottom);
136
+ const horizontalDelta =
137
+ Math.min(0, elementLeft - viewportLeft) +
138
+ Math.max(0, elementRight - viewportRight);
139
+
140
+ listDOMElement.scrollBy({
141
+ top: verticalDelta,
142
+ left: horizontalDelta,
143
+ behavior: treeFocused && ownerID == null ? 'smooth' : 'instant',
144
+ });
145
}
103
- }, [inspectedElementIndex, componentsPanelVisible]);
146
+ }, [inspectedElementIndex, componentsPanelVisible, lineHeight]);
147
148
// Picking an element in the inspector should put focus into the tree.
149
// If possible, navigation works right after picking a node.
335
isNavigatingWithKeyboard,
336
onElementMouseEnter: handleElementMouseEnter,
337
treeFocused,
338
+ calculateElementOffset,
339
}),
296
- [isNavigatingWithKeyboard, handleElementMouseEnter, treeFocused],
340
+ [
341
+ isNavigatingWithKeyboard,
342
+ handleElementMouseEnter,
343
+ treeFocused,
344
+ calculateElementOffset,
345
+ ],
346
);
347
348
const itemKey = useCallback(
472
itemKey={itemKey}
473
itemSize={lineHeight}
474
ref={listRef}
475
+ outerRef={listDOMElementRef}
476
+ overscanCount={10}
477
width={width}>
478
{Element}
479
</FixedSizeList>
486
);
487
}
488
438
-// Indentation size can be adjusted but child width is fixed.
439
-// We need to adjust indentations so the widest child can fit without overflowing.
440
-// Sometimes the widest child is also the deepest in the tree:
441
-// ┏----------------------┓
442
-// ┆ <Foo> ┆
443
-// ┆ ••••<Foobar> ┆
444
-// ┆ ••••••••<Baz> ┆
445
-// ┗----------------------┛
446
-//
447
-// But this is not always the case.
448
-// Even with the above example, a change in indentation may change the overall widest child:
449
-// ┏----------------------┓
450
-// ┆ <Foo> ┆
451
-// ┆ ••<Foobar> ┆
452
-// ┆ ••••<Baz> ┆
453
-// ┗----------------------┛
454
-//
455
-// In extreme cases this difference can be important:
456
-// ┏----------------------┓
457
-// ┆ <ReallyLongName> ┆
458
-// ┆ ••<Foo> ┆
459
-// ┆ ••••<Bar> ┆
460
-// ┆ ••••••<Baz> ┆
461
-// ┆ ••••••••<Qux> ┆
462
-// ┗----------------------┛
463
-//
464
-// In the above example, the current indentation is fine,
465
-// but if we naively assumed that the widest element is also the deepest element,
466
-// we would end up compressing the indentation unnecessarily:
467
-// ┏----------------------┓
468
-// ┆ <ReallyLongName> ┆
469
-// ┆ •<Foo> ┆
470
-// ┆ ••<Bar> ┆
471
-// ┆ •••<Baz> ┆
472
-// ┆ ••••<Qux> ┆
473
-// ┗----------------------┛
474
-//
475
-// The way we deal with this is to compute the max indentation size that can fit each child,
476
-// given the child's fixed width and depth within the tree.
477
-// Then we take the smallest of these indentation sizes...
478
-function updateIndentationSizeVar(
479
- innerDiv: HTMLDivElement,
480
- cachedChildWidths: WeakMap<HTMLElement, number>,
481
- indentationSizeRef: {current: number},
482
- prevListWidthRef: {current: number},
483
-): void {
484
- const list = ((innerDiv.parentElement: any): HTMLDivElement);
485
- const listWidth = list.clientWidth;
486
-
487
- // Skip measurements when the Components panel is hidden.
488
- if (listWidth === 0) {
489
- return;
490
- }
491
-
492
- // Reset the max indentation size if the width of the tree has increased.
493
- if (listWidth > prevListWidthRef.current) {
494
- indentationSizeRef.current = MAX_INDENTATION_SIZE;
495
- }
496
- prevListWidthRef.current = listWidth;
497
-
498
- let indentationSize: number = indentationSizeRef.current;
499
-
500
- // eslint-disable-next-line no-for-of-loops/no-for-of-loops
501
- for (const child of innerDiv.children) {
502
- const depth = parseInt(child.getAttribute('data-depth'), 10) || 0;
503
-
504
- let childWidth: number = 0;
505
-
506
- const cachedChildWidth = cachedChildWidths.get(child);
507
- if (cachedChildWidth != null) {
508
- childWidth = cachedChildWidth;
509
- } else {
510
- const {firstElementChild} = child;
511
-
512
- // Skip over e.g. the guideline element
513
- if (firstElementChild != null) {
514
- childWidth = firstElementChild.clientWidth;
515
- cachedChildWidths.set(child, childWidth);
516
- }
517
- }
518
-
519
- const remainingWidth = Math.max(0, listWidth - childWidth);
489
+// $FlowFixMe[missing-local-annot]
490
+function InnerElementType({children, style}) {
491
+ const store = useContext(StoreContext);
492
521
- indentationSize = Math.min(indentationSize, remainingWidth / depth);
522
- }
493
+ const {height} = style;
494
+ const maxDepth = store.getMaximumRecordedDepth();
495
+ // Maximum possible indentation plus some arbitrary offset for the node content.
496
+ const width = calculateElementOffset(maxDepth) + 500;
497
524
- indentationSize = Math.max(indentationSize, MIN_INDENTATION_SIZE);
525
- indentationSizeRef.current = indentationSize;
498
+ return (
499
+ <div className={styles.InnerElementType} style={{height, width}}>
500
+ {children}
501
527
- list.style.setProperty('--indentation-size', `${indentationSize}px`);
502
+ <VerticalDelimiter />
503
+ </div>
504
+ );
505
}
506
530
-// $FlowFixMe[missing-local-annot]
531
-function InnerElementType({children, style}) {
532
- const {ownerID} = useContext(TreeStateContext);
507
+function VerticalDelimiter() {
508
+ const store = useContext(StoreContext);
509
+ const {ownerID, inspectedElementIndex} = useContext(TreeStateContext);
510
+ const {lineHeight} = useContext(SettingsContext);
511
534
- const cachedChildWidths = useMemo<WeakMap<HTMLElement, number>>(
535
- () => new WeakMap(),
536
- [],
537
- );
512
+ if (ownerID != null || inspectedElementIndex == null) {
513
+ return null;
514
+ }
515
539
- // This ref tracks the current indentation size.
540
- // We decrease indentation to fit wider/deeper trees.
541
- // We intentionally do not increase it again afterward, to avoid the perception of content "jumping"
542
- // e.g. clicking to toggle/collapse a row might otherwise jump horizontally beneath your cursor,
543
- // e.g. scrolling a wide row off screen could cause narrower rows to jump to the right some.
544
- //
545
- // There are two exceptions for this:
546
- // 1. The first is when the width of the tree increases.
547
- // The user may have resized the window specifically to make more room for DevTools.
548
- // In either case, this should reset our max indentation size logic.
549
- // 2. The second is when the user enters or exits an owner tree.
550
- const indentationSizeRef = useRef<number>(MAX_INDENTATION_SIZE);
551
- const prevListWidthRef = useRef<number>(0);
552
- const prevOwnerIDRef = useRef<number | null>(ownerID);
553
- const divRef = useRef<HTMLDivElement | null>(null);
554
-
555
- // We shouldn't retain this width across different conceptual trees though,
556
- // so when the user opens the "owners tree" view, we should discard the previous width.
557
- if (ownerID !== prevOwnerIDRef.current) {
558
- prevOwnerIDRef.current = ownerID;
559
- indentationSizeRef.current = MAX_INDENTATION_SIZE;
516
+ const element = store.getElementAtIndex(inspectedElementIndex);
517
+ if (element == null) {
518
+ return null;
519
+ }
520
+ const indexOfLowestDescendant =
521
+ store.getIndexOfLowestDescendantElement(element);
522
+ if (indexOfLowestDescendant == null) {
523
+ return null;
524
}
525
562
- // When we render new content, measure to see if we need to shrink indentation to fit it.
563
- useEffect(() => {
564
- if (divRef.current !== null) {
565
- updateIndentationSizeVar(
566
- divRef.current,
567
- cachedChildWidths,
568
- indentationSizeRef,
569
- prevListWidthRef,
570
- );
571
- }
572
- });
526
+ const delimiterLeft = calculateElementOffset(element.depth) + 12;
527
+ const delimiterTop = (inspectedElementIndex + 1) * lineHeight;
528
+ const delimiterHeight =
529
+ (indexOfLowestDescendant + 1) * lineHeight - delimiterTop;
530
574
- // This style override enables the background color to fill the full visible width,
575
- // when combined with the CSS tweaks in Element.
576
- // A lot of options were considered; this seemed the one that requires the least code.
577
- // See https://github.com/bvaughn/react-devtools-experimental/issues/9
531
return (
532
<div
580
- className={styles.InnerElementType}
581
- ref={divRef}
582
- style={{...style, pointerEvents: null}}>
583
- <SelectedTreeHighlight />
584
- {children}
585
- </div>
533
+ className={styles.VerticalDelimiter}
534
+ style={{
535
+ left: delimiterLeft,
536
+ top: delimiterTop,
537
+ height: delimiterHeight,
538
+ }}
539
+ />
540
);
541
}
542