fix(editor): render NodeViewContent first so click hit-testing isn't offset (#146) #147
Reference in New Issue
Block a user
Delete Branch "fix/146-nodeview-content-order"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes #146 — caret/selection offset in NodeViews (content-first DOM order)
Three editable NodeViews rendered a
contentEditable={false}chrome element in flow, beforeNodeViewContent. On macOS the browser's click hit-testing (posAtCoords→caretRangeFromPoint) then misses the contentDOM and snaps the caret to the previous node — caret/selection lands a line (code block) or several lines (footnotes → into the body) above the click. Same architectural defect in 3 places; transclusion (absolute overlay) was the "good" reference.Fix (issue's recommended plan)
Make
NodeViewContentthe first child in the DOM; move the non-editable chrome after it and restore its visual position with CSS:<pre><NodeViewContent/></pre>first; language/copy menu follows, lifted above via flexorder(.codeBlockis a flex column).order(.listflex column; separator border on the container).order:-1(stays left); ↩ back-link stays right.Layout visually unchanged.
Verification
Built a real-browser harness (Playwright Chromium + WebKit) running the actual client (real
extensions.ts, real CSS via postcss-mantine, real Mantine code-block-view, real footnote numbering plugin):NodeViewContentin all 3).contentEditable=falseelement precedes it (the issue's Level-2 invariant) — and a screenshot confirms the menu/heading/marker render in their original positions.⚠️ The caret-offset itself is macOS-specific text hit-testing — it does NOT reproduce in headless Chromium/WebKit on Linux (verified exhaustively: both engines, full client, single clicks, glyph-rect
posAtCoords, drag-selection, the exact footnote re-click sequence, zoom 0.67–1.5 — all map correctly). So please confirm the visible fix on macOS. The structural root cause (chrome before contentDOM) is removed regardless.DoD note: the Level-2 vitest structural guard from the issue isn't added here (mounting the React NodeViews in jsdom is fragile and jsdom has no layout); the invariant is instead verified via the real-browser DOM-order check above. Happy to add a guard if wanted.
Code review — PR #147: render NodeViewContent first so click hit-testing isn't offset (#146)
Verdict: Approve with comments. The fix is correct and faithful. Independent stability and regression passes both came back essentially clean after verification — the DOM reorder is compensated by flex
orderwith no behavioral or visual regression I could substantiate (separator border, spacing, marker/back-link/heading positions, mermaidhidden, copy-to-clipboard and footnote numbering all verified DOM-order-independent). Two minor asks below, plus one non-blocking architecture note.Scope reviewed:
git diff develop..fix/146-nodeview-content-order— 6 files, +54/−20, all underapps/client/src/features/editor. Reviewed across security, stability, conventions, regressions, test-coverage, simplification, and architecture.Critical
None.
Warnings
apps/client/src/features/editor/components/footnote/footnotes-list-view.tsx:20-27(same forfootnote-definition-view.tsxandcode-block-view.tsx). The whole fix rests on<NodeViewContent>being the first DOM child; if a future edit reinserts non-editable chrome before it, the macOS caret bug returns silently with nothing to catch it. The "jsdom can't test this" reasoning is valid for the caret symptom (needs real-browser layout) but not for a structural guard: mounting the two footnote NodeViews in the project's existing jsdom +@testing-library/reactsetup was verified to work with trivial mock props, and an assertion that the wrapper's first element child carriesdata-node-view-content(with no[contenteditable="false"]element preceding it) passes on this code and fails on the pre-fix order — a real regression lock, zero new deps. (CodeBlockViewis the genuinely fragile one — needswindow.matchMedia+MantineProvider— so cover it only if cheap.) Fix: addfootnote/footnote-views.structure.test.tsxcovering at least the two footnote views.Suggestions
order—apps/client/src/features/editor/components/code-block/code-block-view.tsx:50-54. The comment says the menu "is positioned as a top-right overlay via CSS (the transclusion pattern), so it no longer sits in-flow above the code." The actual CSS (code-block.module.css:25→.menuGroup { order: -1 };styles/code.css→.codeBlock { display: flex; flex-direction: column }) keeps the menu fully in flow as a full-width row above the code, lifted by flexorder— it is neither an overlay nor the transclusion pattern (the real transclusion view usesposition: absoluteto leave the flow). The comment misdescribes both its own mechanism and the cited reference. Fix: reword to e.g. "content first in the DOM; the menu is rendered after it and lifted back above visually via flexorder: -1(the.codeBlockwrapper is a flex column)."flex-direction: columncontainer the cross (horizontal) axis matches block width behavior, so nothing new is introduced here. If you want to harden it regardless,pre { min-width: 0; overflow-x: auto; }makes such lines scroll inside the block. Out of scope for this PR.Test coverage
The new behavior is JSX reordering + CSS
order, which this project generally does not unit-test — fair. But this is a bug fix, and the cheapest meaningful coverage (a structural DOM-order guard on the two footnote views) is feasible in the existing harness and currently absent. No existing test references any of the three NodeViews. See the warning above.Architecture & design (forward-looking, non-blocking)
NodeViewContentusers): code-block, footnote-definition, footnotes-list, transclusion, callout. This PR hand-fixes the same #146 defect in three of them with the same recipe (content first + flexorder: -1). The invariant is real and load-bearing but lives nowhere enforceable, so a 4th NodeView (or an edit tocallout, safe today only because content is its sole child) can silently reintroduce it. Note also two divergent seams: transclusion keeps chrome before content but usesposition: absolute(out of flow), while this PR keeps chrome in flow and reorders with flex.#146comments document it at each call site; only ~5 heterogeneous views. Cons: invariant stays tribal knowledge; a recurrence is silent and macOS-only.<EditableNodeView>wrapper that always renders content first (effort: large). Pros: makes "content first" impossible to get wrong by construction; one place to document the positioning contract. Cons: speculative for ~5 structurally divergent views (code-block's menu + mermaid vs. footnote's marker + back-link vs. transclusion's absolute overlay); likely a leaky abstraction worse than the repetition; largest blast radius for a 6-file fix.Multi-aspect review (security · stability · conventions · regressions · test-coverage · simplification · architecture). Security, simplification, and regressions returned clean.
после исправления можно мержить
Ghost referenced this pull request2026-06-24 05:28:16 +03:00