@samitouri / QOS-React / commits / 53c9f81049

[DevTools] Use Popover API for TraceUpdates highlighting (#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

YongSeok Jang (장용석) committed May 7, 2025 at 23:48 UTC 53c9f81049b4440a02b5ed3edb128516821c0279
5 files changed +124 -3
packages/react-devtools-extensions/chrome/manifest.json
+1 -1
@@ -4,7 +4,7 @@
4 "description": "Adds React debugging tools to the Chrome Developer Tools.",
5 "version": "6.1.1",
6 "version_name": "6.1.1",
7 - "minimum_chrome_version": "102",
7 + "minimum_chrome_version": "114",
8 "icons": {
9 "16": "icons/16-production.png",
10 "32": "icons/32-production.png",
packages/react-devtools-extensions/edge/manifest.json
+1 -1
@@ -4,7 +4,7 @@
4 "description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
5 "version": "6.1.1",
6 "version_name": "6.1.1",
7 - "minimum_chrome_version": "102",
7 + "minimum_chrome_version": "114",
8 "icons": {
9 "16": "icons/16-production.png",
10 "32": "icons/32-production.png",
packages/react-devtools-shared/src/backend/views/TraceUpdates/canvas.js
+33 -1
@@ -65,6 +65,24 @@ function drawWeb(nodeToData: Map<HostInstance, Data>) {
65 drawGroupBorders(context, group);
66 drawGroupLabel(context, group);
67 });
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 + }
86 }
87
88 type GroupItem = {
@@ -191,7 +209,15 @@ function destroyNative(agent: Agent) {
209
210 function destroyWeb() {
211 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
219 if (canvas.parentNode != null) {
220 + // $FlowFixMe[incompatible-call]: Flow doesn't track that canvas is non-null here
221 canvas.parentNode.removeChild(canvas);
222 }
223 canvas = null;
@@ -204,6 +230,9 @@ export function destroy(agent: Agent): void {
230
231 function initialize(): void {
232 canvas = window.document.createElement('canvas');
233 + canvas.setAttribute('popover', 'manual');
234 +
235 + // $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
236 canvas.style.cssText = `
237 xx-background-color: red;
238 xx-opacity: 0.5;
@@ -213,7 +242,10 @@ function initialize(): void {
242 position: fixed;
243 right: 0;
244 top: 0;
216 - z-index: 1000000000;
245 + background-color: transparent;
246 + outline: none;
247 + box-shadow: none;
248 + border: none;
249 `;
250
251 const root = window.document.documentElement;
packages/react-devtools-shell/src/app/TraceUpdatesTest/index.js new
+87
@@ -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
+2
@@ -19,6 +19,7 @@ import Toggle from './Toggle';
19 import ErrorBoundaries from './ErrorBoundaries';
20 import PartiallyStrictApp from './PartiallyStrictApp';
21 import SuspenseTree from './SuspenseTree';
22 +import TraceUpdatesTest from './TraceUpdatesTest';
23 import {ignoreErrors, ignoreLogs, ignoreWarnings} from './console';
24
25 import './styles.css';
@@ -112,6 +113,7 @@ function mountTestApp() {
113 mountApp(SuspenseTree);
114 mountApp(DeeplyNestedComponents);
115 mountApp(Iframe);
116 + mountApp(TraceUpdatesTest);
117
118 if (shouldRenderLegacy) {
119 mountLegacyApp(PartiallyStrictApp);