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

feat: static Components panel layout (#33696)

## Summary Follow-up to https://github.com/facebook/react/pull/33517. With https://github.com/facebook/react/pull/33517, we now preserve at least some minimal indent. This actually doesn't work with the current setup, because we don't allow the container to overflow, so basically deeply nested elements will go off the screen. With these changes, we completely change the approach: - The layout will be static and it will have a constant indentation that will always be preserved. - The container will allow overflows, so users will be able to scroll horizontally and vertically. - We will implement automatic horizontal and vertical scrolls, if selected element is not in a viewport. - New: added vertical delimiter that can be used for simpler visual navigation. ## Demo ### Current public release https://github.com/user-attachments/assets/58645d42-c6b8-408b-b76f-95fb272f2e1e ### With https://github.com/facebook/react/pull/33517 https://github.com/user-attachments/assets/845285c8-5a01-4739-bcd7-ffc089e771bf ### This PR https://github.com/user-attachments/assets/72086b84-8d84-4626-94b3-e22e114e028e

Ruslan Lesiutin committed Jul 4, 2025 at 12:29 UTC d45db667d4509a5d82d4509e4aa51fdb266aa136
8 files changed +214 -305
packages/react-devtools-shared/src/devtools/store.js
+63 -1
@@ -195,6 +195,10 @@ export default class Store extends EventEmitter<{
195 // Only used in browser extension for synchronization with built-in Elements panel.
196 _lastSelectedHostInstanceElementId: Element['id'] | null = null;
197
198 + // Maximum recorded node depth during the lifetime of this Store.
199 + // Can only increase: not guaranteed to return maximal value for currently recorded elements.
200 + _maximumRecordedDepth = 0;
201 +
202 constructor(bridge: FrontendBridge, config?: Config) {
203 super();
204
@@ -698,6 +702,50 @@ export default class Store extends EventEmitter<{
702 return index;
703 }
704
705 + isDescendantOf(parentId: number, descendantId: number): boolean {
706 + if (descendantId === 0) {
707 + return false;
708 + }
709 +
710 + const descendant = this.getElementByID(descendantId);
711 + if (descendant === null) {
712 + return false;
713 + }
714 +
715 + if (descendant.parentID === parentId) {
716 + return true;
717 + }
718 +
719 + const parent = this.getElementByID(parentId);
720 + if (!parent || parent.depth >= descendant.depth) {
721 + return false;
722 + }
723 +
724 + return this.isDescendantOf(parentId, descendant.parentID);
725 + }
726 +
727 + /**
728 + * Returns index of the lowest descendant element, if available.
729 + * May not be the deepest element, the lowest is used in a sense of bottom-most from UI Tree representation perspective.
730 + */
731 + getIndexOfLowestDescendantElement(element: Element): number | null {
732 + let current: null | Element = element;
733 + while (current !== null) {
734 + if (current.isCollapsed || current.children.length === 0) {
735 + if (current === element) {
736 + return null;
737 + }
738 +
739 + return this.getIndexOfElementID(current.id);
740 + } else {
741 + const lastChildID = current.children[current.children.length - 1];
742 + current = this.getElementByID(lastChildID);
743 + }
744 + }
745 +
746 + return null;
747 + }
748 +
749 getOwnersListForElement(ownerID: number): Array<Element> {
750 const list: Array<Element> = [];
751 const element = this._idToElement.get(ownerID);
@@ -1089,9 +1137,15 @@ export default class Store extends EventEmitter<{
1137 compiledWithForget,
1138 } = parseElementDisplayNameFromBackend(displayName, type);
1139
1140 + const elementDepth = parentElement.depth + 1;
1141 + this._maximumRecordedDepth = Math.max(
1142 + this._maximumRecordedDepth,
1143 + elementDepth,
1144 + );
1145 +
1146 const element: Element = {
1147 children: [],
1094 - depth: parentElement.depth + 1,
1148 + depth: elementDepth,
1149 displayName: displayNameWithoutHOCs,
1150 hocDisplayNames,
1151 id,
@@ -1536,6 +1590,14 @@ export default class Store extends EventEmitter<{
1590 }
1591 };
1592
1593 + /**
1594 + * Maximum recorded node depth during the lifetime of this Store.
1595 + * Can only increase: not guaranteed to return maximal value for currently recorded elements.
1596 + */
1597 + getMaximumRecordedDepth(): number {
1598 + return this._maximumRecordedDepth;
1599 + }
1600 +
1601 updateHookSettings: (settings: $ReadOnly<DevToolsHookSettings>) => void =
1602 settings => {
1603 this._hookSettings = settings;
packages/react-devtools-shared/src/devtools/views/Components/Components.js
+1 -1
@@ -176,7 +176,7 @@ function Components(_: {}) {
176
177 const LOCAL_STORAGE_KEY = 'React::DevTools::createResizeReducer';
178 const VERTICAL_MODE_MAX_WIDTH = 600;
179 -const MINIMUM_SIZE = 50;
179 +const MINIMUM_SIZE = 100;
180
181 function initResizeState(): ResizeState {
182 let horizontalPercentage = 0.65;
packages/react-devtools-shared/src/devtools/views/Components/Element.css
+11 -2
@@ -1,7 +1,9 @@
1 .Element,
2 +.HoveredElement,
3 .InactiveSelectedElement,
3 -.SelectedElement,
4 -.HoveredElement {
4 +.HighlightedElement,
5 +.InactiveHighlightedElement,
6 +.SelectedElement {
7 color: var(--color-component-name);
8 }
9 .HoveredElement {
@@ -10,8 +12,15 @@
12 .InactiveSelectedElement {
13 background-color: var(--color-background-inactive);
14 }
15 +.HighlightedElement {
16 + background-color: var(--color-selected-tree-highlight-active);
17 +}
18 +.InactiveHighlightedElement {
19 + background-color: var(--color-selected-tree-highlight-inactive);
20 +}
21
22 .Wrapper {
23 + position: relative;
24 padding: 0 0.25rem;
25 white-space: pre;
26 height: var(--line-height-data);
packages/react-devtools-shared/src/devtools/views/Components/Element.js
+33 -22
@@ -45,10 +45,6 @@ export default function Element({data, index, style}: Props): React.Node {
45
46 const [isHovered, setIsHovered] = useState(false);
47
48 - const {isNavigatingWithKeyboard, onElementMouseEnter, treeFocused} = data;
49 - const id = element === null ? null : element.id;
50 - const isSelected = inspectedElementID === id;
51 -
48 const errorsAndWarningsSubscription = useMemo(
49 () => ({
50 getCurrentValue: () =>
@@ -68,6 +64,15 @@ export default function Element({data, index, style}: Props): React.Node {
64 }>(errorsAndWarningsSubscription);
65
66 const changeOwnerAction = useChangeOwnerAction();
67 +
68 + // Handle elements that are removed from the tree while an async render is in progress.
69 + if (element == null) {
70 + console.warn(`<Element> Could not find element at index ${index}`);
71 +
72 + // This return needs to happen after hooks, since hooks can't be conditional.
73 + return null;
74 + }
75 +
76 const handleDoubleClick = () => {
77 if (id !== null) {
78 changeOwnerAction(id);
@@ -107,15 +112,8 @@ export default function Element({data, index, style}: Props): React.Node {
112 event.preventDefault();
113 };
114
110 - // Handle elements that are removed from the tree while an async render is in progress.
111 - if (element == null) {
112 - console.warn(`<Element> Could not find element at index ${index}`);
113 -
114 - // This return needs to happen after hooks, since hooks can't be conditional.
115 - return null;
116 - }
117 -
115 const {
116 + id,
117 depth,
118 displayName,
119 hocDisplayNames,
@@ -123,6 +121,19 @@ export default function Element({data, index, style}: Props): React.Node {
121 key,
122 compiledWithForget,
123 } = element;
124 + const {
125 + isNavigatingWithKeyboard,
126 + onElementMouseEnter,
127 + treeFocused,
128 + calculateElementOffset,
129 + } = data;
130 +
131 + const isSelected = inspectedElementID === id;
132 + const isDescendantOfSelected =
133 + inspectedElementID !== null &&
134 + !isSelected &&
135 + store.isDescendantOf(inspectedElementID, id);
136 + const elementOffset = calculateElementOffset(depth);
137
138 // Only show strict mode non-compliance badges for top level elements.
139 // Showing an inline badge for every element in the tree would be noisy.
@@ -135,6 +146,10 @@ export default function Element({data, index, style}: Props): React.Node {
146 : styles.InactiveSelectedElement;
147 } else if (isHovered && !isNavigatingWithKeyboard) {
148 className = styles.HoveredElement;
149 + } else if (isDescendantOfSelected) {
150 + className = treeFocused
151 + ? styles.HighlightedElement
152 + : styles.InactiveHighlightedElement;
153 }
154
155 return (
@@ -144,17 +159,13 @@ export default function Element({data, index, style}: Props): React.Node {
159 onMouseLeave={handleMouseLeave}
160 onMouseDown={handleClick}
161 onDoubleClick={handleDoubleClick}
147 - style={style}
148 - data-testname="ComponentTreeListItem"
149 - data-depth={depth}>
162 + style={{
163 + ...style,
164 + paddingLeft: elementOffset,
165 + }}
166 + data-testname="ComponentTreeListItem">
167 {/* This wrapper is used by Tree for measurement purposes. */}
151 - <div
152 - className={styles.Wrapper}
153 - style={{
154 - // Left offset presents the appearance of a nested tree structure.
155 - // We must use padding rather than margin/left because of the selected background color.
156 - transform: `translateX(calc(${depth} * var(--indentation-size)))`,
157 - }}>
168 + <div className={styles.Wrapper}>
169 {ownerID === null && (
170 <ExpandCollapseToggle element={element} store={store} />
171 )}
packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.css deleted
-16
@@ -1,16 +0,0 @@
1 -.Active,
2 -.Inactive {
3 - position: absolute;
4 - left: 0;
5 - width: 100%;
6 - z-index: 0;
7 - pointer-events: none;
8 -}
9 -
10 -.Active {
11 - background-color: var(--color-selected-tree-highlight-active);
12 -}
13 -
14 -.Inactive {
15 - background-color: var(--color-selected-tree-highlight-inactive);
16 -}
packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.js deleted
-110
@@ -1,110 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @flow
8 - */
9 -
10 -import type {Element} from 'react-devtools-shared/src/frontend/types';
11 -
12 -import * as React from 'react';
13 -import {useContext, useMemo} from 'react';
14 -import {TreeStateContext} from './TreeContext';
15 -import {SettingsContext} from '../Settings/SettingsContext';
16 -import TreeFocusedContext from './TreeFocusedContext';
17 -import {StoreContext} from '../context';
18 -import {useSubscription} from '../hooks';
19 -
20 -import styles from './SelectedTreeHighlight.css';
21 -
22 -type Data = {
23 - startIndex: number,
24 - stopIndex: number,
25 -};
26 -
27 -export default function SelectedTreeHighlight(_: {}): React.Node {
28 - const {lineHeight} = useContext(SettingsContext);
29 - const store = useContext(StoreContext);
30 - const treeFocused = useContext(TreeFocusedContext);
31 - const {ownerID, inspectedElementID} = useContext(TreeStateContext);
32 -
33 - const subscription = useMemo(
34 - () => ({
35 - getCurrentValue: () => {
36 - if (
37 - inspectedElementID === null ||
38 - store.isInsideCollapsedSubTree(inspectedElementID)
39 - ) {
40 - return null;
41 - }
42 -
43 - const element = store.getElementByID(inspectedElementID);
44 - if (
45 - element === null ||
46 - element.isCollapsed ||
47 - element.children.length === 0
48 - ) {
49 - return null;
50 - }
51 -
52 - const startIndex = store.getIndexOfElementID(element.children[0]);
53 - if (startIndex === null) {
54 - return null;
55 - }
56 -
57 - let stopIndex = null;
58 - let current: null | Element = element;
59 - while (current !== null) {
60 - if (current.isCollapsed || current.children.length === 0) {
61 - // We've found the last/deepest descendant.
62 - stopIndex = store.getIndexOfElementID(current.id);
63 - current = null;
64 - } else {
65 - const lastChildID = current.children[current.children.length - 1];
66 - current = store.getElementByID(lastChildID);
67 - }
68 - }
69 -
70 - if (stopIndex === null) {
71 - return null;
72 - }
73 -
74 - return {
75 - startIndex,
76 - stopIndex,
77 - };
78 - },
79 - subscribe: (callback: Function) => {
80 - store.addListener('mutated', callback);
81 - return () => {
82 - store.removeListener('mutated', callback);
83 - };
84 - },
85 - }),
86 - [inspectedElementID, store],
87 - );
88 - const data = useSubscription<Data | null>(subscription);
89 -
90 - if (ownerID !== null) {
91 - return null;
92 - }
93 -
94 - if (data === null) {
95 - return null;
96 - }
97 -
98 - const {startIndex, stopIndex} = data;
99 -
100 - return (
101 - <div
102 - className={treeFocused ? styles.Active : styles.Inactive}
103 - style={{
104 - position: 'absolute',
105 - top: `${startIndex * lineHeight}px`,
106 - height: `${(stopIndex + 1 - startIndex) * lineHeight}px`,
107 - }}
108 - />
109 - );
110 -}
packages/react-devtools-shared/src/devtools/views/Components/Tree.css
+7 -8
@@ -5,17 +5,16 @@
5 display: flex;
6 flex-direction: column;
7 border-top: 1px solid var(--color-border);
8 -
9 - /* Default size will be adjusted by Tree after scrolling */
10 - --indentation-size: 12px;
8 }
9
13 -.List {
14 - overflow-x: hidden !important;
10 +.InnerElementType {
11 + position: relative;
12 }
13
17 -.InnerElementType {
18 - overflow-x: hidden;
14 +.VerticalDelimiter {
15 + position: absolute;
16 + width: 0.025rem;
17 + background: #b0b0b0;
18 }
19
20 .SearchInput {
@@ -97,4 +96,4 @@
96
97 .Link {
98 color: var(--color-button-active);
100 -}
\ No newline at end of file
99 +}
packages/react-devtools-shared/src/devtools/views/Components/Tree.js
+99 -145
@@ -29,7 +29,6 @@ import InspectHostNodesToggle from './InspectHostNodesToggle';
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';
@@ -40,14 +39,18 @@ import {logEvent} from 'react-devtools-shared/src/Logger';
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(
@@ -91,16 +94,56 @@ export default function Tree(): React.Node {
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.
@@ -292,8 +335,14 @@ export default function Tree(): React.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(
@@ -423,6 +472,8 @@ export default function Tree(): React.Node {
472 itemKey={itemKey}
473 itemSize={lineHeight}
474 ref={listRef}
475 + outerRef={listDOMElementRef}
476 + overscanCount={10}
477 width={width}>
478 {Element}
479 </FixedSizeList>
@@ -435,154 +486,57 @@ export default function Tree(): React.Node {
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