Skip to content

Commit 9e7581d

Browse files
authored
feat(ai-conversation): add conditional autoScroll handling (#6551)
1 parent 759e374 commit 9e7581d

File tree

4 files changed

+116
-11
lines changed

4 files changed

+116
-11
lines changed

.changeset/brave-bears-teach.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@aws-amplify/ui-react-ai": minor
3+
---
4+
5+
feat(ai-conversation): add conditional autoScroll handling

packages/react-ai/src/components/AIConversation/AIConversation.tsx

+10-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import * as React from 'react';
2+
23
import { Flex, ScrollView } from '@aws-amplify/ui-react';
34
import {
45
IconAssistant,
56
IconUser,
67
useIcons,
78
} from '@aws-amplify/ui-react/internal';
9+
10+
import useConversationScrollProps from './useConversationScrollProps';
811
import type {
912
AIConversation as AIConversationType,
1013
AIConversationInput,
@@ -29,6 +32,7 @@ interface AIConversationBaseProps
2932
function AIConversationBase({
3033
avatars,
3134
controls,
35+
messages,
3236
...rest
3337
}: AIConversationBaseProps): React.JSX.Element {
3438
useSetUserAgent({
@@ -51,25 +55,20 @@ function AIConversationBase({
5155

5256
const providerProps = {
5357
...rest,
54-
avatars: {
55-
...defaultAvatars,
56-
...avatars,
57-
},
58-
controls: {
59-
MessageList,
60-
PromptList,
61-
Form,
62-
...controls,
63-
},
58+
avatars: { ...defaultAvatars, ...avatars },
59+
controls: { MessageList, PromptList, Form, ...controls },
60+
messages,
6461
};
6562

63+
const scrollProps = useConversationScrollProps(messages);
64+
6665
return (
6766
<AIConversationProvider {...providerProps}>
6867
<Flex
6968
className={ComponentClassName.AIConversation}
7069
testId="ai-conversation"
7170
>
72-
<ScrollView autoScroll="smooth" flex="1">
71+
<ScrollView {...scrollProps} flex="1">
7372
<DefaultMessageControl />
7473
<MessagesControl />
7574
</ScrollView>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
import useShouldAutoScroll from '../useConversationScrollProps';
3+
import { ConversationMessage } from '../../../types';
4+
5+
const messages: ConversationMessage[] = [];
6+
7+
type ScrollEvent = React.UIEvent<HTMLDivElement>;
8+
9+
describe('useShouldAutoScroll', () => {
10+
it('returns the expected values on mount', () => {
11+
const { result } = renderHook(() => useShouldAutoScroll(messages));
12+
13+
const { autoScroll, onScroll } = result.current;
14+
15+
expect(autoScroll).toBe('smooth');
16+
expect(onScroll).toStrictEqual(expect.any(Function));
17+
});
18+
19+
it('returns the expected values on messages length change', () => {
20+
const { result, rerender } = renderHook(
21+
(_messages: ConversationMessage[] = messages) =>
22+
useShouldAutoScroll(_messages)
23+
);
24+
25+
const { autoScroll: initAutoScroll, onScroll } = result.current;
26+
27+
expect(initAutoScroll).toBe('smooth');
28+
29+
const scrollStartEvent = {
30+
currentTarget: { scrollTop: 120 },
31+
} as ScrollEvent;
32+
33+
act(() => {
34+
onScroll?.(scrollStartEvent);
35+
});
36+
37+
const { autoScroll: startAutoScroll } = result.current;
38+
expect(startAutoScroll).toBe('smooth');
39+
40+
const scrollUpEvent = { currentTarget: { scrollTop: 119 } } as ScrollEvent;
41+
42+
act(() => {
43+
onScroll?.(scrollUpEvent);
44+
});
45+
46+
const { autoScroll: disabledAutoScroll } = result.current;
47+
48+
expect(disabledAutoScroll).toBeUndefined();
49+
50+
// increase `messages` length
51+
rerender([...messages, {} as ConversationMessage]);
52+
53+
const { autoScroll: nextMessagesAutoScroll } = result.current;
54+
55+
expect(nextMessagesAutoScroll).toBe('smooth');
56+
});
57+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as React from 'react';
2+
3+
import type { ScrollViewProps } from '@aws-amplify/ui-react';
4+
import { useHasValueUpdated } from '@aws-amplify/ui-react-core';
5+
6+
import type { ConversationMessage } from '../../types';
7+
8+
interface ConverationScrollProps
9+
extends Pick<ScrollViewProps, 'onScroll' | 'autoScroll'> {}
10+
11+
export default function useConversationScrollProps(
12+
messages: ConversationMessage[]
13+
): ConverationScrollProps {
14+
const [autoScroll, setAutoScroll] =
15+
React.useState<ConverationScrollProps['autoScroll']>('smooth');
16+
17+
const messagesLength = messages.length;
18+
const hasMessagesLengthChanged = useHasValueUpdated(messagesLength, true);
19+
20+
const lastScrollTop = React.useRef<number | undefined>();
21+
22+
const onScroll: ConverationScrollProps['onScroll'] = ({ currentTarget }) => {
23+
if (autoScroll !== 'smooth') return;
24+
25+
const { scrollTop } = currentTarget;
26+
27+
// set `autoScroll` and `lastScrollTop` to `undefined` on user scroll up
28+
if (lastScrollTop.current && scrollTop < lastScrollTop.current) {
29+
setAutoScroll(undefined);
30+
lastScrollTop.current = undefined;
31+
} else {
32+
lastScrollTop.current = scrollTop;
33+
}
34+
};
35+
36+
React.useEffect(() => {
37+
// reset `autoScroll` to 'smooth' on new message
38+
if (hasMessagesLengthChanged) {
39+
setAutoScroll('smooth');
40+
}
41+
}, [hasMessagesLengthChanged]);
42+
43+
return { autoScroll, onScroll };
44+
}

0 commit comments

Comments
 (0)