Skip to content

fix(web): render in-context editor in browser top layer#3519

Open
dkrizan wants to merge 1 commit into
mainfrom
dkrizan/in-context-editor-top-layer
Open

fix(web): render in-context editor in browser top layer#3519
dkrizan wants to merge 1 commit into
mainfrom
dkrizan/in-context-editor-top-layer

Conversation

@dkrizan

@dkrizan dkrizan commented May 19, 2026

Copy link
Copy Markdown
Member

Problem

When a translation key sits inside a native <dialog> opened with showModal(), alt-clicking it opens the Tolgee Quick translation editor — but the host dialog paints on top of the editor and steals focus from its inputs. The editor becomes unusable: you can see the form behind the host dialog, but you can't type into it.

Repro on this branch: testapps/react/translation-methods → "Open dialog" → alt-click the translation inside.

Cause

A modal <dialog> element (one opened via .showModal()) is promoted to the browser's top layer, a special render layer that sits above the entire normal stacking context. z-index cannot reach the top layer, and the dialog enforces a focus scope.

The in-context editor was rendered via MUI's <Dialog> (packages/web/src/package/ui/KeyDialog/TranslationDialogWrapper.tsx) — a portal'd <div> with zIndex: DEVTOOLS_Z_INDEX. No matter how large that z-index is, a host modal <dialog> paints above it.

Solution

Wrap the existing MUI <Dialog> in a native <dialog> element and call showModal() on mount. That promotes the editor into the same top layer as any host modal. The top layer behaves like a stack — the most-recently-shown modal paints above earlier ones and receives focus — so the editor now overlays the host dialog and takes focus from it.

Implementation notes:

  • Native <dialog> UA defaults (border, padding, centred sizing) are flattened via GlobalStyles. The native ::backdrop is set transparent because MUI's own backdrop continues to provide the dim overlay.
  • pointer-events: none on the native <dialog> with pointer-events: auto on its children lets MUI's backdrop keep handling outside-click-to-close.
  • The native cancel event (Escape) is preventDefault-ed so the existing Escape handler in dialogContext remains the single source of truth for closing.
  • The useBrowserWindow path (open editor in a detached window) is untouched.
  • The editor lives inside a shadow root, which doesn't affect this — the top layer is per-document, so a <dialog> inside shadow DOM is still promoted to the document's top layer.

