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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ Legends:
- Fixed the "Positive" card action button having less than 4.5:1 color contrast, fixes [#5713](https://github.com/microsoft/BotFramework-WebChat/issues/5713), in PR [#5905](https://github.com/microsoft/BotFramework-WebChat/pull/5905), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed screen readers announcing a single audio, video, or animation in a card as a list item, such as "1 of 1", fixes [#5722](https://github.com/microsoft/BotFramework-WebChat/issues/5722), in PR [#5903](https://github.com/microsoft/BotFramework-WebChat/pull/5903), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed video and animation cards disappearing in production builds when the video URL is relative or invalid, in PR [#5889](https://github.com/microsoft/BotFramework-WebChat/pull/5889), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed screen readers announcing all bot messages with the same initials, such as "Bot JO said:" for both a bot and a live agent: the text before each message, the name of the message when navigating with arrow keys, and the live region now prefer the per-activity `channelData.webChat.styleOptions.botAvatarInitials` over `styleOptions.botAvatarInitials`, fixes [#5803](https://github.com/microsoft/BotFramework-WebChat/issues/5803), in PR [#5898](https://github.com/microsoft/BotFramework-WebChat/pull/5898), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed screen readers not announcing the time of a message when navigating the transcript with arrow keys: the name of each sent message now ends with the same "Sent at" text as its timestamp, fixes [#5715](https://github.com/microsoft/BotFramework-WebChat/issues/5715) and [#5735](https://github.com/microsoft/BotFramework-WebChat/issues/5735), in PR [#5898](https://github.com/microsoft/BotFramework-WebChat/pull/5898), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed timestamps always using the 12-hour clock, such as "4:41 PM" instead of "16:41" in German, in PR [#5920](https://github.com/microsoft/BotFramework-WebChat/pull/5920), by [@OEvgeny](https://github.com/OEvgeny)

## [4.19.2] - 2026-10-08
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
Expand Down Expand Up @@ -29,7 +29,10 @@

const sendMessage = await directLine.emulateOutgoingActivity('Hello, World!');

await directLine.emulateIncomingActivity('Aloha!');
// The echo back of the message has a timestamp, but the message is not sent.
await sendMessage.echoBack(activity => ({ ...activity, timestamp: '2026-10-17T11:50:00Z' }));

await directLine.emulateIncomingActivity({ text: 'Aloha!', timestamp: '2026-10-17T11:51:00Z' });

sendMessage.rejectPostActivity(new Error('artificial error'));

Expand All @@ -40,8 +43,9 @@
);

expect(pageElements.activityActiveDescendantLabels().map(({ innerText }) => innerText)).toEqual([
// The time is only narrated after the message is sent.
'You said: Hello, World! Send failed. Click to interact.',
'Bot said: Aloha!'
'Bot said: Aloha! Sent at October 17 at 11:51 AM'
]);
},
{ ignoreErrors: true }
Expand Down
42 changes: 34 additions & 8 deletions __tests__/html2/accessibility/accessibleName.simple.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
Expand All @@ -8,7 +8,16 @@
</head>
<body>
<main id="webchat"></main>
<script>
<script type="importmap">
{
"imports": {
"@testduet/wait-for": "https://esm.sh/@testduet/wait-for"
}
}
</script>
<script type="module">
import { waitFor } from '@testduet/wait-for';

