Fix a selection issue where the CodeMirror is the first element in ProseMirror #78

Closed
iamakulov wants to merge 1 commit from main into main
iamakulov commented 2026-01-08 20:18:29 +01:00 (Migrated from github.com)

When CodeMirror is the first element in ProseMirror, and you press Cmd+A in ProseMirror, the selection doesn’t get applied (or, rather, applies for a single frame). Instead, the CodeMirror element steals the focus.

Repro: CodeSandbox, demo:

https://github.com/user-attachments/assets/94557f2b-544e-461c-a0b8-05dd343df634

Cause: This seems to boil down to an interaction between ProseMirror’s selection logic and CodeMirror being contenteditable itself. When ProseMirror handles a Cmd+A event, it calls domSel.collapse() before calling domSel.expand():

github.com/ProseMirror/prosemirror-view@76c7c47f03/src/viewdesc.ts (L464-L468)

  • domSel.collapse() makes the selection collapse to a single point – at the beginning of the editor
  • The element at the beginning of the editor is CodeMirror – so when that happens, the browser triggers the native focus event on it.
  • This, in turn, triggers CodeMirror’s focus handling, causing it to steal the focus from ProseMirror after the 10ms timer fires.

Solution: This PR attempts to fix the issue. The fix seems to work, but maybe there’s a better place for it, or some edge cases I don’t know about (I’m not very familiar with contenteditable gotchas).

