Skip to content

Commit 53c9f81

Browse files
authored
[DevTools] Use Popover API for TraceUpdates highlighting (facebook#32614)
## Summary When using React DevTools to highlight component updates, the highlights would sometimes appear behind elements that use the browser's [top-layer](https://developer.mozilla.org/en-US/docs/Glossary/Top_layer) (such as `<dialog>` elements or components using the Popover API). This made it difficult to see which components were updating when they were inside or behind top-layer elements. This PR fixes the issue by using the Popover API to ensure that highlighting appears on top of all content, including elements in the top-layer. The implementation maintains backward compatibility with browsers that don't support the Popover API. ## How did you test this change? I tested this change in the following ways: 1. Manually tested in Chrome (which supports the Popover API) with: - Created a test application with React components inside `<dialog>` elements and custom elements using the Popover API - Verified that component highlighting appears above these elements when they update - Confirmed that highlighting displays correctly for nested components within top-layer elements 2. Verified backward compatibility: - Tested in browsers without Popover API support to ensure fallback behavior works correctly - Confirmed that no errors occur and highlighting still functions as before 3. Ran the React DevTools test suite: - All tests pass successfully - No regressions were introduced [demo-page](https://devtools-toplayer-demo.vercel.app/) [demo-repo](https://github.com/yongsk0066/devtools-toplayer-demo) ### AS-IS https://github.com/user-attachments/assets/dc2e1281-969f-4f61-82c3-480153916969 ### TO-BE https://github.com/user-attachments/assets/dd52ce35-816c-42f0-819b-0d5d0a8a21e5
1 parent e5a8de8 commit 53c9f81

File tree

5 files changed

+124
-3
lines changed

5 files changed

+124
-3
lines changed

packages/react-devtools-extensions/chrome/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "Adds React debugging tools to the Chrome Developer Tools.",
55
"version": "6.1.1",
66
"version_name": "6.1.1",
7-
"minimum_chrome_version": "102",
7+
"minimum_chrome_version": "114",
88
"icons": {
99
"16": "icons/16-production.png",
1010
"32": "icons/32-production.png",

packages/react-devtools-extensions/edge/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
55
"version": "6.1.1",
66
"version_name": "6.1.1",
7-
"minimum_chrome_version": "102",
7+
"minimum_chrome_version": "114",
88
"icons": {
99
"16": "icons/16-production.png",
1010
"32": "icons/32-production.png",

packages/react-devtools-shared/src/backend/views/TraceUpdates/canvas.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,24 @@ function drawWeb(nodeToData: Map<HostInstance, Data>) {
6565
drawGroupBorders(context, group);
6666
drawGroupLabel(context, group);
6767
});
68+
69+
if (canvas !== null) {
70+
if (nodeToData.size === 0 && canvas.matches(':popover-open')) {
71+
// $FlowFixMe[prop-missing]: Flow doesn't recognize Popover API
72+
// $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
73+
canvas.hidePopover();
74+
return;
75+
}
76+
// $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
77+
if (canvas.matches(':popover-open')) {
78+
// $FlowFixMe[prop-missing]: Flow doesn't recognize Popover API
79+
// $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
80+
canvas.hidePopover();
81+
}
82+
// $FlowFixMe[prop-missing]: Flow doesn't recognize Popover API
83+
// $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
84+
canvas.showPopover();
85+
}
6886
}
6987

7088
type GroupItem = {
@@ -191,7 +209,15 @@ function destroyNative(agent: Agent) {
191209

192210
function destroyWeb() {
193211
if (canvas !== null) {
212+
if (canvas.matches(':popover-open')) {
213+
// $FlowFixMe[prop-missing]: Flow doesn't recognize Popover API
214+
// $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
215+
canvas.hidePopover();
216+
}
217+
218+
// $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API and loses canvas nullability tracking
194219
if (canvas.parentNode != null) {
220+
// $FlowFixMe[incompatible-call]: Flow doesn't track that canvas is non-null here
195221
canvas.parentNode.removeChild(canvas);
196222
}
197223
canvas = null;
@@ -204,6 +230,9 @@ export function destroy(agent: Agent): void {
204230

205231
function initialize(): void {
206232
canvas = window.document.createElement('canvas');
233+
canvas.setAttribute('popover', 'manual');
234+
235+
// $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
207236
canvas.style.cssText = `
208237
xx-background-color: red;
209238
xx-opacity: 0.5;
@@ -213,7 +242,10 @@ function initialize(): void {
213242
position: fixed;
214243
right: 0;
215244
top: 0;
216-
z-index: 1000000000;
245+
background-color: transparent;
246+
outline: none;
247+
box-shadow: none;
248+
border: none;
217249
`;
218250

219251
const root = window.document.documentElement;
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import * as React from 'react';
11+
import {useRef, useState} from 'react';
12+
13+
const Counter = () => {
14+
const [count, setCount] = useState(0);
15+
16+
return (
17+
<div>
18+
<h3>Count: {count}</h3>
19+
<button onClick={() => setCount(c => c + 1)}>Increment</button>
20+
</div>
21+
);
22+
};
23+
24+
function DialogComponent() {
25+
const dialogRef = useRef(null);
26+
27+
const openDialog = () => {
28+
if (dialogRef.current) {
29+
dialogRef.current.showModal();
30+
}
31+
};
32+
33+
const closeDialog = () => {
34+
if (dialogRef.current) {
35+
dialogRef.current.close();
36+
}
37+
};
38+
39+
return (
40+
<div style={{margin: '10px 0'}}>
41+
<button onClick={openDialog}>Open Dialog</button>
42+
<dialog ref={dialogRef} style={{padding: '20px'}}>
43+
<h3>Dialog Content</h3>
44+
<Counter />
45+
<button onClick={closeDialog}>Close</button>
46+
</dialog>
47+
</div>
48+
);
49+
}
50+
51+
function RegularComponent() {
52+
return (
53+
<div style={{margin: '10px 0'}}>
54+
<h3>Regular Component</h3>
55+
<Counter />
56+
</div>
57+
);
58+
}
59+
60+
export default function TraceUpdatesTest(): React.Node {
61+
return (
62+
<div>
63+
<h2>TraceUpdates Test</h2>
64+
65+
<div style={{marginBottom: '20px'}}>
66+
<h3>Standard Component</h3>
67+
<RegularComponent />
68+
</div>
69+
70+
<div style={{marginBottom: '20px'}}>
71+
<h3>Dialog Component (top-layer element)</h3>
72+
<DialogComponent />
73+
</div>
74+
75+
<div
76+
style={{marginTop: '20px', padding: '10px', border: '1px solid #ddd'}}>
77+
<h3>How to Test:</h3>
78+
<ol>
79+
<li>Open DevTools Components panel</li>
80+
<li>Enable "Highlight updates when components render" in settings</li>
81+
<li>Click increment buttons and observe highlights</li>
82+
<li>Open the dialog and test increments there as well</li>
83+
</ol>
84+
</div>
85+
</div>
86+
);
87+
}

packages/react-devtools-shell/src/app/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import Toggle from './Toggle';
1919
import ErrorBoundaries from './ErrorBoundaries';
2020
import PartiallyStrictApp from './PartiallyStrictApp';
2121
import SuspenseTree from './SuspenseTree';
22+
import TraceUpdatesTest from './TraceUpdatesTest';
2223
import {ignoreErrors, ignoreLogs, ignoreWarnings} from './console';
2324

2425
import './styles.css';
@@ -112,6 +113,7 @@ function mountTestApp() {
112113
mountApp(SuspenseTree);
113114
mountApp(DeeplyNestedComponents);
114115
mountApp(Iframe);
116+
mountApp(TraceUpdatesTest);
115117

116118
if (shouldRenderLegacy) {
117119
mountLegacyApp(PartiallyStrictApp);

0 commit comments

Comments
 (0)