Skip to content

Commit 76a877d

Browse files
Changed TooltipComponent into a Class Component
1 parent 1d50063 commit 76a877d

File tree

1 file changed

+162
-154
lines changed

1 file changed

+162
-154
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,189 @@ 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;
17+
export class TooltipComponent extends React.Component<TooltipProps, TooltipState> {
18+
public static defaultProps = {
19+
content: '⏳',
20+
visible: false,
21+
fadeTransition: 500
22+
};
23+
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+
};
41+
42+
this.debouncedUpdate = debounce(this.updateState, 500);
43+
}
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);
49+
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 });
53+
54+
// Handle transition end for z-index updates
55+
if (this.tooltipRef.current) {
56+
this.tooltipRef.current.addEventListener('transitionend', this.handleTransitionEnd);
3357
}
58+
}
3459

35-
const viewportWidth = window.innerWidth;
36-
const viewportHeight = window.innerHeight;
37-
const tooltipRect = tooltipRef.current.getBoundingClientRect();
60+
private handleMouseMove = (e: MouseEvent): void => {
61+
this.lastMousePosition = { x: e.clientX, y: e.clientY };
62+
};
3863

39-
let x = lastMousePosition.current.x + 10;
40-
let y = lastMousePosition.current.y + 10;
64+
private handleMouseLeave = (e: MouseEvent): void => {
65+
// Check if the mouse has actually left the viewport
66+
if (e.clientY <= 0 || e.clientY >= window.innerHeight || e.clientX <= 0 || e.clientX >= window.innerWidth) {
67+
this.setState({ visible: false });
68+
}
69+
};
4170

42-
if (x + tooltipRect.width > viewportWidth) {
43-
x = lastMousePosition.current.x - tooltipRect.width - 10;
71+
private handleScroll = (): void => {
72+
if (this.state.visible) {
73+
this.setState({
74+
visible: false
75+
});
4476
}
77+
};
4578

46-
if (y + tooltipRect.height > viewportHeight) {
47-
y = lastMousePosition.current.y - tooltipRect.height - 10;
79+
private handleTransitionEnd = (e: TransitionEvent): void => {
80+
if (e.propertyName === 'opacity' && !this.state.visible) {
81+
this.setState({ zIndex: -99999 });
4882
}
83+
};
4984

50-
tooltipRef.current.style.left = `${x}px`;
51-
tooltipRef.current.style.top = `${y}px`;
85+
private handleTooltipMouseEnter = (): void => {
86+
this.isMouseOver = true;
5287
};
5388

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-
};
89+
private handleTooltipMouseLeave = (): void => {
90+
this.isMouseOver = false;
91+
this.setState({ visible: false });
92+
};
9093

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-
};
94+
public componentDidUpdate(prevProps: TooltipProps): void {
95+
// Handle content and visibility changes
96+
if (
97+
!this.isMouseOver &&
98+
(prevProps.content !== this.props.content || prevProps.visible !== this.props.visible)
99+
) {
100+
this.debouncedUpdate({
101+
content: this.props.content,
102+
visible: this.props.visible
103+
});
104+
}
105+
}
97106

98-
document.addEventListener('mousemove', handleMouseMove);
99-
document.addEventListener('mouseleave', handleMouseLeave);
107+
public componentWillUnmount(): void {
108+
// Cleanup event listeners
109+
document.removeEventListener('mousemove', this.handleMouseMove);
110+
document.removeEventListener('mouseleave', this.handleMouseLeave);
111+
window.removeEventListener('scroll', this.handleScroll, { capture: true });
112+
document.removeEventListener('scroll', this.handleScroll, { capture: true });
100113

101-
return () => {
102-
document.removeEventListener('mousemove', handleMouseMove);
103-
document.removeEventListener('mouseleave', handleMouseLeave);
104-
};
105-
}, [debouncedUpdate]);
114+
if (this.tooltipRef.current) {
115+
this.tooltipRef.current.removeEventListener('transitionend', this.handleTransitionEnd);
116+
}
106117

