Skip to content

Fix accessible names, pressed state, and live regions in the chat composer #288

Description

@HMarzban

Problem

Screen readers get no accessible name for the chat composer's format buttons or its text area. The recording bar is also a polite live region whose timer text changes every second.

The design system already states the rule for icon-only buttons (.cursor/docs/design-system.md:458):

Off-system: on touch, Tooltip returns its children unwrapped, so a tooltip string is not an accessible name — icon-only controls need their own aria-label.

The pad toolbar follows it (apps/webapp/src/components/TipTap/toolbar/desktop/EditorToolbar.tsx:107-108). The composer format buttons do not.

On 14ab7f9c1. Composer paths are under apps/webapp/src/components/chatroom/components/MessageComposer/.

Format buttons. In this issue, "the nine format buttons" means all ten except Mention.

  • Nine of the ten format buttons pass only a tooltip, for example BoldButton (components/Toolbar/ToolbarButtons/BoldButton.tsx:16-20). The Mention button sets its own aria-label and aria-pressed (components/Actions/ActionButtons/MentionButton.tsx:82-87).
  • The composer ToolbarButton (components/ui/Button.tsx) passes tooltip to the shared Button, which wraps the button in Tooltip (apps/webapp/src/components/ui/Button.tsx:153-159). On a touch-only device, Tooltip returns the button with no tooltip (apps/webapp/src/components/ui/Tooltip.tsx:21, :102).
  • On desktop, the tooltip opens on hover, not on keyboard focus (apps/webapp/src/components/ui/Tooltip.tsx:78-86). Its useRole sets aria-describedby on the button only while the tooltip is open. That is a description, not an accessible name.
  • The button holds only an icon SVG with no title. So it has no accessible name on any device.
  • In the composer ToolbarButton, the active state is the class is-active btn-active, with no aria-pressed (components/ui/Button.tsx:39, :50-53).

Text area. The text area is the editable element that TipTap renders.

  • TipTap gives it role="textbox" (@tiptap/core 3.31.3, apps/webapp/node_modules/@tiptap/core/src/Editor.ts:598-601). The composer passes no editorProps.attributes, so the text area has no accessible name (hooks/useTiptapEditor.ts:207-235).
  • The placeholder is a data-placeholder attribute on the empty paragraph (hooks/useTiptapEditor.ts:154-163). CSS draws it with content: attr(data-placeholder) (apps/webapp/src/styles/_chat-editor.scss:41-47). It is not an accessible name. In comment mode it reads "Add a comment…".

Progress ring.

  • The upload progress ring is role="progressbar" with a value and no accessible name (components/Attachments/AttachmentStrip.tsx:19-40). A listener gets a percent, but not the file it belongs to.
  • The progress ring is inside the attachment strip's polite live region. That region reacts to added nodes and changed text (components/Attachments/AttachmentStrip.tsx:255-263).
  • Progress is a whole percent, written at most once per animation frame (apps/webapp/src/api/storage/upload.ts:65-68, apps/webapp/src/components/chatroom/utils/chatMediaUploadRunner.ts:134-154). So each step changes the text of the progress ring. ARIA makes the children of a progress bar presentational. So it is not known whether a screen reader speaks each step.

Recording bar.

  • While recording, the recording bar is role="status" with aria-live="polite" (components/VoiceRecordingBar.tsx:68-74). The elapsed time is plain text in that region (components/VoiceRecordingBar.tsx:80).
  • The recorder updates the time every 250 ms, and the m:ss text changes once a second (hooks/useVoiceRecorder.ts:19-24, :225). A recording can run for up to 5 minutes (hooks/useVoiceRecorder.ts:4).
  • On desktop, the mic button starts a locked recording (components/Actions/ActionButtons/ComposerPrimaryAction.tsx:55-58, :96). So a keyboard user reaches this state.

Steps to reproduce

  1. On desktop, sign in and open a channel as a member. Open the insert menu (the + button) and choose Text formatting.
  2. In the browser DevTools, select the Bold button and open the Accessibility pane. See no accessible name.
  3. Turn Bold on. See that the button looks active, and that the pane shows no pressed state.
  4. Select the text area: the .ProseMirror element inside #chatroom-editor. See the role textbox and no accessible name.
  5. Attach a large file. While it uploads, see the progress ring: role="progressbar", no accessible name, inside the aria-live attachment strip.
  6. Remove the file and clear the text, so the mic button shows. Press it, and allow the microphone.
  7. In the Elements panel, see the time in the role="status" recording bar change every second.