When CodeMirror is the first element in ProseMirror, and you press Cmd+A in ProseMirror, the selection doesn’t get applied (or, rather, applies for a single frame). Instead, the CodeMirror element steals the focus. **Repro:** [CodeSandbox](https://codesandbox.io/p/devbox/dazzling-thunder-forked-3pzfq4?workspaceId=ws_G7ZXTydwb2wV26Xn2zPSPT), demo: https://github.com/user-attachments/assets/94557f2b-544e-461c-a0b8-05dd343df634 **Cause:** This seems to boil down to an interaction between ProseMirror’s selection logic and CodeMirror being contenteditable itself. When ProseMirror handles a Cmd+A event, it calls `domSel.collapse()` before calling `domSel.expand()`: https://github.com/ProseMirror/prosemirror-view/blob/76c7c47f03730b18397b94bd269ece8a9cb7f486/src/viewdesc.ts#L464-L468 - `domSel.collapse()` makes the selection collapse to a single point – at the beginning of the editor - The element at the beginning of the editor is `CodeMirror` – so when that happens, the browser triggers the native `focus` event on it. - This, in turn, triggers CodeMirror’s focus handling, causing it to steal the focus from ProseMirror [after the 10ms timer fires](https://github.com/codemirror/view/blob/e15ace9dc121c652fdf358462cabe23322fd8df3/src/input.ts#L790-L797). **Solution:** This PR attempts to fix the issue. The fix seems to work, but maybe there’s a better place for it, or some edge cases I don’t know about (I’m not very familiar with contenteditable gotchas).
iamakulov commented 2026-01-08 20:38:55 +01:00 (Migrated from github.com)

(You can test this in CodeSandbox by swapping the @codemirror/view import with @iamakulov/codemirror-view.)

(You can test this in CodeSandbox by swapping the `@codemirror/view` import with `@iamakulov/codemirror-view`.)
iamakulov commented 2026-01-08 20:42:41 +01:00 (Migrated from github.com)

(The issue seems to be Chromium-only, btw.)

(The issue seems to be Chromium-only, btw.)
iamakulov commented 2026-01-08 20:53:09 +01:00 (Migrated from github.com)

(And it seems to be new: in BrowserStack, I can only repro it starting Chrome 135+. Could be a browser bug, after all!)

(_And_ it seems to be new: in BrowserStack, I can only repro it starting Chrome 135+. Could be a browser bug, after all!)
iamakulov commented 2026-01-08 21:56:01 +01:00 (Migrated from github.com)

Filed upstream: https://issues.chromium.org/issues/474377386. Thank you for being my rubber duck :D Let’s see what Chromium responds.

Filed upstream: https://issues.chromium.org/issues/474377386. Thank you for being my rubber duck :D Let’s see what Chromium responds.
marijnh commented 2026-01-09 09:47:36 +01:00 (Migrated from github.com)

I find this patch somewhat scary. It is entirely possible for an editor to receive focus while the selection is elsewhere in the document, and this would cause the library to ignore such focus events entirely, allowing focus-tracking code to get confused.

It seems possible to work around this problem by adding any kind of inline element in front of the inner editor. Would adding a kludge like <div style="position: absolute" aria-hidden=true>&#8203;</div> to the start of your CodeMirror node view work for you? It's not a general fix, but it's also less likely to cause additional issues.

I find this patch somewhat scary. It is entirely possible for an editor to receive focus while the selection is elsewhere in the document, and this would cause the library to ignore such focus events entirely, allowing focus-tracking code to get confused. It seems possible to work around this problem by adding any kind of inline element in front of the inner editor. Would adding a kludge like `<div style="position: absolute" aria-hidden=true>&#8203;</div>` to the start of your CodeMirror node view work for you? It's not a general fix, but it's also less likely to cause additional issues.
iamakulov commented 2026-01-09 16:10:32 +01:00 (Migrated from github.com)

Oh, that’s clever!

That seems to solve Cmd+A (CodeSandbox), but it actually does cause more issues haha. With this workaround, Safari stops showing the Cmd+A highlight at all, and in both Chrome and Firefox, pressing gets the cursor stuck in the zero-width space div:

https://github.com/user-attachments/assets/b9a58e2e-9f9b-4bdc-81bd-c69a39065481

You can work around that by also adding a contenteditable="true" on the div with the zero-width space. It’s still not great, tho (it leads to an extra cursor position in front of the code editor, and to Safari highlighting the whole page on Cmd+A). Meh :/


Another thing I noticed: having CodeMirror first in ProseMirror also seems to break Cmd+C? (LMK if you’d like me to raise a separate issue for this.)

For example, in the original demo, if you Cmd+A in Safari (which works) and then Cmd+C, the only thing the browser copies is // This CodeMirror block is first instead of the whole text. Chrome (with this PR’s fix) has a similar issue. (Firefox works fine.)

The zero-width div workaround that you suggested solves it, but at the cost of the above issues.

Oh, that’s clever! That seems to solve Cmd+A ([CodeSandbox](https://codesandbox.io/p/devbox/dazzling-thunder-forked-pmqn5j?file=/src/index.mjs&workspaceId=ws_G7ZXTydwb2wV26Xn2zPSPT)), but it actually does cause more issues haha. With this workaround, Safari stops showing the Cmd+A highlight at all, and in both Chrome and Firefox, pressing <kbd>↑</kbd> gets the cursor stuck in the zero-width space div: https://github.com/user-attachments/assets/b9a58e2e-9f9b-4bdc-81bd-c69a39065481 You can work around _that_ by also adding a `contenteditable="true"` on the div with the zero-width space. It’s still not great, tho (it leads to an extra cursor position in front of the code editor, and to Safari highlighting the whole page on Cmd+A). Meh :/ --- Another thing I noticed: having CodeMirror first in ProseMirror also seems to break Cmd+C? (LMK if you’d like me to raise a separate issue for this.) For example, [in the original demo](https://codesandbox.io/p/devbox/dazzling-thunder-forked-3pzfq4?workspaceId=ws_G7ZXTydwb2wV26Xn2zPSPT), if you Cmd+A in Safari (which works) and then Cmd+C, the only thing the browser copies is `// This CodeMirror block is first` instead of the whole text. Chrome (with this PR’s fix) has a similar issue. (Firefox works fine.) The zero-width div workaround that you suggested solves it, but at the cost of the above issues.
iamakulov commented 2026-01-11 23:46:42 +01:00 (Migrated from github.com)

Another thing I noticed: having CodeMirror first in ProseMirror also seems to break Cmd+C? (LMK if you’d like me to raise a separate issue for this.)

Filed a fix for that: https://github.com/codemirror/view/pull/79

> Another thing I noticed: having CodeMirror first in ProseMirror also seems to break Cmd+C? (LMK if you’d like me to raise a separate issue for this.) Filed a fix for that: https://github.com/codemirror/view/pull/79
marijnh commented 2026-01-13 11:21:23 +01:00 (Migrated from github.com)

With this workaround, Safari stops showing the Cmd+A highlight at all, and in both Chrome and Firefox, pressing ↑ gets the cursor stuck in the zero-width space div

Ughhh, you're right. Nesting editable elements is a space where browsers do some really bizarre things.

I'm still worried that the fix is worse than the problem here. Even if we ignore that focus event, it seems the browser does put the actual focus on the inner editor, which is clearly not what you want. So maybe a kludge in ProseMirror that detects this situation right after selecting all, and forces the focus back to itself, is a more promising direction.

> With this workaround, Safari stops showing the Cmd+A highlight at all, and in both Chrome and Firefox, pressing ↑ gets the cursor stuck in the zero-width space div Ughhh, you're right. Nesting editable elements is a space where browsers do some really bizarre things. I'm still worried that the fix is worse than the problem here. Even if we ignore that focus event, it seems the browser does put the actual focus on the inner editor, which is clearly not what you want. So maybe a kludge in ProseMirror that detects this situation right after selecting all, and forces the focus back to itself, is a more promising direction.
iamakulov commented 2026-01-19 00:39:10 +01:00 (Migrated from github.com)

Looks like this is getting fixed upstream! https://issues.chromium.org/issues/474377386

I’ll close the PR 🙌

Looks like this is getting fixed upstream! https://issues.chromium.org/issues/474377386 I’ll close the PR 🙌

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
codemirror/view!78
No description provided.