fix(editor): render link prompt inside the task dialog so it works in the Kanban popup (#2940)
The Kanban task popup renders the description editor inside a native <dialog> opened via showModal(), which lives in the browser's top-layer. inputPrompt appended its URL <input> to document.body, so it was painted behind the top-layer dialog (z-index cannot beat the top-layer) and could not be focused through the dialog's focus trap. As a result clicking "Link" in the popup did nothing, while it worked on the full task page (no modal). Thread the TipTap editor through inputPrompt and append the prompt to getPopupContainer(editor) — the open dialog ancestor when present, falling back to document.body otherwise, so non-modal usage is unchanged. This is the same helper the slash menu and mentions already use to escape the top-layer (#1746). Fixes #2940
This commit is contained in:
parent
b6af132845
commit
2126130fdd
|
|
@ -722,7 +722,7 @@ async function addImage(event: Event) {
|
|||
return
|
||||
}
|
||||
|
||||
const url = await inputPrompt(event.target.getBoundingClientRect())
|
||||
const url = await inputPrompt(event.target.getBoundingClientRect(), '', editor.value)
|
||||
|
||||
if (url) {
|
||||
editor.value?.chain().focus().setImage({src: url}).run()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import inputPrompt from '@/helpers/inputPrompt'
|
|||
|
||||
export async function setLinkInEditor(pos: DOMRect, editor: Editor | null | undefined) {
|
||||
const previousUrl = editor?.getAttributes('link').href || ''
|
||||
const url = await inputPrompt(pos, previousUrl)
|
||||
const url = await inputPrompt(pos, previousUrl, editor ?? undefined)
|
||||
|
||||
// empty
|
||||
if (url === '') {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,17 @@ import {createRandomID} from '@/helpers/randomId'
|
|||
import {computePosition, flip, shift, offset} from '@floating-ui/dom'
|
||||
import {nextTick} from 'vue'
|
||||
import {eventToShortcutString} from '@/helpers/shortcut'
|
||||
import type {Editor} from '@tiptap/core'
|
||||
import {getPopupContainer} from '@/components/input/editor/popupContainer'
|
||||
|
||||
export default function inputPrompt(pos: ClientRect, oldValue: string = ''): Promise<string> {
|
||||
export default function inputPrompt(pos: ClientRect, oldValue: string = '', editor?: Editor): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const id = 'link-input-' + createRandomID()
|
||||
// Append inside the open task <dialog> (top-layer) when present, otherwise
|
||||
// document.body. A body-level popup is painted behind a showModal() dialog
|
||||
// and unfocusable through its focus trap, breaking the link prompt in the
|
||||
// Kanban task popup (#2940).
|
||||
const container = getPopupContainer(editor)
|
||||
|
||||
// Create popup element
|
||||
const popupElement = document.createElement('div')
|
||||
|
|
@ -26,7 +33,7 @@ export default function inputPrompt(pos: ClientRect, oldValue: string = ''): Pro
|
|||
inputElement.value = oldValue
|
||||
wrapperDiv.appendChild(inputElement)
|
||||
popupElement.appendChild(wrapperDiv)
|
||||
document.body.appendChild(popupElement)
|
||||
container.appendChild(popupElement)
|
||||
|
||||
// Create a local mutable copy of the position for scroll tracking
|
||||
let currentRect = new DOMRect(pos.left, pos.top, pos.width, pos.height)
|
||||
|
|
@ -84,8 +91,8 @@ export default function inputPrompt(pos: ClientRect, oldValue: string = ''): Pro
|
|||
|
||||
const cleanup = () => {
|
||||
window.removeEventListener('scroll', handleScroll, true)
|
||||
if (document.body.contains(popupElement)) {
|
||||
document.body.removeChild(popupElement)
|
||||
if (container.contains(popupElement)) {
|
||||
container.removeChild(popupElement)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import {test, expect} from '../../support/fixtures'
|
||||
import {ProjectFactory} from '../../factories/project'
|
||||
import {ProjectViewFactory} from '../../factories/project_view'
|
||||
import {BucketFactory} from '../../factories/bucket'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {TaskBucketFactory} from '../../factories/task_buckets'
|
||||
|
||||
// Regression test for #2940: in the Kanban task popup the description editor is
|
||||
// rendered inside a native <dialog> opened via showModal() (browser top-layer).
|
||||
// The link prompt used to be appended to document.body, so it was painted behind
|
||||
// the dialog and unfocusable through its focus trap, making "set link" a no-op.
|
||||
test.describe('Editor link prompt inside the Kanban task popup', () => {
|
||||
test('creates a link in the description when opened as the Kanban popup', async ({authenticatedPage: page}) => {
|
||||
const projects = await ProjectFactory.create(1)
|
||||
const views = await ProjectViewFactory.create(1, {
|
||||
id: 1,
|
||||
project_id: projects[0].id,
|
||||
view_kind: 3,
|
||||
bucket_configuration_mode: 1,
|
||||
})
|
||||
const buckets = await BucketFactory.create(1, {
|
||||
project_view_id: views[0].id,
|
||||
})
|
||||
const tasks = await TaskFactory.create(1, {
|
||||
project_id: projects[0].id,
|
||||
description: 'link me',
|
||||
index: 1,
|
||||
})
|
||||
await TaskBucketFactory.create(1, {
|
||||
task_id: tasks[0].id,
|
||||
bucket_id: buckets[0].id,
|
||||
project_view_id: views[0].id,
|
||||
})
|
||||
|
||||
await page.goto(`/projects/${projects[0].id}/${views[0].id}`)
|
||||
|
||||
const card = page.locator('.kanban .bucket .tasks .task').filter({hasText: tasks[0].title})
|
||||
await expect(card).toBeVisible()
|
||||
await card.click()
|
||||
|
||||
// The task popup must be a native <dialog> in the top layer.
|
||||
const dialog = page.locator('dialog[open]')
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.locator('.task-view')).toBeVisible()
|
||||
|
||||
const editButton = dialog.locator('.details.content.description .tiptap button.done-edit').filter({hasText: 'Edit'})
|
||||
await expect(editButton).toBeVisible({timeout: 10000})
|
||||
await editButton.click()
|
||||
|
||||
const description = dialog.locator('.details.content.description')
|
||||
const editor = description.locator('[contenteditable="true"]').first()
|
||||
await expect(editor).toBeVisible({timeout: 10000})
|
||||
await editor.click()
|
||||
await page.keyboard.press('ControlOrMeta+a')
|
||||
|
||||
await description.locator('.editor-toolbar__button').filter({hasText: 'Link'}).click()
|
||||
|
||||
const urlInput = dialog.locator('input.input[placeholder="URL"]')
|
||||
await expect(urlInput).toBeVisible()
|
||||
await urlInput.fill('https://vikunja.io')
|
||||
await urlInput.press('Enter')
|
||||
|
||||
const link = editor.locator('a[href="https://vikunja.io"]')
|
||||
await expect(link).toBeVisible()
|
||||
await expect(link).toHaveText('link me')
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue