File size: 4,122 Bytes
126ef31 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | <script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, useTemplateRef, nextTick } from 'vue'
const props = withDefaults(defineProps<{
content?: string
trigger?: 'hover' | 'click'
widthClass?: string
}>(), {
trigger: 'hover',
widthClass: 'w-64',
})
const show = ref(false)
const triggerRef = useTemplateRef<HTMLElement>('trigger')
const tooltipRef = useTemplateRef<HTMLElement>('tooltip')
const tooltipStyle = ref({ top: '0px', left: '0px' })
function openTooltip() {
show.value = true
nextTick(updatePosition)
}
function closeTooltip() {
show.value = false
}
function onEnter() {
if (props.trigger !== 'hover') return
openTooltip()
}
function onLeave() {
if (props.trigger !== 'hover') return
closeTooltip()
}
function onClick(event: MouseEvent) {
if (props.trigger !== 'click') return
event.stopPropagation()
if (show.value) {
closeTooltip()
return
}
openTooltip()
}
function onDocumentClick(event: MouseEvent) {
if (props.trigger !== 'click' || !show.value) return
const target = event.target as Node | null
if (!target) return
if (triggerRef.value?.contains(target) || tooltipRef.value?.contains(target)) return
closeTooltip()
}
function onDocumentKeydown(event: KeyboardEvent) {
if (props.trigger !== 'click') return
if (event.key === 'Escape') {
closeTooltip()
}
}
function onViewportChange() {
if (!show.value) return
updatePosition()
}
function updatePosition() {
const el = triggerRef.value
if (!el) return
const rect = el.getBoundingClientRect()
tooltipStyle.value = {
top: `${rect.top + window.scrollY}px`,
left: `${rect.left + rect.width / 2 + window.scrollX}px`,
}
}
onMounted(() => {
document.addEventListener('click', onDocumentClick, true)
document.addEventListener('keydown', onDocumentKeydown)
window.addEventListener('resize', onViewportChange)
window.addEventListener('scroll', onViewportChange, true)
})
onBeforeUnmount(() => {
document.removeEventListener('click', onDocumentClick, true)
document.removeEventListener('keydown', onDocumentKeydown)
window.removeEventListener('resize', onViewportChange)
window.removeEventListener('scroll', onViewportChange, true)
})
</script>
<template>
<div
ref="trigger"
class="group relative ml-1 inline-flex items-center align-middle"
@mouseenter="onEnter"
@mouseleave="onLeave"
@click="onClick"
>
<!-- Trigger Icon -->
<slot name="trigger">
<svg
class="h-4 w-4 cursor-help text-gray-400 transition-colors hover:text-primary-600 dark:text-gray-500 dark:hover:text-primary-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</slot>
<!-- Teleport to body to escape modal overflow clipping -->
<Teleport to="body">
<div
ref="tooltip"
v-show="show"
role="tooltip"
:class="[
'fixed z-[99999] -translate-x-1/2 -translate-y-full rounded-lg bg-gray-900 p-3 text-xs leading-relaxed text-white shadow-xl ring-1 ring-white/10 dark:bg-gray-800',
props.widthClass,
]"
:style="{ top: `calc(${tooltipStyle.top} - 8px)`, left: tooltipStyle.left }"
>
<button
v-if="props.trigger === 'click'"
type="button"
class="absolute right-1.5 top-1.5 rounded p-1 text-gray-300 transition-colors hover:bg-white/10 hover:text-white"
aria-label="Close"
@click.stop="closeTooltip"
>
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<slot>{{ content }}</slot>
<div class="absolute -bottom-1 left-1/2 h-2 w-2 -translate-x-1/2 rotate-45 bg-gray-900 dark:bg-gray-800"></div>
</div>
</Teleport>
</div>
</template>
|