You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(chat): announce replies once and make the history keyboard-scrollable - #7295
ChatMessages is no longer an active live region (aria-live="off" on the role="log" root). A new visually hidden role="status" region (.ais-ChatMessages-status) announces once when a reply starts, once ("Reply received.") when it completes, and on error.
The scrollable history is now a focusable, named region (tabindex="0", role="region", label "Conversation"), with a :focus-visible outline in instantsearch.css.
New translations: conversationLabel, respondingStatusText, responseCompleteStatusText (receives the reply, so apps can announce its text), errorStatusText. New class name key: status.
Result
Screen readers read a reply once instead of chunk by chunk, and keyboard users can scroll text-only answers. Chat unit and common-widget tests pass; lint, format and type-check are clean. Not yet checked with VoiceOver or NVDA.
Both changes are inspired by accessibility fixes in a customer shopping assistant, which worked around these gaps on its own.
Not included, to follow separately: dialog semantics and focus management for the overlay layout, the heading outline, and product list semantics.
…lable
The message log announced every streamed chunk, and VoiceOver ignores its
aria-busy. Silence it and announce once when a reply starts, once with the
full reply when it completes, and on error through a status region.
Make the scrollable history a focusable, named region so keyboard users
can scroll text-only answers.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- announce only a trailing assistant message, so a stopped request does not
repeat the previous answer
- strip only unambiguous Markdown (and none in plain-text mode) so literal
characters like C# and order_id survive
- move aria-busy to the history so the status region is not in a busy subtree
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Initializing previous from the current props skips the first announcement when the component mounts with submitted, streaming, or error. A subsequent submitted → streaming transition also stays silent because previous.isProcessing is already true. Initialize the tracking state as idle so processing and error states are announced after mount, while existing history stays silent when mounting with ready.
The vanilla JavaScript widget does not expose these four translations: packages/instantsearch.js/src/widgets/chat/chat.tsx:782–788 omits them from messagesTranslations, and ChatTemplates.messages at lines 1059–1076 has no corresponding options. Localized JS chats therefore get English announcements and an English conversation label with no supported way to customize them. Add the public template options, forward them to messagesTranslations, and cover the mapping with translation tests.
aria-live="off" does not guarantee silence while focus is inside the region. The newly focusable history is inside this log, so focusing it to scroll can still expose streamed updates to screen readers, followed by the full status announcement. Remove the log's live-region semantics and keep the status element as the only live region. Update the corresponding markup assertions and snapshots.
Announce a plain 'Reply received.' by default and pass the reply to
responseCompleteStatusText, so consumers that know how a reply is rendered
can announce its text.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Initializing previous from the current props makes the first effect return when mounted with submitted, streaming, or error. An initially submitted request also stays silent when it switches to streaming, since previous.isProcessing is already true. Initialize the transition baseline to ready/non-processing so an already-active request or error gets an announcement, while restored ready history remains silent.
Clear stale completion announcements when chat history is emptied
Clearing a completed conversation empties messages without changing the ready status, so this early return preserves the old announcement. When responseCompleteStatusText returns reply text, the cleared answer remains in the DOM and accessible to screen readers. Clear the announcement when ready history becomes empty, before checking whether the status changed.
Expose all chat translation options in the vanilla widget
The vanilla JavaScript Chat widget cannot customize these four translations. Its explicit messagesTranslations mapping in packages/instantsearch.js/src/widgets/chat/chat.tsx:782–788 and ChatTemplates.messages type at lines 1059–1076 omit them. Localized JavaScript chats therefore use English labels and announcements, and cannot use the completion callback to announce reply text. Expose and forward all four options through the widget's templates API, and cover them in the common translation tests.
Start the announcement baseline as idle so a request already in flight
when the messages mount is announced, and clear the announced reply when
the history is emptied.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Forward conversationLabelText, respondingStatusText,
responseCompleteStatusText and errorStatusText to the messages
translations.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Addressed the three findings from Copilot's latest review summary in 0633d83 and db884a5:
Initially active requests / errors: the announcement baseline now starts idle, so a request already in flight on mount announces "Responding…". An error that predates the mount stays silent on purpose, so reopening the chat doesn't replay it. (submitted → streaming staying silent is intended: "Responding…" is said once per reply.)
Stale announcement after clearing: the announcement is cleared when the history becomes empty.
JS widget translations:conversationLabelText, respondingStatusText, responseCompleteStatusText and errorStatusText are now exposed under templates.messages and forwarded. The common translations test covers conversationLabelText in JS and React.
Merge translations through a helper that ignores keys explicitly set to
undefined, so a default is never replaced by a missing label or callback
(responseCompleteStatusText: undefined used to throw when a reply
finished).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Fixed in the latest push, at the source rather than at the call site: chat components now merge translations through a mergeTranslations helper that ignores keys explicitly set to undefined, so defaults are resolved once and never overwritten. This covers every chat component, not only ChatMessages, and has a regression test for responseCompleteStatusText: undefined. The same spread pattern exists in the non-chat components; I left those for a separate change.
…fined"
This reverts commit 6db8804. Explicitly
undefined translation overrides replace the default in every React and
ui-components widget; fixing that belongs in its own change across all
widgets rather than only in chat.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A resume attempt with no active stream can announce an unchanged historical reply as newly received. resumeStream() sets submitted, then returns to ready without changing messages when reconnection returns null (for example, on HTTP 404; see ai-lite/transport.ts:225–236 and abstract-chat.ts:996–1004). If the mounted history ends in an assistant message, this role-only check passes and calls responseCompleteStatusText on that old answer. Track whether the attempt produced or updated a reply before announcing completion, and add a regression test for a no-stream resume with existing history. Preserve completion announcements when mounting during an active stream.
A resume with no stream to reconnect to goes submitted → ready without
touching the history, and the restored assistant message was announced
as a new reply. Remember the last message while the request is
submitted, and only announce a trailing assistant message that changed
since.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ChatMessagesis no longer an active live region (aria-live="off"on therole="log"root). A new visually hiddenrole="status"region (.ais-ChatMessages-status) announces once when a reply starts, once ("Reply received.") when it completes, and on error.tabindex="0",role="region", label "Conversation"), with a:focus-visibleoutline ininstantsearch.css.conversationLabel,respondingStatusText,responseCompleteStatusText(receives the reply, so apps can announce its text),errorStatusText. New class name key:status.Result
Screen readers read a reply once instead of chunk by chunk, and keyboard users can scroll text-only answers. Chat unit and common-widget tests pass; lint, format and type-check are clean. Not yet checked with VoiceOver or NVDA.
Both changes are inspired by accessibility fixes in a customer shopping assistant, which worked around these gaps on its own.
Not included, to follow separately: dialog semantics and focus management for the overlay layout, the heading outline, and product list semantics.
🤖 Generated with Claude Code