[DevTools] Clear element inspection if host element not owned by any renderer is selected (#35504)
Sebastian "Sebbie" Silbermann committed
Jan 16, 2026 at 13:20 UTC
01c4d03d841b2695bf889ddeddc722b65f82031a
5 files changed
+28
-14
packages/react-devtools-extensions/src/main/index.js
+1
@@ -330,6 +330,7 @@ function createElementsInspectPanel() {
330
inspectedElementPortalContainer = portal.container;
331
if (inspectedElementPortalContainer != null && render) {
332
ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
333
+ bridge.send('syncSelectionFromBuiltinElementsPanel');
334
335
render();
336
portal.injectStyles(cloneStyleTags);
packages/react-devtools-shared/src/backend/agent.js
+14
-9
@@ -938,11 +938,19 @@ export default class Agent extends EventEmitter<{
938
}
939
};
940
941
- selectNode(target: HostInstance): void {
942
- const match = this.getIDForHostInstance(target);
943
- if (match !== null) {
944
- this._bridge.send('selectElement', match.id);
945
- }
941
+ selectNode(target: HostInstance | null): void {
942
+ const match = target !== null ? this.getIDForHostInstance(target) : null;
943
+ this._bridge.send(
944
+ 'selectElement',
945
+ match !== null
946
+ ? match.id
947
+ : // If you click outside a React root in the Elements panel, we want to give
948
+ // feedback that no selection is possible so we clear the selection.
949
+ // Otherwise clicking outside a React root is indistinguishable from clicking
950
+ // a different host node that leads to the same selected React element
951
+ // due to Component filters
952
+ null,
953
+ );
954
}
955
956
registerRendererInterface(
@@ -988,10 +996,7 @@ export default class Agent extends EventEmitter<{
996
997
syncSelectionFromBuiltinElementsPanel: () => void = () => {
998
const target = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0;
991
- if (target == null) {
992
- return;
993
- }
994
- this.selectNode(target);
999
+ this.selectNode(target == null ? null : target);
1000
};
1001
1002
shutdown: () => void = () => {
packages/react-devtools-shared/src/bridge.js
+1
-1
@@ -214,7 +214,7 @@ export type BackendEvents = {
214
profilingStatus: [boolean],
215
reloadAppForProfiling: [],
216
saveToClipboard: [string],
217
- selectElement: [number],
217
+ selectElement: [number | null],
218
shutdown: [],
219
stopInspectingHost: [boolean],
220
scrollTo: [{left: number, top: number, right: number, bottom: number}],
packages/react-devtools-shared/src/devtools/store.js
+10
-3
@@ -147,7 +147,7 @@ export default class Store extends EventEmitter<{
147
enableSuspenseTab: [],
148
error: [Error],
149
hookSettings: [$ReadOnly<DevToolsHookSettings>],
150
- hostInstanceSelected: [Element['id']],
150
+ hostInstanceSelected: [Element['id'] | null],
151
settingsUpdated: [$ReadOnly<DevToolsHookSettings>],
152
mutated: [
153
[
@@ -2381,8 +2381,15 @@ export default class Store extends EventEmitter<{
2381
this._bridge.send('getHookSettings'); // Warm up cached hook settings
2382
};
2383
2384
- onHostInstanceSelected: (elementId: number) => void = elementId => {
2385
- if (this._lastSelectedHostInstanceElementId === elementId) {
2384
+ onHostInstanceSelected: (elementId: number | null) => void = elementId => {
2385
+ if (
2386
+ this._lastSelectedHostInstanceElementId === elementId &&
2387
+ // Force clear selection e.g. when we inspect an element in the Components panel
2388
+ // and then switch to the browser's Elements panel.
2389
+ // We wouldn't want to stay on the inspected element if we're inspecting
2390
+ // an element not owned by React when switching to the browser's Elements panel.
2391
+ elementId !== null
2392
+ ) {
2393
return;
2394
}
2395
packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js
+2
-1
@@ -967,8 +967,9 @@ function TreeContextController({
967
968
// Listen for host element selections.
969
useEffect(() => {
970
- const handler = (id: Element['id']) =>
970
+ const handler = (id: Element['id'] | null) => {
971
transitionDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: id});
972
+ };
973
974
store.addListener('hostInstanceSelected', handler);
975
return () => store.removeListener('hostInstanceSelected', handler);