fix(editor): render NodeViewContent first so click hit-testing isn't offset (#146)

Three editable NodeViews rendered a contentEditable=false "chrome" element IN
FLOW BEFORE NodeViewContent. On macOS the browser's click hit-testing
(posAtCoords → caretRangeFromPoint) then misses the contentDOM and snaps the
caret to the previous node — the caret/selection lands a line (code block) or
several lines (footnotes, into the body) above where the user clicked.

Fix (the transclusion pattern / issue #146 plan): make the editable
NodeViewContent the FIRST child in the DOM and move the non-editable chrome
AFTER it, restoring its visual position with CSS:

- code-block-view: <pre><NodeViewContent/></pre> first; the language/copy menu
  follows and is lifted above via flex `order` (.codeBlock is now a flex column).
- footnotes-list-view: NodeViewContent first; the "Footnotes" heading follows and
  is lifted above via flex `order` (.list is a flex column; the separator border
  stays on the container).
- footnote-definition-view: NodeViewContent first; the "N." marker follows with
  `order:-1` to stay on the left; the ↩ back-link stays on the right.

Layout is visually unchanged. Verified in a real browser (Chromium): the
contentDOM is now the first child of every editable NodeView wrapper (no
contentEditable=false element precedes it), and the menu/heading/marker still
render in their original positions.

NOTE: the caret-offset itself is macOS-specific text hit-testing and does not
reproduce in headless Chromium/WebKit on Linux (verified extensively), so the
visible fix is best confirmed on macOS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-23 21:48:21 +03:00
parent aeea315618
commit 38544e2ddc
6 changed files with 54 additions and 20 deletions

View File

@@ -47,6 +47,23 @@ export default function CodeBlockView(props: NodeViewProps) {
return (
<NodeViewWrapper className="codeBlock">
{/* #146: the editable <pre><code> (contentDOM) MUST come first in the DOM.
With the non-editable menu rendered before it, the browser's click
hit-testing snapped the caret up one line. Render content first; the
menu follows and is positioned as a top-right overlay via CSS (the
transclusion pattern), so it no longer sits in-flow above the code. */}
<pre
spellCheck="false"
hidden={
((language === "mermaid" && !editor.isEditable) ||
(language === "mermaid" && !isSelected)) &&
node.textContent.length > 0
}
>
{/* @ts-ignore */}
<NodeViewContent as="code" className={`language-${language}`} />
</pre>
<Group
justify="flex-end"
contentEditable={false}
@@ -83,18 +100,6 @@ export default function CodeBlockView(props: NodeViewProps) {
</CopyButton>
</Group>
<pre
spellCheck="false"
hidden={
((language === "mermaid" && !editor.isEditable) ||
(language === "mermaid" && !isSelected)) &&
node.textContent.length > 0
}
>
{/* @ts-ignore */}
<NodeViewContent as="code" className={`language-${language}`} />
</pre>
{language === "mermaid" && (
<Suspense fallback={null}>
<MermaidView props={props} />