Skip to content

fix(core): slash menu fails in custom blocks after space#2553

Open
YousefED wants to merge 5 commits intomainfrom
fix/slash-menu-custom-blocks-2531
Open

fix(core): slash menu fails in custom blocks after space#2553
YousefED wants to merge 5 commits intomainfrom
fix/slash-menu-custom-blocks-2531

Conversation

@YousefED
Copy link
Collaborator

@YousefED YousefED commented Mar 11, 2026

Summary

Fix the slash menu (and other suggestion menus) not opening when the trigger character is typed after a space inside custom blocks with isolating: true.

Closes #2531
Closes #659

Rationale

Tiptap's NodeViewWrapper hardcodes white-space: normal as an inline style on all React-based custom block wrappers. ProseMirror requires white-space: pre-wrap on editable content areas — without it, browsers (especially Chrome) normalize trailing ASCII spaces to NBSP (\u00a0) during input.

When a user types / after a trailing space inside a custom block, the browser first converts the space to NBSP. ProseMirror's readDOMChangefindDiff then sees a replacement (space→NBSP+char) instead of a pure insertion, calling handleTextInput(view, from, to, text) with from !== to. The SuggestionMenu plugin's if (from === to) guard rejects this, and the menu never opens.

Issue #659 (spaces eaten at end of custom blocks in Firefox) shares the same root cause — white-space: normal collapses trailing whitespace. While we could not reproduce #659 on current Firefox versions bundled with Playwright, the fix addresses the underlying CSS problem.

Changes

  • packages/core/src/editor/Block.css: Added white-space: pre-wrap to .bn-inline-content, overriding the normal inherited from tiptap's NodeViewWrapper.
  • tests/src/end-to-end/slashmenu/slashmenu-customblock.test.ts: New Playwright e2e regression test using the alert-block demo — verifies the slash menu opens both with and without a preceding space.

Impact

  • For standard blocks (paragraph, heading, etc.): white-space was already pre-wrap via inheritance from ProseMirror's default styles. The explicit declaration is a no-op.
  • For custom blocks wrapped by tiptap's NodeViewWrapper: white-space changes from normal to pre-wrap, restoring correct ProseMirror behavior. This is the intended fix.
  • No visual changes expected — pre-wrap is what ProseMirror already uses everywhere else.

Testing

  • Playwright e2e: Verified the test fails on Chromium without the fix, and passes on both Chromium and Firefox with the fix.
  • Manually tested slash menu in paragraphs, headings, and custom alert blocks.
  • Existing unit tests for SuggestionMenu continue to pass.

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

Summary by CodeRabbit

  • Bug Fixes

    • Improved inline whitespace handling to prevent trailing-space normalization from breaking suggestion triggers and restore consistent editor behavior.
  • Tests

    • Added end-to-end regression tests ensuring the global slash menu opens correctly inside custom alert blocks across typing scenarios, plus related test configuration for the alert-block page.

…h menu in custom blocks

Tiptap's NodeViewWrapper hardcodes `white-space: normal` on custom block
wrappers. Under this style, browsers normalize trailing spaces to NBSP,
causing ProseMirror's readDOMChange to compute a replacement (from !== to)
instead of a pure insertion. The SuggestionMenu plugin's handleTextInput
guard then rejects the input and the menu fails to open.

