main
js 318 lines 8.96 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import * as React from 'react';
11 import {Fragment, startTransition, useContext, useMemo, useState} from 'react';
12 import Store from 'react-devtools-shared/src/devtools/store';
13 import {ElementTypeActivity} from 'react-devtools-shared/src/frontend/types';
14 import ButtonIcon from '../ButtonIcon';
15 import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
16 import {StoreContext} from '../context';
17 import {useSubscription} from '../hooks';
18 import {logEvent} from 'react-devtools-shared/src/Logger';
19 import IndexableElementBadges from './IndexableElementBadges';
20 import IndexableDisplayName from './IndexableDisplayName';
21
22 import type {ItemData} from './Tree';
23 import type {Element as ElementType} from 'react-devtools-shared/src/frontend/types';
24
25 import styles from './Element.css';
26 import Icon from '../Icon';
27 import {useChangeOwnerAction} from './OwnersListContext';
28 import Tooltip from './reach-ui/tooltip';
29 import {useChangeActivitySliceAction} from '../SuspenseTab/ActivityList';
30
31 type Props = {
32 data: ItemData,
33 index: number,
34 style: Object,
35 ...
36 };
37
38 export default function Element({data, index, style}: Props): React.Node {
39 const store = useContext(StoreContext);
40 const {ownerFlatTree, ownerID, inspectedElementID} =
41 useContext(TreeStateContext);
42 const dispatch = useContext(TreeDispatcherContext);
43
44 const element =
45 ownerFlatTree !== null
46 ? ownerFlatTree[index]
47 : store.getElementAtIndex(index);
48
49 const [isHovered, setIsHovered] = useState(false);
50
51 const errorsAndWarningsSubscription = useMemo(
52 () => ({
53 getCurrentValue: () =>
54 element === null
55 ? {errorCount: 0, warningCount: 0}
56 : store.getErrorAndWarningCountForElementID(element.id),
57 subscribe: (callback: Function) => {
58 store.addListener('mutated', callback);
59 return () => store.removeListener('mutated', callback);
60 },
61 }),
62 [store, element],
63 );
64 const {errorCount, warningCount} = useSubscription<{
65 errorCount: number,
66 warningCount: number,
67 }>(errorsAndWarningsSubscription);
68
69 const changeOwnerAction = useChangeOwnerAction();
70 const changeActivitySliceAction = useChangeActivitySliceAction();
71
72 // Handle elements that are removed from the tree while an async render is in progress.
73 if (element == null) {
74 console.warn(`<Element> Could not find element at index ${index}`);
75
76 // This return needs to happen after hooks, since hooks can't be conditional.
77 return null;
78 }
79
80 const handleDoubleClick = () => {
81 startTransition(() => {
82 if (element.type === ElementTypeActivity) {
83 changeActivitySliceAction(element.id);
84 } else {
85 changeOwnerAction(element.id);
86 }
87 });
88 };
89
90 // $FlowFixMe[missing-local-annot]
91 const handleClick = ({metaKey, button}) => {
92 // $FlowFixMe[invalid-compare]
93 if (id !== null && button === 0) {
94 logEvent({
95 event_name: 'select-element',
96 metadata: {source: 'click-element'},
97 });
98 dispatch({
99 type: 'SELECT_ELEMENT_BY_ID',
100 payload: metaKey ? null : id,
101 });
102 }
103 };
104
105 const handleMouseEnter = () => {
106 setIsHovered(true);
107 // $FlowFixMe[invalid-compare]
108 if (id !== null) {
109 onElementMouseEnter(id);
110 }
111 };
112
113 const handleMouseLeave = () => {
114 setIsHovered(false);
115 };
116
117 // $FlowFixMe[missing-local-annot]
118 const handleKeyDoubleClick = event => {
119 // Double clicks on key value are used for text selection (if the text has been truncated).
120 // They should not enter the owners tree view.
121 event.stopPropagation();
122 event.preventDefault();
123 };
124
125 const {
126 id,
127 depth,
128 displayName,
129 hocDisplayNames,
130 isStrictModeNonCompliant,
131 isActivityHidden,
132 isInsideHiddenActivity,
133 key,
134 nameProp,
135 compiledWithForget,
136 } = element;
137 const {
138 isNavigatingWithKeyboard,
139 onElementMouseEnter,
140 treeFocused,
141 calculateElementOffset,
142 } = data;
143
144 const isSelected = inspectedElementID === id;
145 const isDescendantOfSelected =
146 inspectedElementID !== null &&
147 !isSelected &&
148 store.isDescendantOf(inspectedElementID, id);
149 const elementOffset = calculateElementOffset(depth);
150
151 // Only show strict mode non-compliance badges for top level elements.
152 // Showing an inline badge for every element in the tree would be noisy.
153 const showStrictModeBadge = isStrictModeNonCompliant && depth === 0;
154
155 let className = styles.Element;
156 if (isSelected) {
157 className = treeFocused
158 ? styles.SelectedElement
159 : styles.InactiveSelectedElement;
160 } else if (isHovered && !isNavigatingWithKeyboard) {
161 className = styles.HoveredElement;
162 } else if (isDescendantOfSelected) {
163 className = treeFocused
164 ? styles.HighlightedElement
165 : styles.InactiveHighlightedElement;
166 }
167
168 return (
169 <div
170 className={className}
171 onMouseEnter={handleMouseEnter}
172 onMouseLeave={handleMouseLeave}
173 onMouseDown={handleClick}
174 onDoubleClick={handleDoubleClick}
175 title={
176 isInsideHiddenActivity
177 ? 'This component is inside a hidden Activity subtree.'
178 : undefined
179 }
180 style={{
181 ...style,
182 paddingLeft: elementOffset,
183 opacity: isInsideHiddenActivity ? 0.75 : 1,
184 }}
185 data-testname="ComponentTreeListItem">
186 {/* This wrapper is used by Tree for measurement purposes. */}
187 <div className={styles.Wrapper}>
188 {ownerID === null && (
189 <ExpandCollapseToggle element={element} store={store} />
190 )}
191
192 <IndexableDisplayName displayName={displayName} id={id} />
193
194 {key && (
195 <Fragment>
196 &nbsp;<span className={styles.KeyName}>key</span>="
197 <span
198 className={styles.KeyValue}
199 title={key}
200 onDoubleClick={handleKeyDoubleClick}>
201 <IndexableDisplayName displayName={key} id={id} />
202 </span>
203 "
204 </Fragment>
205 )}
206
207 {nameProp && (
208 <Fragment>
209 &nbsp;<span className={styles.KeyName}>name</span>="
210 <span
211 className={styles.KeyValue}
212 title={nameProp}
213 onDoubleClick={handleKeyDoubleClick}>
214 <IndexableDisplayName displayName={nameProp} id={id} />
215 </span>
216 "
217 </Fragment>
218 )}
219
220 {element.type === ElementTypeActivity && (
221 <Fragment>
222 &nbsp;<span className={styles.KeyName}>mode</span>="
223 <span
224 className={styles.KeyValue}
225 title={isActivityHidden ? 'hidden' : 'visible'}>
226 {isActivityHidden ? 'hidden' : 'visible'}
227 </span>
228 "
229 </Fragment>
230 )}
231
232 <IndexableElementBadges
233 hocDisplayNames={hocDisplayNames}
234 compiledWithForget={compiledWithForget}
235 elementID={id}
236 className={styles.BadgesBlock}
237 />
238
239 {errorCount > 0 && (
240 <Icon
241 type="error"
242 className={
243 isSelected && treeFocused
244 ? styles.ErrorIconContrast
245 : styles.ErrorIcon
246 }
247 />
248 )}
249 {warningCount > 0 && (
250 <Icon
251 type="warning"
252 className={
253 isSelected && treeFocused
254 ? styles.WarningIconContrast
255 : styles.WarningIcon
256 }
257 />
258 )}
259 {showStrictModeBadge && (
260 <Tooltip label="This component is not running in StrictMode.">
261 <Icon
262 className={
263 isSelected && treeFocused
264 ? styles.StrictModeContrast
265 : styles.StrictMode
266 }
267 type="strict-mode-non-compliant"
268 />
269 </Tooltip>
270 )}
271 </div>
272 </div>
273 );
274 }
275
276 // Prevent double clicks on toggle from drilling into the owner list.
277 // $FlowFixMe[missing-local-annot]
278 const swallowDoubleClick = event => {
279 event.preventDefault();
280 event.stopPropagation();
281 };
282
283 type ExpandCollapseToggleProps = {
284 element: ElementType,
285 store: Store,
286 };
287
288 function ExpandCollapseToggle({element, store}: ExpandCollapseToggleProps) {
289 const {children, id, isCollapsed} = element;
290
291 // $FlowFixMe[missing-local-annot]
292 const toggleCollapsed = event => {
293 event.preventDefault();
294 event.stopPropagation();
295
296 store.toggleIsCollapsed(id, !isCollapsed);
297 };
298
299 // $FlowFixMe[missing-local-annot]
300 const stopPropagation = event => {
301 // Prevent the row from selecting
302 event.stopPropagation();
303 };
304
305 if (children.length === 0) {
306 return <div className={styles.ExpandCollapseToggle} />;
307 }
308
309 return (
310 <div
311 className={styles.ExpandCollapseToggle}
312 onMouseDown={stopPropagation}
313 onClick={toggleCollapsed}
314 onDoubleClick={swallowDoubleClick}>
315 <ButtonIcon type={isCollapsed ? 'collapsed' : 'expanded'} />
316 </div>
317 );
318 }