1+ import { createSignal , JSX , Show } from "solid-js" ;
2+ import QuestionMarkIcon from "../icon/QuestionMarkIcon" ;
3+
4+ export enum TooltipAlignment {
5+ Center = "center" ,
6+ Left = "left" ,
7+ Right = "right" ,
8+ }
9+
10+ interface TooltipProps {
11+ text : string ;
12+ children ?: JSX . Element ;
13+ }
14+
15+ export default function QuestionTooltip ( props : TooltipProps ) {
16+ const [ isVisible , setIsVisible ] = createSignal ( false ) ;
17+ const [ position , setPosition ] = createSignal < {
18+ x : number ;
19+ align : TooltipAlignment ;
20+ containerWidth : number ;
21+ } > ( { x : 0 , align : TooltipAlignment . Center , containerWidth : 0 } ) ;
22+
23+ let containerRef : HTMLDivElement | undefined ;
24+
25+ const determineAlignment = ( rect : DOMRect ) : TooltipAlignment => {
26+ const tooltipWidth = 256 ; // w-64 is 16rem = 256px
27+
28+ // Calculate potential left and right bounds if centered
29+ const centerLeft = rect . left + rect . width / 2 - tooltipWidth / 2 ;
30+ const centerRight = centerLeft + tooltipWidth ;
31+
32+ const viewportWidth = window . innerWidth ;
33+ const padding = 16 ; // 16px safety padding from screen edges
34+
35+ if ( centerLeft < padding ) {
36+ return TooltipAlignment . Left ;
37+ } else if ( centerRight > viewportWidth - padding ) {
38+ return TooltipAlignment . Right ;
39+ } else {
40+ return TooltipAlignment . Center ;
41+ }
42+ } ;
43+
44+ const handleMouseEnter = ( ) => {
45+ if ( containerRef ) {
46+ const rect = containerRef . getBoundingClientRect ( ) ;
47+ const align = determineAlignment ( rect ) ;
48+ setPosition ( { x : 0 , align, containerWidth : rect . width } ) ;
49+ }
50+ setIsVisible ( true ) ;
51+ } ;
52+
53+ const handleMouseLeave = ( ) => setIsVisible ( false ) ;
54+
55+ return (
56+ < div
57+ ref = { containerRef }
58+ class = "relative inline-flex items-center justify-center cursor-help"
59+ onMouseEnter = { handleMouseEnter }
60+ onMouseLeave = { handleMouseLeave }
61+ onFocus = { handleMouseEnter }
62+ onBlur = { handleMouseLeave }
63+ tabIndex = { 0 }
64+ aria-label = "More information"
65+ >
66+ < Show
67+ when = { props . children }
68+ fallback = {
69+ < QuestionMarkIcon class = "size-5 text-gray-500 hover:text-gray-700 transition-colors" />
70+ }
71+ >
72+ { props . children }
73+ </ Show >
74+
75+ < Show when = { isVisible ( ) } >
76+ < div
77+ class = { `absolute z-50 w-64 p-3 mt-2 text-sm text-gray-800 bg-white border border-gray-200 rounded-lg shadow-lg top-full pointer-events-none fade-in ${
78+ position ( ) . align === TooltipAlignment . Center
79+ ? "left-1/2 -translate-x-1/2"
80+ : position ( ) . align === TooltipAlignment . Left
81+ ? "left-0"
82+ : "right-0"
83+ } `}
84+ >
85+ { props . text }
86+ { /* Decorative arrow pointing up */ }
87+ < div
88+ class = { `absolute w-3 h-3 bg-white border-t border-l border-gray-200 rotate-45 -top-[7px] ${
89+ position ( ) . align === TooltipAlignment . Center
90+ ? "left-1/2 -translate-x-1/2"
91+ : ""
92+ } `}
93+ style = {
94+ position ( ) . align === TooltipAlignment . Left
95+ ? { left : `${ position ( ) . containerWidth / 2 - 6 } px` }
96+ : position ( ) . align === TooltipAlignment . Right
97+ ? { right : `${ position ( ) . containerWidth / 2 - 6 } px` }
98+ : { }
99+ }
100+ />
101+ </ div >
102+ </ Show >
103+ </ div >
104+ ) ;
105+ }
0 commit comments