From e9ae5d5e236a9b01115582470859d319625ac6b6 Mon Sep 17 00:00:00 2001 From: adarshsm <24850536+adarshsm@users.noreply.github.com> Date: Tue, 29 Sep 2026 11:42:01 +0530 Subject: [PATCH] fix(react): give comment action buttons an accessible name The add reaction, resolve, re-open and more actions buttons in a comment contain only an icon and passed their text as mainTooltip but not as label, which is what the Mantine, Ariakit and ShadCN toolbar buttons use for aria-label. Screen readers announced each one as a bare "button". Part of #2824 --- .../src/components/Comments/Comment.test.tsx | 121 ++++++++++++++++++ .../react/src/components/Comments/Comment.tsx | 4 + 2 files changed, 125 insertions(+) create mode 100644 packages/react/src/components/Comments/Comment.test.tsx diff --git a/packages/react/src/components/Comments/Comment.test.tsx b/packages/react/src/components/Comments/Comment.test.tsx new file mode 100644 index 0000000000..c82205eaca --- /dev/null +++ b/packages/react/src/components/Comments/Comment.test.tsx @@ -0,0 +1,121 @@ +import { en } from "@blocknote/core/locales"; +import type { CommentData, ThreadData } from "@blocknote/core/comments"; +import type { ReactNode } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it, vi } from "vite-plus/test"; + +import { Comment } from "./Comment.js"; + +const { passThrough, components, allow } = vi.hoisted(() => { + const passThrough = ({ children }: { children?: ReactNode }) => ( + <>{children} + ); + + // Stand-ins for the UI library components. Like the real Mantine, Ariakit + // and ShadCN toolbar buttons, this one uses `label` as its `aria-label`. + const components = { + Comments: { + Comment: ({ actions }: { actions?: ReactNode }) =>
{actions}
, + }, + Generic: { + Toolbar: { + Root: passThrough, + Button: ({ + label, + children, + }: { + label?: string; + children?: ReactNode; + }) => , + }, + Menu: { + Root: passThrough, + Trigger: passThrough, + Dropdown: () => null, + Item: () => null, + }, + }, + }; + + return { passThrough, components, allow: () => true }; +}); + +vi.mock("../../editor/ComponentsContext.js", () => ({ + useComponentsContext: () => components, +})); +vi.mock("../../editor/PortalElementOverride.js", () => ({ + usePortalElement: () => undefined, +})); +vi.mock("../../i18n/dictionary.js", () => ({ + useDictionary: () => en, +})); +vi.mock("../../hooks/useExtension.js", () => ({ + useExtension: () => ({ + threadStore: { + auth: { + canAddReaction: allow, + canDeleteComment: allow, + canUpdateComment: allow, + canResolveThread: allow, + canUnresolveThread: allow, + }, + }, + }), +})); +vi.mock("../../hooks/useCreateBlockNote.js", () => ({ + useCreateBlockNote: () => ({ getExtension: () => ({ submit: vi.fn() }) }), +})); +vi.mock("./useCommentUsers.js", () => ({ useCommentUser: () => undefined })); +vi.mock("./CommentEditor.js", () => ({ CommentEditor: () => null })); +vi.mock("./EmojiPicker.js", () => ({ EmojiPicker: passThrough })); + +const date = new Date(2026, 0, 1); +const comment = { + type: "comment", + id: "comment-1", + userId: "user-1", + createdAt: date, + updatedAt: date, + reactions: [], + metadata: {}, + body: [], +} as unknown as CommentData; + +function thread(resolved: boolean) { + return { + type: "thread", + id: "thread-1", + createdAt: date, + updatedAt: date, + comments: [comment], + resolved, + metadata: {}, + } as unknown as ThreadData; +} + +describe("Comment actions", () => { + it.each([ + [false, en.comments.actions.resolve], + [true, en.comments.actions.reopen], + ])( + "icon-only buttons have an accessible name (resolved: %s)", + (resolved, resolveOrReopen) => { + const html = renderToStaticMarkup( + , + ); + + const buttons = html.match(/]*>/g) ?? []; + expect( + buttons.map((button) => button.match(/aria-label="([^"]*)"/)?.[1]), + ).toEqual([ + en.comments.actions.add_reaction, + resolveOrReopen, + en.comments.actions.more_actions, + ]); + }, + ); +}); diff --git a/packages/react/src/components/Comments/Comment.tsx b/packages/react/src/components/Comments/Comment.tsx index 527cb739b4..1e0f0bd77c 100644 --- a/packages/react/src/components/Comments/Comment.tsx +++ b/packages/react/src/components/Comments/Comment.tsx @@ -270,6 +270,7 @@ export const Comment = ({ > @@ -281,6 +282,7 @@ export const Comment = ({ (thread.resolved ? (