Acceptance criteria

  • Each of the nine format buttons has an accessible name that says its action. This holds on desktop and on a touch-only device.
  • The eight toggle buttons have aria-pressed. They are the nine format buttons except Hyperlink. The value always equals the button's visual active state.
  • On desktop, after the user turns Bold on at the caret, the Bold button has aria-pressed="true".
  • The Hyperlink button has an accessible name and no aria-pressed. It opens a dialog; it is not a toggle. It keeps its active style inside a link, and this issue accepts that the style has no ARIA state.
  • The Send button and the insert menu button have no aria-pressed. The Emoji, Mention, and mic buttons keep their aria-pressed as today.
  • The text area has an accessible name. In comment mode, the name says the text is a comment. Outside comment mode, it says the text is a message.
  • When comment mode starts or ends, the text area stays the same DOM element. The editor is not created again.
  • Each progress ring has an accessible name that includes the file name.
  • A progress step changes no text that a screen reader can read inside a live region. Text under aria-hidden="true" does not count. A file added next to an existing one still appears inside a live region, as today.
  • On a full-size attachment tile, when an upload finishes or fails, the live region still reacts to its new status text, as today. That status is a text change in the same element, so aria-relevant keeps text, or the status becomes a new node.
  • While recording, the elapsed time changes no text inside a live region. The change to "Recording locked" still appears inside a live region, as today.

Agent Brief

Category: bug
Summary: Give the format buttons, the text area, and the progress ring accessible names. Expose the toggle state, and stop the timer announcement each second.

Current behavior:
Nine format buttons have only a tooltip. The tooltip opens on hover, gives a description rather than a name, and does not render on touch. Their active state is a class only. The text area has the role textbox and no accessible name; its placeholder is CSS text. The progress ring is an unnamed progressbar inside a polite live region. The recording bar is a status region whose time text changes every second.

Desired behavior:

  • Each format button has an accessible name that says its action, on every device.
  • The eight toggle buttons expose aria-pressed from the same value that sets the active class. The Hyperlink button opens a dialog, so it has no pressed state. Buttons that are not toggles gain no aria-pressed.
  • The text area has an accessible name that follows comment mode, as the placeholder does. The editor is not created again to change it.
  • The progress ring has an accessible name with the file name. Progress steps are not announced. The attachment strip keeps its other announcements.
  • The recording bar does not announce the time each second, but still announces its state changes. Keep the time outside the role="status" element. A timer role inside that element is still inside its live region.

Key interfaces:

  • The composer ToolbarButton — computes the active state from editor.isActive(). The Send button and the insert menu button also use it.
  • BoldButton, ItalicButton, StrikethroughButton, CodeButton, HyperlinkButton, BulletListButton, OrderedListButton, BlockquoteButton, CodeBlockButton — the nine format buttons.
  • MentionButton and EmojiButton — the composer pattern: each passes its own aria-label and aria-pressed.
  • Tooltip — hover only; it describes and does not name.
  • useTiptapEditor() — creates the editor once per mount. Its Placeholder callback reads comment mode from commentMessageMemory at run time.
  • AttachmentStrip and UploadProgressRing.
  • VoiceRecordingBar and useVoiceRecorder()elapsedLabel is the time text.

Out of scope

Notes

Two pad toolbars show the pattern. The desktop one passes its tooltip text as aria-label (apps/webapp/src/components/TipTap/toolbar/desktop/EditorToolbar.tsx:107-108). The mobile one sets aria-label and aria-pressed on each format button (apps/webapp/src/components/TipTap/toolbar/mobile/FormatSelection.tsx:38-39).

The pressed value comes from the same state as the active class. So on a phone it stays stale until #269 lands. The Inline code button reports not pressed until #282 lands.

Coordinate with #271; both change the composer ToolbarButton. Coordinate with #270; it removes the tab stops from the text area wrappers in the composer Input. Put the accessible name on the text area itself, not on those wrappers. The composer already sets attributes on the text area after creation (apps/webapp/src/components/chatroom/components/MessageComposer/hooks/useComposerAttachmentLifecycle.ts:102-107). Coordinate with #289; it sets aria-controls and aria-activedescendant on the same text area. Set each attribute on its own, so neither fix removes the other's attributes. Coordinate with #265 and #279; both also change the recording bar.

The evidence is a code trace on 14ab7f9c1, confirmed by two verifiers. It was not run in a browser, on a device, or with a screen reader. Confirm the recording bar's speech with VoiceOver or NVDA, before and after the fix. The spoken result of the progress ring steps may differ by browser, because a progress bar's children are presentational.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ChatRelated to chat featuresUIbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions