1- import { createSignal , JSX , Show } from "solid-js" ;
1+ import { createSignal , JSX , onCleanup , Show } from "solid-js" ;
22import QuestionMarkIcon from "../icon/QuestionMarkIcon" ;
33
44export enum TooltipAlignment {
@@ -8,8 +8,10 @@ export enum TooltipAlignment {
88}
99
1010interface TooltipProps {
11- text : string ;
12- children ?: JSX . Element ;
11+ /** Optional trigger label – defaults to the question-mark icon */
12+ label ?: JSX . Element ;
13+ /** Popover content (can include links, rich JSX, etc.) */
14+ children : JSX . Element ;
1315}
1416
1517export default function QuestionTooltip ( props : TooltipProps ) {
@@ -21,6 +23,12 @@ export default function QuestionTooltip(props: TooltipProps) {
2123 } > ( { x : 0 , align : TooltipAlignment . Center , containerWidth : 0 } ) ;
2224
2325 let containerRef : HTMLDivElement | undefined ;
26+ let hideTimeout : ReturnType < typeof setTimeout > | undefined ;
27+
28+ // Clean up any pending timeout when the component unmounts
29+ onCleanup ( ( ) => {
30+ if ( hideTimeout ) clearTimeout ( hideTimeout ) ;
31+ } ) ;
2432
2533 const determineAlignment = ( rect : DOMRect ) : TooltipAlignment => {
2634 const tooltipWidth = 256 ; // w-64 is 16rem = 256px
@@ -41,7 +49,11 @@ export default function QuestionTooltip(props: TooltipProps) {
4149 }
4250 } ;
4351
44- const handleMouseEnter = ( ) => {
52+ const show = ( ) => {
53+ if ( hideTimeout ) {
54+ clearTimeout ( hideTimeout ) ;
55+ hideTimeout = undefined ;
56+ }
4557 if ( containerRef ) {
4658 const rect = containerRef . getBoundingClientRect ( ) ;
4759 const align = determineAlignment ( rect ) ;
@@ -50,39 +62,42 @@ export default function QuestionTooltip(props: TooltipProps) {
5062 setIsVisible ( true ) ;
5163 } ;
5264
53- const handleMouseLeave = ( ) => setIsVisible ( false ) ;
65+ const hide = ( ) => {
66+ // Small delay so the user can move their cursor into the popover
67+ hideTimeout = setTimeout ( ( ) => setIsVisible ( false ) , 150 ) ;
68+ } ;
5469
5570 return (
5671 < div
5772 ref = { containerRef }
5873 class = "relative inline-flex items-center justify-center cursor-help"
59- onMouseEnter = { handleMouseEnter }
60- onMouseLeave = { handleMouseLeave }
61- onFocus = { handleMouseEnter }
62- onBlur = { handleMouseLeave }
74+ onMouseEnter = { show }
75+ onMouseLeave = { hide }
76+ onFocusIn = { show }
77+ onFocusOut = { ( e : FocusEvent ) => {
78+ // Only hide if focus is leaving the entire tooltip container
79+ if ( ! containerRef ?. contains ( e . relatedTarget as Node ) ) {
80+ hide ( ) ;
81+ }
82+ } }
6383 tabIndex = { 0 }
6484 aria-label = "More information"
6585 >
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 >
86+ { props . label ?? (
87+ < QuestionMarkIcon class = "size-5 text-gray-500 hover:text-gray-700 transition-colors" />
88+ ) }
7489
7590 < Show when = { isVisible ( ) } >
7691 < 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 ${
92+ 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 fade-in ${
7893 position ( ) . align === TooltipAlignment . Center
7994 ? "left-1/2 -translate-x-1/2"
8095 : position ( ) . align === TooltipAlignment . Left
8196 ? "left-0"
8297 : "right-0"
8398 } `}
8499 >
85- { props . text }
100+ { props . children }
86101 { /* Decorative arrow pointing up */ }
87102 < div
88103 class = { `absolute w-3 h-3 bg-white border-t border-l border-gray-200 rotate-45 -top-[7px] ${
0 commit comments