Skip to content

Commit 12603f2

Browse files
authored
fix(tooltip): improve handling when user scrolls (#2514)
* fix(tooltip): improve handling when user scrolls Signed-off-by: Adam Setch <adam.setch@outlook.com> * fix(tooltip): improve handling when user scrolls Signed-off-by: Adam Setch <adam.setch@outlook.com> * fix(tooltip): improve handling when user scrolls Signed-off-by: Adam Setch <adam.setch@outlook.com> --------- Signed-off-by: Adam Setch <adam.setch@outlook.com>
1 parent 00da671 commit 12603f2

2 files changed

Lines changed: 64 additions & 6 deletions

File tree

src/renderer/components/fields/Tooltip.test.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,26 @@ describe('renderer/components/fields/Tooltip.tsx', () => {
2222

2323
const tooltipIconElement = screen.getByTestId('tooltip-icon-test');
2424

25+
// Open tooltip
2526
await userEvent.click(tooltipIconElement);
2627
expect(screen.queryByText(props.tooltip as string)).toBeInTheDocument();
2728

29+
// Close tooltip
2830
await userEvent.click(tooltipIconElement);
2931
expect(screen.queryByText(props.tooltip as string)).not.toBeInTheDocument();
3032
});
3133

32-
it('should hide tooltip contents on leave', async () => {
34+
it('should hide tooltip when clicking outside', async () => {
3335
renderWithAppContext(<Tooltip {...props} />);
3436

3537
const tooltipIconElement = screen.getByTestId('tooltip-icon-test');
3638

39+
// Open tooltip
3740
await userEvent.click(tooltipIconElement);
3841
expect(screen.queryByText(props.tooltip as string)).toBeInTheDocument();
3942

40-
const tooltipContentElement = screen.getByTestId('tooltip-content-test');
41-
42-
await userEvent.unhover(tooltipContentElement);
43+
// Click outside to close
44+
await userEvent.click(document.body);
4345
expect(screen.queryByText(props.tooltip as string)).not.toBeInTheDocument();
4446
});
4547
});

src/renderer/components/fields/Tooltip.tsx

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type FC, type ReactNode, useState } from 'react';
1+
import { type FC, type ReactNode, useEffect, useRef, useState } from 'react';
22

33
import { QuestionIcon } from '@primer/octicons-react';
44
import { AnchoredOverlay } from '@primer/react';
@@ -12,6 +12,62 @@ export interface TooltipProps {
1212

1313
export const Tooltip: FC<TooltipProps> = (props: TooltipProps) => {
1414
const [showTooltip, setShowTooltip] = useState(false);
15+
const scrollContainerRef = useRef<HTMLElement | null>(null);
16+
const overlayRef = useRef<HTMLDivElement>(null);
17+
18+
useEffect(() => {
19+
if (!showTooltip) {
20+
return;
21+
}
22+
23+
// Find the scrollable parent container
24+
const findScrollContainer = (
25+
element: HTMLElement | null,
26+
): HTMLElement | null => {
27+
if (!element) {
28+
return null;
29+
}
30+
31+
const { overflow, overflowY } = window.getComputedStyle(element);
32+
const isScrollable = /(auto|scroll)/.test(overflow + overflowY);
33+
34+
if (isScrollable && element.scrollHeight > element.clientHeight) {
35+
return element;
36+
}
37+
38+
return findScrollContainer(element.parentElement);
39+
};
40+
41+
const tooltipButton = document.getElementById(props.name);
42+
scrollContainerRef.current = findScrollContainer(tooltipButton);
43+
44+
const handleScroll = () => {
45+
setShowTooltip(false);
46+
};
47+
48+
const handleClickOutside = (event: MouseEvent) => {
49+
if (
50+
overlayRef.current &&
51+
!overlayRef.current.contains(event.target as Node) &&
52+
!tooltipButton?.contains(event.target as Node)
53+
) {
54+
setShowTooltip(false);
55+
}
56+
};
57+
58+
if (scrollContainerRef.current) {
59+
scrollContainerRef.current.addEventListener('scroll', handleScroll);
60+
}
61+
62+
document.addEventListener('mousedown', handleClickOutside);
63+
64+
return () => {
65+
if (scrollContainerRef.current) {
66+
scrollContainerRef.current.removeEventListener('scroll', handleScroll);
67+
}
68+
document.removeEventListener('mousedown', handleClickOutside);
69+
};
70+
}, [showTooltip, props.name]);
1571

1672
return (
1773
<AnchoredOverlay
@@ -38,7 +94,7 @@ export const Tooltip: FC<TooltipProps> = (props: TooltipProps) => {
3894
'rounded-sm border border-gray-300 shadow-sm bg-gitify-tooltip-popout',
3995
)}
4096
data-testid={`tooltip-content-${props.name}`}
41-
onMouseLeave={() => setShowTooltip(false)}
97+
ref={overlayRef}
4298
role="tooltip"
4399
>
44100
{props.tooltip}

0 commit comments

Comments
 (0)