mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
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
This commit is contained in:
parent
3fc1bc6f28
commit
d45db667d4
|
|
@ -195,6 +195,10 @@ export default class Store extends EventEmitter<{
|
|||
// Only used in browser extension for synchronization with built-in Elements panel.
|
||||
_lastSelectedHostInstanceElementId: Element['id'] | null = null;
|
||||
|
||||
// Maximum recorded node depth during the lifetime of this Store.
|
||||
// Can only increase: not guaranteed to return maximal value for currently recorded elements.
|
||||
_maximumRecordedDepth = 0;
|
||||
|
||||
constructor(bridge: FrontendBridge, config?: Config) {
|
||||
super();
|
||||
|
||||
|
|
@ -698,6 +702,50 @@ export default class Store extends EventEmitter<{
|
|||
return index;
|
||||
}
|
||||
|
||||
isDescendantOf(parentId: number, descendantId: number): boolean {
|
||||
if (descendantId === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const descendant = this.getElementByID(descendantId);
|
||||
if (descendant === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (descendant.parentID === parentId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const parent = this.getElementByID(parentId);
|
||||
if (!parent || parent.depth >= descendant.depth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.isDescendantOf(parentId, descendant.parentID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns index of the lowest descendant element, if available.
|
||||
* May not be the deepest element, the lowest is used in a sense of bottom-most from UI Tree representation perspective.
|
||||
*/
|
||||
getIndexOfLowestDescendantElement(element: Element): number | null {
|
||||
let current: null | Element = element;
|
||||
while (current !== null) {
|
||||
if (current.isCollapsed || current.children.length === 0) {
|
||||
if (current === element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.getIndexOfElementID(current.id);
|
||||
} else {
|
||||
const lastChildID = current.children[current.children.length - 1];
|
||||
current = this.getElementByID(lastChildID);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getOwnersListForElement(ownerID: number): Array<Element> {
|
||||
const list: Array<Element> = [];
|
||||
const element = this._idToElement.get(ownerID);
|
||||
|
|
@ -1089,9 +1137,15 @@ export default class Store extends EventEmitter<{
|
|||
compiledWithForget,
|
||||
} = parseElementDisplayNameFromBackend(displayName, type);
|
||||
|
||||
const elementDepth = parentElement.depth + 1;
|
||||
this._maximumRecordedDepth = Math.max(
|
||||
this._maximumRecordedDepth,
|
||||
elementDepth,
|
||||
);
|
||||
|
||||
const element: Element = {
|
||||
children: [],
|
||||
depth: parentElement.depth + 1,
|
||||
depth: elementDepth,
|
||||
displayName: displayNameWithoutHOCs,
|
||||
hocDisplayNames,
|
||||
id,
|
||||
|
|
@ -1536,6 +1590,14 @@ export default class Store extends EventEmitter<{
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Maximum recorded node depth during the lifetime of this Store.
|
||||
* Can only increase: not guaranteed to return maximal value for currently recorded elements.
|
||||
*/
|
||||
getMaximumRecordedDepth(): number {
|
||||
return this._maximumRecordedDepth;
|
||||
}
|
||||
|
||||
updateHookSettings: (settings: $ReadOnly<DevToolsHookSettings>) => void =
|
||||
settings => {
|
||||
this._hookSettings = settings;
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ function Components(_: {}) {
|
|||
|
||||
const LOCAL_STORAGE_KEY = 'React::DevTools::createResizeReducer';
|
||||
const VERTICAL_MODE_MAX_WIDTH = 600;
|
||||
const MINIMUM_SIZE = 50;
|
||||
const MINIMUM_SIZE = 100;
|
||||
|
||||
function initResizeState(): ResizeState {
|
||||
let horizontalPercentage = 0.65;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
.Element,
|
||||
.HoveredElement,
|
||||
.InactiveSelectedElement,
|
||||
.SelectedElement,
|
||||
.HoveredElement {
|
||||
.HighlightedElement,
|
||||
.InactiveHighlightedElement,
|
||||
.SelectedElement {
|
||||
color: var(--color-component-name);
|
||||
}
|
||||
.HoveredElement {
|
||||
|
|
@ -10,8 +12,15 @@
|
|||
.InactiveSelectedElement {
|
||||
background-color: var(--color-background-inactive);
|
||||
}
|
||||
.HighlightedElement {
|
||||
background-color: var(--color-selected-tree-highlight-active);
|
||||
}
|
||||
.InactiveHighlightedElement {
|
||||
background-color: var(--color-selected-tree-highlight-inactive);
|
||||
}
|
||||
|
||||
.Wrapper {
|
||||
position: relative;
|
||||
padding: 0 0.25rem;
|
||||
white-space: pre;
|
||||
height: var(--line-height-data);
|
||||
|
|
|
|||
|
|
@ -45,10 +45,6 @@ export default function Element({data, index, style}: Props): React.Node {
|
|||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const {isNavigatingWithKeyboard, onElementMouseEnter, treeFocused} = data;
|
||||
const id = element === null ? null : element.id;
|
||||
const isSelected = inspectedElementID === id;
|
||||
|
||||
const errorsAndWarningsSubscription = useMemo(
|
||||
() => ({
|
||||
getCurrentValue: () =>
|
||||
|
|
@ -68,6 +64,15 @@ export default function Element({data, index, style}: Props): React.Node {
|
|||
}>(errorsAndWarningsSubscription);
|
||||
|
||||
const changeOwnerAction = useChangeOwnerAction();
|
||||
|
||||
// Handle elements that are removed from the tree while an async render is in progress.
|
||||
if (element == null) {
|
||||
console.warn(`<Element> Could not find element at index ${index}`);
|
||||
|
||||
// This return needs to happen after hooks, since hooks can't be conditional.
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleDoubleClick = () => {
|
||||
if (id !== null) {
|
||||
changeOwnerAction(id);
|
||||
|
|
@ -107,15 +112,8 @@ export default function Element({data, index, style}: Props): React.Node {
|
|||
event.preventDefault();
|
||||
};
|
||||
|
||||
// Handle elements that are removed from the tree while an async render is in progress.
|
||||
if (element == null) {
|
||||
console.warn(`<Element> Could not find element at index ${index}`);
|
||||
|
||||
// This return needs to happen after hooks, since hooks can't be conditional.
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
id,
|
||||
depth,
|
||||
displayName,
|
||||
hocDisplayNames,
|
||||
|
|
@ -123,6 +121,19 @@ export default function Element({data, index, style}: Props): React.Node {
|
|||
key,
|
||||
compiledWithForget,
|
||||
} = element;
|
||||
const {
|
||||
isNavigatingWithKeyboard,
|
||||
onElementMouseEnter,
|
||||
treeFocused,
|
||||
calculateElementOffset,
|
||||
} = data;
|
||||
|
||||
const isSelected = inspectedElementID === id;
|
||||
const isDescendantOfSelected =
|
||||
inspectedElementID !== null &&
|
||||
!isSelected &&
|
||||
store.isDescendantOf(inspectedElementID, id);
|
||||
const elementOffset = calculateElementOffset(depth);
|
||||
|
||||
// Only show strict mode non-compliance badges for top level elements.
|
||||
// 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 {
|
|||
: styles.InactiveSelectedElement;
|
||||
} else if (isHovered && !isNavigatingWithKeyboard) {
|
||||
className = styles.HoveredElement;
|
||||
} else if (isDescendantOfSelected) {
|
||||
className = treeFocused
|
||||
? styles.HighlightedElement
|
||||
: styles.InactiveHighlightedElement;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -144,17 +159,13 @@ export default function Element({data, index, style}: Props): React.Node {
|
|||
onMouseLeave={handleMouseLeave}
|
||||
onMouseDown={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
style={style}
|
||||
data-testname="ComponentTreeListItem"
|
||||
data-depth={depth}>
|
||||
style={{
|
||||
...style,
|
||||
paddingLeft: elementOffset,
|
||||
}}
|
||||
data-testname="ComponentTreeListItem">
|
||||
{/* This wrapper is used by Tree for measurement purposes. */}
|
||||
<div
|
||||
className={styles.Wrapper}
|
||||
style={{
|
||||
// Left offset presents the appearance of a nested tree structure.
|
||||
// We must use padding rather than margin/left because of the selected background color.
|
||||
transform: `translateX(calc(${depth} * var(--indentation-size)))`,
|
||||
}}>
|
||||
<div className={styles.Wrapper}>
|
||||
{ownerID === null && (
|
||||
<ExpandCollapseToggle element={element} store={store} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
.Active,
|
||||
.Inactive {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.Active {
|
||||
background-color: var(--color-selected-tree-highlight-active);
|
||||
}
|
||||
|
||||
.Inactive {
|
||||
background-color: var(--color-selected-tree-highlight-inactive);
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {Element} from 'react-devtools-shared/src/frontend/types';
|
||||
|
||||
import * as React from 'react';
|
||||
import {useContext, useMemo} from 'react';
|
||||
import {TreeStateContext} from './TreeContext';
|
||||
import {SettingsContext} from '../Settings/SettingsContext';
|
||||
import TreeFocusedContext from './TreeFocusedContext';
|
||||
import {StoreContext} from '../context';
|
||||
import {useSubscription} from '../hooks';
|
||||
|
||||
import styles from './SelectedTreeHighlight.css';
|
||||
|
||||
type Data = {
|
||||
startIndex: number,
|
||||
stopIndex: number,
|
||||
};
|
||||
|
||||
export default function SelectedTreeHighlight(_: {}): React.Node {
|
||||
const {lineHeight} = useContext(SettingsContext);
|
||||
const store = useContext(StoreContext);
|
||||
const treeFocused = useContext(TreeFocusedContext);
|
||||
const {ownerID, inspectedElementID} = useContext(TreeStateContext);
|
||||
|
||||
const subscription = useMemo(
|
||||
() => ({
|
||||
getCurrentValue: () => {
|
||||
if (
|
||||
inspectedElementID === null ||
|
||||
store.isInsideCollapsedSubTree(inspectedElementID)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const element = store.getElementByID(inspectedElementID);
|
||||
if (
|
||||
element === null ||
|
||||
element.isCollapsed ||
|
||||
element.children.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startIndex = store.getIndexOfElementID(element.children[0]);
|
||||
if (startIndex === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let stopIndex = null;
|
||||
let current: null | Element = element;
|
||||
while (current !== null) {
|
||||
if (current.isCollapsed || current.children.length === 0) {
|
||||
// We've found the last/deepest descendant.
|
||||
stopIndex = store.getIndexOfElementID(current.id);
|
||||
current = null;
|
||||
} else {
|
||||
const lastChildID = current.children[current.children.length - 1];
|
||||
current = store.getElementByID(lastChildID);
|
||||
}
|
||||
}
|
||||
|
||||
if (stopIndex === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
startIndex,
|
||||
stopIndex,
|
||||
};
|
||||
},
|
||||
subscribe: (callback: Function) => {
|
||||
store.addListener('mutated', callback);
|
||||
return () => {
|
||||
store.removeListener('mutated', callback);
|
||||
};
|
||||
},
|
||||
}),
|
||||
[inspectedElementID, store],
|
||||
);
|
||||
const data = useSubscription<Data | null>(subscription);
|
||||
|
||||
if (ownerID !== null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {startIndex, stopIndex} = data;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={treeFocused ? styles.Active : styles.Inactive}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: `${startIndex * lineHeight}px`,
|
||||
height: `${(stopIndex + 1 - startIndex) * lineHeight}px`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,17 +5,16 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: 1px solid var(--color-border);
|
||||
|
||||
/* Default size will be adjusted by Tree after scrolling */
|
||||
--indentation-size: 12px;
|
||||
}
|
||||
|
||||
.List {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.InnerElementType {
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.VerticalDelimiter {
|
||||
position: absolute;
|
||||
width: 0.025rem;
|
||||
background: #b0b0b0;
|
||||
}
|
||||
|
||||
.SearchInput {
|
||||
|
|
@ -97,4 +96,4 @@
|
|||
|
||||
.Link {
|
||||
color: var(--color-button-active);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import InspectHostNodesToggle from './InspectHostNodesToggle';
|
|||
import OwnersStack from './OwnersStack';
|
||||
import ComponentSearchInput from './ComponentSearchInput';
|
||||
import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
|
||||
import SelectedTreeHighlight from './SelectedTreeHighlight';
|
||||
import TreeFocusedContext from './TreeFocusedContext';
|
||||
import {useHighlightHostInstance, useSubscription} from '../hooks';
|
||||
import {clearErrorsAndWarnings as clearErrorsAndWarningsAPI} from 'react-devtools-shared/src/backendAPI';
|
||||
|
|
@ -40,14 +39,18 @@ import {logEvent} from 'react-devtools-shared/src/Logger';
|
|||
import {useExtensionComponentsPanelVisibility} from 'react-devtools-shared/src/frontend/hooks/useExtensionComponentsPanelVisibility';
|
||||
import {useChangeOwnerAction} from './OwnersListContext';
|
||||
|
||||
// Never indent more than this number of pixels (even if we have the room).
|
||||
const MAX_INDENTATION_SIZE = 12;
|
||||
const MIN_INDENTATION_SIZE = 4;
|
||||
// Indent for each node at level N, compared to node at level N - 1.
|
||||
const INDENTATION_SIZE = 10;
|
||||
|
||||
function calculateElementOffset(elementDepth: number): number {
|
||||
return elementDepth * INDENTATION_SIZE;
|
||||
}
|
||||
|
||||
export type ItemData = {
|
||||
isNavigatingWithKeyboard: boolean,
|
||||
onElementMouseEnter: (id: number) => void,
|
||||
treeFocused: boolean,
|
||||
calculateElementOffset: (depth: number) => number,
|
||||
};
|
||||
|
||||
function calculateInitialScrollOffset(
|
||||
|
|
@ -91,16 +94,56 @@ export default function Tree(): React.Node {
|
|||
const treeRef = useRef<HTMLDivElement | null>(null);
|
||||
const focusTargetRef = useRef<HTMLDivElement | null>(null);
|
||||
const listRef = useRef(null);
|
||||
const listDOMElementRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!componentsPanelVisible) {
|
||||
if (!componentsPanelVisible || inspectedElementIndex == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (listRef.current != null && inspectedElementIndex !== null) {
|
||||
listRef.current.scrollToItem(inspectedElementIndex, 'smart');
|
||||
const listDOMElement = listDOMElementRef.current;
|
||||
if (listDOMElement == null) {
|
||||
return;
|
||||
}
|
||||
}, [inspectedElementIndex, componentsPanelVisible]);
|
||||
|
||||
const viewportHeight = listDOMElement.clientHeight;
|
||||
const viewportLeft = listDOMElement.scrollLeft;
|
||||
const viewportRight = viewportLeft + listDOMElement.clientWidth;
|
||||
const viewportTop = listDOMElement.scrollTop;
|
||||
const viewportBottom = viewportTop + viewportHeight;
|
||||
|
||||
const element = store.getElementAtIndex(inspectedElementIndex);
|
||||
if (element == null) {
|
||||
return;
|
||||
}
|
||||
const elementLeft = calculateElementOffset(element.depth);
|
||||
// Because of virtualization, this element might not be rendered yet; we can't look up its width.
|
||||
// Assuming that it may take up to the half of the vieport.
|
||||
const elementRight = elementLeft + listDOMElement.clientWidth / 2;
|
||||
const elementTop = inspectedElementIndex * lineHeight;
|
||||
const elementBottom = elementTop + lineHeight;
|
||||
|
||||
const isElementFullyVisible =
|
||||
elementTop >= viewportTop &&
|
||||
elementBottom <= viewportBottom &&
|
||||
elementLeft >= viewportLeft &&
|
||||
elementRight <= viewportRight;
|
||||
|
||||
if (!isElementFullyVisible) {
|
||||
const verticalDelta =
|
||||
Math.min(0, elementTop - viewportTop) +
|
||||
Math.max(0, elementBottom - viewportBottom);
|
||||
const horizontalDelta =
|
||||
Math.min(0, elementLeft - viewportLeft) +
|
||||
Math.max(0, elementRight - viewportRight);
|
||||
|
||||
listDOMElement.scrollBy({
|
||||
top: verticalDelta,
|
||||
left: horizontalDelta,
|
||||
behavior: treeFocused && ownerID == null ? 'smooth' : 'instant',
|
||||
});
|
||||
}
|
||||
}, [inspectedElementIndex, componentsPanelVisible, lineHeight]);
|
||||
|
||||
// Picking an element in the inspector should put focus into the tree.
|
||||
// If possible, navigation works right after picking a node.
|
||||
|
|
@ -292,8 +335,14 @@ export default function Tree(): React.Node {
|
|||
isNavigatingWithKeyboard,
|
||||
onElementMouseEnter: handleElementMouseEnter,
|
||||
treeFocused,
|
||||
calculateElementOffset,
|
||||
}),
|
||||
[isNavigatingWithKeyboard, handleElementMouseEnter, treeFocused],
|
||||
[
|
||||
isNavigatingWithKeyboard,
|
||||
handleElementMouseEnter,
|
||||
treeFocused,
|
||||
calculateElementOffset,
|
||||
],
|
||||
);
|
||||
|
||||
const itemKey = useCallback(
|
||||
|
|
@ -423,6 +472,8 @@ export default function Tree(): React.Node {
|
|||
itemKey={itemKey}
|
||||
itemSize={lineHeight}
|
||||
ref={listRef}
|
||||
outerRef={listDOMElementRef}
|
||||
overscanCount={10}
|
||||
width={width}>
|
||||
{Element}
|
||||
</FixedSizeList>
|
||||
|
|
@ -435,154 +486,57 @@ export default function Tree(): React.Node {
|
|||
);
|
||||
}
|
||||
|
||||
// Indentation size can be adjusted but child width is fixed.
|
||||
// We need to adjust indentations so the widest child can fit without overflowing.
|
||||
// Sometimes the widest child is also the deepest in the tree:
|
||||
// ┏----------------------┓
|
||||
// ┆ <Foo> ┆
|
||||
// ┆ ••••<Foobar> ┆
|
||||
// ┆ ••••••••<Baz> ┆
|
||||
// ┗----------------------┛
|
||||
//
|
||||
// But this is not always the case.
|
||||
// Even with the above example, a change in indentation may change the overall widest child:
|
||||
// ┏----------------------┓
|
||||
// ┆ <Foo> ┆
|
||||
// ┆ ••<Foobar> ┆
|
||||
// ┆ ••••<Baz> ┆
|
||||
// ┗----------------------┛
|
||||
//
|
||||
// In extreme cases this difference can be important:
|
||||
// ┏----------------------┓
|
||||
// ┆ <ReallyLongName> ┆
|
||||
// ┆ ••<Foo> ┆
|
||||
// ┆ ••••<Bar> ┆
|
||||
// ┆ ••••••<Baz> ┆
|
||||
// ┆ ••••••••<Qux> ┆
|
||||
// ┗----------------------┛
|
||||
//
|
||||
// In the above example, the current indentation is fine,
|
||||
// but if we naively assumed that the widest element is also the deepest element,
|
||||
// we would end up compressing the indentation unnecessarily:
|
||||
// ┏----------------------┓
|
||||
// ┆ <ReallyLongName> ┆
|
||||
// ┆ •<Foo> ┆
|
||||
// ┆ ••<Bar> ┆
|
||||
// ┆ •••<Baz> ┆
|
||||
// ┆ ••••<Qux> ┆
|
||||
// ┗----------------------┛
|
||||
//
|
||||
// The way we deal with this is to compute the max indentation size that can fit each child,
|
||||
// given the child's fixed width and depth within the tree.
|
||||
// Then we take the smallest of these indentation sizes...
|
||||
function updateIndentationSizeVar(
|
||||
innerDiv: HTMLDivElement,
|
||||
cachedChildWidths: WeakMap<HTMLElement, number>,
|
||||
indentationSizeRef: {current: number},
|
||||
prevListWidthRef: {current: number},
|
||||
): void {
|
||||
const list = ((innerDiv.parentElement: any): HTMLDivElement);
|
||||
const listWidth = list.clientWidth;
|
||||
|
||||
// Skip measurements when the Components panel is hidden.
|
||||
if (listWidth === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the max indentation size if the width of the tree has increased.
|
||||
if (listWidth > prevListWidthRef.current) {
|
||||
indentationSizeRef.current = MAX_INDENTATION_SIZE;
|
||||
}
|
||||
prevListWidthRef.current = listWidth;
|
||||
|
||||
let indentationSize: number = indentationSizeRef.current;
|
||||
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const child of innerDiv.children) {
|
||||
const depth = parseInt(child.getAttribute('data-depth'), 10) || 0;
|
||||
|
||||
let childWidth: number = 0;
|
||||
|
||||
const cachedChildWidth = cachedChildWidths.get(child);
|
||||
if (cachedChildWidth != null) {
|
||||
childWidth = cachedChildWidth;
|
||||
} else {
|
||||
const {firstElementChild} = child;
|
||||
|
||||
// Skip over e.g. the guideline element
|
||||
if (firstElementChild != null) {
|
||||
childWidth = firstElementChild.clientWidth;
|
||||
cachedChildWidths.set(child, childWidth);
|
||||
}
|
||||
}
|
||||
|
||||
const remainingWidth = Math.max(0, listWidth - childWidth);
|
||||
|
||||
indentationSize = Math.min(indentationSize, remainingWidth / depth);
|
||||
}
|
||||
|
||||
indentationSize = Math.max(indentationSize, MIN_INDENTATION_SIZE);
|
||||
indentationSizeRef.current = indentationSize;
|
||||
|
||||
list.style.setProperty('--indentation-size', `${indentationSize}px`);
|
||||
}
|
||||
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
function InnerElementType({children, style}) {
|
||||
const {ownerID} = useContext(TreeStateContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const cachedChildWidths = useMemo<WeakMap<HTMLElement, number>>(
|
||||
() => new WeakMap(),
|
||||
[],
|
||||
const {height} = style;
|
||||
const maxDepth = store.getMaximumRecordedDepth();
|
||||
// Maximum possible indentation plus some arbitrary offset for the node content.
|
||||
const width = calculateElementOffset(maxDepth) + 500;
|
||||
|
||||
return (
|
||||
<div className={styles.InnerElementType} style={{height, width}}>
|
||||
{children}
|
||||
|
||||
<VerticalDelimiter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// This ref tracks the current indentation size.
|
||||
// We decrease indentation to fit wider/deeper trees.
|
||||
// We intentionally do not increase it again afterward, to avoid the perception of content "jumping"
|
||||
// e.g. clicking to toggle/collapse a row might otherwise jump horizontally beneath your cursor,
|
||||
// e.g. scrolling a wide row off screen could cause narrower rows to jump to the right some.
|
||||
//
|
||||
// There are two exceptions for this:
|
||||
// 1. The first is when the width of the tree increases.
|
||||
// The user may have resized the window specifically to make more room for DevTools.
|
||||
// In either case, this should reset our max indentation size logic.
|
||||
// 2. The second is when the user enters or exits an owner tree.
|
||||
const indentationSizeRef = useRef<number>(MAX_INDENTATION_SIZE);
|
||||
const prevListWidthRef = useRef<number>(0);
|
||||
const prevOwnerIDRef = useRef<number | null>(ownerID);
|
||||
const divRef = useRef<HTMLDivElement | null>(null);
|
||||
function VerticalDelimiter() {
|
||||
const store = useContext(StoreContext);
|
||||
const {ownerID, inspectedElementIndex} = useContext(TreeStateContext);
|
||||
const {lineHeight} = useContext(SettingsContext);
|
||||
|
||||
// We shouldn't retain this width across different conceptual trees though,
|
||||
// so when the user opens the "owners tree" view, we should discard the previous width.
|
||||
if (ownerID !== prevOwnerIDRef.current) {
|
||||
prevOwnerIDRef.current = ownerID;
|
||||
indentationSizeRef.current = MAX_INDENTATION_SIZE;
|
||||
if (ownerID != null || inspectedElementIndex == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// When we render new content, measure to see if we need to shrink indentation to fit it.
|
||||
useEffect(() => {
|
||||
if (divRef.current !== null) {
|
||||
updateIndentationSizeVar(
|
||||
divRef.current,
|
||||
cachedChildWidths,
|
||||
indentationSizeRef,
|
||||
prevListWidthRef,
|
||||
);
|
||||
}
|
||||
});
|
||||
const element = store.getElementAtIndex(inspectedElementIndex);
|
||||
if (element == null) {
|
||||
return null;
|
||||
}
|
||||
const indexOfLowestDescendant =
|
||||
store.getIndexOfLowestDescendantElement(element);
|
||||
if (indexOfLowestDescendant == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const delimiterLeft = calculateElementOffset(element.depth) + 12;
|
||||
const delimiterTop = (inspectedElementIndex + 1) * lineHeight;
|
||||
const delimiterHeight =
|
||||
(indexOfLowestDescendant + 1) * lineHeight - delimiterTop;
|
||||
|
||||
// This style override enables the background color to fill the full visible width,
|
||||
// when combined with the CSS tweaks in Element.
|
||||
// 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
|
||||
return (
|
||||
<div
|
||||
className={styles.InnerElementType}
|
||||
ref={divRef}
|
||||
style={{...style, pointerEvents: null}}>
|
||||
<SelectedTreeHighlight />
|
||||
{children}
|
||||
</div>
|
||||
className={styles.VerticalDelimiter}
|
||||
style={{
|
||||
left: delimiterLeft,
|
||||
top: delimiterTop,
|
||||
height: delimiterHeight,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user