Skip to content

fix: Drag & drop of blocks without inline content opens formatting toolbar (BLO-1116)#2628

Merged
matthewlipski merged 4 commits into
TypeCellOS:mainfrom
miadnguyen:fix/image-drag-drop-toolbar
Apr 23, 2026
Merged

fix: Drag & drop of blocks without inline content opens formatting toolbar (BLO-1116)#2628
matthewlipski merged 4 commits into
TypeCellOS:mainfrom
miadnguyen:fix/image-drag-drop-toolbar

Conversation

@miadnguyen
Copy link
Copy Markdown
Contributor

@miadnguyen miadnguyen commented Apr 4, 2026

Summary

Fixes #2603. When dragging an image block using the side menu drag handle, the formatting toolbar incorrectly opens mid-drag.

Rationale

The formatting toolbar should only appear when the user clicks on the image or during text selection, not during a drag-and-drop operation. The unexpected toolbar appearance is disruptive.

Changes

  • Added a preventShowWhileDragging flag in FormattingToolbar.ts that mirrors the existing preventShowWhileMouseDown pattern
  • Registered dragstart and dragend listeners on editor.prosemirrorView.root to set and clear this flag for the full duration of any drag operation
  • Removed existing check to not show if block with inline content is selected.
  • Added a Playwright end-to-end test verifying the toolbar does not appear while dragging an image block
  • Moved one existing test from images to dragdrop

Impact

The toolbar is now suppressed during any block drag operation. Normal toolbar behavior is unaffected.

Testing

  • Manually verified fix on Chrome and Firefox
  • Added Playwright test in tests/src/end-to-end/images/images.test.ts: "Formatting toolbar should not appear when dragging image block"
  • Existing delete image test continues to pass

Screenshots/Video

image-drag-drop-toolbar.mov

Checklist

  • Code follows the project's coding standards.
  • Unit tests covering the new feature have been added.
  • All existing tests pass.
  • The documentation has been updated to reflect the new feature

Additional Notes

When the drag handle is used, ProseMirror sets a NodeSelection on the image node before the drag begins. The existing preventShowWhileMouseDown flag did not cover HTML drag events (dragstart/dragend), only pointer events, so the toolbar was not suppressed during drags initiated from the side menu.

Summary by CodeRabbit

  • Bug Fixes

    • Formatting toolbar now stays reliably hidden during drag interactions (including drag-handle drags) and remains hidden after pointer cancellation to avoid flicker or unwanted appearance.
  • Tests

    • End-to-end tests updated: added coverage verifying image-block drag behavior and that the formatting toolbar is not shown during/after dragging; one older drag test was replaced/streamlined.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 4, 2026

@miadnguyen is attempting to deploy a commit to the TypeCell Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

Removed NodeSelection-based toolbar suppression and added a preventShowWhileDragging flag with ProseMirror dragstart/dragend listeners to suppress formatting-toolbar updates during block drags; tests added/adjusted to verify image-block drag behavior and toolbar visibility during drags.

Changes

Cohort / File(s) Summary
FormattingToolbar Enhancement
packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts
Removed NodeSelection-specific suppression, added preventShowWhileDragging, updated editor.onChange / editor.onSelectionChange to early-return during drags, added ProseMirror dragstart/dragend listeners that set/reset the flag and call store.setState(false), and adjusted pointercancel handling.
E2E image drag tests — removed
tests/src/end-to-end/images/images.test.ts
Deleted previous end-to-end test that used dragAndDropBlock to verify image block dragging and snapshot dragImage.
E2E drag/drop tests — added/extended
tests/src/end-to-end/dragdrop/dragdrop.test.ts
Added Playwright tests that insert an image via slash command, drag it onto a heading, snapshot resulting doc (dragImage), and assert .bn-formatting-toolbar is not visible during/after dragging; imports and utilities updated accordingly.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant DOM as ProseMirror Root (DOM)
    participant Editor
    participant ToolbarStore as FormattingToolbar Store

    User->>DOM: pointer down on drag handle
    DOM->>DOM: emit dragstart
    DOM->>Editor: dragstart event -> set preventShowWhileDragging = true
    Editor->>ToolbarStore: onChange/onSelectionChange calls suppressed (early return)
    User->>DOM: drag move (drop targets update)
    Note over Editor,ToolbarStore: Toolbar remains hidden during dragging
    User->>DOM: drop (dragend)
    DOM->>Editor: dragend event -> set preventShowWhileDragging = false
    Editor->>ToolbarStore: trigger recalculation (toolbar may re-open)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I grabbed the image, held my breath,

