mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
258 lines
7.0 KiB
JavaScript
258 lines
7.0 KiB
JavaScript
// @flow
|
|
|
|
import React, {
|
|
Fragment,
|
|
useCallback,
|
|
useContext,
|
|
useLayoutEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
import { ElementTypeClass, ElementTypeFunction } from 'src/devtools/types';
|
|
import Store from 'src/devtools/store';
|
|
import ButtonIcon from '../ButtonIcon';
|
|
import { createRegExp } from '../utils';
|
|
import { TreeContext } from './TreeContext';
|
|
import { StoreContext } from '../context';
|
|
|
|
import type { ItemData } from './Tree';
|
|
import type { Element } from './types';
|
|
|
|
import styles from './Element.css';
|
|
|
|
type Props = {
|
|
data: ItemData,
|
|
index: number,
|
|
style: Object,
|
|
};
|
|
|
|
export default function ElementView({ data, index, style }: Props) {
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const {
|
|
baseDepth,
|
|
getElementAtIndex,
|
|
ownerStack,
|
|
selectOwner,
|
|
selectedElementID,
|
|
selectElementByID,
|
|
} = useContext(TreeContext);
|
|
const store = useContext(StoreContext);
|
|
|
|
const element = getElementAtIndex(index);
|
|
|
|
const {
|
|
lastScrolledIDRef,
|
|
treeFocused,
|
|
isNavigatingWithKeyboard,
|
|
onElementMouseEnter,
|
|
} = data;
|
|
const id = element === null ? null : element.id;
|
|
const isSelected = selectedElementID === id;
|
|
|
|
const handleDoubleClick = useCallback(() => {
|
|
if (id !== null) {
|
|
selectOwner(id);
|
|
}
|
|
}, [id, selectOwner]);
|
|
|
|
const ref = 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;
|
|
|
|
if (ref.current !== null) {
|
|
ref.current.scrollIntoView({
|
|
behavior: 'auto',
|
|
block: 'nearest',
|
|
inline: 'nearest',
|
|
});
|
|
}
|
|
}
|
|
}, [id, isSelected, lastScrolledIDRef]);
|
|
|
|
const handleMouseDown = useCallback(
|
|
({ metaKey }) => {
|
|
if (id !== null) {
|
|
selectElementByID(metaKey ? null : id);
|
|
}
|
|
},
|
|
[id, selectElementByID]
|
|
);
|
|
|
|
const handleMouseEnter = useCallback(() => {
|
|
setIsHovered(true);
|
|
if (id !== null) {
|
|
onElementMouseEnter(id);
|
|
}
|
|
}, [onElementMouseEnter, id]);
|
|
|
|
const handleMouseLeave = useCallback(() => {
|
|
setIsHovered(false);
|
|
}, []);
|
|
|
|
// Handle elements that are removed from the tree while an async render is in progress.
|
|
if (element == null) {
|
|
console.warn(`<ElementView> Could not find element at index ${index}`);
|
|
|
|
// This return needs to happen after hooks, since hooks can't be conditional.
|
|
return null;
|
|
}
|
|
|
|
const { depth, displayName, key, type } = ((element: any): Element);
|
|
|
|
const showDollarR =
|
|
isSelected && (type === ElementTypeClass || type === ElementTypeFunction);
|
|
|
|
let className = styles.Element;
|
|
if (isSelected) {
|
|
className = treeFocused
|
|
? styles.SelectedElement
|
|
: styles.InactiveSelectedElement;
|
|
} else if (isHovered && !isNavigatingWithKeyboard) {
|
|
className = styles.HoveredElement;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={className}
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
onMouseDown={handleMouseDown}
|
|
onDoubleClick={handleDoubleClick}
|
|
style={{
|
|
...style, // "style" comes from react-window
|
|
|
|
// Left padding presents the appearance of a nested tree structure.
|
|
paddingLeft: `${(depth - baseDepth) * 0.75 + 0.25}rem`,
|
|
|
|
// 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`,
|
|
}}
|
|
>
|
|
{ownerStack.length === 0 ? (
|
|
<ExpandCollapseToggle element={element} store={store} />
|
|
) : null}
|
|
<span className={styles.Component} ref={ref}>
|
|
<DisplayName displayName={displayName} id={((id: any): number)} />
|
|
{key && (
|
|
<Fragment>
|
|
<span className={styles.AttributeName}>key</span>=
|
|
<span className={styles.AttributeValue}>"{key}"</span>
|
|
</Fragment>
|
|
)}
|
|
</span>
|
|
{showDollarR && <span className={styles.DollarR}> == $r</span>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Prevent double clicks on toggle from drilling into the owner list.
|
|
const swallowDoubleClick = event => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
};
|
|
|
|
type ExpandCollapseToggleProps = {|
|
|
element: Element,
|
|
store: Store,
|
|
|};
|
|
|
|
function ExpandCollapseToggle({ element, store }: ExpandCollapseToggleProps) {
|
|
const { children, id, isCollapsed } = element;
|
|
|
|
const toggleCollapsed = useCallback(
|
|
event => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
store.toggleIsCollapsed(id, !isCollapsed);
|
|
},
|
|
[id, isCollapsed, store]
|
|
);
|
|
|
|
const stopPropagation = useCallback(event => {
|
|
// Prevent the row from selecting
|
|
event.stopPropagation();
|
|
}, []);
|
|
|
|
if (children.length === 0) {
|
|
return <div className={styles.ExpandCollapseToggle} />;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={styles.ExpandCollapseToggle}
|
|
onMouseDown={stopPropagation}
|
|
onClick={toggleCollapsed}
|
|
onDoubleClick={swallowDoubleClick}
|
|
>
|
|
<ButtonIcon type={isCollapsed ? 'collapsed' : 'expanded'} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type DisplayNameProps = {|
|
|
displayName: string | null,
|
|
id: number,
|
|
|};
|
|
|
|
function DisplayName({ displayName, id }: DisplayNameProps) {
|
|
const { searchIndex, searchResults, searchText } = useContext(TreeContext);
|
|
const isSearchResult = useMemo(() => {
|
|
return searchResults.includes(id);
|
|
}, [id, searchResults]);
|
|
const isCurrentResult =
|
|
searchIndex !== null && id === searchResults[searchIndex];
|
|
|
|
if (!isSearchResult || displayName === null) {
|
|
return displayName;
|
|
}
|
|
|
|
const match = createRegExp(searchText).exec(displayName);
|
|
|
|
if (match === null) {
|
|
return displayName;
|
|
}
|
|
|
|
const startIndex = match.index;
|
|
const stopIndex = startIndex + match[0].length;
|
|
|
|
const children = [];
|
|
if (startIndex > 0) {
|
|
children.push(<span key="begin">{displayName.slice(0, startIndex)}</span>);
|
|
}
|
|
children.push(
|
|
<mark
|
|
key="middle"
|
|
className={isCurrentResult ? styles.CurrentHighlight : styles.Highlight}
|
|
>
|
|
{displayName.slice(startIndex, stopIndex)}
|
|
</mark>
|
|
);
|
|
if (stopIndex < displayName.length) {
|
|
children.push(<span key="end">{displayName.slice(stopIndex)}</span>);
|
|
}
|
|
|
|
return children;
|
|
}
|