import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource';
import * as React from 'react';
import {useCallback, useContext, useSyncExternalStore} from 'react';
import {TreeStateContext} from './TreeContext';
import {BridgeContext, StoreContext, OptionsContext} from '../context';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import Icon from '../Icon';
import Toggle from '../Toggle';
import {
ElementTypeSuspense,
ElementTypeRoot,
} from 'react-devtools-shared/src/frontend/types';
import InspectedElementView from './InspectedElementView';
import {InspectedElementContext} from './InspectedElementContext';
import {getAlwaysOpenInEditor} from '../../../utils';
import {LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR} from '../../../constants';
import FetchFileWithCachingContext from './FetchFileWithCachingContext';
import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource';
import OpenInEditorButton from './OpenInEditorButton';
import InspectedElementViewSourceButton from './InspectedElementViewSourceButton';
import useEditorURL from '../useEditorURL';
import styles from './InspectedElement.css';
import Tooltip from './reach-ui/tooltip';
export type Props = {
actionButtons?: React.Node,
fallbackEmpty: React.Node,
};
const noSourcePromise = Promise.resolve(null);
export default function InspectedElementWrapper({
actionButtons,
fallbackEmpty,
}: Props): React.Node {
const {inspectedElementID} = useContext(TreeStateContext);
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
const {
hideToggleErrorAction,
hideToggleSuspenseAction,
hideLogAction,
hideViewSourceAction,
} = useContext(OptionsContext);
const {hookNames, inspectedElement, parseHookNames, toggleParseHookNames} =
useContext(InspectedElementContext);
const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
const source =
inspectedElement == null
? null
: inspectedElement.source != null
? inspectedElement.source
: inspectedElement.stack != null && inspectedElement.stack.length > 0
? inspectedElement.stack[0]
: null;
const symbolicatedSourcePromise: Promise<SourceMappedLocation | null> =
React.useMemo(() => {
if (fetchFileWithCaching == null) return noSourcePromise;
if (source == null) return noSourcePromise;
const [, sourceURL, line, column] = source;
return symbolicateSourceWithCache(
fetchFileWithCaching,
sourceURL,
line,
column,
);
}, [source]);
const element =
inspectedElementID !== null
? store.getElementByID(inspectedElementID)
: null;
const highlightElement = useCallback(() => {
if (element !== null && inspectedElementID !== null) {
const rendererID = store.getRendererIDForElement(inspectedElementID);
if (rendererID !== null) {
bridge.send('highlightHostInstance', {
displayName: element.displayName,
hideAfterTimeout: true,
id: inspectedElementID,
openBuiltinElementsPanel: true,
rendererID,
scrollIntoView: true,
});
}
}
}, [bridge, element, inspectedElementID, store]);
const logElement = useCallback(() => {
if (inspectedElementID !== null) {
const rendererID = store.getRendererIDForElement(inspectedElementID);
if (rendererID !== null) {
bridge.send('logElementToConsole', {
id: inspectedElementID,
rendererID,
});
}
}
}, [bridge, inspectedElementID, store]);
const isErrored = inspectedElement != null && inspectedElement.isErrored;
const isSuspended =
element !== null &&
element.type === ElementTypeSuspense &&
inspectedElement != null &&
inspectedElement.isSuspended;
const canToggleError =
!hideToggleErrorAction &&
inspectedElement != null &&
inspectedElement.canToggleError;
const canToggleSuspense =
!hideToggleSuspenseAction &&
inspectedElement != null &&
inspectedElement.canToggleSuspense;
const alwaysOpenInEditor = useSyncExternalStore(
useCallback(function subscribe(callback) {
window.addEventListener(LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR, callback);
return function unsubscribe() {
window.removeEventListener(
LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR,
callback,
);
};
}, []),
getAlwaysOpenInEditor,
);
const editorURL = useEditorURL();
const toggleErrored = useCallback(() => {
if (inspectedElement == null) {
return;
}
const rendererID = store.getRendererIDForElement(inspectedElement.id);
if (rendererID !== null) {
bridge.send('overrideError', {
id: inspectedElement.id,
rendererID,
forceError: !isErrored,
});
}
}, [bridge, store, isErrored, inspectedElement]);
const toggleSuspended = useCallback(() => {
if (inspectedElement == null) {
return;
}
const rendererID = store.getRendererIDForElement(inspectedElement.id);
if (rendererID !== null) {
bridge.send('overrideSuspense', {
id: inspectedElement.id,
rendererID,
forceFallback: !isSuspended,
});
}
}, [bridge, store, isSuspended, inspectedElement]);
if (element === null) {
return (
<div className={styles.InspectedElement}>
<div className={styles.TitleRow} />
<div className={styles.NoInspectionFallback}>{fallbackEmpty}</div>
</div>
);
}
let strictModeBadge = null;
if (element.isStrictModeNonCompliant && element.parentID !== 0) {
strictModeBadge = (
<Tooltip label="This component is not running in StrictMode. Click to learn more.">
<a
className={styles.StrictModeNonCompliant}
href="https://react.dev/reference/react/StrictMode"
rel="noopener noreferrer"
target="_blank">
<Icon type="strict-mode-non-compliant" />
</a>
</Tooltip>
);
}
let fullName = element.displayName || '';
if (element.nameProp !== null) {
fullName += ' "' + element.nameProp + '"';
}
if (element.type === ElementTypeRoot) {
fullName = 'Initial Paint';
}
return (
<div
className={styles.InspectedElement}
key={inspectedElementID /* Force reset when selected Element changes */}>
<div className={styles.TitleRow} data-testname="InspectedElement-Title">
{strictModeBadge}
{element.key && (
<>
<div className={styles.Key} title={`key "${element.key}"`}>
{element.key}
</div>
<div className={styles.KeyArrow} />
</>
)}
<div className={styles.SelectedComponentName}>
<div
className={
element.isStrictModeNonCompliant && element.parentID !== 0
? `${styles.ComponentName} ${styles.StrictModeNonCompliantComponentName}`
: styles.ComponentName
}
title={fullName}>
{fullName}
</div>
</div>
{!alwaysOpenInEditor &&
!!editorURL &&
source != null &&
symbolicatedSourcePromise != null && (
<OpenInEditorButton
editorURL={editorURL}
source={source}
symbolicatedSourcePromise={symbolicatedSourcePromise}
/>
)}
{canToggleError && (
<Toggle
isChecked={isErrored}
onChange={toggleErrored}
title={
isErrored
? 'Clear the forced error'
: 'Force the selected component into an errored state'
}>
<ButtonIcon type="error" />
</Toggle>
)}
{canToggleSuspense || isSuspended ? (
<Toggle
isChecked={isSuspended}
isDisabled={!canToggleSuspense}
onChange={toggleSuspended}
title={
isSuspended
? canToggleSuspense
? 'Unsuspend the selected component'
: 'This boundary is still suspended'
: 'Suspend the selected component'
}>
<ButtonIcon type="suspend" />
</Toggle>
) : null}
{store.supportsInspectMatchingDOMElement && (
<Button
onClick={highlightElement}
title="Inspect the matching DOM element">
<ButtonIcon type="view-dom" />
</Button>
)}
{!hideLogAction && (
<Button
onClick={logElement}
title="Log this component data to the console">
<ButtonIcon type="log-data" />
</Button>
)}
{!hideViewSourceAction && (
<InspectedElementViewSourceButton
source={source}
symbolicatedSourcePromise={symbolicatedSourcePromise}
/>
)}
{actionButtons && (
<>
<div className={styles.VRule} />
{actionButtons}
</>
)}
</div>
{inspectedElement === null && (
<div className={styles.Loading}>Loading...</div>
)}
{inspectedElement !== null && (
<InspectedElementView
element={element}
hookNames={hookNames}
inspectedElement={inspectedElement}
parseHookNames={parseHookNames}
toggleParseHookNames={toggleParseHookNames}
symbolicatedSourcePromise={symbolicatedSourcePromise}
/>
)}
</div>
);
}