diff --git a/packages/react/src/components/Comments/Comment.tsx b/packages/react/src/components/Comments/Comment.tsx
index e1dd66871a..b6bd27625d 100644
--- a/packages/react/src/components/Comments/Comment.tsx
+++ b/packages/react/src/components/Comments/Comment.tsx
@@ -130,6 +130,88 @@ export const Comment = ({
const user = useUser(comment.userId);
+ const CommentEditorActions = useCallback(
+ ({ isEmpty }: { isFocused: boolean; isEmpty: boolean }) => {
+ const canAddReaction = threadStore.auth.canAddReaction(comment);
+
+ return (
+ <>
+ {comment.reactions.length > 0 && !isEditing && (
+
+ {comment.reactions.map((reaction) => (
+
+ ))}
+ {canAddReaction && (
+
+ onReactionSelect(emoji.native)
+ }
+ onOpenChange={setEmojiPickerOpen}
+ >
+ }
+ mainTooltip={dict.comments.actions.add_reaction}
+ />
+
+ )}
+
+ )}
+ {isEditing && (
+
+
+ {dict.comments.save_button_text}
+
+
+ {dict.comments.cancel_button_text}
+
+
+ )}
+ >
+ );
+ },
+ [
+ comment,
+ isEditing,
+ threadStore,
+ onReactionSelect,
+ onEditSubmit,
+ onEditCancel,
+ Components,
+ dict,
+ ],
+ );
+
if (!comment.body) {
return null;
}
@@ -249,71 +331,7 @@ export const Comment = ({
editable={isEditing}
actions={
comment.reactions.length > 0 || isEditing
- ? ({ isEmpty }) => (
- <>
- {comment.reactions.length > 0 && !isEditing && (
-
- {comment.reactions.map((reaction) => (
-
- ))}
- {canAddReaction && (
-
- onReactionSelect(emoji.native)
- }
- onOpenChange={setEmojiPickerOpen}
- >
- }
- mainTooltip={dict.comments.actions.add_reaction}
- />
-
- )}
-
- )}
- {isEditing && (
-
-
- {dict.comments.save_button_text}
-
-
- {dict.comments.cancel_button_text}
-
-
- )}
- >
- )
+ ? CommentEditorActions
: undefined
}
/>
diff --git a/packages/react/src/components/Comments/FloatingComposer.tsx b/packages/react/src/components/Comments/FloatingComposer.tsx
index 023a8eccf6..31be57b1ec 100644
--- a/packages/react/src/components/Comments/FloatingComposer.tsx
+++ b/packages/react/src/components/Comments/FloatingComposer.tsx
@@ -8,6 +8,7 @@ import {
StyleSchema,
} from "@blocknote/core";
import { CommentsExtension } from "@blocknote/core/comments";
+import { useCallback } from "react";
import { useComponentsContext } from "../../editor/ComponentsContext.js";
import { useCreateBlockNote } from "../../hooks/useCreateBlockNote.js";
@@ -46,45 +47,45 @@ export function FloatingComposer<
schema: comments.commentEditorSchema || defaultCommentEditorSchema,
});
+ const Actions = useCallback(
+ ({ isEmpty }: { isFocused: boolean; isEmpty: boolean }) => (
+
+ {
+ // (later) For REST API, we should implement a loading state and error state
+ await comments.createThread({
+ initialComment: {
+ body: newCommentEditor.document,
+ },
+ });
+ comments.stopPendingComment();
+ editor.transact((tr) => {
+ tr.setSelection(TextSelection.create(tr.doc, tr.selection.to));
+ });
+ editor.focus();
+ }}
+ >
+ {dict.comments.save_button_text}
+
+
+ ),
+ [Components, dict, comments, newCommentEditor, editor],
+ );
+
return (
(
-
- {
- // (later) For REST API, we should implement a loading state and error state
- await comments.createThread({
- initialComment: {
- body: newCommentEditor.document,
- },
- });
- comments.stopPendingComment();
- editor.transact((tr) => {
- tr.setSelection(
- TextSelection.create(tr.doc, tr.selection.to),
- );
- });
- editor.focus();
- }}
- >
- {dict.comments.save_button_text}
-
-
- )}
+ actions={Actions}
/>
);
diff --git a/packages/react/src/components/Comments/Thread.tsx b/packages/react/src/components/Comments/Thread.tsx
index a1da1484e6..8f5cbb0979 100644
--- a/packages/react/src/components/Comments/Thread.tsx
+++ b/packages/react/src/components/Comments/Thread.tsx
@@ -92,6 +92,31 @@ export const Thread = ({
newCommentEditor.removeBlocks(newCommentEditor.document);
}, [comments, newCommentEditor, thread.id]);
+ const ReplyActions = useCallback(
+ ({ isEmpty }: { isFocused: boolean; isEmpty: boolean }) => {
+ if (isEmpty) {
+ return null;
+ }
+
+ return (
+
+
+ {dict.comments.save_button_text}
+
+
+ );
+ },
+ [Components, dict, onNewCommentSave],
+ );
+
return (
{
- if (isEmpty) {
- return null;
- }
-
- return (
-
-
- {dict.comments.save_button_text}
-
-
- );
- }}
+ actions={ReplyActions}
/>
)}
diff --git a/tests/src/end-to-end/comments/comments.test.ts b/tests/src/end-to-end/comments/comments.test.ts
index 198a8e1ced..9ae1c8284f 100644
--- a/tests/src/end-to-end/comments/comments.test.ts
+++ b/tests/src/end-to-end/comments/comments.test.ts
@@ -3,11 +3,54 @@ import { test } from "../../setup/setupScript.js";
import { COMMENTS_URL, LINK_BUTTON_SELECTOR } from "../../utils/const.js";
import { focusOnEditor } from "../../utils/editor.js";
+const EMOJI_BUTTON_SELECTOR = "em-emoji-picker button[aria-posinset]";
+
test.beforeEach(async ({ page }) => {
await page.goto(COMMENTS_URL);
});
test.describe("Check Comments functionality", () => {
+ test("Should be able to add reactions", async ({ page }) => {
+ await focusOnEditor(page);
+
+ await page.keyboard.type("hello");
+ await page.locator("text=hello").dblclick();
+
+ await page.click('[data-test="addcomment"]');
+ await page.waitForSelector(".bn-thread");
+
+ await page.keyboard.type("test comment");
+ await page.click('button[data-test="save"]');
+
+ // Wait for comment composer to close.
+ await expect(page.locator(".bn-thread")).toHaveCount(0);
+
+ await page.locator("span.bn-thread-mark").first().click();
+ await expect(page.locator(".bn-thread-comment")).toBeVisible();
+
+ // Hover comment to reveal action toolbar.
+ await page.locator(".bn-thread-comment").first().hover();
+ await expect(page.locator('[data-test="addreaction"]')).toBeVisible();
+
+ // Add a reaction via the action toolbar's add-reaction button.
+ await page.click('[data-test="addreaction"]');
+ await expect(page.locator(EMOJI_BUTTON_SELECTOR).first()).toBeVisible();
+ await page.locator(EMOJI_BUTTON_SELECTOR).first().click();
+ await expect(page.locator("em-emoji-picker")).toHaveCount(0);
+ await expect(page.locator(".bn-comment-reaction")).toHaveCount(1);
+
+ // Add a second reaction via the add-reaction badge.
+ await page.locator(".bn-thread-comment").first().hover();
+ await page.click(".bn-comment-add-reaction");
+ await expect(page.locator(EMOJI_BUTTON_SELECTOR).first()).toBeVisible();
+
+ // Pick a different emoji so it's added as a new reaction rather than
+ // toggling the first one off.
+ await page.locator(EMOJI_BUTTON_SELECTOR).nth(5).click();
+ await expect(page.locator("em-emoji-picker")).toHaveCount(0);
+ await expect(page.locator(".bn-comment-reaction")).toHaveCount(2);
+ });
+
test("Should preserve existing comments when adding a link", async ({
page,
}) => {
@@ -30,7 +73,7 @@ test.describe("Check Comments functionality", () => {
await page.keyboard.type("https://example.com");
await page.keyboard.press("Enter");
- await expect(await page.locator("span.bn-thread-mark")).toBeVisible();
+ await expect(page.locator("span.bn-thread-mark")).toBeVisible();
});
test("Should select thread on first click and open link on second click", async ({
@@ -64,6 +107,7 @@ test.describe("Check Comments functionality", () => {
await page.keyboard.press("ArrowDown");
await page.waitForTimeout(500);
await expect(page.locator(".bn-thread-mark-selected")).toHaveCount(0);
+ await expect(page.locator(".bn-formatting-toolbar")).toBeHidden();
const link = page.locator('a[data-inline-content-type="link"]').first();