From 47451a73d8b8602ab57dbf86a7832c25b1595b68 Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Wed, 24 Jun 2026 08:46:11 +0530 Subject: [PATCH 1/2] fix(ScrollArea): reset scroll only on direct viewport child swaps (#1349) --- .../ScrollArea/fragments/ScrollAreaRoot.tsx | 20 +++++++++- .../ui/ScrollArea/tests/ScrollArea.test.tsx | 40 ++++++++++++++++++- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx b/src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx index 39668dbad..d45dc00d9 100644 --- a/src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx +++ b/src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx @@ -56,13 +56,29 @@ const ScrollAreaRoot = forwardRef(({ resizeObserver.observe(viewport); Array.from(viewport.children).forEach(child => resizeObserver.observe(child)); - const mutationObserver = new MutationObserver(() => { + const mutationObserver = new MutationObserver((mutations) => { + const directChildSwap = mutations.some( + (mutation) => + mutation.type === 'childList' + && mutation.target === viewport + && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) + ); + + if (directChildSwap) { + viewport.scrollTop = 0; + viewport.scrollLeft = 0; + } + handleResize(); + + if (directChildSwap) { + handleScroll(); + } }); mutationObserver.observe(viewport, { childList: true, - subtree: true + subtree: false }); window.addEventListener('resize', handleResize); diff --git a/src/components/ui/ScrollArea/tests/ScrollArea.test.tsx b/src/components/ui/ScrollArea/tests/ScrollArea.test.tsx index cc50437a6..209c27b39 100644 --- a/src/components/ui/ScrollArea/tests/ScrollArea.test.tsx +++ b/src/components/ui/ScrollArea/tests/ScrollArea.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { act, fireEvent, render, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import ScrollArea from '../ScrollArea'; @@ -176,4 +176,42 @@ describe('ScrollArea', () => { expect(screen.getByTestId('scrollbar')).toHaveAttribute('data-state', 'visible'); }); + + test('resets scroll when direct viewport child is swapped, not on nested mutations', async() => { + const { rerender } = render( + + +
Panel A
+
+
+ ); + + const viewport = screen.getByTestId('viewport') as HTMLDivElement; + viewport.scrollTop = 120; + + act(() => { + const nested = document.createElement('span'); + nested.textContent = 'nested'; + screen.getByTestId('panel-a').appendChild(nested); + }); + + expect(viewport.scrollTop).toBe(120); + + rerender( + + +
Panel B
+
+
+ ); + + await act(async() => { + await Promise.resolve(); + }); + + const nextViewport = screen.getByTestId('viewport') as HTMLDivElement; + await waitFor(() => { + expect(nextViewport.scrollTop).toBe(0); + }); + }); }); From a68492be326b98439bcb2a283d7e957e64f2ca74 Mon Sep 17 00:00:00 2001 From: Pranay Kothapalli Date: Wed, 24 Jun 2026 16:15:34 +0530 Subject: [PATCH 2/2] fix(ScrollArea): re-sync ResizeObserver targets after viewport child swap --- .../ui/ScrollArea/fragments/ScrollAreaRoot.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx b/src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx index d45dc00d9..41d7a0ff0 100644 --- a/src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx +++ b/src/components/ui/ScrollArea/fragments/ScrollAreaRoot.tsx @@ -53,8 +53,17 @@ const ScrollAreaRoot = forwardRef(({ }; const resizeObserver = new ResizeObserver(() => handleResize()); - resizeObserver.observe(viewport); - Array.from(viewport.children).forEach(child => resizeObserver.observe(child)); + const syncResizeObservers = () => { + resizeObserver.disconnect(); + resizeObserver.observe(viewport); + Array.from(viewport.children).forEach(child => { + if (child instanceof Element) { + resizeObserver.observe(child); + } + }); + }; + + syncResizeObservers(); const mutationObserver = new MutationObserver((mutations) => { const directChildSwap = mutations.some( @@ -67,6 +76,7 @@ const ScrollAreaRoot = forwardRef(({ if (directChildSwap) { viewport.scrollTop = 0; viewport.scrollLeft = 0; + syncResizeObservers(); } handleResize();