@microsoft/omnichannel-chat-widget 2.0.1-main.c997354 → 2.0.1-main.eaef039
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cjs/components/livechatwidget/common/initWebChatComposer.js +2 -2
- package/lib/cjs/components/webchatcontainerstateful/common/defaultProps/defaultMiddlewareLocalizedTexts.js +2 -1
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +16 -2
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.js +68 -40
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +0 -27
- package/lib/esm/components/livechatwidget/common/initWebChatComposer.js +3 -3
- package/lib/esm/components/webchatcontainerstateful/common/defaultProps/defaultMiddlewareLocalizedTexts.js +2 -1
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +16 -2
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.js +65 -39
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +0 -27
- package/lib/types/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.d.ts +11 -4
- package/lib/types/contexts/common/ILiveChatWidgetLocalizedTexts.d.ts +9 -2
- package/package.json +5 -5
|
@@ -67,7 +67,7 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
|
|
|
67
67
|
// Initialize Web Chat's redux store
|
|
68
68
|
let webChatStore = _WebChatStoreLoader.WebChatStoreLoader.store;
|
|
69
69
|
if (!webChatStore) {
|
|
70
|
-
var _state$domainStates$l, _state$domainStates$l2, _state$domainStates$l3, _props$webChatContain7;
|
|
70
|
+
var _state$domainStates$l, _state$domainStates$l2, _props$controlProps, _state$domainStates$l3, _props$webChatContain7;
|
|
71
71
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
72
72
|
const addConversationalSurveyTagsCallback = action => {
|
|
73
73
|
var _inMemoryState$appSta;
|
|
@@ -126,7 +126,7 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
|
|
|
126
126
|
};
|
|
127
127
|
webChatStore = (0, _botframeworkWebchat.createStore)({},
|
|
128
128
|
//initial state
|
|
129
|
-
_preProcessingMiddleware.default, _attachmentProcessingMiddleware.default, (0, _attachmentUploadValidatorMiddleware.default)((_state$domainStates$l = state.domainStates.liveChatConfig) === null || _state$domainStates$l === void 0 ? void 0 : _state$domainStates$l.allowedFileExtensions, (_state$domainStates$l2 = state.domainStates.liveChatConfig) === null || _state$domainStates$l2 === void 0 ? void 0 : _state$domainStates$l2.maxUploadFileSize, localizedTexts), _attachmentSentAnnouncementMiddleware.default, (0, _customEventMiddleware.default)(_omnichannelChatComponents.BroadcastService), (0, _queueOverflowHandlerMiddleware.createQueueOverflowMiddleware)(state, dispatch), (0, _channelDataMiddleware.default)(addConversationalSurveyTagsCallback), (0, _conversationEndMiddleware.default)(conversationEndCallback, startConversationalSurveyCallback, endConversationalSurveyCallback), (0, _dataMaskingMiddleware.default)((_state$domainStates$l3 = state.domainStates.liveChatConfig) === null || _state$domainStates$l3 === void 0 ? void 0 : _state$domainStates$l3.DataMaskingInfo), _messageTimestampMiddleware.createMessageTimeStampMiddleware, _messageSequenceIdOverrideMiddleware.createMessageSequenceIdOverrideMiddleware, (0, _citationsMiddleware.createCitationsMiddleware)(state, dispatch), _gifUploadMiddleware.default, _htmlPlayerMiddleware.default, (0, _htmlTextMiddleware.default)(honorsTargetInHTMLLinks), (0, _maxMessageSizeValidator.default)(localizedTexts), _sanitizationMiddleware.default, (0, _callActionMiddleware.default)(),
|
|
129
|
+
_preProcessingMiddleware.default, _attachmentProcessingMiddleware.default, (0, _attachmentUploadValidatorMiddleware.default)((_state$domainStates$l = state.domainStates.liveChatConfig) === null || _state$domainStates$l === void 0 ? void 0 : _state$domainStates$l.allowedFileExtensions, (_state$domainStates$l2 = state.domainStates.liveChatConfig) === null || _state$domainStates$l2 === void 0 ? void 0 : _state$domainStates$l2.maxUploadFileSize, localizedTexts), (0, _attachmentSentAnnouncementMiddleware.default)(localizedTexts, ((_props$controlProps = props.controlProps) === null || _props$controlProps === void 0 ? void 0 : _props$controlProps.id) || "oc-lcw"), (0, _customEventMiddleware.default)(_omnichannelChatComponents.BroadcastService), (0, _queueOverflowHandlerMiddleware.createQueueOverflowMiddleware)(state, dispatch), (0, _channelDataMiddleware.default)(addConversationalSurveyTagsCallback), (0, _conversationEndMiddleware.default)(conversationEndCallback, startConversationalSurveyCallback, endConversationalSurveyCallback), (0, _dataMaskingMiddleware.default)((_state$domainStates$l3 = state.domainStates.liveChatConfig) === null || _state$domainStates$l3 === void 0 ? void 0 : _state$domainStates$l3.DataMaskingInfo), _messageTimestampMiddleware.createMessageTimeStampMiddleware, _messageSequenceIdOverrideMiddleware.createMessageSequenceIdOverrideMiddleware, (0, _citationsMiddleware.createCitationsMiddleware)(state, dispatch), _gifUploadMiddleware.default, _htmlPlayerMiddleware.default, (0, _htmlTextMiddleware.default)(honorsTargetInHTMLLinks), (0, _maxMessageSizeValidator.default)(localizedTexts), _sanitizationMiddleware.default, (0, _callActionMiddleware.default)(),
|
|
130
130
|
// Pass a callback so middleware can push initials into React context for reactivity
|
|
131
131
|
(0, _localizedStringsBotInitialsMiddleware.localizedStringsBotInitialsMiddleware)(initials => {
|
|
132
132
|
dispatch({
|
|
@@ -33,7 +33,8 @@ const defaultMiddlewareLocalizedTexts = exports.defaultMiddlewareLocalizedTexts
|
|
|
33
33
|
MIDDLEWARE_BANNER_CHAT_DISCONNECT: "Your conversation has been disconnected. For additional assistance, please start a new chat.",
|
|
34
34
|
THIRD_PARTY_COOKIES_BLOCKED_ALERT_MESSAGE: "Allow sites to save/read cookies in browser settings. Reloading page starts a new chat.",
|
|
35
35
|
MIDDLEWARE_BANNER_FILE_IS_MALICIOUS: "{0} has been blocked because the file may contain a malware.",
|
|
36
|
-
MIDDLEWARE_BANNER_FILE_SENT: "File sent
|
|
36
|
+
MIDDLEWARE_BANNER_FILE_SENT: "File {2} sent",
|
|
37
|
+
MIDDLEWARE_BANNER_FILES_SENT: "{0} files sent",
|
|
37
38
|
MIDDLEWARE_BANNER_FILE_EMAIL_ADDRESS_RECORDED_SUCCESS: "Email will be sent after chat ends!",
|
|
38
39
|
MIDDLEWARE_BANNER_FILE_EMAIL_ADDRESS_RECORDED_ERROR: "Email {0} could not be saved, try again later.",
|
|
39
40
|
MIDDLEWARE_SR_PREFIX_SUCCESS: "Success. ",
|
|
@@ -94,6 +94,19 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
|
|
|
94
94
|
}
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
// action-button-double-read: the adaptivecards renderer writes the action
|
|
98
|
+
// title to the button text, aria-label AND title. The title becomes the
|
|
99
|
+
// accessible description, so TalkBack reads the label twice ("Submit,
|
|
100
|
+
// button, Submit"). Drop the title only when it repeats the button name;
|
|
101
|
+
// a distinct title (an action tooltip) adds information and is kept.
|
|
102
|
+
container.querySelectorAll("button[title]").forEach(btn => {
|
|
103
|
+
const title = (btn.getAttribute("title") || "").trim();
|
|
104
|
+
const name = (btn.getAttribute("aria-label") ?? btn.textContent ?? "").trim();
|
|
105
|
+
if (title && title === name) {
|
|
106
|
+
btn.removeAttribute("title");
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
97
110
|
// Find all radio inputs inside the adaptive card
|
|
98
111
|
const radios = container.querySelectorAll("input[type='radio']");
|
|
99
112
|
if (radios.length === 0) return;
|
|
@@ -175,7 +188,8 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
|
|
|
175
188
|
|
|
176
189
|
// Use MutationObserver to patch after WebChat finishes rendering the card.
|
|
177
190
|
// Also watch attribute changes on aria-hidden so we can re-apply if React
|
|
178
|
-
// reconciliation removes it from labels
|
|
191
|
+
// reconciliation removes it from labels, and on title so a re-added
|
|
192
|
+
// duplicate button title is removed again.
|
|
179
193
|
const observer = new MutationObserver(() => {
|
|
180
194
|
patchRadioGroups();
|
|
181
195
|
});
|
|
@@ -183,7 +197,7 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
|
|
|
183
197
|
childList: true,
|
|
184
198
|
subtree: true,
|
|
185
199
|
attributes: true,
|
|
186
|
-
attributeFilter: ["aria-hidden"]
|
|
200
|
+
attributeFilter: ["aria-hidden", "title"]
|
|
187
201
|
});
|
|
188
202
|
|
|
189
203
|
// Also run immediately in case the card is already rendered
|
|
@@ -3,31 +3,41 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.default = void 0;
|
|
6
|
+
exports.getFileSentMessage = exports.default = exports.createAttachmentSentAnnouncementMiddleware = exports.ATTACHMENT_ANNOUNCEMENT_REGION_ID = void 0;
|
|
7
7
|
var _IWebChatAction = require("../../../interfaces/IWebChatAction");
|
|
8
8
|
var _WebChatActionType = require("../../enums/WebChatActionType");
|
|
9
9
|
/******
|
|
10
10
|
* AttachmentSentAnnouncementMiddleware
|
|
11
11
|
*
|
|
12
12
|
* When a file attachment is successfully sent, announces the result to
|
|
13
|
-
* screen readers
|
|
14
|
-
*
|
|
13
|
+
* screen readers so that assistive technology users receive immediate
|
|
14
|
+
* feedback ("File report.pdf sent").
|
|
15
|
+
*
|
|
16
|
+
* The announcement region lives inside the chat dialog. While the dialog is
|
|
17
|
+
* open it is aria-modal and its host siblings are aria-hidden, so screen
|
|
18
|
+
* readers (TalkBack, VoiceOver) ignore live regions outside it.
|
|
15
19
|
******/
|
|
16
20
|
|
|
17
|
-
const
|
|
21
|
+
const ATTACHMENT_ANNOUNCEMENT_REGION_ID = exports.ATTACHMENT_ANNOUNCEMENT_REGION_ID = "oc-lcw-attachment-announcement";
|
|
22
|
+
const DEFAULT_FILE_SENT_TEXT = "File {2} sent";
|
|
23
|
+
const DEFAULT_FILES_SENT_TEXT = "{0} files sent";
|
|
24
|
+
|
|
25
|
+
// Let the send-box focus shift settle before speaking, then keep the node
|
|
26
|
+
// long enough for the screen reader to read it.
|
|
27
|
+
const ANNOUNCE_DELAY_MS = 100;
|
|
28
|
+
const ANNOUNCEMENT_LIFETIME_MS = 5000;
|
|
18
29
|
|
|
19
30
|
/**
|
|
20
|
-
* Ensures a visually-hidden aria-live region exists
|
|
21
|
-
* announcing attachment upload results to screen readers.
|
|
31
|
+
* Ensures a visually-hidden aria-live region exists inside the chat dialog
|
|
32
|
+
* for announcing attachment upload results to screen readers.
|
|
22
33
|
*/
|
|
23
|
-
const ensureAriaLiveRegion =
|
|
24
|
-
|
|
34
|
+
const ensureAriaLiveRegion = widgetElementId => {
|
|
35
|
+
const host = document.getElementById(widgetElementId) || document.body;
|
|
36
|
+
let region = document.getElementById(ATTACHMENT_ANNOUNCEMENT_REGION_ID);
|
|
25
37
|
if (!region) {
|
|
26
38
|
region = document.createElement("div");
|
|
27
|
-
region.id =
|
|
39
|
+
region.id = ATTACHMENT_ANNOUNCEMENT_REGION_ID;
|
|
28
40
|
region.setAttribute("aria-live", "polite");
|
|
29
|
-
region.setAttribute("role", "status");
|
|
30
|
-
region.setAttribute("aria-atomic", "true");
|
|
31
41
|
// Visually hidden but available to screen readers
|
|
32
42
|
Object.assign(region.style, {
|
|
33
43
|
position: "absolute",
|
|
@@ -40,44 +50,62 @@ const ensureAriaLiveRegion = () => {
|
|
|
40
50
|
whiteSpace: "nowrap",
|
|
41
51
|
border: "0"
|
|
42
52
|
});
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
}
|
|
54
|
+
if (region.parentElement !== host) {
|
|
55
|
+
host.appendChild(region);
|
|
45
56
|
}
|
|
46
57
|
return region;
|
|
47
58
|
};
|
|
48
59
|
|
|
49
60
|
/**
|
|
50
|
-
* Announces a message
|
|
51
|
-
*
|
|
61
|
+
* Announces a message by appending a fresh node to the live region.
|
|
62
|
+
* TalkBack reliably speaks newly added nodes but often misses text changes
|
|
63
|
+
* on an existing node, so each announcement gets its own element.
|
|
52
64
|
*/
|
|
53
|
-
const announce = message => {
|
|
54
|
-
const region = ensureAriaLiveRegion();
|
|
55
|
-
region.textContent = "";
|
|
56
|
-
// Small delay so the screen reader detects the content change
|
|
65
|
+
const announce = (message, widgetElementId) => {
|
|
66
|
+
const region = ensureAriaLiveRegion(widgetElementId);
|
|
57
67
|
setTimeout(() => {
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
const node = document.createElement("div");
|
|
69
|
+
node.textContent = message;
|
|
70
|
+
region.appendChild(node);
|
|
71
|
+
setTimeout(() => node.remove(), ANNOUNCEMENT_LIFETIME_MS);
|
|
72
|
+
}, ANNOUNCE_DELAY_MS);
|
|
60
73
|
};
|
|
61
74
|
|
|
62
|
-
// eslint-disable-next-line @typescript-eslint/no-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
|
+
const getFileSentMessage = (attachments, localizedTexts) => {
|
|
77
|
+
var _attachments$;
|
|
78
|
+
if (attachments.length > 1) {
|
|
79
|
+
// Replacer callbacks insert values literally; a string would treat "$&" or "$$" as patterns.
|
|
80
|
+
return ((localizedTexts === null || localizedTexts === void 0 ? void 0 : localizedTexts.MIDDLEWARE_BANNER_FILES_SENT) || DEFAULT_FILES_SENT_TEXT).replace("{0}", () => String(attachments.length));
|
|
81
|
+
}
|
|
82
|
+
const name = typeof ((_attachments$ = attachments[0]) === null || _attachments$ === void 0 ? void 0 : _attachments$.name) === "string" ? attachments[0].name.trim() : "";
|
|
83
|
+
return ((localizedTexts === null || localizedTexts === void 0 ? void 0 : localizedTexts.MIDDLEWARE_BANNER_FILE_SENT) || DEFAULT_FILE_SENT_TEXT).replace("{2}", () => name).replace(/\s{2,}/g, " ").trim();
|
|
84
|
+
};
|
|
85
|
+
exports.getFileSentMessage = getFileSentMessage;
|
|
86
|
+
const createAttachmentSentAnnouncementMiddleware = function (localizedTexts) {
|
|
87
|
+
let widgetElementId = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "oc-lcw";
|
|
88
|
+
return (
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
90
|
+
_ref => {
|
|
91
|
+
let dispatch = _ref.dispatch;
|
|
92
|
+
return next => action => {
|
|
93
|
+
if (!(0, _IWebChatAction.isWebChatAction)(action)) {
|
|
94
|
+
return next(action);
|
|
95
|
+
}
|
|
69
96
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
// Intercept successfully posted activities that contain attachments
|
|
98
|
+
if (action.type === _WebChatActionType.WebChatActionType.DIRECT_LINE_POST_ACTIVITY_FULFILLED) {
|
|
99
|
+
var _action$payload;
|
|
100
|
+
const attachments = (_action$payload = action.payload) === null || _action$payload === void 0 || (_action$payload = _action$payload.activity) === null || _action$payload === void 0 ? void 0 : _action$payload.attachments;
|
|
101
|
+
if (attachments && attachments.length > 0) {
|
|
102
|
+
announce(getFileSentMessage(attachments, localizedTexts), widgetElementId);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return next(action);
|
|
106
|
+
};
|
|
79
107
|
}
|
|
80
|
-
|
|
81
|
-
};
|
|
108
|
+
);
|
|
82
109
|
};
|
|
83
|
-
|
|
110
|
+
exports.createAttachmentSentAnnouncementMiddleware = createAttachmentSentAnnouncementMiddleware;
|
|
111
|
+
var _default = exports.default = createAttachmentSentAnnouncementMiddleware;
|
|
@@ -18,27 +18,6 @@ var _WebChatActionType = require("../../enums/WebChatActionType");
|
|
|
18
18
|
******/
|
|
19
19
|
|
|
20
20
|
const MBtoBRatio = 1000000;
|
|
21
|
-
const announceFileSent = message => {
|
|
22
|
-
// TalkBack on Android WebView reliably announces newly *appended*
|
|
23
|
-
// role="alert" nodes but often misses text-content updates on existing
|
|
24
|
-
// nodes. Wait 500ms for the send-box focus shift to settle, then inject
|
|
25
|
-
// a fresh alert element appended to document.body and remove it after 3s.
|
|
26
|
-
setTimeout(() => {
|
|
27
|
-
const el = document.createElement("div");
|
|
28
|
-
el.setAttribute("role", "alert");
|
|
29
|
-
el.setAttribute("aria-live", "assertive");
|
|
30
|
-
el.setAttribute("aria-atomic", "true");
|
|
31
|
-
el.style.cssText = "position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;";
|
|
32
|
-
el.textContent = message;
|
|
33
|
-
document.body.appendChild(el);
|
|
34
|
-
// Remove after TalkBack has had time to read it
|
|
35
|
-
setTimeout(() => {
|
|
36
|
-
if (el.parentNode) {
|
|
37
|
-
el.parentNode.removeChild(el);
|
|
38
|
-
}
|
|
39
|
-
}, 3000);
|
|
40
|
-
}, 500);
|
|
41
|
-
};
|
|
42
21
|
|
|
43
22
|
/*
|
|
44
23
|
* If an attachment is invalid, delete this attachment from the attachments list
|
|
@@ -227,13 +206,7 @@ const createAttachmentUploadValidatorMiddleware = (allowedFileExtensions, maxFil
|
|
|
227
206
|
var _payload$activity, _payload$activity2;
|
|
228
207
|
const payload = action.payload;
|
|
229
208
|
if (payload !== null && payload !== void 0 && payload.activity.attachments && payload.activity.attachments.length > 0 && (payload === null || payload === void 0 || (_payload$activity = payload.activity) === null || _payload$activity === void 0 || (_payload$activity = _payload$activity.attachments) === null || _payload$activity === void 0 ? void 0 : _payload$activity.length) === (payload === null || payload === void 0 || (_payload$activity2 = payload.activity) === null || _payload$activity2 === void 0 || (_payload$activity2 = _payload$activity2.channelData) === null || _payload$activity2 === void 0 || (_payload$activity2 = _payload$activity2.attachmentSizes) === null || _payload$activity2 === void 0 ? void 0 : _payload$activity2.length)) {
|
|
230
|
-
var _validatedAction$payl;
|
|
231
209
|
const validatedAction = validateAttachment(action, allowedFileExtensions, maxFileSizeSupportedByDynamics, localizedTexts);
|
|
232
|
-
|
|
233
|
-
// Announce to screen readers that the file was sent when validation passes
|
|
234
|
-
if (((_validatedAction$payl = validatedAction.payload) === null || _validatedAction$payl === void 0 || (_validatedAction$payl = _validatedAction$payl.activity) === null || _validatedAction$payl === void 0 || (_validatedAction$payl = _validatedAction$payl.attachments) === null || _validatedAction$payl === void 0 ? void 0 : _validatedAction$payl.length) > 0) {
|
|
235
|
-
announceFileSent(localizedTexts.MIDDLEWARE_BANNER_FILE_SENT ?? "File sent successfully.");
|
|
236
|
-
}
|
|
237
210
|
return next(validatedAction);
|
|
238
211
|
}
|
|
239
212
|
}
|
|
@@ -11,7 +11,7 @@ import { LiveChatWidgetActionType } from "../../../contexts/common/LiveChatWidge
|
|
|
11
11
|
import { TelemetryHelper } from "../../../common/telemetry/TelemetryHelper";
|
|
12
12
|
import { WebChatStoreLoader } from "../../webchatcontainerstateful/webchatcontroller/WebChatStoreLoader";
|
|
13
13
|
import attachmentProcessingMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentProcessingMiddleware";
|
|
14
|
-
import
|
|
14
|
+
import createAttachmentSentAnnouncementMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware";
|
|
15
15
|
import channelDataMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/channelDataMiddleware";
|
|
16
16
|
import { createActivityMiddleware } from "../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityMiddleware";
|
|
17
17
|
import { createActivityStatusMiddleware } from "../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityStatusMiddleware";
|
|
@@ -62,7 +62,7 @@ export const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endCh
|
|
|
62
62
|
// Initialize Web Chat's redux store
|
|
63
63
|
let webChatStore = WebChatStoreLoader.store;
|
|
64
64
|
if (!webChatStore) {
|
|
65
|
-
var _state$domainStates$l, _state$domainStates$l2, _state$domainStates$l3, _props$webChatContain7;
|
|
65
|
+
var _state$domainStates$l, _state$domainStates$l2, _props$controlProps, _state$domainStates$l3, _props$webChatContain7;
|
|
66
66
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
67
67
|
const addConversationalSurveyTagsCallback = action => {
|
|
68
68
|
var _inMemoryState$appSta;
|
|
@@ -121,7 +121,7 @@ export const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endCh
|
|
|
121
121
|
};
|
|
122
122
|
webChatStore = createStore({},
|
|
123
123
|
//initial state
|
|
124
|
-
preProcessingMiddleware, attachmentProcessingMiddleware, createAttachmentUploadValidatorMiddleware((_state$domainStates$l = state.domainStates.liveChatConfig) === null || _state$domainStates$l === void 0 ? void 0 : _state$domainStates$l.allowedFileExtensions, (_state$domainStates$l2 = state.domainStates.liveChatConfig) === null || _state$domainStates$l2 === void 0 ? void 0 : _state$domainStates$l2.maxUploadFileSize, localizedTexts),
|
|
124
|
+
preProcessingMiddleware, attachmentProcessingMiddleware, createAttachmentUploadValidatorMiddleware((_state$domainStates$l = state.domainStates.liveChatConfig) === null || _state$domainStates$l === void 0 ? void 0 : _state$domainStates$l.allowedFileExtensions, (_state$domainStates$l2 = state.domainStates.liveChatConfig) === null || _state$domainStates$l2 === void 0 ? void 0 : _state$domainStates$l2.maxUploadFileSize, localizedTexts), createAttachmentSentAnnouncementMiddleware(localizedTexts, ((_props$controlProps = props.controlProps) === null || _props$controlProps === void 0 ? void 0 : _props$controlProps.id) || "oc-lcw"), createCustomEventMiddleware(BroadcastService), createQueueOverflowMiddleware(state, dispatch), channelDataMiddleware(addConversationalSurveyTagsCallback), createConversationEndMiddleware(conversationEndCallback, startConversationalSurveyCallback, endConversationalSurveyCallback), createDataMaskingMiddleware((_state$domainStates$l3 = state.domainStates.liveChatConfig) === null || _state$domainStates$l3 === void 0 ? void 0 : _state$domainStates$l3.DataMaskingInfo), createMessageTimeStampMiddleware, createMessageSequenceIdOverrideMiddleware, createCitationsMiddleware(state, dispatch), gifUploadMiddleware, htmlPlayerMiddleware, htmlTextMiddleware(honorsTargetInHTMLLinks), createMaxMessageSizeValidator(localizedTexts), sanitizationMiddleware, createCallActionMiddleware(),
|
|
125
125
|
// Pass a callback so middleware can push initials into React context for reactivity
|
|
126
126
|
localizedStringsBotInitialsMiddleware(initials => {
|
|
127
127
|
dispatch({
|
|
@@ -27,7 +27,8 @@ export const defaultMiddlewareLocalizedTexts = {
|
|
|
27
27
|
MIDDLEWARE_BANNER_CHAT_DISCONNECT: "Your conversation has been disconnected. For additional assistance, please start a new chat.",
|
|
28
28
|
THIRD_PARTY_COOKIES_BLOCKED_ALERT_MESSAGE: "Allow sites to save/read cookies in browser settings. Reloading page starts a new chat.",
|
|
29
29
|
MIDDLEWARE_BANNER_FILE_IS_MALICIOUS: "{0} has been blocked because the file may contain a malware.",
|
|
30
|
-
MIDDLEWARE_BANNER_FILE_SENT: "File sent
|
|
30
|
+
MIDDLEWARE_BANNER_FILE_SENT: "File {2} sent",
|
|
31
|
+
MIDDLEWARE_BANNER_FILES_SENT: "{0} files sent",
|
|
31
32
|
MIDDLEWARE_BANNER_FILE_EMAIL_ADDRESS_RECORDED_SUCCESS: "Email will be sent after chat ends!",
|
|
32
33
|
MIDDLEWARE_BANNER_FILE_EMAIL_ADDRESS_RECORDED_ERROR: "Email {0} could not be saved, try again later.",
|
|
33
34
|
MIDDLEWARE_SR_PREFIX_SUCCESS: "Success. ",
|
|
@@ -88,6 +88,19 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
|
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
+
// action-button-double-read: the adaptivecards renderer writes the action
|
|
92
|
+
// title to the button text, aria-label AND title. The title becomes the
|
|
93
|
+
// accessible description, so TalkBack reads the label twice ("Submit,
|
|
94
|
+
// button, Submit"). Drop the title only when it repeats the button name;
|
|
95
|
+
// a distinct title (an action tooltip) adds information and is kept.
|
|
96
|
+
container.querySelectorAll("button[title]").forEach(btn => {
|
|
97
|
+
const title = (btn.getAttribute("title") || "").trim();
|
|
98
|
+
const name = (btn.getAttribute("aria-label") ?? btn.textContent ?? "").trim();
|
|
99
|
+
if (title && title === name) {
|
|
100
|
+
btn.removeAttribute("title");
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
91
104
|
// Find all radio inputs inside the adaptive card
|
|
92
105
|
const radios = container.querySelectorAll("input[type='radio']");
|
|
93
106
|
if (radios.length === 0) return;
|
|
@@ -169,7 +182,8 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
|
|
|
169
182
|
|
|
170
183
|
// Use MutationObserver to patch after WebChat finishes rendering the card.
|
|
171
184
|
// Also watch attribute changes on aria-hidden so we can re-apply if React
|
|
172
|
-
// reconciliation removes it from labels
|
|
185
|
+
// reconciliation removes it from labels, and on title so a re-added
|
|
186
|
+
// duplicate button title is removed again.
|
|
173
187
|
const observer = new MutationObserver(() => {
|
|
174
188
|
patchRadioGroups();
|
|
175
189
|
});
|
|
@@ -177,7 +191,7 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
|
|
|
177
191
|
childList: true,
|
|
178
192
|
subtree: true,
|
|
179
193
|
attributes: true,
|
|
180
|
-
attributeFilter: ["aria-hidden"]
|
|
194
|
+
attributeFilter: ["aria-hidden", "title"]
|
|
181
195
|
});
|
|
182
196
|
|
|
183
197
|
// Also run immediately in case the card is already rendered
|
|
@@ -2,26 +2,36 @@
|
|
|
2
2
|
* AttachmentSentAnnouncementMiddleware
|
|
3
3
|
*
|
|
4
4
|
* When a file attachment is successfully sent, announces the result to
|
|
5
|
-
* screen readers
|
|
6
|
-
*
|
|
5
|
+
* screen readers so that assistive technology users receive immediate
|
|
6
|
+
* feedback ("File report.pdf sent").
|
|
7
|
+
*
|
|
8
|
+
* The announcement region lives inside the chat dialog. While the dialog is
|
|
9
|
+
* open it is aria-modal and its host siblings are aria-hidden, so screen
|
|
10
|
+
* readers (TalkBack, VoiceOver) ignore live regions outside it.
|
|
7
11
|
******/
|
|
8
12
|
|
|
9
13
|
import { isWebChatAction } from "../../../interfaces/IWebChatAction";
|
|
10
14
|
import { WebChatActionType } from "../../enums/WebChatActionType";
|
|
11
|
-
const
|
|
15
|
+
export const ATTACHMENT_ANNOUNCEMENT_REGION_ID = "oc-lcw-attachment-announcement";
|
|
16
|
+
const DEFAULT_FILE_SENT_TEXT = "File {2} sent";
|
|
17
|
+
const DEFAULT_FILES_SENT_TEXT = "{0} files sent";
|
|
18
|
+
|
|
19
|
+
// Let the send-box focus shift settle before speaking, then keep the node
|
|
20
|
+
// long enough for the screen reader to read it.
|
|
21
|
+
const ANNOUNCE_DELAY_MS = 100;
|
|
22
|
+
const ANNOUNCEMENT_LIFETIME_MS = 5000;
|
|
12
23
|
|
|
13
24
|
/**
|
|
14
|
-
* Ensures a visually-hidden aria-live region exists
|
|
15
|
-
* announcing attachment upload results to screen readers.
|
|
25
|
+
* Ensures a visually-hidden aria-live region exists inside the chat dialog
|
|
26
|
+
* for announcing attachment upload results to screen readers.
|
|
16
27
|
*/
|
|
17
|
-
const ensureAriaLiveRegion =
|
|
18
|
-
|
|
28
|
+
const ensureAriaLiveRegion = widgetElementId => {
|
|
29
|
+
const host = document.getElementById(widgetElementId) || document.body;
|
|
30
|
+
let region = document.getElementById(ATTACHMENT_ANNOUNCEMENT_REGION_ID);
|
|
19
31
|
if (!region) {
|
|
20
32
|
region = document.createElement("div");
|
|
21
|
-
region.id =
|
|
33
|
+
region.id = ATTACHMENT_ANNOUNCEMENT_REGION_ID;
|
|
22
34
|
region.setAttribute("aria-live", "polite");
|
|
23
|
-
region.setAttribute("role", "status");
|
|
24
|
-
region.setAttribute("aria-atomic", "true");
|
|
25
35
|
// Visually hidden but available to screen readers
|
|
26
36
|
Object.assign(region.style, {
|
|
27
37
|
position: "absolute",
|
|
@@ -34,44 +44,60 @@ const ensureAriaLiveRegion = () => {
|
|
|
34
44
|
whiteSpace: "nowrap",
|
|
35
45
|
border: "0"
|
|
36
46
|
});
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
}
|
|
48
|
+
if (region.parentElement !== host) {
|
|
49
|
+
host.appendChild(region);
|
|
39
50
|
}
|
|
40
51
|
return region;
|
|
41
52
|
};
|
|
42
53
|
|
|
43
54
|
/**
|
|
44
|
-
* Announces a message
|
|
45
|
-
*
|
|
55
|
+
* Announces a message by appending a fresh node to the live region.
|
|
56
|
+
* TalkBack reliably speaks newly added nodes but often misses text changes
|
|
57
|
+
* on an existing node, so each announcement gets its own element.
|
|
46
58
|
*/
|
|
47
|
-
const announce = message => {
|
|
48
|
-
const region = ensureAriaLiveRegion();
|
|
49
|
-
region.textContent = "";
|
|
50
|
-
// Small delay so the screen reader detects the content change
|
|
59
|
+
const announce = (message, widgetElementId) => {
|
|
60
|
+
const region = ensureAriaLiveRegion(widgetElementId);
|
|
51
61
|
setTimeout(() => {
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
const node = document.createElement("div");
|
|
63
|
+
node.textContent = message;
|
|
64
|
+
region.appendChild(node);
|
|
65
|
+
setTimeout(() => node.remove(), ANNOUNCEMENT_LIFETIME_MS);
|
|
66
|
+
}, ANNOUNCE_DELAY_MS);
|
|
54
67
|
};
|
|
55
68
|
|
|
56
|
-
// eslint-disable-next-line @typescript-eslint/no-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
export const getFileSentMessage = (attachments, localizedTexts) => {
|
|
71
|
+
var _attachments$;
|
|
72
|
+
if (attachments.length > 1) {
|
|
73
|
+
// Replacer callbacks insert values literally; a string would treat "$&" or "$$" as patterns.
|
|
74
|
+
return ((localizedTexts === null || localizedTexts === void 0 ? void 0 : localizedTexts.MIDDLEWARE_BANNER_FILES_SENT) || DEFAULT_FILES_SENT_TEXT).replace("{0}", () => String(attachments.length));
|
|
75
|
+
}
|
|
76
|
+
const name = typeof ((_attachments$ = attachments[0]) === null || _attachments$ === void 0 ? void 0 : _attachments$.name) === "string" ? attachments[0].name.trim() : "";
|
|
77
|
+
return ((localizedTexts === null || localizedTexts === void 0 ? void 0 : localizedTexts.MIDDLEWARE_BANNER_FILE_SENT) || DEFAULT_FILE_SENT_TEXT).replace("{2}", () => name).replace(/\s{2,}/g, " ").trim();
|
|
78
|
+
};
|
|
79
|
+
export const createAttachmentSentAnnouncementMiddleware = function (localizedTexts) {
|
|
80
|
+
let widgetElementId = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "oc-lcw";
|
|
81
|
+
return (
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
83
|
+
_ref => {
|
|
84
|
+
let dispatch = _ref.dispatch;
|
|
85
|
+
return next => action => {
|
|
86
|
+
if (!isWebChatAction(action)) {
|
|
87
|
+
return next(action);
|
|
88
|
+
}
|
|
63
89
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
// Intercept successfully posted activities that contain attachments
|
|
91
|
+
if (action.type === WebChatActionType.DIRECT_LINE_POST_ACTIVITY_FULFILLED) {
|
|
92
|
+
var _action$payload;
|
|
93
|
+
const attachments = (_action$payload = action.payload) === null || _action$payload === void 0 || (_action$payload = _action$payload.activity) === null || _action$payload === void 0 ? void 0 : _action$payload.attachments;
|
|
94
|
+
if (attachments && attachments.length > 0) {
|
|
95
|
+
announce(getFileSentMessage(attachments, localizedTexts), widgetElementId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return next(action);
|
|
99
|
+
};
|
|
73
100
|
}
|
|
74
|
-
|
|
75
|
-
};
|
|
101
|
+
);
|
|
76
102
|
};
|
|
77
|
-
export default
|
|
103
|
+
export default createAttachmentSentAnnouncementMiddleware;
|
|
@@ -12,27 +12,6 @@ import { NotificationScenarios } from "../../enums/NotificationScenarios";
|
|
|
12
12
|
import { TelemetryHelper } from "../../../../../common/telemetry/TelemetryHelper";
|
|
13
13
|
import { WebChatActionType } from "../../enums/WebChatActionType";
|
|
14
14
|
const MBtoBRatio = 1000000;
|
|
15
|
-
const announceFileSent = message => {
|
|
16
|
-
// TalkBack on Android WebView reliably announces newly *appended*
|
|
17
|
-
// role="alert" nodes but often misses text-content updates on existing
|
|
18
|
-
// nodes. Wait 500ms for the send-box focus shift to settle, then inject
|
|
19
|
-
// a fresh alert element appended to document.body and remove it after 3s.
|
|
20
|
-
setTimeout(() => {
|
|
21
|
-
const el = document.createElement("div");
|
|
22
|
-
el.setAttribute("role", "alert");
|
|
23
|
-
el.setAttribute("aria-live", "assertive");
|
|
24
|
-
el.setAttribute("aria-atomic", "true");
|
|
25
|
-
el.style.cssText = "position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;";
|
|
26
|
-
el.textContent = message;
|
|
27
|
-
document.body.appendChild(el);
|
|
28
|
-
// Remove after TalkBack has had time to read it
|
|
29
|
-
setTimeout(() => {
|
|
30
|
-
if (el.parentNode) {
|
|
31
|
-
el.parentNode.removeChild(el);
|
|
32
|
-
}
|
|
33
|
-
}, 3000);
|
|
34
|
-
}, 500);
|
|
35
|
-
};
|
|
36
15
|
|
|
37
16
|
/*
|
|
38
17
|
* If an attachment is invalid, delete this attachment from the attachments list
|
|
@@ -221,13 +200,7 @@ const createAttachmentUploadValidatorMiddleware = (allowedFileExtensions, maxFil
|
|
|
221
200
|
var _payload$activity, _payload$activity2;
|
|
222
201
|
const payload = action.payload;
|
|
223
202
|
if (payload !== null && payload !== void 0 && payload.activity.attachments && payload.activity.attachments.length > 0 && (payload === null || payload === void 0 || (_payload$activity = payload.activity) === null || _payload$activity === void 0 || (_payload$activity = _payload$activity.attachments) === null || _payload$activity === void 0 ? void 0 : _payload$activity.length) === (payload === null || payload === void 0 || (_payload$activity2 = payload.activity) === null || _payload$activity2 === void 0 || (_payload$activity2 = _payload$activity2.channelData) === null || _payload$activity2 === void 0 || (_payload$activity2 = _payload$activity2.attachmentSizes) === null || _payload$activity2 === void 0 ? void 0 : _payload$activity2.length)) {
|
|
224
|
-
var _validatedAction$payl;
|
|
225
203
|
const validatedAction = validateAttachment(action, allowedFileExtensions, maxFileSizeSupportedByDynamics, localizedTexts);
|
|
226
|
-
|
|
227
|
-
// Announce to screen readers that the file was sent when validation passes
|
|
228
|
-
if (((_validatedAction$payl = validatedAction.payload) === null || _validatedAction$payl === void 0 || (_validatedAction$payl = _validatedAction$payl.activity) === null || _validatedAction$payl === void 0 || (_validatedAction$payl = _validatedAction$payl.attachments) === null || _validatedAction$payl === void 0 ? void 0 : _validatedAction$payl.length) > 0) {
|
|
229
|
-
announceFileSent(localizedTexts.MIDDLEWARE_BANNER_FILE_SENT ?? "File sent successfully.");
|
|
230
|
-
}
|
|
231
204
|
return next(validatedAction);
|
|
232
205
|
}
|
|
233
206
|
}
|
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
* AttachmentSentAnnouncementMiddleware
|
|
3
3
|
*
|
|
4
4
|
* When a file attachment is successfully sent, announces the result to
|
|
5
|
-
* screen readers
|
|
6
|
-
*
|
|
5
|
+
* screen readers so that assistive technology users receive immediate
|
|
6
|
+
* feedback ("File report.pdf sent").
|
|
7
|
+
*
|
|
8
|
+
* The announcement region lives inside the chat dialog. While the dialog is
|
|
9
|
+
* open it is aria-modal and its host siblings are aria-hidden, so screen
|
|
10
|
+
* readers (TalkBack, VoiceOver) ignore live regions outside it.
|
|
7
11
|
******/
|
|
12
|
+
import { ILiveChatWidgetLocalizedTexts } from "../../../../../contexts/common/ILiveChatWidgetLocalizedTexts";
|
|
8
13
|
import { WebChatStoreMiddleware } from "../../../interfaces/IWebChatAction";
|
|
9
|
-
declare const
|
|
10
|
-
export
|
|
14
|
+
export declare const ATTACHMENT_ANNOUNCEMENT_REGION_ID = "oc-lcw-attachment-announcement";
|
|
15
|
+
export declare const getFileSentMessage: (attachments: any[], localizedTexts?: ILiveChatWidgetLocalizedTexts) => string;
|
|
16
|
+
export declare const createAttachmentSentAnnouncementMiddleware: (localizedTexts?: ILiveChatWidgetLocalizedTexts, widgetElementId?: string) => WebChatStoreMiddleware;
|
|
17
|
+
export default createAttachmentSentAnnouncementMiddleware;
|
|
@@ -35,10 +35,17 @@ export interface ILiveChatWidgetLocalizedTexts {
|
|
|
35
35
|
*/
|
|
36
36
|
MIDDLEWARE_BANNER_FILE_IS_MALICIOUS?: string;
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
38
|
+
* Message announced by screen readers when one file is successfully sent.
|
|
39
|
+
* {2} - File name
|
|
40
|
+
* e.g. "File {2} sent"
|
|
40
41
|
*/
|
|
41
42
|
MIDDLEWARE_BANNER_FILE_SENT?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Message announced by screen readers when several files are sent in one message.
|
|
45
|
+
* {0} - Number of files
|
|
46
|
+
* e.g. "{0} files sent"
|
|
47
|
+
*/
|
|
48
|
+
MIDDLEWARE_BANNER_FILES_SENT?: string;
|
|
42
49
|
/**
|
|
43
50
|
* Success message, indicating the email address introduced has been registered to receive the transcript.
|
|
44
51
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microsoft/omnichannel-chat-widget",
|
|
3
|
-
"version": "2.0.1-main.
|
|
3
|
+
"version": "2.0.1-main.eaef039",
|
|
4
4
|
"description": "Microsoft Omnichannel Chat Widget",
|
|
5
5
|
"main": "lib/cjs/index.js",
|
|
6
6
|
"types": "lib/types/index.d.ts",
|
|
@@ -98,13 +98,13 @@
|
|
|
98
98
|
"abort-controller": "^3",
|
|
99
99
|
"botframework-webchat": "4.18.1-hotfix.20260308.b15b405",
|
|
100
100
|
"core-js-pure": "^3.42.0",
|
|
101
|
-
"dompurify": "3.4.
|
|
102
|
-
"markdown-it": "^14.3.
|
|
101
|
+
"dompurify": "3.4.17",
|
|
102
|
+
"markdown-it": "^14.3.3",
|
|
103
103
|
"markdown-it-attrs": "^4.1.6",
|
|
104
104
|
"markdown-it-for-inline": "^0.1.1",
|
|
105
105
|
"md5-typescript": "^1.0.5",
|
|
106
106
|
"rxjs": "5.5.12",
|
|
107
|
-
"sanitize-html": "2.
|
|
107
|
+
"sanitize-html": "2.18.1",
|
|
108
108
|
"simple-update-in": "2.2.0",
|
|
109
109
|
"slack-markdown-it": "^1.0.5"
|
|
110
110
|
},
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
"**/@types/trusted-types": "2.0.7",
|
|
165
165
|
"**/join-images": "1.1.5",
|
|
166
166
|
"**/valibot": "1.4.2",
|
|
167
|
-
"**/nanoid": "3.3.
|
|
167
|
+
"**/nanoid": "^3.3.20"
|
|
168
168
|
},
|
|
169
169
|
"jest": {
|
|
170
170
|
"verbose": true,
|