Fix GitHub Issues/PRs Infinite Scroll Auto-Fetch (#1239)

* auto-claude: subtask-1-1 - Add onViewportRef callback prop to ScrollArea comp

* auto-claude: subtask-2-1 - Add viewport ref state and update IntersectionObserver to use viewport as root

* auto-claude: subtask-3-1 - Add viewport ref state and update IntersectionObserver

- Added useState import for viewportElement state
- Replaced scrollAreaRef with viewportElement state
- Updated IntersectionObserver root from null to viewportElement
- Added viewportElement to useEffect dependencies
- Changed ScrollArea to use onViewportRef callback

This fixes infinite scroll in PRList by using the ScrollArea viewport
as the IntersectionObserver root, matching the pattern used in IssueList.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: remove node_modules symlink and improve IntersectionObserver consistency

- Remove accidentally committed symlink at apps/frontend/node_modules
- Add explicit .gitignore entry for symlink file (without trailing slash)
- Add onLoadMore to PRList useEffect dependency array for consistency with IssueList
- Add viewportElement guard to both IssueList and PRList to prevent unnecessary observer creation

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Test User <test@example.com>
This commit is contained in:
Andy
2026-01-17 20:13:19 +01:00
committed by GitHub
parent 8833feb2b7
commit b74b628b6e
4 changed files with 41 additions and 28 deletions
+1
View File
@@ -108,6 +108,7 @@ dmypy.json
# Node.js (apps/frontend)
# ===========================
node_modules
apps/frontend/node_modules
.npm
.yarn/
.pnp.*
@@ -1,4 +1,4 @@
import { useRef, useEffect, useCallback } from 'react';
import { useRef, useEffect, useCallback, useState } from 'react';
import { Loader2, AlertCircle } from 'lucide-react';
import { ScrollArea } from '../../ui/scroll-area';
import { IssueListItem } from './IssueListItem';
@@ -19,6 +19,7 @@ export function IssueList({
}: IssueListProps) {
const { t } = useTranslation('common');
const loadMoreTriggerRef = useRef<HTMLDivElement>(null);
const [viewportElement, setViewportElement] = useState<HTMLDivElement | null>(null);
// Intersection Observer for infinite scroll
const handleIntersection = useCallback((entries: IntersectionObserverEntry[]) => {
@@ -30,10 +31,10 @@ export function IssueList({
useEffect(() => {
const trigger = loadMoreTriggerRef.current;
if (!trigger || !onLoadMore) return;
if (!trigger || !onLoadMore || !viewportElement) return;
const observer = new IntersectionObserver(handleIntersection, {
root: null,
root: viewportElement,
rootMargin: '100px',
threshold: 0
});
@@ -43,7 +44,7 @@ export function IssueList({
return () => {
observer.disconnect();
};
}, [handleIntersection, onLoadMore]);
}, [handleIntersection, onLoadMore, viewportElement]);
// Only show blocking error view when no issues are loaded
// Load-more errors are shown inline near the load-more trigger
@@ -71,7 +72,7 @@ export function IssueList({
}
return (
<ScrollArea className="flex-1">
<ScrollArea className="flex-1" onViewportRef={setViewportElement}>
<div className="p-2 space-y-1">
{issues.map((issue) => (
<IssueListItem
@@ -1,4 +1,4 @@
import { useRef, useEffect, useCallback } from 'react';
import { useRef, useEffect, useCallback, useState } from 'react';
import { GitPullRequest, User, Clock, FileDiff, Loader2 } from 'lucide-react';
import { ScrollArea } from '../../ui/scroll-area';
import { Badge } from '../../ui/badge';
@@ -207,8 +207,8 @@ export function PRList({
onLoadMore
}: PRListProps) {
const { t } = useTranslation('common');
const scrollAreaRef = useRef<HTMLDivElement>(null);
const loadMoreTriggerRef = useRef<HTMLDivElement>(null);
const [viewportElement, setViewportElement] = useState<HTMLDivElement | null>(null);
// Intersection Observer for infinite scroll
const handleIntersection = useCallback((entries: IntersectionObserverEntry[]) => {
@@ -220,11 +220,11 @@ export function PRList({
useEffect(() => {
const trigger = loadMoreTriggerRef.current;
if (!trigger) return;
if (!trigger || !viewportElement) return;
const observer = new IntersectionObserver(handleIntersection, {
root: null, // Use viewport as root
rootMargin: '100px', // Start loading 100px before reaching the bottom
root: viewportElement,
rootMargin: '100px',
threshold: 0
});
@@ -233,7 +233,7 @@ export function PRList({
return () => {
observer.disconnect();
};
}, [handleIntersection]);
}, [handleIntersection, onLoadMore, viewportElement]);
if (isLoading && prs.length === 0) {
return (
@@ -268,7 +268,7 @@ export function PRList({
}
return (
<ScrollArea className="flex-1" ref={scrollAreaRef}>
<ScrollArea className="flex-1" onViewportRef={setViewportElement}>
<div className="divide-y divide-border">
{prs.map((pr) => {
const reviewState = getReviewStateForPR(pr.number);
@@ -5,23 +5,34 @@ import { cn } from '../../lib/utils';
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> & {
viewportClassName?: string;
}
>(({ className, children, viewportClassName, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
className={cn('h-full w-full rounded-[inherit]', viewportClassName)}
viewportClassName?: string;
onViewportRef?: (element: HTMLDivElement | null) => void;
}
>(({ className, children, viewportClassName, onViewportRef, ...props }, ref) => {
const viewportRef = React.useCallback(
(element: HTMLDivElement | null) => {
onViewportRef?.(element);
},
[onViewportRef]
);
return (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
<ScrollAreaPrimitive.Viewport
ref={viewportRef}
className={cn('h-full w-full rounded-[inherit]', viewportClassName)}
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
});
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<