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`.
This commit is contained in:
Ruslan Lesiutin
2025-01-09 18:13:24 +00:00
committed by GitHub
parent 3a3cdd3e91
commit 677e16ca76
12 changed files with 265 additions and 278 deletions
@@ -119,7 +119,7 @@ test.describe('Components', () => {
runOnlyForReactRange('>=16.8');
// Select the first list item in DevTools.
await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp');
await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp', true);
// Then read the inspected values.
const sourceText = await page.evaluate(() => {
@@ -127,7 +127,7 @@ test.describe('Components', () => {
const container = document.getElementById('devtools');
const source = findAllNodes(container, [
createTestNameSelector('InspectedElementView-Source'),
createTestNameSelector('InspectedElementView-FormattedSourceString'),
])[0];
return source.innerText;
@@ -237,35 +237,35 @@ test.describe('Components', () => {
}
await focusComponentSearch();
page.keyboard.insertText('List');
await page.keyboard.insertText('List');
let count = await getComponentSearchResultsCount();
expect(count).toBe('1 | 4');
page.keyboard.insertText('Item');
await page.keyboard.insertText('Item');
count = await getComponentSearchResultsCount();
expect(count).toBe('1 | 3');
page.keyboard.press('Enter');
await page.keyboard.press('Enter');
count = await getComponentSearchResultsCount();
expect(count).toBe('2 | 3');
page.keyboard.press('Enter');
await page.keyboard.press('Enter');
count = await getComponentSearchResultsCount();
expect(count).toBe('3 | 3');
page.keyboard.press('Enter');
await page.keyboard.press('Enter');
count = await getComponentSearchResultsCount();
expect(count).toBe('1 | 3');
page.keyboard.press('Shift+Enter');
await page.keyboard.press('Shift+Enter');
count = await getComponentSearchResultsCount();
expect(count).toBe('3 | 3');
page.keyboard.press('Shift+Enter');
await page.keyboard.press('Shift+Enter');
count = await getComponentSearchResultsCount();
expect(count).toBe('2 | 3');
page.keyboard.press('Shift+Enter');
await page.keyboard.press('Shift+Enter');
count = await getComponentSearchResultsCount();
expect(count).toBe('1 | 3');
});
@@ -27,7 +27,12 @@ async function getElementCount(page, displayName) {
}, displayName);
}
async function selectElement(page, displayName, waitForOwnersText) {
async function selectElement(
page,
displayName,
waitForOwnersText,
waitForSourceLoaded = false
) {
await page.evaluate(listItemText => {
const {createTestNameSelector, createTextSelector, findAllNodes} =
window.REACT_DOM_DEVTOOLS;
@@ -69,6 +74,20 @@ async function selectElement(page, displayName, waitForOwnersText) {
{titleText: displayName, ownersListText: waitForOwnersText}
);
}
if (waitForSourceLoaded) {
await page.waitForFunction(() => {
const {createTestNameSelector, findAllNodes} = window.REACT_DOM_DEVTOOLS;
const container = document.getElementById('devtools');
const sourceStringBlock = findAllNodes(container, [
createTestNameSelector('InspectedElementView-FormattedSourceString'),
])[0];
// Wait for a new source line to be fetched
return sourceStringBlock != null && sourceStringBlock.innerText != null;
});
}
}
module.exports = {
@@ -115,16 +115,15 @@ describe('InspectedElement', () => {
const Contexts = ({
children,
defaultSelectedElementID = null,
defaultSelectedElementIndex = null,
defaultInspectedElementID = null,
defaultInspectedElementIndex = null,
}) => (
<BridgeContext.Provider value={bridge}>
<StoreContext.Provider value={store}>
<SettingsContextController>
<TreeContextController
defaultSelectedElementID={defaultSelectedElementID}
defaultSelectedElementIndex={defaultSelectedElementIndex}
defaultInspectedElementID={defaultSelectedElementID}>
defaultInspectedElementID={defaultInspectedElementID}
defaultInspectedElementIndex={defaultInspectedElementIndex}>
<InspectedElementContextController>
{children}
</InspectedElementContextController>
@@ -167,8 +166,8 @@ describe('InspectedElement', () => {
testRendererInstance.update(
<ErrorBoundary>
<Contexts
defaultSelectedElementID={id}
defaultSelectedElementIndex={index}>
defaultInspectedElementID={id}
defaultInspectedElementIndex={index}>
<React.Suspense fallback={null}>
<Suspender id={id} index={index} />
</React.Suspense>
@@ -355,7 +354,7 @@ describe('InspectedElement', () => {
const {index, shouldHaveLegacyContext} = cases[i];
// HACK: Recreate TestRenderer instance because we rely on default state values
// from props like defaultSelectedElementID and it's easier to reset here than
// from props like defaultInspectedElementID and it's easier to reset here than
// to read the TreeDispatcherContext and update the selected ID that way.
// We're testing the inspected values here, not the context wiring, so that's ok.
withErrorsOrWarningsIgnored(
@@ -2069,7 +2068,7 @@ describe('InspectedElement', () => {
}, false);
// HACK: Recreate TestRenderer instance because we rely on default state values
// from props like defaultSelectedElementID and it's easier to reset here than
// from props like defaultInspectedElementID and it's easier to reset here than
// to read the TreeDispatcherContext and update the selected ID that way.
// We're testing the inspected values here, not the context wiring, so that's ok.
withErrorsOrWarningsIgnored(
@@ -2129,7 +2128,7 @@ describe('InspectedElement', () => {
}, false);
// HACK: Recreate TestRenderer instance because we rely on default state values
// from props like defaultSelectedElementID and it's easier to reset here than
// from props like defaultInspectedElementID and it's easier to reset here than
// to read the TreeDispatcherContext and update the selected ID that way.
// We're testing the inspected values here, not the context wiring, so that's ok.
withErrorsOrWarningsIgnored(
@@ -2408,8 +2407,8 @@ describe('InspectedElement', () => {
await utils.actAsync(() => {
root = TestRenderer.create(
<Contexts
defaultSelectedElementID={id}
defaultSelectedElementIndex={index}>
defaultInspectedElementID={id}
defaultInspectedElementIndex={index}>
<React.Suspense fallback={null}>
<Suspender target={id} />
</React.Suspense>
@@ -3101,6 +3100,7 @@ describe('InspectedElement', () => {
await utils.actAsync(() => {
store.componentFilters = [utils.createDisplayNameFilter('Wrapper')];
jest.runOnlyPendingTimers();
}, false);
expect(state).toMatchInlineSnapshot(`
@@ -3120,6 +3120,7 @@ describe('InspectedElement', () => {
await utils.actAsync(() => {
store.componentFilters = [];
jest.runOnlyPendingTimers();
}, false);
expect(state).toMatchInlineSnapshot(`
✕ 0, ⚠ 2
@@ -69,14 +69,14 @@ describe('ProfilerContext', () => {
const Contexts = ({
children = null,
defaultSelectedElementID = null,
defaultSelectedElementIndex = null,
defaultInspectedElementID = null,
defaultInspectedElementIndex = null,
}: any) => (
<BridgeContext.Provider value={bridge}>
<StoreContext.Provider value={store}>
<TreeContextController
defaultSelectedElementID={defaultSelectedElementID}
defaultSelectedElementIndex={defaultSelectedElementIndex}>
defaultInspectedElementID={defaultInspectedElementID}
defaultInspectedElementIndex={defaultInspectedElementIndex}>
<ProfilerContextController>{children}</ProfilerContextController>
</TreeContextController>
</StoreContext.Provider>
@@ -225,8 +225,8 @@ describe('ProfilerContext', () => {
await utils.actAsync(() =>
TestRenderer.create(
<Contexts
defaultSelectedElementID={store.getElementIDAtIndex(3)}
defaultSelectedElementIndex={3}>
defaultInspectedElementID={store.getElementIDAtIndex(3)}
defaultInspectedElementIndex={3}>
<ContextReader />
</Contexts>,
),
@@ -276,8 +276,8 @@ describe('ProfilerContext', () => {
await utils.actAsync(() =>
TestRenderer.create(
<Contexts
defaultSelectedElementID={store.getElementIDAtIndex(3)}
defaultSelectedElementIndex={3}>
defaultInspectedElementID={store.getElementIDAtIndex(3)}
defaultInspectedElementIndex={3}>
<ContextReader />
</Contexts>,
),
@@ -323,8 +323,8 @@ describe('ProfilerContext', () => {
await utils.actAsync(() =>
TestRenderer.create(
<Contexts
defaultSelectedElementID={store.getElementIDAtIndex(3)}
defaultSelectedElementIndex={3}>
defaultInspectedElementID={store.getElementIDAtIndex(3)}
defaultInspectedElementIndex={3}>
<ContextReader />
</Contexts>,
),
@@ -374,8 +374,8 @@ describe('ProfilerContext', () => {
await utils.actAsync(() =>
TestRenderer.create(
<Contexts
defaultSelectedElementID={store.getElementIDAtIndex(3)}
defaultSelectedElementIndex={3}>
defaultInspectedElementID={store.getElementIDAtIndex(3)}
defaultInspectedElementIndex={3}>
<ContextReader />
</Contexts>,
),
@@ -415,11 +415,12 @@ describe('ProfilerContext', () => {
let context: Context = ((null: any): Context);
let dispatch: DispatcherContext = ((null: any): DispatcherContext);
let selectedElementID = null;
let inspectedElementID = null;
function ContextReader() {
context = React.useContext(ProfilerContext);
dispatch = React.useContext(TreeDispatcherContext);
selectedElementID = React.useContext(TreeStateContext).selectedElementID;
inspectedElementID =
React.useContext(TreeStateContext).inspectedElementID;
return null;
}
@@ -428,13 +429,15 @@ describe('ProfilerContext', () => {
// Select an element within the second root.
await utils.actAsync(() =>
TestRenderer.create(
<Contexts defaultSelectedElementID={id} defaultSelectedElementIndex={3}>
<Contexts
defaultInspectedElementID={id}
defaultInspectedElementIndex={3}>
<ContextReader />
</Contexts>,
),
);
expect(selectedElementID).toBe(id);
expect(inspectedElementID).toBe(id);
// Profile and record more updates to both roots
await utils.actAsync(() => store.profilerStore.startProfiling());
@@ -448,7 +451,7 @@ describe('ProfilerContext', () => {
utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
// Verify that the initial Profiler root selection is maintained.
expect(selectedElementID).toBe(otherID);
expect(inspectedElementID).toBe(otherID);
expect(context).not.toBeNull();
expect(context.rootID).toBe(store.getRootIDForElement(id));
});
@@ -484,11 +487,12 @@ describe('ProfilerContext', () => {
let context: Context = ((null: any): Context);
let dispatch: DispatcherContext = ((null: any): DispatcherContext);
let selectedElementID = null;
let inspectedElementID = null;
function ContextReader() {
context = React.useContext(ProfilerContext);
dispatch = React.useContext(TreeDispatcherContext);
selectedElementID = React.useContext(TreeStateContext).selectedElementID;
inspectedElementID =
React.useContext(TreeStateContext).inspectedElementID;
return null;
}
@@ -497,13 +501,15 @@ describe('ProfilerContext', () => {
// Select an element within the second root.
await utils.actAsync(() =>
TestRenderer.create(
<Contexts defaultSelectedElementID={id} defaultSelectedElementIndex={3}>
<Contexts
defaultInspectedElementID={id}
defaultInspectedElementIndex={3}>
<ContextReader />
</Contexts>,
),
);
expect(selectedElementID).toBe(id);
expect(inspectedElementID).toBe(id);
// Profile and record more updates to both roots
await utils.actAsync(() => store.profilerStore.startProfiling());
@@ -517,7 +523,7 @@ describe('ProfilerContext', () => {
utils.act(() => dispatch({type: 'SELECT_ELEMENT_AT_INDEX', payload: 0}));
// Verify that the initial Profiler root selection is maintained.
expect(selectedElementID).toBe(otherID);
expect(inspectedElementID).toBe(otherID);
expect(context).not.toBeNull();
expect(context.rootID).toBe(store.getRootIDForElement(id));
});
@@ -553,10 +559,11 @@ describe('ProfilerContext', () => {
`);
let context: Context = ((null: any): Context);
let selectedElementID = null;
let inspectedElementID = null;
function ContextReader() {
context = React.useContext(ProfilerContext);
selectedElementID = React.useContext(TreeStateContext).selectedElementID;
inspectedElementID =
React.useContext(TreeStateContext).inspectedElementID;
return null;
}
@@ -567,14 +574,14 @@ describe('ProfilerContext', () => {
</Contexts>,
),
);
expect(selectedElementID).toBeNull();
expect(inspectedElementID).toBeNull();
// Select an element in the Profiler tab and verify that the selection is synced to the Components tab.
await utils.actAsync(() => context.selectFiber(parentID, 'Parent'));
expect(selectedElementID).toBe(parentID);
expect(inspectedElementID).toBe(parentID);
// Select an unmounted element and verify no Components tab selection doesn't change.
await utils.actAsync(() => context.selectFiber(childID, 'Child'));
expect(selectedElementID).toBe(parentID);
expect(inspectedElementID).toBe(parentID);
});
});
+1 -1
View File
@@ -67,7 +67,7 @@ export function printStore(
if (state === null) {
return '';
}
return state.selectedElementIndex === index ? `→` : ' ';
return state.inspectedElementIndex === index ? `→` : ' ';
}
function printErrorsAndWarnings(element: Element): string {
@@ -33,7 +33,7 @@ type Props = {
export default function Element({data, index, style}: Props): React.Node {
const store = useContext(StoreContext);
const {ownerFlatTree, ownerID, selectedElementID} =
const {ownerFlatTree, ownerID, inspectedElementID} =
useContext(TreeStateContext);
const dispatch = useContext(TreeDispatcherContext);
@@ -46,7 +46,7 @@ export default function Element({data, index, style}: Props): React.Node {
const {isNavigatingWithKeyboard, onElementMouseEnter, treeFocused} = data;
const id = element === null ? null : element.id;
const isSelected = selectedElementID === id;
const isSelected = inspectedElementID === id;
const errorsAndWarningsSubscription = useMemo(
() => ({
@@ -28,7 +28,7 @@ function InspectedElementSourcePanel({
symbolicatedSourcePromise,
}: Props): React.Node {
return (
<div data-testname="InspectedElementView-Source">
<div>
<div className={styles.SourceHeaderRow}>
<div className={styles.SourceHeader}>source</div>
@@ -84,7 +84,9 @@ function FormattedSourceString({source, symbolicatedSourcePromise}: Props) {
const {sourceURL, line} = source;
return (
<div className={styles.SourceOneLiner}>
<div
className={styles.SourceOneLiner}
data-testname="InspectedElementView-FormattedSourceString">
{formatSourceForDisplay(sourceURL, line)}
</div>
);
@@ -93,7 +95,9 @@ function FormattedSourceString({source, symbolicatedSourcePromise}: Props) {
const {sourceURL, line} = symbolicatedSource;
return (
<div className={styles.SourceOneLiner}>
<div
className={styles.SourceOneLiner}
data-testname="InspectedElementView-FormattedSourceString">
{formatSourceForDisplay(sourceURL, line)}
</div>
);
@@ -100,10 +100,10 @@ function NativeStyleContextController({children}: Props): React.Node {
[store],
);
// It's very important that this context consumes selectedElementID and not NativeStyleID.
// It's very important that this context consumes inspectedElementID and not NativeStyleID.
// Otherwise the effect that sends the "inspect" message across the bridge-
// would itself be blocked by the same render that suspends (waiting for the data).
const {selectedElementID} = useContext<StateContext>(TreeStateContext);
const {inspectedElementID} = useContext<StateContext>(TreeStateContext);
const [currentStyleAndLayout, setCurrentStyleAndLayout] =
useState<StyleAndLayoutFrontend | null>(null);
@@ -128,7 +128,7 @@ function NativeStyleContextController({children}: Props): React.Node {
resource.write(element, styleAndLayout);
// Schedule update with React if the currently-selected element has been invalidated.
if (id === selectedElementID) {
if (id === inspectedElementID) {
setCurrentStyleAndLayout(styleAndLayout);
}
}
@@ -141,15 +141,15 @@ function NativeStyleContextController({children}: Props): React.Node {
'NativeStyleEditor_styleAndLayout',
onStyleAndLayout,
);
}, [bridge, currentStyleAndLayout, selectedElementID, store]);
}, [bridge, currentStyleAndLayout, inspectedElementID, store]);
// This effect handler polls for updates on the currently selected element.
useEffect(() => {
if (selectedElementID === null) {
if (inspectedElementID === null) {
return () => {};
}
const rendererID = store.getRendererIDForElement(selectedElementID);
const rendererID = store.getRendererIDForElement(inspectedElementID);
let timeoutID: TimeoutID | null = null;
@@ -158,7 +158,7 @@ function NativeStyleContextController({children}: Props): React.Node {
if (rendererID !== null) {
bridge.send('NativeStyleEditor_measure', {
id: selectedElementID,
id: inspectedElementID,
rendererID,
});
}
@@ -170,7 +170,7 @@ function NativeStyleContextController({children}: Props): React.Node {
const onStyleAndLayout = ({id}: StyleAndLayoutBackend) => {
// If this is the element we requested, wait a little bit and then ask for another update.
if (id === selectedElementID) {
if (id === inspectedElementID) {
if (timeoutID !== null) {
clearTimeout(timeoutID);
}
@@ -190,7 +190,7 @@ function NativeStyleContextController({children}: Props): React.Node {
clearTimeout(timeoutID);
}
};
}, [bridge, selectedElementID, store]);
}, [bridge, inspectedElementID, store]);
const value = useMemo(
() => ({getStyleAndLayout}),
@@ -28,19 +28,19 @@ export default function SelectedTreeHighlight(_: {}): React.Node {
const {lineHeight} = useContext(SettingsContext);
const store = useContext(StoreContext);
const treeFocused = useContext(TreeFocusedContext);
const {ownerID, selectedElementID} = useContext(TreeStateContext);
const {ownerID, inspectedElementID} = useContext(TreeStateContext);
const subscription = useMemo(
() => ({
getCurrentValue: () => {
if (
selectedElementID === null ||
store.isInsideCollapsedSubTree(selectedElementID)
inspectedElementID === null ||
store.isInsideCollapsedSubTree(inspectedElementID)
) {
return null;
}
const element = store.getElementByID(selectedElementID);
const element = store.getElementByID(inspectedElementID);
if (
element === null ||
element.isCollapsed ||
@@ -83,7 +83,7 @@ export default function SelectedTreeHighlight(_: {}): React.Node {
};
},
}),
[selectedElementID, store],
[inspectedElementID, store],
);
const data = useSubscription<Data | null>(subscription);
@@ -54,8 +54,8 @@ export default function Tree(): React.Node {
ownerID,
searchIndex,
searchResults,
selectedElementID,
selectedElementIndex,
inspectedElementID,
inspectedElementIndex,
} = useContext(TreeStateContext);
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
@@ -84,11 +84,11 @@ export default function Tree(): React.Node {
// Using a callback ref accounts for this case...
const listCallbackRef = useCallback(
(list: $FlowFixMe) => {
if (list != null && selectedElementIndex !== null) {
list.scrollToItem(selectedElementIndex, 'smart');
if (list != null && inspectedElementIndex !== null) {
list.scrollToItem(inspectedElementIndex, 'smart');
}
},
[selectedElementIndex],
[inspectedElementIndex],
);
// Picking an element in the inspector should put focus into the tree.
@@ -133,8 +133,8 @@ export default function Tree(): React.Node {
case 'ArrowLeft':
event.preventDefault();
element =
selectedElementID !== null
? store.getElementByID(selectedElementID)
inspectedElementID !== null
? store.getElementByID(inspectedElementID)
: null;
if (element !== null) {
if (event.altKey) {
@@ -153,8 +153,8 @@ export default function Tree(): React.Node {
case 'ArrowRight':
event.preventDefault();
element =
selectedElementID !== null
? store.getElementByID(selectedElementID)
inspectedElementID !== null
? store.getElementByID(inspectedElementID)
: null;
if (element !== null) {
if (event.altKey) {
@@ -202,7 +202,7 @@ export default function Tree(): React.Node {
return () => {
container.removeEventListener('keydown', handleKeyDown);
};
}, [dispatch, selectedElementID, store]);
}, [dispatch, inspectedElementID, store]);
// Focus management.
const handleBlur = useCallback(() => setTreeFocused(false), []);
@@ -213,15 +213,15 @@ export default function Tree(): React.Node {
switch (event.key) {
case 'Enter':
case ' ':
if (selectedElementID !== null) {
dispatch({type: 'SELECT_OWNER', payload: selectedElementID});
if (inspectedElementID !== null) {
dispatch({type: 'SELECT_OWNER', payload: inspectedElementID});
}
break;
default:
break;
}
},
[dispatch, selectedElementID],
[dispatch, inspectedElementID],
);
// If we switch the selected element while using the keyboard,
@@ -238,8 +238,8 @@ export default function Tree(): React.Node {
didSelectNewSearchResult = true;
}
if (isNavigatingWithKeyboard || didSelectNewSearchResult) {
if (selectedElementID !== null) {
highlightHostInstance(selectedElementID);
if (inspectedElementID !== null) {
highlightHostInstance(inspectedElementID);
} else {
clearHighlightHostInstance();
}
@@ -250,7 +250,7 @@ export default function Tree(): React.Node {
highlightHostInstance,
searchIndex,
searchResults,
selectedElementID,
inspectedElementID,
]);
// Highlight last hovered element.
@@ -29,14 +29,12 @@ import type {ReactContext} from 'shared/ReactTypes';
import * as React from 'react';
import {
createContext,
useCallback,
useContext,
useEffect,
useLayoutEffect,
useMemo,
useReducer,
useRef,
startTransition,
} from 'react';
import {createRegExp} from '../utils';
import {StoreContext} from '../context';
@@ -48,8 +46,6 @@ export type StateContext = {
// Tree
numElements: number,
ownerSubtreeLeafElementID: number | null,
selectedElementID: number | null,
selectedElementIndex: number | null,
// Search
searchIndex: number | null,
@@ -62,6 +58,7 @@ export type StateContext = {
// Inspection element panel
inspectedElementID: number | null,
inspectedElementIndex: number | null,
};
type ACTION_GO_TO_NEXT_SEARCH_RESULT = {
@@ -123,9 +120,6 @@ type ACTION_SET_SEARCH_TEXT = {
type: 'SET_SEARCH_TEXT',
payload: string,
};
type ACTION_UPDATE_INSPECTED_ELEMENT_ID = {
type: 'UPDATE_INSPECTED_ELEMENT_ID',
};
type Action =
| ACTION_GO_TO_NEXT_SEARCH_RESULT
@@ -145,8 +139,7 @@ type Action =
| ACTION_SELECT_PREVIOUS_SIBLING_IN_TREE
| ACTION_SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE
| ACTION_SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE
| ACTION_SET_SEARCH_TEXT
| ACTION_UPDATE_INSPECTED_ELEMENT_ID;
| ACTION_SET_SEARCH_TEXT;
export type DispatcherContext = (action: Action) => void;
@@ -162,8 +155,6 @@ type State = {
// Tree
numElements: number,
ownerSubtreeLeafElementID: number | null,
selectedElementID: number | null,
selectedElementIndex: number | null,
// Search
searchIndex: number | null,
@@ -176,14 +167,15 @@ type State = {
// Inspection element panel
inspectedElementID: number | null,
inspectedElementIndex: number | null,
};
function reduceTreeState(store: Store, state: State, action: Action): State {
let {
numElements,
ownerSubtreeLeafElementID,
selectedElementIndex,
selectedElementID,
inspectedElementID,
inspectedElementIndex,
} = state;
const ownerID = state.ownerID;
@@ -201,34 +193,33 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
// We deduce the parent-child mapping from removedIDs (id -> parentID)
// because by now it's too late to read them from the store.
while (
selectedElementID !== null &&
removedIDs.has(selectedElementID)
inspectedElementID !== null &&
removedIDs.has(inspectedElementID)
) {
selectedElementID = ((removedIDs.get(
selectedElementID,
): any): number);
// $FlowExpectedError[incompatible-type]
inspectedElementID = removedIDs.get(inspectedElementID);
}
if (selectedElementID === 0) {
if (inspectedElementID === 0) {
// The whole root was removed.
selectedElementIndex = null;
inspectedElementIndex = null;
}
break;
case 'SELECT_CHILD_ELEMENT_IN_TREE':
ownerSubtreeLeafElementID = null;
if (selectedElementIndex !== null) {
const selectedElement = store.getElementAtIndex(
((selectedElementIndex: any): number),
if (inspectedElementIndex !== null) {
const inspectedElement = store.getElementAtIndex(
inspectedElementIndex,
);
if (
selectedElement !== null &&
selectedElement.children.length > 0 &&
!selectedElement.isCollapsed
inspectedElement !== null &&
inspectedElement.children.length > 0 &&
!inspectedElement.isCollapsed
) {
const firstChildID = selectedElement.children[0];
const firstChildID = inspectedElement.children[0];
const firstChildIndex = store.getIndexOfElementID(firstChildID);
if (firstChildIndex !== null) {
selectedElementIndex = firstChildIndex;
inspectedElementIndex = firstChildIndex;
}
}
}
@@ -236,7 +227,8 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
case 'SELECT_ELEMENT_AT_INDEX':
ownerSubtreeLeafElementID = null;
selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
inspectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX)
.payload;
break;
case 'SELECT_ELEMENT_BY_ID':
ownerSubtreeLeafElementID = null;
@@ -245,30 +237,30 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
// It might also cause problems if the specified element was inside of a (not yet expanded) subtree.
lookupIDForIndex = false;
selectedElementID = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
selectedElementIndex =
selectedElementID === null
inspectedElementID = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
inspectedElementIndex =
inspectedElementID === null
? null
: store.getIndexOfElementID(selectedElementID);
: store.getIndexOfElementID(inspectedElementID);
break;
case 'SELECT_NEXT_ELEMENT_IN_TREE':
ownerSubtreeLeafElementID = null;
if (
selectedElementIndex === null ||
selectedElementIndex + 1 >= numElements
inspectedElementIndex === null ||
inspectedElementIndex + 1 >= numElements
) {
selectedElementIndex = 0;
inspectedElementIndex = 0;
} else {
selectedElementIndex++;
inspectedElementIndex++;
}
break;
case 'SELECT_NEXT_SIBLING_IN_TREE':
ownerSubtreeLeafElementID = null;
if (selectedElementIndex !== null) {
if (inspectedElementIndex !== null) {
const selectedElement = store.getElementAtIndex(
((selectedElementIndex: any): number),
((inspectedElementIndex: any): number),
);
if (selectedElement !== null && selectedElement.parentID !== 0) {
const parent = store.getElementByID(selectedElement.parentID);
@@ -279,23 +271,23 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
selectedChildIndex < children.length - 1
? children[selectedChildIndex + 1]
: children[0];
selectedElementIndex = store.getIndexOfElementID(nextChildID);
inspectedElementIndex = store.getIndexOfElementID(nextChildID);
}
}
}
break;
case 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE':
if (selectedElementIndex !== null) {
if (inspectedElementIndex !== null) {
if (
ownerSubtreeLeafElementID !== null &&
ownerSubtreeLeafElementID !== selectedElementID
ownerSubtreeLeafElementID !== inspectedElementID
) {
const leafElement = store.getElementByID(ownerSubtreeLeafElementID);
if (leafElement !== null) {
let currentElement: null | Element = leafElement;
while (currentElement !== null) {
if (currentElement.ownerID === selectedElementID) {
selectedElementIndex = store.getIndexOfElementID(
if (currentElement.ownerID === inspectedElementID) {
inspectedElementIndex = store.getIndexOfElementID(
currentElement.id,
);
break;
@@ -308,23 +300,23 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
}
break;
case 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE':
if (selectedElementIndex !== null) {
if (inspectedElementIndex !== null) {
if (ownerSubtreeLeafElementID === null) {
// If this is the first time we're stepping through the owners tree,
// pin the current component as the owners list leaf.
// This will enable us to step back down to this component.
ownerSubtreeLeafElementID = selectedElementID;
ownerSubtreeLeafElementID = inspectedElementID;
}
const selectedElement = store.getElementAtIndex(
((selectedElementIndex: any): number),
((inspectedElementIndex: any): number),
);
if (selectedElement !== null && selectedElement.ownerID !== 0) {
const ownerIndex = store.getIndexOfElementID(
selectedElement.ownerID,
);
if (ownerIndex !== null) {
selectedElementIndex = ownerIndex;
inspectedElementIndex = ownerIndex;
}
}
}
@@ -332,16 +324,16 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
case 'SELECT_PARENT_ELEMENT_IN_TREE':
ownerSubtreeLeafElementID = null;
if (selectedElementIndex !== null) {
if (inspectedElementIndex !== null) {
const selectedElement = store.getElementAtIndex(
((selectedElementIndex: any): number),
((inspectedElementIndex: any): number),
);
if (selectedElement !== null && selectedElement.parentID !== 0) {
const parentIndex = store.getIndexOfElementID(
selectedElement.parentID,
);
if (parentIndex !== null) {
selectedElementIndex = parentIndex;
inspectedElementIndex = parentIndex;
}
}
}
@@ -349,18 +341,18 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
ownerSubtreeLeafElementID = null;
if (selectedElementIndex === null || selectedElementIndex === 0) {
selectedElementIndex = numElements - 1;
if (inspectedElementIndex === null || inspectedElementIndex === 0) {
inspectedElementIndex = numElements - 1;
} else {
selectedElementIndex--;
inspectedElementIndex--;
}
break;
case 'SELECT_PREVIOUS_SIBLING_IN_TREE':
ownerSubtreeLeafElementID = null;
if (selectedElementIndex !== null) {
if (inspectedElementIndex !== null) {
const selectedElement = store.getElementAtIndex(
((selectedElementIndex: any): number),
((inspectedElementIndex: any): number),
);
if (selectedElement !== null && selectedElement.parentID !== 0) {
const parent = store.getElementByID(selectedElement.parentID);
@@ -371,7 +363,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
selectedChildIndex > 0
? children[selectedChildIndex - 1]
: children[children.length - 1];
selectedElementIndex = store.getIndexOfElementID(nextChildID);
inspectedElementIndex = store.getIndexOfElementID(nextChildID);
}
}
}
@@ -384,7 +376,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
}
let flatIndex = 0;
if (selectedElementIndex !== null) {
if (inspectedElementIndex !== null) {
// Resume from the current position in the list.
// Otherwise step to the previous item, relative to the current selection.
for (
@@ -393,7 +385,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
i--
) {
const {index} = elementIndicesWithErrorsOrWarnings[i];
if (index >= selectedElementIndex) {
if (index >= inspectedElementIndex) {
flatIndex = i;
} else {
break;
@@ -407,12 +399,12 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
elementIndicesWithErrorsOrWarnings[
elementIndicesWithErrorsOrWarnings.length - 1
];
selectedElementID = prevEntry.id;
selectedElementIndex = prevEntry.index;
inspectedElementID = prevEntry.id;
inspectedElementIndex = prevEntry.index;
} else {
prevEntry = elementIndicesWithErrorsOrWarnings[flatIndex - 1];
selectedElementID = prevEntry.id;
selectedElementIndex = prevEntry.index;
inspectedElementID = prevEntry.id;
inspectedElementIndex = prevEntry.index;
}
lookupIDForIndex = false;
@@ -426,12 +418,12 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
}
let flatIndex = -1;
if (selectedElementIndex !== null) {
if (inspectedElementIndex !== null) {
// Resume from the current position in the list.
// Otherwise step to the next item, relative to the current selection.
for (let i = 0; i < elementIndicesWithErrorsOrWarnings.length; i++) {
const {index} = elementIndicesWithErrorsOrWarnings[i];
if (index <= selectedElementIndex) {
if (index <= inspectedElementIndex) {
flatIndex = i;
} else {
break;
@@ -442,12 +434,12 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
let nextEntry;
if (flatIndex >= elementIndicesWithErrorsOrWarnings.length - 1) {
nextEntry = elementIndicesWithErrorsOrWarnings[0];
selectedElementID = nextEntry.id;
selectedElementIndex = nextEntry.index;
inspectedElementID = nextEntry.id;
inspectedElementIndex = nextEntry.index;
} else {
nextEntry = elementIndicesWithErrorsOrWarnings[flatIndex + 1];
selectedElementID = nextEntry.id;
selectedElementIndex = nextEntry.index;
inspectedElementID = nextEntry.id;
inspectedElementIndex = nextEntry.index;
}
lookupIDForIndex = false;
@@ -460,12 +452,15 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
}
// Keep selected item ID and index in sync.
if (lookupIDForIndex && selectedElementIndex !== state.selectedElementIndex) {
if (selectedElementIndex === null) {
selectedElementID = null;
if (
lookupIDForIndex &&
inspectedElementIndex !== state.inspectedElementIndex
) {
if (inspectedElementIndex === null) {
inspectedElementID = null;
} else {
selectedElementID = store.getElementIDAtIndex(
((selectedElementIndex: any): number),
inspectedElementID = store.getElementIDAtIndex(
((inspectedElementIndex: any): number),
);
}
}
@@ -475,8 +470,8 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
numElements,
ownerSubtreeLeafElementID,
selectedElementIndex,
selectedElementID,
inspectedElementIndex,
inspectedElementID,
};
}
@@ -485,8 +480,8 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
searchIndex,
searchResults,
searchText,
selectedElementID,
selectedElementIndex,
inspectedElementID,
inspectedElementIndex,
} = state;
const ownerID = state.ownerID;
@@ -594,11 +589,11 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
});
if (searchResults.length > 0) {
if (prevSearchIndex === null) {
if (selectedElementIndex !== null) {
if (inspectedElementIndex !== null) {
searchIndex = getNearestResultIndex(
store,
searchResults,
selectedElementIndex,
inspectedElementIndex,
);
} else {
searchIndex = 0;
@@ -619,7 +614,7 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
}
if (searchText !== prevSearchText) {
const newSearchIndex = searchResults.indexOf(selectedElementID);
const newSearchIndex = searchResults.indexOf(inspectedElementID);
if (newSearchIndex === -1) {
// Only move the selection if the new query
// doesn't match the current selection anymore.
@@ -631,17 +626,17 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
}
}
if (didRequestSearch && searchIndex !== null) {
selectedElementID = ((searchResults[searchIndex]: any): number);
selectedElementIndex = store.getIndexOfElementID(
((selectedElementID: any): number),
inspectedElementID = ((searchResults[searchIndex]: any): number);
inspectedElementIndex = store.getIndexOfElementID(
((inspectedElementID: any): number),
);
}
return {
...state,
selectedElementID,
selectedElementIndex,
inspectedElementID,
inspectedElementIndex,
searchIndex,
searchResults,
@@ -652,14 +647,14 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
function reduceOwnersState(store: Store, state: State, action: Action): State {
let {
numElements,
selectedElementID,
selectedElementIndex,
ownerID,
ownerFlatTree,
inspectedElementID,
inspectedElementIndex,
} = state;
const {searchIndex, searchResults, searchText} = state;
let prevSelectedElementIndex = selectedElementIndex;
let prevInspectedElementIndex = inspectedElementIndex;
switch (action.type) {
case 'HANDLE_STORE_MUTATION':
@@ -667,75 +662,76 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
if (!store.containsElement(ownerID)) {
ownerID = null;
ownerFlatTree = null;
selectedElementID = null;
prevInspectedElementIndex = null;
} else {
ownerFlatTree = store.getOwnersListForElement(ownerID);
if (selectedElementID !== null) {
if (inspectedElementID !== null) {
// Mutation might have caused the index of this ID to shift.
selectedElementIndex = ownerFlatTree.findIndex(
element => element.id === selectedElementID,
prevInspectedElementIndex = ownerFlatTree.findIndex(
element => element.id === inspectedElementID,
);
}
}
} else {
if (selectedElementID !== null) {
if (inspectedElementID !== null) {
// Mutation might have caused the index of this ID to shift.
selectedElementIndex = store.getIndexOfElementID(selectedElementID);
inspectedElementIndex = store.getIndexOfElementID(inspectedElementID);
}
}
if (selectedElementIndex === -1) {
if (inspectedElementIndex === -1) {
// If we couldn't find this ID after mutation, unselect it.
selectedElementIndex = null;
selectedElementID = null;
inspectedElementIndex = null;
inspectedElementID = null;
}
break;
case 'RESET_OWNER_STACK':
ownerID = null;
ownerFlatTree = null;
selectedElementIndex =
selectedElementID !== null
? store.getIndexOfElementID(selectedElementID)
inspectedElementIndex =
inspectedElementID !== null
? store.getIndexOfElementID(inspectedElementID)
: null;
break;
case 'SELECT_ELEMENT_AT_INDEX':
if (ownerFlatTree !== null) {
selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
inspectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX)
.payload;
}
break;
case 'SELECT_ELEMENT_BY_ID':
if (ownerFlatTree !== null) {
const payload = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
if (payload === null) {
selectedElementIndex = null;
inspectedElementIndex = null;
} else {
selectedElementIndex = ownerFlatTree.findIndex(
inspectedElementIndex = ownerFlatTree.findIndex(
element => element.id === payload,
);
// If the selected element is outside of the current owners list,
// exit the list and select the element in the main tree.
// This supports features like toggling Suspense.
if (selectedElementIndex !== null && selectedElementIndex < 0) {
if (inspectedElementIndex !== null && inspectedElementIndex < 0) {
ownerID = null;
ownerFlatTree = null;
selectedElementIndex = store.getIndexOfElementID(payload);
inspectedElementIndex = store.getIndexOfElementID(payload);
}
}
}
break;
case 'SELECT_NEXT_ELEMENT_IN_TREE':
if (ownerFlatTree !== null && ownerFlatTree.length > 0) {
if (selectedElementIndex === null) {
selectedElementIndex = 0;
} else if (selectedElementIndex + 1 < ownerFlatTree.length) {
selectedElementIndex++;
if (inspectedElementIndex === null) {
inspectedElementIndex = 0;
} else if (inspectedElementIndex + 1 < ownerFlatTree.length) {
inspectedElementIndex++;
}
}
break;
case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
if (ownerFlatTree !== null && ownerFlatTree.length > 0) {
if (selectedElementIndex !== null && selectedElementIndex > 0) {
selectedElementIndex--;
if (inspectedElementIndex !== null && inspectedElementIndex > 0) {
inspectedElementIndex--;
}
}
break;
@@ -747,8 +743,8 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
ownerFlatTree = store.getOwnersListForElement(ownerID);
// Always force reset selection to be the top of the new owner tree.
selectedElementIndex = 0;
prevSelectedElementIndex = null;
inspectedElementIndex = 0;
prevInspectedElementIndex = null;
}
break;
default:
@@ -769,12 +765,12 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
}
// Keep selected item ID and index in sync.
if (selectedElementIndex !== prevSelectedElementIndex) {
if (selectedElementIndex === null) {
selectedElementID = null;
if (inspectedElementIndex !== prevInspectedElementIndex) {
if (inspectedElementIndex === null) {
inspectedElementID = null;
} else {
if (ownerFlatTree !== null) {
selectedElementID = ownerFlatTree[selectedElementIndex].id;
inspectedElementID = ownerFlatTree[inspectedElementIndex].id;
}
}
}
@@ -783,8 +779,6 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
...state,
numElements,
selectedElementID,
selectedElementIndex,
searchIndex,
searchResults,
@@ -792,49 +786,27 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
ownerID,
ownerFlatTree,
inspectedElementID,
inspectedElementIndex,
};
}
function reduceSuspenseState(
store: Store,
state: State,
action: Action,
): State {
const {type} = action;
switch (type) {
case 'UPDATE_INSPECTED_ELEMENT_ID':
if (state.inspectedElementID !== state.selectedElementID) {
return {
...state,
inspectedElementID: state.selectedElementID,
};
}
break;
default:
break;
}
// React can bailout of no-op updates.
return state;
}
type Props = {
children: React$Node,
// Used for automated testing
defaultInspectedElementID?: ?number,
defaultOwnerID?: ?number,
defaultSelectedElementID?: ?number,
defaultSelectedElementIndex?: ?number,
defaultInspectedElementID?: ?number,
defaultInspectedElementIndex?: ?number,
};
// TODO Remove TreeContextController wrapper element once global Context.write API exists.
function TreeContextController({
children,
defaultInspectedElementID,
defaultOwnerID,
defaultSelectedElementID,
defaultSelectedElementIndex,
defaultInspectedElementID,
defaultInspectedElementIndex,
}: Props): React.Node {
const store = useContext(StoreContext);
@@ -865,23 +837,22 @@ function TreeContextController({
case 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE':
case 'SELECT_PREVIOUS_SIBLING_IN_TREE':
case 'SELECT_OWNER':
case 'UPDATE_INSPECTED_ELEMENT_ID':
case 'SET_SEARCH_TEXT':
state = reduceTreeState(store, state, action);
state = reduceSearchState(store, state, action);
state = reduceOwnersState(store, state, action);
state = reduceSuspenseState(store, state, action);
// TODO(hoxyq): review
// If the selected ID is in a collapsed subtree, reset the selected index to null.
// We'll know the correct index after the layout effect will toggle the tree,
// and the store tree is mutated to account for that.
if (
state.selectedElementID !== null &&
store.isInsideCollapsedSubTree(state.selectedElementID)
state.inspectedElementID !== null &&
store.isInsideCollapsedSubTree(state.inspectedElementID)
) {
return {
...state,
selectedElementIndex: null,
inspectedElementIndex: null,
};
}
@@ -897,16 +868,6 @@ function TreeContextController({
// Tree
numElements: store.numElements,
ownerSubtreeLeafElementID: null,
selectedElementID:
defaultSelectedElementID != null
? defaultSelectedElementID
: store.lastSelectedHostInstanceElementId,
selectedElementIndex:
defaultSelectedElementIndex != null
? defaultSelectedElementIndex
: store.lastSelectedHostInstanceElementId
? store.getIndexOfElementID(store.lastSelectedHostInstanceElementId)
: null,
// Search
searchIndex: null,
@@ -922,42 +883,38 @@ function TreeContextController({
defaultInspectedElementID != null
? defaultInspectedElementID
: store.lastSelectedHostInstanceElementId,
inspectedElementIndex:
defaultInspectedElementIndex != null
? defaultInspectedElementIndex
: store.lastSelectedHostInstanceElementId
? store.getIndexOfElementID(store.lastSelectedHostInstanceElementId)
: null,
});
const dispatchWrapper = useCallback(
(action: Action) => {
dispatch(action);
startTransition(() => {
dispatch({type: 'UPDATE_INSPECTED_ELEMENT_ID'});
});
},
[dispatch],
);
// Listen for host element selections.
useEffect(() => {
const handler = (id: Element['id']) =>
dispatchWrapper({type: 'SELECT_ELEMENT_BY_ID', payload: id});
dispatch({type: 'SELECT_ELEMENT_BY_ID', payload: id});
store.addListener('hostInstanceSelected', handler);
return () => store.removeListener('hostInstanceSelected', handler);
}, [store, dispatchWrapper]);
}, [store, dispatch]);
// If a newly-selected search result or inspection selection is inside of a collapsed subtree, auto expand it.
// This needs to be a layout effect to avoid temporarily flashing an incorrect selection.
const prevSelectedElementID = useRef<number | null>(null);
const prevInspectedElementID = useRef<number | null>(null);
useLayoutEffect(() => {
if (state.selectedElementID !== prevSelectedElementID.current) {
prevSelectedElementID.current = state.selectedElementID;
if (state.inspectedElementID !== prevInspectedElementID.current) {
prevInspectedElementID.current = state.inspectedElementID;
if (state.selectedElementID !== null) {
const element = store.getElementByID(state.selectedElementID);
if (state.inspectedElementID !== null) {
const element = store.getElementByID(state.inspectedElementID);
if (element !== null && element.parentID > 0) {
store.toggleIsCollapsed(element.parentID, false);
}
}
}
}, [state.selectedElementID, store]);
}, [state.inspectedElementID, store]);
// Mutations to the underlying tree may impact this context (e.g. search results, selection state).
useEffect(() => {
@@ -965,7 +922,7 @@ function TreeContextController({
Array<number>,
Map<number, number>,
]) => {
dispatchWrapper({
dispatch({
type: 'HANDLE_STORE_MUTATION',
payload: [addedElementIDs, removedElementIDs],
});
@@ -976,20 +933,19 @@ function TreeContextController({
// At the moment, we can treat this as a mutation.
// We don't know which Elements were newly added/removed, but that should be okay in this case.
// It would only impact the search state, which is unlikely to exist yet at this point.
dispatchWrapper({
dispatch({
type: 'HANDLE_STORE_MUTATION',
payload: [[], new Map()],
});
}
store.addListener('mutated', handleStoreMutated);
return () => store.removeListener('mutated', handleStoreMutated);
}, [dispatchWrapper, initialRevision, store]);
}, [dispatch, initialRevision, store]);
return (
<TreeStateContext.Provider value={state}>
<TreeDispatcherContext.Provider value={dispatchWrapper}>
<TreeDispatcherContext.Provider value={dispatch}>
{children}
</TreeDispatcherContext.Provider>
</TreeStateContext.Provider>
@@ -1028,11 +984,11 @@ function recursivelySearchTree(
function getNearestResultIndex(
store: Store,
searchResults: Array<number>,
selectedElementIndex: number,
inspectedElementIndex: number,
): number {
const index = searchResults.findIndex(id => {
const innerIndex = store.getIndexOfElementID(id);
return innerIndex !== null && innerIndex >= selectedElementIndex;
return innerIndex !== null && innerIndex >= inspectedElementIndex;
});
return index === -1 ? 0 : index;
@@ -88,7 +88,7 @@ type Props = {
function ProfilerContextController({children}: Props): React.Node {
const store = useContext(StoreContext);
const {selectedElementID} = useContext(TreeStateContext);
const {inspectedElementID} = useContext(TreeStateContext);
const dispatch = useContext(TreeDispatcherContext);
const {profilerStore} = store;
@@ -176,9 +176,9 @@ function ProfilerContextController({children}: Props): React.Node {
if (rootID === null || !dataForRoots.has(rootID)) {
let selectedElementRootID = null;
if (selectedElementID !== null) {
if (inspectedElementID !== null) {
selectedElementRootID =
store.getRootIDForElement(selectedElementID);
store.getRootIDForElement(inspectedElementID);
}
if (
selectedElementRootID !== null &&