@@ -60,7 +60,7 @@ export const TooltipWrapper: React.FC<TooltipWrapperProps> = ({ inline = false,
6060interface TooltipProps {
6161 align ?: "left" | "center" | "right" ;
6262 width ?: "auto" | "wide" ;
63- position ?: "top" | "bottom" ;
63+ position ?: "top" | "bottom" | "left" | "right" ;
6464 children : React . ReactNode ;
6565 className ?: string ;
6666 interactive ?: boolean ;
@@ -109,52 +109,99 @@ export const Tooltip: React.FC<TooltipProps> = ({
109109 let left : number ;
110110 let finalPosition = position ;
111111 const gap = 8 ; // Gap between trigger and tooltip
112+ const isHorizontalPosition = position === "left" || position === "right" ;
112113
113- // Vertical positioning with collision detection
114- if ( position === "bottom" ) {
115- top = trigger . bottom + gap ;
116- // Check if tooltip would overflow bottom of viewport
117- if ( top + tooltip . height > viewportHeight ) {
118- // Flip to top
119- finalPosition = "top" ;
120- top = trigger . top - tooltip . height - gap ;
114+ if ( isHorizontalPosition ) {
115+ // Horizontal positioning (left/right of trigger)
116+ top = trigger . top + trigger . height / 2 - tooltip . height / 2 ;
117+
118+ if ( position === "left" ) {
119+ left = trigger . left - tooltip . width - gap ;
120+ // Check if tooltip would overflow left of viewport
121+ if ( left < 8 ) {
122+ finalPosition = "right" ;
123+ left = trigger . right + gap ;
124+ }
125+ } else {
126+ // position === "right"
127+ left = trigger . right + gap ;
128+ // Check if tooltip would overflow right of viewport
129+ if ( left + tooltip . width > viewportWidth - 8 ) {
130+ finalPosition = "left" ;
131+ left = trigger . left - tooltip . width - gap ;
132+ }
121133 }
134+
135+ // Vertical collision detection for horizontal tooltips
136+ top = Math . max ( 8 , Math . min ( viewportHeight - tooltip . height - 8 , top ) ) ;
122137 } else {
123- // position === "top"
124- top = trigger . top - tooltip . height - gap ;
125- // Check if tooltip would overflow top of viewport
126- if ( top < 0 ) {
127- // Flip to bottom
128- finalPosition = "bottom" ;
138+ // Vertical positioning (top/bottom of trigger) with collision detection
139+ if ( position === "bottom" ) {
129140 top = trigger . bottom + gap ;
141+ // Check if tooltip would overflow bottom of viewport
142+ if ( top + tooltip . height > viewportHeight ) {
143+ // Flip to top
144+ finalPosition = "top" ;
145+ top = trigger . top - tooltip . height - gap ;
146+ }
147+ } else {
148+ // position === "top"
149+ top = trigger . top - tooltip . height - gap ;
150+ // Check if tooltip would overflow top of viewport
151+ if ( top < 0 ) {
152+ // Flip to bottom
153+ finalPosition = "bottom" ;
154+ top = trigger . bottom + gap ;
155+ }
130156 }
131- }
132157
133- // Horizontal positioning based on align
134- if ( align === "left" ) {
135- left = trigger . left ;
136- } else if ( align === "right" ) {
137- left = trigger . right - tooltip . width ;
138- } else {
139- // center
140- left = trigger . left + trigger . width / 2 - tooltip . width / 2 ;
158+ // Horizontal positioning based on align
159+ if ( align === "left" ) {
160+ left = trigger . left ;
161+ } else if ( align === "right" ) {
162+ left = trigger . right - tooltip . width ;
163+ } else {
164+ // center
165+ left = trigger . left + trigger . width / 2 - tooltip . width / 2 ;
166+ }
167+
168+ // Horizontal collision detection
169+ const minLeft = 8 ; // Min distance from viewport edge
170+ const maxLeft = viewportWidth - tooltip . width - 8 ;
171+ left = Math . max ( minLeft , Math . min ( maxLeft , left ) ) ;
141172 }
142173
143- // Horizontal collision detection
144- const minLeft = 8 ; // Min distance from viewport edge
145- const maxLeft = viewportWidth - tooltip . width - 8 ;
146- const originalLeft = left ;
147- left = Math . max ( minLeft , Math . min ( maxLeft , left ) ) ;
148-
149- // Calculate arrow position - stays aligned with trigger even if tooltip shifts
150- let arrowLeft : number ;
151- if ( align === "center" ) {
152- arrowLeft = trigger . left + trigger . width / 2 - left ;
153- } else if ( align === "right" ) {
154- arrowLeft = tooltip . width - 15 ; // 10px from right + 5px arrow width
174+ // Calculate arrow style based on final position
175+ const arrowStyle : React . CSSProperties = { } ;
176+ const finalIsHorizontal = finalPosition === "left" || finalPosition === "right" ;
177+
178+ if ( finalIsHorizontal ) {
179+ // Arrow on left or right side of tooltip, vertically centered
180+ arrowStyle . top = "50%" ;
181+ arrowStyle . transform = "translateY(-50%)" ;
182+ if ( finalPosition === "left" ) {
183+ arrowStyle . left = "100%" ;
184+ arrowStyle . borderColor = "transparent transparent transparent #2d2d30" ;
185+ } else {
186+ arrowStyle . right = "100%" ;
187+ arrowStyle . borderColor = "transparent #2d2d30 transparent transparent" ;
188+ }
155189 } else {
156- // left
157- arrowLeft = Math . max ( 10 , Math . min ( originalLeft - left + 10 , tooltip . width - 15 ) ) ;
190+ // Arrow on top or bottom of tooltip
191+ let arrowLeft : number ;
192+ if ( align === "center" ) {
193+ arrowLeft = trigger . left + trigger . width / 2 - left ;
194+ } else if ( align === "right" ) {
195+ arrowLeft = tooltip . width - 15 ;
196+ } else {
197+ arrowLeft = Math . max ( 10 , Math . min ( trigger . left - left + 10 , tooltip . width - 15 ) ) ;
198+ }
199+ arrowStyle . left = `${ arrowLeft } px` ;
200+ arrowStyle [ finalPosition === "bottom" ? "bottom" : "top" ] = "100%" ;
201+ arrowStyle . borderColor =
202+ finalPosition === "bottom"
203+ ? "transparent transparent #2d2d30 transparent"
204+ : "#2d2d30 transparent transparent transparent" ;
158205 }
159206
160207 // Update all state atomically to prevent flashing
@@ -166,14 +213,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
166213 visibility : "visible" ,
167214 opacity : 1 ,
168215 } ,
169- arrowStyle : {
170- left : `${ arrowLeft } px` ,
171- [ finalPosition === "bottom" ? "bottom" : "top" ] : "100%" ,
172- borderColor :
173- finalPosition === "bottom"
174- ? "transparent transparent #2d2d30 transparent"
175- : "#2d2d30 transparent transparent transparent" ,
176- } ,
216+ arrowStyle,
177217 isPositioned : true ,
178218 } ) ;
179219 } ;
0 commit comments