Skip to content

Commit 7308cd9

Browse files
committed
fix(game-stream): correctly scroll to bottom of content first time it's needed
1 parent dd2b3a1 commit 7308cd9

File tree

1 file changed

+50
-22
lines changed

1 file changed

+50
-22
lines changed

electron/renderer/components/game/game-stream.tsx

+50-22
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,12 @@ export const GameStream: React.FC<GameStreamProps> = (
4848
const appendGameLogLines = useCallback(
4949
(newLogLines: Array<GameLogLine>) => {
5050
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.
5454
newLogLines = newLogLines.slice(maxLines * -1);
55-
return newLogLines;
56-
});
55+
return newLogLines;
56+
});
5757
},
5858
[maxLines]
5959
);
@@ -96,31 +96,59 @@ export const GameStream: React.FC<GameStreamProps> = (
9696
// https://css-tricks.com/books/greatest-css-tricks/pin-scrolling-to-bottom/
9797
const scrollableRef = useRef<HTMLDivElement>(null);
9898
const scrollTargetRef = useRef<HTMLDivElement>(null);
99-
const didInitialScrollRef = useRef<boolean>(false);
99+
const observedTargetCountRef = useRef<number>(0);
100100

101101
// The scroll behavior of `overflowAnchor: auto` doesn't take effect
102102
// 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
104104
// added to the scrollable element to warrant an initial scroll.
105105
// After that, the browser handles it automatically.
106106
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+
});
121137
});
138+
};
139+
140+
const observer = new IntersectionObserver(callback, {
141+
threshold: 1.0,
142+
});
143+
144+
if (scrollTargetRef.current) {
145+
observer.observe(scrollTargetRef.current);
122146
}
123-
});
147+
148+
return () => {
149+
observer.disconnect();
150+
};
151+
}, []);
124152

125153
return (
126154
<EuiPanel

0 commit comments

Comments
 (0)