Inline decorations cause incorrect anchor/head behavior during keyboard selection (Shift + Arrow) in Safari #1560

Open
opened 2026-02-17 14:28:57 +01:00 by rdavletshin-a11y · 2 comments
rdavletshin-a11y commented 2026-02-17 14:28:57 +01:00 (Migrated from github.com)

We implemented custom highlighting using Decoration.inline and discovered an issue with selecting text from the keyboard using Shift + Arrow combinations in the Safari browser.

In Safari, when using keyboard selection (Shift + Arrow), the selection area can only be enlarged and cannot be reduced back. Once the selection has started to expand, it continues to expand in both directions, regardless of the direction of the arrow keys, and does not return to a smaller range.

This is probably because the anchor and head values switch places depending on the direction of the hotkey used, which leads to incorrect selection logic when using Decoration.inline.

Steps to reproduce

  1. Select a word in the editor
  2. Press Shift + ArrowRight to expand the selection
  3. Press Shift + ArrowLeft to reduce the selection

Expected behavior

  • The selection expands to the right
  • After pressing Shift + ArrowLeft, the selection shrinks back toward the original range

Actual behavior

  • The selection expands to the right
  • After pressing Shift + ArrowLeft, the selection expands in the opposite direction instead of shrinking

Image

Example

To demonstrate the problem, a minimal editor based on basic schema was prepared. A plugin using Decoration.inline was added to the editor to implement custom highlighting. The example is available in CodeSandbox and reproduces the described behavior - https://codesandbox.io/p/sandbox/inline-decoration-selection-tvkfyq

Plugin code

import { Plugin, PluginKey } from "prosemirror-state";
import { Decoration, DecorationSet } from "prosemirror-view";

const selectionPlugin = () =>
  new Plugin({
    key: new PluginKey("customSelection"),
    props: {
      decorations(state) {
        return DecorationSet.create(state.doc, [
          Decoration.inline(state.selection.from, state.selection.to, {
            class: "customSelection",
          }),
        ]);
      },
    },
  });

Environment

prosemirror-view: 1.41.5
prosemirror-state: 1.4.4
Safari: 26.1 (21622.2.11.11.9)
macOS: 26.1

We implemented custom highlighting using Decoration.inline and discovered an issue with selecting text from the keyboard using `Shift + Arrow `combinations in the Safari browser. In Safari, when using keyboard selection (Shift + Arrow), the selection area can only be enlarged and cannot be reduced back. Once the selection has started to expand, it continues to expand in both directions, regardless of the direction of the arrow keys, and does not return to a smaller range. This is probably because the anchor and head values switch places depending on the direction of the hotkey used, which leads to incorrect selection logic when using `Decoration.inline`. **Steps to reproduce** 1. Select a word in the editor 2. Press `Shift + ArrowRight` to expand the selection 3. Press `Shift + ArrowLeft` to reduce the selection **Expected behavior** - The selection expands to the right - After pressing `Shift + ArrowLeft`, the selection shrinks back toward the original range **Actual behavior** - The selection expands to the right - After pressing `Shift + ArrowLeft`, the selection expands in the opposite direction instead of shrinking ![Image](https://github.com/user-attachments/assets/edde9e4e-d6e6-407a-9d75-3675d164acd9) ### Example To demonstrate the problem, a minimal editor based on _basic schema_ was prepared. A plugin using `Decoration.inline` was added to the editor to implement custom highlighting. The example is available in CodeSandbox and reproduces the described behavior - https://codesandbox.io/p/sandbox/inline-decoration-selection-tvkfyq ### Plugin code ``` import { Plugin, PluginKey } from "prosemirror-state"; import { Decoration, DecorationSet } from "prosemirror-view"; const selectionPlugin = () => new Plugin({ key: new PluginKey("customSelection"), props: { decorations(state) { return DecorationSet.create(state.doc, [ Decoration.inline(state.selection.from, state.selection.to, { class: "customSelection", }), ]); }, }, }); ``` ### Environment prosemirror-view: 1.41.5 prosemirror-state: 1.4.4 Safari: 26.1 (21622.2.11.11.9) macOS: 26.1
marijnh commented 2026-02-18 11:22:54 +01:00 (Migrated from github.com)

This is probably because the anchor and head values switch places depending on the direction of the hotkey used, which leads to incorrect selection logic when using Decoration.inline.

This seems to be something weird that Safari is doing. I confirmed that the DOM selection focus side (window.getSelection().focusNode) is still in the right place when the key event for shift-leftarrow fires, yet the native selection update that happens for that key event moves the wrong side of the selection (and inverts the DOM selection, which caused ProseMirror to then adopt that inversion for its own selection state as well).

As you found, this only happens when a decoration like the one you're using is active, not generally. And other browsers don't have the issue. So I believe it is caused by some obscure misbehavior in Safari that this situation triggers. I tried some workarounds, like always resetting the DOM selection on every update, but couldn't find one that actually manages to suppress this behavior.

For various reasons, ProseMirror relies on browsers' native behavior for cursor motion (motion in bidirectional text or weirdly styled block structure is complicated and code-heavy to implement), so when that misbehaves, we're kind of thrown. It may be possible to encode an extremely specific kludge that detects precisely this situation, but that feels a bit dodgy.

> This is probably because the anchor and head values switch places depending on the direction of the hotkey used, which leads to incorrect selection logic when using Decoration.inline. This seems to be something weird that Safari is doing. I confirmed that the DOM selection focus side (`window.getSelection().focusNode`) is still in the right place when the key event for shift-leftarrow fires, **yet** the native selection update that happens for that key event moves the wrong side of the selection (and inverts the DOM selection, which caused ProseMirror to then adopt that inversion for its own selection state as well). As you found, this only happens when a decoration like the one you're using is active, not generally. And other browsers don't have the issue. So I believe it is caused by some obscure misbehavior in Safari that this situation triggers. I tried some workarounds, like always resetting the DOM selection on every update, but couldn't find one that actually manages to suppress this behavior. For various reasons, ProseMirror relies on browsers' native behavior for cursor motion (motion in bidirectional text or weirdly styled block structure is complicated and code-heavy to implement), so when that misbehaves, we're kind of thrown. It may be possible to encode an extremely specific kludge that detects precisely this situation, but that feels a bit dodgy.
rdavletshin-a11y commented 2026-02-18 14:49:31 +01:00 (Migrated from github.com)

@marijnh Thank you very much for your reply! I will try to find a solution.

@marijnh Thank you very much for your reply! I will try to find a solution.
Sign in to join this conversation.
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
prosemirror/prosemirror#1560
No description provided.