fix(superdoc): restore find input focus after match navigation (SD-3045)#3240
Conversation
The Search extension's goToSearchResult calls editor.view.focus() so the new selection is visible. When the user pressed Enter in the built-in find input, the synchronous focus steal blurred the input — subsequent Enter keystrokes were swallowed by the ProseMirror editor, splitting paragraphs at the match position, invalidating the search session, and resetting the active match index. The user had to click back into the input between every navigation. Re-focus the find input synchronously after goNext / goPrev so repeated Enter / Shift+Enter keeps advancing through matches and the editor never receives the keystroke.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
hey @tupizz! Here's the test document I was using to test this: If you search for |
|
@luccas-harbour nice suggestion |
…D-3045)
Pressing Enter or clicking next/prev on a search match that lives on a
different page often left the match just below the visible area, slightly
above the viewport, or didn't scroll at all. Three races were colluding:
1. The Vue surface lives in the document's normal flow, so any focus or
.select() on its input or buttons triggers the browser's
scroll-element-into-view behaviour and snaps the document back to the
find bar — undoing the goNext scroll. Drop .select() (cmd+A still
works), focus with preventScroll: true, and pin every scrollable
ancestor's scrollTop across the focus call. Route the button clicks
through the same goNext/goPrev → focusFindInput path so all three
navigation entry points (Enter, Shift+Enter, button click) have the
same focus-restore guarantee.
2. PresentationEditor.scrollToPosition runs after dispatch(setSelection),
but the selectionUpdate emitted by that dispatch sets
#shouldScrollSelectionIntoView = true. The next #updateSelection then
calls #scrollActiveEndIntoView and snaps the caret to its
minimum-visibility position (often 20px from the viewport edge),
visibly displacing the match. Consume the flag inside
scrollToPosition.
3. A later selectionUpdate (focus blur as the user moves focus back to
the find input, or async PM events) re-sets the flag to true after we
consumed it, and the RAF-deferred #updateSelection scrolls the caret
again. Re-assert the scrollIntoView on the next animation frame so any
such late displacement is corrected; the no-op case is cheap.
Tests cover all three: Enter / Shift+Enter / button-click focus restore
use { preventScroll: true } on the find input. Manual repro on Luccas's
finding1.docx fixture: 4 'titlePg' matches across 3 pages — every click
now lands with the match in the centre of the viewport.
|
Pushed What it wasThree races in one keystroke. Pressing Enter (or clicking next/prev) on a match on another page fired:
Depending on whether What changed
Verification
If you can sanity-check your own repro, that'd be great. If anything still drifts I'll keep iterating. |
luccas-harbour
left a comment
There was a problem hiding this comment.
hey @tupizz, I noticed one more thing. I was testing with the document basic/advanced-text.docx from the test corpus and if you search for the word Oscar, you'll notice that some of the matches are not highlighted (I think matches 3, 5 and 7?). I know this is a bit more than what the ticket asked for so let me know if it's a quick fix.
… (SD-3045)
Search matches were invisible on runs whose source rPr carried a highlight
mark (e.g. `<w:highlight w:val="white"/>`). The DomPainter writes
`style.backgroundColor = run.highlight` inline on the same span the
DecorationBridge later tags with `.ProseMirror-search-match`, and inline
styles win over class selectors — so the find colour was painted over.
Add `!important` to both search-match background rules (the `.superdoc`
scope used in presentation mode and the `.sd-editor-scoped` scope used by
the hidden editor) so the transient find colour overrides any source-doc
highlight while a search session is live. The class is only present during
a search; clearing it restores the original highlight.
Repro: load `basic/advanced-text.docx` from the corpus, search for "Oscar".
5 of 8 matches sit in runs imported as `highlight: { color: '#FFFFFF' }`
and had no visible find highlight pre-fix. All 8 are now correctly coloured.
Regression test asserts !important is present in both CSS files plus a
JSDOM specificity sanity check showing the rule wins over an inline white.
|
Pushed Root causeReproduced on Inventory of computed
Five of eight, not three — your annotation marked one I had counted ( FixTwo CSS files, four lines:
Why not Chrome's "0/20"When you saw
Chrome doesn't distinguish visible from off-screen and walks accessibility text. SuperDoc's 8 is the correct count of semantically unique occurrences in Verification
|
|
🎉 This PR is included in @superdoc-dev/mcp v0.3.0-next.96 The release is available on GitHub release |
|
🎉 This PR is included in @superdoc-dev/react v1.2.0-next.140 The release is available on GitHub release |
|
🎉 This PR is included in vscode-ext v2.3.0-next.142 |
|
🎉 This PR is included in superdoc-cli v0.8.0-next.111 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-sdk v1.8.0-next.94 |
|
🎉 This PR is included in superdoc v1.30.0-next.91 The release is available on GitHub release |
Demo
CleanShot.2026-05-14.at.09.18.13.mp4
Summary
Pressing Enter in the built-in find/replace input used to advance to the next match once, then drop focus into the ProseMirror editor. Subsequent Enter keystrokes were swallowed by the editor (splitting paragraphs at the match position, invalidating the search session, and resetting the active match index back to zero), forcing the user to click back into the input between every navigation. The "alternating jump back to page 1" symptom in the ticket title is a downstream effect of the same focus steal.
Root cause: the Search extension's
goToSearchResultcallseditor.view.focus()so its newly-set selection is visible — by design when called programmatically — butFindReplaceSurface.vue's Enter handler did not restore focus afterwards.Fix: re-focus the find input synchronously after
goNext/goPrevreturns. The handler already callse.preventDefault(), so once focus stays on the input the editor never sees the keystroke.The change is local to
FindReplaceSurface.vue— the underlying Search command behaviour (used by the dev sidebar and the publicSuperDoc.goToSearchResultAPI) is unchanged.Test plan
FindReplaceSurface.test.jscovers: Enter keeps focus whengoNextsynchronously steals focus elsewhere; Shift+Enter /goPrevdoes the same; Enter's default is still prevented.