Skip to content

fix(editor): keep selections made through the accessibility API - #1245

Open
runjuu wants to merge 1 commit into
codexu:devfrom
runjuu:dev
Open

fix(editor): keep selections made through the accessibility API#1245
runjuu wants to merge 1 commit into
codexu:devfrom
runjuu:dev

Conversation

@runjuu

@runjuu runjuu commented Sep 4, 2026

Copy link
Copy Markdown

Hi! I'm building Refine, a macOS grammar checker with
offline AI, and I ran into this while testing it against NoteGen. Refine selects the phrase it wants to
replace through the accessibility API, reads the selection back before
pasting, and finds a caret instead. That was surprising enough that I dug in.

What happens

Any selection created through the macOS accessibility API turns into a caret
within one frame of being set. The tool that made the selection can no longer
act on it. This hits every writing assistant that edits text inside other
apps that way, Grammarly's desktop app included, and it hits VoiceOver's text
selection too. In Refine's case the suggestion fails with an "unable to
apply" error.

The bubble menu's intent guard in bubble-menu.tsx does this. It trusts only
selections it saw the user make with the pointer or keyboard, and it collapses
every other non-empty selection to selection.to on the assumption that the
selection was restored from saved view state at app start or file reopen. An
accessibility client sets the DOM selection without a pointer or key event, so
the guard collapses it before anything can use it.

The change

  • restoreEditorViewState in tiptap-editor.tsx marks the transaction that
    re-applies the saved selection with a VIEW_STATE_RESTORE_META meta key, in
    both the desktop and the mobile branch.
  • updatePosition in bubble-menu.tsx reads that transaction from the
    transaction event and collapses a selection only when the marker is
    present. A selection without pointer or keyboard intent still hides the
    menu, but stays in place.
  • selection-intent.ts holds the shared meta key.

Three files, +25 / -6. Pointer and keyboard selections behave as before, and a
restored selection is still collapsed the way it was.

How to reproduce it

  1. Open a note in the Visual editor with a few paragraphs.
  2. In Xcode, open Developer Tool > Accessibility Inspector, click the target
    button, and pick the editor. The element should be a Text Area ("text
    entry area").
  3. Edit the "Selected Text Range" attribute to a non-empty range inside one
    paragraph and confirm.
  4. Read the attributes again. The range is now zero-length at the end of what
    you asked for, and "Selected Text" is empty.

As a control I ran the same steps on the ProseMirror demo at prosemirror.net
in Safari. The range stays put there, so this is NoteGen's guard rather than
WebKit or ProseMirror.

After the fix

Same steps, and the range stays selected with "Selected Text" showing the
requested characters. I verified this on a local debug build, where Refine's
suggestion now applies instead of failing its selection check.

Notes for review

  • The marker rides only on the restore transaction, so the guard still does
    what it was written for and keeps the menu hidden for a stale restored
    selection.
  • handlePointerStart still collapses on pointer down. I did not touch that
    path.
  • I went with a transaction meta rather than a broader "trust any selection
    while the editor has focus" rule because it keeps the original behaviour
    exactly where it was intended and nowhere else. If you would rather handle
    it differently, I can rework it.

中文摘要:你好,我在开发 Refine(一款 macOS 上的离线 AI 语法检查工具)时发现了这个问题。通过辅助功能 API 创建的选区会在一帧内被折叠为光标,所有以这种方式在其他应用中编辑文本的写作助手(包括 Grammarly 桌面版)以及 VoiceOver 的文本选择都会受影响,工具因此无法对选中文本进行操作。原因是气泡菜单把所有非鼠标/键盘产生的选区都当作"从视图状态恢复的旧选区"折叠。此修复让恢复选区的事务携带标记,气泡菜单只折叠带标记的选区;鼠标/键盘选区和恢复选区的行为保持不变。

The bubble menu collapses any non-empty selection it did not see the user
make with the pointer or keyboard, on the assumption that such a selection
was restored from saved view state. That also collapses selections created
through the macOS accessibility API (VoiceOver, writing assistants) within
one frame, so those tools cannot act on the text they selected.

Mark the transaction dispatched by restoreEditorViewState with a meta key
and collapse only selections carrying that marker. Every other selection
without pointer or keyboard intent still hides the menu but is left alone.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant