diff --git a/CHANGELOG.md b/CHANGELOG.md index 152d426838..5b956d1c5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ Legends: - Fixed screen readers announcing a single audio, video, or animation in a card as a list item, such as "1 of 1", fixes [#5722](https://github.com/microsoft/BotFramework-WebChat/issues/5722), in PR [#5903](https://github.com/microsoft/BotFramework-WebChat/pull/5903), by [@OEvgeny](https://github.com/OEvgeny) - Fixed video and animation cards disappearing in production builds when the video URL is relative or invalid, in PR [#5889](https://github.com/microsoft/BotFramework-WebChat/pull/5889), by [@OEvgeny](https://github.com/OEvgeny) - Fixed timestamps always using the 12-hour clock, such as "4:41 PM" instead of "16:41" in German, in PR [#5920](https://github.com/microsoft/BotFramework-WebChat/pull/5920), by [@OEvgeny](https://github.com/OEvgeny) +- Fixed screen readers saying "article" and "group" before new messages and each of their lines in the live region, in PR [#5921](https://github.com/microsoft/BotFramework-WebChat/pull/5921), by [@OEvgeny](https://github.com/OEvgeny) ## [4.19.2] - 2026-10-08 diff --git a/__tests__/html2/accessibility/liveRegion/noRoleWords.html b/__tests__/html2/accessibility/liveRegion/noRoleWords.html new file mode 100644 index 0000000000..7e477f8f52 --- /dev/null +++ b/__tests__/html2/accessibility/liveRegion/noRoleWords.html @@ -0,0 +1,117 @@ + + + + + + + + + + + +
+ + + + diff --git a/__tests__/html2/accessibility/liveRegion/noRoleWords.html.snap-1.png b/__tests__/html2/accessibility/liveRegion/noRoleWords.html.snap-1.png new file mode 100644 index 0000000000..4b7ff55dd8 Binary files /dev/null and b/__tests__/html2/accessibility/liveRegion/noRoleWords.html.snap-1.png differ diff --git a/docs/ACCESSIBILITY.md b/docs/ACCESSIBILITY.md index 3f2db34d0b..3b89e2528d 100644 --- a/docs/ACCESSIBILITY.md +++ b/docs/ACCESSIBILITY.md @@ -279,6 +279,7 @@ To make the live region more consistent across browsers and easier to control, w - Screen reader only reads the chat history marked as live region - Web Chat does not narrate attachment contents: attachments can be customized and the DOM tree could be very complex with interactive elements - Empty messages will be narrated if they have suggested actions + - Lines and containers in the copy are `role="none"`, otherwise screen readers on Windows read "group" before every line; custom `attachmentForScreenReaderMiddleware` should do the same - The live region contains activities that were recently - When the DOM element appear in the live region, the screen reader will compute the alternative text and queue it for narration in a first-come-first-serve manner - The screen reader will keep the alternative text in the queue even after the DOM element is removed from the live region diff --git a/packages/bundle/src/adaptiveCards/AttachmentForScreenReader/AdaptiveCardAttachment.js b/packages/bundle/src/adaptiveCards/AttachmentForScreenReader/AdaptiveCardAttachment.js index 8b38b0f1f0..c1d4395ebd 100644 --- a/packages/bundle/src/adaptiveCards/AttachmentForScreenReader/AdaptiveCardAttachment.js +++ b/packages/bundle/src/adaptiveCards/AttachmentForScreenReader/AdaptiveCardAttachment.js @@ -32,7 +32,7 @@ const AdaptiveCardChoiceSetInput = ({ input: { choices, defaultValue, label } }) const defaultChoice = choices.find(({ value }) => defaultValue === value || (!defaultValue && !value)); return ( -
+
) : input instanceof OpenUrlAction || input instanceof ShowCardAction || input instanceof SubmitAction ? ( -
+
@@ -157,7 +157,7 @@ const AdaptiveCardAttachment = ({ content }) => { false ) )} - +
); }; diff --git a/packages/bundle/src/adaptiveCards/AttachmentForScreenReader/RichCardAttachment.js b/packages/bundle/src/adaptiveCards/AttachmentForScreenReader/RichCardAttachment.js index 582e6430b5..16e6b5db0c 100644 --- a/packages/bundle/src/adaptiveCards/AttachmentForScreenReader/RichCardAttachment.js +++ b/packages/bundle/src/adaptiveCards/AttachmentForScreenReader/RichCardAttachment.js @@ -16,8 +16,8 @@ const RichCardAttachment = ({ content = {} }) => { const cardLabel = localize('ATTACHMENT_CARD', title || '', subtitle || '', text || ''); return ( -
-
{cardLabel}
+
+
{cardLabel}
{!!image && !!image.alt && {image.alt}} {!!images && !!images.length && images.map(({ alt }, index) => {alt})} {!!facts && !!facts.length && ( @@ -35,32 +35,32 @@ const RichCardAttachment = ({ content = {} }) => { {items.map(({ image, price, quantity, subtitle, text, title }, index) => (
  • {!!image && !!image.alt && {image.alt}} - {!!title &&

    {title}

    } - {!!subtitle &&

    {subtitle}

    } - {!!text &&

    {text}

    } - {!!quantity &&

    {quantity}

    } - {!!price &&

    {price}

    } + {!!title &&

    {title}

    } + {!!subtitle &&

    {subtitle}

    } + {!!text &&

    {text}

    } + {!!quantity &&

    {quantity}

    } + {!!price &&

    {price}

    }
  • ))} )} {!!vat && ( -

    +

    {vatLabel} {vat}

    )} {!!tax && ( -

    +

    {taxLabel} {tax}

    )} {!!total && ( -

    +

    {totalLabel} {total}

    )} {!!buttons && !!buttons.length && ( -
    +
    {buttons.map(({ title }, index) => (
    )} -
    +
    ); }; diff --git a/packages/component/src/LiveRegion/LiveRegionActivity.tsx b/packages/component/src/LiveRegion/LiveRegionActivity.tsx index 46a1630ae1..4b1ee99ee6 100644 --- a/packages/component/src/LiveRegion/LiveRegionActivity.tsx +++ b/packages/component/src/LiveRegion/LiveRegionActivity.tsx @@ -1,10 +1,10 @@ /* eslint no-magic-numbers: ["error", { "ignore": [2] }] */ import { hooks } from 'botframework-webchat-api'; -import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; +import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; import classNames from 'classnames'; import React, { Fragment, useMemo } from 'react'; -import { any, object, pipe, readonly, type InferInput } from 'valibot'; +import { any, object, pipe, readonly } from 'valibot'; import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject'; import useRenderMarkdownAsHTML from '../hooks/useRenderMarkdownAsHTML'; @@ -34,7 +34,7 @@ const liveRegionActivityPropsSchema = pipe( readonly() ); -type LiveRegionActivityProps = InferInput; +type LiveRegionActivityProps = InferReactProps; function LiveRegionActivity(props: LiveRegionActivityProps) { const { activity } = validateProps(liveRegionActivityPropsSchema, props); @@ -56,14 +56,16 @@ function LiveRegionActivity(props: LiveRegionActivityProps) { ).replace(/\s{2,}/gu, ' '); const validFallbackText = fallbackText && typeof fallbackText === 'string'; + // Lines are `role="none"`: on Windows, generic `
    ` and `

    ` are exposed as "group", screen readers would read "group" before each line. + // The message itself stays in the accessibility tree, as required by `aria-atomic`. return ( -

    -
    {greetingAlt}
    +
    +
    {greetingAlt}
    {validFallbackText ? ( -
    {fallbackText}
    +
    {fallbackText}
    ) : ( -
    {textAlt}
    +
    {textAlt}
    {type === 'message' && ( {!!activity.suggestedActions && ( @@ -74,7 +76,7 @@ function LiveRegionActivity(props: LiveRegionActivityProps) { )} )} -
    +
    ); } diff --git a/packages/component/src/LiveRegion/private/LiveRegionAttachments.tsx b/packages/component/src/LiveRegion/private/LiveRegionAttachments.tsx index b9b0de929e..c447970cf5 100644 --- a/packages/component/src/LiveRegion/private/LiveRegionAttachments.tsx +++ b/packages/component/src/LiveRegion/private/LiveRegionAttachments.tsx @@ -37,10 +37,13 @@ const LiveRegionAttachments = ({ activity }: LiveRegionAttachmentsProps) => { {attachmentForScreenReaderRenderers.map((render, index) => ( // Direct Line does not have key for attachment other than index. + // Like the lines of the message, `role="none"` keeps "group" from being read, see `LiveRegionActivity`. // eslint-disable-next-line react/no-array-index-key -
    {typeof render === 'function' && render()}
    +
    + {typeof render === 'function' && render()} +
    ))} - {numAttachmentsAlt &&

    {numAttachmentsAlt}

    } + {numAttachmentsAlt &&

    {numAttachmentsAlt}

    }
    ); }; diff --git a/packages/component/src/LiveRegion/private/LiveRegionSuggestedActions.tsx b/packages/component/src/LiveRegion/private/LiveRegionSuggestedActions.tsx index 5e6369cc50..a15a390980 100644 --- a/packages/component/src/LiveRegion/private/LiveRegionSuggestedActions.tsx +++ b/packages/component/src/LiveRegion/private/LiveRegionSuggestedActions.tsx @@ -1,6 +1,6 @@ -import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; +import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; import React, { memo } from 'react'; -import { any, array, object, pipe, readonly, type InferInput } from 'valibot'; +import { any, array, object, pipe, readonly } from 'valibot'; import computeSuggestedActionText from '../../Utils/computeSuggestedActionText'; @@ -17,14 +17,14 @@ const liveRegionSuggestedActionsPropSchema = pipe( readonly() ); -type LiveRegionSuggestedActionsProps = InferInput; +type LiveRegionSuggestedActionsProps = InferReactProps; function LiveRegionSuggestedActions(props: LiveRegionSuggestedActionsProps) { const { suggestedActions } = validateProps(liveRegionSuggestedActionsPropSchema, props); return ( suggestedActions.actions?.length && ( -

    +

    {suggestedActions.actions.map((action, index) => ( // Direct Line schema does not have key other than index. // eslint-disable-next-line react/no-array-index-key diff --git a/packages/component/src/Middleware/AttachmentForScreenReader/AudioAttachment.js b/packages/component/src/Middleware/AttachmentForScreenReader/AudioAttachment.js index b34f4a44e3..2d75ba12dd 100644 --- a/packages/component/src/Middleware/AttachmentForScreenReader/AudioAttachment.js +++ b/packages/component/src/Middleware/AttachmentForScreenReader/AudioAttachment.js @@ -8,7 +8,7 @@ const AudioAttachment = () => { const label = localize('ATTACHMENT_AUDIO'); - return

    {label}
    ; + return
    {label}
    ; }; export default AudioAttachment; diff --git a/packages/component/src/Middleware/AttachmentForScreenReader/FileAttachment.js b/packages/component/src/Middleware/AttachmentForScreenReader/FileAttachment.js index 3d01ff2b9c..dfc0475f95 100644 --- a/packages/component/src/Middleware/AttachmentForScreenReader/FileAttachment.js +++ b/packages/component/src/Middleware/AttachmentForScreenReader/FileAttachment.js @@ -9,7 +9,7 @@ const FileAttachment = ({ attachment: { name = '' } = {} }) => { const label = localize('ATTACHMENT_FILE', name); - return
    {label}
    ; + return
    {label}
    ; }; FileAttachment.propTypes = { diff --git a/packages/component/src/Middleware/AttachmentForScreenReader/ImageAttachment.js b/packages/component/src/Middleware/AttachmentForScreenReader/ImageAttachment.js index 454c371535..5c5e1e016e 100644 --- a/packages/component/src/Middleware/AttachmentForScreenReader/ImageAttachment.js +++ b/packages/component/src/Middleware/AttachmentForScreenReader/ImageAttachment.js @@ -8,7 +8,7 @@ const ImageAttachment = () => { const label = localize('ATTACHMENT_IMAGE'); - return
    {label}
    ; + return
    {label}
    ; }; export default ImageAttachment; diff --git a/packages/component/src/Middleware/AttachmentForScreenReader/TextAttachment.js b/packages/component/src/Middleware/AttachmentForScreenReader/TextAttachment.js index 20ce747505..ee4cc54fd0 100644 --- a/packages/component/src/Middleware/AttachmentForScreenReader/TextAttachment.js +++ b/packages/component/src/Middleware/AttachmentForScreenReader/TextAttachment.js @@ -9,7 +9,7 @@ const TextAttachment = ({ attachment: { content = '' } = {} }) => { const label = localize('ATTACHMENT_TEXT', content); - return
    {label}
    ; + return
    {label}
    ; }; TextAttachment.propTypes = { diff --git a/packages/component/src/Middleware/AttachmentForScreenReader/VideoAttachment.js b/packages/component/src/Middleware/AttachmentForScreenReader/VideoAttachment.js index c41f8c9117..8f6e99701b 100644 --- a/packages/component/src/Middleware/AttachmentForScreenReader/VideoAttachment.js +++ b/packages/component/src/Middleware/AttachmentForScreenReader/VideoAttachment.js @@ -8,7 +8,7 @@ const VideoAttachment = () => { const label = localize('ATTACHMENT_VIDEO'); - return
    {label}
    ; + return
    {label}
    ; }; export default VideoAttachment;