Skip to content

Commit ce6c04b

Browse files
authored
Merge pull request #304 from CodeForPhilly/tooltip-component
Tooltip component
2 parents b8efd16 + 0889547 commit ce6c04b

File tree

2 files changed

+123
-0
lines changed

2 files changed

+123
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export default function QuestionMarkIcon(props: { class?: string }) {
2+
return (
3+
<svg
4+
xmlns="http://www.w3.org/2000/Url"
5+
fill="none"
6+
viewBox="0 0 24 24"
7+
strokeWidth={1.5}
8+
stroke="currentColor"
9+
class={props.class || "size-6"}
10+
>
11+
<path
12+
strokeLinecap="round"
13+
strokeLinejoin="round"
14+
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"
15+
/>
16+
</svg>
17+
);
18+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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

Comments
 (0)