Skip to content

fix(SelectionContext): keep selection tooltip alive after plugin view re-creation#1171

Draft
resure wants to merge 1 commit into
gravity-ui:mainfrom
resure:fix/selection-context-tooltip-rearm
Draft

fix(SelectionContext): keep selection tooltip alive after plugin view re-creation#1171
resure wants to merge 1 commit into
gravity-ui:mainfrom
resure:fix/selection-context-tooltip-rearm

Conversation

@resure

@resure resure commented Jul 2, 2026

Copy link
Copy Markdown

Text below and fix itself is mostly ai-generated, but I've also checked suggested fix manually on my project that uses markdown-editor


The floating selection toolbar (WYSIWYG mode, non-empty selectionContext config) goes permanently dead after any host-side EditorState swap.

The bug

SelectionTooltip keeps two mutable flags on the extension instance — destroyed and _isMousePressed — and nothing re-arms them when its plugin view is re-created. ProseMirror re-creates every plugin view whenever state.plugins changes identity, and EditorState.create always builds a fresh plugins array — so any host-side state swap triggers this (e.g. an undo-history reset), even one that passes view.state.plugins through unchanged:

  1. The swap destroys the plugin views → destroy() sets destroyed = true. The view is re-created immediately, but view() never resets the flag.
  2. The next mousedown hides the tooltip, sets _isMousePressed = true, and arms a one-shot document mouseup listener.
  3. That listener starts with if (this.destroyed) return — so _isMousePressed is never reset.
  4. Every later update() exits at if (this._isMousePressed) return. The toolbar never appears again for the lifetime of the editor — no errors, nothing in the console.

Minimal reproduction:

// 1. Select text with the mouse → the floating toolbar appears. Fine.
// 2. Swap the state once:
view.updateState(EditorState.create({doc: view.state.doc, plugins: view.state.plugins}));
// 3. Click once anywhere in the editor.
// 4. Select any text, with mouse or keyboard → the toolbar never appears again.

Two related flaws in the same handler:

  • The mouseup re-check passes the state snapshotted at mousedown, and update() skips when doc + selection are unchanged since then. But the mousedown just hid the tooltip — so a press-release cycle that keeps the selection (e.g. re-selecting the same word) strands the toolbar hidden over a live selection.
  • The mouseup gate is armed for every button, but some presses never deliver a document mouseup at all — a right-click on macOS (the native context menu opens during mousedown and swallows the release), or a press that turns into a native drag (ends with dragend) — wedging _isMousePressed until the next completed left click.

The fix

  • view(): re-arm destroyed and _isMousePressed when the plugin view is (re-)created — plugin views are re-created far more often than plugins are.
  • mouseup: re-evaluate without the mousedown snapshot — "unchanged" must not mean "keep hidden".
  • mousedown: arm the gate only for the primary button (a non-left press still hides, as before), and release it on dragstart.

The plugin-view update path keeps its prevState short-circuit — that one is correct (the hide-meta check runs before it).

Tests

The new index.test.ts drives the plugin through the public SelectionContext extension with a real EditorView in jsdom, including a real view.updateState(EditorState.create(...)) swap for the main scenario — so it exercises ProseMirror's actual plugin-view re-creation. On current main the baseline test passes and the four regression tests fail (one per flaw); with the fix all five pass. The package unit suite stays green.

Found in gravity-notes, which currently ships a vendored copy of this plugin with these fixes; also verified end-to-end there in a packaged app with the plugin's gate decisions instrumented (before: mousedown → mouseup → (nothing), toolbar dead; after: mousedown → mouseup → SHOW).

Summary by Sourcery

Fix SelectionContext tooltip behavior so the floating selection toolbar remains functional after editor state swaps and various mouse interactions.

Bug Fixes:

  • Prevent the selection tooltip from becoming permanently disabled after plugin view re-creation by rearming internal state flags when the plugin view is created.
  • Ensure the tooltip reappears after clicks that keep the document and selection unchanged instead of remaining hidden.
  • Avoid wedging tooltip updates on non-primary mouse button presses that may not emit a corresponding mouseup event.
  • Release the tooltip’s mouseup gating when a mouse press turns into a native drag sequence so subsequent updates are not blocked.

Tests:

  • Add integration-style tests for SelectionContext using a real EditorView to cover state swaps, unchanged-click scenarios, non-primary button presses, and drag interactions.

@gravity-ui

gravity-ui Bot commented Jul 2, 2026

Copy link
Copy Markdown

🎭 Playwright Report

resure added a commit to resure/gravity-notes that referenced this pull request Jul 2, 2026
The virtualized note list now renders only an IconPickerButton glyph per
row and shares ONE controlled IconPickerPopup (the split lives in
IconPicker.tsx) — a per-row picker was a large render cost and an open
one died when its row left the virtual window. Plus fixes from a
max-effort review of the change:

- editorCaret: the no-rect first-line fallback treats block boundaries
  as line breaks (an empty second bullet/blockquote line no longer
  yanks ArrowUp to the title), skips ProseMirror-trailingBreak
  placeholders (a caret at (p,1) on a blank line still hands off),
  gates on the collapsed range start (backward selections), and scans
  with comparePoint instead of deep-cloning via cloneContents
- NoteList: Enter on a focused row button activates the button instead
  of opening the note; the ⋯ menu gains the picker's anchor keepalive +
  close-when-note-leaves; the shared popup is memoized with stable
  handlers (live-ref) and drops picks whose note id went stale
- IconPicker: popup resets query/tab/scroll/highlight on every close so
  no state leaks across rows; open derived from the anchor
- Workspace: F2 no-ops inside any floating layer (.g-popup /
  [role=dialog]) instead of a per-widget class blocklist
- docs: EDITOR_BUG_REPORT.md superseded by the filed upstream PR
  (gravity-ui/markdown-editor#1171, linked in selectionContextFix.ts);
  HANDOFF.md follow-ups moved to README (fonts-CSP known limitation,
  icon-picker backlog); note-icons feature bullet added

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@resure resure force-pushed the fix/selection-context-tooltip-rearm branch from 255ed54 to 00d197b Compare July 6, 2026 12:54
…w re-creations and native context menus

ProseMirror destroys and re-creates plugin views whenever state.plugins
changes identity (any host-side EditorState.create, even one that passes
the current plugins through). The spec's destroyed flag stayed true after
that, so the mouse release was ignored, the press gate stuck, and the
tooltip never showed again. Re-arm the flag in view() and re-evaluate on
release without the mousedown snapshot, so a click that leaves the
doc/selection unchanged re-shows the tooltip instead of stranding it hidden.

Rework the press gate around a single armPressGate helper that owns its
document listeners via AbortController:

- treat ctrl+click as a context-menu press: macOS reports it with button 0
  while the native menu still swallows the mouseup, which left the gate stuck
- gate updates during a non-primary press so the browser's right-click
  word-selection doesn't pop the tooltip up under the open native menu;
  the gate releases on the next input that reaches the page
- keep the gate armed across a mid-press plugin view re-creation instead of
  resetting it, so a host-side state swap during a drag doesn't un-gate updates
- disarm the pending mouseup listener on dragstart instead of leaving it to
  fire on the next unrelated mouseup
- ignore chorded secondary-button releases and releases whose editor was
  destroyed while the button was held
@resure resure force-pushed the fix/selection-context-tooltip-rearm branch from 00d197b to 089e9a7 Compare July 6, 2026 15:24
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