diff --git a/CHANGELOG.md b/CHANGELOG.md index 8492bc4485..d8f67a8dc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -363,6 +363,8 @@ Breaking changes in this release: ### Fixed +- Fixed screen reader (Narrator/NVDA) not announcing Adaptive Card content in stacked layout, by [@uzirthapa](https://github.com/uzirthapa) + - Adaptive Cards without `speak` property now derive `aria-label` from visible text content so screen readers can announce card content - Fixed [#5256](https://github.com/microsoft/BotFramework-WebChat/issues/5256). `styleOptions.maxMessageLength` should support any JavaScript number value including `Infinity`, by [@compulim](https://github.com/compulim), in PR [#5255](https://github.com/microsoft/BotFramework-WebChat/issues/pull/5255) - Fixes [#4965](https://github.com/microsoft/BotFramework-WebChat/issues/4965). Removed keyboard helper screen in [#5234](https://github.com/microsoft/BotFramework-WebChat/pull/5234), by [@amirmursal](https://github.com/amirmursal) and [@OEvgeny](https://github.com/OEvgeny) - Fixes [#5268](https://github.com/microsoft/BotFramework-WebChat/issues/5268). Concluded livestream is sealed and activities received afterwards are ignored, and `streamSequence` is not required in final activity, in PR [#5273](https://github.com/microsoft/BotFramework-WebChat/pull/5273), by [@compulim](https://github.com/compulim) diff --git a/__tests__/html2/accessibility/adaptiveCard/attachmentRow.focusable.html b/__tests__/html2/accessibility/adaptiveCard/attachmentRow.focusable.html new file mode 100644 index 0000000000..8580f3f187 --- /dev/null +++ b/__tests__/html2/accessibility/adaptiveCard/attachmentRow.focusable.html @@ -0,0 +1,78 @@ + + + + + + + + + +
+ + + diff --git a/__tests__/html2/accessibility/adaptiveCard/hack.roleMod.ariaLabelFromTextContent.html b/__tests__/html2/accessibility/adaptiveCard/hack.roleMod.ariaLabelFromTextContent.html new file mode 100644 index 0000000000..866da6a4a0 --- /dev/null +++ b/__tests__/html2/accessibility/adaptiveCard/hack.roleMod.ariaLabelFromTextContent.html @@ -0,0 +1,95 @@ + + + + + + + + + +
+ + + diff --git a/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardHacks/useRoleModEffect.ts b/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardHacks/useRoleModEffect.ts index 2c84be189d..5288d32524 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardHacks/useRoleModEffect.ts +++ b/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardHacks/useRoleModEffect.ts @@ -34,17 +34,43 @@ export default function useRoleModEffect( adaptiveCard: AdaptiveCard ): readonly [(cardElement: HTMLElement) => void, () => void] { const modder = useMemo( - () => (_, cardElement: HTMLElement) => - setOrRemoveAttributeIfFalseWithUndo( + () => (_, cardElement: HTMLElement) => { + // Check if the card already has an aria-label from the "speak" property before we derive one. + const hasOriginalAriaLabel = !!cardElement.getAttribute('aria-label'); + + // If the card doesn't have an aria-label (i.e. no "speak" property was set), + // derive one from the card's text content so screen readers can announce it. + let undoAriaLabel: (() => void) | undefined; + + if (!hasOriginalAriaLabel) { + const textContent = (cardElement.textContent || '').replace(/\s+/gu, ' ').trim(); + + if (textContent) { + undoAriaLabel = setOrRemoveAttributeIfFalseWithUndo(cardElement, 'aria-label', textContent); + } + } + + // Only use role="form" when the card has an original aria-label (from "speak" property). + // Derived aria-labels should use role="figure" to avoid duplicate form landmarks + // when the page also contains the send box
. + const undoRole = setOrRemoveAttributeIfFalseWithUndo( cardElement, 'role', // "form" role requires either "aria-label", "aria-labelledby", or "title". - (cardElement.querySelector('button, input, select, textarea') && cardElement.getAttribute('aria-label')) || + (cardElement.querySelector('button, input, select, textarea') && + hasOriginalAriaLabel && + cardElement.getAttribute('aria-label')) || cardElement.getAttribute('aria-labelledby') || cardElement.getAttribute('title') ? 'form' : 'figure' - ), + ); + + return () => { + undoRole(); + undoAriaLabel?.(); + }; + }, [] );