Skip to content

fix(editor): selection highlight flickers when dragging across mark boundaries (SD-2024)#2205

Merged
harbournick merged 7 commits intomainfrom
tadeu/sd-2024-fix-selection-flicker-across-marks
Feb 28, 2026
Merged

fix(editor): selection highlight flickers when dragging across mark boundaries (SD-2024)#2205
harbournick merged 7 commits intomainfrom
tadeu/sd-2024-fix-selection-flicker-across-marks

Conversation

@tupizz
Copy link
Contributor

@tupizz tupizz commented Feb 27, 2026

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

  1. 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.

  2. Instrumented selection monitoring — injected a 3ms-interval monitor on window.editor.state.selection to track every selection change during drag. The PM selection was stable — the flicker was purely visual, not a state issue.

  3. Traced the rendering path — followed the selection overlay pipeline:

    • PresentationEditor.#updateSelectionFromEditor() reads the PM selection {from, to}
    • calls computeSelectionRectsFromDom()DomPositionIndex.findEntriesInRange(from, to) to find DOM elements in the selection range
    • builds DOMRange objects from those elements → range.getClientRects() → draws overlay divs
  4. Found 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:

    <span data-pm-start="1" data-pm-end="5">bold</span>   ← run close token at 5, open token at 6
    <span data-pm-start="7" data-pm-end="12">italic</span> ← positions 5-7 are structural tokens
    

    A selection at {from: 5, to: 7} spans only structural tokens — no text content.

  5. Identified the root causefindEntriesInRange uses half-open [start, end) semantics. For the range [5, 7):

    • Entry [1,5]: pmEnd (5) <= start (5)excluded (boundary touch)
    • Entry [7,12]: pmStart (7) >= end (7)excluded (boundary touch)
    • Result: zero entries → zero DOM rects → overlay cleared → flicker

Solution

Three targeted changes:

1. DomPositionIndex.findEntriesInRange — add boundaryInclusive option

Added an optional { boundaryInclusive: true } parameter that switches from half-open [start, end) to closed [start, end] overlap checks:

// Before (half-open): excludes entries touching at boundary
if (entry.pmStart >= end) break;
if (entry.pmEnd <= start) continue;

// After (inclusive): includes entries touching at boundary
if (entry.pmStart > end) break;
if (entry.pmEnd < start) continue;

This is opt-in — the default half-open behavior is preserved for all other callers.

2. DomSelectionGeometry.computeSelectionRectsFromDom — use inclusive boundaries

All calls to findEntriesInRange in 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 net

If 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

  • 4 new unit tests in DomPositionIndex.test.ts:
    • Default half-open semantics exclude boundary entries
    • boundaryInclusive includes both adjacent entries at a run gap
    • boundaryInclusive includes entry whose pmEnd equals query start
    • boundaryInclusive includes entry whose pmStart equals query end
  • All 7083 super-editor tests pass
  • All 1220 layout-bridge tests pass
  • Manual browser test: drag-selecting across bold/italic/color boundaries no longer flickers

…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.
@linear
Copy link

linear bot commented Feb 27, 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: 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".

…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.
Copy link
Contributor

@caio-pizzol caio-pizzol left a comment

Choose a reason for hiding this comment

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

@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.

…-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
@tupizz tupizz requested a review from caio-pizzol February 27, 2026 17:26
Copy link
Contributor

@caio-pizzol caio-pizzol left a comment

Choose a reason for hiding this comment

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

@tupizz the debugLog and the JSDoc fix look good. one thing still open — left an inline comment on the zero-rect guard.

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.
Copy link
Contributor

@caio-pizzol caio-pizzol left a comment

Choose a reason for hiding this comment

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

@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.

@caio-pizzol
Copy link
Contributor

@tupizz

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
2. drag-select across the mark boundary
3. assert the selection range stays continuous and doesn't collapse

Copy link
Collaborator

@harbournick harbournick left a comment

Choose a reason for hiding this comment

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

LGTM

@harbournick harbournick merged commit ba03e76 into main Feb 28, 2026
8 checks passed
@harbournick harbournick deleted the tadeu/sd-2024-fix-selection-flicker-across-marks branch February 28, 2026 00:08
@superdoc-bot
Copy link
Contributor

superdoc-bot bot commented Feb 28, 2026

🎉 This PR is included in superdoc-cli v0.2.0-next.37

The release is available on GitHub release

@superdoc-bot
Copy link
Contributor

superdoc-bot bot commented Feb 28, 2026

🎉 This PR is included in superdoc v1.17.0-next.42

The release is available on GitHub release

harbournick pushed a commit that referenced this pull request Feb 28, 2026
# [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))
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.

4 participants