From 4f273bd36493cd3e818a15a5da5f82ba6af7f812 Mon Sep 17 00:00:00 2001 From: petertdinh Date: Sat, 2 May 2026 01:50:26 -0700 Subject: [PATCH 1/2] fix(devtools): clear highlight when mouse leaves DevTools panel (#36177) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #17855 When hovering a component in the DevTools Components inspector, a highlight overlay appears on the inspected page. The highlight is cleared via `onMouseLeave` on the tree container `div`. But this React synthetic event only fires when the pointer transitions between elements _within the same document_. When the user moves their mouse out of the DevTools panel window entirely (e.g. to the browser viewport), no element in the React tree receives `mouseleave`, so `clearHostInstanceHighlight` is never sent over the bridge and the overlay persists on the page. The fix adds a native `mouseleave` listener on the DevTools panel's `ownerDocument` in `Tree.js`. When the pointer exits the panel viewport, it fires `clearHighlightHostInstance` and removes the overlay. Using `ownerDocument` (rather than document) is consistent with the existing pattern in `Tree.js` for browser extension compatibility. How did you test this change? Tested manually using the Chrome extension: 1. Opened React DevTools → Components tab on a React app 2. Hovered a component in the tree — highlight appeared on the page ✓ 3. Moved the mouse out of the DevTools panel into the browser viewport — highlight cleared immediately ✓ (previously it persisted) 4. Moved the mouse back into the panel and hovered a component — highlighting still works normally ✓ 5. Unhovered within the panel — highlight still clears correctly ✓ Ran the DevTools test suite: yarn test --no-watchman ReactDevTools — all tests pass. --- .../src/devtools/views/Components/Tree.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js index 5623d507a3b..502056f936c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js @@ -384,6 +384,23 @@ export default function Tree(): React.Node { const handleMouseLeave = clearHighlightHostInstance; + // The synthetic onMouseLeave on the tree div only fires within the document, + // so we need a native listener on the document itself. + useEffect(() => { + const container = focusTargetRef.current; + if (container == null) { + return; + } + const ownerDocument = container.ownerDocument; + ownerDocument.addEventListener('mouseleave', clearHighlightHostInstance); + return () => { + ownerDocument.removeEventListener( + 'mouseleave', + clearHighlightHostInstance, + ); + }; + }, [clearHighlightHostInstance]); + // Let react-window know to re-render any time the underlying tree data changes. // This includes the owner context, since it controls a filtered view of the tree. const itemData = useMemo( From 9635257c1b557acc81f95b1e974a54c752e703a2 Mon Sep 17 00:00:00 2001 From: zxuhan7 Date: Sat, 2 May 2026 10:50:40 +0200 Subject: [PATCH 2/2] [DevTools] Preserve -Infinity in inspected values (#36347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `getDataType` collapsed both `Infinity` and `-Infinity` to the `'infinity'` data type, so a `-Infinity` value coming from inspected props/state/hooks was rehydrated on the frontend as `Infinity`. This adds a `'-infinity'` `DataType`, routes it through `dehydrate`/`hydrate` alongside the existing `'infinity'` arm, and makes `smartParse`/`smartStringify` (used for editable hook values) symmetric. ## Files - `packages/react-devtools-shared/src/utils.js` — extend `DataType`, split sign in `getDataType`, route `'-infinity'` through `formatDataForPreview`. - `packages/react-devtools-shared/src/hydration.js` — `dehydrate` and `hydrate` cases for `'-infinity'`. - `packages/react-devtools-shared/src/devtools/utils.js` — `smartParse` accepts `'-Infinity'`; `smartStringify` returns `'-Infinity'` for negative infinite values. - `packages/react-devtools-shared/src/__tests__/inspectedElement-test.js` and `legacy/inspectElement-test.js` — added `minus_infinity={-Infinity}` to the simple-data-types tests + snapshots. - `packages/react-devtools-shell/src/app/InspectableElements/SimpleValues.js` — added `minusInfinity` to the dev shell so the path is exercised manually. ## Test plan - [x] `yarn prettier` / `yarn linc` - [x] `yarn flow dom-node` — no errors - [x] `yarn test --silent --no-watchman -t "should support simple data types"` (source channel) - [x] `yarn test-www --silent --no-watchman -t "should support simple data types"` (www-modern) - [ ] `yarn test-build-devtools` — relies on a built bundle; left to CI per repo policy. Fixes #32552 --- .../src/__tests__/inspectedElement-test.js | 2 ++ .../src/__tests__/legacy/inspectElement-test.js | 2 ++ packages/react-devtools-shared/src/devtools/utils.js | 4 +++- packages/react-devtools-shared/src/hydration.js | 3 +++ packages/react-devtools-shared/src/utils.js | 4 +++- .../src/app/InspectableElements/SimpleValues.js | 1 + 6 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js index 09f811172f3..a73acd55bf4 100644 --- a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js @@ -582,6 +582,7 @@ describe('InspectedElement', () => { boolean_false={false} boolean_true={true} infinity={Infinity} + minus_infinity={-Infinity} integer_zero={0} integer_one={1} float={1.23} @@ -604,6 +605,7 @@ describe('InspectedElement', () => { "infinity": Infinity, "integer_one": 1, "integer_zero": 0, + "minus_infinity": -Infinity, "nan": NaN, "string": "abc", "string_empty": "", diff --git a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js index f306ab97093..62ba2e13608 100644 --- a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js @@ -98,6 +98,7 @@ describe('InspectedElementContext', () => { boolean_false: false, boolean_true: true, infinity: Infinity, + minus_infinity: -Infinity, integer_zero: 0, integer_one: 1, float: 1.23, @@ -128,6 +129,7 @@ describe('InspectedElementContext', () => { "infinity": Infinity, "integer_one": 1, "integer_zero": 0, + "minus_infinity": -Infinity, "nan": NaN, "string": "abc", "string_empty": "", diff --git a/packages/react-devtools-shared/src/devtools/utils.js b/packages/react-devtools-shared/src/devtools/utils.js index 876e01345b4..43d3ea6c837 100644 --- a/packages/react-devtools-shared/src/devtools/utils.js +++ b/packages/react-devtools-shared/src/devtools/utils.js @@ -235,6 +235,8 @@ export function smartParse(value: any): any | void | number { switch (value) { case 'Infinity': return Infinity; + case '-Infinity': + return -Infinity; case 'NaN': return NaN; case 'undefined': @@ -249,7 +251,7 @@ export function smartStringify(value: any): string { if (Number.isNaN(value)) { return 'NaN'; } else if (!Number.isFinite(value)) { - return 'Infinity'; + return value > 0 ? 'Infinity' : '-Infinity'; } } else if (value === undefined) { return 'undefined'; diff --git a/packages/react-devtools-shared/src/hydration.js b/packages/react-devtools-shared/src/hydration.js index 2468917d939..3670514ff4c 100644 --- a/packages/react-devtools-shared/src/hydration.js +++ b/packages/react-devtools-shared/src/hydration.js @@ -596,6 +596,7 @@ export function dehydrate( return value; } case 'infinity': + case '-infinity': case 'nan': case 'undefined': // Some values are lossy when sent through a WebSocket. @@ -704,6 +705,8 @@ export function hydrate( return; } else if (value.type === 'infinity') { parent[last] = Infinity; + } else if (value.type === '-infinity') { + parent[last] = -Infinity; } else if (value.type === 'nan') { parent[last] = NaN; } else if (value.type === 'undefined') { diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index 66de8440b65..aa7ba33dc59 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -708,6 +708,7 @@ export type DataType = | 'html_all_collection' | 'html_element' | 'infinity' + | '-infinity' | 'iterator' | 'opaque_iterator' | 'nan' @@ -765,7 +766,7 @@ export function getDataType(data: Object): DataType { if (Number.isNaN(data)) { return 'nan'; } else if (!Number.isFinite(data)) { - return 'infinity'; + return data > 0 ? 'infinity' : '-infinity'; } else { return 'number'; } @@ -1219,6 +1220,7 @@ export function formatDataForPreview( case 'boolean': case 'number': case 'infinity': + case '-infinity': case 'nan': case 'null': case 'undefined': diff --git a/packages/react-devtools-shell/src/app/InspectableElements/SimpleValues.js b/packages/react-devtools-shell/src/app/InspectableElements/SimpleValues.js index 22c54ec91ee..1a17dc2279c 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/SimpleValues.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/SimpleValues.js @@ -25,6 +25,7 @@ export default class SimpleValues extends Component { null={null} nan={NaN} infinity={Infinity} + minusInfinity={-Infinity} true={true} false={false} function={noop}