Skip to content

Commit 205f9db

Browse files
authored
fix(chat): refocus input after submit + force scroll on new messages (#34)
1 parent 49d2c87 commit 205f9db

3 files changed

Lines changed: 36 additions & 20 deletions

File tree

libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -219,19 +219,23 @@ export class ChatDebugComponent {
219219
/** Track message count to trigger auto-scroll */
220220
private readonly messageCount = computed(() => this.ref().messages().length);
221221

222+
private prevMessageCount = 0;
223+
222224
constructor() {
223-
// Auto-scroll to bottom when new messages arrive or loading state changes
224225
effect(() => {
225-
this.messageCount(); // track
226+
const count = this.messageCount();
226227
this.ref().isLoading(); // track
227228
const el = this.scrollContainer()?.nativeElement;
228-
if (el) {
229-
const isNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150;
230-
if (isNearBottom) {
231-
requestAnimationFrame(() => {
232-
el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
233-
});
234-
}
229+
if (!el) return;
230+
231+
const isNewMessage = count !== this.prevMessageCount;
232+
this.prevMessageCount = count;
233+
234+
const isNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150;
235+
if (isNewMessage || isNearBottom) {
236+
requestAnimationFrame(() => {
237+
el.scrollTo({ top: el.scrollHeight, behavior: isNewMessage ? 'instant' : 'smooth' });
238+
});
235239
}
236240
});
237241
}

libs/chat/src/lib/compositions/chat/chat.component.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -188,21 +188,26 @@ export class ChatComponent {
188188
/** Track message count to trigger auto-scroll */
189189
private readonly messageCount = computed(() => this.ref().messages().length);
190190

191+
private prevMessageCount = 0;
192+
191193
constructor() {
192-
// Auto-scroll to bottom when new messages arrive.
193-
// Only scrolls if user is already near the bottom (within 150px),
194-
// so reading earlier messages isn't interrupted.
194+
// Auto-scroll to bottom:
195+
// - Always scroll when message count increases (new message sent/received)
196+
// - During streaming partials, only scroll if user is near bottom
195197
effect(() => {
196-
this.messageCount(); // track
198+
const count = this.messageCount();
197199
this.ref().isLoading(); // track
198200
const el = this.scrollContainer()?.nativeElement;
199-
if (el) {
200-
const isNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150;
201-
if (isNearBottom) {
202-
requestAnimationFrame(() => {
203-
el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
204-
});
205-
}
201+
if (!el) return;
202+
203+
const isNewMessage = count !== this.prevMessageCount;
204+
this.prevMessageCount = count;
205+
206+
const isNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150;
207+
if (isNewMessage || isNearBottom) {
208+
requestAnimationFrame(() => {
209+
el.scrollTo({ top: el.scrollHeight, behavior: isNewMessage ? 'instant' : 'smooth' });
210+
});
206211
}
207212
});
208213
}

libs/chat/src/lib/primitives/chat-input/chat-input.component.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
input,
66
output,
77
signal,
8+
viewChild,
9+
ElementRef,
810
ChangeDetectionStrategy,
911
} from '@angular/core';
1012
import { FormsModule } from '@angular/forms';
@@ -49,6 +51,7 @@ export function submitMessage(
4951
(focus)="focused.set(true)"
5052
(blur)="focused.set(false)"
5153
rows="1"
54+
#textareaEl
5255
class="flex-1 bg-transparent border-0 outline-none resize-none max-h-[120px] overflow-y-auto"
5356
[style.color]="'var(--chat-text)'"
5457
[style.fontFamily]="'var(--chat-font-family)'"
@@ -79,11 +82,15 @@ export class ChatInputComponent {
7982
readonly isDisabled = computed(() => this.ref().isLoading());
8083
readonly focused = signal(false);
8184

85+
private readonly textareaEl = viewChild<ElementRef<HTMLTextAreaElement>>('textareaEl');
86+
8287
onSubmit(): void {
8388
const submitted = submitMessage(this.ref(), this.messageText());
8489
if (submitted !== null) {
8590
this.submitted.emit(submitted);
8691
this.messageText.set('');
92+
// Re-focus the textarea after submit
93+
requestAnimationFrame(() => this.textareaEl()?.nativeElement.focus());
8794
}
8895
}
8996

0 commit comments

Comments
 (0)