diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.spec.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.spec.ts new file mode 100644 index 0000000000..47c3b2db30 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-content/ai-chat-content.spec.ts @@ -0,0 +1,41 @@ +/** + * @vitest-environment happy-dom + */ +import { describe, expect, test, vi } from 'vitest'; + +import { AIChatContent } from './ai-chat-content'; + +describe('AIChatContent pinned scroll tracking', () => { + test('records scroll position from the chat messages host', async () => { + let scrollEndHandler: (() => void) | undefined; + + const chatMessages = { + scrollTop: 256, + updateComplete: Promise.resolve(), + addEventListener: vi.fn((event: string, handler: EventListener) => { + if (event === 'scrollend') { + scrollEndHandler = handler as () => void; + } + }), + }; + + const content = { + chatMessagesRef: { value: chatMessages }, + _scrollListenersInitialized: false, + lastScrollTop: undefined, + } as unknown as AIChatContent; + + (AIChatContent.prototype as any)._initializeScrollListeners.call(content); + await chatMessages.updateComplete; + await Promise.resolve(); + + expect(chatMessages.addEventListener).toHaveBeenCalledWith( + 'scrollend', + expect.any(Function) + ); + + scrollEndHandler?.(); + + expect((content as any).lastScrollTop).toBe(256); + }); +}); diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/ai-chat-messages.spec.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/ai-chat-messages.spec.ts new file mode 100644 index 0000000000..cbc4700776 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-messages/ai-chat-messages.spec.ts @@ -0,0 +1,50 @@ +/** + * @vitest-environment happy-dom + */ +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +import { AIChatMessages } from './ai-chat-messages'; + +describe('AIChatMessages scrolling', () => { + beforeEach(() => { + vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => { + cb(0); + return 1; + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + test('scrollToEnd scrolls the host element', () => { + const scrollTo = vi.fn(); + const element = { + scrollTo, + } as unknown as AIChatMessages; + + Object.defineProperty(element, 'scrollHeight', { + configurable: true, + value: 480, + }); + + AIChatMessages.prototype.scrollToEnd.call(element); + + expect(scrollTo).toHaveBeenCalledWith({ + top: 480, + behavior: 'smooth', + }); + }); + + test('scrollToPos scrolls the host element', () => { + const scrollTo = vi.fn(); + const element = { + scrollTo, + } as unknown as AIChatMessages; + + AIChatMessages.prototype.scrollToPos.call(element, 128); + + expect(scrollTo).toHaveBeenCalledWith({ top: 128 }); + }); +}); diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/rich-text.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/rich-text.ts index 11197a6017..0a8b31c0ba 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/rich-text.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/rich-text.ts @@ -32,6 +32,7 @@ export class ChatContentRichText extends WithDisposable(ShadowlessElement) { extensions: this.extensions, affineFeatureFlagService: this.affineFeatureFlagService, theme: this.theme, + scrollable: false, })(text, this.state)}`; } } diff --git a/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts b/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts index ccf2a6e464..c67cd94c00 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts @@ -85,6 +85,7 @@ export type TextRendererOptions = { testId?: string; affineFeatureFlagService?: FeatureFlagService; theme?: Signal; + scrollable?: boolean; }; // todo: refactor it for more general purpose usage instead of AI only? @@ -140,9 +141,12 @@ export class TextRenderer extends SignalWatcher( } .text-renderer-container { + padding: 0; + } + + .text-renderer-container.scrollable { overflow-y: auto; overflow-x: hidden; - padding: 0; overscroll-behavior-y: none; } .text-renderer-container.show-scrollbar::-webkit-scrollbar { @@ -325,6 +329,7 @@ export class TextRenderer extends SignalWatcher( const classes = classMap({ 'text-renderer-container': true, 'custom-heading': !!customHeading, + scrollable: this.options.scrollable !== false, }); const theme = this.options.theme?.value; return html`