From 91d5cfb1c051acd5d4988211cae4a4ef445075cf Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 15 Apr 2026 09:48:09 +0200 Subject: [PATCH] fix(frontend): render editor popups inside modal dialog top-layer Native 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 ancestor of the editor (falling back to document.body) so they join the same top-layer stacking context. --- .../input/editor/mention/mentionSuggestion.ts | 6 ++++-- .../src/components/input/editor/popupContainer.ts | 11 +++++++++++ frontend/src/components/input/editor/suggestion.ts | 5 +++-- 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/input/editor/popupContainer.ts diff --git a/frontend/src/components/input/editor/mention/mentionSuggestion.ts b/frontend/src/components/input/editor/mention/mentionSuggestion.ts index 0aaf756a8..1afd2f66a 100644 --- a/frontend/src/components/input/editor/mention/mentionSuggestion.ts +++ b/frontend/src/components/input/editor/mention/mentionSuggestion.ts @@ -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() diff --git a/frontend/src/components/input/editor/popupContainer.ts b/frontend/src/components/input/editor/popupContainer.ts new file mode 100644 index 000000000..92679d936 --- /dev/null +++ b/frontend/src/components/input/editor/popupContainer.ts @@ -0,0 +1,11 @@ +import type {Editor} from '@tiptap/core' + +// Native 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 +} diff --git a/frontend/src/components/input/editor/suggestion.ts b/frontend/src/components/input/editor/suggestion.ts index 151afc7f7..55146c34f 100644 --- a/frontend/src/components/input/editor/suggestion.ts +++ b/frontend/src/components/input/editor/suggestion.ts @@ -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()