Skip to content

Commit e7c0dba

Browse files
OEvgenyEugene Olonov
andauthored
Localize the "message" and "attachment" role descriptions (#5916)
* Localize the "message" and "attachment" role descriptions Add ACTIVITY_MESSAGE_ARIA_ROLE_ALT and ACTIVITY_ATTACHMENT_ARIA_ROLE_ALT and use them for aria-roledescription of the message and attachment groups in stacked and carousel layouts, instead of hard-coded English. * Add PR number to changelog * Show the live region copy in role description snapshots * Move the changelog entry to .changelog/ --------- Co-authored-by: Eugene Olonov <eugeneolonov@microsoft.com>
1 parent fb9d46f commit e7c0dba

9 files changed

Lines changed: 154 additions & 7 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Fixed
2+
3+
- 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)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<!doctype html>
2+
<html lang="en-US">
3+
<head>
4+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
5+
<!-- Shows the live region, so the snapshots show the copy sent to screen readers. -->
6+
<link href="/assets/accessibility.liveRegionAttachment.css" rel="stylesheet" type="text/css" />
7+
</head>
8+
<body>
9+
<main id="webchat"></main>
10+
<script type="importmap">
11+
{
12+
"imports": {
13+
"@testduet/wait-for": "https://esm.sh/@testduet/wait-for",
14+
"botframework-webchat": "/__dist__/packages/bundle/static/botframework-webchat.js",
15+
"botframework-webchat/": "/__dist__/packages/bundle/static/botframework-webchat/",
16+
"react": "/__dist__/packages/bundle/static/react.js",
17+
"react-dom": "/__dist__/packages/bundle/static/react-dom.js",
18+
"react-dom/client": "/__dist__/packages/bundle/static/react-dom/client.js"
19+
}
20+
}
21+
</script>
22+
<script type="module">
23+
import '/test-harness.mjs';
24+
import '/test-page-object.mjs';
25+
26+
import { waitFor } from '@testduet/wait-for';
27+
import { createStoreWithOptions, ReactWebChat } from 'botframework-webchat';
28+
import { createElement } from 'react';
29+
import { createRoot } from 'react-dom/client';
30+
31+
// TODO: This is for `createDirectLineEmulator` only, should find ways to eliminate this line.
32+
window.WebChat = { createStoreWithOptions };
33+
34+
run(async function () {
35+
const { directLine, store } = testHelpers.createDirectLineEmulator();
36+
const root = createRoot(document.getElementById('webchat'));
37+
const render = props =>
38+
root.render(
39+
createElement(ReactWebChat, {
40+
directLine,
41+
store,
42+
// Keeps the live region copy of new messages, so it can be seen in the snapshots.
43+
styleOptions: { hideSendBox: true, internalLiveRegionFadeAfter: 60000 },
44+
...props
45+
})
46+
);
47+
48+
// Role descriptions of the message and attachments, per activity.
49+
// Selects by role, not by the attribute, so an element missing its role description shows up as null.
50+
const getRoleDescriptions = () =>
51+
pageElements
52+
.activities()
53+
.map(activity =>
54+
Array.from(activity.querySelectorAll('[role="group"], [role="listitem"]'), element =>
55+
element.getAttribute('aria-roledescription')
56+
)
57+
);
58+
59+
// GIVEN: Web Chat in the default locale (en-US).
60+
render({});
61+
62+
await pageConditions.uiConnected();
63+
64+
// GIVEN: A message with text and 2 attachments in each attachment layout.
65+
for (const attachmentLayout of ['list', 'carousel']) {
66+
await directLine.emulateIncomingActivity({
67+
attachmentLayout,
68+
attachments: [1, 2].map(index => ({
69+
content: { title: `Card ${index} (${attachmentLayout})` },
70+
contentType: 'application/vnd.microsoft.card.hero'
71+
})),
72+
from: { role: 'bot' },
73+
text: `Message with ${attachmentLayout} attachments`,
74+
type: 'message'
75+
});
76+
}
77+
78+
await pageConditions.numActivitiesShown(2);
79+
await pageConditions.liveRegionStabilized();
80+
81+
// THEN: A stacked message, then a carousel message, each with its text and 2 cards.
82+
// THEN: The live region (top) shows what screen readers announce for each: "Bot said:", the text, and "A card:" per card, without role descriptions.
83+
await host.snapshot('local');
84+
85+
// THEN: Message and attachments are described in English.
86+
expect(getRoleDescriptions()).toEqual([
87+
['message', 'attachment', 'attachment'],
88+
['message', 'attachment', 'attachment']
89+
]);
90+
91+
// WHEN: The host switches to German and provides the strings (no translation in de-DE yet).
92+
render({
93+
locale: 'de-DE',
94+
overrideLocalizedStrings: {
95+
ACTIVITY_ATTACHMENT_ARIA_ROLE_ALT: 'Anhang',
96+
ACTIVITY_MESSAGE_ARIA_ROLE_ALT: 'Nachricht'
97+
}
98+
});
99+
100+
// THEN: Message and attachments are described in German.
101+
await waitFor(() =>
102+
expect(getRoleDescriptions()).toEqual([
103+
['Nachricht', 'Anhang', 'Anhang'],
104+
['Nachricht', 'Anhang', 'Anhang']
105+
])
106+
);
107+
108+
// WHEN: The bot sends another message.
109+
await directLine.emulateIncomingActivity({
110+
attachments: [{ content: { title: 'Card 3' }, contentType: 'application/vnd.microsoft.card.hero' }],
111+
from: { role: 'bot' },
112+
text: 'Message after switching to German',
113+
type: 'message'
114+
});
115+
116+
await pageConditions.numActivitiesShown(3);
117+
await pageConditions.liveRegionStabilized();
118+
119+
// 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.
120+
await host.snapshot('local');
121+
});
122+
</script>
123+
</body>
124+
</html>
30.6 KB
Loading
40.9 KB
Loading

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
"_ACTIVITY_YOU_SAID_ALT.comment": "This is for screen reader only.",
1313
"ACTIVITY_BOT_ATTACHED_ALT": "Bot attached:",
1414
"_ACTIVITY_BOT_ATTACHED_ALT.comment": "This is for screen reader and is narrated before each attachments sent by the bot.",
15+
"ACTIVITY_ATTACHMENT_ARIA_ROLE_ALT": "attachment",
16+
"_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.",
17+
"ACTIVITY_MESSAGE_ARIA_ROLE_ALT": "message",
18+
"_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.",
1519
"ACTIVITY_CODE_ALT": "Code sample: $1",
1620
"_ACTIVITY_CODE_ALT.comment": "This is for screen reader code sample dialog label, $1 is the dialog title.",
1721
"ACTIVITY_CONTENT_CAUTION": "AI-generated content may be incorrect",