107-
// Hide tooltip when scrolling
108-
useEffect(() => {
109-
const handleScroll = () => {
110-
setState(prev => ({
111-
...prev,
112-
visible: false
113-
}));
118+
// Cancel debounced function
119+
this.debouncedUpdate.cancel();
120+
}
121+
122+
public render(): React.ReactPortal {
123+
const tooltipStyle: React.CSSProperties = {
124+
position: 'fixed',
125+
pointerEvents: 'auto',
126+
opacity: this.state.visible ? 1 : 0,
127+
transition: `opacity ${this.props.fadeTransition}ms ease-in-out`,
128+
zIndex: this.state.zIndex,
129+
backgroundColor: '#337AB7',
130+
fontSize: '13px',
131+
color: '#fff',
132+
padding: '9px 11px',
133+
border: '1px solid transparent',
134+
borderRadius: '3px',
135+
boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
114136
};
115137

116-
// Use capture to catch all scroll events before they might be stopped
117-
const options = { capture: true };
138+
return createPortal(
139+
<div
140+
ref={this.tooltipRef}
141+
className="trace-compass-tooltip"
142+
style={tooltipStyle}
143+
onMouseEnter={this.handleTooltipMouseEnter}
144+
onMouseLeave={this.handleTooltipMouseLeave}
145+
>
146+
{this.state.content}
147+
</div>,
148+
document.body
149+
);
150+
}
151+
152+
private calculateAndSetPosition = (): void => {
153+
if (!this.tooltipRef.current) {
154+
return;
155+
}
118156

119-
window.addEventListener('scroll', handleScroll, options);
120-
document.addEventListener('scroll', handleScroll, options);
157+
const viewportWidth = window.innerWidth;
158+
const viewportHeight = window.innerHeight;
159+
const tooltipRect = this.tooltipRef.current.getBoundingClientRect();
121160

122-
return () => {
123-
window.removeEventListener('scroll', handleScroll, options);
124-
document.removeEventListener('scroll', handleScroll, options);
125-
};
126-
}, []);
161+
let x = this.lastMousePosition.x + 10;
162+
let y = this.lastMousePosition.y + 10;
127163

128-
// Handle content and visibility changes
129-
useEffect(() => {
130-
if (!isMouseOverRef.current) {
131-
debouncedUpdate({ content, visible });
132-
}
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;
164+
if (x + tooltipRect.width > viewportWidth) {
165+
x = this.lastMousePosition.x - tooltipRect.width - 10;
148166
}
149167

150-
const handleTransitionEnd = (e: TransitionEvent) => {
151-
if (e.propertyName === 'opacity' && !state.visible) {
152-
setState(prev => ({ ...prev, zIndex: -99999 }));
153-
}
154-
};
168+
if (y + tooltipRect.height > viewportHeight) {
169+
y = this.lastMousePosition.y - tooltipRect.height - 10;
170+
}
155171

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)'
172+
this.tooltipRef.current.style.left = `${x}px`;
173+
this.tooltipRef.current.style.top = `${y}px`;
175174
};
176175

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-
};
176+
private updateState = (newState: Partial<TooltipState>): void => {
177+
this.setState(prevState => {
178+
const updatedState = { ...prevState, ...newState };
179+
180+
// Hide the tooltips when visibility goes form true => false
181+
const weAreHidingTooltip = prevState.visible === true && newState.visible === false;
182+
183+
if (this.isMouseOver) {
184+
this.debouncedUpdate.cancel();
185+
return prevState;
186+
} else if (weAreHidingTooltip) {
187+
// Don't update the content
188+
updatedState.content = prevState.content;
189+
// Keep z-index high during fade out
190+
updatedState.zIndex = prevState.zIndex;
191+
return updatedState;
192+
} else {
193+
this.calculateAndSetPosition();
194+
// Update z-index immediately when showing
195+
if (newState.visible) {
196+
updatedState.zIndex = 99999;
197+
}
198+
return updatedState;
199+
}
200+
});
201+
};
202+
}

0 commit comments

Comments
 (0)