Skip to content

[ew] refactor toolbar - #1175

Open
hannessolo wants to merge 9 commits into
mainfrom
tbctrl
Open

[ew] refactor toolbar#1175
hannessolo wants to merge 9 commits into
mainfrom
tbctrl

Conversation

@hannessolo

Copy link
Copy Markdown
Contributor

hannessolo and others added 4 commits July 24, 2026 10:29
…t active surface

The toolbar's visibility was derived from the doc view's `view.hasFocus()` while
that same value was faked to keep collab awareness working in WYSIWYG mode — a
self-contradiction that made the toolbar flicker, drop out, or fail to appear
(especially on the WYSIWYG side of split view).

Introduce a single ToolbarController that owns visibility, derived once per frame
from an explicit active surface ('doc' | 'wysiwyg') plus selection/mode — never
from focus. Editors emit intent; nothing else shows/hides the toolbar.

Key fixes:
- Null `cursor-move` (a da-nx per-block blur) is awareness-only, no longer hides
  the toolbar — the dominant drop-out.
- Detect focus entering the cross-origin iframe via `window` blur + shadow-piercing
  active element, since the iframe's own focus events don't fire and da-nx sends no
  message when a click doesn't change the block selection (e.g. end of a line).
- The focus lie is scoped to mirror dispatches (collab awareness only) and never
  read by the visibility layer.
- Coalesce visibility updates to one requestAnimationFrame render.

Design and investigation notes in docs/canvas-toolbar-architecture.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
y-prosemirror's cursor plugin broadcasts this user's caret to collaborators
only while the doc view "has focus", and clears it on the next update once
focus is lost. Its updateCursorInfo is the plugin's view.update callback, so it
runs after every transaction. A focus lie scoped to a single mirror dispatch
therefore only survived one tick: the next unrelated update (a remote edit, the
iframe streaming into Yjs, or the redraw our own setLocalStateField triggers)
ran with the real hasFocus() === false and wiped the just-broadcast cursor. The
caret flashed to peers and vanished.

Gate the lie on the active surface instead: while activeSurface === 'wysiwyg'
the doc view reports focus, so the cursor keeps broadcasting for the whole time
the iframe owns editing (layout and split). When the user leaves, the real
check returns and the cursor is correctly cleared.

The lie makes selectionToDOM treat the doc view as owning the selection, but
that only writes a DOM selection range (no focus move). The one thing that would
steal focus is view.focus(), so neuter it while the iframe is active — no caller
(drop handler, command, restoreFocus) can pull focus off the iframe and revive
the toolbar-visibility bugs. dispatchWithFakeFocus is retained as a complement
for the instant before the surface flips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The WYSIWYG iframe owns block-level structure, so the shared selection toolbar
now renders a surface-appropriate button set. The controller pushes the active
surface onto the element, and the element gates its sections:

- doc surface: full toolbar (block-type picker, marks, structure, tables, links,
  images incl. add-image)
- wysiwyg surface: inline marks, link controls, and image alt-text editing only
  — the block-type picker, list/structure, table controls, and the add-image
  action are dropped (block insertion and block structure belong to the iframe)

In split view the button set follows the pane you're editing. Ports the
per-surface intent from the earlier PR #1018 into the current architecture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
handleCursorMove preserved state.storedMarks whenever the new position had no
adjacent marks, so moving the caret off bold text left bold queued on unmarked
text. That preservation was only meant to survive the same-position cursor-move
the iframe re-reports right after a toolbar toggle — not a real move.

Track the last cursor offset and branch on it: a genuine move resets stored
marks to what's at the new location (nothing on unmarked text), while a
same-position re-report keeps a toolbar-toggled mark until the user types or
actually moves. Also forget the position on iframe blur so re-entry counts as a
move.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@aem-code-sync

aem-code-sync Bot commented Jul 24, 2026

Copy link
Copy Markdown

Hello, I'm the AEM Code Sync Bot and I will run some actions to deploy your branch.
In case there are problems, just click the checkbox below to rerun the respective action.

  • Re-sync branch
Commits

@hannessolo

Copy link
Copy Markdown
Contributor Author

Replaces #1018

getSelectionToolbar() was a one-line pass-through to
toolbarController.ensureToolbar(); production code already calls the controller
directly. Remove it and point the remaining test at ensureToolbar().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hannessolo
hannessolo marked this pull request as ready for review July 24, 2026 10:22
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

2 participants