Skip to content

Commit 05346a6

Browse files
feat: Allow Tooltip to remain visible when trigger is clicked (#9027)
* feat(tooltip): add closeOnPress prop to keep tooltip visible after clicked * remove default value for closeOnPress prop in TooltipTrigger stories * add default value on closeOnPress props Co-authored-by: Robert Snow <snowystinger@gmail.com> --------- Co-authored-by: Robert Snow <snowystinger@gmail.com>
1 parent f48d2c8 commit 05346a6

File tree

5 files changed

+65
-5
lines changed

5 files changed

+65
-5
lines changed

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export interface TooltipTriggerAria {
3636
export function useTooltipTrigger(props: TooltipTriggerProps, state: TooltipTriggerState, ref: RefObject<FocusableElement | null>) : TooltipTriggerAria {
3737
let {
3838
isDisabled,
39-
trigger
39+
trigger,
40+
closeOnPress = true
4041
} = props;
4142

4243
let tooltipId = useId();
@@ -102,6 +103,10 @@ export function useTooltipTrigger(props: TooltipTriggerProps, state: TooltipTrig
102103
};
103104

104105
let onPressStart = () => {
106+
// if closeOnPress is false, we should not close the tooltip
107+
if (!closeOnPress) {
108+
return;
109+
}
105110
// no matter how the trigger is pressed, we should close the tooltip
106111
isFocused.current = false;
107112
isHovered.current = false;

packages/@react-spectrum/tooltip/src/TooltipTrigger.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@ import {useTooltipTriggerState} from '@react-stately/tooltip';
2222

2323
const DEFAULT_OFFSET = -1; // Offset needed to reach 4px/5px (med/large) distance between tooltip and trigger button
2424
const DEFAULT_CROSS_OFFSET = 0;
25+
const DEFAULT_CLOSE_ON_PRESS = true; // Whether the tooltip should close when the trigger is pressed
2526

2627
function TooltipTrigger(props: SpectrumTooltipTriggerProps) {
2728
let {
2829
children,
2930
crossOffset = DEFAULT_CROSS_OFFSET,
3031
isDisabled,
3132
offset = DEFAULT_OFFSET,
32-
trigger: triggerAction
33+
trigger: triggerAction,
34+
closeOnPress = DEFAULT_CLOSE_ON_PRESS
3335
} = props;
3436

3537
let [trigger, tooltip] = React.Children.toArray(children) as [ReactElement, ReactElement];
@@ -40,7 +42,8 @@ function TooltipTrigger(props: SpectrumTooltipTriggerProps) {
4042

4143
let {triggerProps, tooltipProps} = useTooltipTrigger({
4244
isDisabled,
43-
trigger: triggerAction
45+
trigger: triggerAction,
46+
closeOnPress
4447
}, state, tooltipTriggerRef);
4548

4649
let [borderRadius, setBorderRadius] = useState(0);

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ const argTypes = {
7272
},
7373
children: {
7474
control: {disable: true}
75+
},
76+
closeOnPress: {
77+
control: 'boolean'
7578
}
7679
};
7780

@@ -113,7 +116,8 @@ export default {
113116
<ActionButton aria-label="Edit Name"><Edit /></ActionButton>,
114117
<Tooltip>Change Name</Tooltip>
115118
],
116-
onOpenChange: action('openChange')
119+
onOpenChange: action('openChange'),
120+
closeOnPress: true
117121
},
118122
argTypes: argTypes
119123
} as Meta<typeof TooltipTrigger>;

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,48 @@ describe('TooltipTrigger', function () {
330330
expect(queryByRole('tooltip')).toBeNull();
331331
});
332332

333+
it('does not close if the trigger is clicked when closeOnPress is false', async () => {
334+
let {getByRole, getByLabelText} = render(
335+
<Provider theme={theme}>
336+
<TooltipTrigger onOpenChange={onOpenChange} delay={0} closeOnPress={false}>
337+
<ActionButton aria-label="trigger" />
338+
<Tooltip>Helpful information.</Tooltip>
339+
</TooltipTrigger>
340+
</Provider>
341+
);
342+
await user.click(document.body);
343+
344+
let button = getByLabelText('trigger');
345+
await user.hover(button);
346+
expect(onOpenChange).toHaveBeenCalledWith(true);
347+
let tooltip = getByRole('tooltip');
348+
expect(tooltip).toBeVisible();
349+
await user.click(button);
350+
expect(onOpenChange).toHaveBeenCalledTimes(1);
351+
expect(tooltip).toBeVisible();
352+
});
353+
354+
it('does not close if the trigger is clicked with the keyboard when closeOnPress is false', async () => {
355+
let {getByRole, getByLabelText} = render(
356+
<Provider theme={theme}>
357+
<TooltipTrigger onOpenChange={onOpenChange} delay={0} closeOnPress={false}>
358+
<ActionButton aria-label="trigger" />
359+
<Tooltip>Helpful information.</Tooltip>
360+
</TooltipTrigger>
361+
</Provider>
362+
);
363+
364+
let button = getByLabelText('trigger');
365+
await user.tab();
366+
expect(onOpenChange).toHaveBeenCalledWith(true);
367+
let tooltip = getByRole('tooltip');
368+
expect(tooltip).toBeVisible();
369+
fireEvent.keyDown(button, {key: 'Enter'});
370+
fireEvent.keyUp(button, {key: 'Enter'});
371+
expect(onOpenChange).toHaveBeenCalledTimes(1);
372+
expect(tooltip).toBeVisible();
373+
});
374+
333375
describe('delay', () => {
334376
it('opens immediately for focus', () => {
335377
let {getByRole, getByLabelText} = render(

packages/@react-types/tooltip/src/index.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@ export interface TooltipTriggerProps extends OverlayTriggerProps {
3636
* By default, opens for both focus and hover. Can be made to open only for focus.
3737
* @default 'hover'
3838
*/
39-
trigger?: 'hover' | 'focus'
39+
trigger?: 'hover' | 'focus',
40+
41+
/**
42+
* Whether the tooltip should close when the trigger is pressed.
43+
* @default true
44+
*/
45+
closeOnPress?: boolean
4046
}
4147

4248
export interface SpectrumTooltipTriggerProps extends Omit<TooltipTriggerProps, 'closeDelay'>, PositionProps {

0 commit comments

Comments
 (0)