feat: support hover highlighting inside portals and modals#509
feat: support hover highlighting inside portals and modals#509Akshay090 wants to merge 6 commits intozh-lx:mainfrom
Conversation
…ression
## What changed
1. **JSXMemberExpression support** (`transform-jsx.ts`)
- `<Typography.H2>`, `<Page.Content>`, `<Icons.Close />` etc. now correctly
appear in `data-insp-path`. Previously only simple identifiers like `<div>`
were captured; member expressions were silently dropped.
2. **React 19 jsx-dev-runtime patch** (`packages/vite`)
- React 19 removed `_debugSource` from fibers (facebook/react#32574).
The Vite plugin now patches `jsx-dev-runtime.js` at dev time to inject
the `source` parameter into `_debugInfo`, restoring file/line/column
info for the client to read. Supports React 19.0–19.2+.
- Adapted from vite-plugin-react-click-to-component by ArnaudBarre.
3. **Fiber-based component tree in context menu** (`packages/core/client`)
- The right-click "Click node to locate" panel now walks React's
`_debugOwner` chain to show actual component names (e.g. `<Dashboard>`,
`<UserProfile>`) instead of only DOM elements (`<div>`, `<span>`).
- Components from `node_modules` or with unresolvable names are filtered out.
- Falls back to the original DOM-based tree for non-React apps.
## Before / After
**Before:** tree shows mostly `<div>` tags with only a few component names that
happened to have `data-insp-path`.
**After:** tree shows the actual React component hierarchy with proper names,
making it far easier to navigate to the right source file.
Made-with: Cursor
Vite pre-bundles dependencies into chunk files (e.g. chunk-XXXXX.js), so the jsx-dev-runtime code ends up in a chunk without "jsx-dev-runtime" in the filename. Add content-based fallback detection for these chunks by checking for "_debugInfo" and "value: null" in the code content. Made-with: Cursor
…nsp-path parsing data-insp-path uses colon as its delimiter (filePath:line:column:tagName). JSXNamespacedName produces names like `svg:xlink` which would break the client-side parser. Use dot separator instead (e.g. `svg.xlink`). Added a test to verify the separator is dot, not colon. Made-with: Cursor
When elements are rendered inside React portals (e.g. modals, drawers), the hover highlight (Option+Shift) previously didn't work because: 1. Portal content is often rendered by library components (from node_modules) which don't have data-insp-path attributes 2. composedPath() may return the modal backdrop instead of the actual content when a backdrop overlay intercepts mouse events This commit fixes both issues: - Add fiber-based fallback in handleMouseMove: when no data-insp-path is found in the DOM path, fall back to getLayersFromFiber() to find the nearest user component with source info - Add elementFromPoint-based target detection: use document.elementFromPoint() to find the visually topmost element, bypassing backdrop overlays that intercept composedPath() - Extend renderCover to accept optional SourceInfo parameter so fiber- derived source info can be passed directly without requiring data-insp-path on the DOM element - Add project root inference to convert absolute fiber paths to relative paths matching data-insp-path format - Clean up "undefined" fragments in component displayNames (e.g. "WithFormField(Form.undefined)" → "WithFormField(Form)") - Add null safety for this.element in the render template Made-with: Cursor
Review Summary by QodoSupport hover highlighting inside portals and modals with fiber-based fallback
WalkthroughsDescription• Add fiber-based hover highlighting for React portals and modals • Support JSXMemberExpression and JSXNamespacedName in JSX transform • Patch React 19 jsx-dev-runtime to restore source info on fibers • Implement effective target detection using elementFromPoint for overlays • Add component name cleanup and relative path inference for fiber sources Diagramflowchart LR
A["Mouse Event"] --> B["getEffectiveNodePath"]
B --> C["elementFromPoint for topmost element"]
C --> D["getValidNodeList with data-insp-path"]
D --> E{Found valid node?}
E -->|Yes| F["renderCover with DOM path"]
E -->|No| G["getLayersFromFiber fallback"]
G --> H["Extract React component info"]
H --> I["renderCover with fiber SourceInfo"]
J["React 19 jsx-dev-runtime"] --> K["Patch _debugInfo with source"]
K --> L["Client reads source info"]
L --> M["Component tree generation"]
File Changes1. packages/core/src/client/index.ts
|
Code Review by Qodo
1. Fiber walk stops early
|
| while (current) { | ||
| const source = getFiberSource(current); | ||
| if (source?.fileName && isValidSourcePath(source.fileName)) { | ||
| const name = getFiberComponentName(current); | ||
| if (name.includes('Unknown')) { current = current._debugOwner; continue; } | ||
| const domNode = (current.stateNode instanceof HTMLElement) | ||
| ? current.stateNode | ||
| : findFirstDomNode(current); | ||
|
|
||
| layers.push({ | ||
| name, | ||
| path: toRelativePath(source.fileName), | ||
| line: source.lineNumber ?? 1, | ||
| column: source.columnNumber ?? 1, | ||
| element: domNode || target, | ||
| }); | ||
| } | ||
| current = current._debugOwner; | ||
| } |
There was a problem hiding this comment.
1. Fiber walk stops early 🐞 Bug ≡ Correctness
getLayersFromFiber() only walks via _debugOwner, so if _debugOwner is missing on the starting/host fiber the traversal terminates immediately and the new portal hover fallback (and fiber-based node tree) can return no layers even when an owner chain exists.
Agent Prompt
### Issue description
`getLayersFromFiber()` climbs only via `current._debugOwner`. When `_debugOwner` is unset on the starting fiber, traversal ends and `layers` becomes empty, breaking the new hover fallback inside portals/modals and the fiber-based context menu tree.
### Issue Context
`inferProjectRoot()` already uses `_debugOwner ?? return`, indicating the codebase expects `_debugOwner` can be missing.
### Fix Focus Areas
- packages/core/src/client/index.ts[219-246]
- packages/core/src/client/index.ts[187-209]
### What to change
- In `getLayersFromFiber()`, advance `current` using `current._debugOwner ?? current.return` (similar to `inferProjectRoot`).
- Add a small guard against non-progressing loops (optional): if neither pointer changes, break.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
getLayersFromFiber() previously only walked via _debugOwner, which can be unset on host fibers (e.g. plain div elements). This caused the traversal to stop immediately, returning no layers and breaking the portal hover fallback. Now falls back to fiber.return when _debugOwner is absent, matching the pattern already used in inferProjectRoot(). Added a visited set guard to prevent infinite loops. Made-with: Cursor
When building fiber layers, _debugSource points to the call site (e.g. withSpinner.func.tsx where <Component> is rendered), not the component's definition file. This is confusing when clicking a component in the tree. Now checks if the fiber's DOM node has a data-insp-path attribute and prefers its file path, which points to where the component's render output is actually defined. This fixes navigation for all HOC-wrapped components (withSpinner, connect, memo, forwardRef, etc.). Made-with: Cursor
Problem
When elements are rendered inside React portals (e.g. modals, drawers, popovers), the hover highlight (Option+Shift hover) doesn't work. The right-click component tree works fine because it uses the fiber-based approach from #506, but hover highlighting fails for two reasons:
No
data-insp-pathon portal content: Portal content is often rendered by library components (fromnode_modules) which are excluded from the JSX transform. The hover handler (handleMouseMove) relies entirely ondata-insp-pathto find elements, so it finds nothing inside portals.composedPath()returns backdrop element: When a modal has a backdrop overlay,composedPath()returns the backdropdivas the event target rather than the actual modal content the user sees.Solution
1. Fiber-based fallback for hover (
handleMouseMove)When
getValidNodeListreturns empty (nodata-insp-pathfound in the DOM path), fall back togetLayersFromFiber()— the same fiber tree infrastructure used by the right-click context menu — to find the nearest user component with source info and a highlightable DOM node.2.
elementFromPointfor effective target detectionAdded
getEffectiveNodePath()which usesdocument.elementFromPoint(e.clientX, e.clientY)to find the visually topmost element at the mouse coordinates, bypassing transparent backdrop overlays. Falls back tocomposedPath()when both return the same element.3.
renderCoveraccepts optionalSourceInfoExtended
renderCoverto accept an optionalSourceInfoparameter. When the fiber fallback is used, source info (name, path, line, column) is passed directly — avoiding the crash that occurred when trying to parsedata-insp-pathfrom a DOM element that doesn't have it.4. Relative path inference for fiber paths
React's
_debugSource.fileNamestores absolute paths, butdata-insp-pathuses relative paths (whenpathType: 'relative'). AddedinferProjectRoot()which detects the project root by comparing a relativedata-insp-pathwith the absolute fiber path from the same element, thentoRelativePath()strips the prefix. Result is cached after first lookup.5. Component name cleanup
Cleaned up
undefinedfragments in componentdisplayNamevalues. For example, when a HOC wraps an anonymous component:WithFormField(Form.undefined)→WithFormField(Form).Testing
Tested on a large React 19 + Vite application with design system modals:
Dependencies