Skip to content

Commit d45db66

Browse files
authored
feat: static Components panel layout (#33696)
## Summary Follow-up to #33517. With #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 #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
1 parent 3fc1bc6 commit d45db66

File tree

8 files changed

+214
-305
lines changed

8 files changed

+214
-305
lines changed

packages/react-devtools-shared/src/devtools/store.js

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ export default class Store extends EventEmitter<{
195195
// Only used in browser extension for synchronization with built-in Elements panel.
196196
_lastSelectedHostInstanceElementId: Element['id'] | null = null;
197197

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+
198202
constructor(bridge: FrontendBridge, config?: Config) {
199203
super();
200204

@@ -698,6 +702,50 @@ export default class Store extends EventEmitter<{
698702
return index;
699703
}
700704

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+
701749
getOwnersListForElement(ownerID: number): Array<Element> {
702750
const list: Array<Element> = [];
703751
const element = this._idToElement.get(ownerID);
@@ -1089,9 +1137,15 @@ export default class Store extends EventEmitter<{
10891137
compiledWithForget,
10901138
} = parseElementDisplayNameFromBackend(displayName, type);
10911139

1140+
const elementDepth = parentElement.depth + 1;
1141+
this._maximumRecordedDepth = Math.max(
1142+
this._maximumRecordedDepth,
1143+
elementDepth,
1144+
);
1145+
10921146
const element: Element = {
10931147
children: [],
1094-
depth: parentElement.depth + 1,
1148+
depth: elementDepth,
10951149
displayName: displayNameWithoutHOCs,
10961150
hocDisplayNames,
10971151
id,
@@ -1536,6 +1590,14 @@ export default class Store extends EventEmitter<{
15361590
}
15371591
};
15381592

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+
15391601
updateHookSettings: (settings: $ReadOnly<DevToolsHookSettings>) => void =
15401602
settings => {
15411603
this._hookSettings = settings;

packages/react-devtools-shared/src/devtools/views/Components/Components.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ function Components(_: {}) {
176176

177177
const LOCAL_STORAGE_KEY = 'React::DevTools::createResizeReducer';
178178
const VERTICAL_MODE_MAX_WIDTH = 600;
179-
const MINIMUM_SIZE = 50;
179+
const MINIMUM_SIZE = 100;
180180

181181
function initResizeState(): ResizeState {
182182
let horizontalPercentage = 0.65;

packages/react-devtools-shared/src/devtools/views/Components/Element.css

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
.Element,
2+
.HoveredElement,
23
.InactiveSelectedElement,
3-
.SelectedElement,
4-
.HoveredElement {
4+
.HighlightedElement,
5+
.InactiveHighlightedElement,
6+
.SelectedElement {
57
color: var(--color-component-name);
68
}
79
.HoveredElement {
@@ -10,8 +12,15 @@
1012
.InactiveSelectedElement {
1113
background-color: var(--color-background-inactive);
1214
}
15+
.HighlightedElement {
16+
background-color: var(--color-selected-tree-highlight-active);
17+
}
18+
.InactiveHighlightedElement {
19+
background-color: var(--color-selected-tree-highlight-inactive);
20+
}
1321

1422
.Wrapper {
23+
position: relative;
1524
padding: 0 0.25rem;
1625
white-space: pre;
1726
height: var(--line-height-data);

packages/react-devtools-shared/src/devtools/views/Components/Element.js

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,6 @@ export default function Element({data, index, style}: Props): React.Node {
4545

4646
const [isHovered, setIsHovered] = useState(false);
4747

48-
const {isNavigatingWithKeyboard, onElementMouseEnter, treeFocused} = data;
49-
const id = element === null ? null : element.id;
50-
const isSelected = inspectedElementID === id;
51-
5248
const errorsAndWarningsSubscription = useMemo(
5349
() => ({
5450
getCurrentValue: () =>
@@ -68,6 +64,15 @@ export default function Element({data, index, style}: Props): React.Node {
6864
}>(errorsAndWarningsSubscription);
6965

7066
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+
7176
const handleDoubleClick = () => {
7277
if (id !== null) {
7378
changeOwnerAction(id);
@@ -107,22 +112,28 @@ export default function Element({data, index, style}: Props): React.Node {
107112
event.preventDefault();
108113
};
109114

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-
118115
const {
116+
id,
119117
depth,
120118
displayName,
121119
hocDisplayNames,
122120
isStrictModeNonCompliant,
123121
key,
124122
compiledWithForget,
125123
} = 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);
126137

127138
// Only show strict mode non-compliance badges for top level elements.
128139
// 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 {
135146
: styles.InactiveSelectedElement;
136147
} else if (isHovered && !isNavigatingWithKeyboard) {
137148
className = styles.HoveredElement;
149+
} else if (isDescendantOfSelected) {
150+
className = treeFocused
151+
? styles.HighlightedElement
152+
: styles.InactiveHighlightedElement;
138153
}
139154

140155
return (
@@ -144,17 +159,13 @@ export default function Element({data, index, style}: Props): React.Node {
144159
onMouseLeave={handleMouseLeave}
145160
onMouseDown={handleClick}
146161
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">
150167
{/* 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}>
158169
{ownerID === null && (
159170
<ExpandCollapseToggle element={element} store={store} />
160171
)}

packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.css

Lines changed: 0 additions & 16 deletions
This file was deleted.

packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.js

Lines changed: 0 additions & 110 deletions
This file was deleted.

packages/react-devtools-shared/src/devtools/views/Components/Tree.css

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,16 @@
55
display: flex;
66
flex-direction: column;
77
border-top: 1px solid var(--color-border);
8-
9-
/* Default size will be adjusted by Tree after scrolling */
10-
--indentation-size: 12px;
118
}
129

13-
.List {
14-
overflow-x: hidden !important;
10+
.InnerElementType {
11+
position: relative;
1512
}
1613

17-
.InnerElementType {
18-
overflow-x: hidden;
14+
.VerticalDelimiter {
15+
position: absolute;
16+
width: 0.025rem;
17+
background: #b0b0b0;
1918
}
2019

2120
.SearchInput {
@@ -97,4 +96,4 @@
9796

9897
.Link {
9998
color: var(--color-button-active);
100-
}
99+
}

0 commit comments

Comments
 (0)