From 941f6bb1bef23da0ce65f053c28c1ba3ca054599 Mon Sep 17 00:00:00 2001 From: Tink bot Date: Mon, 18 May 2026 15:22:27 +0000 Subject: [PATCH] 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 opened via showModal(), which places it in the browser's top layer. floating-vue teleports tooltips to by default, so they were rendered *below* the dialog backdrop and hidden behind it. Wrap the v-tooltip directive to detect the nearest 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 as before. --- frontend/src/directives/tooltip.ts | 49 ++++++++++++++++++++++++++++++ frontend/src/main.ts | 4 +-- 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 frontend/src/directives/tooltip.ts diff --git a/frontend/src/directives/tooltip.ts b/frontend/src/directives/tooltip.ts new file mode 100644 index 000000000..4c76d6ed6 --- /dev/null +++ b/frontend/src/directives/tooltip.ts @@ -0,0 +1,49 @@ +import type {Directive, DirectiveBinding} from 'vue' +import {vTooltip} from 'floating-vue' + +// When a tooltip target lives inside a opened via showModal(), the +// dialog is in the browser's top layer. floating-vue teleports tooltips to +// 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 + if (typeof value === 'string') { + normalized = {content: value} + } else if (value && typeof value === 'object') { + normalized = {...value as Record} + } 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 = { + 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 diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 1a81de2a2..6f64e8d8c 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -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)