diff --git a/.changeset/tooltip-window-refocus.md b/.changeset/tooltip-window-refocus.md
new file mode 100644
index 0000000000..7ae2c496bb
--- /dev/null
+++ b/.changeset/tooltip-window-refocus.md
@@ -0,0 +1,6 @@
+---
+'@radix-ui/react-tooltip': patch
+'radix-ui': patch
+---
+
+Fixed a bug where the tooltip opened when switching back to the browser tab while its trigger was focused.
diff --git a/packages/react/tooltip/src/tooltip.test.tsx b/packages/react/tooltip/src/tooltip.test.tsx
index 215e0d6d88..c31a966975 100644
--- a/packages/react/tooltip/src/tooltip.test.tsx
+++ b/packages/react/tooltip/src/tooltip.test.tsx
@@ -83,6 +83,41 @@ describe('Tooltip', () => {
});
});
+ // Regression test for https://github.com/radix-ui/primitives/issues/1800
+ // Switching away from and back to the browser tab refocuses the previously-focused
+ // trigger, firing a synthetic `focus` event that must not reopen the tooltip.
+ it('does not open on focus caused by the window regaining focus', () => {
+ render(
+
+
+ Tooltip Trigger
+
+ Tooltip Content
+
+
+ ,
+ );
+
+ const trigger = screen.getByText('Tooltip Trigger');
+
+ // A genuine focus opens the tooltip.
+ act(() => void fireEvent.focus(trigger));
+ expect(trigger).toHaveAttribute('data-state', 'instant-open');
+ act(() => void fireEvent.blur(trigger));
+ expect(trigger).toHaveAttribute('data-state', 'closed');
+
+ // Simulate the window losing focus, then the tab regaining focus and refocusing
+ // the trigger. The synthetic refocus must NOT reopen the tooltip.
+ act(() => void window.dispatchEvent(new FocusEvent('blur')));
+ act(() => void fireEvent.focus(trigger));
+ expect(trigger).toHaveAttribute('data-state', 'closed');
+ expect(screen.queryByText('Tooltip Content')).not.toBeInTheDocument();
+
+ // A subsequent genuine focus still opens it (the guard is consumed once).
+ act(() => void fireEvent.focus(trigger));
+ expect(trigger).toHaveAttribute('data-state', 'instant-open');
+ });
+
// Regression test for https://github.com/radix-ui/primitives/issues/2375
// Hovering a trigger inside a shared `TooltipProvider` must not re-render
// sibling tooltips. The provider keeps its "open delayed" flag in a ref so
diff --git a/packages/react/tooltip/src/tooltip.tsx b/packages/react/tooltip/src/tooltip.tsx
index 42d432f103..fbb98d8a17 100644
--- a/packages/react/tooltip/src/tooltip.tsx
+++ b/packages/react/tooltip/src/tooltip.tsx
@@ -276,12 +276,22 @@ const TooltipTrigger = React.forwardRef (isPointerDownRef.current = false), []);
React.useEffect(() => {
return () => document.removeEventListener('pointerup', handlePointerUp);
}, [handlePointerUp]);
+ // When the window/tab loses focus, the browser refocuses the previously active
+ // element once focus returns, firing a `focus` event that would otherwise reopen
+ // the tooltip. Track the blur so we can ignore that synthetic refocus. See #1800.
+ React.useEffect(() => {
+ const handleWindowBlur = () => (isWindowBlurredRef.current = true);
+ window.addEventListener('blur', handleWindowBlur);
+ return () => window.removeEventListener('blur', handleWindowBlur);
+ }, []);
+
return (
{
+ // Ignore focus caused by the tab/window regaining focus, which would
+ // otherwise reopen the tooltip on the previously-focused trigger. See #1800.
+ if (isWindowBlurredRef.current) {
+ isWindowBlurredRef.current = false;
+ return;
+ }
if (!isPointerDownRef.current) context.onOpen();
})}
onBlur={composeEventHandlers(props.onBlur, context.onClose)}