source dump of claude code
at main 171 lines 5.0 kB view raw
1import { useCallback, useMemo, useRef } from 'react' 2 3const DEFAULT_MAX_VISIBLE = 5 4 5type UsePaginationOptions = { 6 totalItems: number 7 maxVisible?: number 8 selectedIndex?: number 9} 10 11type UsePaginationResult<T> = { 12 // For backwards compatibility with page-based terminology 13 currentPage: number 14 totalPages: number 15 startIndex: number 16 endIndex: number 17 needsPagination: boolean 18 pageSize: number 19 // Get visible slice of items 20 getVisibleItems: (items: T[]) => T[] 21 // Convert visible index to actual index 22 toActualIndex: (visibleIndex: number) => number 23 // Check if actual index is visible 24 isOnCurrentPage: (actualIndex: number) => boolean 25 // Navigation (kept for API compatibility) 26 goToPage: (page: number) => void 27 nextPage: () => void 28 prevPage: () => void 29 // Handle selection - just updates the index, scrolling is automatic 30 handleSelectionChange: ( 31 newIndex: number, 32 setSelectedIndex: (index: number) => void, 33 ) => void 34 // Page navigation - returns false for continuous scrolling (not needed) 35 handlePageNavigation: ( 36 direction: 'left' | 'right', 37 setSelectedIndex: (index: number) => void, 38 ) => boolean 39 // Scroll position info for UI display 40 scrollPosition: { 41 current: number 42 total: number 43 canScrollUp: boolean 44 canScrollDown: boolean 45 } 46} 47 48export function usePagination<T>({ 49 totalItems, 50 maxVisible = DEFAULT_MAX_VISIBLE, 51 selectedIndex = 0, 52}: UsePaginationOptions): UsePaginationResult<T> { 53 const needsPagination = totalItems > maxVisible 54 55 // Use a ref to track the previous scroll offset for smooth scrolling 56 const scrollOffsetRef = useRef(0) 57 58 // Compute the scroll offset based on selectedIndex 59 // This ensures the selected item is always visible 60 const scrollOffset = useMemo(() => { 61 if (!needsPagination) return 0 62 63 const prevOffset = scrollOffsetRef.current 64 65 // If selected item is above the visible window, scroll up 66 if (selectedIndex < prevOffset) { 67 scrollOffsetRef.current = selectedIndex 68 return selectedIndex 69 } 70 71 // If selected item is below the visible window, scroll down 72 if (selectedIndex >= prevOffset + maxVisible) { 73 const newOffset = selectedIndex - maxVisible + 1 74 scrollOffsetRef.current = newOffset 75 return newOffset 76 } 77 78 // Selected item is within visible window, keep current offset 79 // But ensure offset is still valid 80 const maxOffset = Math.max(0, totalItems - maxVisible) 81 const clampedOffset = Math.min(prevOffset, maxOffset) 82 scrollOffsetRef.current = clampedOffset 83 return clampedOffset 84 }, [selectedIndex, maxVisible, needsPagination, totalItems]) 85 86 const startIndex = scrollOffset 87 const endIndex = Math.min(scrollOffset + maxVisible, totalItems) 88 89 const getVisibleItems = useCallback( 90 (items: T[]): T[] => { 91 if (!needsPagination) return items 92 return items.slice(startIndex, endIndex) 93 }, 94 [needsPagination, startIndex, endIndex], 95 ) 96 97 const toActualIndex = useCallback( 98 (visibleIndex: number): number => { 99 return startIndex + visibleIndex 100 }, 101 [startIndex], 102 ) 103 104 const isOnCurrentPage = useCallback( 105 (actualIndex: number): boolean => { 106 return actualIndex >= startIndex && actualIndex < endIndex 107 }, 108 [startIndex, endIndex], 109 ) 110 111 // These are mostly no-ops for continuous scrolling but kept for API compatibility 112 const goToPage = useCallback((_page: number) => { 113 // No-op - scrolling is controlled by selectedIndex 114 }, []) 115 116 const nextPage = useCallback(() => { 117 // No-op - scrolling is controlled by selectedIndex 118 }, []) 119 120 const prevPage = useCallback(() => { 121 // No-op - scrolling is controlled by selectedIndex 122 }, []) 123 124 // Simple selection handler - just updates the index 125 // Scrolling happens automatically via the useMemo above 126 const handleSelectionChange = useCallback( 127 (newIndex: number, setSelectedIndex: (index: number) => void) => { 128 const clampedIndex = Math.max(0, Math.min(newIndex, totalItems - 1)) 129 setSelectedIndex(clampedIndex) 130 }, 131 [totalItems], 132 ) 133 134 // Page navigation - disabled for continuous scrolling 135 const handlePageNavigation = useCallback( 136 ( 137 _direction: 'left' | 'right', 138 _setSelectedIndex: (index: number) => void, 139 ): boolean => { 140 return false 141 }, 142 [], 143 ) 144 145 // Calculate page-like values for backwards compatibility 146 const totalPages = Math.max(1, Math.ceil(totalItems / maxVisible)) 147 const currentPage = Math.floor(scrollOffset / maxVisible) 148 149 return { 150 currentPage, 151 totalPages, 152 startIndex, 153 endIndex, 154 needsPagination, 155 pageSize: maxVisible, 156 getVisibleItems, 157 toActualIndex, 158 isOnCurrentPage, 159 goToPage, 160 nextPage, 161 prevPage, 162 handleSelectionChange, 163 handlePageNavigation, 164 scrollPosition: { 165 current: selectedIndex + 1, 166 total: totalItems, 167 canScrollUp: scrollOffset > 0, 168 canScrollDown: scrollOffset + maxVisible < totalItems, 169 }, 170 } 171}