mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
Stacked on https://github.com/facebook/react/pull/31009. 1. Instead of keeping `showInlineWarningsAndErrors` in `Settings` context (which was removed in https://github.com/facebook/react/pull/30610), `Store` will now have a boolean flag, which controls if the UI should be displaying information about errors and warnings. 2. The errors and warnings counters in the Tree view are now counting only unique errors. This makes more sense, because it is part of the Elements Tree view, so ideally it should be showing number of components with errors and number of components of warnings. Consider this example: 2.1. Warning for element `A` was emitted once and warning for element `B` was emitted twice. 2.2. With previous implementation, we would show `3 ⚠️`, because in total there were 3 warnings in total. If user tries to iterate through these, it will only take 2 steps to do the full cycle, because there are only 2 elements with warnings (with one having same warning, which was emitted twice). 2.3 With current implementation, we would show `2 ⚠️`. Inspecting the element with doubled warning will still show the warning counter (2) before the warning message. With these changes, the feature correctly works. https://fburl.com/a7fw92m4
261 lines
7.2 KiB
JavaScript
261 lines
7.2 KiB
JavaScript
/**
|
|
* 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 * as React from 'react';
|
|
import {Fragment, useContext, useMemo, useState} from 'react';
|
|
import Store from 'react-devtools-shared/src/devtools/store';
|
|
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';
|
|
|
|
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, selectedElementID} =
|
|
useContext(TreeStateContext);
|
|
const dispatch = useContext(TreeDispatcherContext);
|
|
|
|
const element =
|
|
ownerFlatTree !== null
|
|
? ownerFlatTree[index]
|
|
: store.getElementAtIndex(index);
|
|
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
|
|
const {isNavigatingWithKeyboard, onElementMouseEnter, treeFocused} = data;
|
|
const id = element === null ? null : element.id;
|
|
const isSelected = selectedElementID === id;
|
|
|
|
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 handleDoubleClick = () => {
|
|
if (id !== null) {
|
|
dispatch({type: 'SELECT_OWNER', payload: id});
|
|
}
|
|
};
|
|
|
|
// $FlowFixMe[missing-local-annot]
|
|
const handleClick = ({metaKey}) => {
|
|
if (id !== null) {
|
|
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);
|
|
};
|
|
|
|
// $FlowFixMe[missing-local-annot]
|
|
const handleKeyDoubleClick = event => {
|
|
// Double clicks on key value are used for text selection (if the text has been truncated).
|
|
// They should not enter the owners tree view.
|
|
event.stopPropagation();
|
|
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 {
|
|
depth,
|
|
displayName,
|
|
hocDisplayNames,
|
|
isStrictModeNonCompliant,
|
|
key,
|
|
compiledWithForget,
|
|
} = element;
|
|
|
|
// Only show strict mode non-compliance badges for top level elements.
|
|
// Showing an inline badge for every element in the tree would be noisy.
|
|
const showStrictModeBadge = isStrictModeNonCompliant && depth === 0;
|
|
|
|
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={handleClick}
|
|
onDoubleClick={handleDoubleClick}
|
|
style={style}
|
|
data-testname="ComponentTreeListItem"
|
|
data-depth={depth}>
|
|
{/* 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)))`,
|
|
}}>
|
|
{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}>
|
|
{key}
|
|
</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 && (
|
|
<Icon
|
|
className={
|
|
isSelected && treeFocused
|
|
? styles.StrictModeContrast
|
|
: styles.StrictMode
|
|
}
|
|
title="This component is not running in StrictMode."
|
|
type="strict-mode-non-compliant"
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Prevent double clicks on toggle from drilling into the owner list.
|
|
// $FlowFixMe[missing-local-annot]
|
|
const swallowDoubleClick = event => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
};
|
|
|
|
type ExpandCollapseToggleProps = {
|
|
element: ElementType,
|
|
store: Store,
|
|
};
|
|
|
|
function ExpandCollapseToggle({element, store}: ExpandCollapseToggleProps) {
|
|
const {children, id, isCollapsed} = element;
|
|
|
|
// $FlowFixMe[missing-local-annot]
|
|
const toggleCollapsed = event => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
store.toggleIsCollapsed(id, !isCollapsed);
|
|
};
|
|
|
|
// $FlowFixMe[missing-local-annot]
|
|
const stopPropagation = 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>
|
|
);
|
|
}
|