the toolbar napped, avoided meth,
It floated past the heading's light,
and landed softly, snug and right,
A bunny cheer for tidy flight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description comprehensively covers all template sections with concrete details about the problem, solution, testing, and impact.
Linked Issues check ✅ Passed The PR fully addresses issue #2603 by preventing toolbar appearance during drag operations and enabling proper image block drag-and-drop functionality.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the drag-and-drop issue; the removal of the inline content check and image drag test are justified refactoring supporting the new drag suppression approach.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title accurately describes the main fix: preventing the formatting toolbar from opening during block drag-and-drop operations, which directly addresses the issue where dragging blocks (specifically images) would unexpectedly open the toolbar.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts (1)

103-110: ⚠️ Potential issue | 🟠 Major

pointercancel should clear preventShowWhileMouseDown, not set it.

Line 107 contradicts the comment above it. In this file, the only reset path for preventShowWhileMouseDown is the pointerup handler, so setting it to true here can leave the toolbar suppressed after a canceled drag/pointer sequence.

Suggested change
      dom.addEventListener(
        "pointercancel",
        () => {
-          preventShowWhileMouseDown = true;
+          preventShowWhileMouseDown = false;
        },
        { signal, capture: true },
      );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts` around
lines 103 - 110, The pointercancel handler currently sets
preventShowWhileMouseDown to true which contradicts its comment and leaves the
toolbar suppressed; update the "pointercancel" event listener so it clears the
flag (set preventShowWhileMouseDown to false) — mirror the reset behavior of the
existing pointerup handler and ensure the handler on "pointercancel" (inside
FormattingToolbar.ts near the preventShowWhileMouseDown logic) resets the state
instead of setting it.
🧹 Nitpick comments (1)
tests/src/end-to-end/images/images.test.ts (1)

166-171: Please assert the drop result after mouse.up().

This currently proves only that the toolbar stays hidden mid-drag. It still passes if the handle path never reorders the document, which is the user-visible part of #2603. Add a post-drop snapshot or DOM-order assertion.

Possible follow-up
     const toolbar = page.locator(".bn-formatting-toolbar");
     await expect(toolbar).not.toBeVisible();
     
     await page.mouse.up();
+    await compareDocToSnapshot(page, "dragImageViaHandle");
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/src/end-to-end/images/images.test.ts` around lines 166 - 171, After the
drag is finished (after the existing await page.mouse.up()), add a post-drop
assertion to verify the actual reorder happened rather than only that the
toolbar is hidden: use the same test's DOM locators to either take a snapshot of
the document and compare it (post-drop snapshot) or assert the DOM order of the
moved elements (e.g., grab the image/handle nodes and assert their
src/text/index positions have changed). Place this assertion immediately after
await page.mouse.up() (near the existing toolbar locator) so the test fails if
the handle never reorders the document.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts`:
- Around line 103-110: The pointercancel handler currently sets
preventShowWhileMouseDown to true which contradicts its comment and leaves the
toolbar suppressed; update the "pointercancel" event listener so it clears the
flag (set preventShowWhileMouseDown to false) — mirror the reset behavior of the
existing pointerup handler and ensure the handler on "pointercancel" (inside
FormattingToolbar.ts near the preventShowWhileMouseDown logic) resets the state
instead of setting it.

---

Nitpick comments:
In `@tests/src/end-to-end/images/images.test.ts`:
- Around line 166-171: After the drag is finished (after the existing await
page.mouse.up()), add a post-drop assertion to verify the actual reorder
happened rather than only that the toolbar is hidden: use the same test's DOM
locators to either take a snapshot of the document and compare it (post-drop
snapshot) or assert the DOM order of the moved elements (e.g., grab the
image/handle nodes and assert their src/text/index positions have changed).
Place this assertion immediately after await page.mouse.up() (near the existing
toolbar locator) so the test fails if the handle never reorders the document.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5b850337-2b91-4ef2-bdbc-615cbe5cb0d4

📥 Commits

Reviewing files that changed from the base of the PR and between 38be5fd and 589c7e7.

📒 Files selected for processing (2)
  • packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts
  • tests/src/end-to-end/images/images.test.ts

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview Apr 23, 2026 8:48am
blocknote-website Ready Ready Preview Apr 23, 2026 8:48am

Request Review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 22, 2026

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/@blocknote/ariakit@2628

@blocknote/code-block

npm i https://pkg.pr.new/@blocknote/code-block@2628

@blocknote/core

npm i https://pkg.pr.new/@blocknote/core@2628

@blocknote/mantine

npm i https://pkg.pr.new/@blocknote/mantine@2628

@blocknote/react

npm i https://pkg.pr.new/@blocknote/react@2628

@blocknote/server-util

npm i https://pkg.pr.new/@blocknote/server-util@2628

@blocknote/shadcn

npm i https://pkg.pr.new/@blocknote/shadcn@2628

@blocknote/xl-ai

npm i https://pkg.pr.new/@blocknote/xl-ai@2628

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@2628

@blocknote/xl-email-exporter

npm i https://pkg.pr.new/@blocknote/xl-email-exporter@2628

@blocknote/xl-multi-column

npm i https://pkg.pr.new/@blocknote/xl-multi-column@2628

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@2628

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@2628

commit: f83b5d6

@matthewlipski
Copy link
Copy Markdown
Collaborator

This is great, thanks! With your fix, I've also removed the check in shouldShow we used before to prevent the formatting toolbar from showing up on drop for blocks with inline content, since your solution works for all blocks.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts (1)

94-118: ⚠️ Potential issue | 🔴 Critical

pointercancel handler contradicts its own comment and permanently suppresses the toolbar after a drag.

The comment on line 94 explicitly states the goal: "we don't want to be stuck in the preventShowWhileMouseDown state". However, the handler sets preventShowWhileMouseDown = true on line 98—directly trapping it in that state. The flag can only be reset to false in the pointerup handler (line 85).

In a typical drag sequence:

  1. pointerdownpreventShowWhileMouseDown = true
  2. dragstartpreventShowWhileDragging = true, toolbar hidden
  3. pointercancel → fires when browser takes over for native DnD; flag remains true
  4. dragend → only clears preventShowWhileDragging; preventShowWhileMouseDown is not reset
  5. pointerup → typically not dispatched after native HTML5 drag

Result: after the first drag, preventShowWhileMouseDown is stuck true. Since both onChange (line 58) and onSelectionChange (line 65) short-circuit when this flag is set, the formatting toolbar will stop appearing on text selections until a clean pointerdown→pointerup cycle occurs.

Fix: change line 98 from preventShowWhileMouseDown = true; to preventShowWhileMouseDown = false;, or defensively also reset it in the dragend handler to cover the drag-takeover path on all browsers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts` around
lines 94 - 118, The pointercancel handler currently sets
preventShowWhileMouseDown = true which contradicts the comment and leaves the
toolbar permanently suppressed after native drag; update the pointercancel
handler to set preventShowWhileMouseDown = false (or at minimum also clear that
flag in the dragend handler) so preventShowWhileMouseDown is reset when the
browser takes over dragging; locate the pointercancel listener and the dragend
listener in FormattingToolbar (look for preventShowWhileMouseDown,
preventShowWhileDragging, and the event listeners on
editor.prosemirrorView.root/dom) and change the assignment accordingly so
selection/toolbar logic (onChange/onSelectionChange) can run after drag
sequences.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts`:
- Around line 94-118: The pointercancel handler currently sets
preventShowWhileMouseDown = true which contradicts the comment and leaves the
toolbar permanently suppressed after native drag; update the pointercancel
handler to set preventShowWhileMouseDown = false (or at minimum also clear that
flag in the dragend handler) so preventShowWhileMouseDown is reset when the
browser takes over dragging; locate the pointercancel listener and the dragend
listener in FormattingToolbar (look for preventShowWhileMouseDown,
preventShowWhileDragging, and the event listeners on
editor.prosemirrorView.root/dom) and change the assignment accordingly so
selection/toolbar logic (onChange/onSelectionChange) can run after drag
sequences.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9d9af037-1a14-49e1-b111-ca27e7c67691

📥 Commits

Reviewing files that changed from the base of the PR and between 589c7e7 and 6d0218b.

📒 Files selected for processing (1)
  • packages/core/src/extensions/FormattingToolbar/FormattingToolbar.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/src/end-to-end/dragdrop/dragdrop.test.ts (1)

101-114: Assertion only covers post-drop state, not during-drag.

The toolbar-suppression fix targets the window between dragstart and dragend, but this test only checks .bn-formatting-toolbar visibility after dragAndDropBlock has fully completed. If a regression ever re-introduces a transient toolbar flash mid-drag (the exact symptom from #2603), this assertion would still pass.

Consider also asserting the toolbar is hidden mid-drag — e.g., split dragAndDropBlock into its mousedown/move/up steps and check await expect(toolbar).not.toBeVisible() after the drag has started but before the drop, or listen for the dragstart event and sample visibility then. Non-blocking; the current assertion still guards the end-state user impact.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/src/end-to-end/dragdrop/dragdrop.test.ts` around lines 101 - 114, The
test only asserts toolbar visibility after dragAndDropBlock completes; update it
to verify the toolbar is hidden during the drag start window as well by
splitting the drag into explicit steps: use
focusOnEditor/executeSlashCommand/insertHeading as before, locate dragTarget
(IMAGE_SELECTOR) and dropTarget (H_ONE_BLOCK_SELECTOR), then initiate the drag
manually (e.g., dispatch mousedown or call page.mouse.move/start by moving to
dragTarget and issuing a mouse.down and a mouse.move toward dropTarget) and
immediately assert await
expect(page.locator(".bn-formatting-toolbar")).not.toBeVisible() before
completing the action (mouse.up or drop); alternatively adjust dragAndDropBlock
to provide a “pause before drop” hook and assert mid-drag there.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/src/end-to-end/dragdrop/dragdrop.test.ts`:
- Around line 87-114: The two new tests ("Should be able to drag image" and
"Formatting toolbar should not appear when dragging image block") use
dragAndDropBlock and must be skipped on Firefox like the existing drag tests;
add the same Firefox guard (e.g., test.skip(browserName === "firefox",
"Playwright doesn't correctly simulate drag events in Firefox")) to each test
declaration that uses dragAndDropBlock (referencing IMAGE_SELECTOR,
H_ONE_BLOCK_SELECTOR and the dragAndDropBlock helper) so they are not executed
on the Firefox runner.

---

Nitpick comments:
In `@tests/src/end-to-end/dragdrop/dragdrop.test.ts`:
- Around line 101-114: The test only asserts toolbar visibility after
dragAndDropBlock completes; update it to verify the toolbar is hidden during the
drag start window as well by splitting the drag into explicit steps: use
focusOnEditor/executeSlashCommand/insertHeading as before, locate dragTarget
(IMAGE_SELECTOR) and dropTarget (H_ONE_BLOCK_SELECTOR), then initiate the drag
manually (e.g., dispatch mousedown or call page.mouse.move/start by moving to
dragTarget and issuing a mouse.down and a mouse.move toward dropTarget) and
immediately assert await
expect(page.locator(".bn-formatting-toolbar")).not.toBeVisible() before
completing the action (mouse.up or drop); alternatively adjust dragAndDropBlock
to provide a “pause before drop” hook and assert mid-drag there.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 82bbc5d4-fc0d-49ad-a12c-242bd2955bce

📥 Commits

Reviewing files that changed from the base of the PR and between 6d0218b and 88aae16.

📒 Files selected for processing (5)
  • tests/src/end-to-end/dragdrop/dragdrop.test.ts
  • tests/src/end-to-end/dragdrop/dragdrop.test.ts-snapshots/dragImage-chromium-linux.json
  • tests/src/end-to-end/dragdrop/dragdrop.test.ts-snapshots/dragImage-firefox-linux.json
  • tests/src/end-to-end/dragdrop/dragdrop.test.ts-snapshots/dragImage-webkit-linux.json
  • tests/src/end-to-end/images/images.test.ts
✅ Files skipped from review due to trivial changes (1)
  • tests/src/end-to-end/images/images.test.ts

Comment thread tests/src/end-to-end/dragdrop/dragdrop.test.ts Outdated
@matthewlipski matthewlipski changed the title fix: move an image doesn't work with drag and drop (#2603) fix: move an image doesn't work with drag and drop (BLO-1116) Apr 23, 2026
@matthewlipski matthewlipski changed the title fix: move an image doesn't work with drag and drop (BLO-1116) fix: Drag & drop of blocks without inline content opens formatting toolbar (BLO-1116) Apr 23, 2026
@matthewlipski matthewlipski merged commit fc44d00 into TypeCellOS:main Apr 23, 2026
58 of 59 checks passed
@nperez0111
Copy link
Copy Markdown
Contributor

Thanks @miadnguyen, we really appreciate your contribution. Should be out sometime early next week

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.

Move an image doesn't work with drag and drop

3 participants