Skip to content

Commit e56eaf6

Browse files
committed
[LG-5488] fix(tooltip): only open tooltip on keyboard-driven focus
Gate focus-triggered open behind useUsingKeyboardContext so programmatic focus (e.g. focus restored after modal close) no longer opens the tooltip. Code copy button now opens its tooltip explicitly on click.
1 parent 15b7695 commit e56eaf6

5 files changed

Lines changed: 135 additions & 3 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@leafygreen-ui/tooltip': patch
3+
'@leafygreen-ui/code': patch
4+
---
5+
6+
[LG-5488](https://jira.mongodb.org/browse/LG-5488): `Tooltip` no longer opens when its trigger receives focus from mouse interaction or programmatic focus (e.g. focus restored after closing a modal). It still opens when focus comes from keyboard navigation. `Code`'s copy button now explicitly opens its tooltip on click to continue showing the copied confirmation.

packages/code/src/CopyButton/CopyButton.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ function CopyButton({ onCopy, contents, className, ...rest }: CopyProps) {
8080
e.preventDefault();
8181
onCopy?.();
8282
setCopied(true);
83+
// Explicitly open the tooltip to show the copied confirmation,
84+
// since focus alone no longer opens the tooltip
85+
openTooltip();
8386
};
8487

8588
/**

packages/tooltip/src/Tooltip/Tooltip.spec.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { axe } from 'jest-axe';
1111

1212
import { Icon } from '@leafygreen-ui/icon';
1313
import CloudIcon from '@leafygreen-ui/icon/dist/Cloud';
14+
import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider';
1415
import { HTMLElementProps, OneOf } from '@leafygreen-ui/lib';
1516
import { RenderMode } from '@leafygreen-ui/popover';
1617
import { transitionDuration } from '@leafygreen-ui/tokens';
@@ -157,6 +158,59 @@ describe('packages/tooltip', () => {
157158
expect(queryByTestId(tooltipTestId)).not.toBeInTheDocument();
158159
});
159160

161+
describe('when "triggerEvent" is "hover", focusing the trigger (LG-5488)', () => {
162+
const renderTooltipWithProvider = () => {
163+
const utils = render(
164+
<LeafyGreenProvider>
165+
<div data-testid="backdrop" />
166+
<Tooltip
167+
trigger={<button>{buttonText}</button>}
168+
data-testid={tooltipTestId}
169+
triggerEvent="hover"
170+
>
171+
<div>Tooltip Contents!</div>
172+
</Tooltip>
173+
</LeafyGreenProvider>,
174+
);
175+
176+
const button = utils.getByText(buttonText);
177+
const backdrop = utils.getByTestId('backdrop');
178+
179+
return { ...utils, button, backdrop };
180+
};
181+
182+
test('does not open the tooltip when focus follows mouse usage', async () => {
183+
const { queryByTestId, button, backdrop } =
184+
renderTooltipWithProvider();
185+
186+
// Mouse usage sets usingKeyboard to false
187+
await userEvent.click(backdrop);
188+
189+
// Programmatic focus, e.g. focus restored after closing a modal
190+
await act(async () => {
191+
button.focus();
192+
await waitForTimeout(200);
193+
});
194+
195+
expect(button).toHaveFocus();
196+
expect(queryByTestId(tooltipTestId)).not.toBeInTheDocument();
197+
});
198+
199+
test('opens the tooltip when focus comes from keyboard navigation', async () => {
200+
const { getByTestId, button, backdrop } = renderTooltipWithProvider();
201+
202+
// Mouse usage sets usingKeyboard to false
203+
await userEvent.click(backdrop);
204+
205+
// Tabbing to the trigger sets usingKeyboard back to true
206+
await userEvent.tab();
207+
208+
expect(button).toHaveFocus();
209+
await waitFor(() => getByTestId(tooltipTestId));
210+
expect(getByTestId(tooltipTestId)).toBeInTheDocument();
211+
});
212+
});
213+
160214
async function testTriggerEventWhenDisabled(
161215
triggerEvent: 'hover' | 'click',
162216
) {

packages/tooltip/src/Tooltip/utils/useTooltipTriggerEventHandlers.spec.tsx

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React from 'react';
2-
import { renderHook } from '@testing-library/react-hooks';
2+
import { act, renderHook } from '@testing-library/react-hooks';
3+
4+
import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider';
35

46
import { TriggerEvent } from '../Tooltip.types';
57
import { CALLBACK_DEBOUNCE, DEFAULT_HOVER_DELAY } from '../tooltipConstants';
@@ -11,6 +13,10 @@ const mockMouseEvent = {
1113
target: document.createElement('div'),
1214
} as unknown as React.MouseEvent<HTMLElement>;
1315

16+
const mockFocusEvent = {
17+
target: document.createElement('div'),
18+
} as unknown as React.FocusEvent<HTMLElement>;
19+
1420
describe('packages/tooltip/useTooltipTriggerEventHandlers', () => {
1521
// Common test setup
1622
const setState = jest.fn();
@@ -115,6 +121,59 @@ describe('packages/tooltip/useTooltipTriggerEventHandlers', () => {
115121
expect(result.current).toBe(initialHandlers);
116122
});
117123

124+
describe('usingKeyboard', () => {
125+
test('onFocus opens tooltip when usingKeyboard is true (default)', () => {
126+
const onFocus = jest.fn();
127+
const hoverArgs: UseTooltipEventsArgs<typeof TriggerEvent.Hover> = {
128+
setState,
129+
triggerEvent: TriggerEvent.Hover,
130+
onFocus,
131+
};
132+
133+
const { result } = renderHook(() =>
134+
useTooltipTriggerEventHandlers(hoverArgs),
135+
);
136+
137+
result.current.onFocus(mockFocusEvent);
138+
139+
expect(onFocus).toHaveBeenCalled();
140+
expect(setState).toHaveBeenCalledWith(true);
141+
});
142+
143+
test('onFocus does not open tooltip after mouse usage', () => {
144+
const onFocus = jest.fn();
145+
const hoverArgs: UseTooltipEventsArgs<typeof TriggerEvent.Hover> = {
146+
setState,
147+
triggerEvent: TriggerEvent.Hover,
148+
onFocus,
149+
};
150+
151+
const { result } = renderHook(
152+
() => useTooltipTriggerEventHandlers(hoverArgs),
153+
{
154+
wrapper: ({ children }: React.PropsWithChildren<unknown>) => (
155+
<LeafyGreenProvider>{children}</LeafyGreenProvider>
156+
),
157+
},
158+
);
159+
160+
// Mouse usage sets usingKeyboard to false
161+
act(() => {
162+
document.dispatchEvent(new MouseEvent('mousedown'));
163+
});
164+
165+
result.current.onFocus(mockFocusEvent);
166+
167+
// Consumer's onFocus handler still fires, but the tooltip does not open
168+
expect(onFocus).toHaveBeenCalled();
169+
expect(setState).not.toHaveBeenCalled();
170+
171+
// Blur still closes the tooltip
172+
result.current.onBlur(mockFocusEvent);
173+
expect(setState).toHaveBeenCalledWith(false);
174+
});
175+
});
176+
118177
test('only changes identity when args change', () => {
119178
const hoverArgs: UseTooltipEventsArgs<typeof TriggerEvent.Hover> = {
120179
setState,

packages/tooltip/src/Tooltip/utils/useTooltipTriggerEventHandlers.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
import { flushSync } from 'react-dom';
1010
import debounce from 'lodash/debounce';
1111

12+
import { useUsingKeyboardContext } from '@leafygreen-ui/leafygreen-provider';
13+
1214
import { TriggerEvent } from '../Tooltip.types';
1315
import { CALLBACK_DEBOUNCE, DEFAULT_HOVER_DELAY } from '../tooltipConstants';
1416

@@ -29,6 +31,8 @@ export function useTooltipTriggerEventHandlers<Trigger extends TriggerEvent>(
2931
): TooltipEventHandlers<Trigger> {
3032
const { setState, triggerEvent, tooltipRef, isEnabled = true } = args;
3133

34+
const { usingKeyboard } = useUsingKeyboardContext();
35+
3236
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
3337

3438
return useMemo(() => {
@@ -66,7 +70,13 @@ export function useTooltipTriggerEventHandlers<Trigger extends TriggerEvent>(
6670
const onFocus: FocusEventHandler = (e: FocusEvent<HTMLElement>) => {
6771
if (isEnabled) {
6872
args.onFocus?.(e);
69-
setState(true);
73+
74+
// Only open the tooltip on keyboard-driven focus.
75+
// Prevents the tooltip from appearing when focus is set
76+
// programmatically (e.g. focus restored after closing a modal)
77+
if (usingKeyboard) {
78+
setState(true);
79+
}
7080
}
7181
};
7282

@@ -98,5 +108,5 @@ export function useTooltipTriggerEventHandlers<Trigger extends TriggerEvent>(
98108
onClick,
99109
} as TooltipEventHandlers<Trigger>;
100110
}
101-
}, [args, isEnabled, setState, tooltipRef, triggerEvent]);
111+
}, [args, isEnabled, setState, tooltipRef, triggerEvent, usingKeyboard]);
102112
}

0 commit comments

Comments
 (0)