[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
This commit is contained in:
YongSeok Jang (장용석) 2025-05-07 23:48:17 +09:00 committed by GitHub
parent e5a8de81e5
commit 53c9f81049
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 124 additions and 3 deletions

View File

@ -4,7 +4,7 @@
"description": "Adds React debugging tools to the Chrome Developer Tools.",
"version": "6.1.1",
"version_name": "6.1.1",
"minimum_chrome_version": "102",
"minimum_chrome_version": "114",
"icons": {
"16": "icons/16-production.png",
"32": "icons/32-production.png",

View File

@ -4,7 +4,7 @@
"description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
"version": "6.1.1",
"version_name": "6.1.1",
"minimum_chrome_version": "102",
"minimum_chrome_version": "114",
"icons": {
"16": "icons/16-production.png",
"32": "icons/32-production.png",

View File

@ -65,6 +65,24 @@ function drawWeb(nodeToData: Map<HostInstance, Data>) {
drawGroupBorders(context, group);
drawGroupLabel(context, group);
});
if (canvas !== null) {
if (nodeToData.size === 0 && canvas.matches(':popover-open')) {
// $FlowFixMe[prop-missing]: Flow doesn't recognize Popover API
// $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
canvas.hidePopover();
return;
}
// $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
if (canvas.matches(':popover-open')) {
// $FlowFixMe[prop-missing]: Flow doesn't recognize Popover API
// $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
canvas.hidePopover();
}
// $FlowFixMe[prop-missing]: Flow doesn't recognize Popover API
// $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
canvas.showPopover();
}
}
type GroupItem = {
@ -191,7 +209,15 @@ function destroyNative(agent: Agent) {
function destroyWeb() {
if (canvas !== null) {
if (canvas.matches(':popover-open')) {
// $FlowFixMe[prop-missing]: Flow doesn't recognize Popover API
// $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
canvas.hidePopover();
}
// $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API and loses canvas nullability tracking
if (canvas.parentNode != null) {
// $FlowFixMe[incompatible-call]: Flow doesn't track that canvas is non-null here
canvas.parentNode.removeChild(canvas);
}
canvas = null;
@ -204,6 +230,9 @@ export function destroy(agent: Agent): void {
function initialize(): void {
canvas = window.document.createElement('canvas');
canvas.setAttribute('popover', 'manual');
// $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
canvas.style.cssText = `
xx-background-color: red;
xx-opacity: 0.5;
@ -213,7 +242,10 @@ function initialize(): void {
position: fixed;
right: 0;
top: 0;
z-index: 1000000000;
background-color: transparent;
outline: none;
box-shadow: none;
border: none;
`;
const root = window.document.documentElement;

View File

@ -0,0 +1,87 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import {useRef, useState} from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<h3>Count: {count}</h3>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
};
function DialogComponent() {
const dialogRef = useRef(null);
const openDialog = () => {
if (dialogRef.current) {
dialogRef.current.showModal();
}
};
const closeDialog = () => {
if (dialogRef.current) {
dialogRef.current.close();
}
};
return (
<div style={{margin: '10px 0'}}>
<button onClick={openDialog}>Open Dialog</button>
<dialog ref={dialogRef} style={{padding: '20px'}}>
<h3>Dialog Content</h3>
<Counter />
<button onClick={closeDialog}>Close</button>
</dialog>
</div>
);
}
function RegularComponent() {
return (
<div style={{margin: '10px 0'}}>
<h3>Regular Component</h3>
<Counter />
</div>
);
}
export default function TraceUpdatesTest(): React.Node {
return (
<div>
<h2>TraceUpdates Test</h2>
<div style={{marginBottom: '20px'}}>
<h3>Standard Component</h3>
<RegularComponent />
</div>
<div style={{marginBottom: '20px'}}>
<h3>Dialog Component (top-layer element)</h3>
<DialogComponent />
</div>
<div
style={{marginTop: '20px', padding: '10px', border: '1px solid #ddd'}}>
<h3>How to Test:</h3>
<ol>
<li>Open DevTools Components panel</li>
<li>Enable "Highlight updates when components render" in settings</li>
<li>Click increment buttons and observe highlights</li>
<li>Open the dialog and test increments there as well</li>
</ol>
</div>
</div>
);
}

View File

@ -19,6 +19,7 @@ import Toggle from './Toggle';
import ErrorBoundaries from './ErrorBoundaries';
import PartiallyStrictApp from './PartiallyStrictApp';
import SuspenseTree from './SuspenseTree';
import TraceUpdatesTest from './TraceUpdatesTest';
import {ignoreErrors, ignoreLogs, ignoreWarnings} from './console';
import './styles.css';
@ -112,6 +113,7 @@ function mountTestApp() {
mountApp(SuspenseTree);
mountApp(DeeplyNestedComponents);
mountApp(Iframe);
mountApp(TraceUpdatesTest);
if (shouldRenderLegacy) {
mountLegacyApp(PartiallyStrictApp);