11import { scopeProjectRef , scopeThreadRef } from "@t3tools/client-runtime" ;
22import type { EnvironmentId , GitBranch , ThreadId } from "@t3tools/contracts" ;
33import { useInfiniteQuery , useQueryClient } from "@tanstack/react-query" ;
4- import { useVirtualizer } from "@tanstack/ react-virtual " ;
4+ import { LegendList , type LegendListRef } from "@legendapp/list/ react" ;
55import { ChevronDownIcon } from "lucide-react" ;
66import {
7- type CSSProperties ,
87 useCallback ,
98 useDeferredValue ,
109 useEffect ,
@@ -38,6 +37,7 @@ import {
3837 ComboboxInput ,
3938 ComboboxItem ,
4039 ComboboxList ,
40+ ComboboxListVirtualized ,
4141 ComboboxPopup ,
4242 ComboboxStatus ,
4343 ComboboxTrigger ,
@@ -390,7 +390,7 @@ export function BranchToolbarBranchSelector({
390390 } , [ activeThreadBranch , activeWorktreePath , currentGitBranch , effectiveEnvMode , setThreadBranch ] ) ;
391391
392392 // ---------------------------------------------------------------------------
393- // Combobox / virtualizer plumbing
393+ // Combobox / list plumbing
394394 // ---------------------------------------------------------------------------
395395 const handleOpenChange = useCallback (
396396 ( open : boolean ) => {
@@ -425,49 +425,22 @@ export function BranchToolbarBranchSelector({
425425
426426 void fetchNextPage ( ) . catch ( ( ) => undefined ) ;
427427 } , [ fetchNextPage , hasNextPage , isBranchMenuOpen , isFetchingNextPage ] ) ;
428- const branchListVirtualizer = useVirtualizer ( {
429- count : filteredBranchPickerItems . length ,
430- estimateSize : ( index ) =>
431- filteredBranchPickerItems [ index ] === checkoutPullRequestItemValue ? 44 : 28 ,
432- getScrollElement : ( ) => branchListScrollElementRef . current ,
433- overscan : 12 ,
434- enabled : isBranchMenuOpen && shouldVirtualizeBranchList ,
435- initialRect : {
436- height : 224 ,
437- width : 0 ,
438- } ,
439- } ) ;
440- const virtualBranchRows = branchListVirtualizer . getVirtualItems ( ) ;
441- const setBranchListRef = useCallback (
442- ( element : HTMLDivElement | null ) => {
443- branchListScrollElementRef . current =
444- ( element ?. parentElement as HTMLDivElement | null ) ?? null ;
445- if ( element ) {
446- branchListVirtualizer . measure ( ) ;
447- }
448- } ,
449- [ branchListVirtualizer ] ,
450- ) ;
451-
452- useEffect ( ( ) => {
453- if ( ! isBranchMenuOpen || ! shouldVirtualizeBranchList ) return ;
454- queueMicrotask ( ( ) => {
455- branchListVirtualizer . measure ( ) ;
456- } ) ;
457- } , [
458- branchListVirtualizer ,
459- filteredBranchPickerItems . length ,
460- isBranchMenuOpen ,
461- shouldVirtualizeBranchList ,
462- ] ) ;
428+ const branchListRef = useRef < LegendListRef | null > ( null ) ;
429+ const setBranchListRef = useCallback ( ( element : HTMLDivElement | null ) => {
430+ branchListScrollElementRef . current = ( element ?. parentElement as HTMLDivElement | null ) ?? null ;
431+ } , [ ] ) ;
463432
464433 useEffect ( ( ) => {
465434 if ( ! isBranchMenuOpen ) {
466435 return ;
467436 }
468437
469- branchListScrollElementRef . current ?. scrollTo ( { top : 0 } ) ;
470- } , [ deferredTrimmedBranchQuery , isBranchMenuOpen ] ) ;
438+ if ( shouldVirtualizeBranchList ) {
439+ branchListRef . current ?. scrollToOffset ?.( { offset : 0 , animated : false } ) ;
440+ } else {
441+ branchListScrollElementRef . current ?. scrollTo ( { top : 0 } ) ;
442+ }
443+ } , [ deferredTrimmedBranchQuery , isBranchMenuOpen , shouldVirtualizeBranchList ] ) ;
471444
472445 useEffect ( ( ) => {
473446 const scrollElement = branchListScrollElementRef . current ;
@@ -487,24 +460,24 @@ export function BranchToolbarBranchSelector({
487460 } , [ isBranchMenuOpen , maybeFetchNextBranchPage ] ) ;
488461
489462 useEffect ( ( ) => {
463+ if ( shouldVirtualizeBranchList ) return ;
490464 maybeFetchNextBranchPage ( ) ;
491- } , [ branches . length , maybeFetchNextBranchPage ] ) ;
465+ } , [ branches . length , maybeFetchNextBranchPage , shouldVirtualizeBranchList ] ) ;
492466
493467 const triggerLabel = getBranchTriggerLabel ( {
494468 activeWorktreePath,
495469 effectiveEnvMode,
496470 resolvedActiveBranch,
497471 } ) ;
498472
499- function renderPickerItem ( itemValue : string , index : number , style ?: CSSProperties ) {
473+ function renderPickerItem ( itemValue : string , index : number ) {
500474 if ( checkoutPullRequestItemValue && itemValue === checkoutPullRequestItemValue ) {
501475 return (
502476 < ComboboxItem
503477 hideIndicator
504478 key = { itemValue }
505479 index = { index }
506480 value = { itemValue }
507- style = { style }
508481 onClick = { ( ) => {
509482 if ( ! prReference || ! onCheckoutPullRequestRequest ) {
510483 return ;
@@ -529,7 +502,6 @@ export function BranchToolbarBranchSelector({
529502 key = { itemValue }
530503 index = { index }
531504 value = { itemValue }
532- style = { style }
533505 onClick = { ( ) => createBranch ( trimmedBranchQuery ) }
534506 >
535507 < span className = "truncate" > Create new branch "{ trimmedBranchQuery } "</ span >
@@ -557,7 +529,6 @@ export function BranchToolbarBranchSelector({
557529 key = { itemValue }
558530 index = { index }
559531 value = { itemValue }
560- style = { style }
561532 onClick = { ( ) => selectBranch ( branch ) }
562533 >
563534 < div className = "flex w-full items-center justify-between gap-2" >
@@ -575,8 +546,13 @@ export function BranchToolbarBranchSelector({
575546 autoHighlight
576547 virtualized = { shouldVirtualizeBranchList }
577548 onItemHighlighted = { ( _value , eventDetails ) => {
578- if ( ! isBranchMenuOpen || eventDetails . index < 0 ) return ;
579- branchListVirtualizer . scrollToIndex ( eventDetails . index , { align : "auto" } ) ;
549+ if ( ! isBranchMenuOpen || eventDetails . index < 0 || eventDetails . reason !== "keyboard" ) {
550+ return ;
551+ }
552+ branchListRef . current ?. scrollIndexIntoView ?.( {
553+ index : eventDetails . index ,
554+ animated : false ,
555+ } ) ;
580556 } }
581557 onOpenChange = { handleOpenChange }
582558 open = { isBranchMenuOpen }
@@ -604,30 +580,30 @@ export function BranchToolbarBranchSelector({
604580 </ div >
605581 < ComboboxEmpty > No branches found.</ ComboboxEmpty >
606582
607- < ComboboxList ref = { setBranchListRef } className = "max-h-56" >
608- { shouldVirtualizeBranchList ? (
609- < div
610- className = "relative"
611- style = { {
612- height : `${ branchListVirtualizer . getTotalSize ( ) } px` ,
583+ { shouldVirtualizeBranchList ? (
584+ < ComboboxListVirtualized >
585+ < LegendList < string >
586+ ref = { branchListRef }
587+ data = { filteredBranchPickerItems }
588+ keyExtractor = { ( item ) => item }
589+ renderItem = { ( { item, index } ) => renderPickerItem ( item , index ) }
590+ estimatedItemSize = { 28 }
591+ drawDistance = { 336 }
592+ onEndReached = { ( ) => {
593+ if ( hasNextPage && ! isFetchingNextPage ) {
594+ void fetchNextPage ( ) . catch ( ( ) => undefined ) ;
595+ }
613596 } }
614- >
615- { virtualBranchRows . map ( ( virtualRow ) => {
616- const itemValue = filteredBranchPickerItems [ virtualRow . index ] ;
617- if ( ! itemValue ) return null ;
618- return renderPickerItem ( itemValue , virtualRow . index , {
619- position : "absolute" ,
620- top : 0 ,
621- left : 0 ,
622- width : "100%" ,
623- transform : `translateY(${ virtualRow . start } px)` ,
624- } ) ;
625- } ) }
626- </ div >
627- ) : (
628- filteredBranchPickerItems . map ( ( itemValue , index ) => renderPickerItem ( itemValue , index ) )
629- ) }
630- </ ComboboxList >
597+ style = { { maxHeight : "14rem" } }
598+ />
599+ </ ComboboxListVirtualized >
600+ ) : (
601+ < ComboboxList ref = { setBranchListRef } className = "max-h-56" >
602+ { filteredBranchPickerItems . map ( ( itemValue , index ) =>
603+ renderPickerItem ( itemValue , index ) ,
604+ ) }
605+ </ ComboboxList >
606+ ) }
631607 { branchStatusText ? < ComboboxStatus > { branchStatusText } </ ComboboxStatus > : null }
632608 </ ComboboxPopup >
633609 </ Combobox >
0 commit comments