1
1
import * as React from 'react' ;
2
2
import { createPortal } from 'react-dom' ;
3
3
import { debounce } from 'lodash' ;
4
- const { useState, useEffect, useRef, useMemo } = React ;
5
4
6
5
interface TooltipProps {
7
6
content ?: React . ReactNode ;
@@ -15,180 +14,196 @@ interface TooltipState {
15
14
zIndex : number ;
16
15
}
17
16
18
- export const TooltipComponent : React . FC < TooltipProps > = ( { content = '⏳' , visible = false , fadeTransition = 500 } ) => {
19
- // eslint-disable-next-line no-null/no-null
20
- const tooltipRef = useRef < HTMLDivElement > ( null ) ;
21
- const lastMousePosition = useRef ( { x : 0 , y : 0 } ) ;
22
- const [ state , setState ] = useState < TooltipState > ( {
23
- content,
24
- visible,
25
- zIndex : visible ? 99999 : - 99999
26
- } ) ;
27
- const isMouseOverRef = useRef ( false ) ;
28
-
29
- // Calculate position based on current mouse position and tooltip dimensions
30
- const calculateAndSetPosition = ( ) => {
31
- if ( ! tooltipRef . current ) {
32
- return ;
33
- }
17
+ export class TooltipComponent extends React . Component < TooltipProps , TooltipState > {
18
+ public static defaultProps = {
19
+ content : '⏳' ,
20
+ visible : false ,
21
+ fadeTransition : 500
22
+ } ;
34
23
35
- const viewportWidth = window . innerWidth ;
36
- const viewportHeight = window . innerHeight ;
37
- const tooltipRect = tooltipRef . current . getBoundingClientRect ( ) ;
24
+ private tooltipRef : React . RefObject < HTMLDivElement > ;
25
+ private lastMousePosition : { x : number ; y : number } ;
26
+ private isMouseOver : boolean ;
27
+ private debouncedUpdate : ReturnType < typeof debounce > ;
28
+
29
+ constructor ( props : TooltipProps ) {
30
+ super ( props ) ;
31
+
32
+ this . tooltipRef = React . createRef ( ) ;
33
+ this . lastMousePosition = { x : 0 , y : 0 } ;
34
+ this . isMouseOver = false ;
35
+
36
+ this . state = {
37
+ content : props . content ,
38
+ visible : props . visible || false ,
39
+ zIndex : ( props . visible || false ) ? 99999 : - 99999
40
+ } ;
38
41
39
- let x = lastMousePosition . current . x + 10 ;
40
- let y = lastMousePosition . current . y + 10 ;
42
+ this . debouncedUpdate = debounce ( this . updateState , 500 ) ;
43
+ }
41
44
42
- if ( x + tooltipRect . width > viewportWidth ) {
43
- x = lastMousePosition . current . x - tooltipRect . width - 10 ;
44
- }
45
+ public componentDidMount ( ) : void {
46
+ // Track mouse position and detect when mouse leaves viewport
47
+ document . addEventListener ( 'mousemove' , this . handleMouseMove ) ;
48
+ document . addEventListener ( 'mouseleave' , this . handleMouseLeave ) ;
45
49
46
- if ( y + tooltipRect . height > viewportHeight ) {
47
- y = lastMousePosition . current . y - tooltipRect . height - 10 ;
48
- }
50
+ // Hide tooltip when scrolling (use capture to catch all scroll events)
51
+ window . addEventListener ( 'scroll' , this . handleScroll , { capture : true } ) ;
52
+ document . addEventListener ( 'scroll' , this . handleScroll , { capture : true } ) ;
49
53
50
- tooltipRef . current . style . left = `${ x } px` ;
51
- tooltipRef . current . style . top = `${ y } px` ;
52
- } ;
54
+ // Handle transition end for z-index updates
55
+ if ( this . tooltipRef . current ) {
56
+ this . tooltipRef . current . addEventListener ( 'transitionend' , this . handleTransitionEnd ) ;
57
+ }
58
+ }
59
+
60
+
61
+ private handleMouseMove = ( e : MouseEvent ) : void => {
62
+ this . lastMousePosition = { x : e . clientX , y : e . clientY } ;
63
+ }
64
+
65
+ private handleMouseLeave = ( e : MouseEvent ) : void => {
66
+ // Check if the mouse has actually left the viewport
67
+ if (
68
+ e . clientY <= 0 ||
69
+ e . clientY >= window . innerHeight ||
70
+ e . clientX <= 0 ||
71
+ e . clientX >= window . innerWidth
72
+ ) {
73
+ this . setState ( { visible : false } ) ;
74
+ }
75
+ }
53
76
54
- const debouncedUpdate = useMemo (
55
- ( ) =>
56
- debounce ( ( newState : Partial < TooltipState > ) => {
57
- setState ( prevState => {
58
- const updatedState = { ...prevState , ...newState } ;
59
-
60
- const weAreHidingTooltip = prevState . visible === true && newState . visible === false ;
61
- const weAreHoveringOverTooltip = isMouseOverRef . current ;
62
-
63
- if ( weAreHoveringOverTooltip ) {
64
- debouncedUpdate . cancel ( ) ;
65
- return prevState ;
66
- } else if ( weAreHidingTooltip ) {
67
- // Don't update the content
68
- updatedState . content = prevState . content ;
69
- // Keep z-index high during fade out
70
- updatedState . zIndex = prevState . zIndex ;
71
- return updatedState ;
72
- } else {
73
- calculateAndSetPosition ( ) ;
74
- // Update z-index immediately when showing
75
- if ( newState . visible ) {
76
- updatedState . zIndex = 99999 ;
77
- }
78
- return updatedState ;
79
- }
80
- } ) ;
81
- } , 500 ) ,
82
- [ ]
83
- ) ;
84
-
85
- // Track mouse position and detect when mouse leaves viewport
86
- useEffect ( ( ) => {
87
- const handleMouseMove = ( e : MouseEvent ) => {
88
- lastMousePosition . current = { x : e . clientX , y : e . clientY } ;
89
- } ;
77
+ private handleScroll = ( ) : void => {
78
+ if ( this . state . visible ) {
79
+ this . setState ( {
80
+ visible : false
81
+ } ) ;
82
+ }
83
+ }
90
84
91
- const handleMouseLeave = ( e : MouseEvent ) => {
92
- // Check if the mouse has actually left the viewport
93
- if ( e . clientY <= 0 || e . clientY >= window . innerHeight || e . clientX <= 0 || e . clientX >= window . innerWidth ) {
94
- setState ( prev => ( { ...prev , visible : false } ) ) ;
95
- }
96
- } ;
85
+ private handleTransitionEnd = ( e : TransitionEvent ) : void => {
86
+ if ( e . propertyName === 'opacity' && ! this . state . visible ) {
87
+ this . setState ( { zIndex : - 99999 } ) ;
88
+ }
89
+ }
90
+
91
+ private handleTooltipMouseEnter = ( ) : void => {
92
+ this . isMouseOver = true ;
93
+ }
94
+
95
+ private handleTooltipMouseLeave = ( ) : void => {
96
+ this . isMouseOver = false ;
97
+ this . setState ( { visible : false } ) ;
98
+ }
99
+
100
+ public componentDidUpdate ( prevProps : TooltipProps ) : void {
101
+ // Handle content and visibility changes
102
+ if (
103
+ ! this . isMouseOver &&
104
+ ( prevProps . content !== this . props . content || prevProps . visible !== this . props . visible )
105
+ ) {
106
+ this . debouncedUpdate ( {
107
+ content : this . props . content ,
108
+ visible : this . props . visible
109
+ } ) ;
110
+ }
111
+ }
97
112
98
- document . addEventListener ( 'mousemove' , handleMouseMove ) ;
99
- document . addEventListener ( 'mouseleave' , handleMouseLeave ) ;
113
+ public componentWillUnmount ( ) : void {
114
+ // Cleanup event listeners
115
+ document . removeEventListener ( 'mousemove' , this . handleMouseMove ) ;
116
+ document . removeEventListener ( 'mouseleave' , this . handleMouseLeave ) ;
117
+ window . removeEventListener ( 'scroll' , this . handleScroll , { capture : true } ) ;
118
+ document . removeEventListener ( 'scroll' , this . handleScroll , { capture : true } ) ;
100
119
101
- return ( ) => {
102
- document . removeEventListener ( 'mousemove' , handleMouseMove ) ;
103
- document . removeEventListener ( 'mouseleave' , handleMouseLeave ) ;
104
- } ;
105
- } , [ debouncedUpdate ] ) ;
120
+ if ( this . tooltipRef . current ) {
121
+ this . tooltipRef . current . removeEventListener ( 'transitionend' , this . handleTransitionEnd ) ;
122
+ }
106
123
107
- // Hide tooltip when scrolling
108
- useEffect ( ( ) => {
109
- const handleScroll = ( ) => {
110
- setState ( prev => ( {
111
- ...prev ,
112
- visible : false
113
- } ) ) ;
124
+ // Cancel debounced function
125
+ this . debouncedUpdate . cancel ( ) ;
126
+ }
127
+
128
+ public render ( ) : React . ReactPortal {
129
+ const tooltipStyle : React . CSSProperties = {
130
+ position : 'fixed' ,
131
+ pointerEvents : 'auto' ,
132
+ opacity : this . state . visible ? 1 : 0 ,
133
+ transition : `opacity ${ this . props . fadeTransition } ms ease-in-out` ,
134
+ zIndex : this . state . zIndex ,
135
+ backgroundColor : '#337AB7' ,
136
+ fontSize : '13px' ,
137
+ color : '#fff' ,
138
+ padding : '9px 11px' ,
139
+ border : '1px solid transparent' ,
140
+ borderRadius : '3px' ,
141
+ boxShadow : '0 2px 4px rgba(0,0,0,0.2)'
114
142
} ;
115
143
116
- // Use capture to catch all scroll events before they might be stopped
117
- const options = { capture : true } ;
144
+ return createPortal (
145
+ < div
146
+ ref = { this . tooltipRef }
147
+ className = "trace-compass-tooltip"
148
+ style = { tooltipStyle }
149
+ onMouseEnter = { this . handleTooltipMouseEnter }
150
+ onMouseLeave = { this . handleTooltipMouseLeave }
151
+ >
152
+ { this . state . content }
153
+ </ div > ,
154
+ document . body
155
+ ) ;
156
+ }
157
+
158
+ private calculateAndSetPosition = ( ) : void => {
159
+ if ( ! this . tooltipRef . current ) {
160
+ return ;
161
+ }
118
162
119
- window . addEventListener ( 'scroll' , handleScroll , options ) ;
120
- document . addEventListener ( 'scroll' , handleScroll , options ) ;
163
+ const viewportWidth = window . innerWidth ;
164
+ const viewportHeight = window . innerHeight ;
165
+ const tooltipRect = this . tooltipRef . current . getBoundingClientRect ( ) ;
121
166
122
- return ( ) => {
123
- window . removeEventListener ( 'scroll' , handleScroll , options ) ;
124
- document . removeEventListener ( 'scroll' , handleScroll , options ) ;
125
- } ;
126
- } , [ ] ) ;
167
+ let x = this . lastMousePosition . x + 10 ;
168
+ let y = this . lastMousePosition . y + 10 ;
127
169
128
- // Handle content and visibility changes
129
- useEffect ( ( ) => {
130
- if ( ! isMouseOverRef . current ) {
131
- debouncedUpdate ( { content, visible } ) ;
170
+ if ( x + tooltipRect . width > viewportWidth ) {
171
+ x = this . lastMousePosition . x - tooltipRect . width - 10 ;
132
172
}
133
- } , [ content , visible , debouncedUpdate ] ) ;
134
-
135
- // Cleanup debounced function
136
- useEffect (
137
- ( ) => ( ) => {
138
- debouncedUpdate . cancel ( ) ;
139
- } ,
140
- [ debouncedUpdate ]
141
- ) ;
142
-
143
- // Handle opacity transition end
144
- useEffect ( ( ) => {
145
- const tooltip = tooltipRef . current ;
146
- if ( ! tooltip ) {
147
- return ;
173
+
174
+ if ( y + tooltipRect . height > viewportHeight ) {
175
+ y = this . lastMousePosition . y - tooltipRect . height - 10 ;
148
176
}
149
177
150
- const handleTransitionEnd = ( e : TransitionEvent ) => {
151
- if ( e . propertyName === 'opacity' && ! state . visible ) {
152
- setState ( prev => ( { ...prev , zIndex : - 99999 } ) ) ;
178
+ this . tooltipRef . current . style . left = `${ x } px` ;
179
+ this . tooltipRef . current . style . top = `${ y } px` ;
180
+ }
181
+
182
+ private updateState = ( newState : Partial < TooltipState > ) : void => {
183
+ this . setState ( ( prevState ) => {
184
+ const updatedState = { ...prevState , ...newState } ;
185
+
186
+ // Hide the tooltips when visibility goes form true => false
187
+ const weAreHidingTooltip = prevState . visible === true && newState . visible === false ;
188
+
189
+ if ( this . isMouseOver ) {
190
+ this . debouncedUpdate . cancel ( ) ;
191
+ return prevState ;
192
+ } else if ( weAreHidingTooltip ) {
193
+ // Don't update the content
194
+ updatedState . content = prevState . content ;
195
+ // Keep z-index high during fade out
196
+ updatedState . zIndex = prevState . zIndex ;
197
+ return updatedState ;
198
+ } else {
199
+ this . calculateAndSetPosition ( ) ;
200
+ // Update z-index immediately when showing
201
+ if ( newState . visible ) {
202
+ updatedState . zIndex = 99999 ;
203
+ }
204
+ return updatedState ;
153
205
}
154
- } ;
155
-
156
- tooltip . addEventListener ( 'transitionend' , handleTransitionEnd ) ;
157
- return ( ) => {
158
- tooltip . removeEventListener ( 'transitionend' , handleTransitionEnd ) ;
159
- } ;
160
- } , [ state . visible ] ) ;
161
-
162
- const tooltipStyle : React . CSSProperties = {
163
- position : 'fixed' ,
164
- pointerEvents : 'auto' ,
165
- opacity : state . visible ? 1 : 0 ,
166
- transition : `opacity ${ fadeTransition } ms ease-in-out` ,
167
- zIndex : state . zIndex ,
168
- backgroundColor : '#337AB7' ,
169
- fontSize : '13px' ,
170
- color : '#fff' ,
171
- padding : '9px 11px' ,
172
- border : '1px solid transparent' ,
173
- borderRadius : '3px' ,
174
- boxShadow : '0 2px 4px rgba(0,0,0,0.2)'
206
+ } ) ;
175
207
} ;
176
208
177
- return createPortal (
178
- < div
179
- ref = { tooltipRef }
180
- className = "trace-compass-tooltip"
181
- style = { tooltipStyle }
182
- onMouseEnter = { ( ) => {
183
- isMouseOverRef . current = true ;
184
- } }
185
- onMouseLeave = { ( ) => {
186
- isMouseOverRef . current = false ;
187
- setState ( prev => ( { ...prev , visible : false } ) ) ;
188
- } }
189
- >
190
- { state . content }
191
- </ div > ,
192
- document . body
193
- ) ;
194
- } ;
209
+ }
0 commit comments