Skip to content

feat(react): Added select component for text align feature#2728

Open
GuiLeme wants to merge 7 commits into
TypeCellOS:mainfrom
GuiLeme:text-align-select
Open

feat(react): Added select component for text align feature#2728
GuiLeme wants to merge 7 commits into
TypeCellOS:mainfrom
GuiLeme:text-align-select

Conversation

@GuiLeme
Copy link
Copy Markdown

@GuiLeme GuiLeme commented May 8, 2026

Summary

Added a selector component (Just like we have for the block-type selector) for the text align feature.

Rationale

I am thinking of 2 good reasons for this addition:

  • With this component, hopefully, customized toolbars can use it in order to fit in less space than with the 3 buttons (besides also adding the justify, so, a bit of improvement there as well).
  • Outside of that, we can take advantage of the user-experience with other major text editor softwares that already use this approach.

Besides those, there is one other reason.

  • Currently in BigBlueButton, we are integrating BlockNote as our new shared-notes, and we are currently making some changes to smooth the transition between Etherpad to BlockNote, one of those changes includes fix the toolbar on the top of the document, instead of appearing only when selecting the text. This ends up taking a lot of space so what we did was exactly this - compile these 3 alignment buttons into 1 selector (and we also removed the text of it just to save some space, but that's a minor detail of ours). See screenshot ahead
image

With that in mind, it would be much simpler if we had this component for us to use it out-of-the-box.

Changes

  • Added the TextAlignSelect.tsx component;
  • Added 1 new i18n key to be the label of the new selection tool.

Impact

As this will not be part of the default toolbar, I'd say no impact on the other components (it will just be used by developers to create their own customized toolbar).

Testing

I didn't create any automated tests, but if you guys wanted to have something like that, I can dig further.

As for manual testing, I just changed one of the playground examples and tried to break it some how.

In my case, I used examples/03-ui-components/02-formatting-toolbar-buttons/src/App.tsx I added it in place of the three align buttons:

-- a/examples/03-ui-components/02-formatting-toolbar-buttons/src/App.tsx
+++ b/examples/03-ui-components/02-formatting-toolbar-buttons/src/App.tsx
@@ -11,7 +11,7 @@ import {
   FormattingToolbar,
   FormattingToolbarController,
   NestBlockButton,
-  TextAlignButton,
+  TextAlignSelect,
   UnnestBlockButton,
   useCreateBlockNote,
 } from "@blocknote/react";
@@ -38,9 +38,7 @@ const CustomFormattingToolbar = () => (
     {/* Extra button to toggle code styles */}
     <BasicTextStyleButton key={"codeStyleButton"} basicTextStyle={"code"} />
 
-    <TextAlignButton textAlignment={"left"} key={"textAlignLeftButton"} />
-    <TextAlignButton textAlignment={"center"} key={"textAlignCenterButton"} />
-    <TextAlignButton textAlignment={"right"} key={"textAlignRightButton"} />
+    <TextAlignSelect key={"textAlignSelect"} />

After that I just had to build the lib and test it locally.

Screenshots/Video

image

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

I'll definitely dig into all that for the following commits. All of the above is covered now.

Additional Notes

I'd just like to mention that the Logic I use for the align text in the selector is exactly the same as for the button. So that's why I say it follows the project coding standards.

PR I did the same thing for BBB is here

Summary by CodeRabbit

  • New Features

    • Added a text alignment dropdown to the formatting toolbar (Left, Center, Right, Justify) and exported it for use.
  • Localization

    • Added human-readable labels for alignment options across many locales.
  • Documentation

    • Added docs demonstrating how to add and use the text alignment dropdown.
  • Tests

    • Added unit tests verifying rendering and reactive behavior of the alignment control.

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 8, 2026

@GuiLeme 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 May 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f89fc02d-d962-4923-83f7-21ed944c9c57

📥 Commits

Reviewing files that changed from the base of the PR and between b0a9bf5 and 5443eb0.

📒 Files selected for processing (1)
  • packages/react/src/components/FormattingToolbar/DefaultSelects/TextAlignSelect.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/react/src/components/FormattingToolbar/DefaultSelects/TextAlignSelect.tsx

📝 Walkthrough

Walkthrough

Adds a TextAlignSelect formatting toolbar dropdown, updates many locale files to include user-facing alignment label fields, re-exports the component from the React package, and adds documentation plus unit tests.

Changes

Text Alignment Toolbar Component

Layer / File(s) Summary
Locale Data
packages/core/src/i18n/locales/*
Adds label properties to formatting_toolbar.align_left, align_center, align_right, and align_justify across multiple locale files (labels added alongside existing tooltip).
Component — imports & helpers
packages/react/src/components/FormattingToolbar/DefaultSelects/TextAlignSelect.tsx
Adds table-related imports, alignment→icon mapping, supported alignment list, and getCellCursorOffset helper.
Component — table cursor restore
packages/react/src/components/FormattingToolbar/DefaultSelects/TextAlignSelect.tsx
Implements restoreTableCellCursor to recreate a cell-level TextSelection using TableMap and an offset.
Component — apply alignment to table block
packages/react/src/components/FormattingToolbar/DefaultSelects/TextAlignSelect.tsx
Implements applyTextAlignmentToTableBlock to set props.textAlignment on selected table cells, update the table block, restore the cell cursor, and focus the editor.
Component — state & handler
packages/react/src/components/FormattingToolbar/DefaultSelects/TextAlignSelect.tsx
Derives current alignment from selected blocks or table cells and implements setTextAlignment to update blocks or delegate to table helper.
Component — UI & render
packages/react/src/components/FormattingToolbar/DefaultSelects/TextAlignSelect.tsx
Builds localized Select items (label + icon + isSelected + handler) and renders the toolbar Select only when alignment state is available.
Public API Export
packages/react/src/index.ts
Re-exports TextAlignSelect from the React package entrypoint.
Documentation
docs/content/docs/react/components/formatting-toolbar.mdx
Adds “Adding a Text Align Select” subsection with usage example and table-cell behavior notes.
Tests
tests/src/unit/react/TextAlignSelect.test.tsx
Adds Vitest tests verifying non-editable behavior, editable rendering, and reactive label updates when block textAlignment changes.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant FormattingToolbar
  participant TextAlignSelect
  participant Editor
  participant Storage
  User->>FormattingToolbar: open select
  FormattingToolbar->>TextAlignSelect: render options (localized labels)
  User->>TextAlignSelect: choose alignment
  TextAlignSelect->>Editor: focus, read selection
  alt selection is table cells
    TextAlignSelect->>Editor: update table block content (table cells props)
  else normal blocks
    TextAlignSelect->>Editor: update selected blocks' props.textAlignment
  end
  Editor->>Storage: persist changes
  Editor-->>User: cursor restored, UI updated
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • nperez0111
  • matthewlipski

Poem

🐰 I hopped through labels, left to right,
A tiny select to make text tight.
Tables bowed as cells aligned,
Multilingual labels neatly signed.
Hop, click, format — the toolbar's bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a select component for text alignment in the React package.
Description check ✅ Passed The description covers all required sections: Summary, Rationale, Changes, Impact, Testing, Screenshots, and Checklist. It provides comprehensive context about motivation, implementation, and validation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/i18n/locales/pt.ts`:
- Around line 318-333: The 21 locale files are missing the required label
properties for the alignment entries causing a type mismatch against Dictionary
= typeof en; update each locale where align_left, align_center, align_right, and
align_justify exist but lack a label by adding a label field (matching the
intent of the tooltip or using the same short word used in en/pt) to each of
those keys (align_left, align_center, align_right, align_justify) so every
locale object satisfies the Dictionary shape; ensure the new label fields are
string values consistent with the locale language.

In
`@packages/react/src/components/FormattingToolbar/DefaultSelects/TextAlignSelect.tsx`:
- Around line 75-93: The table branch can crash and shows wrong alignment
because it unconditionally reads (firstBlock.content as
TableContent).rows[0].cells[0] and ignores the actual cell selection; update the
block in the TextAlignSelect logic to (1) check that firstBlock.content is a
TableContent and that rows exists and rows.length>0 and the targeted row has
cells and cells.length>0, returning undefined if bounds fail, and (2) use the
TableHandlesExtension.getCellSelection() result to determine the selected cell
index/coordinates (use the first selected cell if multiple) instead of
hardcoding [0][0], then pass that specific cell into mapTableCell to read
props.textAlignment; keep existing guards (selectedBlocks.length === 1,
blockHasType, etc.) and return the same shape ({ textAlignment, blocks:
[firstBlock]}) when valid.
- Around line 127-136: The current code shallow-copies table cells via
mapTableCell so the nested props object is still the original reference; then
newTable[row].cells[col].props.textAlignment = textAlignment mutates the live
block state before updateBlock is called. Fix by replacing the targeted cell
with an updated copy that deep-copies props (e.g., set newTable[row].cells[col]
= { ...newTable[row].cells[col], props: { ...newTable[row].cells[col].props,
textAlignment } }) so you never mutate the original props object; update the
logic around mapTableCell/newTable/cellSelection to assign the new cell object
rather than mutating props in-place.
🪄 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: fda086e2-5f8f-4b85-97a9-86f5878295a8

📥 Commits

Reviewing files that changed from the base of the PR and between dac995c and 36480f4.

📒 Files selected for processing (4)
  • packages/core/src/i18n/locales/en.ts
  • packages/core/src/i18n/locales/pt.ts
  • packages/react/src/components/FormattingToolbar/DefaultSelects/TextAlignSelect.tsx
  • packages/react/src/index.ts

Comment thread packages/core/src/i18n/locales/pt.ts
@gustavotrott
Copy link
Copy Markdown

I think the floating toolbar would also benefit from this new component!
It would reduce the toolbar size (make it feel less cluttered) and align it with an already established pattern.
Combining the 3 existing buttons into a single dropdown feels like a significant UX refinement.

Of course, that would be for a future PR. I'm just sharing feedback as someone who has been using BlockNote every day

@nperez0111
Copy link
Copy Markdown
Contributor

I'll let @matthewlipski lead here, but I'm generally for this change. I probably wouldn't even have "Left" in the toolbar just the icon perhaps with the existing tooltip

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/unit/react/TextAlignSelect.test.tsx (1)

21-87: ⚡ Quick win

Make cleanup unconditional in tests to avoid leaked editor/root state.
On failure paths, cleanup at Line 36-38 / Line 54-56 / Line 85-87 is skipped. Move cleanup to afterEach-managed refs or wrap each test body in try/finally.

♻️ Suggested pattern
 describe("TextAlignSelect", () => {
   let container: HTMLDivElement;
+  let root: ReturnType<typeof createRoot> | undefined;
+  let editor: BlockNoteEditor | undefined;

   beforeEach(() => {
     container = document.createElement("div");
     document.body.appendChild(container);
   });

   afterEach(() => {
+    root?.unmount();
+    editor?._tiptapEditor.destroy();
+    root = undefined;
+    editor = undefined;
     document.body.removeChild(container);
   });

   it("renders null when editor is not editable", () => {
-    const editor = BlockNoteEditor.create();
+    editor = BlockNoteEditor.create();
     editor.isEditable = false;
-    const root = createRoot(container);
+    root = createRoot(container);
@@
-    root.unmount();
-    editor._tiptapEditor.destroy();
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/src/unit/react/TextAlignSelect.test.tsx` around lines 21 - 87, Tests
currently skip cleanup on failure because root.unmount() and
editor._tiptapEditor.destroy() are called inline; make cleanup unconditional by
moving root and editor refs to outer-scope variables and adding an afterEach
that unmounts the root (if set) and destroys the editor._tiptapEditor (if set),
or wrap each test body in try/finally to call root.unmount() and
editor._tiptapEditor.destroy(); reference BlockNoteEditor.create(),
createRoot(container), the root variable, and editor._tiptapEditor.destroy()
when implementing the unconditional cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/i18n/locales/zh.ts`:
- Around line 379-382: The tooltip for align_justify is too generic ("文本对齐") and
should match the label "两端对齐"; update the align_justify object so its tooltip
property is the same descriptive text as label (e.g., change tooltip to "两端对齐")
to avoid ambiguity when displayed in the UI.

---

Nitpick comments:
In `@tests/src/unit/react/TextAlignSelect.test.tsx`:
- Around line 21-87: Tests currently skip cleanup on failure because
root.unmount() and editor._tiptapEditor.destroy() are called inline; make
cleanup unconditional by moving root and editor refs to outer-scope variables
and adding an afterEach that unmounts the root (if set) and destroys the
editor._tiptapEditor (if set), or wrap each test body in try/finally to call
root.unmount() and editor._tiptapEditor.destroy(); reference
BlockNoteEditor.create(), createRoot(container), the root variable, and
editor._tiptapEditor.destroy() when implementing the unconditional cleanup.
🪄 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: c3ec5dd1-040a-45d7-ac15-49343178b42d

📥 Commits

Reviewing files that changed from the base of the PR and between 36480f4 and b5a99ab.

📒 Files selected for processing (23)
  • docs/content/docs/react/components/formatting-toolbar.mdx
  • packages/core/src/i18n/locales/ar.ts
  • packages/core/src/i18n/locales/de.ts
  • packages/core/src/i18n/locales/es.ts
  • packages/core/src/i18n/locales/fa.ts
  • packages/core/src/i18n/locales/fr.ts
  • packages/core/src/i18n/locales/he.ts
  • packages/core/src/i18n/locales/hr.ts
  • packages/core/src/i18n/locales/is.ts
  • packages/core/src/i18n/locales/it.ts
  • packages/core/src/i18n/locales/ja.ts
  • packages/core/src/i18n/locales/ko.ts
  • packages/core/src/i18n/locales/nl.ts
  • packages/core/src/i18n/locales/no.ts
  • packages/core/src/i18n/locales/pl.ts
  • packages/core/src/i18n/locales/ru.ts
  • packages/core/src/i18n/locales/sk.ts
  • packages/core/src/i18n/locales/uk.ts
  • packages/core/src/i18n/locales/uz.ts
  • packages/core/src/i18n/locales/vi.ts
  • packages/core/src/i18n/locales/zh-tw.ts
  • packages/core/src/i18n/locales/zh.ts
  • tests/src/unit/react/TextAlignSelect.test.tsx
✅ Files skipped from review due to trivial changes (7)
  • packages/core/src/i18n/locales/vi.ts
  • packages/core/src/i18n/locales/he.ts
  • packages/core/src/i18n/locales/fr.ts
  • docs/content/docs/react/components/formatting-toolbar.mdx
  • packages/core/src/i18n/locales/es.ts
  • packages/core/src/i18n/locales/no.ts
  • packages/core/src/i18n/locales/fa.ts

Comment thread packages/core/src/i18n/locales/zh.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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/react/src/components/FormattingToolbar/DefaultSelects/TextAlignSelect.tsx`:
- Around line 140-148: The write loop in TextAlignSelect that iterates
cellSelection.cells can access undefined rows/cells on newTable and throw;
update the loop in the handler that builds newTable to mirror the read-path
guards (like rows[row]?.cells[col]) by checking that newTable[row] and
newTable[row].cells[col] exist before assigning or mutating; for example, skip
or continue when those are undefined (or create shallow copies as needed) so
assignments to newTable[row].cells[col].props.textAlignment are safe.
🪄 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: a6ee18f4-70fe-4e5a-a685-fd91ce9bb3f3

📥 Commits

Reviewing files that changed from the base of the PR and between b5a99ab and b1052b0.

📒 Files selected for processing (1)
  • packages/react/src/components/FormattingToolbar/DefaultSelects/TextAlignSelect.tsx

@GuiLeme
Copy link
Copy Markdown
Author

GuiLeme commented May 11, 2026

Hello, @nperez0111.

Thanks for the feedback. I agree with removing the text from the selected Item, but the problem is:

  • With the current toolbar Select component's API the text displayed inline in the toolbar is tightly bound to the selected item;

  • Besides, the Select component doesn't display a tooltip when hovering the selected Item;

So for that, the best approach (we thought of) would be to also change the Select component itself adding a flag like labelDisplayMode?: 'inline' | 'tooltip' (default: inline) and this would control how to display the selected item text (displayed directly in the select component - current behavior - or only show the icon with no text and having a tooltip for the selected item text).

If that's implemented, we could use that flag in the TextAlignSelect so that no text would appear in the component in the toolbar (as suggested by you), and we could also remove the new translations added and use the previous tooltip attribute of the respective dictionary key instead, since it is not going to be displayed inline anymore.

What do you say?

@vercel
Copy link
Copy Markdown

vercel Bot commented May 11, 2026

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

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview May 11, 2026 2:01pm

Request Review

@nperez0111
Copy link
Copy Markdown
Contributor

I understand that the Select API as it is now may not be sufficient to represent this. But we'd change it to support this

I'm proposing that the change here should be:

  • collapse these 3 options into 1 dropdown, where the button icon of the dropdown is the current state. (Otherwise you really aren't saving horizontal space)
  • The options can describe left / right / justify (either inline or as a tooltip, unsure on UX)

I'll let @matthewlipski lead here though

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.

4 participants