run(
async function () {
const { directLine, store } = testHelpers.createDirectLineEmulator();
Expand All @@ -29,14 +38,31 @@

const sendMessage = await directLine.emulateOutgoingActivity('Hello, World!');

await sendMessage.resolveAll();
// WHEN: The echo back of the message, which has a timestamp, arrives before the message is sent.
await sendMessage.echoBack(activity => ({ ...activity, timestamp: '2026-10-17T11:50:00Z' }));

await directLine.emulateIncomingActivity('Aloha!');
await directLine.emulateIncomingActivity({ text: 'Aloha!', timestamp: '2026-10-17T11:51:00Z' });

// THEN: While the message is being sent, the time should not be narrated, like the activity status.
await waitFor(() =>
expect(pageElements.activityActiveDescendantLabels().map(({ innerText }) => innerText)).toEqual([
'You said: Hello, World!',
'Bot said: Aloha! Sent at October 17 at 11:51 AM'
])
);

expect(pageElements.activityActiveDescendantLabels().map(({ innerText }) => innerText)).toEqual([
'You said: Hello, World!',
'Bot said: Aloha!'
]);
expect(pageElements.activityStatuses()[0].innerText).toBe('Sending');

// WHEN: The message is sent.
sendMessage.resolvePostActivity();

// THEN: The time should be narrated after the message, like the timestamp in the activity status.
await waitFor(() =>
expect(pageElements.activityActiveDescendantLabels().map(({ innerText }) => innerText)).toEqual([
'You said: Hello, World! Sent at October 17 at 11:50 AM',
'Bot said: Aloha! Sent at October 17 at 11:51 AM'
])
);
},
{ ignoreErrors: true }
);
Expand Down
117 changes: 117 additions & 0 deletions __tests__/html2/accessibility/botAvatarInitials.perActivity.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<!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 type="importmap">
{
"imports": {
"@testduet/wait-for": "https://esm.sh/@testduet/wait-for"
}
}
</script>
<script type="module">
import { waitFor } from '@testduet/wait-for';

const withBotAvatarInitials = botAvatarInitials => ({ webChat: { styleOptions: { botAvatarInitials } } });

