From 2782534436ad7f65b7c864b09c6c57bd5cf0b8ff Mon Sep 17 00:00:00 2001 From: Igor Kostyukov Date: Mon, 20 May 2024 12:38:04 +0300 Subject: [PATCH] fix: controlled tooltips are not close properly --- .../tooltip/src/useTooltipTriggerState.ts | 9 ++- .../test/useTooltipTriggerState.test.js | 60 ++++++++++++++++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/packages/@react-stately/tooltip/src/useTooltipTriggerState.ts b/packages/@react-stately/tooltip/src/useTooltipTriggerState.ts index f0e9866c6e6..9d3b74a0bde 100644 --- a/packages/@react-stately/tooltip/src/useTooltipTriggerState.ts +++ b/packages/@react-stately/tooltip/src/useTooltipTriggerState.ts @@ -46,6 +46,7 @@ export function useTooltipTriggerState(props: TooltipTriggerProps = {}): Tooltip let {isOpen, open, close} = useOverlayTriggerState(props); let id = useMemo(() => `${++tooltipId}`, []); let closeTimeout = useRef>(); + let closeCallback = useRef<() => void>(close); let ensureTooltipEntry = () => { tooltips[id] = hideTooltip; @@ -81,11 +82,11 @@ export function useTooltipTriggerState(props: TooltipTriggerProps = {}): Tooltip if (immediate || closeDelay <= 0) { clearTimeout(closeTimeout.current); closeTimeout.current = null; - close(); + closeCallback.current(); } else if (!closeTimeout.current) { closeTimeout.current = setTimeout(() => { closeTimeout.current = null; - close(); + closeCallback.current(); }, closeDelay); } @@ -119,6 +120,10 @@ export function useTooltipTriggerState(props: TooltipTriggerProps = {}): Tooltip } }; + useEffect(() => { + closeCallback.current = close; + }, [close]); + // eslint-disable-next-line arrow-body-style useEffect(() => { return () => { diff --git a/packages/@react-stately/tooltip/test/useTooltipTriggerState.test.js b/packages/@react-stately/tooltip/test/useTooltipTriggerState.test.js index 6334fd48568..6e84dd425d0 100644 --- a/packages/@react-stately/tooltip/test/useTooltipTriggerState.test.js +++ b/packages/@react-stately/tooltip/test/useTooltipTriggerState.test.js @@ -37,7 +37,7 @@ function TooltipTrigger(props) { return ( - {state.isOpen && @@ -46,6 +46,23 @@ function TooltipTrigger(props) { ); } +function ManualTooltipTrigger(props) { + let [isOpen, setOpen] = React.useState(false); + + const onOpenChange = (isOpen) => { + props.onOpenChange(isOpen); + setOpen(isOpen); + }; + + return ( + + ); +} + /** * Most of the tests for useTooltipTriggerState are in the React Spectrum package at * @react-spectrum/tooltip/test/TooltipTrigger.test.js. The React Spectrum tooltip @@ -196,4 +213,45 @@ describe('useTooltipTriggerState', function () { expect(onOpenChange).toHaveBeenCalledWith(false); }); }); + + describe('multiple controlled tooltips', () => { + it('closes previus tooltip when opening a new one', () => { + let secondOnOpenChange = jest.fn(); + + let {queryByRole, getByLabelText} = render( + <> + + Trigger 1 + + + + Trigger 2 + + + ); + + fireEvent.mouseDown(document.body); + fireEvent.mouseUp(document.body); + + let button1 = getByLabelText('trigger1'); + fireEvent.mouseEnter(button1); + fireEvent.mouseMove(button1); + + // run through open timer and confirm that it has opened + act(() => jest.advanceTimersByTime(TOOLTIP_DELAY)); + + expect(onOpenChange).toHaveBeenCalledWith(true); + expect(queryByRole('tooltip')).toBeVisible(); + + let button2 = getByLabelText('trigger2'); + fireEvent.mouseEnter(button2); + fireEvent.mouseMove(button2); + + // run through open timer and confirm that it has opened + act(() => jest.advanceTimersByTime(TOOLTIP_DELAY)); + + expect(onOpenChange).toHaveBeenCalledTimes(2); + expect(onOpenChange).toHaveBeenCalledWith(false); + }); + }); });