Skip to content

Commit bd9852c

Browse files
authoredSep 29, 2020
Tooltip bugs (adobe#1087)
1 parent 471eba3 commit bd9852c

File tree

3 files changed

+93
-18
lines changed

3 files changed

+93
-18
lines changed
 

‎packages/@react-aria/tooltip/src/useTooltipTrigger.ts

+23-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {FocusEvents} from '@react-types/shared';
14-
import {HoverProps, isFocusVisible, PressProps, usePress} from '@react-aria/interactions';
14+
import {getInteractionModality, HoverProps, isFocusVisible, PressProps, usePress} from '@react-aria/interactions';
1515
import {HTMLAttributes, RefObject, useEffect, useRef} from 'react';
1616
import {mergeProps, useId} from '@react-aria/utils';
1717
import {TooltipTriggerProps} from '@react-types/tooltip';
@@ -35,7 +35,9 @@ export function useTooltipTrigger(props: TooltipTriggerProps, state: TooltipTrig
3535
let isFocused = useRef(false);
3636

3737
let handleShow = () => {
38-
state.open(isFocused.current);
38+
if (isHovered.current || isFocused.current) {
39+
state.open(isFocused.current);
40+
}
3941
};
4042

4143
let handleHide = () => {
@@ -63,28 +65,43 @@ export function useTooltipTrigger(props: TooltipTriggerProps, state: TooltipTrig
6365
}, [ref, state]);
6466

6567
let onHoverStart = () => {
66-
isHovered.current = true;
68+
// In chrome, if you hover a trigger, then another element obscures it, due to keyboard
69+
// interactions for example, hover will end. When hover is restored after that element disappears,
70+
// focus moves on for example, then the tooltip will reopen. We check the modality to know if the hover
71+
// is the result of moving the mouse.
72+
if (getInteractionModality() === 'pointer') {
73+
isHovered.current = true;
74+
} else {
75+
isHovered.current = false;
76+
}
6777
handleShow();
6878
};
79+
6980
let onHoverEnd = () => {
81+
// no matter how the trigger is left, we should close the tooltip
82+
isFocused.current = false;
7083
isHovered.current = false;
7184
handleHide();
7285
};
86+
7387
let onPressStart = () => {
74-
if (isFocused.current) {
75-
isFocused.current = false;
76-
}
88+
// no matter how the trigger is pressed, we should close the tooltip
89+
isFocused.current = false;
90+
isHovered.current = false;
7791
handleHide();
7892
};
93+
7994
let onFocus = () => {
8095
let isVisible = isFocusVisible();
8196
if (isVisible) {
8297
isFocused.current = true;
8398
handleShow();
8499
}
85100
};
101+
86102
let onBlur = () => {
87103
isFocused.current = false;
104+
isHovered.current = false;
88105
handleHide();
89106
};
90107

‎packages/@react-spectrum/tooltip/stories/TooltipTrigger.stories.tsx

+15-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {action} from '@storybook/addon-actions';
14-
import {ActionButton} from '@react-spectrum/button';
14+
import {ActionButton, Button} from '@react-spectrum/button';
1515
import {Flex} from '@react-spectrum/layout';
1616
import React, {useState} from 'react';
1717
import {storiesOf} from '@storybook/react';
@@ -86,6 +86,20 @@ storiesOf('TooltipTrigger', module)
8686
</TooltipTrigger>
8787
</div>
8888
)
89+
)
90+
.add(
91+
'tooltip with other hoverables',
92+
() => (
93+
<Flex gap="size-100">
94+
<TooltipTrigger onOpenChange={action('openChange')}>
95+
<ActionButton>Trigger Tooltip</ActionButton>
96+
<Tooltip>
97+
Long tooltip message that just goes on and on.
98+
</Tooltip>
99+
</TooltipTrigger>
100+
<Button variant="secondary">No Tooltip</Button>
101+
</Flex>
102+
)
89103
);
90104

