feat(adaptive-cards): add richCardTitleAsHeading styleOption to opt out of role=heading on rich card titles#5839
Conversation
…ut 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 microsoft#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 microsoft#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.
|
@cjennison please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.
Contributor License AgreementContribution License AgreementThis Contribution License Agreement (“Agreement”) is agreed to by the party signing below (“You”),
|
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR introduces a new Adaptive Cards style option to control whether rich card titles are rendered as programmatic headings for accessibility, defaulting to the historical behavior.
Changes:
- Added
styleOptions.richCardTitleAsHeading(defaulttrue) and documented it in the style options type. - Updated rich card header rendering to optionally omit Adaptive Cards
style: 'heading'. - Added an accessibility HTML test covering the “no heading” configuration and updated the changelog.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/bundle/src/adaptiveCards/defaultStyleOptions.ts | Adds a default value for the new richCardTitleAsHeading option. |
| packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardBuilder.ts | Conditionally applies style: 'heading' to rich card titles based on the new option. |
| packages/bundle/src/adaptiveCards/AdaptiveCardsStyleOptions.ts | Documents and exposes the new style option in the public style options type. |
| tests/html2/accessibility/attachment/heroCard.noHeading.html | Adds an accessibility regression test ensuring no heading role is applied when opted out. |
| CHANGELOG.md | Announces the newly added style option. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // The hero card title text block should be present... | ||
| const titleTextBlock = document.querySelector('.ac-textBlock'); | ||
| expect(titleTextBlock).toHaveProperty('innerText', '\u200BDetails about image 1\u200B'); | ||
|
|
||
| // ...but it must not be a programmatic heading when richCardTitleAsHeading is false. | ||
| expect(document.querySelector('.ac-textBlock[role="heading"]')).toBe(null); |
| * - Original request to add the heading: https://github.com/microsoft/BotFramework-WebChat/issues/4327 | ||
| * - Reverse request to make it removable: (the audit motivating this option) | ||
| */ | ||
| richCardTitleAsHeading: boolean | undefined; |
| addCommonHeaders(content: ICommonContent) { | ||
| const { richCardWrapTitle } = this.styleOptions; | ||
| const { richCardTitleAsHeading, richCardWrapTitle } = this.styleOptions; | ||
| // Default to `true` to preserve historical behavior (see github.com/microsoft/BotFramework-WebChat/issues/4327). |
Summary
Make
role="heading"/aria-levelon hero/thumbnail/audio/video/animation/receipt card titles opt-out via a newstyleOptions.richCardTitleAsHeading(defaulttrue, preserves today's behavior).Why
Today
AdaptiveCardBuilder.addCommonHeaders()hardcodes Adaptive Cardsstyle: 'heading'on the card title TextBlock. The Adaptive Cards SDK then renders that asrole="heading"+aria-level.This was originally requested in issue #4327 (
Title in Hero Card does not havearia-levelspecified) and shipped in 4.15.3.However, downstream a11y audits — particularly for hosts where these cards appear inside a chat transcript (e.g. Microsoft Copilot Studio Test Chat) — 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 pollute the document outline that assistive tech relies on.The two requirements are mutually exclusive and both came from accessibility audits. The right fix is to make the host control it.
Behavior
styleOptions.richCardTitleAsHeadingtrue(default — unchanged from today)<div role="heading" aria-level="..." class="ac-textBlock">…</div>(per #4327)false<div class="ac-textBlock">…</div>(no programmatic heading)No breaking change — existing consumers keep today's output without action.
Test
Adds a sibling HTML test
__tests__/html2/accessibility/attachment/heroCard.noHeading.htmlthat mirrors the existingheroCard.heading.html, passesstyleOptions = { richCardTitleAsHeading: false }, and assertsdocument.querySelector('.ac-textBlock[role="heading"]')isnull.Changelog
Added under
[Unreleased]›Added.Notes
Required<AdaptiveCardsStyleOptions>updated in defaults.style: 'heading').addCommonHeadersis gated — that covers hero/thumbnail/audio/video/animation/receipt cards (every type that flows throughaddCommon).