Skip to content

fix(editor): prevent focus loss when typing in header/footer editors (SD-1993)#2238

Open
tupizz wants to merge 1 commit intomainfrom
tadeu/sd-1993-fix-header-footer-typing-focus
Open

fix(editor): prevent focus loss when typing in header/footer editors (SD-1993)#2238
tupizz wants to merge 1 commit intomainfrom
tadeu/sd-1993-fix-header-footer-typing-focus

Conversation

@tupizz
Copy link
Contributor

@tupizz tupizz commented Mar 2, 2026

Summary

Fixes SD-1993: headers and footers lose focus after the first keystroke, making them effectively uneditable.

Root cause

Every keystroke in a header/footer PM editor triggers a full body rerender cycle:

User types in H/F editor
  → contentChanged event → requestRerender()
  → RAF → #rerender() → painter.paint()
  → renderVirtualized() → updateVirtualWindow()
  → appendChild(pageElement)   ← browser fires blur on focused H/F editor

DomPainter.updateVirtualWindow() has an ordering loop that re-appends all mounted page elements via appendChild. Calling appendChild on an element already in the DOM is a move-to-end operation. The browser fires a blur event on any focused element inside the moved subtree. This kills focus on the header/footer PM editor after every keystroke.

All three reported symptoms stem from this single focus loss:

  • Header only accepts first keystroke: first char triggers rerender → blur → second char has no target
  • Space scrolls to bottom: focus loss + spacer height recalculation → browser adjusts scroll
  • Footer text appears in body: after focus loss, input bridge may route keystrokes to the body editor

Fix

1. Cursor-based DOM reconciliation in updateVirtualWindow (renderer.ts)

Replace the unconditional appendChild ordering loop with cursor-based reconciliation. Tracks a cursor pointer walking through the existing children. For each mounted page, if it's already at the cursor position, skip the DOM mutation and advance. Only call insertBefore when elements are actually out of order.

In the common case (H/F typing with no scroll or window change), pages are already in the correct order after clearGapSpacers() removes old gaps. Zero moves = zero blur = focus preserved.

2. Focus save/restore safety net (PresentationEditor.ts)

Captures the active H/F editor's focus state before #rerender(). In the finally block, if focus was lost (e.g. from edge cases like scroll during editing, viewport resize, page count changes), restores it via activeHfEditor.view.focus().

Test plan

  • All 645 painter-dom tests pass
  • All 7411 super-editor tests pass
  • Pre-commit hooks pass (format, lint, typecheck, commitlint)
  • Manual: load document with header/footer, double-click header, type multiple characters rapidly. All characters appear, no scroll jump.
  • Manual: double-click footer, type text. Text appears in footer only, not in body.
  • Manual: press Escape to exit, verify changes persist in static render.
  • Manual: verify normal body editing and page virtualization (scrolling) still work correctly.

…(SD-1993)

Replace unconditional appendChild calls in DomPainter.updateVirtualWindow()
with cursor-based DOM reconciliation that skips moves for elements already
in the correct position. Add focus save/restore safety net in
PresentationEditor.#flushRerenderQueue for edge cases.
@linear
Copy link

linear bot commented Mar 2, 2026

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 41a9dec4be

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +3185 to +3188
if (hadHfFocus && activeHfEditor?.view) {
const doc = this.#visibleHost.ownerDocument;
const editorDom = activeHfEditor.view.dom;
if (doc && !editorDom.contains(doc.activeElement)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate focus recovery on current H/F session

The new focus recovery in #flushRerenderQueue always refocuses the previously-captured header/footer editor when it lost focus during rerender, but it does not verify that this editor is still the active session. If the user intentionally exits header/footer mode (or switches to another header/footer editor) while #rerender() is awaiting, exitMode() can correctly focus the body editor and then this block will immediately steal focus back to the stale editor, causing unexpected focus jumps and mode-switch glitches.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant