11<template >
22 <div
33 ref =" target"
4- class =" relative flex w-fit flex-col items-center text-typography-default"
4+ class =" flex w-fit flex-col items-center text-typography-default"
55 @mouseleave =" mouseleave"
66 @mouseenter =" mouseenter"
77 >
3232
3333<script lang="ts" setup>
3434import type { TooltipProps } from " ./types" ;
35- import { ref , watch , nextTick , type ComponentPublicInstance } from " vue" ;
35+ import { ref , watch , nextTick , onUnmounted , type ComponentPublicInstance } from " vue" ;
3636import { motion , AnimatePresence } from " motion-v" ;
3737
3838const props = withDefaults (defineProps <TooltipProps >(), {
@@ -43,17 +43,56 @@ const tooltip = ref(null as ComponentPublicInstance | null);
4343const target = ref (null as HTMLDivElement | null );
4444const tooltipStyle = ref <Record <string , string >>({ position: " fixed" , top: " -9999px" , left: " -9999px" });
4545const arrowStyle = ref <Record <string , string >>({});
46+
47+ let unsubscribeRotation: (() => void ) | null = null ;
48+ watch (() => props .rotateValue , (mv ) => {
49+ unsubscribeRotation ?.();
50+ unsubscribeRotation = null ;
51+ if (! mv ) return ;
52+ unsubscribeRotation = mv .on (" change" , (v ) => {
53+ const el = tooltip .value ?.$el as HTMLElement | undefined ;
54+ if (el ) {
55+ el .style .transformOrigin = " 50% calc(100% + 10px)" ;
56+ el .style .rotate = ` ${v }deg ` ;
57+ }
58+ });
59+ }, { immediate: true });
4660const isVisible = ref (false );
61+ const isHovered = ref (false );
62+
63+ const MIN_VISIBLE_MS = 1000 ;
64+
65+ let hideTimeout: ReturnType <typeof setTimeout > | null = null ;
4766
4867const mouseenter = () => {
68+ isHovered .value = true ;
69+ if (hideTimeout ) { clearTimeout (hideTimeout ); hideTimeout = null ; }
4970 isVisible .value = true ;
5071 nextTick (() => requestAnimationFrame (calculateTooltipPosition ));
5172};
5273
5374const mouseleave = () => {
54- isVisible .value = false ;
75+ isHovered .value = false ;
76+ if (! props .forceVisible && ! hideTimeout ) {
77+ isVisible .value = false ;
78+ }
5579};
5680
81+ watch (() => props .forceVisible , (val ) => {
82+ if (val ) {
83+ if (hideTimeout ) { clearTimeout (hideTimeout ); hideTimeout = null ; }
84+ isVisible .value = true ;
85+ nextTick (() => requestAnimationFrame (calculateTooltipPosition ));
86+ } else {
87+ if (hideTimeout ) { clearTimeout (hideTimeout ); hideTimeout = null ; }
88+ hideTimeout = setTimeout (() => { isVisible .value = false ; hideTimeout = null ; }, MIN_VISIBLE_MS );
89+ }
90+ }, { flush: ' sync' });
91+ onUnmounted (() => {
92+ unsubscribeRotation ?.();
93+ if (hideTimeout ) clearTimeout (hideTimeout );
94+ });
95+
5796
5897const calculateTooltipPosition = () => {
5998 const offset = 10 ; // Offset between the tooltip and the target element
@@ -98,9 +137,18 @@ const calculateTooltipPosition = () => {
98137 left: ` ${left ! }px ` ,
99138 position: " fixed"
100139 };
140+
141+ // Also set directly on the DOM element to avoid Vue's render-cycle lag
142+ // (important when called from rAF loops during CSS transitions)
143+ if (tooltipEl ) {
144+ tooltipEl .style .top = ` ${top ! }px ` ;
145+ tooltipEl .style .left = ` ${left ! }px ` ;
146+ }
101147};
102148
103149watch (() => [target .value , props .position ], calculateTooltipPosition );
150+
151+ defineExpose ({ recalculate: calculateTooltipPosition });
104152 </script >
105153
106154<style lang="scss" scoped>
0 commit comments