@@ -48,12 +48,12 @@ export const GameStream: React.FC<GameStreamProps> = (
48
48
const appendGameLogLines = useCallback (
49
49
( newLogLines : Array < GameLogLine > ) => {
50
50
setGameLogLines ( ( oldLogLines : Array < GameLogLine > ) : Array < GameLogLine > => {
51
- // Append new log line to the list.
52
- newLogLines = oldLogLines . concat ( newLogLines ) ;
53
- // Trim the back of the list to keep it within the scrollback buffer.
51
+ // Append new log line to the list.
52
+ newLogLines = oldLogLines . concat ( newLogLines ) ;
53
+ // Trim the back of the list to keep it within the scrollback buffer.
54
54
newLogLines = newLogLines . slice ( maxLines * - 1 ) ;
55
- return newLogLines ;
56
- } ) ;
55
+ return newLogLines ;
56
+ } ) ;
57
57
} ,
58
58
[ maxLines ]
59
59
) ;
@@ -96,31 +96,59 @@ export const GameStream: React.FC<GameStreamProps> = (
96
96
// https://css-tricks.com/books/greatest-css-tricks/pin-scrolling-to-bottom/
97
97
const scrollableRef = useRef < HTMLDivElement > ( null ) ;
98
98
const scrollTargetRef = useRef < HTMLDivElement > ( null ) ;
99
- const didInitialScrollRef = useRef < boolean > ( false ) ;
99
+ const observedTargetCountRef = useRef < number > ( 0 ) ;
100
100
101
101
// The scroll behavior of `overflowAnchor: auto` doesn't take effect
102
102
// to pin the content to the bottom until after an initial scroll event.
103
- // Therefore, on each render we check if sufficient content has been
103
+ // Therefore, we observe the target to know if sufficient content has been
104
104
// added to the scrollable element to warrant an initial scroll.
105
105
// After that, the browser handles it automatically.
106
106
useEffect ( ( ) => {
107
- if (
108
- // We haven't done an initial scroll yet.
109
- ! didInitialScrollRef . current &&
110
- // There's something to scroll to.
111
- scrollTargetRef . current &&
112
- scrollableRef . current &&
113
- // The scrollable element is scrollable.
114
- scrollableRef . current . scrollHeight > scrollableRef . current . clientHeight
115
- ) {
116
- didInitialScrollRef . current = true ;
117
- scrollTargetRef . current . scrollIntoView ( {
118
- behavior : 'instant' ,
119
- block : 'end' ,
120
- inline : 'nearest' ,
107
+ const callback : IntersectionObserverCallback = (
108
+ entries : Array < IntersectionObserverEntry >
109
+ ) => {
110
+ // The callback receives an entry for each observed target.
111
+ // In practice, we are only observing one target so we loop once.
112
+ entries . forEach ( ( entry ) => {
113
+ // When the component is first rendering, there is a period where
114
+ // there is no content and the scroll target is not visible.
115
+ // The observer invokes the callback that initial time, but we
116
+ // don't actually want to scroll to the bottom then, it's too soon.
117
+ // So we ignore the first invocation and only scroll on the second.
118
+ observedTargetCountRef . current += 1 ;
119
+ if ( observedTargetCountRef . current <= 1 ) {
120
+ return ;
121
+ }
122
+ // If the scroll target is visible, nothing to do yet.
123
+ if ( entry . isIntersecting ) {
124
+ return ;
125
+ }
126
+ // The scroll target is now not visible, meaning that there's
127
+ // enough content on screen to cause the window to scroll.
128
+ // Perform our initial scroll to bottom and disconnect the observer.
129
+ // From now on, if the user scrolls away that's fine, we won't keep
130
+ // it pinned to bottom until they scroll back to bottom.
131
+ observer . disconnect ( ) ;
132
+ scrollTargetRef . current ?. scrollIntoView ( {
133
+ behavior : 'instant' ,
134
+ block : 'end' ,
135
+ inline : 'nearest' ,
136
+ } ) ;
121
137
} ) ;
138
+ } ;
139
+
140
+ const observer = new IntersectionObserver ( callback , {
141
+ threshold : 1.0 ,
142
+ } ) ;
143
+
144
+ if ( scrollTargetRef . current ) {
145
+ observer . observe ( scrollTargetRef . current ) ;
122
146
}
123
- } ) ;
147
+
148
+ return ( ) => {
149
+ observer . disconnect ( ) ;
150
+ } ;
151
+ } , [ ] ) ;
124
152
125
153
return (
126
154
< EuiPanel
0 commit comments