91105
function render(props = {}) {

‎packages/@react-spectrum/tooltip/test/TooltipTrigger.test.js

+55-11
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {Provider} from '@react-spectrum/provider';
1616
import React from 'react';
1717
import {theme} from '@react-spectrum/theme-default';
1818
import {Tooltip, TooltipTrigger} from '../';
19+
import {triggerPress} from '@react-spectrum/test-utils';
1920

2021
// Sync with useTooltipTriggerState.ts
2122
const TOOLTIP_DELAY = 1500;
@@ -87,6 +88,8 @@ describe('TooltipTrigger', function () {
8788
</TooltipTrigger>
8889
</Provider>
8990
);
91+
fireEvent.mouseDown(document.body);
92+
fireEvent.mouseUp(document.body);
9093

9194
let button = getByLabelText('trigger');
9295
fireEvent.mouseEnter(button);
@@ -104,7 +107,7 @@ describe('TooltipTrigger', function () {
104107
expect(tooltip).not.toBeInTheDocument();
105108
});
106109

107-
it('if hovered and focused, will not hide if hover leaves', () => {
110+
it('if hovered and focused, will hide if hover leaves', () => {
108111
let {getByRole, getByLabelText} = render(
109112
<Provider theme={theme}>
110113
<TooltipTrigger onOpenChange={onOpenChange} delay={0}>
@@ -133,11 +136,11 @@ describe('TooltipTrigger', function () {
133136

134137
// remove hover
135138
fireEvent.mouseLeave(button);
136-
expect(onOpenChange).toHaveBeenCalledTimes(1);
139+
expect(onOpenChange).toHaveBeenCalledTimes(2);
137140
act(() => {
138141
jest.runAllTimers();
139142
});
140-
expect(tooltip).toBeVisible();
143+
expect(tooltip).not.toBeInTheDocument();
141144

142145
// remove focus
143146
act(() => {
@@ -151,7 +154,7 @@ describe('TooltipTrigger', function () {
151154
expect(tooltip).not.toBeInTheDocument();
152155
});
153156

154-
it('if hovered and focused, will not hide if focus leaves', () => {
157+
it('if hovered and focused, will hide if focus leaves', () => {
155158
let {getByRole, getByLabelText} = render(
156159
<Provider theme={theme}>
157160
<TooltipTrigger onOpenChange={onOpenChange} delay={0}>
@@ -182,11 +185,11 @@ describe('TooltipTrigger', function () {
182185
act(() => {
183186
button.blur();
184187
});
185-
expect(onOpenChange).toHaveBeenCalledTimes(1);
188+
expect(onOpenChange).toHaveBeenCalledTimes(2);
186189
act(() => {
187190
jest.runAllTimers();
188191
});
189-
expect(tooltip).toBeVisible();
192+
expect(tooltip).not.toBeInTheDocument();
190193

191194
// remove hover
192195
fireEvent.mouseLeave(button);
@@ -241,6 +244,8 @@ describe('TooltipTrigger', function () {
241244
<input type="text" />
242245
</Provider>
243246
);
247+
fireEvent.mouseDown(document.body);
248+
fireEvent.mouseUp(document.body);
244249

245250
let button = getByLabelText('trigger');
246251
let input = getByRole('textbox');
@@ -278,16 +283,15 @@ describe('TooltipTrigger', function () {
278283
</TooltipTrigger>
279284
</Provider>
280285
);
286+
fireEvent.mouseDown(document.body);
287+
fireEvent.mouseUp(document.body);
281288

282289
let button = getByLabelText('trigger');
283-
act(() => {
284-
button.focus();
285-
});
290+
fireEvent.mouseEnter(button);
286291
expect(onOpenChange).toHaveBeenCalledWith(true);
287292
let tooltip = getByRole('tooltip');
288293
expect(tooltip).toBeVisible();
289-
fireEvent.mouseDown(document.activeElement, {button: 0});
290-
fireEvent.mouseUp(document.activeElement, {button: 0});
294+
triggerPress(button);
291295
expect(onOpenChange).toHaveBeenCalledWith(false);
292296
act(() => {
293297
jest.advanceTimersByTime(CLOSE_TIME);
@@ -303,6 +307,39 @@ describe('TooltipTrigger', function () {
303307
});
304308
});
305309

310+
it('is closed if the trigger is clicked with the keyboard', () => {
311+
let {getByRole, getByLabelText} = render(
312+
<Provider theme={theme}>
313+
<TooltipTrigger onOpenChange={onOpenChange} delay={0}>
314+
<ActionButton aria-label="trigger" />
315+
<Tooltip>Helpful information.</Tooltip>
316+
</TooltipTrigger>
317+
</Provider>
318+
);
319+
320+
let button = getByLabelText('trigger');
321+
act(() => {
322+
button.focus();
323+
});
324+
expect(onOpenChange).toHaveBeenCalledWith(true);
325+
let tooltip = getByRole('tooltip');
326+
expect(tooltip).toBeVisible();
327+
fireEvent.keyDown(button, {key: 'Enter'});
328+
fireEvent.keyUp(button, {key: 'Enter'});
329+
expect(onOpenChange).toHaveBeenCalledWith(false);
330+
act(() => {
331+
jest.advanceTimersByTime(CLOSE_TIME);
332+
});
333+
expect(tooltip).not.toBeInTheDocument();
334+
act(() => {
335+
button.blur();
336+
});
337+
act(() => {
338+
jest.runAllTimers();
339+
});
340+
expect(() => getByRole('tooltip')).toThrow();
341+
});
342+
306343
describe('delay', () => {
307344
it('opens immediately for focus', () => {
308345
let {getByRole, getByLabelText} = render(
@@ -340,6 +377,8 @@ describe('TooltipTrigger', function () {
340377
</TooltipTrigger>
341378
</Provider>
342379
);
380+
fireEvent.mouseDown(document.body);
381+
fireEvent.mouseUp(document.body);
343382

344383
let button = getByLabelText('trigger');
345384
fireEvent.mouseEnter(button);
@@ -439,6 +478,8 @@ describe('TooltipTrigger', function () {
439478
</TooltipTrigger>
440479
</Provider>
441480
);
481+
fireEvent.mouseDown(document.body);
482+
fireEvent.mouseUp(document.body);
442483

443484
let button = getByLabelText('trigger');
444485
fireEvent.mouseEnter(button);
@@ -701,6 +742,7 @@ describe('TooltipTrigger', function () {
701742
});
702743
expect(getByText(helpfulText)).toBeVisible();
703744

745+
fireEvent.mouseMove(document.body);
704746
// we've used focus to open one, now we can use hover to try and open a second one
705747
fireEvent.mouseEnter(badButton);
706748
fireEvent.mouseMove(badButton);
@@ -728,6 +770,7 @@ describe('TooltipTrigger', function () {
728770
let badButton = getByLabelText('bad-trigger');
729771
expect(getByText(helpfulText)).toBeVisible();
730772

773+
fireEvent.mouseMove(document.body);
731774
// open a second one through interaction
732775
fireEvent.mouseEnter(badButton);
733776
fireEvent.mouseMove(badButton);
@@ -786,6 +829,7 @@ describe('TooltipTrigger', function () {
786829
</TooltipTrigger>
787830
</Provider>
788831
);
832+
fireEvent.mouseMove(document.body);
789833
let button = getByLabelText('trigger');
790834
fireEvent.mouseEnter(button);
791835
fireEvent.mouseMove(button);

0 commit comments

Comments
 (0)
Failed to load comments.