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.
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.
|
@microsoft-github-policy-service agree |
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 microsoft#4327 only. 3. AdaptiveCardBuilder.ts: add https:// prefix to the microsoft#4327 URL so tooling auto-links it.
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
| expect(titleTextBlock).toBeTruthy(); | ||
|
|
||
| expect(titleTextBlock.getAttribute('role')).toBe(null); | ||
| expect(heroCardActivity.querySelector('.ac-textBlock[role="heading"]')).toBe(null); |
There was a problem hiding this comment.
Suggestion: add both test one with visible styling for [role="heading"], and this one. Add snapshots for both so the change can be visually inspected.
OEvgeny
left a comment
There was a problem hiding this comment.
Looks good, waiting for the tests update
|
|
||
| ### Added | ||
|
|
||
| - Added `styleOptions.richCardTitleAsHeading` (default `true`) to opt out of `style: 'heading'` on rich card titles, by [@cjennison](https://github.com/cjennison). Resolves the conflict with [#4327](https://github.com/microsoft/BotFramework-WebChat/issues/4327) for hosts where card titles are not navigational headings. |
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).