Caret flickers across a mark view with `contenteditable=false` child

Online demo: ProseMirror mark view selection repro

Source code: GitHub - issueset/repro-pm-markview-selection-ios · GitHub

A mention mark view renders a non-editable label followed by the editable source text:

<span class="mention" data-dom>
  <span class="label" contenteditable="false">🅰️TeamA</span>
  <span class="source" data-content-dom>🅱️@Alice</span>
</span>

:a_button_blood_type: and :b_button_blood_type: are different DOM positions, but they should be treated as the same ProseMirror document position.

However, isEquivalentPosition treats them as different ProseMirror positions because its scan stops at the contenteditable=false element. So whenever the browser places the text caret on :a_button_blood_type:, prosemirror-view moves it to :b_button_blood_type:, causing the caret to flicker.

More specifically, this causes the following behavior:

  1. On macOS Chrome, I cannot press ArrowRight to move the caret across the mark view.

    https://github.com/user-attachments/assets/13d34c1b-dcbd-45a7-94df-0dd8a4e0cd0f

  2. On iOS Safari, I cannot long-press the spacebar to move the caret across the mark view.

    https://github.com/user-attachments/assets/8885a829-e593-4d3b-9ce9-ce87f153310e

isEquivalentPosition should treat visually distinct positions as non-equivalent, because it is used to decide whether to normalize the selection, and the library shouldn’t leave the cursor in a place that it isn’t allowed to be in.

The code responsible for skipping things during cursor motion is in capturekeys.ts. Attached patch adjusts it to allow such elements to be cursored over in Chrome.

I’m not sure it is possible to make iOS cursor motion do the right thing here. That provides very little opportunity for scripts to influence what the cursor actually does.

Thanks for the quick reply!


Confirmed, this fixes the keyboard case in Chrome. Video


As you expected, iOS is still not working with the patch. Video

In trackpad mode (holding the space bar and moving), Safari only sends selectionchange events to the page, no key/mouse/touch events, so you are right that there is little to hook into.

What happens on each touch move is: Safari puts the selection in a place ProseMirror does not allow, ProseMirror moves it back, and Safari puts it there again. That loop is the flicker in the video, as you can see a lot of selectionchange events being fired.

I am wondering whether it would be acceptable to let a node view or mark view opt in to tolerating some invalid DOM selection position. Simular to the existing relaxedSide public API, or the private ViewDesc.ignoreForSelection API.

I played around a bit more with space-bar swiping, but what I’m seeing looks pretty bleak. Android unfocuses the editor element when swiping into an uneditable element. iOS doesn’t, at least, but it seems to get irrevocably stuck on such elements, even when ProseMirror isn’t resetting the selection back.