Skip to content

Commit 33a7102

Browse files
committed
feat: use intersection observable api to detect if should auto scroll
1 parent 5eb8656 commit 33a7102

File tree

2 files changed

+86
-35
lines changed

2 files changed

+86
-35
lines changed

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

+12-35
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ 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';
78
import type { GameLogLine } from '../../types/game.types';
89

910
export interface GameContentProps {
@@ -143,43 +144,19 @@ export const GameContent: React.FC<GameContentProps> = (
143144
});
144145

145146
const scrollableRef = useRef<HTMLDivElement>(null);
146-
const scrollBottomRef = useRef<HTMLDivElement>(null);
147+
const scrollTargetRef = useRef<HTMLDivElement>(null);
147148

148-
const [autoScrollEnabled, setAutoScrollEnabled] = useState<boolean>(
149-
enableScrollToNewLogLines
150-
);
151-
152-
useEffect(() => {
153-
if (!enableScrollToNewLogLines) {
154-
return;
155-
}
156-
157-
let scrollableElmt = scrollableRef.current;
158-
159-
const onScroll = () => {
160-
scrollableElmt = scrollableRef.current;
161-
162-
if (!scrollableElmt) {
163-
return;
164-
}
165-
166-
const { scrollTop, scrollHeight, clientHeight } = scrollableElmt;
167-
const difference = scrollHeight - clientHeight - scrollTop;
168-
const enableAutoScroll = difference <= clientHeight;
169-
170-
setAutoScrollEnabled(enableAutoScroll);
171-
};
172-
173-
scrollableElmt?.addEventListener('scroll', onScroll);
174-
175-
return () => {
176-
scrollableElmt?.removeEventListener('scroll', onScroll);
177-
};
178-
}, [enableScrollToNewLogLines]);
149+
const [isScrollTargetVisible] = useIsElementVisible({
150+
root: scrollableRef.current,
151+
target: scrollTargetRef.current,
152+
threshold: 0.8,
153+
});
179154

180155
useEffect(() => {
181-
if (autoScrollEnabled) {
182-
scrollBottomRef.current?.scrollIntoView({
156+
// If the user is scrolled to the bottom, then continue
157+
// to scroll to the bottom as new log lines are added.
158+
if (enableScrollToNewLogLines && isScrollTargetVisible) {
159+
scrollTargetRef.current?.scrollIntoView({
183160
behavior: 'instant',
184161
block: 'end',
185162
inline: 'nearest',
@@ -203,7 +180,7 @@ export const GameContent: React.FC<GameContentProps> = (
203180
);
204181
})}
205182
<EuiSpacer size="s" />
206-
<div ref={scrollBottomRef} />
183+
<div ref={scrollTargetRef} />
207184
</EuiPanel>
208185
);
209186
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { useCallback, useEffect, useMemo, useState } from 'react';
2+
3+
export interface UseIsElementVisibleOptions {
4+
/**
5+
* Where to attach the observer.
6+
* The target must be a child of this element.
7+
*/
8+
root: Element | null;
9+
/**
10+
* The element to track its visibility.
11+
* The target must be a child of the root element.
12+
*/
13+
target: Element | null;
14+
/**
15+
* Either a single number or an array of numbers which indicate
16+
* at what percentage of the target's visibility the observer's
17+
* callback should be executed.
18+
*
19+
* The values must be in the range of 0.0 and 1.0.
20+
*
21+
* Default is 1.0
22+
*/
23+
threshold?: number | Array<number>;
24+
}
25+
26+
export type UseIsElementVisibleResult = [isVisible: boolean];
27+
28+
/**
29+
* Hook that implements the Intersection Observer API.
30+
* https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
31+
*/
32+
export function useIsElementVisible(
33+
options: UseIsElementVisibleOptions
34+
): UseIsElementVisibleResult {
35+
const { root, target, threshold = 1 } = options;
36+
37+
const [isVisible, setIsVisible] = useState(false);
38+
39+
const onObserveCallback = useCallback<IntersectionObserverCallback>(
40+
(entries: Array<IntersectionObserverEntry>) => {
41+
// This hook only observes one target, so we can safely assume
42+
// that the first entry is the one we care about.
43+
const [entry] = entries;
44+
setIsVisible(entry.isIntersecting);
45+
},
46+
[]
47+
);
48+
49+
const observer = useMemo<IntersectionObserver>(() => {
50+
const observer = new IntersectionObserver(onObserveCallback, {
51+
root,
52+
threshold,
53+
});
54+
55+
return observer;
56+
}, [root, threshold, onObserveCallback]);
57+
58+
// For some reason, if we have a dependency array then
59+
// the observations stop working. Seems excessive, but
60+
// the only way I got this to work is to run on each render.
61+
useEffect(() => {
62+
if (target) {
63+
observer.observe(target);
64+
}
65+
66+
return () => {
67+
if (target) {
68+
observer.unobserve(target);
69+
}
70+
};
71+
});
72+
73+
return [isVisible];
74+
}

0 commit comments

Comments
 (0)