Skip to content

Commit abcc789

Browse files
authored
Merge pull request #351 from CodeForPhilly/Add-links-to-tooltip-component
Refactor: Rename QuestionTooltip to Tooltip with interactive popover support
2 parents 04d24ef + e26f1c9 commit abcc789

File tree

1 file changed

+34
-19
lines changed

1 file changed

+34
-19
lines changed

builder-frontend/src/components/shared/QuestionTooltip.tsx renamed to builder-frontend/src/components/shared/Tooltip.tsx

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createSignal, JSX, Show } from "solid-js";
1+
import { createSignal, JSX, onCleanup, Show } from "solid-js";
22
import QuestionMarkIcon from "../icon/QuestionMarkIcon";
33

44
export enum TooltipAlignment {
@@ -8,8 +8,10 @@ export enum TooltipAlignment {
88
}
99

1010
interface 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

1517
export 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

Comments
 (0)