-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Expand file tree
/
Copy pathLiveRegionTranscript.tsx
More file actions
144 lines (119 loc) · 5.82 KB
/
LiveRegionTranscript.tsx
File metadata and controls
144 lines (119 loc) · 5.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import { hooks } from 'botframework-webchat-api';
import type { WebChatActivity } from 'botframework-webchat-core';
import type { RefObject } from 'react';
import React, { memo, useEffect, useMemo, useRef } from 'react';
import LiveRegionActivity from '../LiveRegion/LiveRegionActivity';
import tabbableElements from '../Utils/tabbableElements';
import useLocalizeAccessKey from '../hooks/internal/useLocalizeAccessKey';
import useSuggestedActionsAccessKey from '../hooks/internal/useSuggestedActionsAccessKey';
import { useQueueStaticElement } from '../providers/LiveRegionTwin';
import LiveRegionSendFailed from './LiveRegion/SendFailed';
import LiveRegionLongSend from './LiveRegion/LongSend';
import isPresentational from './LiveRegion/isPresentational';
import useTypistNames from './useTypistNames';
import type { ActivityElementMap } from './types';
const { useActivities, useGetKeyByActivity, useLocalizer } = hooks;
type RenderingActivities = Map<string, WebChatActivity>;
type LiveRegionTranscriptProps = Readonly<{
activityElementMapRef: RefObject<ActivityElementMap>;
}>;
const LiveRegionTranscript = ({ activityElementMapRef }: LiveRegionTranscriptProps) => {
// We are looking for all activities instead of just those will be rendered.
// This is because some activities that chosen not be rendered in the chat history,
// we might still need to be read by screen reader. Such as, suggested actions without text content.
const [accessKey] = useSuggestedActionsAccessKey();
const [activities] = useActivities();
const [typistNames] = useTypistNames();
const getKeyByActivity = useGetKeyByActivity();
const localize = useLocalizer();
const localizeAccessKeyAsAccessibleName = useLocalizeAccessKey('accessible name');
const queueStaticElement = useQueueStaticElement();
const liveRegionInteractiveLabelAlt = localize('TRANSCRIPT_LIVE_REGION_INTERACTIVE_LABEL_ALT');
const liveRegionInteractiveWithLinkLabelAlt = localize('TRANSCRIPT_LIVE_REGION_INTERACTIVE_WITH_LINKS_LABEL_ALT');
const typingIndicator =
!!typistNames.length &&
localize(
typistNames.length > 1 ? 'TYPING_INDICATOR_MULTIPLE_TEXT' : 'TYPING_INDICATOR_SINGLE_TEXT',
typistNames[0]
);
const liveRegionSuggestedActionsLabelAlt = accessKey
? localize(
'TRANSCRIPT_LIVE_REGION_SUGGESTED_ACTIONS_WITH_ACCESS_KEY_LABEL_ALT',
localizeAccessKeyAsAccessibleName(accessKey)
)
: localize('TRANSCRIPT_LIVE_REGION_SUGGESTED_ACTIONS_LABEL_ALT');
const keyedActivities = useMemo<Readonly<RenderingActivities>>(
() =>
Object.freeze(
activities.reduce<RenderingActivities>((intermediate, activity) => {
// Only "message" activity will be read by screen reader.
if (activity.type === 'message') {
return intermediate.set(getKeyByActivity(activity), activity);
}
return intermediate;
}, new Map<string, WebChatActivity>())
),
[activities, getKeyByActivity]
);
const prevRenderingActivitiesRef = useRef<Readonly<RenderingActivities>>();
useEffect(() => {
const { current: prevRenderingActivities } = prevRenderingActivitiesRef;
const appendedActivities: { activity: WebChatActivity; key: string }[] = [];
// Bottom-up, find activities which are recently appended (i.e. new activity will have a new key).
// We only consider new activities added to the bottom of the chat history.
// Based on how `aria-relevant="additions"` works, activities that are updated, deleted, or reordered, should be ignored.
for (const [key, activity] of Array.from(keyedActivities.entries()).reverse()) {
if (prevRenderingActivities?.has(key)) {
break;
}
appendedActivities.unshift({ activity, key });
isPresentational(activity) || queueStaticElement(<LiveRegionActivity activity={activity} />);
}
const hasNewLink = appendedActivities.some(({ key }) => activityElementMapRef.current.get(key)?.querySelector('a'));
const hasNewWidget = appendedActivities.some(
({ key }) =>
!!tabbableElements(
activityElementMapRef.current.get(key)?.querySelector('.webchat__basic-transcript__activity-body')
).length
);
const hasSuggestedActions = appendedActivities.some(
({ activity }) => activity.type === 'message' && activity.suggestedActions?.actions?.length
);
// This is a footnote reading either:
// - "Message is interactive. Press shift tab key 2 to 3 times to switch to the chat history. Then click on the message to interact.", or;
// - "One or more links in the message. Press shift tab key 2 to 3 times to switch to the chat history. Then click on the message to interact."
if (hasNewLink || hasNewWidget) {
queueStaticElement(
<div className="webchat__live-region-transcript__note" role="note">
{hasNewLink ? liveRegionInteractiveWithLinkLabelAlt : liveRegionInteractiveLabelAlt}
</div>
);
}
// This is a footnote reading "Suggested actions container: has content. Press CTRL + SHIFT + A to select."
if (hasSuggestedActions) {
queueStaticElement(
<div className="webchat__live-region-transcript__note" role="note">
{liveRegionSuggestedActionsLabelAlt}
</div>
);
}
prevRenderingActivitiesRef.current = keyedActivities;
}, [
activityElementMapRef,
liveRegionInteractiveLabelAlt,
liveRegionInteractiveWithLinkLabelAlt,
liveRegionSuggestedActionsLabelAlt,
prevRenderingActivitiesRef,
queueStaticElement,
keyedActivities
]);
useMemo(() => typingIndicator && queueStaticElement(typingIndicator), [queueStaticElement, typingIndicator]);
return (
<React.Fragment>
<LiveRegionSendFailed />
<LiveRegionLongSend />
</React.Fragment>
);
};
LiveRegionTranscript.displayName = 'LiveRegionTranscript';
export default memo(LiveRegionTranscript);