Fix: add `white-space: pre-wrap` to `.bn-inline-content` in Block.css,
overriding the inherited `normal` and preventing NBSP normalization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Mar 11, 2026

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

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview Mar 11, 2026 3:28pm
blocknote-website Ready Ready Preview Mar 11, 2026 3:28pm

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (3)
  • tests/src/end-to-end/static/static.test.ts-snapshots/static-rendering-chromium-linux.png is excluded by !**/*.png
  • tests/src/end-to-end/static/static.test.ts-snapshots/static-rendering-firefox-linux.png is excluded by !**/*.png
  • tests/src/end-to-end/static/static.test.ts-snapshots/static-rendering-webkit-linux.png is excluded by !**/*.png

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d6d05963-9b02-4ed6-ac7c-1bfe9c7469e0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a CSS rule to preserve trailing spaces in inline block content, a new end-to-end Playwright test for slash-menu behavior in a custom alert block, and a new test URL constant for the alert-block test.

Changes

Cohort / File(s) Summary
Whitespace Handling
packages/core/src/editor/Block.css
Added white-space: pre-wrap; to .bn-inline-content to ensure trailing spaces are preserved even if a parent resets whitespace, preventing trailing-space normalization from affecting ProseMirror insertion logic and suggestion triggers.
Slash Menu Regression Tests
tests/src/end-to-end/slashmenu/slashmenu-customblock.test.ts
Added Playwright end-to-end tests verifying the global slash menu opens inside a custom alert block when typing / at the end and when typing / (space then slash).
Test Constants
tests/src/utils/const.ts
Added ALERT_BLOCK_URL export following existing environment pattern to point tests at the alert-block custom-schema page (uses RUN_IN_DOCKER conditional).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐇 I nibble spaces, small and bright,
I tuck them in with gentle might,
A slash hops in, the menu sings,
Preserved at end—oh joyous things! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the primary fix: the slash menu failing in custom blocks when a space precedes the trigger character.
Description check ✅ Passed The description covers most template sections with detailed technical context, but the documentation update checkbox is unchecked.
Linked Issues check ✅ Passed The CSS fix and e2e test directly address both linked issues: #2531 (mention trigger after space in custom blocks) and #659 (trailing-space handling).
Out of Scope Changes check ✅ Passed All code changes are directly scoped to fixing the reported issues and include only necessary CSS, test constants, and e2e regression tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/slash-menu-custom-blocks-2531

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

Copy link

@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.

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

46-48: Minor: Consider combining keystrokes for brevity.

The space and slash could be typed in a single call for conciseness, though keeping them separate also clearly documents the regression scenario.

     // Type a space first — this is the scenario that broke the menu
-    await page.keyboard.type(" ");
-    await page.keyboard.type("/");
+    await page.keyboard.type(" /");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/src/end-to-end/slashmenu/slashmenu-customblock.test.ts` around lines 46
- 48, The test types a space then a slash in two separate calls to
page.keyboard.type; compress these into a single call to page.keyboard.type that
sends both characters together to make the test more concise while preserving
the same keystroke sequence (update the two page.keyboard.type invocations to
one combined invocation).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/src/end-to-end/slashmenu/slashmenu-customblock.test.ts`:
- Around line 46-48: The test types a space then a slash in two separate calls
to page.keyboard.type; compress these into a single call to page.keyboard.type
that sends both characters together to make the test more concise while
preserving the same keystroke sequence (update the two page.keyboard.type
invocations to one combined invocation).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c3e013af-dadc-4f7d-850f-3aba1fca2e96

📥 Commits

Reviewing files that changed from the base of the PR and between a69bba9 and e78c722.

📒 Files selected for processing (2)
  • packages/core/src/editor/Block.css
  • tests/src/end-to-end/slashmenu/slashmenu-customblock.test.ts

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 11, 2026

Open in StackBlitz

@blocknote/ariakit

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

@blocknote/code-block

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

@blocknote/core

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

@blocknote/mantine

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

@blocknote/react

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

@blocknote/server-util

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

@blocknote/shadcn

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

@blocknote/xl-ai

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

@blocknote/xl-docx-exporter

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

@blocknote/xl-email-exporter

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

@blocknote/xl-multi-column

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

@blocknote/xl-odt-exporter

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

@blocknote/xl-pdf-exporter

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

commit: 47e100b

@nperez0111
Copy link
Contributor

I was able to validate that we fixed #659 on firefox with this

The e2e test was using port 5173 (local dev server) instead of port
3000 which is what CI expects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@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/slashmenu/slashmenu-customblock.test.ts (1)

24-30: Extract shared alert-content focus steps into a helper.

Both tests duplicate the same locator/focus/caret placement flow; pulling this into a helper will make future updates safer.

Refactor sketch
+async function focusAlertInlineContent(page: import("@playwright/test").Page) {
+  const alertContent = page
+    .locator('[data-content-type="alert"]')
+    .first()
+    .locator(".bn-inline-content");
+  await alertContent.click();
+  await page.keyboard.press("End");
+  return alertContent;
+}
+
 test.describe("Slash menu in custom (alert) block – issue `#2531`", () => {
@@
-    const alertContent = page
-      .locator('[data-content-type="alert"]')
-      .first()
-      .locator(".bn-inline-content");
-    await alertContent.click();
-    await page.keyboard.press("End");
+    await focusAlertInlineContent(page);
@@
-    const alertContent = page
-      .locator('[data-content-type="alert"]')
-      .first()
-      .locator(".bn-inline-content");
-    await alertContent.click();
-    await page.keyboard.press("End");
+    await focusAlertInlineContent(page);

Also applies to: 39-45

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

In `@tests/src/end-to-end/slashmenu/slashmenu-customblock.test.ts` around lines 24
- 30, Extract the duplicated locator/focus/caret placement into a small async
helper (e.g., focusAlertContent or getAndFocusAlertContent) that accepts the
Playwright Page, finds the element via
locator('[data-content-type="alert"]').first().locator('.bn-inline-content'),
clicks it and presses "End" to place the caret, then returns the locator if
needed; replace both occurrences (the block using alertContent at the top and
the similar block around lines 39-45) with calls to this helper and await its
result so future updates to the focus behavior are centralized.
🤖 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/slashmenu/slashmenu-customblock.test.ts`:
- Around line 6-8: The ternary that sets ALERT_BLOCK_URL relies on string
truthiness of process.env.RUN_IN_DOCKER (using !process.env.RUN_IN_DOCKER),
which misbehaves for values like "false"; change the check to an explicit string
comparison (e.g., process.env.RUN_IN_DOCKER === 'true') and flip the ternary
branches so ALERT_BLOCK_URL selects host.docker.internal when RUN_IN_DOCKER ===
'true' and localhost otherwise; apply the same explicit comparison pattern to
other similar constants (e.g., BASE_URL, SHADCN_URL, ARIAKIT_URL) to avoid
accidental Docker URL selection.

---

Nitpick comments:
In `@tests/src/end-to-end/slashmenu/slashmenu-customblock.test.ts`:
- Around line 24-30: Extract the duplicated locator/focus/caret placement into a
small async helper (e.g., focusAlertContent or getAndFocusAlertContent) that
accepts the Playwright Page, finds the element via
locator('[data-content-type="alert"]').first().locator('.bn-inline-content'),
clicks it and presses "End" to place the caret, then returns the locator if
needed; replace both occurrences (the block using alertContent at the top and
the similar block around lines 39-45) with calls to this helper and await its
result so future updates to the focus behavior are centralized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d0b65165-a213-4f21-b694-606129242d2f

📥 Commits

Reviewing files that changed from the base of the PR and between e78c722 and 0d905bf.

📒 Files selected for processing (1)
  • tests/src/end-to-end/slashmenu/slashmenu-customblock.test.ts

Comment on lines +6 to +8
const ALERT_BLOCK_URL = !process.env.RUN_IN_DOCKER
? `http://localhost:${PORT}/custom-schema/alert-block?hideMenu`
: `http://host.docker.internal:${PORT}/custom-schema/alert-block?hideMenu`;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C2 '\bRUN_IN_DOCKER\b' -g '!**/node_modules/**'

Repository: TypeCellOS/BlockNote

Length of output: 4428


Make RUN_IN_DOCKER evaluation explicit to avoid accidental Docker URL selection.

Using !process.env.RUN_IN_DOCKER relies on string truthiness—!"false" evaluates to false (not true), causing the wrong URL to be selected if the variable is set to the string "false". This pattern is used throughout the codebase (tests/src/utils/const.ts has 10+ instances) and is set explicitly to "true" in CI scripts (tests/package.json), but an explicit comparison guards against environment-dependent test flakiness.

Proposed fix
 const PORT = 3000;
-const ALERT_BLOCK_URL = !process.env.RUN_IN_DOCKER
+const isDocker = process.env.RUN_IN_DOCKER === "true";
+const ALERT_BLOCK_URL = !isDocker
   ? `http://localhost:${PORT}/custom-schema/alert-block?hideMenu`
   : `http://host.docker.internal:${PORT}/custom-schema/alert-block?hideMenu`;

Note: This same pattern exists in tests/src/utils/const.ts for BASE_URL, SHADCN_URL, ARIAKIT_URL, and others. Consider applying the same fix across the codebase for consistency.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ALERT_BLOCK_URL = !process.env.RUN_IN_DOCKER
? `http://localhost:${PORT}/custom-schema/alert-block?hideMenu`
: `http://host.docker.internal:${PORT}/custom-schema/alert-block?hideMenu`;
const isDocker = process.env.RUN_IN_DOCKER === "true";
const ALERT_BLOCK_URL = !isDocker
? `http://localhost:${PORT}/custom-schema/alert-block?hideMenu`
: `http://host.docker.internal:${PORT}/custom-schema/alert-block?hideMenu`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/src/end-to-end/slashmenu/slashmenu-customblock.test.ts` around lines 6
- 8, The ternary that sets ALERT_BLOCK_URL relies on string truthiness of
process.env.RUN_IN_DOCKER (using !process.env.RUN_IN_DOCKER), which misbehaves
for values like "false"; change the check to an explicit string comparison
(e.g., process.env.RUN_IN_DOCKER === 'true') and flip the ternary branches so
ALERT_BLOCK_URL selects host.docker.internal when RUN_IN_DOCKER === 'true' and
localhost otherwise; apply the same explicit comparison pattern to other similar
constants (e.g., BASE_URL, SHADCN_URL, ARIAKIT_URL) to avoid accidental Docker
URL selection.

Align with the pattern used by all other Playwright tests — URL is
defined once in tests/src/utils/const.ts and imported, instead of
being hardcoded in the test file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The CSS fix (white-space: pre-wrap on .bn-inline-content) causes a slight
visual difference in static rendering where ProseMirror styles aren't loaded.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.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.

Mention element doesn't work as expected within custom blocks Bug: Cannot add space at the end of a custom block's content

2 participants