Skip to content

Fix focus phantom children, FocusTrap restore, and CodeBlock re-tokenization#67

Merged
zion-off merged 3 commits into
mainfrom
perf-improvements
Mar 1, 2026
Merged

Fix focus phantom children, FocusTrap restore, and CodeBlock re-tokenization#67
zion-off merged 3 commits into
mainfrom
perf-improvements

Conversation

@zion-off

@zion-off zion-off commented Mar 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • fix(focus): When a node's parentId changes between renders, the node was left in the old parent's childrenIds, allowing navigateSibling to land on it as a phantom child. Now cleaned up before re-linking to the new parent.
  • fix(focus-trap): FocusTrap read store.getFocusedId() after calling useFocusNode(), which calls registerNode() during render and may silently set focusedId to the trap node itself. Reordered so the previous focus is captured first. Added a self-restoration guard (previousFocus !== id) to handle the auto-focus edge case.
  • perf(code-block): Prism.tokenize() and the token color merge ran unconditionally on every render. Both are now memoized — re-tokenization is skipped when code content and language haven't changed.

Test plan

  • Open a modal (FocusTrap-backed) and confirm focus restores to the previously focused element on close
  • Confirm CodeBlock does not re-tokenize when its parent re-renders without changing children or language
  • Run npx tsc --noEmit and pnpm lint — both pass

zion-off added 3 commits March 2, 2026 05:37
…e it

useFocusNode() calls registerNode() during render, which may call
setFocusedIdSilently() (auto-focus on first node). Reading focusedId
after that captures the trap node itself as the "previous" focus,
causing restore-on-unmount to attempt focusNode() on an already-
unregistered node and silently drop focus.

Reorder so useStore() and useRef run first, and add a self-restoration
guard (previousFocus !== id) to handle the edge case where auto-focus
still sets focusedId to the trap node before we read it.
Both Prism.tokenize() and the defaultTokenColors spread ran
unconditionally on every render. Wrap colors in useMemo keyed on
tokenColors, and content in useMemo keyed on children/grammar/colors,
so re-tokenization is skipped when the code and language haven't changed.
@zion-off zion-off merged commit 4bf7d4e into main Mar 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant