Skip to content

Commit 1431fe4

Browse files
cjennisonChristopher JennisonOEvgenycompulim
authored
feat(adaptive-cards): add richCardTitleAsHeading styleOption to opt out of role=heading on rich card titles (#5839)
* feat(adaptive-cards): add richCardTitleAsHeading styleOption to opt out of role=heading on rich card titles Today the title of hero/thumbnail/audio/video/animation/receipt cards is rendered with Adaptive Cards style: 'heading', which the Adaptive Cards SDK exposes as role='heading' + aria-level. This was originally requested in issue #4327 and shipped in 4.15.3. Subsequent a11y audits (e.g. for hosts where these cards appear inside a chat transcript) flag the same heading as 'Unnecessary heading level is programmatically defined for Title' under MAS 1.3.1 / WCAG 1.3.1, because card titles inside a chat are not page-level headings and break document outline tools. Reconcile the two by making the behavior configurable via styleOptions.richCardTitleAsHeading. Default is true so existing consumers (including the original #4327 reporter) keep today's behavior; consumers can pass false to drop the heading style. Adds a sibling test heroCard.noHeading.html to the existing heroCard.heading.html that asserts no .ac-textBlock[role='heading'] is rendered when richCardTitleAsHeading is false. * address review comments from automated reviewer 1. heroCard.noHeading.html: scope queries to the hero card activity container instead of querying the whole document. Match the title text block by its expected text so future text blocks elsewhere on the page do not make the test flaky. 2. AdaptiveCardsStyleOptions.ts: drop the incomplete 'reverse request' bullet that had no link; keep the @see link to #4327 only. 3. AdaptiveCardBuilder.ts: add https:// prefix to the #4327 URL so tooling auto-links it. * code-review: drop 'as const', narrow doc comment to actual code paths 1. AdaptiveCardBuilder.ts: drop 'as const' on the conditional 'style: heading'. AGENTS.md says 'Avoid as'; the original code wrote 'style: heading' without any cast because addTextBlock takes Partial<TextBlock>, and TextBlock.style accepts string. Same here. 2. AdaptiveCardsStyleOptions.ts: tighten the doc-comment to match the actual call graph instead of listing card types. Cards that flow through addCommonHeaders today: - hero (via addCommon) - OAuth (direct) - thumbnail no-image branch (via addCommon) - animation/audio/video (via CommonCard -> addCommon) Cards that DON'T (their titles use direct addTextBlock w/o style:heading): - receipt - thumbnail with images - signin * trim verbose comments and changelog entry to match repo style * address review: snapshot both heroCard heading tests + fix changelog format - heroCard.noHeading.html: add host.snapshot('local') and wait for allImagesLoaded so the captured snapshot is stable. - heroCard.heading.html: bring the default-styling test to parity with the noHeading test - scope queries to the hero card activity, assert role='heading' on the title text block, and add host.snapshot('local') so both behaviors can be visually compared (per OEvgeny). - CHANGELOG.md: rewrite the entry in the repo's standard '<verb> <desc>, in PR [#NNNN](url), by [@author](url)' single-line form (per compulim). * test: commit image snapshot baselines for heroCard heading/noHeading Generated by running the docker compose / selenium / jest stack from a clean WSL Ubuntu build: npm install && npm run build docker compose -f docker-compose-wsl2.yml up --detach --scale chrome=2 ./node_modules/.bin/jest --ci=false --forceExit --runInBand --testPathPattern=heroCard -u Both snapshots are visually identical (as expected - the only difference is the role='heading' attribute on the title text block, which has no visual styling). The behavioural difference is asserted via the role assertions in each test's JS. * test: visually highlight [role=heading] in heroCard snapshots Per OEvgeny: add custom CSS so the role='heading' attribute is visually visible in image snapshots. Adds a small heading-indicator.css (yellow background + dashed red outline on any [role='heading'] element, following the focus-indicator.css pattern), linked from both heroCard.heading.html and heroCard.noHeading.html. Result: heroCard.heading.html.snap-1.png now shows the highlighted title; heroCard.noHeading.html.snap-1.png stays unhighlighted, so reviewers can visually confirm the role=heading semantics from the snapshots alone. * Reverse styleOptions so default is falsy --------- Co-authored-by: Christopher Jennison <cjennison@microsoft.com> Co-authored-by: Eugene <EOlonov@gmail.com> Co-authored-by: William Wong <compulim@users.noreply.github.com>
1 parent 1ffc7bc commit 1431fe4

9 files changed

Lines changed: 79 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ Legends:
1919

2020
## [Unreleased]
2121

22+
### Added
23+
24+
- Added `styleOptions.richCardTitleOmitHeadingRole` (default `false`) to opt out of `style: 'heading'` on rich card titles, in PR [#5839](https://github.com/microsoft/BotFramework-WebChat/pull/5839), by [@cjennison](https://github.com/cjennison)
25+
26+
2227
## [4.19.1] - 2026-06-09
2328

2429
### Changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[role='heading'] {
2+
background-color: yellow !important;
3+
outline: dashed 2px Red !important;
4+
}

__tests__/html2/accessibility/attachment/heroCard.heading.html

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<html lang="en-US">
33
<head>
44
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
5+
<link href="heading-indicator.css" rel="stylesheet" type="text/css" />
56
<script crossorigin="anonymous" src="/test-harness.js"></script>
67
<script crossorigin="anonymous" src="/test-page-object.js"></script>
78
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
@@ -21,12 +22,21 @@
2122

2223
await pageObjects.sendMessageViaSendBox('herocard', { waitForSend: true });
2324
await pageConditions.minNumActivitiesShown(2);
25+
await pageConditions.allImagesLoaded();
2426
await pageConditions.scrollToBottomCompleted();
2527

26-
expect(document.querySelector('.ac-textBlock[role="heading"]')).toHaveProperty(
27-
'innerText',
28-
'\u200BDetails about image 1\u200B'
28+
const heroCardActivity = Array.from(webChatElement.querySelectorAll('.webchat__basic-transcript__activity')).find(
29+
(activity) => !!activity.querySelector('.ac-textBlock')
2930
);
31+
expect(heroCardActivity).toBeTruthy();
32+
33+
const titleTextBlock = Array.from(heroCardActivity.querySelectorAll('.ac-textBlock')).find(
34+
(block) => block.innerText === '\u200BDetails about image 1\u200B'
35+
);
36+
expect(titleTextBlock).toBeTruthy();
37+
expect(titleTextBlock.getAttribute('role')).toBe('heading');
38+
39+
await host.snapshot('local');
3040
});
3141
</script>
3242
</body>
70.1 KB
Loading
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<!DOCTYPE html>
2+
<html lang="en-US">
3+
<head>
4+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
5+
<link href="heading-indicator.css" rel="stylesheet" type="text/css" />
6+
<script crossorigin="anonymous" src="/test-harness.js"></script>
7+
<script crossorigin="anonymous" src="/test-page-object.js"></script>
8+
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
9+
</head>
10+
<body>
11+
<main id="webchat"></main>
12+
<script>
13+
run(async function () {
14+
const store = testHelpers.createStore();
15+
const directLine = WebChat.createDirectLine({ token: await testHelpers.token.fetchDirectLineToken() });
16+
const styleOptions = { richCardTitleOmitHeadingRole: true };
17+
const baseProps = { directLine, store, styleOptions };
18+
const webChatElement = document.getElementById('webchat');
19+
20+
WebChat.renderWebChat(baseProps, webChatElement);
21+
22+
await pageConditions.uiConnected();
23+
24+
await pageObjects.sendMessageViaSendBox('herocard', { waitForSend: true });
25+
await pageConditions.minNumActivitiesShown(2);
26+
await pageConditions.allImagesLoaded();
27+
await pageConditions.scrollToBottomCompleted();
28+
29+
const heroCardActivity = Array.from(webChatElement.querySelectorAll('.webchat__basic-transcript__activity')).find(
30+
(activity) => !!activity.querySelector('.ac-textBlock')
31+
);
32+
expect(heroCardActivity).toBeTruthy();
33+
34+
const titleTextBlock = Array.from(heroCardActivity.querySelectorAll('.ac-textBlock')).find(
35+
(block) => block.innerText === '\u200BDetails about image 1\u200B'
36+
);
37+
expect(titleTextBlock).toBeTruthy();
38+
39+
expect(titleTextBlock.getAttribute('role')).toBe(null);
40+
expect(heroCardActivity.querySelector('.ac-textBlock[role="heading"]')).toBe(null);
41+
42+
await host.snapshot('local');
43+
});
44+
</script>
45+
</body>
46+
</html>
69.4 KB
Loading

