| <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" |
| > |
| |
| <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"> |
| <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> |
|
|