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 @@ -62,6 +62,7 @@ Legends:
- 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 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)
- Fixed the "message" and "attachment" role descriptions being hard-coded in English, in PR [#5916](https://github.com/microsoft/BotFramework-WebChat/pull/5916), by [@OEvgeny](https://github.com/OEvgeny)

## [4.19.2] - 2026-10-08

Expand Down
124 changes: 124 additions & 0 deletions __tests__/html2/accessibility/aria-roledescription.localized.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<!-- Shows the live region, so the snapshots show the copy sent to screen readers. -->
<link href="/assets/accessibility.liveRegionAttachment.css" rel="stylesheet" type="text/css" />
</head>
<body>
<main id="webchat"></main>
<script type="importmap">
{
"imports": {
"@testduet/wait-for": "https://esm.sh/@testduet/wait-for",
"botframework-webchat": "/__dist__/packages/bundle/static/botframework-webchat.js",
"botframework-webchat/": "/__dist__/packages/bundle/static/botframework-webchat/",
"react": "/__dist__/packages/bundle/static/react.js",
"react-dom": "/__dist__/packages/bundle/static/react-dom.js",
"react-dom/client": "/__dist__/packages/bundle/static/react-dom/client.js"
}
}
</script>
<script type="module">
import '/test-harness.mjs';
import '/test-page-object.mjs';

import { waitFor } from '@testduet/wait-for';
import { createStoreWithOptions, ReactWebChat } from 'botframework-webchat';
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';

// TODO: This is for `createDirectLineEmulator` only, should find ways to eliminate this line.
window.WebChat = { createStoreWithOptions };

run(async function () {
const { directLine, store } = testHelpers.createDirectLineEmulator();
const root = createRoot(document.getElementById('webchat'));
const render = props =>
root.render(
createElement(ReactWebChat, {
directLine,
store,
// Keeps the live region copy of new messages, so it can be seen in the snapshots.
styleOptions: { hideSendBox: true, internalLiveRegionFadeAfter: 60000 },
...props
})
);

// Role descriptions of the message and attachments, per activity.
// Selects by role, not by the attribute, so an element missing its role description shows up as null.
const getRoleDescriptions = () =>
pageElements
.activities()
.map(activity =>
Array.from(activity.querySelectorAll('[role="group"], [role="listitem"]'), element =>
element.getAttribute('aria-roledescription')
)
);

// GIVEN: Web Chat in the default locale (en-US).
render({});

await pageConditions.uiConnected();

// GIVEN: A message with text and 2 attachments in each attachment layout.
for (const attachmentLayout of ['list', 'carousel']) {
await directLine.emulateIncomingActivity({
attachmentLayout,
attachments: [1, 2].map(index => ({
content: { title: `Card ${index} (${attachmentLayout})` },
contentType: 'application/vnd.microsoft.card.hero'
})),
from: { role: 'bot' },
text: `Message with ${attachmentLayout} attachments`,
type: 'message'
});
}

await pageConditions.numActivitiesShown(2);
await pageConditions.liveRegionStabilized();

// THEN: A stacked message, then a carousel message, each with its text and 2 cards.
// THEN: The live region (top) shows what screen readers announce for each: "Bot said:", the text, and "A card:" per card, without role descriptions.
await host.snapshot('local');

// THEN: Message and attachments are described in English.
expect(getRoleDescriptions()).toEqual([
['message', 'attachment', 'attachment'],
['message', 'attachment', 'attachment']
]);

// WHEN: The host switches to German and provides the strings (no translation in de-DE yet).
render({
locale: 'de-DE',
overrideLocalizedStrings: {
ACTIVITY_ATTACHMENT_ARIA_ROLE_ALT: 'Anhang',
ACTIVITY_MESSAGE_ARIA_ROLE_ALT: 'Nachricht'
}
});

// THEN: Message and attachments are described in German.
await waitFor(() =>
expect(getRoleDescriptions()).toEqual([
['Nachricht', 'Anhang', 'Anhang'],
['Nachricht', 'Anhang', 'Anhang']
])
);

// WHEN: The bot sends another message.
await directLine.emulateIncomingActivity({
attachments: [{ content: { title: 'Card 3' }, contentType: 'application/vnd.microsoft.card.hero' }],
from: { role: 'bot' },
text: 'Message after switching to German',
type: 'message'
});

await pageConditions.numActivitiesShown(3);
await pageConditions.liveRegionStabilized();

// THEN: The live region shows the new message announced in German ("Bot sagte:", "Eine Karte:", from de-DE); the earlier copies are re-rendered in German too.
await host.snapshot('local');
});
</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.
4 changes: 4 additions & 0 deletions packages/api/src/localization/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"_ACTIVITY_YOU_SAID_ALT.comment": "This is for screen reader only.",
"ACTIVITY_BOT_ATTACHED_ALT": "Bot attached:",
"_ACTIVITY_BOT_ATTACHED_ALT.comment": "This is for screen reader and is narrated before each attachments sent by the bot.",
"ACTIVITY_ATTACHMENT_ARIA_ROLE_ALT": "attachment",
"_ACTIVITY_ATTACHMENT_ARIA_ROLE_ALT.comment": "This is for screen reader for the aria-roledescription of each attachment in a message, narrated as the type of the element.",
"ACTIVITY_MESSAGE_ARIA_ROLE_ALT": "message",
"_ACTIVITY_MESSAGE_ARIA_ROLE_ALT.comment": "This is for screen reader for the aria-roledescription of the text of a message, narrated as the type of the element.",
"ACTIVITY_CODE_ALT": "Code sample: $1",
"_ACTIVITY_CODE_ALT.comment": "This is for screen reader code sample dialog label, $1 is the dialog title.",
"ACTIVITY_CONTENT_CAUTION": "AI-generated content may be incorrect",
Expand Down
16 changes: 12 additions & 4 deletions packages/component/src/Activity/AttachmentRow.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { reactNode, validateProps } from '@msinternal/botframework-webchat-react-valibot';
import { reactNode, validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot';
import { useStyles } from '@msinternal/botframework-webchat-styles/react';
import { hooks } from 'botframework-webchat-api';
import cx from 'classnames';
import React, { memo } from 'react';
import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot';
import { boolean, object, optional, pipe, readonly, string } from 'valibot';

import ScreenReaderText from '../ScreenReaderText';
import Bubble from './Bubble';

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

const { useLocalizer } = hooks;

const attachmentRowPropsSchema = pipe(
object({
attachedAlt: string(),
Expand All @@ -21,7 +24,7 @@ const attachmentRowPropsSchema = pipe(
readonly()
);

type AttachmentRowProps = InferInput<typeof attachmentRowPropsSchema>;
type AttachmentRowProps = InferReactProps<typeof attachmentRowPropsSchema>;

function AttachmentRow(props: AttachmentRowProps) {
const {
Expand All @@ -33,9 +36,14 @@ function AttachmentRow(props: AttachmentRowProps) {
showBubble = true
} = validateProps(attachmentRowPropsSchema, props);
const classNames = useStyles(styles);
const localize = useLocalizer();

return (
<div aria-roledescription="attachment" className={classNames['stacked-layout__attachment-row']} role="group">
<div
aria-roledescription={localize('ACTIVITY_ATTACHMENT_ARIA_ROLE_ALT')}
className={classNames['stacked-layout__attachment-row']}
role="group"
>
<ScreenReaderText text={attachedAlt} />
{showBubble ? (
<Bubble
Expand Down
6 changes: 5 additions & 1 deletion packages/component/src/Activity/CarouselFilmStrip.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,11 @@ const CarouselFilmStrip = ({
</div>
<div className={classNames['carousel-filmstrip__content']}>
{!!activityDisplayText && (
<div aria-roledescription="message" className={classNames['carousel-filmstrip__message']} role="group">
<div
aria-roledescription={localize('ACTIVITY_MESSAGE_ARIA_ROLE_ALT')}
className={classNames['carousel-filmstrip__message']}
role="group"
>
<ScreenReaderText text={greetingAlt} />
<Bubble
className={classNames['carousel-filmstrip__bubble']}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const CarouselFilmStripAttachment = ({

return (
<li
aria-roledescription="attachment"
aria-roledescription={localize('ACTIVITY_ATTACHMENT_ARIA_ROLE_ALT')}
className={cx(
classNames['carousel-filmstrip-attachment'],
{
Expand Down
6 changes: 5 additions & 1 deletion packages/component/src/Activity/StackedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ const StackedLayoutInner = memo(
return (
<StackedLayoutMain avatar={showAvatar && renderAvatar && renderAvatar()}>
{!!(hasDisplayText || messageThing?.abstract[0]) && (
<div aria-roledescription="message" className={cx(classNames['stacked-layout__message-row'])} role="group">
<div
aria-roledescription={localize('ACTIVITY_MESSAGE_ARIA_ROLE_ALT')}
className={cx(classNames['stacked-layout__message-row'])}
role="group"
>
<ScreenReaderText text={greetingAlt} />
<Bubble
className={classNames['stacked-layout__message']}
Expand Down
Loading