Skip to content

Commit e6493ee

Browse files
fix: Comments emoji picker button issues (BLO-1199) (#2769)
* Memoized `actions` prop in consumers of `CommentEditor` * Added e2e test, minor changes to existing tests * refactor: extract actions into memo components with render prop pattern - Change CommentEditor actions prop from FC to render prop (function returning ReactNode) - Extract inline action components into separate memo-wrapped components - Remove unnecessary useCallback/useMemo wrappers around render props - React.memo on child components handles re-render optimization --------- Co-authored-by: Nick the Sick <computers@nickthesick.com>
1 parent 8b3118f commit e6493ee

5 files changed

Lines changed: 259 additions & 134 deletions

File tree

packages/react/src/components/Comments/Comment.tsx

Lines changed: 115 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"use client";
22

3-
import { mergeCSSClasses } from "@blocknote/core";
3+
import { Dictionary, mergeCSSClasses } from "@blocknote/core";
44
import { CommentsExtension } from "@blocknote/core/comments";
55
import type { CommentData, ThreadData } from "@blocknote/core/comments";
6-
import { MouseEvent, ReactNode, useCallback, useState } from "react";
6+
import { ThreadStore } from "@blocknote/core/comments";
7+
import { MouseEvent, ReactNode, memo, useCallback, useState } from "react";
78
import {
89
RiArrowGoBackFill,
910
RiCheckFill,
@@ -13,7 +14,7 @@ import {
1314
RiMoreFill,
1415
} from "react-icons/ri";
1516

16-
import { useComponentsContext } from "../../editor/ComponentsContext.js";
17+
import { Components, useComponentsContext } from "../../editor/ComponentsContext.js";
1718
import { useCreateBlockNote } from "../../hooks/useCreateBlockNote.js";
1819
import { useExtension } from "../../hooks/useExtension.js";
1920
import { useDictionary } from "../../i18n/dictionary.js";
@@ -23,6 +24,103 @@ import { ReactionBadge } from "./ReactionBadge.js";
2324
import { defaultCommentEditorSchema } from "./defaultCommentEditorSchema.js";
2425
import { useUser } from "./useUsers.js";
2526

27+
type CommentEditorActionsProps = {
28+
isFocused: boolean;
29+
isEmpty: boolean;
30+
comment: CommentData;
31+
isEditing: boolean;
32+
threadStore: ThreadStore;
33+
onReactionSelect: (emoji: string) => Promise<void>;
34+
onEditSubmit: (event: MouseEvent) => Promise<void>;
35+
onEditCancel: () => void;
36+
onEmojiPickerOpenChange: (open: boolean) => void;
37+
Components: Components;
38+
dict: Dictionary;
39+
};
40+
41+
const CommentEditorActionsComponent = memo(
42+
({
43+
isEmpty: _isEmpty,
44+
comment,
45+
isEditing,
46+
threadStore,
47+
onReactionSelect,
48+
onEditSubmit,
49+
onEditCancel,
50+
onEmojiPickerOpenChange,
51+
Components,
52+
dict,
53+
}: CommentEditorActionsProps) => {
54+
const canAddReaction = threadStore.auth.canAddReaction(comment);
55+
56+
return (
57+
<>
58+
{comment.reactions.length > 0 && !isEditing && (
59+
<Components.Generic.Badge.Group
60+
className={mergeCSSClasses(
61+
"bn-badge-group",
62+
"bn-comment-reactions",
63+
)}
64+
>
65+
{comment.reactions.map((reaction) => (
66+
<ReactionBadge
67+
key={reaction.emoji}
68+
comment={comment}
69+
emoji={reaction.emoji}
70+
onReactionSelect={onReactionSelect}
71+
/>
72+
))}
73+
{canAddReaction && (
74+
<EmojiPicker
75+
onEmojiSelect={(emoji: { native: string }) =>
76+
onReactionSelect(emoji.native)
77+
}
78+
onOpenChange={onEmojiPickerOpenChange}
79+
>
80+
<Components.Generic.Badge.Root
81+
className={mergeCSSClasses(
82+
"bn-badge",
83+
"bn-comment-add-reaction",
84+
)}
85+
text={"+"}
86+
icon={<RiEmotionLine size={16} />}
87+
mainTooltip={dict.comments.actions.add_reaction}
88+
/>
89+
</EmojiPicker>
90+
)}
91+
</Components.Generic.Badge.Group>
92+
)}
93+
{isEditing && (
94+
<Components.Generic.Toolbar.Root
95+
variant="action-toolbar"
96+
className={mergeCSSClasses(
97+
"bn-action-toolbar",
98+
"bn-comment-actions",
99+
)}
100+
>
101+
<Components.Generic.Toolbar.Button
102+
mainTooltip={dict.comments.save_button_text}
103+
variant="compact"
104+
onClick={onEditSubmit}
105+
isDisabled={_isEmpty}
106+
>
107+
{dict.comments.save_button_text}
108+
</Components.Generic.Toolbar.Button>
109+
<Components.Generic.Toolbar.Button
110+
className={"bn-button"}
111+
mainTooltip={dict.comments.cancel_button_text}
112+
variant="compact"
113+
onClick={onEditCancel}
114+
>
115+
{dict.comments.cancel_button_text}
116+
</Components.Generic.Toolbar.Button>
117+
</Components.Generic.Toolbar.Root>
118+
)}
119+
</>
120+
);
121+
},
122+
);
123+
26124
export type CommentProps = {
27125
comment: CommentData;
28126
thread: ThreadData;
@@ -249,70 +347,20 @@ export const Comment = ({
249347
editable={isEditing}
250348
actions={
251349
comment.reactions.length > 0 || isEditing
252-
? ({ isEmpty }) => (
253-
<>
254-
{comment.reactions.length > 0 && !isEditing && (
255-
<Components.Generic.Badge.Group
256-
className={mergeCSSClasses(
257-
"bn-badge-group",
258-
"bn-comment-reactions",
259-
)}
260-
>
261-
{comment.reactions.map((reaction) => (
262-
<ReactionBadge
263-
key={reaction.emoji}
264-
comment={comment}
265-
emoji={reaction.emoji}
266-
onReactionSelect={onReactionSelect}
267-
/>
268-
))}
269-
{canAddReaction && (
270-
<EmojiPicker
271-
onEmojiSelect={(emoji: { native: string }) =>
272-
onReactionSelect(emoji.native)
273-
}
274-
onOpenChange={setEmojiPickerOpen}
275-
>
276-
<Components.Generic.Badge.Root
277-
className={mergeCSSClasses(
278-
"bn-badge",
279-
"bn-comment-add-reaction",
280-
)}
281-
text={"+"}
282-
icon={<RiEmotionLine size={16} />}
283-
mainTooltip={dict.comments.actions.add_reaction}
284-
/>
285-
</EmojiPicker>
286-
)}
287-
</Components.Generic.Badge.Group>
288-
)}
289-
{isEditing && (
290-
<Components.Generic.Toolbar.Root
291-
variant="action-toolbar"
292-
className={mergeCSSClasses(
293-
"bn-action-toolbar",
294-
"bn-comment-actions",
295-
)}
296-
>
297-
<Components.Generic.Toolbar.Button
298-
mainTooltip={dict.comments.save_button_text}
299-
variant="compact"
300-
onClick={onEditSubmit}
301-
isDisabled={isEmpty}
302-
>
303-
{dict.comments.save_button_text}
304-
</Components.Generic.Toolbar.Button>
305-
<Components.Generic.Toolbar.Button
306-
className={"bn-button"}
307-
mainTooltip={dict.comments.cancel_button_text}
308-
variant="compact"
309-
onClick={onEditCancel}
310-
>
311-
{dict.comments.cancel_button_text}
312-
</Components.Generic.Toolbar.Button>
313-
</Components.Generic.Toolbar.Root>
314-
)}
315-
</>
350+
? ({ isFocused, isEmpty }) => (
351+
<CommentEditorActionsComponent
352+
isFocused={isFocused}
353+
isEmpty={isEmpty}
354+
comment={comment}
355+
isEditing={isEditing}
356+
threadStore={threadStore}
357+
onReactionSelect={onReactionSelect}
358+
onEditSubmit={onEditSubmit}
359+
onEditCancel={onEditCancel}
360+
onEmojiPickerOpenChange={setEmojiPickerOpen}
361+
Components={Components}
362+
dict={dict}
363+
/>
316364
)
317365
: undefined
318366
}

packages/react/src/components/Comments/CommentEditor.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BlockNoteEditor } from "@blocknote/core";
2-
import { FC, useCallback, useEffect, useState } from "react";
2+
import { ReactNode, useCallback, useEffect, useState } from "react";
33
import { useComponentsContext } from "../../editor/ComponentsContext.js";
44
import { useEditorState } from "../../hooks/useEditorState.js";
55

@@ -16,10 +16,7 @@ import { useEditorState } from "../../hooks/useEditorState.js";
1616
export const CommentEditor = (props: {
1717
autoFocus?: boolean;
1818
editable: boolean;
19-
actions?: FC<{
20-
isFocused: boolean;
21-
isEmpty: boolean;
22-
}>;
19+
actions?: (args: { isFocused: boolean; isEmpty: boolean }) => ReactNode;
2320
editor: BlockNoteEditor<any, any, any>;
2421
}) => {
2522
const [isFocused, setIsFocused] = useState(false);
@@ -58,7 +55,7 @@ export const CommentEditor = (props: {
5855
/>
5956
{props.actions && (
6057
<div className={"bn-comment-actions-wrapper"}>
61-
<props.actions isFocused={isFocused} isEmpty={isEmpty} />
58+
{props.actions({ isFocused, isEmpty })}
6259
</div>
6360
)}
6461
</>

packages/react/src/components/Comments/FloatingComposer.tsx

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import {
33
DefaultBlockSchema,
44
DefaultInlineContentSchema,
55
DefaultStyleSchema,
6+
Dictionary,
67
InlineContentSchema,
78
mergeCSSClasses,
89
StyleSchema,
910
} from "@blocknote/core";
1011
import { CommentsExtension } from "@blocknote/core/comments";
12+
import { memo, useCallback } from "react";
1113

12-
import { useComponentsContext } from "../../editor/ComponentsContext.js";
14+
import { Components, useComponentsContext } from "../../editor/ComponentsContext.js";
1315
import { useCreateBlockNote } from "../../hooks/useCreateBlockNote.js";
1416
import { useExtension } from "../../hooks/useExtension.js";
1517
import { useDictionary } from "../../i18n/dictionary.js";
@@ -18,6 +20,33 @@ import { defaultCommentEditorSchema } from "./defaultCommentEditorSchema.js";
1820
import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
1921
import { TextSelection } from "@tiptap/pm/state";
2022

23+
type FloatingComposerActionsProps = {
24+
isFocused: boolean;
25+
isEmpty: boolean;
26+
onSave: () => Promise<void>;
27+
Components: Components;
28+
dict: Dictionary;
29+
};
30+
31+
const FloatingComposerActionsComponent = memo(
32+
({ isEmpty, onSave, Components, dict }: FloatingComposerActionsProps) => (
33+
<Components.Generic.Toolbar.Root
34+
className={mergeCSSClasses("bn-action-toolbar", "bn-comment-actions")}
35+
variant="action-toolbar"
36+
>
37+
<Components.Generic.Toolbar.Button
38+
className={"bn-button"}
39+
mainTooltip={dict.comments.save_button_text}
40+
variant="compact"
41+
isDisabled={isEmpty}
42+
onClick={onSave}
43+
>
44+
{dict.comments.save_button_text}
45+
</Components.Generic.Toolbar.Button>
46+
</Components.Generic.Toolbar.Root>
47+
),
48+
);
49+
2150
/**
2251
* The FloatingComposer component displays a comment editor "floating" card.
2352
*
@@ -46,44 +75,34 @@ export function FloatingComposer<
4675
schema: comments.commentEditorSchema || defaultCommentEditorSchema,
4776
});
4877

78+
const onSave = useCallback(async () => {
79+
// (later) For REST API, we should implement a loading state and error state
80+
await comments.createThread({
81+
initialComment: {
82+
body: newCommentEditor.document,
83+
},
84+
});
85+
comments.stopPendingComment();
86+
editor.transact((tr) => {
87+
tr.setSelection(TextSelection.create(tr.doc, tr.selection.to));
88+
});
89+
editor.focus();
90+
}, [comments, newCommentEditor, editor]);
91+
4992
return (
5093
<Components.Comments.Card className={"bn-thread"}>
5194
<CommentEditor
5295
autoFocus={true}
5396
editable={true}
5497
editor={newCommentEditor}
55-
actions={({ isEmpty }) => (
56-
<Components.Generic.Toolbar.Root
57-
className={mergeCSSClasses(
58-
"bn-action-toolbar",
59-
"bn-comment-actions",
60-
)}
61-
variant="action-toolbar"
62-
>
63-
<Components.Generic.Toolbar.Button
64-
className={"bn-button"}
65-
mainTooltip={dict.comments.save_button_text}
66-
variant="compact"
67-
isDisabled={isEmpty}
68-
onClick={async () => {
69-
// (later) For REST API, we should implement a loading state and error state
70-
await comments.createThread({
71-
initialComment: {
72-
body: newCommentEditor.document,
73-
},
74-
});
75-
comments.stopPendingComment();
76-
editor.transact((tr) => {
77-
tr.setSelection(
78-
TextSelection.create(tr.doc, tr.selection.to),
79-
);
80-
});
81-
editor.focus();
82-
}}
83-
>
84-
{dict.comments.save_button_text}
85-
</Components.Generic.Toolbar.Button>
86-
</Components.Generic.Toolbar.Root>
98+
actions={({ isFocused, isEmpty }) => (
99+
<FloatingComposerActionsComponent
100+
isFocused={isFocused}
101+
isEmpty={isEmpty}
102+
onSave={onSave}
103+
Components={Components}
104+
dict={dict}
105+
/>
87106
)}
88107
/>
89108
</Components.Comments.Card>

0 commit comments

Comments
 (0)