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 @@ -63,6 +63,8 @@ Legends:
- 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 carousel cards focused with <kbd>TAB</kbd> staying partially out of view, in PR [#5926](https://github.com/microsoft/BotFramework-WebChat/pull/5926), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed Adaptive Cards without a label being announced as an unnamed figure by screen readers, fixes [#5049](https://github.com/microsoft/BotFramework-WebChat/issues/5049), in PR [#5915](https://github.com/microsoft/BotFramework-WebChat/pull/5915), by [@OEvgeny](https://github.com/OEvgeny)
- Fixed clickable containers and images in Adaptive Cards not showing a pointer cursor, fixes [#5130](https://github.com/microsoft/BotFramework-WebChat/issues/5130), in PR [#5915](https://github.com/microsoft/BotFramework-WebChat/pull/5915), by [@OEvgeny](https://github.com/OEvgeny)

## [4.19.2] - 2026-10-08

Expand Down
15 changes: 13 additions & 2 deletions __tests__/html2/accessibility/adaptiveCard/hack.roleMod.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<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" />
<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>
Expand All @@ -12,7 +14,11 @@
run(async function () {
const { directLine, store } = testHelpers.createDirectLineEmulator();

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

await pageConditions.uiConnected();

Expand All @@ -38,7 +44,7 @@
type: 'AdaptiveCard',
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
version: '1.5',
speak: 'Hello, World!',
speak: 'Please enter your full name',
body: [
{
type: 'Input.Text',
Expand Down Expand Up @@ -66,6 +72,11 @@

expect(secondAdaptiveCard.getAttribute('aria-label')).toBeTruthy();
expect(secondAdaptiveCard.getAttribute('role')).toBe('form');

await pageConditions.liveRegionStabilized();

// THEN: The live region shows what screen readers announce: "Bot said:", one line per card from its "speak" property ("A card: Hello, World!", "A card: Please enter your full name"), then the input of the second card.
await host.snapshot('local');
});
</script>
</body>
Expand Down
Comment thread
OEvgeny marked this conversation as resolved.
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" />
<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';

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

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

await pageConditions.uiConnected();

// WHEN: Bot sends 2 Adaptive Cards without "speak", so the card has no label: one without inputs and one with inputs.
await directLine.emulateIncomingActivity({
attachments: [
{
content: {
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
body: [{ text: 'Sunny, 21°C', type: 'TextBlock' }],
type: 'AdaptiveCard',
version: '1.5'
},
contentType: 'application/vnd.microsoft.card.adaptive'
},
{
content: {
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
actions: [{ title: 'Submit', type: 'Action.Submit' }],
body: [
{
id: 'name',
label: 'What is your full name',
placeholder: 'For example, John Doe', // The live region copy names inputs by their placeholder until #5913 (by their label).
type: 'Input.Text'
}
],
type: 'AdaptiveCard',
version: '1.5'
},
contentType: 'application/vnd.microsoft.card.adaptive'
}
],
type: 'message'
});

await pageConditions.numActivitiesShown(1);
await waitFor(() => expect(document.querySelectorAll('.ac-adaptiveCard')).toHaveLength(2));

const cardElements = Array.from(document.querySelectorAll('.ac-adaptiveCard'));

// THEN: Both cards should have no label.
expect(cardElements.map(cardElement => cardElement.getAttribute('aria-label'))).toEqual([null, null]);

// THEN: Both cards should have no role, as "figure" and "form" without a name are announced without their content.
expect(cardElements.map(cardElement => cardElement.getAttribute('role'))).toEqual([null, null]);

// THEN: Neither card should be a "figure" or a "form" in the accessibility tree.
expect(
(await Promise.all(cardElements.map(cardElement => testHelpers.getAccessibilityNode(cardElement)))).map(
({ role }) => role?.value
)
).toEqual(['generic', 'generic']);

await pageConditions.liveRegionStabilized();

// THEN: The live region shows what screen readers announce: "Bot said:", "A card:" for each card, then the input and the "Submit" button of the second card.
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
Expand Up @@ -29,6 +29,9 @@ import type { AdaptiveCard } from 'adaptivecards';
* This violates WAI-ARIA because "aria-label" must not be set on an element without a "role".
*
* We need to set "role" attribute to "form" if the card has any input fields and is valid as a "form" role, otherwise, "figure".
*
* Without a label, the card gets no "role": "figure" and "form" take their names from the author only,
* so a nameless "figure" or "form" would be announced without its content.
*/
export default function useRoleModEffect(
adaptiveCard: AdaptiveCard
Expand All @@ -43,7 +46,9 @@ export default function useRoleModEffect(
cardElement.getAttribute('aria-labelledby') ||
cardElement.getAttribute('title')
? 'form'
: 'figure'
: cardElement.getAttribute('aria-label')
? 'figure'
: false
),
[]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
padding: var(--webchat__padding--regular);
}

/* Elements with "selectAction", such as containers and images, are clickable. Push buttons keep the default cursor of buttons. */
:global(.ac-selectable:not(.ac-pushButton)) {
cursor: pointer;
}

:global(.ac-pushButton) {
appearance: none;
background-color: white;
Expand Down
Loading