@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.
Files changed (13) hide show
  1. package/lib/cjs/components/livechatwidget/common/initWebChatComposer.js +2 -2
  2. package/lib/cjs/components/webchatcontainerstateful/common/defaultProps/defaultMiddlewareLocalizedTexts.js +2 -1
  3. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +16 -2
  4. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.js +68 -40
  5. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +0 -27
  6. package/lib/esm/components/livechatwidget/common/initWebChatComposer.js +3 -3
  7. package/lib/esm/components/webchatcontainerstateful/common/defaultProps/defaultMiddlewareLocalizedTexts.js +2 -1
  8. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +16 -2
  9. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.js +65 -39
  10. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +0 -27
  11. package/lib/types/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.d.ts +11 -4
  12. package/lib/types/contexts/common/ILiveChatWidgetLocalizedTexts.d.ts +9 -2
  13. 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 successfully.",
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 via an aria-live region so that assistive technology
14
- * users receive immediate feedback.
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 ARIA_LIVE_REGION_ID = "oc-lcw-attachment-announcement";
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 in the DOM for
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
- let region = document.getElementById(ARIA_LIVE_REGION_ID);
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 = ARIA_LIVE_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
- const container = document.getElementById("oc-lcw-container");
44
- (container || document.body).appendChild(region);
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 to screen readers by updating the aria-live region.
51
- * Clears it briefly first so repeated identical messages are still announced.
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
- region.textContent = message;
59
- }, 100);
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-unused-vars
63
- const attachmentSentAnnouncementMiddleware = _ref => {
64
- let dispatch = _ref.dispatch;
65
- return next => action => {
66
- if (!(0, _IWebChatAction.isWebChatAction)(action)) {
67
- return next(action);
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
- // Intercept successfully posted activities that contain attachments
71
- if (action.type === _WebChatActionType.WebChatActionType.DIRECT_LINE_POST_ACTIVITY_FULFILLED) {
72
- var _action$payload;
73
- 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;
74
- if (attachments && attachments.length > 0) {
75
- const count = attachments.length;
76
- const message = count === 1 ? "File sent" : `${count} files sent`;
77
- announce(message);
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
- return next(action);
81
- };
108
+ );
82
109
  };
83
- var _default = exports.default = attachmentSentAnnouncementMiddleware;
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 attachmentSentAnnouncementMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware";
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), attachmentSentAnnouncementMiddleware, 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(),
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 successfully.",
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 via an aria-live region so that assistive technology
6
- * users receive immediate feedback.
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 ARIA_LIVE_REGION_ID = "oc-lcw-attachment-announcement";
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 in the DOM for
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
- let region = document.getElementById(ARIA_LIVE_REGION_ID);
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 = ARIA_LIVE_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
- const container = document.getElementById("oc-lcw-container");
38
- (container || document.body).appendChild(region);
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 to screen readers by updating the aria-live region.
45
- * Clears it briefly first so repeated identical messages are still announced.
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
- region.textContent = message;
53
- }, 100);
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-unused-vars
57
- const attachmentSentAnnouncementMiddleware = _ref => {
58
- let dispatch = _ref.dispatch;
59
- return next => action => {
60
- if (!isWebChatAction(action)) {
61
- return next(action);
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
- // Intercept successfully posted activities that contain attachments
65
- if (action.type === WebChatActionType.DIRECT_LINE_POST_ACTIVITY_FULFILLED) {
66
- var _action$payload;
67
- 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;
68
- if (attachments && attachments.length > 0) {
69
- const count = attachments.length;
70
- const message = count === 1 ? "File sent" : `${count} files sent`;
71
- announce(message);
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
- return next(action);
75
- };
101
+ );
76
102
  };
77
- export default attachmentSentAnnouncementMiddleware;
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 via an aria-live region so that assistive technology
6
- * users receive immediate feedback.
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 attachmentSentAnnouncementMiddleware: WebChatStoreMiddleware;
10
- export default attachmentSentAnnouncementMiddleware;
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
- * Success message announced by screen readers when a file is successfully sent.
39
- * e.g. "File sent successfully."
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.c997354",
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.16",
102
- "markdown-it": "^14.3.0",
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.17.7",
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.18"
167
+ "**/nanoid": "^3.3.20"
168
168
  },
169
169
  "jest": {
170
170
  "verbose": true,