Skip to content

Commit c9f3ef5

Browse files
committed
feat: implement functional tooltips with proper positioning
- Created a new better-tooltip component with proper hover and positioning - Uses React context and portals for best implementation - Replaces the dummy tooltip placeholders with actual working components - Maintains API compatibility with Radix UI tooltips
1 parent b184854 commit c9f3ef5

File tree

2 files changed

+158
-19
lines changed

2 files changed

+158
-19
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import React, { useState, useRef, useEffect } from 'react';
2+
import { createPortal } from 'react-dom';
3+
import { cn } from '@/lib/utils';
4+
5+
// Context for transferring state between components
6+
const TooltipContext = React.createContext<{
7+
open: boolean;
8+
setOpen: (open: boolean) => void;
9+
content: React.ReactNode;
10+
setContent: (content: React.ReactNode) => void;
11+
triggerRef: React.RefObject<HTMLDivElement>;
12+
}>({
13+
open: false,
14+
setOpen: () => {},
15+
content: null,
16+
setContent: () => {},
17+
triggerRef: { current: null },
18+
});
19+
20+
// Provider component
21+
export const TooltipProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
22+
return <>{children}</>;
23+
};
24+
25+
// Main tooltip component
26+
export const Tooltip: React.FC<{ children: React.ReactNode }> = ({ children }) => {
27+
const [open, setOpen] = useState(false);
28+
const [content, setContent] = useState<React.ReactNode>(null);
29+
const triggerRef = useRef<HTMLDivElement>(null);
30+
31+
// Parse children to find TooltipTrigger and TooltipContent
32+
let triggerElement: React.ReactElement | null = null;
33+
let contentElement: React.ReactElement | null = null;
34+
35+
React.Children.forEach(children, (child) => {
36+
if (!React.isValidElement(child)) return;
37+
38+
if (child.type === TooltipTrigger) {
39+
triggerElement = child;
40+
} else if (child.type === TooltipContent) {
41+
contentElement = child;
42+
}
43+
});
44+
45+
// Set content from TooltipContent element
46+
useEffect(() => {
47+
if (contentElement && contentElement.props.children) {
48+
setContent(contentElement.props.children);
49+
}
50+
}, [contentElement]);
51+
52+
return (
53+
<TooltipContext.Provider value={{ open, setOpen, content, setContent, triggerRef }}>
54+
{triggerElement}
55+
{open && content && <TooltipPortal />}
56+
</TooltipContext.Provider>
57+
);
58+
};
59+
60+
// Trigger component
61+
export const TooltipTrigger: React.FC<{
62+
children: React.ReactNode;
63+
asChild?: boolean;
64+
}> = ({ children }) => {
65+
const { setOpen, triggerRef } = React.useContext(TooltipContext);
66+
67+
return (
68+
<div
69+
ref={triggerRef}
70+
onMouseEnter={() => setOpen(true)}
71+
onMouseLeave={() => setOpen(false)}
72+
onFocus={() => setOpen(true)}
73+
onBlur={() => setOpen(false)}
74+
className="inline-block"
75+
>
76+
{children}
77+
</div>
78+
);
79+
};
80+
81+
// Content component - just stores content in context
82+
export const TooltipContent: React.FC<{
83+
children: React.ReactNode;
84+
className?: string;
85+
sideOffset?: number;
86+
}> = (
87+
// We're intentionally not using the props here but TooltipContent element acts as a data container
88+
{ children: _children }
89+
) => {
90+
return null; // This doesn't render directly, content is passed to portal via context
91+
};
92+
93+
// Portal component that actually renders the tooltip
94+
const TooltipPortal: React.FC = () => {
95+
const { open, content, triggerRef } = React.useContext(TooltipContext);
96+
const tooltipRef = useRef<HTMLDivElement>(null);
97+
const [position, setPosition] = useState({ top: 0, left: 0 });
98+
99+
// Calculate position based on trigger
100+
useEffect(() => {
101+
if (!triggerRef.current || !tooltipRef.current) return;
102+
103+
const updatePosition = () => {
104+
const triggerRect = triggerRef.current!.getBoundingClientRect();
105+
const tooltipRect = tooltipRef.current!.getBoundingClientRect();
106+
107+
// Default positioning (above the element)
108+
const top = triggerRect.top - tooltipRect.height - 5;
109+
const left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2;
110+
111+
// Adjust for scroll position
112+
setPosition({
113+
top: top + window.scrollY,
114+
left: left + window.scrollX
115+
});
116+
};
117+
118+
updatePosition();
119+
window.addEventListener('resize', updatePosition);
120+
window.addEventListener('scroll', updatePosition);
121+
122+
return () => {
123+
window.removeEventListener('resize', updatePosition);
124+
window.removeEventListener('scroll', updatePosition);
125+
};
126+
}, [open, triggerRef.current, tooltipRef.current]);
127+
128+
if (!open) return null;
129+
130+
return createPortal(
131+
<div
132+
ref={tooltipRef}
133+
className={cn(
134+
'z-50 overflow-hidden rounded-md border bg-white px-3 py-1.5 text-sm shadow-md',
135+
'absolute'
136+
)}
137+
style={{
138+
top: `${position.top}px`,
139+
left: `${position.left}px`,
140+
zIndex: 9999,
141+
}}
142+
>
143+
{content}
144+
</div>,
145+
document.body
146+
);
147+
};

src/components/ui/radix-compatibility.tsx

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ import {
1515
} from './simple-dialog';
1616

1717
import {
18-
SimpleTooltipProvider,
19-
} from './simple-tooltip';
18+
Tooltip as BetterTooltip,
19+
TooltipTrigger as BetterTooltipTrigger,
20+
TooltipContent as BetterTooltipContent,
21+
TooltipProvider as BetterTooltipProvider
22+
} from './better-tooltip';
2023

2124
import { SimpleLabel } from './simple-label';
2225
import { Slot } from './simple-slot';
@@ -65,18 +68,7 @@ const PopoverContent: React.FC<{children: React.ReactNode, className?: string}>
6568
return <div className={className}>{children}</div>;
6669
};
6770

68-
// Simple Tooltip implementation
69-
const Tooltip: React.FC<{children: React.ReactNode}> = ({ children }) => {
70-
return <>{children}</>;
71-
};
72-
73-
const TooltipTrigger: React.FC<{children: React.ReactNode, asChild?: boolean}> = ({ children }) => {
74-
return <>{children}</>;
75-
};
76-
77-
const TooltipContent: React.FC<{children: React.ReactNode, className?: string}> = ({ children, className }) => {
78-
return <div className={className}>{children}</div>;
79-
};
71+
// Using our better tooltip implementation instead of the dummy ones
8072

8173
// Export all components with Radix-compatible names
8274
export {
@@ -92,11 +84,11 @@ export {
9284
SimpleDialogOverlay as DialogOverlay,
9385
SimpleDialogPortal as DialogPortal,
9486

95-
// Tooltip components
96-
SimpleTooltipProvider as TooltipProvider,
97-
Tooltip,
98-
TooltipTrigger,
99-
TooltipContent,
87+
// Tooltip components - using our fully-functional implementation
88+
BetterTooltipProvider as TooltipProvider,
89+
BetterTooltip as Tooltip,
90+
BetterTooltipTrigger as TooltipTrigger,
91+
BetterTooltipContent as TooltipContent,
10092

10193
// Popover components
10294
Popover,

0 commit comments

Comments
 (0)