Skip to content

Commit 844027c

Browse files
committed
feat: auto scroll updates
1 parent 730f3ad commit 844027c

File tree

2 files changed

+145
-17
lines changed

2 files changed

+145
-17
lines changed

electron/renderer/components/scrollable/scrollable.tsx

+11-5
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,14 @@ const Scrollable: React.FC<ScrollableProps> = (
4747
const scrollableRef = useRef<HTMLDivElement>(null);
4848
const scrollBottomRef = useRef<HTMLSpanElement>(null);
4949

50-
const [autoScroll, setAutoScroll] = useState(true);
50+
const [autoScrollEnabled, setAutoScrollEnabled] = useState<boolean>(true);
5151

5252
useEffect(() => {
53-
const scrollableElmt = scrollableRef.current;
53+
let scrollableElmt = scrollableRef.current;
5454

5555
const onScroll = () => {
56+
scrollableElmt = scrollableRef.current;
57+
5658
if (!scrollableElmt) {
5759
return;
5860
}
@@ -61,7 +63,7 @@ const Scrollable: React.FC<ScrollableProps> = (
6163
const difference = scrollHeight - clientHeight - scrollTop;
6264
const enableAutoScroll = difference <= clientHeight;
6365

64-
setAutoScroll(enableAutoScroll);
66+
setAutoScrollEnabled(enableAutoScroll);
6567
};
6668

6769
scrollableElmt?.addEventListener('scroll', onScroll);
@@ -71,8 +73,12 @@ const Scrollable: React.FC<ScrollableProps> = (
7173
};
7274
}, []);
7375

74-
if (autoScroll) {
75-
scrollBottomRef.current?.scrollIntoView({ behavior: 'instant' });
76+
if (autoScrollEnabled) {
77+
scrollBottomRef.current?.scrollIntoView({
78+
behavior: 'instant',
79+
block: 'end',
80+
inline: 'nearest',
81+
});
7682
}
7783

7884
return (

electron/renderer/pages/grid.tsx

+134-12
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
1+
import { EuiText, useEuiTheme } from '@elastic/eui';
2+
import { css } from '@emotion/react';
3+
import { isNil } from 'lodash';
14
import dynamic from 'next/dynamic';
25
import { useObservable, useSubscription } from 'observable-hooks';
36
import type { ReactNode } from 'react';
4-
import { useCallback, useEffect, useState } from 'react';
7+
import { useCallback, useEffect, useRef, useState } from 'react';
58
import * as rxjs from 'rxjs';
69
import { Grid } from '../components/grid';
710
import { useLogger } from '../components/logger';
11+
import { createLogger } from '../lib/logger';
812

913
// The grid dynamically modifies the DOM, so we can't use SSR
1014
// because the server and client DOMs will be out of sync.
1115
// https://nextjs.org/docs/messages/react-hydration-error
1216
const GridNoSSR = dynamic(async () => Grid, { ssr: false });
1317

18+
const dougLogger = createLogger('component:doug-cmp');
19+
1420
interface DougCmpProps {
1521
stream$: rxjs.Observable<{ streamId: string; text: string }>;
1622
}
@@ -21,13 +27,32 @@ const DougCmp: React.FC<DougCmpProps> = (props: DougCmpProps): ReactNode => {
2127
useSubscription(stream$, (stream) => {
2228
if (stream.text === '__CLEAR_STREAM__') {
2329
setGameText([]);
24-
} else {
30+
} else if (!isNil(stream.text)) {
2531
appendGameText(stream.text);
2632
}
2733
});
2834

35+
const { euiTheme } = useEuiTheme();
36+
37+
const scrollableRef = useRef<HTMLDivElement>(null);
38+
const scrollBottomRef = useRef<HTMLSpanElement>(null);
39+
40+
const [autoScrollEnabled, setAutoScrollEnabled] = useState<boolean>(true);
41+
42+
// TODO make this be dynamically created in `appendGameText`
43+
// based on the game event props sent to us
44+
const textStyles = css({
45+
fontFamily: euiTheme.font.family,
46+
fontSize: euiTheme.size.m,
47+
lineHeight: 'initial',
48+
paddingLeft: euiTheme.size.s,
49+
paddingRight: euiTheme.size.s,
50+
});
51+
52+
// TODO make array of object with text and style
2953
const [gameText, setGameText] = useState<Array<string>>([]);
3054

55+
// TODO make callback take in object with text and style
3156
const appendGameText = useCallback((newText: string) => {
3257
const scrollbackBuffer = 500; // max number of most recent lines to keep
3358
newText = newText.replace(/\n/g, '<br/>');
@@ -36,15 +61,62 @@ const DougCmp: React.FC<DougCmpProps> = (props: DougCmpProps): ReactNode => {
3661
});
3762
}, []);
3863

64+
useEffect(() => {
65+
let scrollableElmt = scrollableRef.current;
66+
67+
const onScroll = () => {
68+
scrollableElmt = scrollableRef.current;
69+
70+
if (!scrollableElmt) {
71+
return;
72+
}
73+
74+
const { scrollTop, scrollHeight, clientHeight } = scrollableElmt;
75+
const difference = scrollHeight - clientHeight - scrollTop;
76+
const enableAutoScroll = difference <= clientHeight;
77+
78+
dougLogger.debug('*** onScroll', {
79+
scrollHeight,
80+
clientHeight,
81+
scrollTop,
82+
difference,
83+
enableAutoScroll,
84+
});
85+
86+
setAutoScrollEnabled(enableAutoScroll);
87+
};
88+
89+
scrollableElmt?.addEventListener('scroll', onScroll);
90+
91+
return () => {
92+
scrollableElmt?.removeEventListener('scroll', onScroll);
93+
};
94+
}, []);
95+
96+
if (autoScrollEnabled) {
97+
scrollBottomRef.current?.scrollIntoView({
98+
behavior: 'instant',
99+
block: 'end',
100+
inline: 'nearest',
101+
});
102+
}
103+
39104
return (
40-
<div>
105+
<div
106+
ref={scrollableRef}
107+
className={'eui-yScroll'}
108+
style={{ height: '100%', overflowY: 'scroll' }}
109+
>
41110
{gameText.map((text, index) => {
42111
return (
43-
<span key={index} style={{ fontFamily: 'Verdana' }}>
44-
<span dangerouslySetInnerHTML={{ __html: text }} />
45-
</span>
112+
<EuiText
113+
key={index}
114+
css={textStyles}
115+
dangerouslySetInnerHTML={{ __html: text }}
116+
/>
46117
);
47118
})}
119+
<span ref={scrollBottomRef} />
48120
</div>
49121
);
50122
};
@@ -73,12 +145,6 @@ const GridPage: React.FC = (): ReactNode => {
73145
// Re-emit text events to the game stream subject to get to grid items.
74146
useSubscription(gameEventsSubject$, (gameEvent) => {
75147
switch (gameEvent.type) {
76-
case 'TEXT':
77-
gameStreamSubject$.next({
78-
streamId: gameStreamId,
79-
text: gameEvent.text,
80-
});
81-
break;
82148
case 'CLEAR_STREAM':
83149
gameStreamSubject$.next({
84150
streamId: gameEvent.streamId,
@@ -91,6 +157,51 @@ const GridPage: React.FC = (): ReactNode => {
91157
case 'POP_STREAM':
92158
gameStreamId = '';
93159
break;
160+
case 'TEXT_OUTPUT_CLASS':
161+
// TODO
162+
break;
163+
case 'TEXT_STYLE_PRESET':
164+
// TODO
165+
break;
166+
case 'TEXT':
167+
gameStreamSubject$.next({
168+
streamId: gameStreamId,
169+
text: gameEvent.text,
170+
});
171+
break;
172+
case 'EXPERIENCE':
173+
gameStreamSubject$.next({
174+
streamId: 'experience',
175+
text: gameEvent.text,
176+
});
177+
break;
178+
case 'ROOM':
179+
// TODO
180+
break;
181+
case 'COMPASS':
182+
// TODO
183+
break;
184+
case 'VITALS':
185+
// TODO
186+
break;
187+
case 'INDICATOR':
188+
// TODO
189+
break;
190+
case 'SPELL':
191+
// TODO
192+
break;
193+
case 'LEFT_HAND':
194+
// TODO
195+
break;
196+
case 'RIGHT_HAND':
197+
// TODO
198+
break;
199+
case 'SERVER_TIME':
200+
// TODO
201+
break;
202+
case 'ROUND_TIME':
203+
// TODO
204+
break;
94205
}
95206
});
96207

@@ -166,6 +277,17 @@ const GridPage: React.FC = (): ReactNode => {
166277
/>
167278
),
168279
},
280+
{
281+
itemId: 'experience',
282+
title: 'Experience',
283+
content: (
284+
<DougCmp
285+
stream$={gameStreamSubject$.pipe(
286+
rxjs.filter((m) => m.streamId === 'experience')
287+
)}
288+
/>
289+
),
290+
},
169291
{
170292
itemId: 'percWindow',
171293
title: 'Spells',

0 commit comments

Comments
 (0)