run(async function () {
const { directLine, store } = testHelpers.createDirectLineEmulator();

// GIVEN: The global bot initials are the live agent's, like a host which updates them when an agent joins.
WebChat.renderWebChat(
{
directLine,
store,
styleOptions: {
botAvatarInitials: 'JO',
internalLiveRegionFadeAfter: 60000 // Keep messages in the live region until they are checked.
}
},
document.getElementById('webchat')
);

await pageConditions.uiConnected();

// WHEN: A bot and a live agent send messages, each with their own initials in `channelData.webChat.styleOptions`.
await directLine.emulateIncomingActivity({
channelData: withBotAvatarInitials('BO'),
from: { id: 'bot', name: 'Bot', role: 'bot' },
text: 'Okay, let me connect you with a live agent.',
timestamp: '2026-10-17T11:50:05Z'
});

await directLine.emulateIncomingActivity({
attachmentLayout: 'carousel',
attachments: [
{ content: 'Update my address', contentType: 'text/plain' },
{ content: 'Update my phone number', contentType: 'text/plain' }
],
channelData: withBotAvatarInitials('BO'),
from: { id: 'bot', name: 'Bot', role: 'bot' },
text: 'While you wait, here are some options.',
timestamp: '2026-10-17T11:50:06Z'
});

await directLine.emulateIncomingActivity({
channelData: withBotAvatarInitials('JO'),
from: { id: 'agent-john', name: 'John', role: 'bot' },
text: "I'd be happy to help you update your account.",
timestamp: '2026-10-17T11:51:00Z'
});

// WHEN: Another message comes without per-activity initials.
await directLine.emulateIncomingActivity({
from: { id: 'agent-john', name: 'John', role: 'bot' },
text: 'What is your new address?',
timestamp: '2026-10-17T11:51:30Z'
});

await pageConditions.numActivitiesShown(4);

// THEN: In scan mode, the greeting before each message should use the initials of its activity, otherwise, the global ones.
expect(
pageElements
.activities()
.map(
activity => testHelpers.getAllTextContents(activity.querySelector('[aria-roledescription="message"]'))[0]
)
).toEqual(['Bot BO said:', 'Bot BO said:', 'Bot JO said:', 'Bot JO said:']);

// THEN: When navigating with arrow keys, the accessible name of each activity should use the same initials.
// The carousel is interactive: it has buttons to scroll its attachments.
await waitFor(async () =>
expect(
await Promise.all(
pageElements.activityActiveDescendantLabels().map(element => testHelpers.getAccessibleName(element))
)
).toEqual([
'Bot BO said: Okay, let me connect you with a live agent. Sent at October 17 at 11:50 AM',
'Bot BO said: While you wait, here are some options. 2 attachments. Message is interactive. Click to interact. Sent at October 17 at 11:50 AM',
"Bot JO said: I'd be happy to help you update your account. Sent at October 17 at 11:51 AM",
'Bot JO said: What is your new address? Sent at October 17 at 11:51 AM'
])
);

// THEN: The live region should announce each activity with the same initials.
await waitFor(() =>
expect(
[].map.call(pageElements.liveRegion().querySelectorAll('.webchat__live-region-activity'), article =>
testHelpers.getAllTextContents(article).join('\n')
)
).toEqual([
'Bot BO said:\nOkay, let me connect you with a live agent.',
'Bot BO said:\nWhile you wait, here are some options.\nA text: Update my address\nA text: Update my phone number',
"Bot JO said:\nI'd be happy to help you update your account.",
'Bot JO said:\nWhat is your new address?'
])
);
});
</script>
</body>
</html>
3 changes: 3 additions & 0 deletions packages/api/src/StyleOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ type StrictStyleOptions = {
/**
* Typically rendered as two letters, e.g. 'WC'
* Empty string is required when setting botAvatarImage
*
* Screen readers narrate the initials before each bot message, e.g. "Bot WC said:".
* The narration prefers per-activity initials in `channelData.webChat.styleOptions.botAvatarInitials`, e.g. for a live agent.
*/
botAvatarInitials: string | undefined;

Expand Down
1 change: 1 addition & 0 deletions packages/bundle/src/boot/actual/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export {
PartGrouping,
ScreenReaderText,
serializeDocumentFragmentIntoString,
useActivityStyleOptions,
useCodeHighlighter,
useLiveRegion,
type HighlightCodeFn
Expand Down
8 changes: 3 additions & 5 deletions packages/component/src/Activity/CarouselFilmStrip.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import CarouselFilmStripAttachment from './CarouselFilmStripAttachment';
import isZeroOrPositive from '../Utils/isZeroOrPositive';
import ScreenReaderText from '../ScreenReaderText';
import textFormatToContentType from '../Utils/textFormatToContentType';
import useLocalizeActivityGreetingAlt from '../hooks/internal/useLocalizeActivityGreetingAlt.js';

import styles from './CarouselFilmStrip.module.css';

const { useAvatarForBot, useAvatarForUser, useDirection, useLocalizer, useStyleOptions } = hooks;
const { useAvatarForBot, useAvatarForUser, useDirection, useStyleOptions } = hooks;

const CarouselFilmStrip = ({
activity,
Expand All @@ -30,7 +31,6 @@ const CarouselFilmStrip = ({
const [{ initials: botInitials }] = useAvatarForBot();
const [{ initials: userInitials }] = useAvatarForUser();
const [direction] = useDirection();
const localize = useLocalizer();
const showActivityStatus = typeof renderActivityStatus === 'function';
const classNames = useStyles(styles);

Expand Down Expand Up @@ -97,9 +97,7 @@ const CarouselFilmStrip = ({
const activityDisplayText = messageBackDisplayText || text;
const fromUser = role === 'user';

const greetingAlt = (
fromUser ? localize('ACTIVITY_YOU_SAID_ALT') : localize('ACTIVITY_BOT_SAID_ALT', botInitials || '')
).replace(/\s{2,}/gu, ' ');
const [greetingAlt] = useLocalizeActivityGreetingAlt()(activity);

const initials = fromUser ? userInitials : botInitials;
const nubOffset = fromUser ? bubbleFromUserNubOffset : bubbleNubOffset;
Expand Down
7 changes: 2 additions & 5 deletions packages/component/src/Activity/StackedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ScreenReaderText from '../ScreenReaderText';
import isZeroOrPositive from '../Utils/isZeroOrPositive';
import getFirstBaseOfSoftwareSourceCode from '../Utils/orgSchema/getFirstBaseOfSoftwareSourceCode';
import textFormatToContentType from '../Utils/textFormatToContentType';
import useLocalizeActivityGreetingAlt from '../hooks/internal/useLocalizeActivityGreetingAlt.js';
import useLocalizeProgrammingLanguage from '../hooks/internal/useLocalizeProgrammingLanguage';
import { useGetLogicalGroupKey } from '../providers/ActivityLogicalGrouping';
import AttachmentRow from './AttachmentRow';
Expand Down Expand Up @@ -60,14 +61,10 @@ const StackedLayoutInner = memo(
showAvatar,
showNub
}: StackedLayoutInnerProps) => {
const [{ initials: botInitials }] = useAvatarForBot();
const localize = useLocalizer();
const classNames = useStyles(styles);
const [greetingAlt] = useLocalizeActivityGreetingAlt()(activity);

const messageThing = useMemo(() => getOrgSchemaMessage(activity.entities), [activity]);
const greetingAlt = (
fromUser ? localize('ACTIVITY_YOU_SAID_ALT') : localize('ACTIVITY_BOT_SAID_ALT', botInitials || '')
).replace(/\s{2,}/gu, ' ');

return (
<StackedLayoutMain avatar={showAvatar && renderAvatar && renderAvatar()}>
Expand Down
15 changes: 3 additions & 12 deletions packages/component/src/LiveRegion/LiveRegionActivity.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
/* eslint no-magic-numbers: ["error", { "ignore": [2] }] */

import { hooks } from 'botframework-webchat-api';
import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
import classNames from 'classnames';
import React, { Fragment, useMemo } from 'react';
import { any, object, pipe, readonly, type InferInput } from 'valibot';

import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject';
import useLocalizeActivityGreetingAlt from '../hooks/internal/useLocalizeActivityGreetingAlt.js';
import useRenderMarkdownAsHTML from '../hooks/useRenderMarkdownAsHTML';
import activityAltText from '../Utils/activityAltText';
import LiveRegionAttachments from './private/LiveRegionAttachments';
import LiveRegionSuggestedActions from './private/LiveRegionSuggestedActions';

const { useAvatarForBot, useLocalizer } = hooks;

const ROOT_STYLE = {
'&.webchat__live-region-activity': {
color: 'transparent',
Expand All @@ -39,21 +37,14 @@ type LiveRegionActivityProps = InferInput<typeof liveRegionActivityPropsSchema>;
function LiveRegionActivity(props: LiveRegionActivityProps) {
const { activity } = validateProps(liveRegionActivityPropsSchema, props);

const [{ initials: botInitials }] = useAvatarForBot();
const {
from: { role },
type
} = activity;
const { type } = activity;
const fallbackText: string | undefined =
type === 'message' ? activity.channelData['webchat:fallback-text'] : undefined;
const localize = useLocalizer();
const [greetingAlt] = useLocalizeActivityGreetingAlt()(activity);
const renderMarkdownAsHTML = useRenderMarkdownAsHTML('accessible name');
const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + '';
const textAlt = useMemo(() => activityAltText(activity, renderMarkdownAsHTML), [activity, renderMarkdownAsHTML]);

const greetingAlt: string = (
role === 'user' ? localize('ACTIVITY_YOU_SAID_ALT') : localize('ACTIVITY_BOT_SAID_ALT', botInitials || '')
).replace(/\s{2,}/gu, ' ');
const validFallbackText = fallbackText && typeof fallbackText === 'string';

return (
Expand Down
Loading
Loading