diff --git a/frontend/src/pages/Findings.tsx b/frontend/src/pages/Findings.tsx
index ca6fe1985..dceb1248c 100644
--- a/frontend/src/pages/Findings.tsx
+++ b/frontend/src/pages/Findings.tsx
@@ -639,16 +639,30 @@ export default function Findings() {
overscan: 6,
})
- // Scroll selected finding into view when it changes
+ // Keep latest virtualRows/virtualizer available without making them
+ // reactive dependencies — they change on every filter/sort, but we only
+ // want to re-scroll when the *selection* actually changes.
+ const virtualRowsRef = useRef(virtualRows)
useEffect(() => {
- if (!selectedFinding) return
- const rowIdx = virtualRows.findIndex(
- (row) => row.kind === 'finding' && row.finding.id === selectedFinding.id,
+ virtualRowsRef.current = virtualRows
+ })
+
+ const virtualizerRef = useRef(virtualizer)
+ useEffect(() => {
+ virtualizerRef.current = virtualizer
+ })
+
+ // Scroll selected finding into view when the selection changes
+ useEffect(() => {
+ if (!selectedFindingId) return
+ const rows = virtualRowsRef.current
+ const rowIdx = rows.findIndex(
+ (row) => row.kind === 'finding' && row.finding.id === selectedFindingId,
)
if (rowIdx !== -1) {
- virtualizer.scrollToIndex(rowIdx, { align: 'auto', behavior: 'smooth' })
+ virtualizerRef.current.scrollToIndex(rowIdx, { align: 'auto', behavior: 'smooth' })
}
- }, [selectedFindingId]) // eslint-disable-line react-hooks/exhaustive-deps
+ }, [selectedFindingId])
return (
diff --git a/frontend/testing/unit/pages/Findings.test.tsx b/frontend/testing/unit/pages/Findings.test.tsx
index e797bf45d..2cd885950 100644
--- a/frontend/testing/unit/pages/Findings.test.tsx
+++ b/frontend/testing/unit/pages/Findings.test.tsx
@@ -535,3 +535,30 @@ describe('Findings — virtualizer scrolling', () => {
expect(mockScrollToIndex).not.toHaveBeenCalled()
})
})
+
+it('scrolls to the correct fresh index after sort order changes then selection changes', async () => {
+ const findings = [
+ makeFinding({ id: 'f1', title: 'Finding Alpha', severity: 'critical', discovered_at: '2024-01-01T00:00:00Z' }),
+ makeFinding({ id: 'f2', title: 'Finding Beta', severity: 'high', discovered_at: '2024-01-03T00:00:00Z' }),
+ makeFinding({ id: 'f3', title: 'Finding Gamma', severity: 'medium', discovered_at: '2024-01-02T00:00:00Z' }),
+ ]
+ vi.mocked(getFindings).mockResolvedValue({ findings })
+
+ render()
+ await waitFor(() => expect(screen.queryByText('Synchronizing findings feed...')).not.toBeInTheDocument())
+
+ // Switch to "newest" sort — new order is Beta(0), Gamma(1), Alpha(2)
+ const selects = screen.getAllByRole('combobox')
+ const sortSelect = selects.find((s) =>
+ Array.from(s.querySelectorAll('option')).some((o) => /Newest First/i.test(o.textContent || '')),
+ )
+ await userEvent.selectOptions(sortSelect!, 'newest')
+
+ mockScrollToIndex.mockClear()
+
+ // Now select Gamma — should scroll to its *post-sort* index (1), not a stale pre-sort index
+ const gammaOption = await screen.findByRole('option', { name: /Finding Gamma/i })
+ await userEvent.click(gammaOption)
+
+ expect(mockScrollToIndex).toHaveBeenCalledWith(1, { align: 'auto', behavior: 'smooth' })
+ })