11import {
22 Alert ,
3+ Box ,
34 Button ,
4- List ,
5+ List as MuiList ,
56 ListItem ,
67 ListItemText ,
78 Typography ,
@@ -10,9 +11,13 @@ import {
1011} from "@mui/material" ;
1112import { g3BorderRadius , G3_PRESETS } from "@/theme/g3Curves" ;
1213import { SearchResultItem } from "./SearchResultItem" ;
14+ import { getHighlightRegex } from "./utils" ;
1315import type { RepoSearchItem } from "@/hooks/github/useRepoSearch" ;
1416import { useI18n } from "@/contexts/I18nContext" ;
15- import React from "react" ;
17+ import React , { useMemo } from "react" ;
18+ import { AutoSizer } from "react-virtualized-auto-sizer" ;
19+ import { List as VirtualList , useDynamicRowHeight } from "react-window" ;
20+ import type { RowComponentProps } from "react-window" ;
1621
1722interface SearchResultsProps {
1823 items : RepoSearchItem [ ] ;
@@ -28,6 +33,51 @@ interface SearchResultsProps {
2833 disableSearchButton : boolean ;
2934}
3035
36+ interface SearchResultRowData {
37+ items : RepoSearchItem [ ] ;
38+ keyword : string ;
39+ keywordLower : string ;
40+ highlightRegex : RegExp | null ;
41+ isSmallScreen : boolean ;
42+ onResultClick : ( item : RepoSearchItem ) => void ;
43+ onOpenGithub : ( item : RepoSearchItem ) => void ;
44+ }
45+
46+ const VIRTUALIZE_THRESHOLD = 30 ;
47+
48+ const SearchResultRow = ( {
49+ ariaAttributes,
50+ index,
51+ style,
52+ ...rowData
53+ } : RowComponentProps < SearchResultRowData > ) : React . ReactElement => {
54+ const item = rowData . items [ index ] ;
55+
56+ if ( item === undefined ) {
57+ return (
58+ < div
59+ style = { style }
60+ { ...ariaAttributes }
61+ aria-hidden = "true"
62+ />
63+ ) ;
64+ }
65+
66+ return (
67+ < SearchResultItem
68+ item = { item }
69+ keyword = { rowData . keyword }
70+ keywordLower = { rowData . keywordLower }
71+ highlightRegex = { rowData . highlightRegex }
72+ isSmallScreen = { rowData . isSmallScreen }
73+ onClick = { rowData . onResultClick }
74+ onOpenGithub = { rowData . onOpenGithub }
75+ style = { style }
76+ ariaAttributes = { ariaAttributes }
77+ />
78+ ) ;
79+ } ;
80+
3181export const SearchResults : React . FC < SearchResultsProps > = ( {
3282 items,
3383 keyword,
@@ -41,6 +91,33 @@ export const SearchResults: React.FC<SearchResultsProps> = ({
4191 const theme = useTheme ( ) ;
4292 const isSmallScreen = useMediaQuery ( theme . breakpoints . down ( 'sm' ) ) ;
4393 const { t } = useI18n ( ) ;
94+ const listHeight = isSmallScreen ? 300 : 400 ;
95+ const shouldVirtualize = items . length >= VIRTUALIZE_THRESHOLD ;
96+ const keywordLower = useMemo ( ( ) => keyword . toLowerCase ( ) , [ keyword ] ) ;
97+ const highlightRegex = useMemo ( ( ) => getHighlightRegex ( keyword ) , [ keyword ] ) ;
98+ const defaultRowHeight = isSmallScreen ? 72 : 88 ;
99+ const dynamicRowHeight = useDynamicRowHeight ( { defaultRowHeight, key : keyword } ) ;
100+
101+ const rowData = useMemo (
102+ ( ) : SearchResultRowData => ( {
103+ items,
104+ keyword,
105+ keywordLower,
106+ highlightRegex,
107+ isSmallScreen,
108+ onResultClick,
109+ onOpenGithub
110+ } ) ,
111+ [
112+ items ,
113+ keyword ,
114+ keywordLower ,
115+ highlightRegex ,
116+ isSmallScreen ,
117+ onResultClick ,
118+ onOpenGithub
119+ ]
120+ ) ;
44121
45122 const showEmptyIndexResult =
46123 ! loading &&
@@ -82,40 +159,74 @@ export const SearchResults: React.FC<SearchResultsProps> = ({
82159 </ Alert >
83160 ) }
84161
85- < List
86- sx = { {
87- maxHeight : isSmallScreen ? 300 : 400 ,
88- overflowY : "auto" ,
89- overflowX : "hidden" ,
90- width : "100%"
91- } }
92- >
93- { items . map ( item => (
94- < SearchResultItem
95- key = { `${ item . branch } :${ item . path } ` }
96- item = { item }
97- keyword = { keyword }
98- onClick = { onResultClick }
99- onOpenGithub = { onOpenGithub }
162+ { shouldVirtualize && items . length > 0 ? (
163+ < Box sx = { { height : listHeight , width : "100%" } } >
164+ < AutoSizer
165+ renderProp = { ( { width, height } ) => {
166+ if ( width === undefined || height === undefined ) {
167+ return null ;
168+ }
169+
170+ return (
171+ < VirtualList
172+ rowCount = { items . length }
173+ rowHeight = { dynamicRowHeight }
174+ rowComponent = { SearchResultRow }
175+ rowProps = { rowData }
176+ overscanCount = { 6 }
177+ tagName = "ul"
178+ style = { {
179+ height,
180+ width,
181+ overflowX : "hidden" ,
182+ listStyle : "none" ,
183+ margin : 0 ,
184+ padding : 0
185+ } }
186+ />
187+ ) ;
188+ } }
100189 />
101- ) ) }
102- { searchResult !== null && searchResult . items . length === 0 && (
103- < ListItem >
104- < ListItemText
105- primary = { t ( 'search.results.noResults' ) }
106- secondary = { t ( 'search.results.noResultsHint' ) }
107- slotProps = { {
108- primary : {
109- variant : isSmallScreen ? "body2" : "body1"
110- } ,
111- secondary : {
112- variant : isSmallScreen ? "caption" : "body2"
113- }
114- } }
190+ </ Box >
191+ ) : (
192+ < MuiList
193+ sx = { {
194+ maxHeight : listHeight ,
195+ overflowY : "auto" ,
196+ overflowX : "hidden" ,
197+ width : "100%"
198+ } }
199+ >
200+ { items . map ( item => (
201+ < SearchResultItem
202+ key = { `${ item . branch } :${ item . path } ` }
203+ item = { item }
204+ keyword = { keyword }
205+ keywordLower = { keywordLower }
206+ highlightRegex = { highlightRegex }
207+ isSmallScreen = { isSmallScreen }
208+ onClick = { onResultClick }
209+ onOpenGithub = { onOpenGithub }
115210 />
116- </ ListItem >
117- ) }
118- </ List >
211+ ) ) }
212+ { searchResult !== null && searchResult . items . length === 0 && (
213+ < ListItem >
214+ < ListItemText
215+ primary = { t ( 'search.results.noResults' ) }
216+ secondary = { t ( 'search.results.noResultsHint' ) }
217+ slotProps = { {
218+ primary : {
219+ variant : isSmallScreen ? "body2" : "body1"
220+ } ,
221+ secondary : {
222+ variant : isSmallScreen ? "caption" : "body2"
223+ }
224+ } }
225+ />
226+ </ ListItem >
227+ ) }
228+ </ MuiList >
229+ ) }
119230 </ >
120231 ) ;
121232} ;
0 commit comments