Fix worktree sidebar virtual sticky ranges (#2780)
This commit is contained in:
parent
99e527352c
commit
0476ad0b59
|
|
@ -1,7 +1,6 @@
|
|||
/* eslint-disable max-lines */
|
||||
import React, { useMemo, useCallback, useRef, useState, useEffect, useLayoutEffect } from 'react'
|
||||
import {
|
||||
defaultRangeExtractor,
|
||||
measureElement as measureVirtualElementSize,
|
||||
useVirtualizer
|
||||
} from '@tanstack/react-virtual'
|
||||
|
|
@ -77,9 +76,8 @@ import {
|
|||
} from './worktree-list-groups'
|
||||
import {
|
||||
estimateRenderRowSize,
|
||||
getActiveStickyHeaderIndex,
|
||||
extractWorktreeVirtualRowIndexes,
|
||||
getActiveStickyHeaderIndexForScroll,
|
||||
getPreviousStickyHeaderIndex,
|
||||
getStickyHeaderIndexes,
|
||||
getVirtualRowTransform,
|
||||
shouldUseHeaderTopSpacing,
|
||||
|
|
@ -1013,30 +1011,16 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
|
|||
activeStickyHeaderIndexRef.current
|
||||
),
|
||||
measureElement: measureCurrentVirtualRowElement,
|
||||
rangeExtractor: useCallback((range: Range) => {
|
||||
stickyRangeStartIndexRef.current = range.startIndex
|
||||
const activeStickyHeaderIndex = getActiveStickyHeaderIndex(
|
||||
stickyHeaderIndexesRef.current,
|
||||
range.startIndex
|
||||
)
|
||||
if (activeStickyHeaderIndex === null) {
|
||||
return defaultRangeExtractor(range)
|
||||
}
|
||||
|
||||
// Why: this mirrors TanStack Virtual's sticky example — the active
|
||||
// section header remains a real virtual row even after it scrolls out.
|
||||
const previousStickyHeaderIndex = getPreviousStickyHeaderIndex(
|
||||
stickyHeaderIndexesRef.current,
|
||||
activeStickyHeaderIndex
|
||||
)
|
||||
return Array.from(
|
||||
new Set([
|
||||
activeStickyHeaderIndex,
|
||||
...(previousStickyHeaderIndex === null ? [] : [previousStickyHeaderIndex]),
|
||||
...defaultRangeExtractor(range)
|
||||
])
|
||||
).sort((a, b) => a - b)
|
||||
}, []),
|
||||
// Why: TanStack memoizes range extraction by function identity. Header
|
||||
// indexes must be deps so grouping/filtering cannot leave stale sticky
|
||||
// slots rendering ordinary worktree rows.
|
||||
rangeExtractor: useCallback(
|
||||
(range: Range) => {
|
||||
stickyRangeStartIndexRef.current = range.startIndex
|
||||
return extractWorktreeVirtualRowIndexes({ range, stickyHeaderIndexes })
|
||||
},
|
||||
[stickyHeaderIndexes]
|
||||
),
|
||||
overscan: 10,
|
||||
gap: 6,
|
||||
// Why: the active sticky group header is rendered inside the virtual list,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
shouldAdjustWorktreeSidebarMeasuredRowScroll
|
||||
} from './WorktreeList'
|
||||
import {
|
||||
extractWorktreeVirtualRowIndexes,
|
||||
estimateRenderRowSize,
|
||||
GROUP_HEADER_ROW_HEIGHT,
|
||||
getActiveStickyHeaderIndexForScroll
|
||||
|
|
@ -213,6 +214,26 @@ describe('getScrollTopToRevealBounds', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('extractWorktreeVirtualRowIndexes', () => {
|
||||
it('extracts the active and previous sticky headers with the visible range', () => {
|
||||
expect(
|
||||
extractWorktreeVirtualRowIndexes({
|
||||
range: { startIndex: 8, endIndex: 10, overscan: 1, count: 20 },
|
||||
stickyHeaderIndexes: [0, 5, 9]
|
||||
})
|
||||
).toEqual([0, 5, 7, 8, 9, 10, 11])
|
||||
})
|
||||
|
||||
it('falls back to the default range when no sticky header is active', () => {
|
||||
expect(
|
||||
extractWorktreeVirtualRowIndexes({
|
||||
range: { startIndex: 2, endIndex: 3, overscan: 1, count: 10 },
|
||||
stickyHeaderIndexes: [5]
|
||||
})
|
||||
).toEqual([1, 2, 3, 4])
|
||||
})
|
||||
})
|
||||
|
||||
describe('estimateRenderRowSize', () => {
|
||||
it('keeps secondary group header size stable while it is the active sticky header', () => {
|
||||
const rows = [makeHeaderRow('first'), makeHeaderRow('second')]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { VirtualItem } from '@tanstack/react-virtual'
|
||||
import { defaultRangeExtractor } from '@tanstack/react-virtual'
|
||||
import type { Range, VirtualItem } from '@tanstack/react-virtual'
|
||||
import type { Row } from './worktree-list-groups'
|
||||
import { PINNED_GROUP_KEY } from './worktree-list-groups'
|
||||
|
||||
|
|
@ -86,6 +87,31 @@ export function getPreviousStickyHeaderIndex(
|
|||
return stickyHeaderIndexes[currentPosition - 1] ?? null
|
||||
}
|
||||
|
||||
export function extractWorktreeVirtualRowIndexes(args: {
|
||||
range: Range
|
||||
stickyHeaderIndexes: readonly number[]
|
||||
}): number[] {
|
||||
const activeStickyHeaderIndex = getActiveStickyHeaderIndex(
|
||||
args.stickyHeaderIndexes,
|
||||
args.range.startIndex
|
||||
)
|
||||
if (activeStickyHeaderIndex === null) {
|
||||
return defaultRangeExtractor(args.range)
|
||||
}
|
||||
|
||||
const previousStickyHeaderIndex = getPreviousStickyHeaderIndex(
|
||||
args.stickyHeaderIndexes,
|
||||
activeStickyHeaderIndex
|
||||
)
|
||||
return Array.from(
|
||||
new Set([
|
||||
activeStickyHeaderIndex,
|
||||
...(previousStickyHeaderIndex === null ? [] : [previousStickyHeaderIndex]),
|
||||
...defaultRangeExtractor(args.range)
|
||||
])
|
||||
).sort((a, b) => a - b)
|
||||
}
|
||||
|
||||
export function getActiveStickyHeaderIndexForScroll(args: {
|
||||
rangeStartIndex: number
|
||||
scrollOffset: number
|
||||
|
|
|
|||
Loading…
Reference in New Issue