@@ -4,7 +4,6 @@ import { useObservable, useSubscription } from 'observable-hooks';
4
4
import type { ReactNode } from 'react' ;
5
5
import { useCallback , useEffect , useRef , useState } from 'react' ;
6
6
import * as rxjs from 'rxjs' ;
7
- import { useIsElementVisible } from '../../hooks/is-element-visible' ;
8
7
import type { GameLogLine } from '../../types/game.types' ;
9
8
10
9
export interface GameStreamProps {
@@ -113,7 +112,7 @@ export const GameStream: React.FC<GameStreamProps> = (
113
112
} ) ;
114
113
} , [ ] ) ;
115
114
116
- const clearStreamTimeoutRef = useRef < NodeJS . Timeout | undefined > ( undefined ) ;
115
+ const clearStreamTimeoutRef = useRef < NodeJS . Timeout > ( ) ;
117
116
118
117
// Ensure all timeouts are cleared when the component is unmounted.
119
118
useEffect ( ( ) => {
@@ -142,20 +141,29 @@ export const GameStream: React.FC<GameStreamProps> = (
142
141
}
143
142
} ) ;
144
143
144
+ // Scroll to the bottom of the scrollable element when new content is added.
145
+ // https://css-tricks.com/books/greatest-css-tricks/pin-scrolling-to-bottom/
145
146
const scrollableRef = useRef < HTMLDivElement > ( null ) ;
146
147
const scrollTargetRef = useRef < HTMLDivElement > ( null ) ;
148
+ const didInitialScrollRef = useRef < boolean > ( false ) ;
147
149
148
- const [ isScrollTargetVisible ] = useIsElementVisible ( {
149
- root : scrollableRef . current ,
150
- target : scrollTargetRef . current ,
151
- threshold : 0.8 ,
152
- } ) ;
153
-
150
+ // The scroll behavior of `overflowAnchor: auto` doesn't take effect
151
+ // to pin the content to the bottom until after an initial scroll event.
152
+ // Therefore, on each render we check if sufficient content has been
153
+ // added to the scrollable element to warrant an initial scroll.
154
+ // After that, the browser handles it automatically.
154
155
useEffect ( ( ) => {
155
- // If the user is scrolled to the bottom, then continue
156
- // to scroll to the bottom as new log lines are added.
157
- if ( enableScrollToNewLogLines && isScrollTargetVisible ) {
158
- scrollTargetRef . current ?. scrollIntoView ( {
156
+ if (
157
+ // We haven't done an initial scroll yet.
158
+ ! didInitialScrollRef . current &&
159
+ // There's something to scroll to.
160
+ scrollTargetRef . current &&
161
+ scrollableRef . current &&
162
+ // The scrollable element is scrollable.
163
+ scrollableRef . current . scrollHeight > scrollableRef . current . clientHeight
164
+ ) {
165
+ didInitialScrollRef . current = true ;
166
+ scrollTargetRef . current . scrollIntoView ( {
159
167
behavior : 'instant' ,
160
168
block : 'end' ,
161
169
inline : 'nearest' ,
@@ -172,13 +180,15 @@ export const GameStream: React.FC<GameStreamProps> = (
172
180
hasBorder = { false }
173
181
hasShadow = { false }
174
182
>
175
- { gameLogLines . map ( ( logLine ) => {
176
- return (
177
- < EuiText key = { logLine . eventId } css = { logLine . styles } >
178
- < span dangerouslySetInnerHTML = { { __html : logLine . text } } />
179
- </ EuiText >
180
- ) ;
181
- } ) }
183
+ < div css = { { overflowAnchor : 'none' } } >
184
+ { gameLogLines . map ( ( logLine ) => {
185
+ return (
186
+ < EuiText key = { logLine . eventId } css = { logLine . styles } >
187
+ < span dangerouslySetInnerHTML = { { __html : logLine . text } } />
188
+ </ EuiText >
189
+ ) ;
190
+ } ) }
191
+ </ div >
182
192
< EuiSpacer size = "s" />
183
193
< div ref = { scrollTargetRef } />
184
194
</ EuiPanel >
0 commit comments