fix(tooltip): show tooltips in top layer when inside modal dialog

Tooltips on relative dates (and other content) were invisible when a task
was opened in the modal. The modal uses <dialog> opened via showModal(),
which places it in the browser's top layer. floating-vue teleports
tooltips to <body> by default, so they were rendered *below* the dialog
backdrop and hidden behind it.

Wrap the v-tooltip directive to detect the nearest <dialog> ancestor of
the target and use it as the tooltip's container, keeping the tooltip in
the same top-layer context as the modal it belongs to. Tooltips outside
any dialog still teleport to <body> as before.
This commit is contained in:
Tink bot 2026-05-18 15:22:27 +00:00 committed by kolaente
parent 52f3dd6806
commit 941f6bb1be
2 changed files with 51 additions and 2 deletions

View File

@ -0,0 +1,49 @@
import type {Directive, DirectiveBinding} from 'vue'
import {vTooltip} from 'floating-vue'
// When a tooltip target lives inside a <dialog> opened via showModal(), the
// dialog is in the browser's top layer. floating-vue teleports tooltips to
// <body> by default, so they render *below* the dialog's ::backdrop and are
// not visible. Teleporting them into the dialog keeps them in the top layer.
function buildBinding(el: Element, binding: DirectiveBinding): DirectiveBinding {
const dialog = el.closest('dialog')
if (!dialog) {
return binding
}
const value = binding.value
let normalized: Record<string, unknown>
if (typeof value === 'string') {
normalized = {content: value}
} else if (value && typeof value === 'object') {
normalized = {...value as Record<string, unknown>}
} else {
return binding
}
if (normalized.container === undefined) {
normalized.container = dialog
}
return {...binding, value: normalized}
}
// Bind via `mounted` rather than `beforeMount` so the element is already
// attached to the DOM — otherwise `el.closest('dialog')` cannot find the
// dialog ancestor.
const tooltip: Directive<Element, unknown> = {
mounted(el, binding) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(vTooltip as any).beforeMount(el, buildBinding(el, binding))
},
updated(el, binding) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(vTooltip as any).updated(el, buildBinding(el, binding))
},
beforeUnmount(el) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(vTooltip as any).beforeUnmount(el)
},
}
export default tooltip

View File

@ -38,7 +38,7 @@ if (window.API_URL.endsWith('/')) {
// directives
import focus from '@/directives/focus'
import {vTooltip} from 'floating-vue'
import tooltip from '@/directives/tooltip'
import 'floating-vue/dist/style.css'
import shortcut from '@/directives/shortcut'
import testid from '@/directives/testid'
@ -66,7 +66,7 @@ setLanguage(browserLanguage).then(() => {
app.use(Notifications)
app.directive('focus', focus)
app.directive('tooltip', vTooltip)
app.directive('tooltip', tooltip)
app.directive('shortcut', shortcut)
app.directive('cy', testid)