@@ -15,12 +15,12 @@ import { CLUSTER_HEIGHT, DETAIL_HEIGHT, NODE_GAP } from "../ClusterGraph/Cluster
1515import { usePreLoadAuthorImg } from "./Summary.hook" ;
1616import { getInitData , getClusterIds , getClusterById , getCommitLatestTag } from "./Summary.util" ;
1717import { Content } from "./Content" ;
18- import type { ClusterRowProps } from "./Summary.type" ;
18+ import type { ClusterRowProps , SummaryProps } from "./Summary.type" ;
1919
2020const COLLAPSED_ROW_HEIGHT = CLUSTER_HEIGHT + NODE_GAP * 2 ;
2121const EXPANDED_ROW_HEIGHT = DETAIL_HEIGHT + COLLAPSED_ROW_HEIGHT ;
2222
23- const Summary = ( ) => {
23+ const Summary = ( { onLoadMore , isLoadingMore , enabled , isLastPage } : SummaryProps ) => {
2424 const [ filteredData , selectedData , toggleSelectedData ] = useDataStore (
2525 useShallow ( ( state ) => [ state . filteredData , state . selectedData , state . toggleSelectedData ] )
2626 ) ;
@@ -31,17 +31,80 @@ const Summary = () => {
3131 const listRef = useRef < List > ( null ) ;
3232 const clusterSizes = getClusterSizes ( filteredData ) ;
3333
34+ const sentinelRef = useRef < HTMLDivElement > ( null ) ;
35+ const observerRef = useRef < IntersectionObserver | null > ( null ) ;
36+ const isObservingRef = useRef ( false ) ;
37+
38+ // Create IntersectionObserver once and reuse it
39+ useEffect ( ( ) => {
40+ observerRef . current = new IntersectionObserver (
41+ ( entries ) => {
42+ if ( entries [ 0 ] . isIntersecting && ! isLoadingMore && enabled ) {
43+ onLoadMore ( ) ;
44+ }
45+ } ,
46+ {
47+ root : null ,
48+ rootMargin : "100px" ,
49+ threshold : 0.1 ,
50+ }
51+ ) ;
52+
53+ return ( ) => {
54+ if ( observerRef . current ) {
55+ observerRef . current . disconnect ( ) ;
56+ observerRef . current = null ;
57+ }
58+ isObservingRef . current = false ;
59+ } ;
60+ } , [ enabled , isLoadingMore , onLoadMore ] ) ;
61+
62+ // Infinite scroll: Observe sentinel when it's rendered
63+ const handleRowsRendered = ( { stopIndex } : { startIndex : number ; stopIndex : number } ) => {
64+ if ( isSentinelRow ( stopIndex ) && sentinelRef . current && enabled && observerRef . current ) {
65+ if ( ! isObservingRef . current ) {
66+ observerRef . current . observe ( sentinelRef . current ) ;
67+ isObservingRef . current = true ;
68+ }
69+ }
70+ } ;
71+
72+ // Unobserve when sentinel is no longer needed
73+ useEffect ( ( ) => {
74+ if ( isLastPage && observerRef . current && isObservingRef . current ) {
75+ observerRef . current . disconnect ( ) ;
76+ isObservingRef . current = false ;
77+ }
78+ } , [ isLastPage ] ) ;
79+
80+ const isSentinelRow = ( index : number ) => ! isLastPage && index === clusters . length ;
81+
3482 const onClickClusterSummary = ( clusterId : number ) => ( ) => {
3583 const selected = getClusterById ( filteredData , clusterId ) ;
3684 toggleSelectedData ( selected , clusterId ) ;
3785 } ;
3886
3987 const getRowHeight = ( { index } : { index : number } ) => {
88+ if ( isSentinelRow ( index ) ) {
89+ return 10 ;
90+ }
91+
4092 const cluster = clusters [ index ] ;
4193 return selectedClusterIds . includes ( cluster . clusterId ) ? EXPANDED_ROW_HEIGHT : COLLAPSED_ROW_HEIGHT ;
4294 } ;
4395
4496 const rowRenderer = ( props : ListRowProps ) => {
97+ // Render sentinel element
98+ if ( isSentinelRow ( props . index ) ) {
99+ return (
100+ < div
101+ ref = { sentinelRef }
102+ key = { props . index }
103+ style = { props . style }
104+ />
105+ ) ;
106+ }
107+
45108 const cluster = clusters [ props . index ] ;
46109 const isExpanded = selectedClusterIds . includes ( cluster . clusterId ) ;
47110 const { key, ...restProps } = props ;
@@ -80,9 +143,10 @@ const Summary = () => {
80143 ref = { listRef }
81144 width = { width }
82145 height = { height }
83- rowCount = { clusters . length }
146+ rowCount = { isLastPage ? clusters . length : clusters . length + 1 }
84147 rowHeight = { getRowHeight }
85148 rowRenderer = { rowRenderer }
149+ onRowsRendered = { handleRowsRendered }
86150 overscanRowCount = { 15 }
87151 className = "cluster-summary"
88152 />
0 commit comments