@microsoft/omnichannel-chat-widget 1.8.4-main.6b93408 → 1.8.4-main.810d050

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 (19) hide show
  1. package/lib/cjs/common/Constants.js +0 -1
  2. package/lib/cjs/components/postchatsurveypanestateful/PostChatSurveyPaneStateful.js +1 -0
  3. package/lib/cjs/components/prechatsurveypanestateful/PreChatSurveyPaneStateful.js +64 -2
  4. package/lib/cjs/components/webchatcontainerstateful/WebChatContainerStateful.js +47 -17
  5. package/lib/cjs/components/webchatcontainerstateful/common/defaultProps/defaultWebChatContainerStatefulProps.js +2 -1
  6. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +15 -0
  7. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +4 -8
  8. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/localizedStringsBotInitialsMiddleware.js +43 -5
  9. package/lib/esm/common/Constants.js +0 -1
  10. package/lib/esm/components/postchatsurveypanestateful/PostChatSurveyPaneStateful.js +1 -0
  11. package/lib/esm/components/prechatsurveypanestateful/PreChatSurveyPaneStateful.js +65 -3
  12. package/lib/esm/components/webchatcontainerstateful/WebChatContainerStateful.js +48 -18
  13. package/lib/esm/components/webchatcontainerstateful/common/defaultProps/defaultWebChatContainerStatefulProps.js +2 -1
  14. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +15 -0
  15. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +5 -9
  16. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/localizedStringsBotInitialsMiddleware.js +43 -5
  17. package/lib/types/common/Constants.d.ts +0 -1
  18. package/lib/types/components/webchatcontainerstateful/interfaces/IWebChatContainerStatefulProps.d.ts +7 -0
  19. package/package.json +4 -4
@@ -158,7 +158,6 @@ let HtmlIdNames = /*#__PURE__*/_createClass(function HtmlIdNames() {
158
158
  });
159
159
  exports.HtmlIdNames = HtmlIdNames;
160
160
  _defineProperty(HtmlIdNames, "MSLiveChatWidget", "MSLiveChatWidget");
161
- _defineProperty(HtmlIdNames, "fileSentAnnouncementRegionId", "ms_lcw_file_sent_announcement");
162
161
  let HtmlClassNames = /*#__PURE__*/_createClass(function HtmlClassNames() {
163
162
  _classCallCheck(this, HtmlClassNames);
164
163
  });
@@ -67,6 +67,7 @@ const PostChatSurveyPaneStateful = props => {
67
67
  };
68
68
  const controlProps = {
69
69
  id: "oc-lcw-postchatsurvey-pane",
70
+ title: "Post-chat survey",
70
71
  surveyURL: ((_props$controlProps = props.controlProps) === null || _props$controlProps === void 0 ? void 0 : _props$controlProps.surveyURL) ?? surveyInviteLink,
71
72
  ...props.controlProps
72
73
  };
@@ -20,6 +20,34 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
20
20
  function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
21
21
  function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
22
22
  let uiTimer;
23
+ const getFocusedElementAnnouncement = element => {
24
+ var _element$textContent;
25
+ const ariaLabel = element.getAttribute(_Constants.HtmlAttributeNames.ariaLabel);
26
+ if (ariaLabel) {
27
+ return ariaLabel.trim();
28
+ }
29
+ const ariaLabelledBy = element.getAttribute(_Constants.HtmlAttributeNames.ariaLabelledby);
30
+ if (ariaLabelledBy) {
31
+ const labelledByText = ariaLabelledBy.split(/\s+/).map(id => {
32
+ var _document$getElementB, _document$getElementB2;
33
+ return (_document$getElementB = document.getElementById(id)) === null || _document$getElementB === void 0 ? void 0 : (_document$getElementB2 = _document$getElementB.textContent) === null || _document$getElementB2 === void 0 ? void 0 : _document$getElementB2.trim();
34
+ }).filter(Boolean).join(" ");
35
+ if (labelledByText) {
36
+ return labelledByText;
37
+ }
38
+ }
39
+ if (element.id) {
40
+ const label = Array.from(document.querySelectorAll("label")).find(candidate => candidate.htmlFor === element.id || candidate.getAttribute("for") === element.id);
41
+ if (label !== null && label !== void 0 && label.textContent) {
42
+ return label.textContent.trim();
43
+ }
44
+ }
45
+ const parentLabel = element.closest("label");
46
+ if (parentLabel !== null && parentLabel !== void 0 && parentLabel.textContent) {
47
+ return parentLabel.textContent.trim();
48
+ }
49
+ return ((_element$textContent = element.textContent) === null || _element$textContent === void 0 ? void 0 : _element$textContent.trim()) ?? "";
50
+ };
23
51
 
24
52
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
53
  const PreChatSurveyPaneStateful = props => {
@@ -42,6 +70,8 @@ const PreChatSurveyPaneStateful = props => {
42
70
  surveyProps,
43
71
  initStartChat
44
72
  } = props;
73
+ const [preChatFocusAnnouncement, setPreChatFocusAnnouncement] = (0, _react.useState)("");
74
+ const liveRegionUpdateTimeout = (0, _react.useRef)();
45
75
  const generalStyleProps = Object.assign({}, _defaultGeneralPreChatSurveyPaneStyleProps.defaultGeneralPreChatSurveyPaneStyleProps, surveyProps === null || surveyProps === void 0 ? void 0 : (_surveyProps$stylePro = surveyProps.styleProps) === null || _surveyProps$stylePro === void 0 ? void 0 : _surveyProps$stylePro.generalStyleProps, {
46
76
  display: state.appStates.isMinimized ? "none" : ""
47
77
  });
@@ -128,6 +158,16 @@ const PreChatSurveyPaneStateful = props => {
128
158
  ...(surveyProps === null || surveyProps === void 0 ? void 0 : surveyProps.styleProps),
129
159
  generalStyleProps: generalStyleProps
130
160
  };
161
+ const announceFocusedElement = event => {
162
+ if (liveRegionUpdateTimeout.current) {
163
+ window.clearTimeout(liveRegionUpdateTimeout.current);
164
+ }
165
+ const announcement = getFocusedElementAnnouncement(event.target);
166
+ setPreChatFocusAnnouncement("");
167
+ liveRegionUpdateTimeout.current = window.setTimeout(() => {
168
+ setPreChatFocusAnnouncement(announcement);
169
+ }, 0);
170
+ };
131
171
  (0, _react.useEffect)(() => {
132
172
  // Set Aria-Label Attribute for Inputs
133
173
  const adaptiveCardElements = document.getElementsByClassName(_Constants.HtmlAttributeNames.adaptiveCardClassName);
@@ -175,10 +215,32 @@ const PreChatSurveyPaneStateful = props => {
175
215
  }
176
216
  }
177
217
  }, [state.appStates.isMinimized]);
