Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .changelog/5929-fix-liveregion-card-text-format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- Fixed screen readers reading Adaptive Card text with its Markdown and `{{DATE()}}` syntax, in PR [#5929](https://github.com/microsoft/BotFramework-WebChat/pull/5929), by [@OEvgeny](https://github.com/OEvgeny)
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<!-- Shows the live region, so the snapshot shows 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": {
"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 { 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();

// GIVEN: Web Chat keeps the live region copy of new messages, so it can be checked and seen in the snapshot.
createRoot(document.getElementById('webchat')).render(
createElement(ReactWebChat, {
directLine,
store,
styleOptions: { hideSendBox: true, internalLiveRegionFadeAfter: 60000 }
})
);

await pageConditions.uiConnected();

// WHEN: The bot sends an Adaptive Card with date and time functions, and without "speak".
await directLine.emulateIncomingActivity({
attachments: [
{
content: {
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
body: [
{
text: 'Arrives {{DATE(2026-10-20T15:00:00Z, SHORT)}} at {{TIME(2026-10-20T15:00:00Z)}}',
type: 'TextBlock',
wrap: true
}
],
type: 'AdaptiveCard',
version: '1.5'
},
contentType: 'application/vnd.microsoft.card.adaptive'
}
],
from: { role: 'bot' },
type: 'message'
});

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

// Adaptive Cards formats the date and time with the browser locale and time zone, so read them from the card as shown.
const cardText = pageElements.activities()[0].querySelector('.ac-textBlock').textContent.trim();

// THEN: The card shows the formatted date and time, not the functions.
expect(cardText).toEqual(expect.stringMatching(/^Arrives \S.* at \S/u));
expect(cardText).not.toContain('{{');

// THEN: The live region copy reads the same formatted date and time as the card.
expect(
testHelpers
.getAllTextContents(pageElements.liveRegion().querySelector('.webchat__live-region-activity'))
.map(text => text.trim())
).toEqual(['Bot said:', `A card: ${cardText}`]);

// THEN: The card and the live region above show the same formatted date and time.
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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<!-- Shows the live region, so the snapshot shows 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();

// GIVEN: Web Chat keeps the live region copy of new messages, so it can be checked and seen in the snapshot.
createRoot(document.getElementById('webchat')).render(
createElement(ReactWebChat, {
directLine,
store,
styleOptions: { hideSendBox: true, internalLiveRegionFadeAfter: 60000 }
})
);

await pageConditions.uiConnected();

// WHEN: The bot sends an Adaptive Card with an image in Markdown and HTML blocks without whitespace between them, and without "speak".
await directLine.emulateIncomingActivity({
attachments: [
{
content: {
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
body: [
{
text: '![Bot avatar](/assets/bot-avatar.jpg) Weather today<div>Sunny</div><p>Umbrella: no</p>',
type: 'TextBlock',
wrap: true
}
],
type: 'AdaptiveCard',
version: '1.5'
},
contentType: 'application/vnd.microsoft.card.adaptive'
}
],
from: { role: 'bot' },
type: 'message'
});

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

const textBlock = pageElements.activities()[0].querySelector('.ac-textBlock');

// THEN: The card shows the image with its alt text, and each block on its own line.
await waitFor(() => expect(textBlock.querySelector('img[alt="Bot avatar"]').complete).toBe(true));
expect(textBlock.innerText.trim().split(/\s*\n\s*/u)).toEqual(['Weather today', 'Sunny', 'Umbrella: no']);

// THEN: The live region copy reads the image alt text, and separates the blocks.
expect(
testHelpers
.getAllTextContents(pageElements.liveRegion().querySelector('.webchat__live-region-activity'))
.map(text => text.trim())
).toEqual(['Bot said:', 'A card: Bot avatar Weather today Sunny Umbrella: no']);

// THEN: The card shows the image and three lines; the live region above reads the alt text and the three lines.
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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<!-- Shows the live region, so the snapshot shows 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": {
"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 { 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();

// GIVEN: Web Chat keeps the live region copy of new messages, so it can be checked and seen in the snapshot.
createRoot(document.getElementById('webchat')).render(
createElement(ReactWebChat, {
directLine,
store,
styleOptions: { hideSendBox: true, internalLiveRegionFadeAfter: 60000 }
})
);

await pageConditions.uiConnected();

// WHEN: The bot sends an Adaptive Card with a Markdown text block (bold, a link, and a bullet list), and without "speak".
await directLine.emulateIncomingActivity({
attachments: [
{
content: {
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
body: [
{
text: 'Today is **sunny**, see [docs](https://example.com).\n\n- Umbrella: no\n- Sunscreen: yes',
type: 'TextBlock',
wrap: true
}
],
type: 'AdaptiveCard',
version: '1.5'
},
contentType: 'application/vnd.microsoft.card.adaptive'
}
],
from: { role: 'bot' },
type: 'message'
});

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

// The card shows the list items on separate lines, the live region reads them as one line.
const cardText = pageElements
.activities()[0]
.querySelector('.ac-textBlock')
.textContent.replace(/\s+/gu, ' ')
.trim();

// THEN: The card shows the text without Markdown syntax ("**", "[", "](", "- ").
expect(cardText).toEqual(
expect.stringMatching(/^Today is sunny, see .*docs.*\. Umbrella: no Sunscreen: yes$/u)
);

// THEN: The live region copy reads the same text as the card.
expect(
testHelpers
.getAllTextContents(pageElements.liveRegion().querySelector('.webchat__live-region-activity'))
.map(text => text.trim())
).toEqual(['Bot said:', `A card: ${cardText}`]);

// THEN: The card shows bold "sunny", a "docs" link and a bullet list; the live region above shows the same words without Markdown syntax.
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.
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import type {
AdaptiveCard,
GlobalSettings as GlobalSettingsType,
HostConfig as HostConfigType,
IMarkdownProcessingResult
} from 'adaptivecards';
import type { AdaptiveCard, GlobalSettings as GlobalSettingsType, HostConfig as HostConfigType } from 'adaptivecards';

import setMarkdownEngine from './setMarkdownEngine';

/**
* Returns `true`, if the object is a plain object and not a class, otherwise, `false`.
Expand All @@ -29,19 +26,7 @@ export default function renderAdaptiveCard(
setTabIndexAtCardRoot: boolean;
}
): { element: HTMLElement } | { errors: Error[] } {
// Currently, the only way to set the Markdown engine is to set it thru static member of AdaptiveCard class

// TODO: [P3] Checks if we could make the "renderMarkdownAsHTML" per card
// This could be limitations from Adaptive Cards package (not supported as of 1.2.5)
// Because there could be timing difference between .parse and .render, we could be using wrong Markdown engine

// "onProcessMarkdown" is a static function but we are trying to scope it to the current object instead.
adaptiveCard.constructor['onProcessMarkdown'] = (text: string, result: IMarkdownProcessingResult) => {
if (renderMarkdownAsHTML) {
result.outputHtml = renderMarkdownAsHTML(text);
result.didProcess = true;
}
};
setMarkdownEngine(adaptiveCard, renderMarkdownAsHTML);

if (adaptiveCardsHostConfig) {
adaptiveCard.hostConfig = isPlainObject(adaptiveCardsHostConfig)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { AdaptiveCard, IMarkdownProcessingResult } from 'adaptivecards';

export default function setMarkdownEngine(
adaptiveCard: AdaptiveCard,
renderMarkdownAsHTML: ((markdown: string) => string) | undefined
): void {
// Currently, the only way to set the Markdown engine is to set it thru static member of AdaptiveCard class

// TODO: [P3] Checks if we could make the "renderMarkdownAsHTML" per card
// This could be limitations from Adaptive Cards package (not supported as of 1.2.5)
// Because there could be timing difference between .parse and .render, we could be using wrong Markdown engine

// "onProcessMarkdown" is a static function but we are trying to scope it to the current object instead.
adaptiveCard.constructor['onProcessMarkdown'] = (text: string, result: IMarkdownProcessingResult) => {
if (renderMarkdownAsHTML) {
result.outputHtml = renderMarkdownAsHTML(text);
result.didProcess = true;
}
};
}
Loading
Loading