@samitouri / QOS-React / commits / d634548243

DevTools: merge element fields in TreeStateContext (#31956)

Stacked on https://github.com/facebook/react/pull/31892, see commit on top. For some reason, there were 2 fields different fields for essentially same thing: `selectedElementID` and `inspectedElementID`. Basically, the change is: ``` selectedElementID -> inspectedElementID selectedElementIndex -> inspectedElementIndex ``` I have a theory that it was due to previously used async approach around element inspection, and the whole `InspectedElementView` was wrapped in `Suspense`.

Ruslan Lesiutin committed Jan 9, 2025 at 18:13 UTC d6345482430952306fc83e62d4c14e2622fb1752
12 files changed +265 -278
packages/react-devtools-inline/__tests__/__e2e__/components.test.js
+10 -10
@@ -119,7 +119,7 @@ test.describe('Components', () => {
119 runOnlyForReactRange('>=16.8');
120
121 // Select the first list item in DevTools.
122 - await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp');
122 + await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp', true);
123
124 // Then read the inspected values.
125 const sourceText = await page.evaluate(() => {
@@ -127,7 +127,7 @@ test.describe('Components', () => {
127 const container = document.getElementById('devtools');
128
129 const source = findAllNodes(container, [
130 - createTestNameSelector('InspectedElementView-Source'),
130 + createTestNameSelector('InspectedElementView-FormattedSourceString'),
131 ])[0];
132
133 return source.innerText;
@@ -237,35 +237,35 @@ test.describe('Components', () => {
237 }
238
239 await focusComponentSearch();
240 - page.keyboard.insertText('List');
240 + await page.keyboard.insertText('List');
241 let count = await getComponentSearchResultsCount();
242 expect(count).toBe('1 | 4');
243
244 - page.keyboard.insertText('Item');
244 + await page.keyboard.insertText('Item');
245 count = await getComponentSearchResultsCount();
246 expect(count).toBe('1 | 3');
247
248 - page.keyboard.press('Enter');
248 + await page.keyboard.press('Enter');
249 count = await getComponentSearchResultsCount();
250 expect(count).toBe('2 | 3');
251
252 - page.keyboard.press('Enter');
252 + await page.keyboard.press('Enter');
253 count = await getComponentSearchResultsCount();
254 expect(count).toBe('3 | 3');
255
256 - page.keyboard.press('Enter');
256 + await page.keyboard.press('Enter');
257 count = await getComponentSearchResultsCount();
258 expect(count).toBe('1 | 3');
259
260 - page.keyboard.press('Shift+Enter');
260 + await page.keyboard.press('Shift+Enter');
261 count = await getComponentSearchResultsCount();
262 expect(count).toBe('3 | 3');
263
264 - page.keyboard.press('Shift+Enter');
264 + await page.keyboard.press('Shift+Enter');
265 count = await getComponentSearchResultsCount();
266 expect(count).toBe('2 | 3');
267
268 - page.keyboard.press('Shift+Enter');
268 + await page.keyboard.press('Shift+Enter');
269 count = await getComponentSearchResultsCount();
270 expect(count).toBe('1 | 3');
271 });
packages/react-devtools-inline/__tests__/__e2e__/devtools-utils.js
+20 -1
@@ -27,7 +27,12 @@ async function getElementCount(page, displayName) {
27 }, displayName);
28 }
29
30 -async function selectElement(page, displayName, waitForOwnersText) {
30 +async function selectElement(
31 + page,
32 + displayName,
33 + waitForOwnersText,
34 + waitForSourceLoaded = false
35 +) {
36 await page.evaluate(listItemText => {
37 const {createTestNameSelector, createTextSelector, findAllNodes} =
38 window.REACT_DOM_DEVTOOLS;
@@ -69,6 +74,20 @@ async function selectElement(page, displayName, waitForOwnersText) {
74 {titleText: displayName, ownersListText: waitForOwnersText}
75 );
76 }
77 +
78 + if (waitForSourceLoaded) {
79 + await page.waitForFunction(() => {
80 + const {createTestNameSelector, findAllNodes} = window.REACT_DOM_DEVTOOLS;
81 + const container = document.getElementById('devtools');
82 +
83 + const sourceStringBlock = findAllNodes(container, [
84 + createTestNameSelector('InspectedElementView-FormattedSourceString'),
85 + ])[0];
86 +
87 + // Wait for a new source line to be fetched
88 + return sourceStringBlock != null && sourceStringBlock.innerText != null;
89 + });
90 + }
91 }
92
93 module.exports = {
packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
+13 -12
@@ -115,16 +115,15 @@ describe('InspectedElement', () => {
115
116 const Contexts = ({
117 children,
118 - defaultSelectedElementID = null,
119 - defaultSelectedElementIndex = null,
118 + defaultInspectedElementID = null,
119 + defaultInspectedElementIndex = null,
120 }) => (
121 <BridgeContext.Provider value={bridge}>
122 <StoreContext.Provider value={store}>
123 <SettingsContextController>
124 <TreeContextController
125 - defaultSelectedElementID={defaultSelectedElementID}
126 - defaultSelectedElementIndex={defaultSelectedElementIndex}
127 - defaultInspectedElementID={defaultSelectedElementID}>
125 + defaultInspectedElementID={defaultInspectedElementID}
126 + defaultInspectedElementIndex={defaultInspectedElementIndex}>
127 <InspectedElementContextController>
128 {children}
129 </InspectedElementContextController>
@@ -167,8 +166,8 @@ describe('InspectedElement', () => {
166 testRendererInstance.update(
167 <ErrorBoundary>
168 <Contexts
170 - defaultSelectedElementID={id}
171 - defaultSelectedElementIndex={index}>
169 + defaultInspectedElementID={id}
170 + defaultInspectedElementIndex={index}>
171 <React.Suspense fallback={null}>
172 <Suspender id={id} index={index} />
173 </React.Suspense>
@@ -355,7 +354,7 @@ describe('InspectedElement', () => {
354 const {index, shouldHaveLegacyContext} = cases[i];
355
356 // HACK: Recreate TestRenderer instance because we rely on default state values
358 - // from props like defaultSelectedElementID and it's easier to reset here than
357 + // from props like defaultInspectedElementID and it's easier to reset here than
358 // to read the TreeDispatcherContext and update the selected ID that way.
359 // We're testing the inspected values here, not the context wiring, so that's ok.
360 withErrorsOrWarningsIgnored(
@@ -2069,7 +2068,7 @@ describe('InspectedElement', () => {
2068 }, false);
2069
2070 // HACK: Recreate TestRenderer instance because we rely on default state values
2072 - // from props like defaultSelectedElementID and it's easier to reset here than
2071 + // from props like defaultInspectedElementID and it's easier to reset here than
2072 // to read the TreeDispatcherContext and update the selected ID that way.
2073 // We're testing the inspected values here, not the context wiring, so that's ok.
2074 withErrorsOrWarningsIgnored(
@@ -2129,7 +2128,7 @@ describe('InspectedElement', () => {
2128 }, false);
2129
2130 // HACK: Recreate TestRenderer instance because we rely on default state values
2132 - // from props like defaultSelectedElementID and it's easier to reset here than
2131 + // from props like defaultInspectedElementID and it's easier to reset here than
2132 // to read the TreeDispatcherContext and update the selected ID that way.
2133 // We're testing the inspected values here, not the context wiring, so that's ok.
2134 withErrorsOrWarningsIgnored(
@@ -2408,8 +2407,8 @@ describe('InspectedElement', () => {
2407 await utils.actAsync(() => {
2408 root = TestRenderer.create(
2409 <Contexts
2411 - defaultSelectedElementID={id}
2412 - defaultSelectedElementIndex={index}>
2410 + defaultInspectedElementID={id}
2411 + defaultInspectedElementIndex={index}>
2412 <React.Suspense fallback={null}>
2413 <Suspender target={id} />
2414 </React.Suspense>
@@ -3101,6 +3100,7 @@ describe('InspectedElement', () => {
3100
3101 await utils.actAsync(() => {
3102 store.componentFilters = [utils.createDisplayNameFilter('Wrapper')];
3103 + jest.runOnlyPendingTimers();
3104 }, false);
3105
3106 expect(state).toMatchInlineSnapshot(`
@@ -3120,6 +3120,7 @@ describe('InspectedElement', () => {
3120
3121 await utils.actAsync(() => {
3122 store.componentFilters = [];
3123 + jest.runOnlyPendingTimers();
3124 }, false);
3125 expect(state).toMatchInlineSnapshot(`
3126 ✕ 0, ⚠ 2
packages/react-devtools-shared/src/__tests__/profilerContext-test.js
+34 -27
@@ -69,14 +69,14 @@ describe('ProfilerContext', () => {
69
70 const Contexts = ({
71 children = null,
72 - defaultSelectedElementID = null,
73 - defaultSelectedElementIndex = null,
72 + defaultInspectedElementID = null,
73 + defaultInspectedElementIndex = null,
74 }: any) => (
75 <BridgeContext.Provider value={bridge}>
76 <StoreContext.Provider value={store}>
77 <TreeContextController
78 - defaultSelectedElementID={defaultSelectedElementID}
79 - defaultSelectedElementIndex={defaultSelectedElementIndex}>
78 + defaultInspectedElementID={defaultInspectedElementID}
79 + defaultInspectedElementIndex={defaultInspectedElementIndex}>
80 <ProfilerContextController>{children}</ProfilerContextController>
81 </TreeContextController>
82 </StoreContext.Provider>
@@ -225,8 +225,8 @@ describe('ProfilerContext', () => {
225 await utils.actAsync(() =>
226 TestRenderer.create(
227 <Contexts
228 - defaultSelectedElementID={store.getElementIDAtIndex(3)}
229 - defaultSelectedElementIndex={3}>
228 + defaultInspectedElementID={store.getElementIDAtIndex(3)}
229 + defaultInspectedElementIndex={3}>
230 <ContextReader />
231 </Contexts>,
232 ),
@@ -276,8 +276,8 @@ describe('ProfilerContext', () => {
276 await utils.actAsync(() =>
277 TestRenderer.create(
278 <Contexts
279 - defaultSelectedElementID={store.getElementIDAtIndex(3)}
280 - defaultSelectedElementIndex={3}>
279 + defaultInspectedElementID={store.getElementIDAtIndex(3)}
280 + defaultInspectedElementIndex={3}>
281 <ContextReader />
282 </Contexts>,
283 ),
@@ -323,8 +323,8 @@ describe('ProfilerContext', () => {
323 await utils.actAsync(() =>
324 TestRenderer.create(
325 <Contexts
326 - defaultSelectedElementID={store.getElementIDAtIndex(3)}
327 - defaultSelectedElementIndex={3}>
326 + defaultInspectedElementID={store.getElementIDAtIndex(3)}
327 + defaultInspectedElementIndex={3}>
328 <ContextReader />
329 </Contexts>,
330 ),
@@ -374,8 +374,8 @@ describe('ProfilerContext', () => {
374 await utils.actAsync(() =>
375 TestRenderer.create(
376 <Contexts
377 - defaultSelectedElementID={store.getElementIDAtIndex(3)}
378 - defaultSelectedElementIndex={3}>
377 + defaultInspectedElementID={store.getElementIDAtIndex(3)}
378 + defaultInspectedElementIndex={3}>
379 <ContextReader />
380 </Contexts>,
381 ),
@@ -415,11 +415,12 @@ describe('ProfilerContext', () => {
415
416 let context: Context = ((null: any): Context);
417 let dispatch: DispatcherContext = ((null: any): DispatcherContext);
418 - let selectedElementID = null;
418 + let inspectedElementID = null;
419 function ContextReader() {
420 context = React.useContext(ProfilerContext);
421 dispatch = React.useContext(TreeDispatcherContext);
422 - selectedElementID = React.useContext(TreeStateContext).selectedElementID;
422 + inspectedElementID =
423 + React.useContext(TreeStateContext).inspectedElementID;
424 return null;
425 }
426
@@ -428,13 +429,15 @@ describe('ProfilerContext', () => {
429 // Select an element within the second root.
430 await utils.actAsync(() =>
431 TestRenderer.create(
431 - <Contexts defaultSelectedElementID={id} defaultSelectedElementIndex={3}>
432 + <Contexts
433 + defaultInspectedElementID={id}
434 + defaultInspectedElementIndex={3}>
435 <ContextReader />
436 </Contexts>,
437 ),
438 );
439
437 - expect(selectedElementID).toBe(id);
440 + expect(inspectedElementID).toBe(id);
441
442 // Profile and record more updates to both roots
443 await utils.actAsync(() => store.profilerStore.startProfiling());
@@ -448,7 +451,7 @@ describe('ProfilerContext', () => {
451 utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
452
453 // Verify that the initial Profiler root selection is maintained.
451 - expect(selectedElementID).toBe(otherID);
454 + expect(inspectedElementID).toBe(otherID);
455 expect(context).not.toBeNull();
456 expect(context.rootID).toBe(store.getRootIDForElement(id));
457 });
@@ -484,11 +487,12 @@ describe('ProfilerContext', () => {
487
488 let context: Context = ((null: any): Context);
489 let dispatch: DispatcherContext = ((null: any): DispatcherContext);
487 - let selectedElementID = null;
490 + let inspectedElementID = null;
491 function ContextReader() {
492 context = React.useContext(ProfilerContext);
493 dispatch = React.useContext(TreeDispatcherContext);
491 - selectedElementID = React.useContext(TreeStateContext).selectedElementID;
494 + inspectedElementID =
495 + React.useContext(TreeStateContext).inspectedElementID;
496 return null;
497 }
498
@@ -497,13 +501,15 @@ describe('ProfilerContext', () => {
501 // Select an element within the second root.
502 await utils.actAsync(() =>
503 TestRenderer.create(
500 - <Contexts defaultSelectedElementID={id} defaultSelectedElementIndex={3}>
504 + <Contexts
505 + defaultInspectedElementID={id}
506 + defaultInspectedElementIndex={3}>
507 <ContextReader />
508 </Contexts>,
509 ),
510 );
511
506 - expect(selectedElementID).toBe(id);
512 + expect(inspectedElementID).toBe(id);
513
514 // Profile and record more updates to both roots
515 await utils.actAsync(() => store.profilerStore.startProfiling());
@@ -517,7 +523,7 @@ describe('ProfilerContext', () => {
523 utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
524
525 // Verify that the initial Profiler root selection is maintained.
520 - expect(selectedElementID).toBe(otherID);
526 + expect(inspectedElementID).toBe(otherID);
527 expect(context).not.toBeNull();
528 expect(context.rootID).toBe(store.getRootIDForElement(id));
529 });
@@ -553,10 +559,11 @@ describe('ProfilerContext', () => {
559 `);
560
561 let context: Context = ((null: any): Context);
556 - let selectedElementID = null;
562 + let inspectedElementID = null;
563 function ContextReader() {
564 context = React.useContext(ProfilerContext);
559 - selectedElementID = React.useContext(TreeStateContext).selectedElementID;
565 + inspectedElementID =
566 + React.useContext(TreeStateContext).inspectedElementID;
567 return null;
568 }
569
@@ -567,14 +574,14 @@ describe('ProfilerContext', () => {
574 </Contexts>,
575 ),
576 );
570 - expect(selectedElementID).toBeNull();
577 + expect(inspectedElementID).toBeNull();
578
579 // Select an element in the Profiler tab and verify that the selection is synced to the Components tab.
580 await utils.actAsync(() => context.selectFiber(parentID, 'Parent'));
574 - expect(selectedElementID).toBe(parentID);
581 + expect(inspectedElementID).toBe(parentID);
582
583 // Select an unmounted element and verify no Components tab selection doesn't change.
584 await utils.actAsync(() => context.selectFiber(childID, 'Child'));
578 - expect(selectedElementID).toBe(parentID);
585 + expect(inspectedElementID).toBe(parentID);
586 });
587 });
packages/react-devtools-shared/src/devtools/utils.js
+1 -1
@@ -67,7 +67,7 @@ export function printStore(
67 if (state === null) {
68 return '';
69 }
70 - return state.selectedElementIndex === index ? `→` : ' ';
70 + return state.inspectedElementIndex === index ? `→` : ' ';
71 }
72
73 function printErrorsAndWarnings(element: Element): string {
packages/react-devtools-shared/src/devtools/views/Components/Element.js
+2 -2
@@ -33,7 +33,7 @@ type Props = {
33
34 export default function Element({data, index, style}: Props): React.Node {
35 const store = useContext(StoreContext);
36 - const {ownerFlatTree, ownerID, selectedElementID} =
36 + const {ownerFlatTree, ownerID, inspectedElementID} =
37 useContext(TreeStateContext);
38 const dispatch = useContext(TreeDispatcherContext);
39
@@ -46,7 +46,7 @@ export default function Element({data, index, style}: Props): React.Node {
46
47 const {isNavigatingWithKeyboard, onElementMouseEnter, treeFocused} = data;
48 const id = element === null ? null : element.id;
49 - const isSelected = selectedElementID === id;
49 + const isSelected = inspectedElementID === id;
50
51 const errorsAndWarningsSubscription = useMemo(
52 () => ({
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSourcePanel.js
+7 -3
@@ -28,7 +28,7 @@ function InspectedElementSourcePanel({
28 symbolicatedSourcePromise,
29 }: Props): React.Node {
30 return (
31 - <div data-testname="InspectedElementView-Source">
31 + <div>
32 <div className={styles.SourceHeaderRow}>
33 <div className={styles.SourceHeader}>source</div>
34
@@ -84,7 +84,9 @@ function FormattedSourceString({source, symbolicatedSourcePromise}: Props) {
84 const {sourceURL, line} = source;
85
86 return (
87 - <div className={styles.SourceOneLiner}>
87 + <div
88 + className={styles.SourceOneLiner}
89 + data-testname="InspectedElementView-FormattedSourceString">
90 {formatSourceForDisplay(sourceURL, line)}
91 </div>
92 );
@@ -93,7 +95,9 @@ function FormattedSourceString({source, symbolicatedSourcePromise}: Props) {
95 const {sourceURL, line} = symbolicatedSource;
96
97 return (
96 - <div className={styles.SourceOneLiner}>
98 + <div
99 + className={styles.SourceOneLiner}
100 + data-testname="InspectedElementView-FormattedSourceString">
101 {formatSourceForDisplay(sourceURL, line)}
102 </div>
103 );
packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/context.js
+9 -9
@@ -100,10 +100,10 @@ function NativeStyleContextController({children}: Props): React.Node {
100 [store],
101 );
102
103 - // It's very important that this context consumes selectedElementID and not NativeStyleID.
103 + // It's very important that this context consumes inspectedElementID and not NativeStyleID.
104 // Otherwise the effect that sends the "inspect" message across the bridge-
105 // would itself be blocked by the same render that suspends (waiting for the data).
106 - const {selectedElementID} = useContext<StateContext>(TreeStateContext);
106 + const {inspectedElementID} = useContext<StateContext>(TreeStateContext);
107
108 const [currentStyleAndLayout, setCurrentStyleAndLayout] =
109 useState<StyleAndLayoutFrontend | null>(null);
@@ -128,7 +128,7 @@ function NativeStyleContextController({children}: Props): React.Node {
128 resource.write(element, styleAndLayout);
129
130 // Schedule update with React if the currently-selected element has been invalidated.
131 - if (id === selectedElementID) {
131 + if (id === inspectedElementID) {
132 setCurrentStyleAndLayout(styleAndLayout);
133 }
134 }
@@ -141,15 +141,15 @@ function NativeStyleContextController({children}: Props): React.Node {
141 'NativeStyleEditor_styleAndLayout',
142 onStyleAndLayout,
143 );
144 - }, [bridge, currentStyleAndLayout, selectedElementID, store]);
144 + }, [bridge, currentStyleAndLayout, inspectedElementID, store]);
145
146 // This effect handler polls for updates on the currently selected element.
147 useEffect(() => {
148 - if (selectedElementID === null) {
148 + if (inspectedElementID === null) {
149 return () => {};
150 }
151
152 - const rendererID = store.getRendererIDForElement(selectedElementID);
152 + const rendererID = store.getRendererIDForElement(inspectedElementID);
153
154 let timeoutID: TimeoutID | null = null;
155
@@ -158,7 +158,7 @@ function NativeStyleContextController({children}: Props): React.Node {
158
159 if (rendererID !== null) {
160 bridge.send('NativeStyleEditor_measure', {
161 - id: selectedElementID,
161 + id: inspectedElementID,
162 rendererID,
163 });
164 }
@@ -170,7 +170,7 @@ function NativeStyleContextController({children}: Props): React.Node {
170
171 const onStyleAndLayout = ({id}: StyleAndLayoutBackend) => {
172 // If this is the element we requested, wait a little bit and then ask for another update.
173 - if (id === selectedElementID) {
173 + if (id === inspectedElementID) {
174 if (timeoutID !== null) {
175 clearTimeout(timeoutID);
176 }
@@ -190,7 +190,7 @@ function NativeStyleContextController({children}: Props): React.Node {
190 clearTimeout(timeoutID);
191 }
192 };
193 - }, [bridge, selectedElementID, store]);
193 + }, [bridge, inspectedElementID, store]);
194
195 const value = useMemo(
196 () => ({getStyleAndLayout}),
packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.js
+5 -5
@@ -28,19 +28,19 @@ export default function SelectedTreeHighlight(_: {}): React.Node {
28 const {lineHeight} = useContext(SettingsContext);
29 const store = useContext(StoreContext);
30 const treeFocused = useContext(TreeFocusedContext);
31 - const {ownerID, selectedElementID} = useContext(TreeStateContext);
31 + const {ownerID, inspectedElementID} = useContext(TreeStateContext);
32
33 const subscription = useMemo(
34 () => ({
35 getCurrentValue: () => {
36 if (
37 - selectedElementID === null ||
38 - store.isInsideCollapsedSubTree(selectedElementID)
37 + inspectedElementID === null ||
38 + store.isInsideCollapsedSubTree(inspectedElementID)
39 ) {
40 return null;
41 }
42
43 - const element = store.getElementByID(selectedElementID);
43 + const element = store.getElementByID(inspectedElementID);
44 if (
45 element === null ||
46 element.isCollapsed ||
@@ -83,7 +83,7 @@ export default function SelectedTreeHighlight(_: {}): React.Node {
83 };
84 },
85 }),
86 - [selectedElementID, store],
86 + [inspectedElementID, store],
87 );
88 const data = useSubscription<Data | null>(subscription);
89
packages/react-devtools-shared/src/devtools/views/Components/Tree.js
+16 -16
@@ -54,8 +54,8 @@ export default function Tree(): React.Node {
54 ownerID,
55 searchIndex,
56 searchResults,
57 - selectedElementID,
58 - selectedElementIndex,
57 + inspectedElementID,
58 + inspectedElementIndex,
59 } = useContext(TreeStateContext);
60 const bridge = useContext(BridgeContext);
61 const store = useContext(StoreContext);
@@ -84,11 +84,11 @@ export default function Tree(): React.Node {
84 // Using a callback ref accounts for this case...
85 const listCallbackRef = useCallback(
86 (list: $FlowFixMe) => {
87 - if (list != null && selectedElementIndex !== null) {
88 - list.scrollToItem(selectedElementIndex, 'smart');
87 + if (list != null && inspectedElementIndex !== null) {
88 + list.scrollToItem(inspectedElementIndex, 'smart');
89 }
90 },
91 - [selectedElementIndex],
91 + [inspectedElementIndex],
92 );
93
94 // Picking an element in the inspector should put focus into the tree.
@@ -133,8 +133,8 @@ export default function Tree(): React.Node {
133 case 'ArrowLeft':
134 event.preventDefault();
135 element =
136 - selectedElementID !== null
137 - ? store.getElementByID(selectedElementID)
136 + inspectedElementID !== null
137 + ? store.getElementByID(inspectedElementID)
138 : null;
139 if (element !== null) {
140 if (event.altKey) {
@@ -153,8 +153,8 @@ export default function Tree(): React.Node {
153 case 'ArrowRight':
154 event.preventDefault();
155 element =
156 - selectedElementID !== null
157 - ? store.getElementByID(selectedElementID)
156 + inspectedElementID !== null
157 + ? store.getElementByID(inspectedElementID)
158 : null;
159 if (element !== null) {
160 if (event.altKey) {
@@ -202,7 +202,7 @@ export default function Tree(): React.Node {
202 return () => {
203 container.removeEventListener('keydown', handleKeyDown);
204 };
205 - }, [dispatch, selectedElementID, store]);
205 + }, [dispatch, inspectedElementID, store]);
206
207 // Focus management.
208 const handleBlur = useCallback(() => setTreeFocused(false), []);
@@ -213,15 +213,15 @@ export default function Tree(): React.Node {
213 switch (event.key) {
214 case 'Enter':
215 case ' ':
216 - if (selectedElementID !== null) {
217 - dispatch({type: 'SELECT_OWNER', payload: selectedElementID});
216 + if (inspectedElementID !== null) {
217 + dispatch({type: 'SELECT_OWNER', payload: inspectedElementID});
218 }
219 break;
220 default:
221 break;
222 }
223 },
224 - [dispatch, selectedElementID],
224 + [dispatch, inspectedElementID],
225 );
226
227 // If we switch the selected element while using the keyboard,
@@ -238,8 +238,8 @@ export default function Tree(): React.Node {
238 didSelectNewSearchResult = true;
239 }
240 if (isNavigatingWithKeyboard || didSelectNewSearchResult) {
241 - if (selectedElementID !== null) {
242 - highlightHostInstance(selectedElementID);
241 + if (inspectedElementID !== null) {
242 + highlightHostInstance(inspectedElementID);
243 } else {
244 clearHighlightHostInstance();
245 }
@@ -250,7 +250,7 @@ export default function Tree(): React.Node {
250 highlightHostInstance,
251 searchIndex,
252 searchResults,
253 - selectedElementID,
253 + inspectedElementID,
254 ]);
255
256 // Highlight last hovered element.
packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js
+145 -189
@@ -29,14 +29,12 @@ import type {ReactContext} from 'shared/ReactTypes';
29 import * as React from 'react';
30 import {
31 createContext,
32 - useCallback,
32 useContext,
33 useEffect,
34 useLayoutEffect,
35 useMemo,
36 useReducer,
37 useRef,
39 - startTransition,
38 } from 'react';
39 import {createRegExp} from '../utils';
40 import {StoreContext} from '../context';
@@ -48,8 +46,6 @@ export type StateContext = {
46 // Tree
47 numElements: number,
48 ownerSubtreeLeafElementID: number | null,
51 - selectedElementID: number | null,
52 - selectedElementIndex: number | null,
49
50 // Search
51 searchIndex: number | null,
@@ -62,6 +58,7 @@ export type StateContext = {
58
59 // Inspection element panel
60 inspectedElementID: number | null,
61 + inspectedElementIndex: number | null,
62 };
63
64 type ACTION_GO_TO_NEXT_SEARCH_RESULT = {
@@ -123,9 +120,6 @@ type ACTION_SET_SEARCH_TEXT = {
120 type: 'SET_SEARCH_TEXT',
121 payload: string,
122 };
126 -type ACTION_UPDATE_INSPECTED_ELEMENT_ID = {
127 - type: 'UPDATE_INSPECTED_ELEMENT_ID',
128 -};
123
124 type Action =
125 | ACTION_GO_TO_NEXT_SEARCH_RESULT
@@ -145,8 +139,7 @@ type Action =
139 | ACTION_SELECT_PREVIOUS_SIBLING_IN_TREE
140 | ACTION_SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE
141 | ACTION_SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE
148 - | ACTION_SET_SEARCH_TEXT
149 - | ACTION_UPDATE_INSPECTED_ELEMENT_ID;
142 + | ACTION_SET_SEARCH_TEXT;
143
144 export type DispatcherContext = (action: Action) => void;
145
@@ -162,8 +155,6 @@ type State = {
155 // Tree
156 numElements: number,
157 ownerSubtreeLeafElementID: number | null,
165 - selectedElementID: number | null,
166 - selectedElementIndex: number | null,
158
159 // Search
160 searchIndex: number | null,
@@ -176,14 +167,15 @@ type State = {
167
168 // Inspection element panel
169 inspectedElementID: number | null,
170 + inspectedElementIndex: number | null,
171 };
172
173 function reduceTreeState(store: Store, state: State, action: Action): State {
174 let {
175 numElements,
176 ownerSubtreeLeafElementID,
185 - selectedElementIndex,
186 - selectedElementID,
177 + inspectedElementID,
178 + inspectedElementIndex,
179 } = state;
180 const ownerID = state.ownerID;
181
@@ -201,34 +193,33 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
193 // We deduce the parent-child mapping from removedIDs (id -> parentID)
194 // because by now it's too late to read them from the store.
195 while (
204 - selectedElementID !== null &&
205 - removedIDs.has(selectedElementID)
196 + inspectedElementID !== null &&
197 + removedIDs.has(inspectedElementID)
198 ) {
207 - selectedElementID = ((removedIDs.get(
208 - selectedElementID,
209 - ): any): number);
199 + // $FlowExpectedError[incompatible-type]
200 + inspectedElementID = removedIDs.get(inspectedElementID);
201 }
211 - if (selectedElementID === 0) {
202 + if (inspectedElementID === 0) {
203 // The whole root was removed.
213 - selectedElementIndex = null;
204 + inspectedElementIndex = null;
205 }
206 break;
207 case 'SELECT_CHILD_ELEMENT_IN_TREE':
208 ownerSubtreeLeafElementID = null;
209
219 - if (selectedElementIndex !== null) {
220 - const selectedElement = store.getElementAtIndex(
221 - ((selectedElementIndex: any): number),
210 + if (inspectedElementIndex !== null) {
211 + const inspectedElement = store.getElementAtIndex(
212 + inspectedElementIndex,
213 );
214 if (
224 - selectedElement !== null &&
225 - selectedElement.children.length > 0 &&
226 - !selectedElement.isCollapsed
215 + inspectedElement !== null &&
216 + inspectedElement.children.length > 0 &&
217 + !inspectedElement.isCollapsed
218 ) {
228 - const firstChildID = selectedElement.children[0];
219 + const firstChildID = inspectedElement.children[0];
220 const firstChildIndex = store.getIndexOfElementID(firstChildID);
221 if (firstChildIndex !== null) {
231 - selectedElementIndex = firstChildIndex;
222 + inspectedElementIndex = firstChildIndex;
223 }
224 }
225 }
@@ -236,7 +227,8 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
227 case 'SELECT_ELEMENT_AT_INDEX':
228 ownerSubtreeLeafElementID = null;
229
239 - selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
230 + inspectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX)
231 + .payload;
232 break;
233 case 'SELECT_ELEMENT_BY_ID':
234 ownerSubtreeLeafElementID = null;
@@ -245,30 +237,30 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
237 // It might also cause problems if the specified element was inside of a (not yet expanded) subtree.
238 lookupIDForIndex = false;
239
248 - selectedElementID = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
249 - selectedElementIndex =
250 - selectedElementID === null
240 + inspectedElementID = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
241 + inspectedElementIndex =
242 + inspectedElementID === null
243 ? null
252 - : store.getIndexOfElementID(selectedElementID);
244 + : store.getIndexOfElementID(inspectedElementID);
245 break;
246 case 'SELECT_NEXT_ELEMENT_IN_TREE':
247 ownerSubtreeLeafElementID = null;
248
249 if (
258 - selectedElementIndex === null ||
259 - selectedElementIndex + 1 >= numElements
250 + inspectedElementIndex === null ||
251 + inspectedElementIndex + 1 >= numElements
252 ) {
261 - selectedElementIndex = 0;
253 + inspectedElementIndex = 0;
254 } else {
263 - selectedElementIndex++;
255 + inspectedElementIndex++;
256 }
257 break;
258 case 'SELECT_NEXT_SIBLING_IN_TREE':
259 ownerSubtreeLeafElementID = null;
260
269 - if (selectedElementIndex !== null) {
261 + if (inspectedElementIndex !== null) {
262 const selectedElement = store.getElementAtIndex(
271 - ((selectedElementIndex: any): number),
263 + ((inspectedElementIndex: any): number),
264 );
265 if (selectedElement !== null && selectedElement.parentID !== 0) {
266 const parent = store.getElementByID(selectedElement.parentID);
@@ -279,23 +271,23 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
271 selectedChildIndex < children.length - 1
272 ? children[selectedChildIndex + 1]
273 : children[0];
282 - selectedElementIndex = store.getIndexOfElementID(nextChildID);
274 + inspectedElementIndex = store.getIndexOfElementID(nextChildID);
275 }
276 }
277 }
278 break;
279 case 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE':
288 - if (selectedElementIndex !== null) {
280 + if (inspectedElementIndex !== null) {
281 if (
282 ownerSubtreeLeafElementID !== null &&
291 - ownerSubtreeLeafElementID !== selectedElementID
283 + ownerSubtreeLeafElementID !== inspectedElementID
284 ) {
285 const leafElement = store.getElementByID(ownerSubtreeLeafElementID);
286 if (leafElement !== null) {
287 let currentElement: null | Element = leafElement;
288 while (currentElement !== null) {
297 - if (currentElement.ownerID === selectedElementID) {
298 - selectedElementIndex = store.getIndexOfElementID(
289 + if (currentElement.ownerID === inspectedElementID) {
290 + inspectedElementIndex = store.getIndexOfElementID(
291 currentElement.id,
292 );
293 break;
@@ -308,23 +300,23 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
300 }
301 break;
302 case 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE':
311 - if (selectedElementIndex !== null) {
303 + if (inspectedElementIndex !== null) {
304 if (ownerSubtreeLeafElementID === null) {
305 // If this is the first time we're stepping through the owners tree,
306 // pin the current component as the owners list leaf.
307 // This will enable us to step back down to this component.
316 - ownerSubtreeLeafElementID = selectedElementID;
308 + ownerSubtreeLeafElementID = inspectedElementID;
309 }
310
311 const selectedElement = store.getElementAtIndex(
320 - ((selectedElementIndex: any): number),
312 + ((inspectedElementIndex: any): number),
313 );
314 if (selectedElement !== null && selectedElement.ownerID !== 0) {
315 const ownerIndex = store.getIndexOfElementID(
316 selectedElement.ownerID,
317 );
318 if (ownerIndex !== null) {
327 - selectedElementIndex = ownerIndex;
319 + inspectedElementIndex = ownerIndex;
320 }
321 }
322 }
@@ -332,16 +324,16 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
324 case 'SELECT_PARENT_ELEMENT_IN_TREE':
325 ownerSubtreeLeafElementID = null;
326
335 - if (selectedElementIndex !== null) {
327 + if (inspectedElementIndex !== null) {
328 const selectedElement = store.getElementAtIndex(
337 - ((selectedElementIndex: any): number),
329 + ((inspectedElementIndex: any): number),
330 );
331 if (selectedElement !== null && selectedElement.parentID !== 0) {
332 const parentIndex = store.getIndexOfElementID(
333 selectedElement.parentID,
334 );
335 if (parentIndex !== null) {
344 - selectedElementIndex = parentIndex;
336 + inspectedElementIndex = parentIndex;
337 }
338 }
339 }
@@ -349,18 +341,18 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
341 case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
342 ownerSubtreeLeafElementID = null;
343
352 - if (selectedElementIndex === null || selectedElementIndex === 0) {
353 - selectedElementIndex = numElements - 1;
344 + if (inspectedElementIndex === null || inspectedElementIndex === 0) {
345 + inspectedElementIndex = numElements - 1;
346 } else {
355 - selectedElementIndex--;
347 + inspectedElementIndex--;
348 }
349 break;
350 case 'SELECT_PREVIOUS_SIBLING_IN_TREE':
351 ownerSubtreeLeafElementID = null;
352
361 - if (selectedElementIndex !== null) {
353 + if (inspectedElementIndex !== null) {
354 const selectedElement = store.getElementAtIndex(
363 - ((selectedElementIndex: any): number),
355 + ((inspectedElementIndex: any): number),
356 );
357 if (selectedElement !== null && selectedElement.parentID !== 0) {
358 const parent = store.getElementByID(selectedElement.parentID);
@@ -371,7 +363,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
363 selectedChildIndex > 0
364 ? children[selectedChildIndex - 1]
365 : children[children.length - 1];
374 - selectedElementIndex = store.getIndexOfElementID(nextChildID);
366 + inspectedElementIndex = store.getIndexOfElementID(nextChildID);
367 }
368 }
369 }
@@ -384,7 +376,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
376 }
377
378 let flatIndex = 0;
387 - if (selectedElementIndex !== null) {
379 + if (inspectedElementIndex !== null) {
380 // Resume from the current position in the list.
381 // Otherwise step to the previous item, relative to the current selection.
382 for (
@@ -393,7 +385,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
385 i--
386 ) {
387 const {index} = elementIndicesWithErrorsOrWarnings[i];
396 - if (index >= selectedElementIndex) {
388 + if (index >= inspectedElementIndex) {
389 flatIndex = i;
390 } else {
391 break;
@@ -407,12 +399,12 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
399 elementIndicesWithErrorsOrWarnings[
400 elementIndicesWithErrorsOrWarnings.length - 1
401 ];
410 - selectedElementID = prevEntry.id;
411 - selectedElementIndex = prevEntry.index;
402 + inspectedElementID = prevEntry.id;
403 + inspectedElementIndex = prevEntry.index;
404 } else {
405 prevEntry = elementIndicesWithErrorsOrWarnings[flatIndex - 1];
414 - selectedElementID = prevEntry.id;
415 - selectedElementIndex = prevEntry.index;
406 + inspectedElementID = prevEntry.id;
407 + inspectedElementIndex = prevEntry.index;
408 }
409
410 lookupIDForIndex = false;
@@ -426,12 +418,12 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
418 }
419
420 let flatIndex = -1;
429 - if (selectedElementIndex !== null) {
421 + if (inspectedElementIndex !== null) {
422 // Resume from the current position in the list.
423 // Otherwise step to the next item, relative to the current selection.
424 for (let i = 0; i < elementIndicesWithErrorsOrWarnings.length; i++) {
425 const {index} = elementIndicesWithErrorsOrWarnings[i];
434 - if (index <= selectedElementIndex) {
426 + if (index <= inspectedElementIndex) {
427 flatIndex = i;
428 } else {
429 break;
@@ -442,12 +434,12 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
434 let nextEntry;
435 if (flatIndex >= elementIndicesWithErrorsOrWarnings.length - 1) {
436 nextEntry = elementIndicesWithErrorsOrWarnings[0];
445 - selectedElementID = nextEntry.id;
446 - selectedElementIndex = nextEntry.index;
437 + inspectedElementID = nextEntry.id;
438 + inspectedElementIndex = nextEntry.index;
439 } else {
440 nextEntry = elementIndicesWithErrorsOrWarnings[flatIndex + 1];
449 - selectedElementID = nextEntry.id;
450 - selectedElementIndex = nextEntry.index;
441 + inspectedElementID = nextEntry.id;
442 + inspectedElementIndex = nextEntry.index;
443 }
444
445 lookupIDForIndex = false;
@@ -460,12 +452,15 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
452 }
453
454 // Keep selected item ID and index in sync.
463 - if (lookupIDForIndex && selectedElementIndex !== state.selectedElementIndex) {
464 - if (selectedElementIndex === null) {
465 - selectedElementID = null;
455 + if (
456 + lookupIDForIndex &&
457 + inspectedElementIndex !== state.inspectedElementIndex
458 + ) {
459 + if (inspectedElementIndex === null) {
460 + inspectedElementID = null;
461 } else {
467 - selectedElementID = store.getElementIDAtIndex(
468 - ((selectedElementIndex: any): number),
462 + inspectedElementID = store.getElementIDAtIndex(
463 + ((inspectedElementIndex: any): number),
464 );
465 }
466 }
@@ -475,8 +470,8 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
470
471 numElements,
472 ownerSubtreeLeafElementID,
478 - selectedElementIndex,
479 - selectedElementID,
473 + inspectedElementIndex,
474 + inspectedElementID,
475 };
476 }
477
@@ -485,8 +480,8 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
480 searchIndex,
481 searchResults,
482 searchText,
488 - selectedElementID,
489 - selectedElementIndex,
483 + inspectedElementID,
484 + inspectedElementIndex,
485 } = state;
486 const ownerID = state.ownerID;
487
@@ -594,11 +589,11 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
589 });
590 if (searchResults.length > 0) {
591 if (prevSearchIndex === null) {
597 - if (selectedElementIndex !== null) {
592 + if (inspectedElementIndex !== null) {
593 searchIndex = getNearestResultIndex(
594 store,
595 searchResults,
601 - selectedElementIndex,
596 + inspectedElementIndex,
597 );
598 } else {
599 searchIndex = 0;
@@ -619,7 +614,7 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
614 }
615
616 if (searchText !== prevSearchText) {
622 - const newSearchIndex = searchResults.indexOf(selectedElementID);
617 + const newSearchIndex = searchResults.indexOf(inspectedElementID);
618 if (newSearchIndex === -1) {
619 // Only move the selection if the new query
620 // doesn't match the current selection anymore.
@@ -631,17 +626,17 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
626 }
627 }
628 if (didRequestSearch && searchIndex !== null) {
634 - selectedElementID = ((searchResults[searchIndex]: any): number);
635 - selectedElementIndex = store.getIndexOfElementID(
636 - ((selectedElementID: any): number),
629 + inspectedElementID = ((searchResults[searchIndex]: any): number);
630 + inspectedElementIndex = store.getIndexOfElementID(
631 + ((inspectedElementID: any): number),
632 );
633 }
634
635 return {
636 ...state,
637
643 - selectedElementID,
644 - selectedElementIndex,
638 + inspectedElementID,
639 + inspectedElementIndex,
640
641 searchIndex,
642 searchResults,
@@ -652,14 +647,14 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
647 function reduceOwnersState(store: Store, state: State, action: Action): State {
648 let {
649 numElements,
655 - selectedElementID,
656 - selectedElementIndex,
650 ownerID,
651 ownerFlatTree,
652 + inspectedElementID,
653 + inspectedElementIndex,
654 } = state;
655 const {searchIndex, searchResults, searchText} = state;
656
662 - let prevSelectedElementIndex = selectedElementIndex;
657 + let prevInspectedElementIndex = inspectedElementIndex;
658
659 switch (action.type) {
660 case 'HANDLE_STORE_MUTATION':
@@ -667,75 +662,76 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
662 if (!store.containsElement(ownerID)) {
663 ownerID = null;
664 ownerFlatTree = null;
670 - selectedElementID = null;
665 + prevInspectedElementIndex = null;
666 } else {
667 ownerFlatTree = store.getOwnersListForElement(ownerID);
673 - if (selectedElementID !== null) {
668 + if (inspectedElementID !== null) {
669 // Mutation might have caused the index of this ID to shift.
675 - selectedElementIndex = ownerFlatTree.findIndex(
676 - element => element.id === selectedElementID,
670 + prevInspectedElementIndex = ownerFlatTree.findIndex(
671 + element => element.id === inspectedElementID,
672 );
673 }
674 }
675 } else {
681 - if (selectedElementID !== null) {
676 + if (inspectedElementID !== null) {
677 // Mutation might have caused the index of this ID to shift.
683 - selectedElementIndex = store.getIndexOfElementID(selectedElementID);
678 + inspectedElementIndex = store.getIndexOfElementID(inspectedElementID);
679 }
680 }
686 - if (selectedElementIndex === -1) {
681 + if (inspectedElementIndex === -1) {
682 // If we couldn't find this ID after mutation, unselect it.
688 - selectedElementIndex = null;
689 - selectedElementID = null;
683 + inspectedElementIndex = null;
684 + inspectedElementID = null;
685 }
686 break;
687 case 'RESET_OWNER_STACK':
688 ownerID = null;
689 ownerFlatTree = null;
695 - selectedElementIndex =
696 - selectedElementID !== null
697 - ? store.getIndexOfElementID(selectedElementID)
690 + inspectedElementIndex =
691 + inspectedElementID !== null
692 + ? store.getIndexOfElementID(inspectedElementID)
693 : null;
694 break;
695 case 'SELECT_ELEMENT_AT_INDEX':
696 if (ownerFlatTree !== null) {
702 - selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
697 + inspectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX)
698 + .payload;
699 }
700 break;
701 case 'SELECT_ELEMENT_BY_ID':
702 if (ownerFlatTree !== null) {
703 const payload = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
704 if (payload === null) {
709 - selectedElementIndex = null;
705 + inspectedElementIndex = null;
706 } else {
711 - selectedElementIndex = ownerFlatTree.findIndex(
707 + inspectedElementIndex = ownerFlatTree.findIndex(
708 element => element.id === payload,
709 );
710
711 // If the selected element is outside of the current owners list,
712 // exit the list and select the element in the main tree.
713 // This supports features like toggling Suspense.
718 - if (selectedElementIndex !== null && selectedElementIndex < 0) {
714 + if (inspectedElementIndex !== null && inspectedElementIndex < 0) {
715 ownerID = null;
716 ownerFlatTree = null;
721 - selectedElementIndex = store.getIndexOfElementID(payload);
717 + inspectedElementIndex = store.getIndexOfElementID(payload);
718 }
719 }
720 }
721 break;
722 case 'SELECT_NEXT_ELEMENT_IN_TREE':
723 if (ownerFlatTree !== null && ownerFlatTree.length > 0) {
728 - if (selectedElementIndex === null) {
729 - selectedElementIndex = 0;
730 - } else if (selectedElementIndex + 1 < ownerFlatTree.length) {
731 - selectedElementIndex++;
724 + if (inspectedElementIndex === null) {
725 + inspectedElementIndex = 0;
726 + } else if (inspectedElementIndex + 1 < ownerFlatTree.length) {
727 + inspectedElementIndex++;
728 }
729 }
730 break;
731 case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
732 if (ownerFlatTree !== null && ownerFlatTree.length > 0) {
737 - if (selectedElementIndex !== null && selectedElementIndex > 0) {
738 - selectedElementIndex--;
733 + if (inspectedElementIndex !== null && inspectedElementIndex > 0) {
734 + inspectedElementIndex--;
735 }
736 }
737 break;
@@ -747,8 +743,8 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
743 ownerFlatTree = store.getOwnersListForElement(ownerID);
744
745 // Always force reset selection to be the top of the new owner tree.
750 - selectedElementIndex = 0;
751 - prevSelectedElementIndex = null;
746 + inspectedElementIndex = 0;
747 + prevInspectedElementIndex = null;
748 }
749 break;
750 default:
@@ -769,12 +765,12 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
765 }
766
767 // Keep selected item ID and index in sync.
772 - if (selectedElementIndex !== prevSelectedElementIndex) {
773 - if (selectedElementIndex === null) {
774 - selectedElementID = null;
768 + if (inspectedElementIndex !== prevInspectedElementIndex) {
769 + if (inspectedElementIndex === null) {
770 + inspectedElementID = null;
771 } else {
772 if (ownerFlatTree !== null) {
777 - selectedElementID = ownerFlatTree[selectedElementIndex].id;
773 + inspectedElementID = ownerFlatTree[inspectedElementIndex].id;
774 }
775 }
776 }
@@ -783,8 +779,6 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
779 ...state,
780
781 numElements,
786 - selectedElementID,
787 - selectedElementIndex,
782
783 searchIndex,
784 searchResults,
@@ -792,49 +786,27 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
786
787 ownerID,
788 ownerFlatTree,
795 - };
796 -}
789
798 -function reduceSuspenseState(
799 - store: Store,
800 - state: State,
801 - action: Action,
802 -): State {
803 - const {type} = action;
804 - switch (type) {
805 - case 'UPDATE_INSPECTED_ELEMENT_ID':
806 - if (state.inspectedElementID !== state.selectedElementID) {
807 - return {
808 - ...state,
809 - inspectedElementID: state.selectedElementID,
810 - };
811 - }
812 - break;
813 - default:
814 - break;
815 - }
816 -
817 - // React can bailout of no-op updates.
818 - return state;
790 + inspectedElementID,
791 + inspectedElementIndex,
792 + };
793 }
794
795 type Props = {
796 children: React$Node,
797
798 // Used for automated testing
825 - defaultInspectedElementID?: ?number,
799 defaultOwnerID?: ?number,
827 - defaultSelectedElementID?: ?number,
828 - defaultSelectedElementIndex?: ?number,
800 + defaultInspectedElementID?: ?number,
801 + defaultInspectedElementIndex?: ?number,
802 };
803
804 // TODO Remove TreeContextController wrapper element once global Context.write API exists.
805 function TreeContextController({
806 children,
834 - defaultInspectedElementID,
807 defaultOwnerID,
836 - defaultSelectedElementID,
837 - defaultSelectedElementIndex,
808 + defaultInspectedElementID,
809 + defaultInspectedElementIndex,
810 }: Props): React.Node {
811 const store = useContext(StoreContext);
812
@@ -865,23 +837,22 @@ function TreeContextController({
837 case 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE':
838 case 'SELECT_PREVIOUS_SIBLING_IN_TREE':
839 case 'SELECT_OWNER':
868 - case 'UPDATE_INSPECTED_ELEMENT_ID':
840 case 'SET_SEARCH_TEXT':
841 state = reduceTreeState(store, state, action);
842 state = reduceSearchState(store, state, action);
843 state = reduceOwnersState(store, state, action);
873 - state = reduceSuspenseState(store, state, action);
844
845 + // TODO(hoxyq): review
846 // If the selected ID is in a collapsed subtree, reset the selected index to null.
847 // We'll know the correct index after the layout effect will toggle the tree,
848 // and the store tree is mutated to account for that.
849 if (
879 - state.selectedElementID !== null &&
880 - store.isInsideCollapsedSubTree(state.selectedElementID)
850 + state.inspectedElementID !== null &&
851 + store.isInsideCollapsedSubTree(state.inspectedElementID)
852 ) {
853 return {
854 ...state,
884 - selectedElementIndex: null,
855 + inspectedElementIndex: null,
856 };
857 }
858
@@ -897,16 +868,6 @@ function TreeContextController({
868 // Tree
869 numElements: store.numElements,
870 ownerSubtreeLeafElementID: null,
900 - selectedElementID:
901 - defaultSelectedElementID != null
902 - ? defaultSelectedElementID
903 - : store.lastSelectedHostInstanceElementId,
904 - selectedElementIndex:
905 - defaultSelectedElementIndex != null
906 - ? defaultSelectedElementIndex
907 - : store.lastSelectedHostInstanceElementId
908 - ? store.getIndexOfElementID(store.lastSelectedHostInstanceElementId)
909 - : null,
871
872 // Search
873 searchIndex: null,
@@ -922,42 +883,38 @@ function TreeContextController({
883 defaultInspectedElementID != null
884 ? defaultInspectedElementID
885 : store.lastSelectedHostInstanceElementId,
886 + inspectedElementIndex:
887 + defaultInspectedElementIndex != null
888 + ? defaultInspectedElementIndex
889 + : store.lastSelectedHostInstanceElementId
890 + ? store.getIndexOfElementID(store.lastSelectedHostInstanceElementId)
891 + : null,
892 });
893
927 - const dispatchWrapper = useCallback(
928 - (action: Action) => {
929 - dispatch(action);
930 - startTransition(() => {
931 - dispatch({type: 'UPDATE_INSPECTED_ELEMENT_ID'});
932 - });
933 - },
934 - [dispatch],
935 - );
936 -
894 // Listen for host element selections.
895 useEffect(() => {
896 const handler = (id: Element['id']) =>
940 - dispatchWrapper({type: 'SELECT_ELEMENT_BY_ID', payload: id});
897 + dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: id});
898
899 store.addListener('hostInstanceSelected', handler);
900 return () => store.removeListener('hostInstanceSelected', handler);
944 - }, [store, dispatchWrapper]);
901 + }, [store, dispatch]);
902
903 // If a newly-selected search result or inspection selection is inside of a collapsed subtree, auto expand it.
904 // This needs to be a layout effect to avoid temporarily flashing an incorrect selection.
948 - const prevSelectedElementID = useRef<number | null>(null);
905 + const prevInspectedElementID = useRef<number | null>(null);
906 useLayoutEffect(() => {
950 - if (state.selectedElementID !== prevSelectedElementID.current) {
951 - prevSelectedElementID.current = state.selectedElementID;
907 + if (state.inspectedElementID !== prevInspectedElementID.current) {
908 + prevInspectedElementID.current = state.inspectedElementID;
909
953 - if (state.selectedElementID !== null) {
954 - const element = store.getElementByID(state.selectedElementID);
910 + if (state.inspectedElementID !== null) {
911 + const element = store.getElementByID(state.inspectedElementID);
912 if (element !== null && element.parentID > 0) {
913 store.toggleIsCollapsed(element.parentID, false);
914 }
915 }
916 }
960 - }, [state.selectedElementID, store]);
917 + }, [state.inspectedElementID, store]);
918
919 // Mutations to the underlying tree may impact this context (e.g. search results, selection state).
920 useEffect(() => {
@@ -965,7 +922,7 @@ function TreeContextController({
922 Array<number>,
923 Map<number, number>,
924 ]) => {
968 - dispatchWrapper({
925 + dispatch({
926 type: 'HANDLE_STORE_MUTATION',
927 payload: [addedElementIDs, removedElementIDs],
928 });
@@ -976,20 +933,19 @@ function TreeContextController({
933 // At the moment, we can treat this as a mutation.
934 // We don't know which Elements were newly added/removed, but that should be okay in this case.
935 // It would only impact the search state, which is unlikely to exist yet at this point.
979 - dispatchWrapper({
936 + dispatch({
937 type: 'HANDLE_STORE_MUTATION',
938 payload: [[], new Map()],
939 });
940 }
941
942 store.addListener('mutated', handleStoreMutated);
986 -
943 return () => store.removeListener('mutated', handleStoreMutated);
988 - }, [dispatchWrapper, initialRevision, store]);
944 + }, [dispatch, initialRevision, store]);
945
946 return (
947 <TreeStateContext.Provider value={state}>
992 - <TreeDispatcherContext.Provider value={dispatchWrapper}>
948 + <TreeDispatcherContext.Provider value={dispatch}>
949 {children}
950 </TreeDispatcherContext.Provider>
951 </TreeStateContext.Provider>
@@ -1028,11 +984,11 @@ function recursivelySearchTree(
984 function getNearestResultIndex(
985 store: Store,
986 searchResults: Array<number>,
1031 - selectedElementIndex: number,
987 + inspectedElementIndex: number,
988 ): number {
989 const index = searchResults.findIndex(id => {
990 const innerIndex = store.getIndexOfElementID(id);
1035 - return innerIndex !== null && innerIndex >= selectedElementIndex;
991 + return innerIndex !== null && innerIndex >= inspectedElementIndex;
992 });
993
994 return index === -1 ? 0 : index;
packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js
+3 -3
@@ -88,7 +88,7 @@ type Props = {
88
89 function ProfilerContextController({children}: Props): React.Node {
90 const store = useContext(StoreContext);
91 - const {selectedElementID} = useContext(TreeStateContext);
91 + const {inspectedElementID} = useContext(TreeStateContext);
92 const dispatch = useContext(TreeDispatcherContext);
93
94 const {profilerStore} = store;
@@ -176,9 +176,9 @@ function ProfilerContextController({children}: Props): React.Node {
176
177 if (rootID === null || !dataForRoots.has(rootID)) {
178 let selectedElementRootID = null;
179 - if (selectedElementID !== null) {
179 + if (inspectedElementID !== null) {
180 selectedElementRootID =
181 - store.getRootIDForElement(selectedElementID);
181 + store.getRootIDForElement(inspectedElementID);
182 }
183 if (
184 selectedElementRootID !== null &&