@samitouri / QOS-React / commits / 01c4d03d84

[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);