Skip to content

Commit 593e4b0

Browse files
Changed TooltipComponent into a Class Component
1 parent 1d50063 commit 593e4b0

File tree

1 file changed

+172
-157
lines changed

1 file changed

+172
-157
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as React from 'react';
22
import { createPortal } from 'react-dom';
33
import { debounce } from 'lodash';
4-
const { useState, useEffect, useRef, useMemo } = React;
54

65
interface TooltipProps {
76
content?: React.ReactNode;
@@ -15,180 +14,196 @@ interface TooltipState {
1514
zIndex: number;
1615
}
1716

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+
};
3423

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+
};
3841

39-
let x = lastMousePosition.current.x + 10;
40-
let y = lastMousePosition.current.y + 10;
42+
this.debouncedUpdate = debounce(this.updateState, 500);
43+
}
4144

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);
4549

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 });
4953

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+
}
5376

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+
}
9084

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+
}
97112

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 });
100119

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+
}
106123

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)'
114142
};
115143

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+
}
118162

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();
121166

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;
127169

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;
132172
}
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;
148176
}
149177

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;
153205
}
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+
});
175207
};
176208

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

Comments
 (0)