Test plan

  • Repro fixed: in testapps/react at /translation-methods, open the new "Translation inside dialog" example and alt-click the key inside — Quick translation editor paints above the host <dialog> and the English input receives focus.
  • Existing alt-click behaviour preserved on translations outside any dialog.
  • Escape closes the editor (and only the editor — the host dialog stays open).
  • Click outside the editor paper (on MUI's backdrop) closes the editor.
  • Screenshot mode: click the camera icon — editor disappears for the capture and reappears after.
  • Open in browser window (the useBrowserWindow path) still works.
  • Focus return on close: closing the editor returns focus into the host <dialog> so its own focus trap resumes.
  • Cross-browser smoke: Chrome, Firefox, Safari.

Summary by CodeRabbit

  • New Features
    • Integrated native HTML dialog element support with enhanced event handling
    • Improved dialog lifecycle with automatic cleanup on close
    • Better dialog overlay styling, transparency, and interaction control
    • Support for translations and content within native dialogs

Review Change Stack

The in-context editor (Quick translation modal) was rendered via MUI's
Dialog, which lives in the normal stacking context. When a host page
opened a translation key inside a modal <dialog> via showModal(), the
host dialog was promoted to the browser top layer and painted above
the editor regardless of z-index, and its focus scope kept stealing
keystrokes from the editor's inputs.

Wrap the existing MUI Dialog in a native <dialog> and call showModal()
on mount. That promotes the editor to the same top layer as any host
modal, and because the top layer is a stack, the most-recently-shown
modal paints on top and receives focus.

Native dialog UA defaults are neutralised via GlobalStyles (full
viewport, transparent backdrop, pointer-events fall through to MUI's
own backdrop). The native cancel event is preventDefault'd so the
existing Escape handler in dialogContext remains the single source of
truth for closing. The browser-window mode is untouched.

A "Translation inside dialog" example is added to the React testapp's
TranslationMethods page so the scenario is reproducible going forward.
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e73c464c-31e7-46b6-8df6-c284832a82da

📥 Commits

Reviewing files that changed from the base of the PR and between 23591f6 and 80e284d.

📒 Files selected for processing (2)
  • packages/web/src/package/ui/KeyDialog/TranslationDialogWrapper.tsx
  • testapps/react/src/TranslationMethods.tsx

Walkthrough

The PR adds native HTML <dialog> element support to TranslationDialogWrapper, wrapping the UI in a programmatically-controlled dialog that opens via showModal(), prevents default cancellation, and styles itself as a full-viewport overlay. A test app example demonstrates the dialog with a trigger button and integrated translation rendering.

Changes

Native HTML Dialog Implementation

Layer / File(s) Summary
Native dialog wrapper with lifecycle management
packages/web/src/package/ui/KeyDialog/TranslationDialogWrapper.tsx
TranslationDialogWrapper wraps content in a native <dialog> controlled via nativeDialogRef. On mount, showModal() is called and a cancel event listener prevents default closing. GlobalStyles injects full-viewport transparent overlay styling. Event listener and dialog are cleaned up on unmount.
Dialog demo and usage example
testapps/react/src/TranslationMethods.tsx
A new "Translation inside dialog" UI block demonstrates the native dialog: a button triggers showModal(), and the dialog contains a <T> translation component with a close button using the native method="dialog" form.

Sequence Diagram

sequenceDiagram
  participant User
  participant DialogButton
  participant NativeDialog
  participant CancelHandler
  User->>DialogButton: Click open button
  DialogButton->>NativeDialog: showModal()
  NativeDialog->>CancelHandler: attach cancel event listener
  User->>NativeDialog: Trigger cancel (e.g., Escape key)
  NativeDialog->>CancelHandler: dispatch cancel event
  CancelHandler->>CancelHandler: preventDefault()
  CancelHandler->>NativeDialog: close() [if needed]
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

A dialog box, native and true,
Opens with showModal() in view,
Top-layer glory, styled just right,
Prevents cancellation's default flight,
🐇 Hops through modal magic with delight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: wrapping the in-context editor in a native element to render it in the browser top layer, addressing the core problem of the host dialog blocking the editor.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dkrizan/in-context-editor-top-layer

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bdshadow

Copy link
Copy Markdown
Member

@dkrizan for some reason npm run build shows lots of errors in this branch (main doesn't have it for me):

@tolgee/ngx-testapp:build: Error: projects/sampleapp/src/app/app.component.html:1:1 - error NG8001: 'router-outlet' is not a known element:                                                                                                                                     
@tolgee/ngx-testapp:build: 1. If 'router-outlet' is an Angular component, then verify that it is included in the '@Component.imports' of this component.
@tolgee/ngx-testapp:build: 2. If 'router-outlet' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message.

And another note. Seems like tooltips stopped working - they're shown behind the popup window:
Screenshot 2026-07-17 at 14 03 36
What AI is saying about it:

Tooltips inside the editor now paint behind it (flagged by code-reviewer, devils-advocate, architecture-devils-advocate — and I verified it)
The shared Tooltip (ui/common/Tooltip.tsx) portals its Popper into getRootElement(), which is an ancestor of the new , so the tooltip lives outside the top-layer subtree. Top-layer content paints above all non-top-layer content regardless of z-index, so DEVTOOLS_Z_INDEX (2147483000) no longer wins. Every tooltip used inside the editor — "Open in separate window" & "Open key in Tolgee platform" (KeyForm.tsx), the translation-state indicator (TranslationTextField.tsx), screenshot controls, state controls — now renders occluded by the editor paper. The Select/Autocomplete overlays are safe because they use disablePortal; the tooltip is the one overlay that escapes.

@bdshadow bdshadow left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check 2 notes in the previous comment (this comment is to "Request changes" when submitting review - doesn't go without a comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants