Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/components/Package/Dependencies.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,15 @@ const numberFormatter = useNumberFormatter()
{{ dep }}
</LinkBase>
<span class="flex items-center gap-1 max-w-[40%]" dir="ltr">
<span
<TooltipApp
v-if="outdatedDeps[dep]"
class="shrink-0"
class="shrink-0 p-2 -m-2"
:class="getVersionClass(outdatedDeps[dep])"
:title="getOutdatedTooltip(outdatedDeps[dep], $t)"
aria-hidden="true"
:text="getOutdatedTooltip(outdatedDeps[dep], $t)"
>
<span class="i-carbon:warning-alt w-3 h-3" />
</span>
</TooltipApp>
<LinkBase
v-if="getVulnerableDepInfo(dep)"
:to="packageRoute(dep, getVulnerableDepInfo(dep)!.version)"
Expand Down
8 changes: 4 additions & 4 deletions app/components/Package/InstallScripts.vue
Original file line number Diff line number Diff line change
Expand Up @@ -115,18 +115,18 @@ const isExpanded = shallowRef(false)
{{ dep }}
</LinkBase>
<span class="flex items-center gap-1">
<span
<TooltipApp
v-if="
outdatedNpxDeps[dep] &&
outdatedNpxDeps[dep].resolved !== outdatedNpxDeps[dep].latest
"
class="shrink-0"
class="shrink-0 p-2 -m-2"
:class="getVersionClass(outdatedNpxDeps[dep])"
:title="getOutdatedTooltip(outdatedNpxDeps[dep], $t)"
aria-hidden="true"
:text="getOutdatedTooltip(outdatedNpxDeps[dep], $t)"
>
<span class="i-carbon:warning-alt w-3 h-3" />
</span>
</TooltipApp>
<span
class="font-mono text-xs text-end truncate"
:class="getVersionClass(outdatedNpxDeps[dep])"
Expand Down
12 changes: 8 additions & 4 deletions app/components/Package/SkillsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,15 @@ function getWarningTooltip(skill: SkillListItem): string | undefined {
aria-hidden="true"
/>
<span class="font-mono text-sm text-fg-muted">{{ skill.name }}</span>
<span
<TooltipApp
v-if="skill.warnings?.length"
class="i-carbon:warning w-3.5 h-3.5 text-amber-500 shrink-0"
:title="getWarningTooltip(skill)"
/>
class="shrink-0 p-2 -m-2"
aria-hidden="true"
:text="getWarningTooltip(skill)"
to="#skills-modal"
>
<span class="i-carbon:warning w-3.5 h-3.5 text-amber-500" />
</TooltipApp>
</button>

<!-- Expandable details -->
Expand Down
3 changes: 3 additions & 0 deletions app/components/Tooltip/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const props = defineProps<{
position?: 'top' | 'bottom' | 'left' | 'right'
/** Enable interactive tooltip (pointer events + hide delay for clickable content) */
interactive?: boolean
/** Teleport target for the tooltip content (defaults to 'body') */
to?: string | HTMLElement
}>()

const isVisible = shallowRef(false)
Expand Down Expand Up @@ -48,6 +50,7 @@ const tooltipAttrs = computed(() => {
:position
:interactive
:tooltip-attr="tooltipAttrs"
:to="props.to"
@mouseenter="show"
@mouseleave="hide"
@focusin="show"
Expand Down
33 changes: 20 additions & 13 deletions app/components/Tooltip/Base.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,25 @@ import type { HTMLAttributes } from 'vue'
import type { Placement } from '@floating-ui/vue'
import { autoUpdate, flip, offset, shift, useFloating } from '@floating-ui/vue'

const props = defineProps<{
/** Tooltip text (optional when using content slot) */
text?: string
/** Position: 'top' | 'bottom' | 'left' | 'right' */
position?: 'top' | 'bottom' | 'left' | 'right'
/** is tooltip visible */
isVisible: boolean
/** Allow pointer events on tooltip (for interactive content like links) */
interactive?: boolean
/** attributes for tooltip element */
tooltipAttr?: HTMLAttributes
}>()
const props = withDefaults(
defineProps<{
/** Tooltip text (optional when using content slot) */
text?: string
/** Position: 'top' | 'bottom' | 'left' | 'right' */
position?: 'top' | 'bottom' | 'left' | 'right'
/** is tooltip visible */
isVisible: boolean
/** Allow pointer events on tooltip (for interactive content like links) */
interactive?: boolean
/** attributes for tooltip element */
tooltipAttr?: HTMLAttributes
/** Teleport target for the tooltip content (defaults to 'body') */
to?: string | HTMLElement
}>(),
{
to: 'body',
},
)

const triggerRef = useTemplateRef('triggerRef')
const tooltipRef = useTemplateRef('tooltipRef')
Expand All @@ -32,7 +39,7 @@ const { floatingStyles } = useFloating(triggerRef, tooltipRef, {
<div ref="triggerRef" class="inline-flex">
<slot />

<Teleport to="body">
<Teleport :to="props.to">
<Transition
enter-active-class="transition-opacity duration-150 motion-reduce:transition-none"
leave-active-class="transition-opacity duration-100 motion-reduce:transition-none"
Expand Down
54 changes: 54 additions & 0 deletions test/nuxt/components/Tooltip.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import TooltipBase from '~/components/Tooltip/Base.vue'

describe('TooltipBase to prop', () => {
it('teleports to body by default', async () => {
await mountSuspended(TooltipBase, {
props: {
text: 'Tooltip text',
isVisible: true,
tooltipAttr: { 'aria-label': 'tooltip' },
},
slots: {
default: '<button>Trigger</button>',
},
})

const tooltip = document.querySelector<HTMLElement>('[aria-label="tooltip"]')
expect(tooltip).not.toBeNull()
expect(tooltip?.textContent).toContain('Tooltip text')

const currentContainer = tooltip?.parentElement?.parentElement
expect(currentContainer).toBe(document.body)
})

it('teleports into provided container when using selector string', async () => {
const container = document.createElement('div')
container.id = 'tooltip-container'
document.body.appendChild(container)

try {
await mountSuspended(TooltipBase, {
props: {
text: 'Tooltip text',
isVisible: true,
to: '#tooltip-container',
tooltipAttr: { 'aria-label': 'tooltip' },
},
slots: {
default: '<button>Trigger</button>',
},
})

const tooltip = container.querySelector<HTMLElement>('[aria-label="tooltip"]')
expect(tooltip).not.toBeNull()
expect(tooltip?.textContent).toContain('Tooltip text')

const currentContainer = tooltip?.parentElement?.parentElement
expect(currentContainer).toBe(container)
} finally {
container.remove()
}
})
})
Loading