@@ -421,19 +421,69 @@ export class CollectionBrowser
421421 const model = this . dataSource . getTileModelAt ( index ) ;
422422 /**
423423 * If we encounter a model we don't have yet and we're not in the middle of an
424- * automated scroll, fetch the page and just return undefined.
424+ * automated scroll, schedule a fetch for the missing page and return undefined.
425425 * The datasource will be updated once the page is loaded and the cell will be rendered.
426426 *
427427 * We disable it during the automated scroll since we don't want to fetch pages for intervening cells the
428428 * user may never see.
429429 */
430430 if ( ! model && ! this . isScrollingToCell && this . dataSource . queryInitialized ) {
431431 const pageNumber = Math . floor ( index / this . pageSize ) + 1 ;
432- this . dataSource . fetchPage ( pageNumber ) ;
432+ this . scheduleDeferredPageFetch ( pageNumber ) ;
433433 }
434434 return model ;
435435 }
436436
437+ /**
438+ * Debounce delay for page fetches initiated by new cells becoming visible.
439+ * Tuned so quick scrolling through unloaded regions doesn't send rapid-fire
440+ * search requests for every page we pass through, but to still feel responsive
441+ * when the scroll ends.
442+ */
443+ private static readonly DEFERRED_FETCH_DELAY_MS = 150 ;
444+
445+ private deferredFetchTimer = 0 ;
446+
447+ /**
448+ * Schedules a fetch for the given page, debounced to ensure we don't
449+ * rapid-fire fetches while scrolling through pages quickly.
450+ *
451+ * If there's no pending fetch timer yet, it will fire a fetch immediately.
452+ * Otherwise, it will reset any existing timer. In either case, a deferred
453+ * fetch for the visible pages is scheduled after a brief delay to account
454+ * for whatever pages we land on after scrolling.
455+ */
456+ private scheduleDeferredPageFetch ( pageNumber : number ) : void {
457+ if ( ! this . deferredFetchTimer ) {
458+ this . dataSource . fetchPage ( pageNumber ) ;
459+ } else {
460+ window . clearTimeout ( this . deferredFetchTimer ) ;
461+ }
462+
463+ this . deferredFetchTimer = window . setTimeout ( ( ) => {
464+ this . deferredFetchTimer = 0 ;
465+ this . fetchVisiblePages ( ) ;
466+ } , CollectionBrowser . DEFERRED_FETCH_DELAY_MS ) ;
467+ }
468+
469+ /**
470+ * Fetch each currently-visible page whose first cell still has no
471+ * loaded model.
472+ */
473+ private fetchVisiblePages ( ) : void {
474+ const visibleIndices = this . infiniteScroller ?. getVisibleCellIndices ( ) ?? [ ] ;
475+ const visiblePages = new Set (
476+ visibleIndices . map ( i => Math . floor ( i / this . pageSize ) + 1 ) ,
477+ ) ;
478+
479+ for ( const page of visiblePages ) {
480+ const firstCellOfPage = ( page - 1 ) * this . pageSize ;
481+ if ( ! this . dataSource . getTileModelAt ( firstCellOfPage ) ) {
482+ this . dataSource . fetchPage ( page ) ;
483+ }
484+ }
485+ }
486+
437487 // this is the total number of tiles we expect if
438488 // the data returned is a full page worth
439489 // this is useful for putting in placeholders for the expected number of tiles
@@ -866,6 +916,8 @@ export class CollectionBrowser
866916 class= ${ this . infiniteScrollerClasses }
867917 itemCount= ${ this . placeholderType ? 0 : nothing }
868918 ariaLand markLabel= "Search results"
919+ .estimatedCellHeight = ${ this . estimatedTileHeight }
920+ .minBufferMarginCells = ${ this . pageSize }
869921 .cellProvider = ${ this }
870922 .placeholderCellTemplate = ${ this . placeholderCellTemplate }
871923 @scrollThresholdReached = ${ this . scrollThresholdReached }
@@ -887,6 +939,25 @@ export class CollectionBrowser
887939 } ) ;
888940 }
889941
942+ /**
943+ * Best-effort hint of how tall a single rendered tile is, by display mode.
944+ * The scroller uses this to better estimate the size its initial scroll
945+ * spacer and buffer position before real cell heights are measured.
946+ * Should roughly match the placeholder heights since the initial render
947+ * of a new page generally shows placeholders only anyway.
948+ */
949+ private get estimatedTileHeight ( ) : number {
950+ switch ( this . displayMode ) {
951+ case 'list-detail' :
952+ return 80 ;
953+ case 'list-compact' :
954+ return 45 ;
955+ case 'grid' :
956+ default :
957+ return 225 ;
958+ }
959+ }
960+
890961 /**
891962 * Template for the sort & filtering bar that appears atop the search results.
892963 */
@@ -1098,7 +1169,7 @@ export class CollectionBrowser
10981169 if ( ( this . currentPage ?? 1 ) > 1 ) {
10991170 this . goToPage ( 1 ) ;
11001171 }
1101- this . currentPage = 1 ;
1172+ this . setCurrentPage ( 1 ) ;
11021173 }
11031174
11041175 /**
@@ -1871,6 +1942,11 @@ export class CollectionBrowser
18711942 window . removeEventListener ( 'popstate' , this . boundNavigationHandler ) ;
18721943 }
18731944
1945+ if ( this . deferredFetchTimer ) {
1946+ window . clearTimeout ( this . deferredFetchTimer ) ;
1947+ this . deferredFetchTimer = 0 ;
1948+ }
1949+
18741950 this . leftColIntersectionObserver ?. disconnect ( ) ;
18751951 this . facetsIntersectionObserver ?. disconnect ( ) ;
18761952 window . removeEventListener ( 'resize' , this . updateLeftColumnHeight ) ;
@@ -2123,27 +2199,45 @@ export class CollectionBrowser
21232199 private visibleCellsChanged (
21242200 e : CustomEvent < { visibleCellIndices : number [ ] } > ,
21252201 ) {
2202+ this . updateVisiblePage ( e . detail . visibleCellIndices ) ;
2203+ }
2204+
2205+ /**
2206+ * Recomputes the current page from the given set of visible cell indices
2207+ * and emits `visiblePageChanged` if the page actually changed.
2208+ */
2209+ private updateVisiblePage ( visibleCellIndices : number [ ] ) : void {
21262210 if ( this . isScrollingToCell ) return ;
2127- const { visibleCellIndices } = e . detail ;
21282211 if ( visibleCellIndices . length === 0 ) return ;
21292212
2213+ // The indices aren't necessarily sorted, so sort them here to ensure our
2214+ // calculations below find the right cell/page.
2215+ const sorted = [ ...visibleCellIndices ] . sort ( ( a , b ) => a - b ) ;
2216+
21302217 // For page determination, do not count more than a single page of visible cells,
21312218 // since otherwise patrons using very tall screens will be treated as one page
21322219 // further than they actually are.
21332220 const lastIndexWithinCurrentPage =
2134- Math . min ( this . pageSize , visibleCellIndices . length ) - 1 ;
2135- const lastVisibleCellIndex = visibleCellIndices [ lastIndexWithinCurrentPage ] ;
2221+ Math . min ( this . pageSize , sorted . length ) - 1 ;
2222+ const lastVisibleCellIndex = sorted [ lastIndexWithinCurrentPage ] ;
21362223 const lastVisibleCellPage =
21372224 Math . floor ( lastVisibleCellIndex / this . pageSize ) + 1 ;
2138- if ( this . currentPage !== lastVisibleCellPage ) {
2139- this . currentPage = lastVisibleCellPage ;
2140- }
2141- const event = new CustomEvent ( 'visiblePageChanged' , {
2142- detail : {
2143- pageNumber : lastVisibleCellPage ,
2144- } ,
2145- } ) ;
2146- this . dispatchEvent ( event ) ;
2225+
2226+ this . setCurrentPage ( lastVisibleCellPage ) ;
2227+ }
2228+
2229+ /**
2230+ * Sets the current page number and emits a `visiblePageChanged`
2231+ * event if the new page differs from the previous one.
2232+ */
2233+ private setCurrentPage ( pageNumber : number ) : void {
2234+ if ( this . currentPage === pageNumber ) return ;
2235+ this . currentPage = pageNumber ;
2236+ this . dispatchEvent (
2237+ new CustomEvent ( 'visiblePageChanged' , {
2238+ detail : { pageNumber } ,
2239+ } ) ,
2240+ ) ;
21472241 }
21482242
21492243 // we only want to scroll on the very first query change
@@ -2243,10 +2337,10 @@ export class CollectionBrowser
22432337 this . selectedCreatorFilter = restorationState . selectedCreatorFilter ?? null ;
22442338 this . selectedFacets = restorationState . selectedFacets ;
22452339 if ( ! this . suppressURLQuery ) this . baseQuery = restorationState . baseQuery ;
2246- this . currentPage = restorationState . currentPage ?? 1 ;
2340+ this . setCurrentPage ( restorationState . currentPage ?? 1 ) ;
22472341 this . minSelectedDate = restorationState . minSelectedDate ;
22482342 this . maxSelectedDate = restorationState . maxSelectedDate ;
2249- if ( this . currentPage > 1 ) {
2343+ if ( this . currentPage && this . currentPage > 1 ) {
22502344 this . goToPage ( this . currentPage ) ;
22512345 }
22522346 }
@@ -2323,25 +2417,43 @@ export class CollectionBrowser
23232417 } ) ;
23242418 }
23252419
2326- private scrollToPage ( pageNumber : number ) : Promise < void > {
2327- return new Promise ( resolve => {
2328- const cellIndexToScrollTo = this . pageSize * ( pageNumber - 1 ) ;
2329- // without this setTimeout, Safari just pauses until the `fetchPage` is complete
2330- // then scrolls to the cell
2331- setTimeout ( ( ) => {
2332- this . isScrollingToCell = true ;
2333- this . infiniteScroller ?. scrollToCell ( cellIndexToScrollTo , true ) ;
2334- // This timeout is to give the scroll animation time to finish
2335- // then updating the infinite scroller once we're done scrolling
2336- // There's no scroll animation completion callback so we're
2337- // giving it 0.5s to finish.
2338- setTimeout ( ( ) => {
2339- this . isScrollingToCell = false ;
2340- this . infiniteScroller ?. refreshAllVisibleCells ( ) ;
2341- resolve ( ) ;
2342- } , 500 ) ;
2343- } , 0 ) ;
2420+ private async scrollToPage ( pageNumber : number ) : Promise < void > {
2421+ const cellIndexToScrollTo = this . pageSize * ( pageNumber - 1 ) ;
2422+
2423+ // Wait for the infinite scroller be rendered before proceeding
2424+ let waitAttempts = 0 ;
2425+ while ( ! this . infiniteScroller && waitAttempts < 20 ) {
2426+ await this . updateComplete ;
2427+ waitAttempts ++ ;
2428+ }
2429+ if ( ! this . infiniteScroller ) return ;
2430+
2431+ // The scroller have its default `itemCount=0`, so propagate our estimated
2432+ // tile count before jumping to the desired page.
2433+ if ( this . infiniteScroller . itemCount < this . estimatedTileCount ) {
2434+ this . infiniteScroller . itemCount = this . estimatedTileCount ;
2435+ await this . updateComplete ;
2436+ }
2437+
2438+ // Without this setTimeout(0), Safari just pauses until the `fetchPage`
2439+ // is complete then scrolls to the cell.
2440+ await new Promise < void > ( resolve => {
2441+ setTimeout ( resolve , 0 ) ;
23442442 } ) ;
2443+
2444+ this . isScrollingToCell = true ;
2445+ const scrolled = await this . infiniteScroller . scrollToCell (
2446+ cellIndexToScrollTo ,
2447+ true ,
2448+ ) ;
2449+ this . isScrollingToCell = false ;
2450+ this . infiniteScroller . refreshAllVisibleCells ( ) ;
2451+
2452+ // After we finish scrolling, recompute the visible page from the new state
2453+ // so that it doesn't fall out of sync.
2454+ if ( scrolled ) {
2455+ this . updateVisiblePage ( this . infiniteScroller . getVisibleCellIndices ( ) ) ;
2456+ }
23452457 }
23462458
23472459 /**
0 commit comments