From 8b2bd23eda26623c18ea56278169a7321412e6ef Mon Sep 17 00:00:00 2001 From: komcal Date: Mon, 13 Oct 2025 13:48:41 +0700 Subject: [PATCH 1/3] feat(tooltip): add closeOnPress prop to keep tooltip visible after clicked --- .../tooltip/src/useTooltipTrigger.ts | 7 +++- .../tooltip/src/TooltipTrigger.tsx | 7 +++- .../stories/TooltipTrigger.stories.tsx | 7 +++- .../tooltip/test/TooltipTrigger.test.js | 42 +++++++++++++++++++ packages/@react-types/tooltip/src/index.d.ts | 8 +++- 5 files changed, 66 insertions(+), 5 deletions(-) diff --git a/packages/@react-aria/tooltip/src/useTooltipTrigger.ts b/packages/@react-aria/tooltip/src/useTooltipTrigger.ts index 4fa5bd7ec1d..75c92604b0b 100644 --- a/packages/@react-aria/tooltip/src/useTooltipTrigger.ts +++ b/packages/@react-aria/tooltip/src/useTooltipTrigger.ts @@ -36,7 +36,8 @@ export interface TooltipTriggerAria { export function useTooltipTrigger(props: TooltipTriggerProps, state: TooltipTriggerState, ref: RefObject) : TooltipTriggerAria { let { isDisabled, - trigger + trigger, + closeOnPress } = props; let tooltipId = useId(); @@ -102,6 +103,10 @@ export function useTooltipTrigger(props: TooltipTriggerProps, state: TooltipTrig }; let onPressStart = () => { + // if closeOnPress is false, we should not close the tooltip + if (!closeOnPress) { + return; + } // no matter how the trigger is pressed, we should close the tooltip isFocused.current = false; isHovered.current = false; diff --git a/packages/@react-spectrum/tooltip/src/TooltipTrigger.tsx b/packages/@react-spectrum/tooltip/src/TooltipTrigger.tsx index 0478fff35f5..0cb42d22ab7 100644 --- a/packages/@react-spectrum/tooltip/src/TooltipTrigger.tsx +++ b/packages/@react-spectrum/tooltip/src/TooltipTrigger.tsx @@ -22,6 +22,7 @@ import {useTooltipTriggerState} from '@react-stately/tooltip'; const DEFAULT_OFFSET = -1; // Offset needed to reach 4px/5px (med/large) distance between tooltip and trigger button const DEFAULT_CROSS_OFFSET = 0; +const DEFAULT_CLOSE_ON_PRESS = true; // Whether the tooltip should close when the trigger is pressed function TooltipTrigger(props: SpectrumTooltipTriggerProps) { let { @@ -29,7 +30,8 @@ function TooltipTrigger(props: SpectrumTooltipTriggerProps) { crossOffset = DEFAULT_CROSS_OFFSET, isDisabled, offset = DEFAULT_OFFSET, - trigger: triggerAction + trigger: triggerAction, + closeOnPress = DEFAULT_CLOSE_ON_PRESS } = props; let [trigger, tooltip] = React.Children.toArray(children) as [ReactElement, ReactElement]; @@ -40,7 +42,8 @@ function TooltipTrigger(props: SpectrumTooltipTriggerProps) { let {triggerProps, tooltipProps} = useTooltipTrigger({ isDisabled, - trigger: triggerAction + trigger: triggerAction, + closeOnPress }, state, tooltipTriggerRef); let [borderRadius, setBorderRadius] = useState(0); diff --git a/packages/@react-spectrum/tooltip/stories/TooltipTrigger.stories.tsx b/packages/@react-spectrum/tooltip/stories/TooltipTrigger.stories.tsx index 0a70eb40117..2feda08cdd6 100644 --- a/packages/@react-spectrum/tooltip/stories/TooltipTrigger.stories.tsx +++ b/packages/@react-spectrum/tooltip/stories/TooltipTrigger.stories.tsx @@ -72,6 +72,10 @@ const argTypes = { }, children: { control: {disable: true} + }, + closeOnPress: { + control: 'boolean', + defaultValue: true } }; @@ -113,7 +117,8 @@ export default { , Change Name ], - onOpenChange: action('openChange') + onOpenChange: action('openChange'), + closeOnPress: true }, argTypes: argTypes } as Meta; diff --git a/packages/@react-spectrum/tooltip/test/TooltipTrigger.test.js b/packages/@react-spectrum/tooltip/test/TooltipTrigger.test.js index 86eb44719c7..5a898b7450e 100644 --- a/packages/@react-spectrum/tooltip/test/TooltipTrigger.test.js +++ b/packages/@react-spectrum/tooltip/test/TooltipTrigger.test.js @@ -330,6 +330,48 @@ describe('TooltipTrigger', function () { expect(queryByRole('tooltip')).toBeNull(); }); + it('does not close if the trigger is clicked when closeOnPress is false', async () => { + let {getByRole, getByLabelText} = render( + + + + Helpful information. + + + ); + await user.click(document.body); + + let button = getByLabelText('trigger'); + await user.hover(button); + expect(onOpenChange).toHaveBeenCalledWith(true); + let tooltip = getByRole('tooltip'); + expect(tooltip).toBeVisible(); + await user.click(button); + expect(onOpenChange).toHaveBeenCalledTimes(1); + expect(tooltip).toBeVisible(); + }); + + it('does not close if the trigger is clicked with the keyboard when closeOnPress is false', async () => { + let {getByRole, getByLabelText} = render( + + + + Helpful information. + + + ); + + let button = getByLabelText('trigger'); + await user.tab(); + expect(onOpenChange).toHaveBeenCalledWith(true); + let tooltip = getByRole('tooltip'); + expect(tooltip).toBeVisible(); + fireEvent.keyDown(button, {key: 'Enter'}); + fireEvent.keyUp(button, {key: 'Enter'}); + expect(onOpenChange).toHaveBeenCalledTimes(1); + expect(tooltip).toBeVisible(); + }); + describe('delay', () => { it('opens immediately for focus', () => { let {getByRole, getByLabelText} = render( diff --git a/packages/@react-types/tooltip/src/index.d.ts b/packages/@react-types/tooltip/src/index.d.ts index c544fc0bac7..73730df859f 100644 --- a/packages/@react-types/tooltip/src/index.d.ts +++ b/packages/@react-types/tooltip/src/index.d.ts @@ -36,7 +36,13 @@ export interface TooltipTriggerProps extends OverlayTriggerProps { * By default, opens for both focus and hover. Can be made to open only for focus. * @default 'hover' */ - trigger?: 'hover' | 'focus' + trigger?: 'hover' | 'focus', + + /** + * Whether the tooltip should close when the trigger is pressed. + * @default true + */ + closeOnPress?: boolean } export interface SpectrumTooltipTriggerProps extends Omit, PositionProps { From 5d7e6e5db253ad444c6a6f57755f223e17f8e461 Mon Sep 17 00:00:00 2001 From: komcal Date: Mon, 13 Oct 2025 21:12:27 +0700 Subject: [PATCH 2/3] remove default value for closeOnPress prop in TooltipTrigger stories --- .../@react-spectrum/tooltip/stories/TooltipTrigger.stories.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/@react-spectrum/tooltip/stories/TooltipTrigger.stories.tsx b/packages/@react-spectrum/tooltip/stories/TooltipTrigger.stories.tsx index 2feda08cdd6..48becc8c2bf 100644 --- a/packages/@react-spectrum/tooltip/stories/TooltipTrigger.stories.tsx +++ b/packages/@react-spectrum/tooltip/stories/TooltipTrigger.stories.tsx @@ -74,8 +74,7 @@ const argTypes = { control: {disable: true} }, closeOnPress: { - control: 'boolean', - defaultValue: true + control: 'boolean' } }; From 0bd44acd472c762c1ad17f3f01047b40daeecb5f Mon Sep 17 00:00:00 2001 From: komkanit Date: Tue, 14 Oct 2025 07:27:59 +0700 Subject: [PATCH 3/3] add default value on closeOnPress props Co-authored-by: Robert Snow --- packages/@react-aria/tooltip/src/useTooltipTrigger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/tooltip/src/useTooltipTrigger.ts b/packages/@react-aria/tooltip/src/useTooltipTrigger.ts index 75c92604b0b..d31c8ae9024 100644 --- a/packages/@react-aria/tooltip/src/useTooltipTrigger.ts +++ b/packages/@react-aria/tooltip/src/useTooltipTrigger.ts @@ -37,7 +37,7 @@ export function useTooltipTrigger(props: TooltipTriggerProps, state: TooltipTrig let { isDisabled, trigger, - closeOnPress + closeOnPress = true } = props; let tooltipId = useId();