178
- return /*#__PURE__*/_react.default.createElement(_omnichannelChatComponents.PreChatSurveyPane, {
218
+ (0, _react.useEffect)(() => () => {
219
+ if (liveRegionUpdateTimeout.current) {
220
+ window.clearTimeout(liveRegionUpdateTimeout.current);
221
+ }
222
+ }, []);
223
+ return /*#__PURE__*/_react.default.createElement("div", {
224
+ onFocusCapture: announceFocusedElement
225
+ }, /*#__PURE__*/_react.default.createElement("div", {
226
+ role: "status",
227
+ "aria-live": "polite",
228
+ "aria-atomic": "true",
229
+ style: {
230
+ position: "absolute",
231
+ width: "1px",
232
+ height: "1px",
233
+ margin: "-1px",
234
+ padding: 0,
235
+ border: 0,
236
+ clip: "rect(0, 0, 0, 0)",
237
+ overflow: "hidden",
238
+ whiteSpace: "nowrap"
239
+ }
240
+ }, preChatFocusAnnouncement), /*#__PURE__*/_react.default.createElement(_omnichannelChatComponents.PreChatSurveyPane, {
179
241
  controlProps: controlProps,
180
242
  styleProps: styleProps
181
- });
243
+ }));
182
244
  };
183
245
  exports.PreChatSurveyPaneStateful = PreChatSurveyPaneStateful;
184
246
  var _default = PreChatSurveyPaneStateful;
@@ -232,6 +232,48 @@ const WebChatContainerStateful = props => {
232
232
  chatHistoryElement.setAttribute(_Constants.HtmlAttributeNames.ariaLabel, webChatContainerProps.webChatHistoryMobileAccessibilityLabel);
233
233
  }
234
234
  }
235
+
236
+ // internal tracking: WebChats BasicToaster renders a `role="log"` container
237
+ // with no accessible name. When the toaster is empty (typical idle
238
+ // state) screen readers traversing the page announce it as "blank".
239
+ // Inject an aria-label so the region has a meaningful name; the
240
+ // consumer can override via the new
241
+ // `webChatNotificationRegionAccessibilityLabel` prop. The toaster
242
+ // may not be rendered yet when this effect first runs, so observe
243
+ // for it via a MutationObserver scoped to the widget root (or, if
244
+ // not yet mounted, the widget container's parent) so we never fire
245
+ // on host-page mutations outside the widget subtree.
246
+ const toasterLabel = (webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.webChatNotificationRegionAccessibilityLabel) ?? _defaultWebChatContainerStatefulProps.defaultWebChatContainerStatefulProps.webChatNotificationRegionAccessibilityLabel ?? "Chat notifications";
247
+ const labelToaster = root => {
248
+ const toaster = root.querySelector(".webchat__toaster[role='log']");
249
+ if (toaster && !toaster.getAttribute(_Constants.HtmlAttributeNames.ariaLabel)) {
250
+ toaster.setAttribute(_Constants.HtmlAttributeNames.ariaLabel, toasterLabel);
251
+ return true;
252
+ }
253
+ return false;
254
+ };
255
+ // Prefer the widget root; if it is not yet in the DOM, fall back to
256
+ // `document` for the *initial* lookup only, then observe the most
257
+ // specific available scope (widget root if present, otherwise the
258
+ // widget's parent / document.body as a last resort). The observer
259
+ // re-resolves the widget root on every callback so we tighten scope
260
+ // as soon as it mounts.
261
+ let toasterObserver;
262
+ const resolveScope = () => document.getElementById("ms_lcw_webchat_root") ?? document.body;
263
+ if (!labelToaster(resolveScope())) {
264
+ toasterObserver = new MutationObserver(() => {
265
+ const scope = resolveScope();
266
+ if (labelToaster(scope)) {
267
+ var _toasterObserver;
268
+ (_toasterObserver = toasterObserver) === null || _toasterObserver === void 0 ? void 0 : _toasterObserver.disconnect();
269
+ toasterObserver = undefined;
270
+ }
271
+ });
272
+ toasterObserver.observe(resolveScope(), {
273
+ childList: true,
274
+ subtree: true
275
+ });
276
+ }
235
277
  dispatch({
236
278
  type: _LiveChatWidgetActionType.LiveChatWidgetActionType.SET_RENDERING_MIDDLEWARE_PROPS,
237
279
  payload: webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.renderingMiddlewareProps
@@ -256,6 +298,10 @@ const WebChatContainerStateful = props => {
256
298
  }
257
299
  }
258
300
  }
301
+ return () => {
302
+ var _toasterObserver2;
303
+ (_toasterObserver2 = toasterObserver) === null || _toasterObserver2 === void 0 ? void 0 : _toasterObserver2.disconnect();
304
+ };
259
305
  }, []);
