Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix accessibility issues in Adaptive Cards library #4335

Merged
merged 16 commits into from
Jul 20, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Fixes [#4295](https://github.com/microsoft/BotFramework-WebChat/issues/4295). Screen reader should not read suggested actions twice when message arrive in live region, by [@compulim](https://github.com/compulim), in PR [#4323](https://github.com/microsoft/BotFramework-WebChat/issues/4323)
- Fixes [#4325](https://github.com/microsoft/BotFramework-WebChat/issues/4325). `aria-keyshortcuts` should use modifier keys according to `KeyboardEvent` key values spec, by [@compulim](https://github.com/compulim), in PR [#4323](https://github.com/microsoft/BotFramework-WebChat/issues/4323)
- Fixes [#4327](https://github.com/microsoft/BotFramework-WebChat/issues/4327). In Adaptive Cards, `TextBlock` with `style="heading"` should have `aria-level` set, by [@compulim](https://github.com/compulim), in PR [#4329](https://github.com/microsoft/BotFramework-WebChat/issues/4329)
- Fixes [#3949](https://github.com/microsoft/BotFramework-WebChat/issues/3949). For accessibility reasons, buttons in Adaptive Cards should be `role="button"` instead of `role="menubar"`/`role="menuitem"`, by [@compulim](https://github.com/compulim), in PR [#4263](https://github.com/microsoft/BotFramework-WebChat/issues/4263)

## Changes

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
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.
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.
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.
63 changes: 0 additions & 63 deletions __tests__/adaptiveCards.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,69 +97,6 @@ test('breakfast card with custom style options', async () => {
expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});

test('disable card inputs', async () => {
const { driver, pageObjects } = await setupWebDriver();

await driver.wait(uiConnected(), timeouts.directLine);
await pageObjects.sendMessageViaSendBox('card inputs', { waitForSend: true });

await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
await driver.wait(allImagesLoaded(), timeouts.fetchImage);
await driver.wait(scrollToBottomCompleted(), timeouts.scrollToBottom);

await driver.executeScript(() => {
document.querySelector('.ac-adaptiveCard input[type="checkbox"]').checked = true;
document.querySelector('.ac-adaptiveCard input[type="date"]').value = '2019-11-01';
document.querySelector('.ac-adaptiveCard input[type="radio"]').checked = true;
document.querySelector('.ac-adaptiveCard input[type="text"]').value = 'William';
document.querySelector('.ac-adaptiveCard input[type="time"]').value = '12:34';
document.querySelector('.ac-adaptiveCard input[type="number"]').value = '1';
document.querySelector('.ac-adaptiveCard select').value = '1';
document.querySelector('.ac-adaptiveCard textarea').value = 'One Redmond Way, Redmond, WA';
});

expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions);

await pageObjects.updateProps({ disabled: true });
await driver.wait(scrollToBottomCompleted(), timeouts.scrollToBottom);

// Click "Submit" button should have no effect
await driver.executeScript(() => {
document.querySelector('.ac-actionSet button:nth-of-type(2)').click();
});

//@todo change to use scrollStabilizer after release
await new Promise(resolve => setTimeout(resolve, 1000));

expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions);

await pageObjects.updateProps({ disabled: false });

// Wait until render after update props
await driver.wait(
() =>
driver.executeScript(() => {
const button = document.querySelector('.ac-actionSet button:nth-of-type(2)');

return button && !button.disabled;
}),
timeouts.ui
);

// Click "Submit" button should send values to the bot
await driver.executeScript(() => {
document.querySelector('.ac-actionSet button:nth-of-type(2)').click();
});

//@todo change to use scrollStabilizer after release
await new Promise(resolve => setTimeout(resolve, 1000));

await driver.wait(minNumActivitiesShown(3), timeouts.directLine);
await driver.wait(scrollToBottomCompleted(), timeouts.scrollToBottom);

expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions);
});

test('broken card of invalid version', async () => {
const { driver, pageObjects } = await setupWebDriver();

Expand Down
68 changes: 11 additions & 57 deletions __tests__/html/accessibility.adaptiveCard.ariaPushed.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,26 +34,6 @@
return false;
}

/** Checks if the page is conform to a subset of WCAG. */
// TODO: We should use axe-core to validate WAI-ARIA conformity.
function expectWCAGConformity() {
// EXPECT: Conform to WAI-ARIA, all "menubar" should have 1 or more descendants of "menuitem".
[...document.querySelectorAll('[role="menubar"]')].forEach(menuBar =>
expect(menuBar.querySelectorAll('[role="menuitem"]').length).toBeTruthy()
);

// EXPECT: Conform to WAI-ARIA, all "menuitem" should be a descendant of "menu" or "menubar".
[...document.querySelectorAll('[role="menuitem"]')].forEach(menuItem => {
expect(
hasAncestor(menuItem, ancestor => {
const ancestorRole = ancestor.getAttribute('role');

return ancestorRole === 'menu' || ancestorRole === 'menubar';
})
).toBeTruthy();
});
}

run(async function () {
const directLine = await testHelpers.createDirectLineWithTranscript([
{
Expand Down Expand Up @@ -127,34 +107,20 @@
await pageConditions.numActivitiesShown(1);
await pageConditions.scrollToBottomCompleted();

// GIVEN: There should be 2 set of `ac-actionSet` containers.
expect(document.querySelectorAll('.ac-actionSet')).toHaveLength(2);
// SETUP: Focus on the send box.
await pageObjects.focusSendBoxTextBox();

// GIVEN: All `ac-actionSet` should have `[role="menubar"]`.
Array.from(document.querySelectorAll('.ac-actionSet')).every(actionSet =>
expect(actionSet.getAttribute('role')).toBe('menubar')
);
// SETUP: There should be 2 set of `ac-actionSet` containers.
expect(document.querySelectorAll('.ac-actionSet')).toHaveLength(2);

// GIVEN: All 'ac-pushButton' should not have "aria-pressed".
// SETUP: All 'ac-pushButton' should not have `aria-pressed="true"`.
Array.from(document.querySelectorAll('.ac-pushButton')).every(pushButton =>
expect(pushButton.hasAttribute('aria-pressed')).toBe(false)
expect(pushButton.hasAttribute('aria-pressed')).not.toBe('true')
);

// GIVEN: The page should conform to WCAG.
expectWCAGConformity();

// WHEN: Clicking on the card action button ("Submit card").
await host.click(
Array.from(document.getElementsByClassName('ac-pushButton')).find(
pushButton => pushButton.innerText === 'Submit card'
)
);

// THEN: The first `ac-actionSet` should have `[role="menubar"]` untouched.
expect(document.querySelectorAll('.ac-actionSet')[0].getAttribute('role')).toBe('menubar');

// THEN: The second `ac-actionSet` should have `[role="menubar"]` removed.
expect(document.querySelectorAll('.ac-actionSet')[1].hasAttribute('role')).toBe(false);
await host.sendShiftTab(3);
await host.sendKeys('ENTER', 'TAB', 'TAB', 'ENTER');

// THEN: Selected `ac-pushButton` should have `aria-pressed` set to `true`.
Array.from(document.querySelectorAll('.ac-pushButton'))
Expand All @@ -171,20 +137,11 @@
// THEN: Non-selection should not have `aria-pressed` set.
Array.from(document.querySelectorAll('.ac-pushButton'))
.filter(pushButton => pushButton.innerText !== 'Submit card')
.forEach(pushButton => expect(pushButton.hasAttribute('aria-pressed')).toBeFalsy());

// THEN: The page should conform to WCAG.
expectWCAGConformity();
.forEach(pushButton => expect(pushButton.getAttribute('aria-pressed')).not.toBe('true'));

// WHEN: Click on the first action set button ("Yes")
await host.click(
Array.from(document.getElementsByClassName('ac-pushButton')).find(
pushButton => pushButton.innerText === 'Yes'
)
);

// THEN: The first `ac-actionSet` should have `[role="menubar"]` removed.
expect(document.querySelectorAll('.ac-actionSet')[0].hasAttribute('role')).toBe(false);
await host.sendShiftTab(2);
await host.sendKeys('ENTER');

// THEN: Selected `ac-pushButton` should have `aria-pressed` set to `true`.
Array.from(document.querySelectorAll('.ac-pushButton'))
Expand All @@ -197,9 +154,6 @@
pushButton => pushButton.getAttribute('aria-pressed') === 'true'
)
).toHaveLength(2);

// THEN: The page should conform to WCAG.
expectWCAGConformity();
});
</script>
</body>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<!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>
<script crossorigin="anonymous" src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
</head>
<body>
<div id="webchat"></div>
<script>
run(async function () {
const {
Components: { AdaptiveCardContent }
} = window.WebChat;

const NOW = Date.now();

let numPostActivityCalled = 0;

const directLine = await testHelpers.createDirectLineWithTranscript(
[
{
from: {
id: 'bot',
role: 'bot'
},
id: '0',
text: 'What is your ZIP code? (optional) for US users only',
timestamp: new Date(NOW).toISOString(),
type: 'message'
},
{
attachments: [
{
contentType: 'application/vnd.microsoft.card.adaptive',
content: {
type: 'AdaptiveCard',
version: '1.0',
body: [
{
type: 'Container',
items: []
}
],
actions: [
{
type: 'Action.ShowCard',
card: {
type: 'AdaptiveCard',
body: [
{
type: 'Input.Text',
id: 'zipcode',
placeholder: 'Click here to enter a 5-digit (12345) or 9-digit (12345-1234) ZIP code'
}
],
actions: [
{
type: 'Action.Submit',
title: 'Submit',
text: 'size:extraLarge',
size: 'extraLarge'
}
]
},
title: 'Set ZIP code'
},
{
type: 'Action.Submit',
title: 'Skip',
text: 'size:extraLarge',
size: 'extraLarge'
}
]
}
}
],
from: {
id: 'bot',
role: 'bot'
},
id: '1',
timestamp: new Date(NOW + 1).toISOString(),
type: 'message'
}
],
{
overridePostActivity: () => {
numPostActivityCalled++;

return new Observable(() => {});
}
}
);

const createAttachmentMiddleware =
disabled =>
() =>
next =>
(...args) => {
const [{ attachment }] = args;

switch (attachment.contentType) {
case 'application/vnd.microsoft.card.adaptive':
return React.createElement(AdaptiveCardContent, {
actionPerformedClassName: 'card__action--performed',
content: attachment.content,
disabled
});

default:
return next(...args);
}
};

const props = {
directLine,
store: testHelpers.createStore()
};

function render(patchProps) {
WebChat.renderWebChat({ ...props, ...patchProps }, document.getElementById('webchat'));
}

render({ attachmentMiddleware: createAttachmentMiddleware(false) });

await pageConditions.uiConnected();
await pageConditions.numActivitiesShown(2);
await pageConditions.scrollToBottomCompleted();

// SETUP: Focus on the send box.
await pageObjects.focusSendBoxTextBox();

// WHEN: Mimick pressing "B" on screen reader, which will focus on the first `role="button"`.
await pageElements.transcript().querySelector('button:not([role]), [role="button"]').focus();

// WHEN: Pressing ENTER key on the "Set ZIP code" button.
await host.sendKeys('ENTER');

// THEN: It should expand the card and the "Submit" button should be shown.
expect(document.querySelector('button[title="Submit"]')).toBeTruthy();
await host.snapshot();

// WHEN: Pressing TAB key.
await host.sendKeys('TAB');

// THEN: It should focus on the first textbox (this is done by Adaptive Cards, they set tabindex="-1" on other buttons).
// We just verify whatever behavior they have.
expect(document.activeElement).toBe(document.querySelector('input'));

// WHEN: Pressing TAB key.
await host.sendKeys('TAB');

// THEN: It should focus on the "Submit" button.
expect(document.activeElement).toBe(document.querySelector('button[title="Submit"]'));

// WHEN: Pressing ENTER key while the focus is on the "Submit" button.
await host.sendKeys('ENTER');

// THEN: It should call postActivity().
expect(numPostActivityCalled).toBe(1);

// WHEN: The UI is disabled.
render({ attachmentMiddleware: createAttachmentMiddleware(true) });

// WHEN: Pressing TAB key.
await host.sendKeys('TAB');

// THEN: It should focus on the "Set ZIP code" button.
expect(document.activeElement).toBe(document.querySelector('button[title="Set ZIP code"]'));

// WHEN: Pressing TAB key again.
await host.sendKeys('TAB');

// THEN: It should still focus on the "Set ZIP code" button.
expect(document.activeElement).toBe(document.querySelector('button[title="Set ZIP code"]'));

// WHEN: Pressing ENTER key while to focus is on "Set ZIP code".
await host.sendKeys('ENTER');

// THEN: It should hide the attached panel.
expect(document.querySelector('button[title="Submit"]')).toBeFalsy();
await host.snapshot();

// WHEN: The UI is re-enabled.
render({ attachmentMiddleware: createAttachmentMiddleware(false) });

// WHEN: Pressing TAB key to focus on the "Skip" button and press ENTER key on it.
await host.sendKeys('TAB', 'ENTER');

// THEN: It should call postActivity().
expect(numPostActivityCalled).toBe(2);
});
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */

describe('accessibility requirement', () => {
test('disabling Adaptive Card with "Action.ShowCard"', () =>
runHTML('accessibility.adaptiveCard.disabled.focus.withShowCard.html'));
});
Loading