Skip to content

Commit 385f011

Browse files
committed
fix: announce sending status to screen readers via live region
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.
1 parent d004a15 commit 385f011

5 files changed

Lines changed: 130 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Breaking changes in this release:
4343

4444
### Added
4545

46+
- Resolves screen reader not announcing when a message is being sent. Added live region narration of `Sending message.` via a new `LiveRegionSendSending` component, by [@isherstneva](https://github.com/isherstneva)
4647
- (Experimental) Added pre-chat message with starter prompts in Fluent UI, in PR [#5255](https://github.com/microsoft/BotFramework-WebChat/issues/5255) and [#5263](https://github.com/microsoft/BotFramework-WebChat/issues/5263), by [@compulim](https://github.com/compulim)
4748
- (Experimental) Added `isPrimary` props to Fluent UI send box. When set, will wire up with `useSendBoxValue` and works with starter prompts in pre-chat message, in PR [#5257](https://github.com/microsoft/BotFramework-WebChat/issues/5257), by [@compulim](https://github.com/compulim)
4849
- (Experimental) Expand Fluent theme support to activities and transcript, in PR [#5258](https://github.com/microsoft/BotFramework-WebChat/pull/5258), by [@OEvgeny](https://github.com/OEvgeny)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<!DOCTYPE html>
2+
<html lang="en-US">
3+
<head>
4+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
5+
<script crossorigin="anonymous" src="/test-harness.js"></script>
6+
<script crossorigin="anonymous" src="/test-page-object.js"></script>
7+
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
8+
</head>
9+
<body>
10+
<main id="webchat"></main>
11+
<script>
12+
run(async function () {
13+
const { directLine, store } = testHelpers.createDirectLineEmulator();
14+
15+
WebChat.renderWebChat(
16+
{
17+
directLine,
18+
store
19+
},
20+
document.getElementById('webchat')
21+
);
22+
23+
await pageConditions.uiConnected();
24+
25+
const { disconnect, flush } = pageObjects.observeLiveRegion();
26+
27+
try {
28+
// Emulate outgoing activity but do not acknowledge it, keeping it in "sending" state.
29+
directLine.emulateOutgoingActivity('Hello, World!');
30+
31+
await pageConditions.became(
32+
'live region narrated sending message',
33+
() => {
34+
try {
35+
expect(flush()).toEqual(['You said:\nHello, World!', 'Sending message.']);
36+
37+
return true;
38+
} catch {
39+
return false;
40+
}
41+
},
42+
1000
43+
);
44+
} finally {
45+
disconnect();
46+
}
47+
});
48+
</script>
49+
</body>
50+
</html>

packages/api/src/localization/en-US.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@
189189
"TRANSCRIPT_LIVE_REGION_SUGGESTED_ACTIONS_WITH_ACCESS_KEY_LABEL_ALT": "Message has suggested actions. Press $1 to select them.",
190190
"_TRANSCRIPT_LIVE_REGION_SUGGESTED_ACTIONS_WITH_ACCESS_KEY_LABEL_ALT.comment": "$1 will be \"ACCESS_KEY_ALT\".",
191191
"TRANSCRIPT_LIVE_REGION_SEND_FAILED_ALT": "Failed to send message.",
192+
"TRANSCRIPT_LIVE_REGION_SEND_SENDING_ALT": "Sending message.",
193+
"_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.",
192194
"TRANSCRIPT_LIVE_REGION_NEW_MESSAGES_ALT": "New messages available. Press $1 to focus the \"$2\" button.",
193195
"_TRANSCRIPT_LIVE_REGION_NEW_MESSAGES_ALT.comment": "$1 will be \"ACCESS_KEY_ALT\".",
194196
"TRANSCRIPT_MORE_MESSAGES": "More messages",
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { hooks } from 'botframework-webchat-api';
2+
import { memo, useMemo } from 'react';
3+
4+
import usePrevious from '../../hooks/internal/usePrevious';
5+
import { useLiveRegion } from '../../providers/LiveRegionTwin';
6+
import { SENDING } from '../../types/internal/SendStatus';
7+
import isPresentational from './isPresentational';
8+
9+
const { useGetActivityByKey, useLocalizer, useSendStatusByActivityKey } = hooks;
10+
11+
/**
12+
* React component to on-demand narrate "Sending message." at the end of the live region.
13+
*
14+
* When the user sends a message the activity enters the "sending" state before the server acknowledges it.
15+
* The visual "Sending" indicator is rendered next to the activity, but that text is not inside an ARIA
16+
* live region and is therefore not announced by screen readers.
17+
*
18+
* This component watches for activities that newly enter the `sending` state and queues the localized
19+
* "Sending message." string into the polite live region so assistive technologies announce it.
20+
*
21+
* Presentational activities (e.g. `event` or `typing`) are excluded to reduce noise.
22+
*/
23+
const LiveRegionSendSending = () => {
24+
const [sendStatusByActivityKey] = useSendStatusByActivityKey();
25+
const getActivityByKey = useGetActivityByKey();
26+
const localize = useLocalizer();
27+
28+
/**
29+
* Set of keys of outgoing and non-presentational activities that are currently being sent.
30+
*/
31+
const activityKeysOfSending = useMemo<Set<string>>(
32+
() =>
33+
Array.from(sendStatusByActivityKey).reduce(
34+
(activityKeysOfSending, [key, sendStatus]) =>
35+
sendStatus === SENDING && !isPresentational(getActivityByKey(key))
36+
? activityKeysOfSending.add(key)
37+
: activityKeysOfSending,
38+
new Set<string>()
39+
),
40+
[getActivityByKey, sendStatusByActivityKey]
41+
);
42+
43+
/** Returns localized "Sending message." */
44+
const liveRegionSendSendingAlt = localize('TRANSCRIPT_LIVE_REGION_SEND_SENDING_ALT');
45+
46+
const prevActivityKeysOfSending = usePrevious(activityKeysOfSending);
47+
48+
/** True, if one or more non-presentational activities newly entered the "sending" state, otherwise false. */
49+
const hasNewSending = useMemo<boolean>(() => {
50+
if (activityKeysOfSending === prevActivityKeysOfSending) {
51+
return false;
52+
}
53+
54+
for (const key of activityKeysOfSending.keys()) {
55+
if (!prevActivityKeysOfSending.has(key)) {
56+
return true;
57+
}
58+
}
59+
60+
return false;
61+
}, [activityKeysOfSending, prevActivityKeysOfSending]);
62+
63+
useLiveRegion(() => hasNewSending && liveRegionSendSendingAlt, [hasNewSending, liveRegionSendSendingAlt]);
64+
65+
return null;
66+
};
67+
68+
LiveRegionSendSending.displayName = 'LiveRegionSendSending';
69+
70+
export default memo(LiveRegionSendSending);

packages/component/src/Transcript/LiveRegionTranscript.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import useLocalizeAccessKey from '../hooks/internal/useLocalizeAccessKey';
99
import useSuggestedActionsAccessKey from '../hooks/internal/useSuggestedActionsAccessKey';
1010
import { useQueueStaticElement } from '../providers/LiveRegionTwin';
1111
import LiveRegionSendFailed from './LiveRegion/SendFailed';
12+
import LiveRegionSendSending from './LiveRegion/SendSending';
1213
import isPresentational from './LiveRegion/isPresentational';
1314
import useTypistNames from './useTypistNames';
1415

@@ -130,7 +131,12 @@ const LiveRegionTranscript = ({ activityElementMapRef }: LiveRegionTranscriptPro
130131

131132
useMemo(() => typingIndicator && queueStaticElement(typingIndicator), [queueStaticElement, typingIndicator]);
132133

133-
return <LiveRegionSendFailed />;
134+
return (
135+
<>
136+
<LiveRegionSendFailed />
137+
<LiveRegionSendSending />
138+
</>
139+
);
134140
};
135141

136142
LiveRegionTranscript.displayName = 'LiveRegionTranscript';

0 commit comments

Comments
 (0)