mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-06 22:55:56 +08:00
fix: prevent outer scrolling in horizontal multi-model messages (#13964)
### What this PR does Before this PR: In horizontal multi-model layout, long assistant responses could create two vertical scroll areas. When the mouse wheel was used on the card edge, header area, or other non-content areas, the outer card wrapper could scroll and move the whole content area out of view, leaving blank space. After this PR: Only the inner message content area scrolls vertically in horizontal multi-model layout. Scrolling on card edges or other non-content areas no longer scrolls the outer card wrapper away. The horizontal group container still scrolls horizontally as before. Fixes # ### Why we need it and why it was done in this way This is a focused UI bug fix for an accidental nested vertical scrolling setup in horizontal multi-model messages. The fix keeps the existing inner content scrolling behavior and height limit, but removes the meaningless outer vertical scrolling from the card wrapper. This keeps the change minimal and reduces regression risk. The following tradeoffs were made: A minimal CSS fix was chosen instead of introducing wheel-event interception logic. The following alternatives were considered: Intercepting wheel events at the card level was considered, but rejected because it would increase complexity and broaden the behavior change beyond this bug. Links to places where the discussion took place: N/A ### Breaking changes None. ### Special notes for your reviewer A regression test was added for the horizontal multi-model layout scroll responsibilities. Manual verification was also performed: - Long responses still scroll correctly inside the content area - Scrolling on the card edge no longer moves the whole card content out of view - Horizontal scrolling of the group still works - No behavioral changes were made to fold, vertical, or grid layouts Before:  After:  ### Checklist - [x] PR: The PR description is expressive enough and will help future contributors - [x] Code: Write code that humans can understand and keep it simple - [x] Refactor: I have left the code cleaner than I found it - [x] Upgrade: Impact of this change on upgrade flows was considered and not required - [x] Documentation: A user-guide update was considered and is not required - [x] Self-review: I have reviewed my own code before requesting review from others ### Release note ```release-note Fixed a UI bug in horizontal multi-model message layout where scrolling on the card edge could scroll the outer message card instead of only the inner content area. ``` Signed-off-by: sjsjysfj <gc160514190803@163.com>
This commit is contained in:
@@ -361,7 +361,7 @@ interface MessageWrapperProps {
|
||||
const MessageWrapper = styled.div<MessageWrapperProps>`
|
||||
&.horizontal {
|
||||
padding: 1px;
|
||||
overflow-y: auto;
|
||||
overflow-y: visible;
|
||||
.message {
|
||||
height: 100%;
|
||||
border: 0.5px solid var(--color-border);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import type { Topic } from '@renderer/types'
|
||||
import type { Message } from '@renderer/types/newMessage'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
editMessage: vi.fn(),
|
||||
scrollIntoView: vi.fn(),
|
||||
setTimeoutTimer: vi.fn(),
|
||||
useChatContext: vi.fn().mockReturnValue({ isMultiSelectMode: false }),
|
||||
useSettings: vi.fn().mockReturnValue({
|
||||
multiModelMessageStyle: 'horizontal',
|
||||
gridColumns: 2,
|
||||
gridPopoverTrigger: 'click'
|
||||
}),
|
||||
EventEmitter: {
|
||||
on: vi.fn(),
|
||||
off: vi.fn()
|
||||
},
|
||||
MessageEditingProvider: vi.fn(({ children }: { children: ReactNode }) => <>{children}</>),
|
||||
MessageGroupMenuBar: vi.fn(() => <div className="group-menu-bar">menu</div>),
|
||||
MessageItem: vi.fn(({ message }: { message: Message }) => (
|
||||
<div className="message">
|
||||
<div className="message-content-container" data-testid={`content-${message.id}`}>
|
||||
Long message content
|
||||
</div>
|
||||
<div className="MessageFooter">footer</div>
|
||||
</div>
|
||||
))
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/context/MessageEditingContext', () => ({
|
||||
MessageEditingProvider: mocks.MessageEditingProvider
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/utils', () => ({
|
||||
classNames: (items: Array<Record<string, boolean> | string | undefined>) =>
|
||||
items
|
||||
.flatMap((item) => {
|
||||
if (!item) return []
|
||||
if (typeof item === 'string') return [item]
|
||||
return Object.entries(item)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key]) => key)
|
||||
})
|
||||
.join(' ')
|
||||
}))
|
||||
|
||||
vi.mock('uuid', () => ({
|
||||
default: () => 'test-uuid',
|
||||
v4: () => 'test-uuid'
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/hooks/useChatContext', () => ({
|
||||
useChatContext: () => mocks.useChatContext()
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/hooks/useAssistant', () => ({
|
||||
useAssistant: () => ({
|
||||
assistant: null
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/hooks/useMessageOperations', () => ({
|
||||
useMessageOperations: () => ({
|
||||
editMessage: mocks.editMessage
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/hooks/useSettings', () => ({
|
||||
useSettings: () => mocks.useSettings()
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/hooks/useTimer', () => ({
|
||||
useTimer: () => ({
|
||||
setTimeoutTimer: mocks.setTimeoutTimer
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/services/EventService', () => ({
|
||||
EVENT_NAMES: {
|
||||
LOCATE_MESSAGE: 'locate-message'
|
||||
},
|
||||
EventEmitter: mocks.EventEmitter
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/utils/dom', () => ({
|
||||
scrollIntoView: mocks.scrollIntoView
|
||||
}))
|
||||
|
||||
vi.mock('../Message', () => ({
|
||||
default: mocks.MessageItem
|
||||
}))
|
||||
|
||||
vi.mock('../MessageGroupMenuBar', () => ({
|
||||
default: mocks.MessageGroupMenuBar
|
||||
}))
|
||||
|
||||
const { default: MessageGroup } = await import('../MessageGroup')
|
||||
|
||||
const createMessage = (id: string, index: number) =>
|
||||
({
|
||||
id,
|
||||
askId: 'ask-1',
|
||||
role: 'assistant',
|
||||
blocks: [],
|
||||
multiModelMessageStyle: 'horizontal',
|
||||
index
|
||||
}) as unknown as Message & { index: number }
|
||||
|
||||
describe('MessageGroup', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('uses only the inner content container for vertical scrolling in horizontal layout', () => {
|
||||
const messages = [createMessage('msg-1', 0), createMessage('msg-2', 1)]
|
||||
const topic = { id: 'topic-1' } as Topic
|
||||
|
||||
render(<MessageGroup messages={messages} topic={topic} />)
|
||||
|
||||
const outerWrapper = document.getElementById('message-msg-1')
|
||||
expect(outerWrapper).not.toBeNull()
|
||||
expect(getComputedStyle(outerWrapper!).overflowY).toBe('visible')
|
||||
|
||||
const contentContainer = screen.getByTestId('content-msg-1')
|
||||
expect(getComputedStyle(contentContainer).overflowY).toBe('auto')
|
||||
|
||||
const horizontalGroup = outerWrapper!.parentElement as HTMLElement
|
||||
expect(getComputedStyle(horizontalGroup).overflowX).toBe('auto')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user