‎packages/component/src/Activity/AttachmentRow.tsx‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
import { reactNode, validateProps } from '@msinternal/botframework-webchat-react-valibot';
1+
import { reactNode, validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot';
22
import { useStyles } from '@msinternal/botframework-webchat-styles/react';
3+
import { hooks } from 'botframework-webchat-api';
34
import cx from 'classnames';
45
import React, { memo } from 'react';
5-
import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot';
6+
import { boolean, object, optional, pipe, readonly, string } from 'valibot';
67

78
import ScreenReaderText from '../ScreenReaderText';
89
import Bubble from './Bubble';
910

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

13+
const { useLocalizer } = hooks;
14+
1215
const attachmentRowPropsSchema = pipe(
1316
object({
1417
attachedAlt: string(),
@@ -21,7 +24,7 @@ const attachmentRowPropsSchema = pipe(
2124
readonly()
2225
);
2326

24-
type AttachmentRowProps = InferInput<typeof attachmentRowPropsSchema>;
27+
type AttachmentRowProps = InferReactProps<typeof attachmentRowPropsSchema>;
2528

2629
function AttachmentRow(props: AttachmentRowProps) {
2730
const {
@@ -33,9 +36,14 @@ function AttachmentRow(props: AttachmentRowProps) {
3336
showBubble = true
3437
} = validateProps(attachmentRowPropsSchema, props);
3538
const classNames = useStyles(styles);
39+
const localize = useLocalizer();
3640

3741
return (
38-
<div aria-roledescription="attachment" className={classNames['stacked-layout__attachment-row']} role="group">
42+
<div
43+
aria-roledescription={localize('ACTIVITY_ATTACHMENT_ARIA_ROLE_ALT')}
44+
className={classNames['stacked-layout__attachment-row']}
45+
role="group"
46+
>
3947
<ScreenReaderText text={attachedAlt} />
4048
{showBubble ? (
4149
<Bubble

‎packages/component/src/Activity/CarouselFilmStrip.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,11 @@ const CarouselFilmStrip = ({
174174
</div>
175175
<div className={classNames['carousel-filmstrip__content']}>
176176
{!!activityDisplayText && (
177-
<div aria-roledescription="message" className={classNames['carousel-filmstrip__message']} role="group">
177+
<div
178+
aria-roledescription={localize('ACTIVITY_MESSAGE_ARIA_ROLE_ALT')}
179+
className={classNames['carousel-filmstrip__message']}
180+
role="group"
181+
>
178182
<ScreenReaderText text={greetingAlt} />
179183
<Bubble
180184
className={classNames['carousel-filmstrip__bubble']}

‎packages/component/src/Activity/CarouselFilmStripAttachment.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const CarouselFilmStripAttachment = ({
3030

3131
return (
3232
<li
33-
aria-roledescription="attachment"
33+
aria-roledescription={localize('ACTIVITY_ATTACHMENT_ARIA_ROLE_ALT')}
3434
className={cx(
3535
classNames['carousel-filmstrip-attachment'],
3636
{

‎packages/component/src/Activity/StackedLayout.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ const StackedLayoutInner = memo(
7272
return (
7373
<StackedLayoutMain avatar={showAvatar && renderAvatar && renderAvatar()}>
7474
{!!(hasDisplayText || messageThing?.abstract[0]) && (
75-
<div aria-roledescription="message" className={cx(classNames['stacked-layout__message-row'])} role="group">
75+
<div
76+
aria-roledescription={localize('ACTIVITY_MESSAGE_ARIA_ROLE_ALT')}
77+
className={cx(classNames['stacked-layout__message-row'])}
78+
role="group"
79+
>
7680
<ScreenReaderText text={greetingAlt} />
7781
<Bubble
7882
className={classNames['stacked-layout__message']}

0 commit comments

Comments
 (0)