packages/bundle/src/adaptiveCards/AdaptiveCardsStyleOptions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ type StrictAdaptiveCardsStyleOptions = {
2525
* Enable title (and subtitle) wrapping
2626
*/
2727
richCardWrapTitle: boolean | undefined;
28+
29+
/**
30+
* Cards: Rich Cards
31+
* When `true`, omits `style: 'heading'` from the title so the rendered TextBlock has no
32+
* `role="heading"` / `aria-level`. Defaults to `false` (existing behavior keeps the heading
33+
* style; see issue #4327).
34+
*/
35+
richCardTitleOmitHeadingRole: boolean | undefined;
2836
};
2937

3038
type AdaptiveCardsStyleOptions = Partial<StrictAdaptiveCardsStyleOptions>;

packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardBuilder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,11 @@ export default class AdaptiveCardBuilder {
119119
}
120120

121121
addCommonHeaders(content: ICommonContent) {
122-
const { richCardWrapTitle } = this.styleOptions;
122+
const { richCardTitleOmitHeadingRole, richCardWrapTitle } = this.styleOptions;
123123
this.addTextBlock(content.title, {
124+
...(richCardTitleOmitHeadingRole ? {} : { style: 'heading' }),
124125
color: TextColor.Default,
125126
size: TextSize.Medium,
126-
style: 'heading',
127127
weight: TextWeight.Bolder,
128128
wrap: richCardWrapTitle
129129
});

packages/bundle/src/adaptiveCards/defaultStyleOptions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const ADAPTIVE_CARDS_DEFAULT_STYLE_OPTIONS: Required<AdaptiveCardsStyleOptions>
55
cardEmphasisBackgroundColor: '#F9F9F9',
66
cardPushButtonBackgroundColor: '#0063B1',
77
cardPushButtonTextColor: 'White',
8+
richCardTitleOmitHeadingRole: false,
89
richCardWrapTitle: false
910
};
1011

0 commit comments

Comments
 (0)