UNPKG

botframework-webchat-component

Version:
119 lines (107 loc) 4.62 kB
import { hooks } from 'botframework-webchat-api'; 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 activityAltText from '../Utils/activityAltText'; import tabbableElements from '../Utils/tabbableElements'; import useRenderMarkdownAsHTML from '../hooks/useRenderMarkdownAsHTML'; import useValueRef from '../hooks/internal/useValueRef'; enum InteractiveType { LINK, WIDGET } const { useAvatarForBot, useGetKeyByActivity, useLocalizer, useSendStatusByActivityKey } = hooks; const ACTIVITY_NUM_ATTACHMENTS_ALT_IDS = { few: 'ACTIVITY_NUM_ATTACHMENTS_FEW_ALT', many: 'ACTIVITY_NUM_ATTACHMENTS_MANY_ALT', one: 'ACTIVITY_NUM_ATTACHMENTS_ONE_ALT', other: 'ACTIVITY_NUM_ATTACHMENTS_OTHER_ALT', two: 'ACTIVITY_NUM_ATTACHMENTS_TWO_ALT' }; /** * Computes accessible name of an activity during transcript navigation. * * This text should be kept simple and short. * * 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<HTMLElement>) { const [{ initials: botInitials }] = useAvatarForBot(); const [interactiveType, setInteractiveType] = useState<InteractiveType | false>(false); const [sendStatusByActivityKey] = useSendStatusByActivityKey(); const fromSelf = activity.from?.role === 'user'; const getKeyByActivity = useGetKeyByActivity(); const localize = useLocalizer(); const localizeWithPlural = useLocalizer({ plural: true }); const numAttachments = activity.type === 'message' ? activity.attachments?.length || 0 : 0; const renderMarkdownAsHTML = useRenderMarkdownAsHTML(); /** "Click to interact." */ const activityInteractiveFootNoteAlt = localize('ACTIVITY_INTERACTIVE_FOOTNOTE_ALT'); /** "Message is interactive." */ const activityInteractiveReasonInteractiveContentAlt = localize( 'ACTIVITY_INTERACTIVE_REASON_INTERACTIVE_CONTENT_ALT' ); /** "One or more links in the message." */ const activityInteractiveReasonLinkAlt = localize('ACTIVITY_INTERACTIVE_REASON_LINK_ALT'); /** "Send failed." */ const activityInteractiveReasonSendFailedAlt = localize('ACTIVITY_INTERACTIVE_REASON_SEND_FAILED_ALT'); const activityKey = useMemo(() => getKeyByActivity(activity), [activity, getKeyByActivity]); const greetingAlt = useMemo( () => (fromSelf ? localize('ACTIVITY_YOU_SAID_ALT') : localize('ACTIVITY_BOT_SAID_ALT', botInitials || '')).replace( /\s{2,}/gu, ' ' ), [botInitials, fromSelf, localize] ); const interactiveTypeRef = useValueRef(interactiveType); const messageTextAlt = useMemo( () => activityAltText(activity, renderMarkdownAsHTML), [activity, renderMarkdownAsHTML] ); const numAttachmentsAlt = useMemo( () => (numAttachments ? localizeWithPlural(ACTIVITY_NUM_ATTACHMENTS_ALT_IDS, numAttachments) : ''), [localizeWithPlural, numAttachments] ); const isSendFailed = useMemo( () => sendStatusByActivityKey.get(activityKey) === SEND_FAILED, [activityKey, sendStatusByActivityKey] ); const accessibleName = useMemo( // We are concatenating in a single string for Safari. If we split it up, Safari will only narrate the first section. () => [ greetingAlt, messageTextAlt, numAttachmentsAlt, isSendFailed ? activityInteractiveReasonSendFailedAlt : '', interactiveType === InteractiveType.LINK ? activityInteractiveReasonLinkAlt : interactiveType === InteractiveType.WIDGET ? activityInteractiveReasonInteractiveContentAlt : '', interactiveType || isSendFailed ? activityInteractiveFootNoteAlt : '' ] .filter(Boolean) .join(' '), [ activityInteractiveFootNoteAlt, activityInteractiveReasonInteractiveContentAlt, activityInteractiveReasonLinkAlt, activityInteractiveReasonSendFailedAlt, greetingAlt, interactiveType, isSendFailed, messageTextAlt, numAttachmentsAlt ] ); useEffect(() => { const hasLinks = !!bodyRef.current?.querySelector('a'); const hasWidgets = !!tabbableElements(bodyRef.current).length; interactiveTypeRef.current !== hasWidgets && setInteractiveType(hasLinks ? InteractiveType.LINK : hasWidgets ? InteractiveType.WIDGET : false); }, [bodyRef, interactiveTypeRef, setInteractiveType]); return [accessibleName]; }