fix(editor): selection highlight flickers when dragging across mark boundaries (SD-2024)#2205
Conversation
…oundaries (SD-2024) DomPositionIndex.findEntriesInRange used half-open [start, end) semantics. When a selection range fell exactly on a run boundary (the 2-position gap between adjacent text spans with different marks), neither adjacent entry matched — producing zero DOM rects and clearing the selection overlay for one frame before the next pointer event restored it. Add a `boundaryInclusive` option that switches to closed [start, end] semantics, and use it in DomSelectionGeometry so entries touching the boundary are always found. Also add a safety net in PresentationEditor that preserves the last overlay when a non-empty selection yields no rects.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 389e1203b1
ℹ️ 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".
packages/super-editor/src/core/presentation-editor/PresentationEditor.ts
Outdated
Show resolved
Hide resolved
…ies (SD-2024) Add two computeSelectionRectsFromDom tests that exercise the exact edge case: selection range falls on the structural gap between two differently-marked runs, verifying the boundaryInclusive path produces non-empty rects.
caio-pizzol
left a comment
There was a problem hiding this comment.
@tupizz boundaryInclusive fix looks solid — right root cause, clean API extension, good tests. left two inline comments: a stale overlay edge case worth a debug log, and a JSDoc that needs reverting to "exclusive". nothing blocking.
packages/super-editor/src/core/presentation-editor/PresentationEditor.ts
Outdated
Show resolved
Hide resolved
packages/super-editor/src/core/presentation-editor/dom/DomPositionIndex.ts
Outdated
Show resolved
Hide resolved
…ection-flicker-across-marks
…-rect path - Revert findElementsInRange JSDoc @param to back to "exclusive" (method does not pass boundaryInclusive) - Add debugLog warn when zero-rect early return fires so stale overlay cases are diagnosable
caio-pizzol
left a comment
There was a problem hiding this comment.
@tupizz the debugLog and the JSDoc fix look good. one thing still open — left an inline comment on the zero-rect guard.
packages/super-editor/src/core/presentation-editor/PresentationEditor.ts
Show resolved
Hide resolved
Narrows the safety net to isDragging so non-drag zero-rect cases (virtualized/disconnected DOM) still clear the overlay correctly.
…ction Refactor clickToPosition to utilize DOM-based page detection via elementsFromPoint, enhancing accuracy in multi-page layouts. This change ensures that containerPoint is treated as page-relative when applicable, addressing issues with virtualization and layout gaps. Additionally, clamp head position in EditorInputManager to prevent selection from entering isolating nodes like tables during drag operations.
…ping (SD-2024) Fix backward drag clamping to resolve to the table boundary instead of an inner cell boundary. Also add behavioral tests for DOM-based page coordinate resolution and selection clamping at isolating nodes.
caio-pizzol
left a comment
There was a problem hiding this comment.
@tupizz all three items from the previous rounds are addressed — the isDragging guard, the JSDoc fix, and the debugLog. the new clickToPosition refactor and isolating-boundary clamping look solid, good test coverage across the board. lgtm, approving.
|
one thing worth adding as a follow-up: a behavior test that reproduces the actual user interaction. something like: 1. load a doc with mixed inline formatting (bold → italic) adjacent to a table |
|
🎉 This PR is included in superdoc-cli v0.2.0-next.37 The release is available on GitHub release |
|
🎉 This PR is included in superdoc v1.17.0-next.42 The release is available on GitHub release |
# [1.17.0](v1.16.0...v1.17.0) (2026-02-28) ### Bug Fixes * active track change ([#2163](#2163)) ([108c14d](108c14d)) * add currentTotalPages getter and pagination-update event ([#2202](#2202)) ([95b4579](95b4579)), closes [#958](#958) * always call resolveComment after custom TC bubble handlers (SD-2049) ([#2204](#2204)) ([34fb4e0](34fb4e0)) * backward replace insert text ([#2172](#2172)) ([66f0849](66f0849)) * **collaboration:** deduplicate updateYdocDocxData during replaceFile (SD-1920) ([#2162](#2162)) ([52962fc](52962fc)) * **comments:** cross-page collision avoidance for floating comment bubbles (SD-1998) ([#2180](#2180)) ([6cfbeca](6cfbeca)) * **comments:** remove synchronous dispatch from plugin apply() (SD-1940) ([#2157](#2157)) ([887175b](887175b)) * **css:** scope ProseMirror CSS to prevent bleeding into host apps (SD-1850) ([#2134](#2134)) ([b9d98fa](b9d98fa)) * document-api improvements, plan mode, query.match, mutations ([6221580](6221580)) * **document-api:** delete table cell fix ([#2209](#2209)) ([5e5c43f](5e5c43f)) * **document-api:** distribute columns command fixes ([#2207](#2207)) ([8f4eaf7](8f4eaf7)) * **document-api:** fix cell shading in document api ([#2215](#2215)) ([456f60e](456f60e)) * **document-api:** insert table cell ([#2210](#2210)) ([357ee90](357ee90)) * **document-api:** plan-engine reliability fixes and error diagnostics ([#2185](#2185)) ([abfd81b](abfd81b)) * **document-api:** split table cell command ([#2217](#2217)) ([0b3e2b4](0b3e2b4)) * **document-api:** split table command ([#2214](#2214)) ([ec31699](ec31699)) * **editor:** render styles applied inside SDT fields (SD-2011) ([#2188](#2188)) ([9c34be3](9c34be3)) * **editor:** selection highlight flickers when dragging across mark boundaries (SD-2024) ([#2205](#2205)) ([ba03e76](ba03e76)) * extract duplicate block identity normalization from docxImporter ([7f7ff93](7f7ff93)) * improve backspace behavior near run boundaries for tracked changes ([#2175](#2175)) ([6c9c7a3](6c9c7a3)) * **layout:** per-section footer constraints for multi-section docs (SD-1837) ([#2022](#2022)) ([e11acc5](e11acc5)) * normalize review namespace into trackChanges, harden input validation ([33e907b](33e907b)) * outside click for toolbar dropdown ([#2174](#2174)) ([5f859c7](5f859c7)) * preserve line spacing and indentation on Google Docs paste ([#2183](#2183)) ([b9a7357](b9a7357)), closes [#2151](#2151) * **shapes:** render grouped DrawingML shapes with custom geometry (SD-1877) ❇️ ([#2105](#2105)) ([14985a5](14985a5)) * support cell spacing ([#1879](#1879)) ([1639967](1639967)) * **tables:** expand auto-width tables to fill available page width ([#2109](#2109)) ([15f36bc](15f36bc)) * text highlight on export ([#2189](#2189)) ([9cbd022](9cbd022)) * track highlight changes ([#2192](#2192)) ([e164625](e164625)) * undo/redo actions ([#2161](#2161)) ([495e92f](495e92f)) ### Features * allow custom accept/reject handlers for TC bubbles ([#1921](#1921)) ([e30abf6](e30abf6)) * **document-api:** add format operations font size alignment color font family ([#2179](#2179)) ([f19c688](f19c688)) * **document-api:** add plan-based mutation engine with query.match and style capture ([#2160](#2160)) ([365293a](365293a)) * **document-api:** doc default initial styles ([#2184](#2184)) ([f25e41f](f25e41f)) * **document-api:** include anchored text in comments list response ([#2177](#2177)) ([b3a2912](b3a2912)) * **document-api:** inline formatting parity core end-to-end ([#2197](#2197)) ([b405b03](b405b03)) * **document-api:** inline formatting rpr parity ([#2198](#2198)) ([41ab771](41ab771)) * **document-api:** section commands ([#2199](#2199)) ([ec4abe3](ec4abe3)) * **document-api:** support deleting entire block nodes not only text ([#2181](#2181)) ([2897246](2897246)) * **document-api:** table of contents commands ([#2200](#2200)) ([baa72c4](baa72c4)) * **document-api:** tables namespace and commands ([#2182](#2182)) ([b80ee31](b80ee31)) * **markdown:** add markdown override to sdk, improve conversion ([#2196](#2196)) ([04a1c71](04a1c71)) * preserve w:view setting through DOCX round-trip ([#2190](#2190)) ([48b4210](48b4210)), closes [#2070](#2070) * **track-changes:** clear comment bubbles when bulk accept or reject TCs ([#2159](#2159)) ([27fbe8e](27fbe8e)) ### Performance Improvements * **comments:** batch tracked change comment creation on load ([#2166](#2166)) ([0c2eca5](0c2eca5)) * **comments:** batch tracked change creation and virtualize floating bubbles (SD-1997) ([#2168](#2168)) ([70fd7d9](70fd7d9))
Demo
CleanShot.2026-02-27.at.12.58.28.mp4
Summary
Fixes the selection highlight flickering/disappearing when drag-selecting across inline mark boundaries (e.g. bold → italic, colored → uncolored text).
Problem
When a user drags to select text that spans two adjacent runs with different marks (e.g., bold followed by italic), the blue selection overlay flickers — it briefly disappears for one frame, then reappears on the next pointer event. This makes drag selection feel broken on any document with mixed inline formatting.
How we debugged it
Reproduced in browser — loaded a complex DOCX (
SuperDoc (5).docx) with mixed marks (bold, italic, colored text, tracked changes). Drag-selected across formatting boundaries and observed the highlight flash.Instrumented selection monitoring — injected a 3ms-interval monitor on
window.editor.state.selectionto track every selection change during drag. The PM selection was stable — the flicker was purely visual, not a state issue.Traced the rendering path — followed the selection overlay pipeline:
PresentationEditor.#updateSelectionFromEditor()reads the PM selection{from, to}computeSelectionRectsFromDom()→DomPositionIndex.findEntriesInRange(from, to)to find DOM elements in the selection rangeDOMRangeobjects from those elements →range.getClientRects()→ draws overlay divsFound the gap — when the selection boundary falls exactly on a run boundary, the PM positions land in the 2-position structural gap between adjacent text spans. Example:
A selection at
{from: 5, to: 7}spans only structural tokens — no text content.Identified the root cause —
findEntriesInRangeuses half-open[start, end)semantics. For the range[5, 7):[1,5]:pmEnd (5) <= start (5)→ excluded (boundary touch)[7,12]:pmStart (7) >= end (7)→ excluded (boundary touch)Solution
Three targeted changes:
1.
DomPositionIndex.findEntriesInRange— addboundaryInclusiveoptionAdded an optional
{ boundaryInclusive: true }parameter that switches from half-open[start, end)to closed[start, end]overlap checks:This is opt-in — the default half-open behavior is preserved for all other callers.
2.
DomSelectionGeometry.computeSelectionRectsFromDom— use inclusive boundariesAll calls to
findEntriesInRangein the selection rect computation now pass{ boundaryInclusive: true }. This ensures the two adjacent text spans are both found when a selection crosses a run boundary.3.
PresentationEditor.#updateSelectionFromEditor— safety netIf a non-empty selection (
from !== to) produces zero DOM rects (edge case where even inclusive boundaries don't find entries), the method now returns early instead of clearing the overlay. This preserves the last valid highlight.Test plan
DomPositionIndex.test.ts:boundaryInclusiveincludes both adjacent entries at a run gapboundaryInclusiveincludes entry whosepmEndequals query startboundaryInclusiveincludes entry whosepmStartequals query end