diff --git a/design-system/packages/ui/src/components/Tooltip/Tooltip.tsx b/design-system/packages/ui/src/components/Tooltip/Tooltip.tsx
index 160ebc6417..326cba2194 100644
--- a/design-system/packages/ui/src/components/Tooltip/Tooltip.tsx
+++ b/design-system/packages/ui/src/components/Tooltip/Tooltip.tsx
@@ -263,7 +263,13 @@ export function Tooltip({
// Capture nested, non-bubbling scrolls during both delayed and visible states.
// Dormant tooltips do not need a document-level scroll subscription.
if (view && !scrollCleanupRef.current) {
- const dismissOnScroll = () => hideCurrentRef.current();
+ const dismissOnScroll = (event: Event) => {
+ // Scrolling the tooltip's own scroll container (a wheel over the
+ // preview or a scrollbar drag inside it) is not a moving anchor.
+ const surface = tooltipRef.current;
+ if (surface && event.target instanceof Node && surface.contains(event.target)) return;
+ hideCurrentRef.current();
+ };
view.addEventListener("scroll", dismissOnScroll, { capture: true, passive: true });
view.visualViewport?.addEventListener("scroll", dismissOnScroll, { passive: true });
scrollCleanupRef.current = () => {
diff --git a/src/web-ui/src/shared/ui/Tooltip.test.tsx b/src/web-ui/src/shared/ui/Tooltip.test.tsx
index c13447c768..4769c09ec9 100644
--- a/src/web-ui/src/shared/ui/Tooltip.test.tsx
+++ b/src/web-ui/src/shared/ui/Tooltip.test.tsx
@@ -114,6 +114,27 @@ describe('shared tooltip visibility', () => {
expect(popup()).toBeNull();
});
+ it('keeps an interactive preview open while its own scroll container scrolls', () => {
+ render(
+