diff --git a/src/renderer/src/components/ContentSearch.tsx b/src/renderer/src/components/ContentSearch.tsx index d322f41616..0dd0acb625 100644 --- a/src/renderer/src/components/ContentSearch.tsx +++ b/src/renderer/src/components/ContentSearch.tsx @@ -1,5 +1,6 @@ import { ActionIconButton } from '@renderer/components/Buttons' import NarrowLayout from '@renderer/pages/home/Messages/NarrowLayout' +import { scrollElementIntoView } from '@renderer/utils' import { Tooltip } from 'antd' import { debounce } from 'lodash' import { CaseSensitive, ChevronDown, ChevronUp, User, WholeWord, X } from 'lucide-react' @@ -181,17 +182,14 @@ export const ContentSearch = React.forwardRef( // 3. 将当前项滚动到视图中 // 获取第一个文本节点的父元素来进行滚动 const parentElement = currentMatchRange.startContainer.parentElement - if (shouldScroll) { - parentElement?.scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'nearest' - }) + if (shouldScroll && parentElement) { + // 优先在指定的滚动容器内滚动,避免滚动整个页面导致索引错乱/看起来"跳到第一条" + scrollElementIntoView(parentElement, target) } } } }, - [allRanges, currentIndex] + [allRanges, currentIndex, target] ) const search = useCallback( diff --git a/src/renderer/src/utils/dom.ts b/src/renderer/src/utils/dom.ts new file mode 100644 index 0000000000..6dd09cda5e --- /dev/null +++ b/src/renderer/src/utils/dom.ts @@ -0,0 +1,55 @@ +/** + * Simple wrapper for scrollIntoView with common default options. + * Provides a unified interface with sensible defaults. + * + * @param element - The target element to scroll into view + * @param options - Scroll options. If not provided, uses { behavior: 'smooth', block: 'center', inline: 'nearest' } + */ +export function scrollIntoView(element: HTMLElement, options?: ScrollIntoViewOptions): void { + const defaultOptions: ScrollIntoViewOptions = { + behavior: 'smooth', + block: 'center', + inline: 'nearest' + } + element.scrollIntoView(options ?? defaultOptions) +} + +/** + * Intelligently scrolls an element into view at the center position. + * Prioritizes scrolling within the specified container to avoid scrolling the entire page. + * + * @param element - The target element to scroll into view + * @param scrollContainer - Optional scroll container. If provided and scrollable, scrolling happens within it; otherwise uses browser default scrolling + * @param behavior - Scroll behavior, defaults to 'smooth' + */ +export function scrollElementIntoView( + element: HTMLElement, + scrollContainer?: HTMLElement | null, + behavior: ScrollBehavior = 'smooth' +): void { + if (!scrollContainer) { + // No container specified, use browser default scrolling + scrollIntoView(element, { behavior, block: 'center', inline: 'nearest' }) + return + } + + // Check if container is scrollable + const canScroll = + scrollContainer.scrollHeight > scrollContainer.clientHeight || + scrollContainer.scrollWidth > scrollContainer.clientWidth + + if (canScroll) { + // Container is scrollable, scroll within the container + const containerRect = scrollContainer.getBoundingClientRect() + const elRect = element.getBoundingClientRect() + + // Calculate element's scrollable offset position relative to the container + const elementTopWithinContainer = elRect.top - containerRect.top + scrollContainer.scrollTop + const desiredTop = elementTopWithinContainer - Math.max(0, scrollContainer.clientHeight - elRect.height) / 2 + + scrollContainer.scrollTo({ top: Math.max(0, desiredTop), behavior }) + } else { + // Container is not scrollable, fallback to browser default scrolling + scrollIntoView(element, { behavior, block: 'center', inline: 'nearest' }) + } +} diff --git a/src/renderer/src/utils/index.ts b/src/renderer/src/utils/index.ts index 91d4961ec6..9822c7f536 100644 --- a/src/renderer/src/utils/index.ts +++ b/src/renderer/src/utils/index.ts @@ -214,6 +214,7 @@ export function uniqueObjectArray(array: T[]): T[] { export * from './api' export * from './collection' export * from './dataLimit' +export * from './dom' export * from './file' export * from './image' export * from './json'