import * as React from 'react';
import {Fragment, startTransition, useContext, useMemo, useState} from 'react';
import Store from 'react-devtools-shared/src/devtools/store';
import {ElementTypeActivity} from 'react-devtools-shared/src/frontend/types';
import ButtonIcon from '../ButtonIcon';
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
import {StoreContext} from '../context';
import {useSubscription} from '../hooks';
import {logEvent} from 'react-devtools-shared/src/Logger';
import IndexableElementBadges from './IndexableElementBadges';
import IndexableDisplayName from './IndexableDisplayName';
import type {ItemData} from './Tree';
import type {Element as ElementType} from 'react-devtools-shared/src/frontend/types';
import styles from './Element.css';
import Icon from '../Icon';
import {useChangeOwnerAction} from './OwnersListContext';
import Tooltip from './reach-ui/tooltip';
import {useChangeActivitySliceAction} from '../SuspenseTab/ActivityList';
type Props = {
data: ItemData,
index: number,
style: Object,
...
};
export default function Element({data, index, style}: Props): React.Node {
const store = useContext(StoreContext);
const {ownerFlatTree, ownerID, inspectedElementID} =
useContext(TreeStateContext);
const dispatch = useContext(TreeDispatcherContext);
const element =
ownerFlatTree !== null
? ownerFlatTree[index]
: store.getElementAtIndex(index);
const [isHovered, setIsHovered] = useState(false);
const errorsAndWarningsSubscription = useMemo(
() => ({
getCurrentValue: () =>
element === null
? {errorCount: 0, warningCount: 0}
: store.getErrorAndWarningCountForElementID(element.id),
subscribe: (callback: Function) => {
store.addListener('mutated', callback);
return () => store.removeListener('mutated', callback);
},
}),
[store, element],
);
const {errorCount, warningCount} = useSubscription<{
errorCount: number,
warningCount: number,
}>(errorsAndWarningsSubscription);
const changeOwnerAction = useChangeOwnerAction();
const changeActivitySliceAction = useChangeActivitySliceAction();
if (element == null) {
console.warn(`<Element> Could not find element at index ${index}`);
return null;
}
const handleDoubleClick = () => {
startTransition(() => {
if (element.type === ElementTypeActivity) {
changeActivitySliceAction(element.id);
} else {
changeOwnerAction(element.id);
}
});
};
const handleClick = ({metaKey, button}) => {
if (id !== null && button === 0) {
logEvent({
event_name: 'select-element',
metadata: {source: 'click-element'},
});
dispatch({
type: 'SELECT_ELEMENT_BY_ID',
payload: metaKey ? null : id,
});
}
};
const handleMouseEnter = () => {
setIsHovered(true);
if (id !== null) {
onElementMouseEnter(id);
}
};
const handleMouseLeave = () => {
setIsHovered(false);
};
const handleKeyDoubleClick = event => {
event.stopPropagation();
event.preventDefault();
};
const {
id,
depth,
displayName,
hocDisplayNames,
isStrictModeNonCompliant,
key,
nameProp,
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);
const showStrictModeBadge = isStrictModeNonCompliant && depth === 0;
let className = styles.Element;
if (isSelected) {
className = treeFocused
? styles.SelectedElement
: styles.InactiveSelectedElement;
} else if (isHovered && !isNavigatingWithKeyboard) {
className = styles.HoveredElement;
} else if (isDescendantOfSelected) {
className = treeFocused
? styles.HighlightedElement
: styles.InactiveHighlightedElement;
}
return (
<div
className={className}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseDown={handleClick}
onDoubleClick={handleDoubleClick}
style={{
...style,
paddingLeft: elementOffset,
}}
data-testname="ComponentTreeListItem">
{/* This wrapper is used by Tree for measurement purposes. */}
<div className={styles.Wrapper}>
{ownerID === null && (
<ExpandCollapseToggle element={element} store={store} />
)}
<IndexableDisplayName displayName={displayName} id={id} />
{key && (
<Fragment>
<span className={styles.KeyName}>key</span>="
<span
className={styles.KeyValue}
title={key}
onDoubleClick={handleKeyDoubleClick}>
<IndexableDisplayName displayName={key} id={id} />
</span>
"
</Fragment>
)}
{nameProp && (
<Fragment>
<span className={styles.KeyName}>name</span>="
<span
className={styles.KeyValue}
title={nameProp}
onDoubleClick={handleKeyDoubleClick}>
<IndexableDisplayName displayName={nameProp} id={id} />
</span>
"
</Fragment>
)}
<IndexableElementBadges
hocDisplayNames={hocDisplayNames}
compiledWithForget={compiledWithForget}
elementID={id}
className={styles.BadgesBlock}
/>
{errorCount > 0 && (
<Icon
type="error"
className={
isSelected && treeFocused
? styles.ErrorIconContrast
: styles.ErrorIcon
}
/>
)}
{warningCount > 0 && (
<Icon
type="warning"
className={
isSelected && treeFocused
? styles.WarningIconContrast
: styles.WarningIcon
}
/>
)}
{showStrictModeBadge && (
<Tooltip label="This component is not running in StrictMode.">
<Icon
className={
isSelected && treeFocused
? styles.StrictModeContrast
: styles.StrictMode
}
type="strict-mode-non-compliant"
/>
</Tooltip>
)}
</div>
</div>
);
}
const swallowDoubleClick = event => {
event.preventDefault();
event.stopPropagation();
};
type ExpandCollapseToggleProps = {
element: ElementType,
store: Store,
};
function ExpandCollapseToggle({element, store}: ExpandCollapseToggleProps) {
const {children, id, isCollapsed} = element;
const toggleCollapsed = event => {
event.preventDefault();
event.stopPropagation();
store.toggleIsCollapsed(id, !isCollapsed);
};
const stopPropagation = event => {
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>
);
}