mirror of
https://github.com/stablyai/orca
synced 2026-04-21 14:17:16 +00:00
fix: Cmd+F search in rich markdown editor now scrolls to active match (#876)
Merge decoration update and selection into a single ProseMirror transaction so scrollIntoView is not lost. After dispatch, manually scroll the outer flex container using coordsAtPos since tr.scrollIntoView cannot reach the non-overflowing editor wrapper. Also change active match highlight from orange to blue for better contrast.
This commit is contained in:
parent
288322cac4
commit
889dbb23cb
3 changed files with 43 additions and 26 deletions
|
|
@ -121,7 +121,7 @@
|
|||
}
|
||||
|
||||
.rich-markdown-search-match[data-active='true'] {
|
||||
background: color-mix(in srgb, #fb923c 60%, transparent);
|
||||
background: color-mix(in srgb, #38bdf8 50%, transparent);
|
||||
}
|
||||
|
||||
.rich-markdown-editor {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable max-lines -- Why: this component co-locates the rich markdown editor surface, toolbar, search, and slash menu so tightly coupled editor state stays in one place. */
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { EditorContent, useEditor } from '@tiptap/react'
|
||||
import type { Editor } from '@tiptap/react'
|
||||
|
|
@ -365,7 +366,8 @@ export default function RichMarkdownEditor({
|
|||
} = useRichMarkdownSearch({
|
||||
editor,
|
||||
isMac,
|
||||
rootRef
|
||||
rootRef,
|
||||
scrollContainerRef
|
||||
})
|
||||
useEffect(() => {
|
||||
openSearchRef.current = openSearch
|
||||
|
|
|
|||
|
|
@ -12,11 +12,13 @@ import {
|
|||
export function useRichMarkdownSearch({
|
||||
editor,
|
||||
isMac,
|
||||
rootRef
|
||||
rootRef,
|
||||
scrollContainerRef
|
||||
}: {
|
||||
editor: Editor | null
|
||||
isMac: boolean
|
||||
rootRef: RefObject<HTMLDivElement | null>
|
||||
scrollContainerRef: RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const searchInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false)
|
||||
|
|
@ -64,8 +66,12 @@ export function useRichMarkdownSearch({
|
|||
return
|
||||
}
|
||||
|
||||
// Why: rawActiveMatchIndex starts at -1 before the user navigates, but the
|
||||
// derived activeMatchIndex is already 0 (first match shown). Using 0 as the
|
||||
// base when raw is -1 ensures the first Enter press advances to match 1
|
||||
// instead of computing (-1+1)%N = 0 and leaving the effect unchanged.
|
||||
setRawActiveMatchIndex((currentIndex) => {
|
||||
const baseIndex = currentIndex >= 0 ? currentIndex : direction === 1 ? -1 : 0
|
||||
const baseIndex = Math.max(currentIndex, 0)
|
||||
return (baseIndex + direction + matchCount) % matchCount
|
||||
})
|
||||
},
|
||||
|
|
@ -117,31 +123,40 @@ export function useRichMarkdownSearch({
|
|||
}
|
||||
|
||||
const query = isSearchOpen ? searchQuery : ''
|
||||
editor.view.dispatch(
|
||||
editor.state.tr.setMeta(richMarkdownSearchPluginKey, {
|
||||
activeIndex: activeMatchIndex,
|
||||
query
|
||||
})
|
||||
)
|
||||
|
||||
if (!query || activeMatchIndex < 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const activeMatch = matches[activeMatchIndex]
|
||||
if (!activeMatch) {
|
||||
return
|
||||
}
|
||||
|
||||
// Why: rich-mode find should navigate within the editor model instead of
|
||||
// the rendered DOM so highlight positions stay correct while the user edits.
|
||||
// Updating the ProseMirror selection keeps scroll-to-match aligned with the
|
||||
// actual markdown document rather than the transient browser layout.
|
||||
// Why: combining decoration meta and selection+scrollIntoView into one
|
||||
// transaction avoids a split-dispatch where the first dispatch updates
|
||||
// editor.state and the second dispatch's scrollIntoView can be lost
|
||||
// when ProseMirror coalesces view updates.
|
||||
const tr = editor.state.tr
|
||||
tr.setSelection(TextSelection.create(tr.doc, activeMatch.from, activeMatch.to))
|
||||
tr.scrollIntoView()
|
||||
tr.setMeta(richMarkdownSearchPluginKey, {
|
||||
activeIndex: activeMatchIndex,
|
||||
query
|
||||
})
|
||||
|
||||
const activeMatch = query && activeMatchIndex >= 0 ? matches[activeMatchIndex] : null
|
||||
if (activeMatch) {
|
||||
tr.setSelection(TextSelection.create(tr.doc, activeMatch.from, activeMatch.to))
|
||||
}
|
||||
|
||||
editor.view.dispatch(tr)
|
||||
}, [activeMatchIndex, editor, isSearchOpen, matches, searchQuery])
|
||||
|
||||
// Why: ProseMirror's tr.scrollIntoView() delegates to the view's
|
||||
// scrollDOMIntoView which may fail to reach the outer flex scroll container
|
||||
// (the editor element itself has min-height: 100% and no overflow).
|
||||
// Reading coordsAtPos *after* the dispatch and manually scrolling the
|
||||
// container mirrors the approach used by MarkdownPreview search.
|
||||
if (activeMatch) {
|
||||
const container = scrollContainerRef.current
|
||||
if (container) {
|
||||
const coords = editor.view.coordsAtPos(activeMatch.from)
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const relativeTop = coords.top - containerRect.top
|
||||
const targetScroll = container.scrollTop + relativeTop - containerRect.height / 2
|
||||
container.scrollTo({ top: targetScroll, behavior: 'instant' })
|
||||
}
|
||||
}
|
||||
}, [activeMatchIndex, editor, isSearchOpen, matches, scrollContainerRef, searchQuery])
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue