From 17d6954481722769776afd9dfee0c87a050cb2bf Mon Sep 17 00:00:00 2001 From: "Khai Shern, Toh" Date: Tue, 15 Sep 2026 07:03:59 +0800 Subject: [PATCH 1/2] fix(web): label composer suggestion lists by trigger A single "Composer suggestions" name tells a screen reader user that a list opened but not which one. Naming it by trigger ("Files and folders", "Commands", "Skills", "Pull requests") says what the options are before any row is read. Co-Authored-By: Claude Opus 5 (1M context) --- apps/web/src/components/chat/ComposerCommandMenu.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/ComposerCommandMenu.tsx b/apps/web/src/components/chat/ComposerCommandMenu.tsx index 6df5ca6806d2..b127e5148862 100644 --- a/apps/web/src/components/chat/ComposerCommandMenu.tsx +++ b/apps/web/src/components/chat/ComposerCommandMenu.tsx @@ -107,7 +107,7 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: { {props.items.length > 0 ? ( @@ -227,6 +227,13 @@ export function composerSuggestionOptionId(listId: string, itemId: string): stri return `${listId}-${encodeURIComponent(itemId)}`; } +const LISTBOX_LABEL_BY_TRIGGER: Record = { + path: "Files and folders", + "pull-request": "Pull requests", + "slash-command": "Commands", + skill: "Skills", +}; + const SKILL_SOURCE_ICON_BY_KIND: Record = { app: BlocksIcon, repo: FolderIcon, From fee7a7d666d22680976472405be133436cc51b4a Mon Sep 17 00:00:00 2001 From: "Khai Shern, Toh" Date: Tue, 15 Sep 2026 07:08:26 +0800 Subject: [PATCH 2/2] fix(web): keep suggestion ARIA off editors without menus ComposerPromptEditor is also the Settings font preview, which has no trigger menus. Hardcoding the "Message" label, aria-autocomplete and aria-haspopup on the editor made that preview announce as a chat field with autocomplete. The chat composer now opts in by passing the label and its listbox id; the font preview passes neither and reads as a plain multiline field. aria-controls follows the active option rather than list visibility, which is the same moment in practice but keeps the reference from ever pointing at a listbox with no highlighted row. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/components/ComposerPromptEditor.tsx | 19 +++++++++++++++---- apps/web/src/components/chat/ChatComposer.tsx | 5 ++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/ComposerPromptEditor.tsx b/apps/web/src/components/ComposerPromptEditor.tsx index 08d7576636ec..4bb0a1de9c97 100644 --- a/apps/web/src/components/ComposerPromptEditor.tsx +++ b/apps/web/src/components/ComposerPromptEditor.tsx @@ -872,6 +872,14 @@ interface ComposerPromptEditorProps { skills: ReadonlyArray; disabled: boolean; placeholder: string; + ariaLabel?: string | undefined; + /** + * Suggestion-menu wiring for screen readers. `suggestionListId` marks an + * editor that can open a suggestion list at all, so editors without menus + * (the Settings font preview) are not announced as having autocomplete. + * `activeSuggestionId` is the highlighted option while that list is + * rendered; it drives aria-activedescendant and aria-controls. + */ suggestionListId?: string | undefined; activeSuggestionId?: string | undefined; containerClassName?: string; @@ -1635,6 +1643,7 @@ function ComposerPromptEditorInner({ skills, disabled, placeholder, + ariaLabel, suggestionListId, activeSuggestionId, containerClassName, @@ -1995,12 +2004,12 @@ function ComposerPromptEditorInner({ className, )} data-testid="composer-editor" - ariaLabel="Message" + ariaLabel={ariaLabel} ariaMultiline={true} - ariaAutoComplete="list" - ariaControls={suggestionListId} + ariaAutoComplete={suggestionListId ? "list" : undefined} + ariaControls={activeSuggestionId ? suggestionListId : undefined} ariaActiveDescendant={activeSuggestionId} - aria-haspopup="listbox" + aria-haspopup={suggestionListId ? "listbox" : undefined} aria-placeholder={placeholder} placeholder={} onKeyDown={(event) => { @@ -2088,6 +2097,7 @@ export function ComposerPromptEditor({ skills, disabled, placeholder, + ariaLabel, suggestionListId, activeSuggestionId, containerClassName, @@ -2137,6 +2147,7 @@ export function ComposerPromptEditor({ skills={skills} disabled={disabled} placeholder={placeholder} + ariaLabel={ariaLabel} suggestionListId={suggestionListId} activeSuggestionId={activeSuggestionId} {...(containerClassName ? { containerClassName } : {})} diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 41ca47ba6955..ca5de4f7e400 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -6698,9 +6698,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ) : null}