@@ -15,6 +15,7 @@ function createChatVirtualizer({
1515 followOnAppend = false ,
1616 threshold = 1 ,
1717 paddingEnd = 0 ,
18+ scrollMargin = 0 ,
1819} : {
1920 messages : Array < { id : string } >
2021 offset : number
@@ -23,15 +24,17 @@ function createChatVirtualizer({
2324 followOnAppend ?: boolean | 'auto' | 'smooth' | 'instant'
2425 threshold ?: number
2526 paddingEnd ?: number
27+ scrollMargin ?: number
2628} ) {
2729 let currentMessages = messages
2830 const scrollToFn = vi . fn ( )
2931 let offsetCb : ( ( offset : number , isScrolling : boolean ) => void ) | null = null
3032 const scrollElement = {
3133 scrollTop : offset ,
3234 scrollLeft : 0 ,
33- // The sizer is `getTotalSize()`, which includes paddingEnd.
34- scrollHeight : messages . length * itemSize + paddingEnd ,
35+ // The sizer is `getTotalSize()`, which includes paddingEnd. Anything
36+ // above the list inside the same scroller adds `scrollMargin` on top.
37+ scrollHeight : scrollMargin + messages . length * itemSize + paddingEnd ,
3538 scrollWidth : 1000 ,
3639 clientHeight : viewportSize ,
3740 clientWidth : 400 ,
@@ -82,6 +85,7 @@ function createChatVirtualizer({
8285 followOnAppend,
8386 scrollEndThreshold : threshold ,
8487 paddingEnd,
88+ scrollMargin,
8589 }
8690 }
8791
@@ -98,7 +102,7 @@ function createChatVirtualizer({
98102 currentMessages = nextMessages
99103 virtualizer . setOptions ( makeOptions ( ) )
100104 ; ( scrollElement as any ) . scrollHeight =
101- nextMessages . length * itemSize + paddingEnd
105+ scrollMargin + nextMessages . length * itemSize + paddingEnd
102106 virtualizer . _willUpdate ( )
103107 } ,
104108 // Simulate the browser's scroll event after a write (or a gesture).
@@ -374,6 +378,40 @@ test('anchorTo:end does not follow streaming growth when user is away from end',
374378 expect ( scrollToFn ) . not . toHaveBeenCalled ( )
375379} )
376380
381+ test ( 'anchorTo:end does not follow streaming growth when user is away from end, with a scrollMargin' , ( ) => {
382+ // Same situation as the test above, with a 150px header sharing the
383+ // scroller. The reader sits 100px above the end, inside the margin.
384+ const messages = Array . from ( { length : 8 } , ( _ , i ) => ( { id : `m-${ i } ` } ) )
385+ const { virtualizer, scrollToFn } = createChatVirtualizer ( {
386+ messages,
387+ offset : 250 ,
388+ scrollMargin : 150 ,
389+ } )
390+
391+ expect ( virtualizer . getDistanceFromEnd ( ) ) . toBe ( 100 )
392+ expect ( virtualizer . isAtEnd ( ) ) . toBe ( false )
393+
394+ virtualizer . resizeItem ( 7 , 120 )
395+
396+ expect ( scrollToFn ) . not . toHaveBeenCalled ( )
397+ } )
398+
399+ test ( 'anchorTo:end still pins an end-pinned viewport with a scrollMargin' , ( ) => {
400+ const messages = Array . from ( { length : 8 } , ( _ , i ) => ( { id : `m-${ i } ` } ) )
401+ const { virtualizer, scrollToFn } = createChatVirtualizer ( {
402+ messages,
403+ offset : 350 ,
404+ scrollMargin : 150 ,
405+ } )
406+
407+ expect ( virtualizer . isAtEnd ( ) ) . toBe ( true )
408+
409+ virtualizer . resizeItem ( 7 , 120 )
410+
411+ expect ( scrollToFn ) . toHaveBeenCalledTimes ( 1 )
412+ expect ( scrollToFn . mock . calls [ 0 ] ! [ 1 ] . adjustments ) . toBe ( 70 )
413+ } )
414+
377415test ( 'isAtEnd and getDistanceFromEnd use the scroll element max offset' , ( ) => {
378416 const messages = Array . from ( { length : 5 } , ( _ , i ) => ( { id : `m-${ i } ` } ) )
379417 const { virtualizer } = createChatVirtualizer ( {
0 commit comments