Skip to content

Commit 07bfe74

Browse files
uzirthapaclaude
andcommitted
fix: Only use role="form" when card has original aria-label from speak
Cards without "speak" that have form inputs were getting role="form" from the derived aria-label, causing landmark-unique axe violations when the page also contains the send box <form>. Now only cards with an explicit "speak" property get role="form". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b8b9f02 commit 07bfe74

File tree

2 files changed

+12
-4
lines changed

2 files changed

+12
-4
lines changed

__tests__/html2/accessibility/adaptiveCard/hack.roleMod.ariaLabelFromTextContent.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@
8686
expect(cardWithSpeak.getAttribute('role')).toBe('figure');
8787

8888
// Card with form inputs and no speak: aria-label should be derived from text content,
89-
// and role should be "form" because it has inputs and an aria-label.
89+
// but role should be "figure" (not "form") to avoid duplicate form landmarks on the page.
9090
expect(cardFormNoSpeak.getAttribute('aria-label')).toBeTruthy();
91-
expect(cardFormNoSpeak.getAttribute('role')).toBe('form');
91+
expect(cardFormNoSpeak.getAttribute('role')).toBe('figure');
9292
});
9393
</script>
9494
</body>

packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardHacks/useRoleModEffect.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,14 @@ export default function useRoleModEffect(
3737
): readonly [(cardElement: HTMLElement) => void, () => void] {
3838
const modder = useMemo(
3939
() => (_, cardElement: HTMLElement) => {
40+
// Check if the card already has an aria-label from the "speak" property before we derive one.
41+
const hasOriginalAriaLabel = !!cardElement.getAttribute('aria-label');
42+
4043
// If the card doesn't have an aria-label (i.e. no "speak" property was set),
4144
// derive one from the card's visible text content so screen readers can announce it.
4245
let undoAriaLabel: (() => void) | undefined;
4346

44-
if (!cardElement.getAttribute('aria-label')) {
47+
if (!hasOriginalAriaLabel) {
4548
const textContent = (cardElement.textContent || '').replace(/\s+/gu, ' ').trim();
4649

4750
if (textContent) {
@@ -54,11 +57,16 @@ export default function useRoleModEffect(
5457
}
5558
}
5659

60+
// Only use role="form" when the card has an original aria-label (from "speak" property).
61+
// Derived aria-labels should use role="figure" to avoid duplicate form landmarks
62+
// when the page also contains the send box <form>.
5763
const undoRole = setOrRemoveAttributeIfFalseWithUndo(
5864
cardElement,
5965
'role',
6066
// "form" role requires either "aria-label", "aria-labelledby", or "title".
61-
(cardElement.querySelector('button, input, select, textarea') && cardElement.getAttribute('aria-label')) ||
67+
(cardElement.querySelector('button, input, select, textarea') &&
68+
hasOriginalAriaLabel &&
69+
cardElement.getAttribute('aria-label')) ||
6270
cardElement.getAttribute('aria-labelledby') ||
6371
cardElement.getAttribute('title')
6472
? 'form'

0 commit comments

Comments
 (0)