Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 106 additions & 36 deletions apps/web/src/components/BranchToolbarBranchSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime";
import type { EnvironmentId, VcsRef, ThreadId } from "@t3tools/contracts";
import { LegendList, type LegendListRef } from "@legendapp/list/react";
import { ChevronDownIcon } from "lucide-react";
import { ChevronDownIcon, GitBranchIcon } from "lucide-react";
import {
useCallback,
useDeferredValue,
useEffect,
useLayoutEffect,
useMemo,
useOptimistic,
useRef,
Expand Down Expand Up @@ -421,7 +422,8 @@ export function BranchToolbarBranchSelector({
[branchRefTarget],
);

const branchListScrollElementRef = useRef<HTMLDivElement | null>(null);
const branchListScrollElementRef = useRef<HTMLElement | null>(null);
const [branchListBottomFadeVisible, setBranchListBottomFadeVisible] = useState(false);
const fetchNextBranchPage = useCallback(() => {
if (!hasNextPage || isFetchingNextPage) {
return;
Expand Down Expand Up @@ -451,11 +453,54 @@ export function BranchToolbarBranchSelector({

fetchNextBranchPage();
}, [fetchNextBranchPage, hasNextPage, isBranchMenuOpen, isFetchingNextPage]);

const syncBranchListScrollChrome = useCallback((scrollEl: HTMLElement | null) => {
if (!scrollEl) {
setBranchListBottomFadeVisible(false);
return;
}
const { scrollTop, scrollHeight, clientHeight } = scrollEl;
const canScroll = scrollHeight > clientHeight + 1;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
setBranchListBottomFadeVisible(canScroll && distanceFromBottom > 6);
}, []);

const branchListRef = useRef<LegendListRef | null>(null);
const setBranchListRef = useCallback((element: HTMLDivElement | null) => {
branchListScrollElementRef.current = (element?.parentElement as HTMLDivElement | null) ?? null;
branchListScrollElementRef.current = element?.parentElement ?? null;
}, []);

useEffect(() => {
if (isBranchMenuOpen) {
return;
}
setBranchListBottomFadeVisible(false);
}, [isBranchMenuOpen]);

useLayoutEffect(() => {
if (!isBranchMenuOpen || !shouldVirtualizeBranchList) {
return;
}

let frame = 0;
const measure = () => {
const el = branchListRef.current?.getScrollableNode?.();
if (el instanceof HTMLElement) {
branchListScrollElementRef.current = el;
syncBranchListScrollChrome(el);
return;
}
frame = requestAnimationFrame(measure);
};
frame = requestAnimationFrame(measure);
return () => cancelAnimationFrame(frame);
}, [
isBranchMenuOpen,
shouldVirtualizeBranchList,
filteredBranchPickerItems.length,
syncBranchListScrollChrome,
]);

Comment thread
cursor[bot] marked this conversation as resolved.
useEffect(() => {
if (!isBranchMenuOpen) {
return;
Expand All @@ -470,7 +515,7 @@ export function BranchToolbarBranchSelector({

useEffect(() => {
const scrollElement = branchListScrollElementRef.current;
if (!scrollElement || !isBranchMenuOpen) {
if (!scrollElement || !isBranchMenuOpen || shouldVirtualizeBranchList) {
return;
}

Expand All @@ -483,7 +528,7 @@ export function BranchToolbarBranchSelector({
return () => {
scrollElement.removeEventListener("scroll", handleScroll);
};
}, [isBranchMenuOpen, maybeFetchNextBranchPage]);
}, [isBranchMenuOpen, maybeFetchNextBranchPage, shouldVirtualizeBranchList]);

useEffect(() => {
if (shouldVirtualizeBranchList) return;
Expand All @@ -504,6 +549,7 @@ export function BranchToolbarBranchSelector({
key={itemValue}
index={index}
value={itemValue}
className="pe-2"
onClick={() => {
if (!prReference || !onCheckoutPullRequestRequest) {
return;
Expand Down Expand Up @@ -562,8 +608,8 @@ export function BranchToolbarBranchSelector({
value={itemValue}
onClick={() => selectBranch(refName)}
>
<div className="flex w-full items-center justify-between gap-2">
<span className="truncate">{itemValue}</span>
<div className="flex w-full min-w-0 items-center justify-between gap-2">
<span className="min-w-0 flex-1 truncate">{itemValue}</span>
{badge && <span className="shrink-0 text-[10px] text-muted-foreground/45">{badge}</span>}
</div>
</ComboboxItem>
Expand Down Expand Up @@ -594,11 +640,12 @@ export function BranchToolbarBranchSelector({
className={cn("min-w-0 text-muted-foreground/70 hover:text-foreground/80", className)}
disabled={isInitialBranchesLoadPending || isBranchActionPending}
>
<GitBranchIcon className="size-3 shrink-0 opacity-70" />
<span className="min-w-0 max-w-[240px] truncate">{triggerLabel}</span>
<ChevronDownIcon className="shrink-0" />
<ChevronDownIcon className="size-3 shrink-0 opacity-50" />
</ComboboxTrigger>
<ComboboxPopup align="end" side="top" className="w-80">
<div className="border-b p-1">
<ComboboxPopup align="end" side="top" className="flex w-80 flex-col">
<div className="shrink-0 border-b p-1">
<ComboboxInput
className="[&_input]:font-sans rounded-md"
inputClassName="ring-0"
Expand All @@ -609,33 +656,56 @@ export function BranchToolbarBranchSelector({
onChange={(event) => setBranchQuery(event.target.value)}
/>
</div>
<ComboboxEmpty>No refs found.</ComboboxEmpty>

{shouldVirtualizeBranchList ? (
<ComboboxListVirtualized>
<LegendList<string>
ref={branchListRef}
data={filteredBranchPickerItems}
keyExtractor={(item) => item}
renderItem={({ item, index }) => renderPickerItem(item, index)}
estimatedItemSize={28}
drawDistance={336}
onEndReached={() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextBranchPage();
}
}}
style={{ maxHeight: "14rem" }}
/>
</ComboboxListVirtualized>
) : (
<ComboboxList ref={setBranchListRef} className="max-h-56">
{filteredBranchPickerItems.map((itemValue, index) =>
renderPickerItem(itemValue, index),
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<ComboboxEmpty>No refs found.</ComboboxEmpty>
<div className="relative min-h-0 w-full max-h-56 flex-1 overflow-hidden">
{shouldVirtualizeBranchList ? (
<>
<ComboboxListVirtualized className="not-empty:ps-1 not-empty:pe-0 not-empty:py-1">
<LegendList<string>
ref={branchListRef}
data={filteredBranchPickerItems}
keyExtractor={(item) => item}
renderItem={({ item, index }) => renderPickerItem(item, index)}
estimatedItemSize={28}
drawDistance={336}
onEndReached={() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextBranchPage();
}
}}
onScroll={() => {
const target = branchListRef.current?.getScrollableNode?.();
if (target instanceof HTMLElement) {
branchListScrollElementRef.current = target;
syncBranchListScrollChrome(target);
}
maybeFetchNextBranchPage();
}}
style={{ maxHeight: "14rem" }}
/>
</ComboboxListVirtualized>
<div
aria-hidden
className={cn(
"pointer-events-none absolute inset-x-0 bottom-0 z-10 h-8 bg-gradient-to-t from-popover to-transparent transition-opacity duration-150",
branchListBottomFadeVisible ? "opacity-100" : "opacity-0",
)}
/>
</>
) : (
<ComboboxList
ref={setBranchListRef}
className="h-full max-h-56 min-h-0 not-empty:ps-1 not-empty:pe-0 not-empty:py-1"
>
{filteredBranchPickerItems.map((itemValue, index) =>
renderPickerItem(itemValue, index),
)}
</ComboboxList>
)}
</ComboboxList>
)}
{branchStatusText ? <ComboboxStatus>{branchStatusText}</ComboboxStatus> : null}
</div>
{branchStatusText ? <ComboboxStatus>{branchStatusText}</ComboboxStatus> : null}
</div>
</ComboboxPopup>
</Combobox>
);
Expand Down
Loading