diff --git a/.changelog/5898-fix-5803-sender-initials.md b/.changelog/5898-fix-5803-sender-initials.md new file mode 100644 index 0000000000..3906df9912 --- /dev/null +++ b/.changelog/5898-fix-5803-sender-initials.md @@ -0,0 +1,4 @@ +### Fixed + +- Fixed [#5803](https://github.com/microsoft/BotFramework-WebChat/issues/5803), screen readers announcing all bot messages with the same initials, such as "Bot JO said:" for both a bot and a live agent: the text before each message, the name of the message when navigating with arrow keys, and the live region now prefer the per-activity `channelData.webChat.styleOptions.botAvatarInitials` over `styleOptions.botAvatarInitials`, in PR [#5898](https://github.com/microsoft/BotFramework-WebChat/pull/5898), by [@OEvgeny](https://github.com/OEvgeny) +- Fixed [#5715](https://github.com/microsoft/BotFramework-WebChat/issues/5715) and [#5735](https://github.com/microsoft/BotFramework-WebChat/issues/5735), screen readers not announcing the time of a message when navigating the transcript with arrow keys: the name of each sent message now ends with the same "Sent at" text as its timestamp, in PR [#5898](https://github.com/microsoft/BotFramework-WebChat/pull/5898), by [@OEvgeny](https://github.com/OEvgeny) diff --git a/__tests__/html2/accessibility/accessibleName.activityStatus.sendFailed.html b/__tests__/html2/accessibility/accessibleName.activityStatus.sendFailed.html index 3ec6adb737..e226ee34ef 100644 --- a/__tests__/html2/accessibility/accessibleName.activityStatus.sendFailed.html +++ b/__tests__/html2/accessibility/accessibleName.activityStatus.sendFailed.html @@ -1,4 +1,4 @@ - +
@@ -29,7 +29,10 @@ const sendMessage = await directLine.emulateOutgoingActivity('Hello, World!'); - await directLine.emulateIncomingActivity('Aloha!'); + // The echo back of the message has a timestamp, but the message is not sent. + await sendMessage.echoBack(activity => ({ ...activity, timestamp: '2026-10-17T11:50:00Z' })); + + await directLine.emulateIncomingActivity({ text: 'Aloha!', timestamp: '2026-10-17T11:51:00Z' }); sendMessage.rejectPostActivity(new Error('artificial error')); @@ -40,8 +43,9 @@ ); expect(pageElements.activityActiveDescendantLabels().map(({ innerText }) => innerText)).toEqual([ + // The time is only narrated after the message is sent. 'You said: Hello, World! Send failed. Click to interact.', - 'Bot said: Aloha!' + 'Bot said: Aloha! Sent at October 17 at 11:51 AM' ]); }, { ignoreErrors: true } diff --git a/__tests__/html2/accessibility/accessibleName.link.html b/__tests__/html2/accessibility/accessibleName.link.html index b2ad5111ea..a24d551675 100644 --- a/__tests__/html2/accessibility/accessibleName.link.html +++ b/__tests__/html2/accessibility/accessibleName.link.html @@ -38,28 +38,35 @@ ); // GIVEN: a message without links. - await directLine.emulateIncomingActivity({ text: 'Hello, World!', type: 'message' }); + await directLine.emulateIncomingActivity({ + text: 'Hello, World!', + timestamp: '2026-10-17T11:50:00Z', + type: 'message' + }); await pageConditions.numActivitiesShown(1); - // THEN: the accessible name has no interactive footnote. + // THEN: the accessible name has no interactive footnote, only the time the message was sent. await waitFor(async () => - expect(await getActivityAccessibleName(pageElements.activities()[0])).toBe('Bot said: Hello, World!') + expect(await getActivityAccessibleName(pageElements.activities()[0])).toBe( + 'Bot said: Hello, World! Sent at October 17 at 11:50 AM' + ) ); // GIVEN: a message with a link. await directLine.emulateIncomingActivity({ text: 'Read the [documentation](https://example.com/docs).', + timestamp: '2026-10-17T11:51:00Z', type: 'message' }); await pageConditions.numActivitiesShown(2); - // THEN: the accessible name tells the message has links and can be interacted with. + // THEN: the accessible name tells the message has links and can be interacted with, followed by the time. // Zero-width spaces wrap every link for line breaking, screen readers do not pause on them. await waitFor(async () => expect((await getActivityAccessibleName(pageElements.activities()[1])).replace(/\u200b/gu, '')).toBe( - 'Bot said: Read the documentation. One or more links in the message. Click to interact.' + 'Bot said: Read the documentation. One or more links in the message. Click to interact. Sent at October 17 at 11:51 AM' ) ); }); diff --git a/__tests__/html2/accessibility/accessibleName.simple.html b/__tests__/html2/accessibility/accessibleName.simple.html index a3d3ef78b3..988998d148 100644 --- a/__tests__/html2/accessibility/accessibleName.simple.html +++ b/__tests__/html2/accessibility/accessibleName.simple.html @@ -1,4 +1,4 @@ - + @@ -8,7 +8,16 @@ - + + + + + + + + + + diff --git a/packages/api/src/StyleOptions.ts b/packages/api/src/StyleOptions.ts index 23a596a438..ac95c4b0ff 100644 --- a/packages/api/src/StyleOptions.ts +++ b/packages/api/src/StyleOptions.ts @@ -97,6 +97,9 @@ type StrictStyleOptions = { /** * Typically rendered as two letters, e.g. 'WC' * Empty string is required when setting botAvatarImage + * + * Screen readers narrate the initials before each bot message, e.g. "Bot WC said:". + * The narration prefers per-activity initials in `channelData.webChat.styleOptions.botAvatarInitials`, e.g. for a live agent. */ botAvatarInitials: string | undefined; diff --git a/packages/bundle/src/boot/actual/internal.ts b/packages/bundle/src/boot/actual/internal.ts index 4f69ec34e2..394a48c44e 100644 --- a/packages/bundle/src/boot/actual/internal.ts +++ b/packages/bundle/src/boot/actual/internal.ts @@ -14,6 +14,7 @@ export { PartGrouping, ScreenReaderText, serializeDocumentFragmentIntoString, + useActivityStyleOptions, useCodeHighlighter, useLiveRegion, type HighlightCodeFn diff --git a/packages/component/src/Activity/CarouselFilmStrip.js b/packages/component/src/Activity/CarouselFilmStrip.js index 8fe7a71f98..5c7632ebd1 100644 --- a/packages/component/src/Activity/CarouselFilmStrip.js +++ b/packages/component/src/Activity/CarouselFilmStrip.js @@ -12,6 +12,7 @@ import CarouselFilmStripAttachment from './CarouselFilmStripAttachment'; import isZeroOrPositive from '../Utils/isZeroOrPositive'; import ScreenReaderText from '../ScreenReaderText'; import textFormatToContentType from '../Utils/textFormatToContentType'; +import useLocalizeActivityGreetingAlt from '../hooks/internal/useLocalizeActivityGreetingAlt.js'; import styles from './CarouselFilmStrip.module.css'; @@ -127,9 +128,7 @@ const CarouselFilmStrip = ({ const activityDisplayText = messageBackDisplayText || text; const fromUser = role === 'user'; - const greetingAlt = ( - fromUser ? localize('ACTIVITY_YOU_SAID_ALT') : localize('ACTIVITY_BOT_SAID_ALT', botInitials || '') - ).replace(/\s{2,}/gu, ' '); + const [greetingAlt] = useLocalizeActivityGreetingAlt()(activity); const initials = fromUser ? userInitials : botInitials; const nubOffset = fromUser ? bubbleFromUserNubOffset : bubbleNubOffset; diff --git a/packages/component/src/Activity/StackedLayout.tsx b/packages/component/src/Activity/StackedLayout.tsx index c87e17b270..377fcea483 100644 --- a/packages/component/src/Activity/StackedLayout.tsx +++ b/packages/component/src/Activity/StackedLayout.tsx @@ -19,6 +19,7 @@ import ScreenReaderText from '../ScreenReaderText'; import isZeroOrPositive from '../Utils/isZeroOrPositive'; import getFirstBaseOfSoftwareSourceCode from '../Utils/orgSchema/getFirstBaseOfSoftwareSourceCode'; import textFormatToContentType from '../Utils/textFormatToContentType'; +import useLocalizeActivityGreetingAlt from '../hooks/internal/useLocalizeActivityGreetingAlt.js'; import useLocalizeProgrammingLanguage from '../hooks/internal/useLocalizeProgrammingLanguage'; import { useGetLogicalGroupKey } from '../providers/ActivityLogicalGrouping'; import AttachmentRow from './AttachmentRow'; @@ -60,14 +61,11 @@ const StackedLayoutInner = memo( showAvatar, showNub }: StackedLayoutInnerProps) => { - const [{ initials: botInitials }] = useAvatarForBot(); const localize = useLocalizer(); const classNames = useStyles(styles); + const [greetingAlt] = useLocalizeActivityGreetingAlt()(activity); const messageThing = useMemo(() => getOrgSchemaMessage(activity.entities), [activity]); - const greetingAlt = ( - fromUser ? localize('ACTIVITY_YOU_SAID_ALT') : localize('ACTIVITY_BOT_SAID_ALT', botInitials || '') - ).replace(/\s{2,}/gu, ' '); return (` are exposed as "group", screen readers would read "group" before each line.
diff --git a/packages/component/src/Transcript/useActivityAccessibleName.ts b/packages/component/src/Transcript/useActivityAccessibleName.ts
index 017531d176..41dc185c97 100644
--- a/packages/component/src/Transcript/useActivityAccessibleName.ts
+++ b/packages/component/src/Transcript/useActivityAccessibleName.ts
@@ -3,15 +3,16 @@ import { useEffect, useMemo, useState } from 'react';
import type { RefObject } from 'react';
import type { WebChatActivity } from 'botframework-webchat-core';
-import { SEND_FAILED } from '../types/internal/SendStatus';
+import { SEND_FAILED, SENT } from '../types/internal/SendStatus';
import activityAltText from '../Utils/activityAltText';
import tabbableElements from '../Utils/tabbableElements';
import useRenderMarkdownAsHTML from '../hooks/useRenderMarkdownAsHTML';
+import useLocalizeActivityGreetingAlt from '../hooks/internal/useLocalizeActivityGreetingAlt.js';
import useValueRef from '../hooks/internal/useValueRef';
type InteractiveType = 'link' | 'widget';
-const { useAvatarForBot, useGetKeyByActivity, useLocalizer, useSendStatusByActivityKey } = hooks;
+const { useDateFormatter, useGetKeyByActivity, useLocalizer, useSendStatusByActivityKey } = hooks;
const ACTIVITY_NUM_ATTACHMENTS_ALT_IDS = {
few: 'ACTIVITY_NUM_ATTACHMENTS_FEW_ALT',
@@ -28,11 +29,13 @@ const ACTIVITY_NUM_ATTACHMENTS_ALT_IDS = {
*
* Due to a bug in Safari, accessible name for active descendant must be a string but not a DOM element.
*/
-export default function useActivityAccessibleName(activity: WebChatActivity, bodyRef: RefObject