260
306
  (0, _react2.useEffect)(() => {
261
307
  var _props$webChatContain6, _props$webChatContain7;
@@ -496,23 +542,7 @@ const WebChatContainerStateful = props => {
496
542
  height: "100%",
497
543
  width: "100%"
498
544
  }
499
- }, shouldLoadPersistentHistoryMessages && /*#__PURE__*/_react2.default.createElement(_WebChatEventSubscribers.default, null), /*#__PURE__*/_react2.default.createElement(BasicWebChat, null))), /*#__PURE__*/_react2.default.createElement("div", {
500
- id: _Constants.HtmlIdNames.fileSentAnnouncementRegionId,
501
- role: "alert",
502
- "aria-live": "assertive",
503
- "aria-atomic": "true",
504
- style: {
505
- position: "absolute",
506
- width: "1px",
507
- height: "1px",
508
- padding: "0",
509
- margin: "-1px",
510
- overflow: "hidden",
511
- clip: "rect(0, 0, 0, 0)",
512
- whiteSpace: "nowrap",
513
- border: "0"
514
- }
515
- }), citationPaneOpen && /*#__PURE__*/_react2.default.createElement(_CitationPaneStateful.default, {
545
+ }, shouldLoadPersistentHistoryMessages && /*#__PURE__*/_react2.default.createElement(_WebChatEventSubscribers.default, null), /*#__PURE__*/_react2.default.createElement(BasicWebChat, null))), citationPaneOpen && /*#__PURE__*/_react2.default.createElement(_CitationPaneStateful.default, {
516
546
  id: ((_props$citationPanePr = props.citationPaneProps) === null || _props$citationPanePr === void 0 ? void 0 : _props$citationPanePr.id) || _Constants.HtmlAttributeNames.ocwCitationPaneClassName,
517
547
  title: ((_props$citationPanePr2 = props.citationPaneProps) === null || _props$citationPanePr2 === void 0 ? void 0 : _props$citationPanePr2.title) || _Constants.HtmlAttributeNames.ocwCitationPaneTitle,
518
548
  contentHtml: citationPaneText,
@@ -20,6 +20,7 @@ const defaultWebChatContainerStatefulProps = {
20
20
  honorsTargetInHTMLLinks: false,
21
21
  hyperlinkTextOverride: false,
22
22
  directLine: new _mockadapter.default(),
23
- adaptiveCardStyles: _defaultAdaptiveCardStyles.defaultAdaptiveCardStyles
23
+ adaptiveCardStyles: _defaultAdaptiveCardStyles.defaultAdaptiveCardStyles,
24
+ webChatNotificationRegionAccessibilityLabel: "Chat notifications"
24
25
  };
25
26
  exports.defaultWebChatContainerStatefulProps = defaultWebChatContainerStatefulProps;
@@ -32,6 +32,21 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
32
32
  const container = containerRef.current;
33
33
  if (!container) return;
34
34
 
35
+ // dropdown-double-label: compact Input.ChoiceSet renders as a native
36
+ // <select> with both aria-labelledby and a sibling visible <label for>.
37
+ // Only remove aria-labelledby when it points solely at that same visible
38
+ // label; preserve composite labels that include required/error/context text.
39
+ const compactSelects = container.querySelectorAll(".ac-input-container select.ac-input.ac-multichoiceInput[aria-labelledby]");
40
+ compactSelects.forEach(select => {
41
+ const id = select.id;
42
+ if (!id) return;
43
+ const visibleLabel = container.querySelector(`label[for="${CSS.escape(id)}"]:not([aria-hidden='true'])`);
44
+ const labelledBy = (select.getAttribute("aria-labelledby") || "").trim().split(/\s+/);
45
+ if (visibleLabel !== null && visibleLabel !== void 0 && visibleLabel.id && labelledBy.length === 1 && labelledBy[0] === visibleLabel.id) {
46
+ select.removeAttribute("aria-labelledby");
47
+ }
48
+ });
49
+
35
50
  // action-button-toggle / login-button-toggle: submit/login action buttons can be
36
51
  // rendered with toggle-only ARIA that causes screen readers to announce them as
37
52
  // switches instead of plain push buttons. Preserve Action.ToggleVisibility buttons,
@@ -18,14 +18,10 @@ var _WebChatActionType = require("../../enums/WebChatActionType");
18
18
 
19
19
  const MBtoBRatio = 1000000;
20
20
  const announceFileSent = message => {
21
- // TalkBack on Android WebView reliably announces newly *appended* role="alert"
22
- // nodes but often misses text-content updates on existing nodes.
23
- // Strategy: clear the static region, wait 500ms for the send-box focus shift
24
- // to settle, then inject a fresh alert element and remove it after 3s.
25
- const region = document.getElementById(_Constants.HtmlIdNames.fileSentAnnouncementRegionId);
26
- if (region) {
27
- region.textContent = "";
28
- }
21
+ // TalkBack on Android WebView reliably announces newly *appended*
22
+ // role="alert" nodes but often misses text-content updates on existing
23
+ // nodes. Wait 500ms for the send-box focus shift to settle, then inject
24
+ // a fresh alert element appended to document.body and remove it after 3s.
29
25
  setTimeout(() => {
30
26
  const el = document.createElement("div");
31
27
  el.setAttribute("role", "alert");
@@ -16,6 +16,32 @@ let currentAgentName = _defaultWebChatStyles.defaultWebChatStyles.botAvatarIniti
16
16
 
17
17
  // Optional external updater (React context dispatch wrapper) set at runtime
18
18
  let externalInitialsUpdater;
19
+ const hasTransferTag = activity => {
20
+ var _activity$channelData;
21
+ const tags = [...(Array.isArray(activity === null || activity === void 0 ? void 0 : (_activity$channelData = activity.channelData) === null || _activity$channelData === void 0 ? void 0 : _activity$channelData.tags) ? activity.channelData.tags : []), ...(Array.isArray(activity === null || activity === void 0 ? void 0 : activity.tags) ? activity.tags : [])];
22
+ return tags.some(tag => typeof tag === "string" && tag.toLowerCase().includes("transfer"));
23
+ };
24
+ const isTransferSystemMessage = activity => {
25
+ const text = ((activity === null || activity === void 0 ? void 0 : activity.text) || "").toString();
26
+ return hasTransferTag(activity) || /\btransfer(?:red|ring)?\b/i.test(text);
27
+ };
28
+ const resetCachedAgentName = dispatch => {
29
+ if (currentAgentName !== _defaultWebChatStyles.defaultWebChatStyles.botAvatarInitials || currentAgentInitials !== _defaultWebChatStyles.defaultWebChatStyles.botAvatarInitials) {
30
+ var _externalInitialsUpda;
31
+ currentAgentName = _defaultWebChatStyles.defaultWebChatStyles.botAvatarInitials;
32
+ currentAgentInitials = _defaultWebChatStyles.defaultWebChatStyles.botAvatarInitials;
33
+ (_externalInitialsUpda = externalInitialsUpdater) === null || _externalInitialsUpda === void 0 ? void 0 : _externalInitialsUpda(currentAgentInitials || "");
34
+ _omnichannelChatComponents.BroadcastService.postMessage({
35
+ eventName: "BotAvatarInitialsUpdated",
36
+ payload: {
37
+ initials: currentAgentInitials
38
+ }
39
+ });
40
+ dispatch({
41
+ type: "__BOT_INITIALS_UPDATED__"
42
+ });
43
+ }
44
+ };
19
45
  const localizedStringsBotInitialsMiddleware = onInitialsChange => _ref => {
20
46
  let {
21
47
  dispatch
@@ -29,19 +55,31 @@ const localizedStringsBotInitialsMiddleware = onInitialsChange => _ref => {
29
55
  var _action$payload, _activity$from;
30
56
  const activity = (_action$payload = action.payload) === null || _action$payload === void 0 ? void 0 : _action$payload.activity;
31
57
  if (activity !== null && activity !== void 0 && (_activity$from = activity.from) !== null && _activity$from !== void 0 && _activity$from.name && activity.from.role !== _Constants.Constants.userMessageTag && activity.from.name !== _Constants.Constants.userMessageTag) {
32
- var _activity$channelData, _activity$channelData2, _activity$tags;
58
+ var _activity$channelData2, _activity$channelData3, _activity$tags;
33
59
  const agentName = activity.from.name.trim();
34
- const isSystemMessage = agentName === "__agent__" || agentName.startsWith("__") || ((_activity$channelData = activity.channelData) === null || _activity$channelData === void 0 ? void 0 : (_activity$channelData2 = _activity$channelData.tags) === null || _activity$channelData2 === void 0 ? void 0 : _activity$channelData2.includes(_Constants.Constants.systemMessageTag)) || ((_activity$tags = activity.tags) === null || _activity$tags === void 0 ? void 0 : _activity$tags.includes(_Constants.Constants.systemMessageTag));
35
- if (!isSystemMessage && agentName) {
60
+ const isSystemMessage = agentName === "__agent__" || agentName.startsWith("__") || ((_activity$channelData2 = activity.channelData) === null || _activity$channelData2 === void 0 ? void 0 : (_activity$channelData3 = _activity$channelData2.tags) === null || _activity$channelData3 === void 0 ? void 0 : _activity$channelData3.includes(_Constants.Constants.systemMessageTag)) || ((_activity$tags = activity.tags) === null || _activity$tags === void 0 ? void 0 : _activity$tags.includes(_Constants.Constants.systemMessageTag));
61
+ if (isSystemMessage) {
62
+ // transfer-stale-bot-name: when a transfer system message indicates the
63
+ // conversation has been routed to a different agent, the
64
+ // previously-cached agent name must NOT linger. Otherwise the
65
+ // next non-system activity (e.g. typing indicator, welcome
66
+ // message, or the new agent's first turn before their name
67
+ // is observed) is announced to screen readers as the OLD
68
+ // agent. Reset to defaults so the next observed name takes
69
+ // effect cleanly.
70
+ if (isTransferSystemMessage(activity)) {
71
+ resetCachedAgentName(dispatch);
72
+ }
73
+ } else if (agentName) {
36
74
  const newInitials = (0, _utils.getIconText)(agentName) || currentAgentInitials;
37
75
  const hasInitialsChanged = newInitials !== currentAgentInitials;
38
76
  const hasNameChanged = agentName !== currentAgentName;
39
77
  currentAgentName = agentName;
40
78
  if (hasInitialsChanged) {
41
- var _externalInitialsUpda;
79
+ var _externalInitialsUpda2;
42
80
  currentAgentInitials = newInitials;
43
81
  // Notify external React context if provided
44
- (_externalInitialsUpda = externalInitialsUpdater) === null || _externalInitialsUpda === void 0 ? void 0 : _externalInitialsUpda(currentAgentInitials || "");
82
+ (_externalInitialsUpda2 = externalInitialsUpdater) === null || _externalInitialsUpda2 === void 0 ? void 0 : _externalInitialsUpda2(currentAgentInitials || "");
45
83
  // Broadcast (optional) for multi-tab sync without forcing consumers
46
84
  _omnichannelChatComponents.BroadcastService.postMessage({
47
85
  eventName: "BotAvatarInitialsUpdated",
@@ -149,7 +149,6 @@ export let HtmlIdNames = /*#__PURE__*/_createClass(function HtmlIdNames() {
149
149
  _classCallCheck(this, HtmlIdNames);
150
150
  });
151
151
  _defineProperty(HtmlIdNames, "MSLiveChatWidget", "MSLiveChatWidget");
152
- _defineProperty(HtmlIdNames, "fileSentAnnouncementRegionId", "ms_lcw_file_sent_announcement");
153
152
  export let HtmlClassNames = /*#__PURE__*/_createClass(function HtmlClassNames() {
154
153
  _classCallCheck(this, HtmlClassNames);
155
154
  });
@@ -58,6 +58,7 @@ export const PostChatSurveyPaneStateful = props => {
58
58
  };
59
59
  const controlProps = {
60
60
  id: "oc-lcw-postchatsurvey-pane",
61
+ title: "Post-chat survey",
61
62
  surveyURL: ((_props$controlProps = props.controlProps) === null || _props$controlProps === void 0 ? void 0 : _props$controlProps.surveyURL) ?? surveyInviteLink,
62
63
  ...props.controlProps
63
64
  };
@@ -1,6 +1,6 @@
1
1
  import { HtmlAttributeNames, Regex } from "../../common/Constants";
2
2
  import { ConversationStage, LogLevel, TelemetryEvent } from "../../common/telemetry/TelemetryConstants";
3
- import React, { useEffect } from "react";
3
+ import React, { useEffect, useRef, useState } from "react";
4
4
  import { createTimer, extractPreChatSurveyResponseValues, findAllFocusableElement, getStateFromCache, getWidgetCacheId, isUndefinedOrEmpty, parseAdaptiveCardPayload } from "../../common/utils";
5
5
  import { ConversationState } from "../../contexts/common/ConversationState";
6
6
  import { LiveChatWidgetActionType } from "../../contexts/common/LiveChatWidgetActionType";
@@ -11,6 +11,34 @@ import { defaultGeneralPreChatSurveyPaneStyleProps } from "./common/defaultStyle
11
11
  import { defaultPreChatSurveyLocalizedTexts } from "./common/defaultProps/defaultPreChatSurveyLocalizedTexts";
12
12
  import useChatContextStore from "../../hooks/useChatContextStore";
13
13
  let uiTimer;
14
+ const getFocusedElementAnnouncement = element => {
15
+ var _element$textContent;
16
+ const ariaLabel = element.getAttribute(HtmlAttributeNames.ariaLabel);
17
+ if (ariaLabel) {
18
+ return ariaLabel.trim();
19
+ }
20
+ const ariaLabelledBy = element.getAttribute(HtmlAttributeNames.ariaLabelledby);
21
+ if (ariaLabelledBy) {
22
+ const labelledByText = ariaLabelledBy.split(/\s+/).map(id => {
23
+ var _document$getElementB, _document$getElementB2;
24
+ return (_document$getElementB = document.getElementById(id)) === null || _document$getElementB === void 0 ? void 0 : (_document$getElementB2 = _document$getElementB.textContent) === null || _document$getElementB2 === void 0 ? void 0 : _document$getElementB2.trim();
25
+ }).filter(Boolean).join(" ");
26
+ if (labelledByText) {
27
+ return labelledByText;
28
+ }
29
+ }
30
+ if (element.id) {
31
+ const label = Array.from(document.querySelectorAll("label")).find(candidate => candidate.htmlFor === element.id || candidate.getAttribute("for") === element.id);
32
+ if (label !== null && label !== void 0 && label.textContent) {
33
+ return label.textContent.trim();
34
+ }
35
+ }
36
+ const parentLabel = element.closest("label");
37
+ if (parentLabel !== null && parentLabel !== void 0 && parentLabel.textContent) {
38
+ return parentLabel.textContent.trim();
39
+ }
40
+ return ((_element$textContent = element.textContent) === null || _element$textContent === void 0 ? void 0 : _element$textContent.trim()) ?? "";
41
+ };
14
42
 
15
43
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
44
  export const PreChatSurveyPaneStateful = props => {
@@ -33,6 +61,8 @@ export const PreChatSurveyPaneStateful = props => {
33
61
  surveyProps,
34
62
  initStartChat
35
63
  } = props;
64
+ const [preChatFocusAnnouncement, setPreChatFocusAnnouncement] = useState("");
65
+ const liveRegionUpdateTimeout = useRef();
36
66
  const generalStyleProps = Object.assign({}, defaultGeneralPreChatSurveyPaneStyleProps, surveyProps === null || surveyProps === void 0 ? void 0 : (_surveyProps$stylePro = surveyProps.styleProps) === null || _surveyProps$stylePro === void 0 ? void 0 : _surveyProps$stylePro.generalStyleProps, {
37
67
  display: state.appStates.isMinimized ? "none" : ""
38
68
  });
@@ -119,6 +149,16 @@ export const PreChatSurveyPaneStateful = props => {
119
149
  ...(surveyProps === null || surveyProps === void 0 ? void 0 : surveyProps.styleProps),
120
150
  generalStyleProps: generalStyleProps
121
151
  };
152
+ const announceFocusedElement = event => {
153
+ if (liveRegionUpdateTimeout.current) {
154
+ window.clearTimeout(liveRegionUpdateTimeout.current);
155
+ }
156
+ const announcement = getFocusedElementAnnouncement(event.target);
157
+ setPreChatFocusAnnouncement("");
158
+ liveRegionUpdateTimeout.current = window.setTimeout(() => {
159
+ setPreChatFocusAnnouncement(announcement);
160
+ }, 0);
161
+ };
122
162
  useEffect(() => {
123
163
  // Set Aria-Label Attribute for Inputs
124
164
  const adaptiveCardElements = document.getElementsByClassName(HtmlAttributeNames.adaptiveCardClassName);
@@ -166,9 +206,31 @@ export const PreChatSurveyPaneStateful = props => {
166
206
  }
167
207
  }
168
208
  }, [state.appStates.isMinimized]);
169
- return /*#__PURE__*/React.createElement(PreChatSurveyPane, {
209
+ useEffect(() => () => {
210
+ if (liveRegionUpdateTimeout.current) {
211
+ window.clearTimeout(liveRegionUpdateTimeout.current);
212
+ }
213
+ }, []);
214
+ return /*#__PURE__*/React.createElement("div", {
215
+ onFocusCapture: announceFocusedElement
216
+ }, /*#__PURE__*/React.createElement("div", {
217
+ role: "status",
218
+ "aria-live": "polite",
219
+ "aria-atomic": "true",
220
+ style: {
221
+ position: "absolute",
222
+ width: "1px",
223
+ height: "1px",
224
+ margin: "-1px",
225
+ padding: 0,
226
+ border: 0,
227
+ clip: "rect(0, 0, 0, 0)",
228
+ overflow: "hidden",
229
+ whiteSpace: "nowrap"
230
+ }
231
+ }, preChatFocusAnnouncement), /*#__PURE__*/React.createElement(PreChatSurveyPane, {
170
232
  controlProps: controlProps,
171
233
  styleProps: styleProps
172
- });
234
+ }));
173
235
  };
174
236
  export default PreChatSurveyPaneStateful;
@@ -1,4 +1,4 @@
1
- import { Constants, HtmlAttributeNames, HtmlClassNames, HtmlIdNames } from "../../common/Constants";
1
+ import { Constants, HtmlAttributeNames, HtmlClassNames } from "../../common/Constants";
2
2
  import { Stack } from "@fluentui/react";
3
3
  import { LogLevel, TelemetryEvent } from "../../common/telemetry/TelemetryConstants";
4
4
  import React, { useEffect, useRef, useState } from "react";
@@ -223,6 +223,48 @@ export const WebChatContainerStateful = props => {
223
223
  chatHistoryElement.setAttribute(HtmlAttributeNames.ariaLabel, webChatContainerProps.webChatHistoryMobileAccessibilityLabel);
224
224
  }
225
225
  }
226
+
227
+ // internal tracking: WebChats BasicToaster renders a `role="log"` container
228
+ // with no accessible name. When the toaster is empty (typical idle
229
+ // state) screen readers traversing the page announce it as "blank".
230
+ // Inject an aria-label so the region has a meaningful name; the
231
+ // consumer can override via the new
232
+ // `webChatNotificationRegionAccessibilityLabel` prop. The toaster
233
+ // may not be rendered yet when this effect first runs, so observe
234
+ // for it via a MutationObserver scoped to the widget root (or, if
235
+ // not yet mounted, the widget container's parent) so we never fire
236
+ // on host-page mutations outside the widget subtree.
237
+ const toasterLabel = (webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.webChatNotificationRegionAccessibilityLabel) ?? defaultWebChatContainerStatefulProps.webChatNotificationRegionAccessibilityLabel ?? "Chat notifications";
238
+ const labelToaster = root => {
239
+ const toaster = root.querySelector(".webchat__toaster[role='log']");
240
+ if (toaster && !toaster.getAttribute(HtmlAttributeNames.ariaLabel)) {
241
+ toaster.setAttribute(HtmlAttributeNames.ariaLabel, toasterLabel);
242
+ return true;
243
+ }
244
+ return false;
245
+ };
246
+ // Prefer the widget root; if it is not yet in the DOM, fall back to
247
+ // `document` for the *initial* lookup only, then observe the most
248
+ // specific available scope (widget root if present, otherwise the
249
+ // widget's parent / document.body as a last resort). The observer
250
+ // re-resolves the widget root on every callback so we tighten scope
251
+ // as soon as it mounts.
252
+ let toasterObserver;
253
+ const resolveScope = () => document.getElementById("ms_lcw_webchat_root") ?? document.body;
254
+ if (!labelToaster(resolveScope())) {
255
+ toasterObserver = new MutationObserver(() => {
256
+ const scope = resolveScope();
257
+ if (labelToaster(scope)) {
258
+ var _toasterObserver;
259
+ (_toasterObserver = toasterObserver) === null || _toasterObserver === void 0 ? void 0 : _toasterObserver.disconnect();
260
+ toasterObserver = undefined;
261
+ }
262
+ });
263
+ toasterObserver.observe(resolveScope(), {
264
+ childList: true,
265
+ subtree: true
266
+ });
267
+ }
226
268
  dispatch({
227
269
  type: LiveChatWidgetActionType.SET_RENDERING_MIDDLEWARE_PROPS,
228
270
  payload: webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.renderingMiddlewareProps
@@ -247,6 +289,10 @@ export const WebChatContainerStateful = props => {
247
289
  }
248
290
  }
249
291
  }
292
+ return () => {
293
+ var _toasterObserver2;
294
+ (_toasterObserver2 = toasterObserver) === null || _toasterObserver2 === void 0 ? void 0 : _toasterObserver2.disconnect();
295
+ };
250
296
  }, []);
251
297
  useEffect(() => {
252
298
  var _props$webChatContain6, _props$webChatContain7;
@@ -487,23 +533,7 @@ export const WebChatContainerStateful = props => {
487
533
  height: "100%",
488
534
  width: "100%"
489
535
  }
490
- }, shouldLoadPersistentHistoryMessages && /*#__PURE__*/React.createElement(WebChatEventSubscribers, null), /*#__PURE__*/React.createElement(BasicWebChat, null))), /*#__PURE__*/React.createElement("div", {
491
- id: HtmlIdNames.fileSentAnnouncementRegionId,
492
- role: "alert",
493
- "aria-live": "assertive",
494
- "aria-atomic": "true",
495
- style: {
496
- position: "absolute",
497
- width: "1px",
498
- height: "1px",
499
- padding: "0",
500
- margin: "-1px",
501
- overflow: "hidden",
502
- clip: "rect(0, 0, 0, 0)",
503
- whiteSpace: "nowrap",
504
- border: "0"
505
- }
506
- }), citationPaneOpen && /*#__PURE__*/React.createElement(CitationPaneStateful, {
536
+ }, shouldLoadPersistentHistoryMessages && /*#__PURE__*/React.createElement(WebChatEventSubscribers, null), /*#__PURE__*/React.createElement(BasicWebChat, null))), citationPaneOpen && /*#__PURE__*/React.createElement(CitationPaneStateful, {
507
537
  id: ((_props$citationPanePr = props.citationPaneProps) === null || _props$citationPanePr === void 0 ? void 0 : _props$citationPanePr.id) || HtmlAttributeNames.ocwCitationPaneClassName,
508
538
  title: ((_props$citationPanePr2 = props.citationPaneProps) === null || _props$citationPanePr2 === void 0 ? void 0 : _props$citationPanePr2.title) || HtmlAttributeNames.ocwCitationPaneTitle,
509
539
  contentHtml: citationPaneText,
@@ -13,5 +13,6 @@ export const defaultWebChatContainerStatefulProps = {
13
13
  honorsTargetInHTMLLinks: false,
14
14
  hyperlinkTextOverride: false,
15
15
  directLine: new MockAdapter(),
16
- adaptiveCardStyles: defaultAdaptiveCardStyles
16
+ adaptiveCardStyles: defaultAdaptiveCardStyles,
17
+ webChatNotificationRegionAccessibilityLabel: "Chat notifications"
17
18
  };
@@ -25,6 +25,21 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
25
25
  const container = containerRef.current;
26
26
  if (!container) return;
27
27
 
28
+ // dropdown-double-label: compact Input.ChoiceSet renders as a native
29
+ // <select> with both aria-labelledby and a sibling visible <label for>.
30
+ // Only remove aria-labelledby when it points solely at that same visible
31
+ // label; preserve composite labels that include required/error/context text.
32
+ const compactSelects = container.querySelectorAll(".ac-input-container select.ac-input.ac-multichoiceInput[aria-labelledby]");
33
+ compactSelects.forEach(select => {
34
+ const id = select.id;
35
+ if (!id) return;
36
+ const visibleLabel = container.querySelector(`label[for="${CSS.escape(id)}"]:not([aria-hidden='true'])`);
37
+ const labelledBy = (select.getAttribute("aria-labelledby") || "").trim().split(/\s+/);
38
+ if (visibleLabel !== null && visibleLabel !== void 0 && visibleLabel.id && labelledBy.length === 1 && labelledBy[0] === visibleLabel.id) {
39
+ select.removeAttribute("aria-labelledby");
40
+ }
41
+ });
42
+
28
43
  // action-button-toggle / login-button-toggle: submit/login action buttons can be
29
44
  // rendered with toggle-only ARIA that causes screen readers to announce them as
30
45
  // switches instead of plain push buttons. Preserve Action.ToggleVisibility buttons,
@@ -5,21 +5,17 @@
5
5
  ******/
6
6
 
7
7
  import { LogLevel, TelemetryEvent } from "../../../../../common/telemetry/TelemetryConstants";
8
- import { AMSConstants, HtmlIdNames } from "../../../../../common/Constants";
8
+ import { AMSConstants } from "../../../../../common/Constants";
9
9
  import { NotificationHandler } from "../../notification/NotificationHandler";
10
10
  import { NotificationScenarios } from "../../enums/NotificationScenarios";
11
11
  import { TelemetryHelper } from "../../../../../common/telemetry/TelemetryHelper";
12
12
  import { WebChatActionType } from "../../enums/WebChatActionType";
13
13
  const MBtoBRatio = 1000000;
14
14
  const announceFileSent = message => {
15
- // TalkBack on Android WebView reliably announces newly *appended* role="alert"
16
- // nodes but often misses text-content updates on existing nodes.
17
- // Strategy: clear the static region, wait 500ms for the send-box focus shift
18
- // to settle, then inject a fresh alert element and remove it after 3s.
19
- const region = document.getElementById(HtmlIdNames.fileSentAnnouncementRegionId);
20
- if (region) {
21
- region.textContent = "";
22
- }
15
+ // TalkBack on Android WebView reliably announces newly *appended*
16
+ // role="alert" nodes but often misses text-content updates on existing
17
+ // nodes. Wait 500ms for the send-box focus shift to settle, then inject
18
+ // a fresh alert element appended to document.body and remove it after 3s.
23
19
  setTimeout(() => {
24
20
  const el = document.createElement("div");
25
21
  el.setAttribute("role", "alert");
@@ -10,6 +10,32 @@ let currentAgentName = defaultWebChatStyles.botAvatarInitials;
10
10
 
11
11
  // Optional external updater (React context dispatch wrapper) set at runtime
12
12
  let externalInitialsUpdater;
13
+ const hasTransferTag = activity => {
14
+ var _activity$channelData;
15
+ const tags = [...(Array.isArray(activity === null || activity === void 0 ? void 0 : (_activity$channelData = activity.channelData) === null || _activity$channelData === void 0 ? void 0 : _activity$channelData.tags) ? activity.channelData.tags : []), ...(Array.isArray(activity === null || activity === void 0 ? void 0 : activity.tags) ? activity.tags : [])];
16
+ return tags.some(tag => typeof tag === "string" && tag.toLowerCase().includes("transfer"));
17
+ };
18
+ const isTransferSystemMessage = activity => {
19
+ const text = ((activity === null || activity === void 0 ? void 0 : activity.text) || "").toString();
20
+ return hasTransferTag(activity) || /\btransfer(?:red|ring)?\b/i.test(text);
21
+ };
22
+ const resetCachedAgentName = dispatch => {
23
+ if (currentAgentName !== defaultWebChatStyles.botAvatarInitials || currentAgentInitials !== defaultWebChatStyles.botAvatarInitials) {
24
+ var _externalInitialsUpda;
25
+ currentAgentName = defaultWebChatStyles.botAvatarInitials;
26
+ currentAgentInitials = defaultWebChatStyles.botAvatarInitials;
27
+ (_externalInitialsUpda = externalInitialsUpdater) === null || _externalInitialsUpda === void 0 ? void 0 : _externalInitialsUpda(currentAgentInitials || "");
28
+ BroadcastService.postMessage({
29
+ eventName: "BotAvatarInitialsUpdated",
30
+ payload: {
31
+ initials: currentAgentInitials
32
+ }
33
+ });
34
+ dispatch({
35
+ type: "__BOT_INITIALS_UPDATED__"
36
+ });
37
+ }
38
+ };
13
39
  export const localizedStringsBotInitialsMiddleware = onInitialsChange => _ref => {
14
40
  let {
15
41
  dispatch
@@ -23,19 +49,31 @@ export const localizedStringsBotInitialsMiddleware = onInitialsChange => _ref =>
23
49
  var _action$payload, _activity$from;
24
50
  const activity = (_action$payload = action.payload) === null || _action$payload === void 0 ? void 0 : _action$payload.activity;
25
51
  if (activity !== null && activity !== void 0 && (_activity$from = activity.from) !== null && _activity$from !== void 0 && _activity$from.name && activity.from.role !== Constants.userMessageTag && activity.from.name !== Constants.userMessageTag) {
26
- var _activity$channelData, _activity$channelData2, _activity$tags;
52
+ var _activity$channelData2, _activity$channelData3, _activity$tags;
27
53
  const agentName = activity.from.name.trim();
28
- const isSystemMessage = agentName === "__agent__" || agentName.startsWith("__") || ((_activity$channelData = activity.channelData) === null || _activity$channelData === void 0 ? void 0 : (_activity$channelData2 = _activity$channelData.tags) === null || _activity$channelData2 === void 0 ? void 0 : _activity$channelData2.includes(Constants.systemMessageTag)) || ((_activity$tags = activity.tags) === null || _activity$tags === void 0 ? void 0 : _activity$tags.includes(Constants.systemMessageTag));
29
- if (!isSystemMessage && agentName) {
54
+ const isSystemMessage = agentName === "__agent__" || agentName.startsWith("__") || ((_activity$channelData2 = activity.channelData) === null || _activity$channelData2 === void 0 ? void 0 : (_activity$channelData3 = _activity$channelData2.tags) === null || _activity$channelData3 === void 0 ? void 0 : _activity$channelData3.includes(Constants.systemMessageTag)) || ((_activity$tags = activity.tags) === null || _activity$tags === void 0 ? void 0 : _activity$tags.includes(Constants.systemMessageTag));
55
+ if (isSystemMessage) {
56
+ // transfer-stale-bot-name: when a transfer system message indicates the
57
+ // conversation has been routed to a different agent, the
58
+ // previously-cached agent name must NOT linger. Otherwise the
59
+ // next non-system activity (e.g. typing indicator, welcome
60
+ // message, or the new agent's first turn before their name
61
+ // is observed) is announced to screen readers as the OLD
62
+ // agent. Reset to defaults so the next observed name takes
63
+ // effect cleanly.
64
+ if (isTransferSystemMessage(activity)) {
65
+ resetCachedAgentName(dispatch);
66
+ }
67
+ } else if (agentName) {
30
68
  const newInitials = getIconText(agentName) || currentAgentInitials;
31
69
  const hasInitialsChanged = newInitials !== currentAgentInitials;
32
70
  const hasNameChanged = agentName !== currentAgentName;
33
71
  currentAgentName = agentName;
34
72
  if (hasInitialsChanged) {
35
- var _externalInitialsUpda;
73
+ var _externalInitialsUpda2;
36
74
  currentAgentInitials = newInitials;
37
75
  // Notify external React context if provided
38
- (_externalInitialsUpda = externalInitialsUpdater) === null || _externalInitialsUpda === void 0 ? void 0 : _externalInitialsUpda(currentAgentInitials || "");
76
+ (_externalInitialsUpda2 = externalInitialsUpdater) === null || _externalInitialsUpda2 === void 0 ? void 0 : _externalInitialsUpda2(currentAgentInitials || "");
39
77
  // Broadcast (optional) for multi-tab sync without forcing consumers
40
78
  BroadcastService.postMessage({
41
79
  eventName: "BotAvatarInitialsUpdated",
@@ -119,7 +119,6 @@ export declare const Regex: {
119
119
  };
120
120
  export declare class HtmlIdNames {
121
121
  static readonly MSLiveChatWidget = "MSLiveChatWidget";
122
- static readonly fileSentAnnouncementRegionId = "ms_lcw_file_sent_announcement";
123
122
  }
124
123
  export declare class HtmlClassNames {
125
124
  static readonly webChatBannerCloseButton = "webchat__toast__dismissButton";
@@ -25,4 +25,11 @@ export interface IWebChatContainerStatefulProps {
25
25
  adaptiveCardStyles?: IAdaptiveCardStyles;
26
26
  sendBoxTextBox?: ISendBox;
27
27
  webChatHistoryMobileAccessibilityLabel?: string;
28
+ /**
29
+ * Accessible name applied to WebChats notification toaster region
30
+ * (`role="log"`). When omitted, defaults to "Chat notifications" so
31
+ * screen readers don't announce the empty live region as "blank"
32
+ * (internal tracking).
33
+ */
34
+ webChatNotificationRegionAccessibilityLabel?: string;
28
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/omnichannel-chat-widget",
3
- "version": "1.8.4-main.6b93408",
3
+ "version": "1.8.4-main.810d050",
4
4
  "description": "Microsoft Omnichannel Chat Widget",
5
5
  "main": "lib/cjs/index.js",
6
6
  "types": "lib/types/index.d.ts",
@@ -84,7 +84,7 @@
84
84
  "react-test-renderer": "^18.3.1",
85
85
  "rimraf": "^6.0.1",
86
86
  "storybook-addon-playwright": "^4.9.2",
87
- "swiper": "^9.0.5",
87
+ "swiper": "^12.1.2",
88
88
  "terser-webpack-plugin": "^4.2.3",
89
89
  "thread-loader": "^2.1.3",
90
90
  "ts-loader": "^9.2.6",
@@ -95,7 +95,7 @@
95
95
  "dependencies": {
96
96
  "@azure/core-tracing": "^1.2.0",
97
97
  "@microsoft/applicationinsights-web": "^3.3.6",
98
- "@microsoft/omnichannel-chat-components": "1.1.17-main.d4c4cb2",
98
+ "@microsoft/omnichannel-chat-components": "1.1.17-main.f21df63",
99
99
  "@microsoft/omnichannel-chat-sdk": "1.11.9-main.da8219b",
100
100
  "@opentelemetry/api": "^1.9.0",
101
101
  "abort-controller": "^3",
@@ -172,7 +172,7 @@
172
172
  "**/lodash": "4.17.23",
173
173
  "**/@babel/runtime-corejs3": "^7.29.0",
174
174
  "**/brace-expansion": "2.0.3",
175
- "**/swiper": "9.4.1"
175
+ "**/swiper": "12.1.2"
176
176
  },
177
177
  "jest": {
178
178
  "verbose": true,