Skip to content

Commit cac1618

Browse files
committed
fix: pin scroll to bottom
1 parent 3648dbb commit cac1618

File tree

2 files changed

+29
-93
lines changed

2 files changed

+29
-93
lines changed

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

+29-19
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useObservable, useSubscription } from 'observable-hooks';
44
import type { ReactNode } from 'react';
55
import { useCallback, useEffect, useRef, useState } from 'react';
66
import * as rxjs from 'rxjs';
7-
import { useIsElementVisible } from '../../hooks/is-element-visible';
87
import type { GameLogLine } from '../../types/game.types';
98

109
export interface GameStreamProps {
@@ -113,7 +112,7 @@ export const GameStream: React.FC<GameStreamProps> = (
113112
});
114113
}, []);
115114

116-
const clearStreamTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
115+
const clearStreamTimeoutRef = useRef<NodeJS.Timeout>();
117116

118117
// Ensure all timeouts are cleared when the component is unmounted.
119118
useEffect(() => {
@@ -142,20 +141,29 @@ export const GameStream: React.FC<GameStreamProps> = (
142141
}
143142
});
144143

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/
145146
const scrollableRef = useRef<HTMLDivElement>(null);
146147
const scrollTargetRef = useRef<HTMLDivElement>(null);
148+
const didInitialScrollRef = useRef<boolean>(false);
147149

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.
154155
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({
159167
behavior: 'instant',
160168
block: 'end',
161169
inline: 'nearest',
@@ -172,13 +180,15 @@ export const GameStream: React.FC<GameStreamProps> = (
172180
hasBorder={false}
173181
hasShadow={false}
174182
>
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>
182192
<EuiSpacer size="s" />
183193
<div ref={scrollTargetRef} />
184194
</EuiPanel>

electron/renderer/hooks/is-element-visible.tsx

-74
This file was deleted.

0 commit comments

Comments
 (0)