fix(frontend): render editor popups inside modal dialog top-layer

Native <dialog> elements opened with showModal() render in the browser's
top-layer. Popups appended to document.body end up behind the dialog
regardless of z-index, which broke the slash-command menu and the user
mention suggestion inside the task detail modal.

Append the popups to the nearest open <dialog> ancestor of the editor
(falling back to document.body) so they join the same top-layer stacking
context.
This commit is contained in:
kolaente 2026-04-15 09:48:09 +02:00 committed by kolaente
parent a1fbc277be
commit 91d5cfb1c0
3 changed files with 18 additions and 4 deletions

View File

@ -3,6 +3,7 @@ import { computePosition, flip, shift, offset, autoUpdate } from '@floating-ui/d
import type { Editor } from '@tiptap/core'
import MentionList from './MentionList.vue'
import { getPopupContainer } from '../popupContainer'
import ProjectUserService from '@/services/projectUsers'
import { fetchAvatarBlobUrl, getDisplayName } from '@/models/user'
import type { IUser } from '@/modelTypes/IUser'
@ -113,7 +114,8 @@ export default function mentionSuggestionSetup(projectId: number) {
popupElement.style.left = '0'
popupElement.style.zIndex = '4700'
popupElement.appendChild(component.element!)
document.body.appendChild(popupElement) // Update virtual reference
getPopupContainer(props.editor).appendChild(popupElement)
// Update virtual reference
const rect = props.clientRect()
if (rect) {
virtualReference.getBoundingClientRect = () => rect
@ -179,7 +181,7 @@ export default function mentionSuggestionSetup(projectId: number) {
cleanupFloating()
}
if (popupElement) {
document.body.removeChild(popupElement)
popupElement.remove()
popupElement = null
}
component.destroy()

View File

@ -0,0 +1,11 @@
import type {Editor} from '@tiptap/core'
// Native <dialog> elements opened with showModal() render in the browser's
// top-layer, so popups appended to document.body end up visually behind them
// regardless of z-index. Appending to the open dialog itself lifts the popup
// into the same top-layer stacking context.
export function getPopupContainer(editor?: Editor): HTMLElement {
const editorEl = editor?.view?.dom as HTMLElement | undefined
const dialog = editorEl?.closest('dialog[open]') as HTMLElement | null
return dialog ?? document.body
}

View File

@ -3,6 +3,7 @@ import {VueRenderer} from '@tiptap/vue-3'
import {computePosition, flip, shift, offset, autoUpdate} from '@floating-ui/dom'
import CommandsList from './CommandsList.vue'
import {getPopupContainer} from './popupContainer'
type TranslateFunction = (key: string) => string
@ -206,7 +207,7 @@ export default function suggestionSetup(t: TranslateFunction) {
popupElement.style.left = '0'
popupElement.style.zIndex = '4700'
popupElement.appendChild(component.element!)
document.body.appendChild(popupElement)
getPopupContainer(props.editor).appendChild(popupElement)
// Update virtual reference
const rect = props.clientRect()
@ -266,7 +267,7 @@ export default function suggestionSetup(t: TranslateFunction) {
cleanupFloating()
}
if (popupElement) {
document.body.removeChild(popupElement)
popupElement.remove()
popupElement = null
}
component.destroy()