fix: announce sending status to screen readers via live region#5781
fix: announce sending status to screen readers via live region#5781isherstneva wants to merge 20 commits intomicrosoft:mainfrom
Conversation
When a user submits a message, the activity enters the "sending" state while awaiting server acknowledgement. The visual "Sending" indicator was rendered next to the activity but not inside an ARIA live region, so screen readers never announced it. Adds LiveRegionSendSending component (mirroring LiveRegionSendFailed) that watches for activities newly entering the "sending" state and queues the localized "Sending message." string into the polite live region. Also adds TRANSCRIPT_LIVE_REGION_SEND_SENDING_ALT localization key and a corresponding integration test.
There was a problem hiding this comment.
Pull request overview
Adds ARIA live-region announcements for the “sending” state so screen readers narrate “Sending message.” when an outgoing activity is awaiting server acknowledgement.
Changes:
- Added
LiveRegionSendSendingcomponent and mounted it inLiveRegionTranscript. - Introduced new localization key
TRANSCRIPT_LIVE_REGION_SEND_SENDING_ALT. - Added an accessibility integration test covering the new live-region narration.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/component/src/Transcript/LiveRegionTranscript.tsx | Mounts the new sending live-region component alongside the existing send-failed narration. |
| packages/component/src/Transcript/LiveRegion/SendSending.tsx | Implements detection of activities newly entering the SENDING state and queues localized narration. |
| packages/api/src/localization/en-US.json | Adds the localized “Sending message.” string and comment for translators. |
| tests/html2/accessibility/liveRegion/activityStatus.sendSending.html | Adds an integration test asserting the live region narrates “Sending message.” for unacknowledged outgoing activities. |
| CHANGELOG.md | Documents the accessibility fix in the release notes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const prevActivityKeysOfSending = usePrevious(activityKeysOfSending); | ||
|
|
||
| /** True, if one or more non-presentational activities newly entered the "sending" state, otherwise false. */ | ||
| const hasNewSending = useMemo<boolean>(() => { | ||
| if (activityKeysOfSending === prevActivityKeysOfSending) { | ||
| return false; | ||
| } | ||
|
|
||
| for (const key of activityKeysOfSending.keys()) { | ||
| if (!prevActivityKeysOfSending.has(key)) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| }, [activityKeysOfSending, prevActivityKeysOfSending]); | ||
|
|
||
| useLiveRegion(() => hasNewSending && liveRegionSendSendingAlt, [hasNewSending, liveRegionSendSendingAlt]); |
There was a problem hiding this comment.
prevActivityKeysOfSending.has(key) will throw if usePrevious returns undefined on the initial render (common for “previous value” hooks). Consider defaulting prevActivityKeysOfSending to an empty Set (or handling the undefined case inside the memo) so the first render cannot crash. Also, useLiveRegion(() => hasNewSending && liveRegionSendSendingAlt, ...) returns false when not sending; if useLiveRegion expects a string/undefined, prefer returning undefined/null rather than a boolean to avoid accidental narration/queuing of a non-string value.
| () => | ||
| Array.from(sendStatusByActivityKey).reduce( | ||
| (activityKeysOfSending, [key, sendStatus]) => | ||
| sendStatus === SENDING && !isPresentational(getActivityByKey(key)) | ||
| ? activityKeysOfSending.add(key) | ||
| : activityKeysOfSending, | ||
| new Set<string>() | ||
| ), |
There was a problem hiding this comment.
Array.from(sendStatusByActivityKey) allocates an intermediate array on every recomputation. Since sendStatusByActivityKey is already iterable, iterating it directly (e.g., a for...of loop) avoids the extra allocation and can reduce overhead for large transcripts.
| () => | |
| Array.from(sendStatusByActivityKey).reduce( | |
| (activityKeysOfSending, [key, sendStatus]) => | |
| sendStatus === SENDING && !isPresentational(getActivityByKey(key)) | |
| ? activityKeysOfSending.add(key) | |
| : activityKeysOfSending, | |
| new Set<string>() | |
| ), | |
| () => { | |
| const activityKeysOfSending = new Set<string>(); | |
| for (const [key, sendStatus] of sendStatusByActivityKey) { | |
| if (sendStatus === SENDING && !isPresentational(getActivityByKey(key))) { | |
| activityKeysOfSending.add(key); | |
| } | |
| } | |
| return activityKeysOfSending; | |
| }, |
| "_TRANSCRIPT_LIVE_REGION_SUGGESTED_ACTIONS_WITH_ACCESS_KEY_LABEL_ALT.comment": "$1 will be \"ACCESS_KEY_ALT\".", | ||
| "TRANSCRIPT_LIVE_REGION_SEND_FAILED_ALT": "Failed to send message.", | ||
| "TRANSCRIPT_LIVE_REGION_SEND_SENDING_ALT": "Sending message.", | ||
| "_TRANSCRIPT_LIVE_REGION_SEND_SENDING_ALT.comment": "This is for screen reader. When the user sends a message, the live region will announce this string to indicate the message is being sent.", |
There was a problem hiding this comment.
Correct the wording “screen reader” → “screen readers” for grammatical correctness in the translator comment.
| "_TRANSCRIPT_LIVE_REGION_SEND_SENDING_ALT.comment": "This is for screen reader. When the user sends a message, the live region will announce this string to indicate the message is being sent.", | |
| "_TRANSCRIPT_LIVE_REGION_SEND_SENDING_ALT.comment": "This is for screen readers. When the user sends a message, the live region will announce this string to indicate the message is being sent.", |
Updated tests to include "Sending message." in expected live region output: - activityStatus.sendFailed.html: prepend Sending message. to expected array - activityStatus.sendSending.html: use toContain with proper accumulation - suggestedActions.accessKey.html: add Sending message. to expected array - suggestedActions.noAccessKey.html: add Sending message. to expected array
The onTelemetry exception event fires asynchronously after activities are rendered. Add a pageConditions.became() wait before asserting on it to avoid a race condition where the assertion runs before the event fires.
- fluentTheme/suggestedActions.html: add allImagesLoaded() before snapshot to ensure external CDN images are fully rendered before comparison - updateActivity/sameActivityId.html: add scrollToBottomCompleted() before snap-1 to ensure scroll animation finishes before comparison These waits were already used in similar tests but missing here, causing flaky pixel differences in CI where the machine is slower.
…ends Only narrate "Sending message." if the outgoing activity remains in the sending state for at least 3 seconds. Fast sends that are acknowledged quickly stay silent; only stalled or slow sends get announced. Update tests accordingly: - sendSending: increase became() timeout to 4000ms to accommodate the delay - sendFailed: remove Sending message. (rejection happens in < 3s) - suggestedActions: remove Sending message. (real DirectLine acks in < 3s)
Sending message. no longer appears in the live region for fast emulated sends, so all liveRegion attachment and alertEmptyMessage snapshots that previously captured it need to be updated.
- Rename LiveRegion/SendSending.tsx -> LiveRegion/LongSend.tsx - Rename component LiveRegionSendSending -> LiveRegionLongSend - Revert unknownActivity.html, fluentTheme/suggestedActions.html, updateActivity/sameActivityId.html to base (unrelated test fixes) - Revert videoCard.html.snap-1.png to base (spurious 1-byte PNG artifact)
@microsoft-github-policy-service agree company="Microsoft" |
compulim
left a comment
There was a problem hiding this comment.
Don't duplicate code. Put the logic in the <SendFailed> component.
…cation Both SendFailed and LongSend shared the same useMemo pattern for filtering activities by send status and isPresentational. Extracted into a shared hook useActivityKeysOfSendStatus(status).
Extracted shared filtering logic into a useActivityKeysOfSendStatus hook, used by both SendFailed and LongSend to eliminate duplicated code. |
There was a problem hiding this comment.
The hook design doesn't match our convention. Please look at HOOKS.md to see how we design our hooks.
When a user submits a message, the activity enters the "sending" state while awaiting server acknowledgement. The visual "Sending" indicator is rendered next to the activity but not inside an ARIA live region, so screen readers never announce it.
This PR adds a LiveRegionLongSend component (mirroring LiveRegionSendFailed) that watches for activities remaining in the "sending" state for longer than 3 seconds and queues the localized "Sending message." string into the polite live region. Fast sends that are acknowledged within 3 seconds stay silent to avoid noisy announcements. Also add TRANSCRIPT_LIVE_REGION_SEND_SENDING_ALT localization key and a corresponding integration test.
Bug ticket: https://msazure.visualstudio.com/CCI/_workitems/edit/36003607/
Changelog Entry
Description
Design
Specific Changes
CHANGELOG.mdReview Checklist
z-index)package.jsonandpackage-lock.jsonreviewed