Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Breaking changes in this release:

### Added

- 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)
- (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)
- (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)
- (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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script crossorigin="anonymous" src="/test-harness.js"></script>
<script crossorigin="anonymous" src="/test-page-object.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
</head>
<body>
<main id="webchat"></main>
<script>
run(async function () {
const { directLine, store } = testHelpers.createDirectLineEmulator();

WebChat.renderWebChat(
{
directLine,
store
},
document.getElementById('webchat')
);

await pageConditions.uiConnected();

const { disconnect, flush } = pageObjects.observeLiveRegion();

try {
// Emulate outgoing activity but do not acknowledge it, keeping it in "sending" state.
directLine.emulateOutgoingActivity('Hello, World!');

await pageConditions.became(
'live region narrated sending message',
() => {
try {
expect(flush()).toEqual(['You said:\nHello, World!', 'Sending message.']);

return true;
} catch {
return false;
}
},
1000
);
} finally {
disconnect();
}
});
</script>
</body>
</html>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions packages/api/src/localization/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@
"TRANSCRIPT_LIVE_REGION_SUGGESTED_ACTIONS_WITH_ACCESS_KEY_LABEL_ALT": "Message has suggested actions. Press $1 to select them.",
"_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.",
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct the wording “screen reader” → “screen readers” for grammatical correctness in the translator comment.

Suggested change
"_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.",

Copilot uses AI. Check for mistakes.
"TRANSCRIPT_LIVE_REGION_NEW_MESSAGES_ALT": "New messages available. Press $1 to focus the \"$2\" button.",
"_TRANSCRIPT_LIVE_REGION_NEW_MESSAGES_ALT.comment": "$1 will be \"ACCESS_KEY_ALT\".",
"TRANSCRIPT_MORE_MESSAGES": "More messages",
Expand Down
70 changes: 70 additions & 0 deletions packages/component/src/Transcript/LiveRegion/SendSending.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { hooks } from 'botframework-webchat-api';
import { memo, useMemo } from 'react';

import usePrevious from '../../hooks/internal/usePrevious';
import { useLiveRegion } from '../../providers/LiveRegionTwin';
import { SENDING } from '../../types/internal/SendStatus';
import isPresentational from './isPresentational';

const { useGetActivityByKey, useLocalizer, useSendStatusByActivityKey } = hooks;

/**
* React component to on-demand narrate "Sending message." at the end of the live region.
*
* When the user sends a message the activity enters the "sending" state before the server acknowledges it.
* The visual "Sending" indicator is rendered next to the activity, but that text is not inside an ARIA
* live region and is therefore not announced by screen readers.
*
* This component watches for activities that newly enter the `sending` state and queues the localized
* "Sending message." string into the polite live region so assistive technologies announce it.
*
* Presentational activities (e.g. `event` or `typing`) are excluded to reduce noise.
*/
const LiveRegionSendSending = () => {
const [sendStatusByActivityKey] = useSendStatusByActivityKey();
const getActivityByKey = useGetActivityByKey();
const localize = useLocalizer();

/**
* Set of keys of outgoing and non-presentational activities that are currently being sent.
*/
const activityKeysOfSending = useMemo<Set<string>>(
() =>
Array.from(sendStatusByActivityKey).reduce(
(activityKeysOfSending, [key, sendStatus]) =>
sendStatus === SENDING && !isPresentational(getActivityByKey(key))
? activityKeysOfSending.add(key)
: activityKeysOfSending,
new Set<string>()
),
Comment on lines +32 to +39
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
() =>
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;
},

Copilot uses AI. Check for mistakes.
[getActivityByKey, sendStatusByActivityKey]
);

/** Returns localized "Sending message." */
const liveRegionSendSendingAlt = localize('TRANSCRIPT_LIVE_REGION_SEND_SENDING_ALT');

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]);
Comment on lines +46 to +63
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

return null;
};

LiveRegionSendSending.displayName = 'LiveRegionSendSending';

export default memo(LiveRegionSendSending);
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useLocalizeAccessKey from '../hooks/internal/useLocalizeAccessKey';
import useSuggestedActionsAccessKey from '../hooks/internal/useSuggestedActionsAccessKey';
import { useQueueStaticElement } from '../providers/LiveRegionTwin';
import LiveRegionSendFailed from './LiveRegion/SendFailed';
import LiveRegionSendSending from './LiveRegion/SendSending';
import isPresentational from './LiveRegion/isPresentational';
import useTypistNames from './useTypistNames';

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

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

return <LiveRegionSendFailed />;
return (
<React.Fragment>
<LiveRegionSendFailed />
<LiveRegionSendSending />
</React.Fragment>
);
};

LiveRegionTranscript.displayName = 'LiveRegionTranscript';
Expand Down
Loading