@microsoft/omnichannel-chat-widget 1.8.4-main.1dd5564 → 1.8.4-main.1f36474

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 (26) hide show
  1. package/lib/cjs/common/Constants.js +0 -1
  2. package/lib/cjs/components/citationpanestateful/CitationPaneStateful.js +7 -1
  3. package/lib/cjs/components/confirmationpanestateful/ConfirmationPaneStateful.js +11 -1
  4. package/lib/cjs/components/emailtranscriptpanestateful/EmailTranscriptPaneStateful.js +5 -1
  5. package/lib/cjs/components/webchatcontainerstateful/WebChatContainerStateful.js +47 -17
  6. package/lib/cjs/components/webchatcontainerstateful/common/DesignerChatAdapter.js +10 -0
  7. package/lib/cjs/components/webchatcontainerstateful/common/defaultProps/defaultWebChatContainerStatefulProps.js +2 -1
  8. package/lib/cjs/components/webchatcontainerstateful/common/utils/chatAdapterUtils.js +3 -1
  9. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +50 -0
  10. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +4 -8
  11. package/lib/cjs/contexts/common/LiveChatWidgetContextInitialState.js +11 -0
  12. package/lib/esm/common/Constants.js +0 -1
  13. package/lib/esm/components/citationpanestateful/CitationPaneStateful.js +7 -1
  14. package/lib/esm/components/confirmationpanestateful/ConfirmationPaneStateful.js +11 -1
  15. package/lib/esm/components/emailtranscriptpanestateful/EmailTranscriptPaneStateful.js +5 -1
  16. package/lib/esm/components/webchatcontainerstateful/WebChatContainerStateful.js +48 -18
  17. package/lib/esm/components/webchatcontainerstateful/common/DesignerChatAdapter.js +10 -0
  18. package/lib/esm/components/webchatcontainerstateful/common/defaultProps/defaultWebChatContainerStatefulProps.js +2 -1
  19. package/lib/esm/components/webchatcontainerstateful/common/utils/chatAdapterUtils.js +3 -1
  20. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +50 -0
  21. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +5 -9
  22. package/lib/esm/contexts/common/LiveChatWidgetContextInitialState.js +11 -0
  23. package/lib/types/common/Constants.d.ts +0 -1
  24. package/lib/types/components/webchatcontainerstateful/common/utils/chatAdapterUtils.d.ts +1 -1
  25. package/lib/types/components/webchatcontainerstateful/interfaces/IWebChatContainerStatefulProps.d.ts +7 -0
  26. 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
  });
@@ -62,7 +62,13 @@ const CitationPaneStateful = props => {
62
62
  Event: _TelemetryConstants.TelemetryEvent.UXCitationPaneCompleted,
63
63
  ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
64
64
  });
65
- return cleanup;
65
+ return () => {
66
+ // internal tracking: restore parent tab indices on unmount so focus
67
+ // can escape the widget even if the pane is dismissed by an
68
+ // external state change rather than a user button click.
69
+ (0, _utils.setTabIndices)(elements, initialTabIndexMap, true);
70
+ cleanup();
71
+ };
66
72
  }, []);
67
73
 
68
74
  // Retry focus once pane is actually visible (isReady) in case initial attempt occurred while wrapper was visibility:hidden
@@ -100,7 +100,17 @@ const ConfirmationPaneStateful = props => {
100
100
  Event: _TelemetryConstants.TelemetryEvent.UXConfirmationPaneCompleted,
101
101
  ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
102
102
  });
103
- return cleanup;
103
+ return () => {
104
+ // internal tracking: when the confirmation pane unmounts (page reload,
105
+ // parent state change, or any path that bypasses onConfirm /
106
+ // onCancel) the tab indices that were forced to -1 on sibling
107
+ // focusable elements must be restored, otherwise Tab order
108
+ // remains broken and focus can be trapped inside the widget.
109
+ // setTabIndices is a no-op when initialTabIndexMap is already
110
+ // empty (onConfirm / onCancel already restored).
111
+ (0, _utils.setTabIndices)(elements, initialTabIndexMap, true);
112
+ cleanup();
113
+ };
104
114
  }, []);
105
115
  return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_DimLayer.DimLayer, {
106
116
  brightness: (controlProps === null || controlProps === void 0 ? void 0 : controlProps.brightnessValueOnDim) ?? "0.2"
@@ -136,7 +136,11 @@ const EmailTranscriptPaneStateful = props => {
136
136
  Event: _TelemetryConstants.TelemetryEvent.UXEmailTranscriptPaneCompleted,
137
137
  ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
138
138
  });
139
- return cleanup;
139
+ return () => {
140
+ // internal tracking: restore parent tab indices on unmount.
141
+ (0, _utils.setTabIndices)(elements, initialTabIndexMap, true);
142
+ cleanup();
143
+ };
140
144
  }, [initialEmail]);
141
145
  return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_DimLayer.DimLayer, {
142
146
  brightness: (controlProps === null || controlProps === void 0 ? void 0 : controlProps.brightnessValueOnDim) ?? "0.2"
@@ -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,
@@ -59,6 +59,16 @@ let DesignerChatAdapter = /*#__PURE__*/function (_MockAdapter) {
59
59
  if (msg.text) {
60
60
  if (msg.suggestedActions) {
61
61
  (0, _chatAdapterUtils.postAgentSuggestedActionsActivity)(this.activityObserver, msg.text, msg.suggestedActions, index * 100);
62
+ } else if (msg.from && msg.from.role) {
63
+ // Route caller-supplied `from` through the shared
64
+ // postBotMessageActivity helper so the activity shape
65
+ // (id, channelData.tags, timestamp, type) matches every
66
+ // other designer-mode path and downstream middleware
67
+ // (localizedStringsBotInitialsMiddleware, telemetry,
68
+ // attachment plumbing) behaves identically. The override
69
+ // narrows only the fields the caller cares about (e.g.
70
+ // from.name / from.role) — base botUser fields remain.
71
+ (0, _chatAdapterUtils.postBotMessageActivity)(this.activityObserver, msg.text, undefined, index * 100, msg.from);
62
72
  } else {
63
73
  (0, _chatAdapterUtils.postBotMessageActivity)(this.activityObserver, msg.text, undefined, index * 100);
64
74
  }
@@ -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;
@@ -44,11 +44,13 @@ exports.postEchoActivity = postEchoActivity;
44
44
  const postBotMessageActivity = function (activityObserver, text) {
45
45
  let tags = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "";
46
46
  let delay = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1000;
47
+ let fromOverride = arguments.length > 4 ? arguments[4] : undefined;
47
48
  setTimeout(() => {
48
49
  activityObserver === null || activityObserver === void 0 ? void 0 : activityObserver.next({
49
50
  id: (0, _omnichannelChatSdk.uuidv4)(),
50
51
  from: {
51
- ...botUser
52
+ ...botUser,
53
+ ...(fromOverride ?? {})
52
54
  },
53
55
  text,
54
56
  type: "message",
@@ -32,6 +32,56 @@ 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
+
50
+ // internal tracking: non-radio inputs (Input.Text including
51
+ // isMultiline=true which renders as <textarea>, Input.Date,
52
+ // Input.Number, Input.Toggle, Input.ChoiceSet multi-select
53
+ // checkbox) rendered by adaptivecards can have THREE accessible-
54
+ // name sources resolving to the same string: a visible
55
+ // <label for>, aria-label, AND aria-labelledby. TalkBack walks
56
+ // each accessibility node independently and reads the duplicated
57
+ // name. When a visible <label for> exists and names the control,
58
+ // strip the redundant aria-label / aria-labelledby on the input
59
+ // so only one announceable source remains.
60
+ const labelledInputs = container.querySelectorAll(".ac-input-container input.ac-input[id]:not([type='radio']), .ac-input-container textarea.ac-input[id]");
61
+ labelledInputs.forEach(input => {
62
+ const id = input.id;
63
+ if (!id) return;
64
+ const visibleLabel = container.querySelector(`label[for="${CSS.escape(id)}"]:not([aria-hidden='true'])`);
65
+ if (!visibleLabel) return;
66
+ const labelText = (visibleLabel.textContent || "").trim();
67
+ if (!labelText) return;
68
+ const ariaLabel = input.getAttribute("aria-label");
69
+ if (ariaLabel && ariaLabel.trim() === labelText) {
70
+ input.removeAttribute("aria-label");
71
+ }
72
+ const labelledBy = (input.getAttribute("aria-labelledby") || "").trim();
73
+ if (labelledBy) {
74
+ const ids = labelledBy.split(/\s+/);
75
+ const resolved = ids.map(rid => {
76
+ var _document$getElementB;
77
+ return (((_document$getElementB = document.getElementById(rid)) === null || _document$getElementB === void 0 ? void 0 : _document$getElementB.textContent) || "").trim();
78
+ }).filter(Boolean).join(" ");
79
+ if (resolved === labelText) {
80
+ input.removeAttribute("aria-labelledby");
81
+ }
82
+ }
83
+ });
84
+
35
85
  // action-button-toggle / login-button-toggle: submit/login action buttons can be
36
86
  // rendered with toggle-only ARIA that causes screen readers to announce them as
37
87
  // 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");
@@ -48,6 +48,17 @@ const getLiveChatWidgetContextInitialState = props => {
48
48
  if (initialStateFromCache.appStates.isUserAuthenticated === undefined) {
49
49
  initialStateFromCache.appStates.isUserAuthenticated = false;
50
50
  }
51
+ // internal tracking: transient modal pane visibility must NOT survive a page
52
+ // reload. If `showConfirmationPane` / `showEmailTranscriptPane` are
53
+ // restored as `true`, the corresponding pane re-mounts and installs
54
+ // its focus trap (and sets sibling tabindex=-1). The user has no
55
+ // context for why the modal appeared and Tab from the host page can
56
+ // no longer traverse out of the widget. Always start cold for these
57
+ // ephemeral UI flags.
58
+ if (initialStateFromCache.uiStates) {
59
+ initialStateFromCache.uiStates.showConfirmationPane = false;
60
+ initialStateFromCache.uiStates.showEmailTranscriptPane = false;
61
+ }
51
62
  return initialStateFromCache;
52
63
  }
53
64
  const LiveChatWidgetContextInitialState = {
@@ -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
  });
@@ -53,7 +53,13 @@ export const CitationPaneStateful = props => {
53
53
  Event: TelemetryEvent.UXCitationPaneCompleted,
54
54
  ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
55
55
  });
56
- return cleanup;
56
+ return () => {
57
+ // internal tracking: restore parent tab indices on unmount so focus
58
+ // can escape the widget even if the pane is dismissed by an
59
+ // external state change rather than a user button click.
60
+ setTabIndices(elements, initialTabIndexMap, true);
61
+ cleanup();
62
+ };
57
63
  }, []);
58
64
 
59
65
  // Retry focus once pane is actually visible (isReady) in case initial attempt occurred while wrapper was visibility:hidden
@@ -91,7 +91,17 @@ export const ConfirmationPaneStateful = props => {
91
91
  Event: TelemetryEvent.UXConfirmationPaneCompleted,
92
92
  ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
93
93
  });
94
- return cleanup;
94
+ return () => {
95
+ // internal tracking: when the confirmation pane unmounts (page reload,
96
+ // parent state change, or any path that bypasses onConfirm /
97
+ // onCancel) the tab indices that were forced to -1 on sibling
98
+ // focusable elements must be restored, otherwise Tab order
99
+ // remains broken and focus can be trapped inside the widget.
100
+ // setTabIndices is a no-op when initialTabIndexMap is already
101
+ // empty (onConfirm / onCancel already restored).
102
+ setTabIndices(elements, initialTabIndexMap, true);
103
+ cleanup();
104
+ };
95
105
  }, []);
96
106
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DimLayer, {
97
107
  brightness: (controlProps === null || controlProps === void 0 ? void 0 : controlProps.brightnessValueOnDim) ?? "0.2"
@@ -127,7 +127,11 @@ export const EmailTranscriptPaneStateful = props => {
127
127
  Event: TelemetryEvent.UXEmailTranscriptPaneCompleted,
128
128
  ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
129
129
  });
130
- return cleanup;
130
+ return () => {
131
+ // internal tracking: restore parent tab indices on unmount.
132
+ setTabIndices(elements, initialTabIndexMap, true);
133
+ cleanup();
134
+ };
131
135
  }, [initialEmail]);
132
136
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DimLayer, {
133
137
  brightness: (controlProps === null || controlProps === void 0 ? void 0 : controlProps.brightnessValueOnDim) ?? "0.2"
@@ -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,
@@ -52,6 +52,16 @@ export let DesignerChatAdapter = /*#__PURE__*/function (_MockAdapter) {
52
52
  if (msg.text) {
53
53
  if (msg.suggestedActions) {
54
54
  postAgentSuggestedActionsActivity(this.activityObserver, msg.text, msg.suggestedActions, index * 100);
55
+ } else if (msg.from && msg.from.role) {
56
+ // Route caller-supplied `from` through the shared
57
+ // postBotMessageActivity helper so the activity shape
58
+ // (id, channelData.tags, timestamp, type) matches every
59
+ // other designer-mode path and downstream middleware
60
+ // (localizedStringsBotInitialsMiddleware, telemetry,
61
+ // attachment plumbing) behaves identically. The override
62
+ // narrows only the fields the caller cares about (e.g.
63
+ // from.name / from.role) — base botUser fields remain.
64
+ postBotMessageActivity(this.activityObserver, msg.text, undefined, index * 100, msg.from);
55
65
  } else {
56
66
  postBotMessageActivity(this.activityObserver, msg.text, undefined, index * 100);
57
67
  }
@@ -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
  };
@@ -34,11 +34,13 @@ export const postEchoActivity = function (activityObserver, activity, user) {
34
34
  export const postBotMessageActivity = function (activityObserver, text) {
35
35
  let tags = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "";
36
36
  let delay = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1000;
37
+ let fromOverride = arguments.length > 4 ? arguments[4] : undefined;
37
38
  setTimeout(() => {
38
39
  activityObserver === null || activityObserver === void 0 ? void 0 : activityObserver.next({
39
40
  id: uuidv4(),
40
41
  from: {
41
- ...botUser
42
+ ...botUser,
43
+ ...(fromOverride ?? {})
42
44
  },
43
45
  text,
44
46
  type: "message",
@@ -25,6 +25,56 @@ 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
+
43
+ // internal tracking: non-radio inputs (Input.Text including
44
+ // isMultiline=true which renders as <textarea>, Input.Date,
45
+ // Input.Number, Input.Toggle, Input.ChoiceSet multi-select
46
+ // checkbox) rendered by adaptivecards can have THREE accessible-
47
+ // name sources resolving to the same string: a visible
48
+ // <label for>, aria-label, AND aria-labelledby. TalkBack walks
49
+ // each accessibility node independently and reads the duplicated
50
+ // name. When a visible <label for> exists and names the control,
51
+ // strip the redundant aria-label / aria-labelledby on the input
52
+ // so only one announceable source remains.
53
+ const labelledInputs = container.querySelectorAll(".ac-input-container input.ac-input[id]:not([type='radio']), .ac-input-container textarea.ac-input[id]");
54
+ labelledInputs.forEach(input => {
55
+ const id = input.id;
56
+ if (!id) return;
57
+ const visibleLabel = container.querySelector(`label[for="${CSS.escape(id)}"]:not([aria-hidden='true'])`);
58
+ if (!visibleLabel) return;
59
+ const labelText = (visibleLabel.textContent || "").trim();
60
+ if (!labelText) return;
61
+ const ariaLabel = input.getAttribute("aria-label");
62
+ if (ariaLabel && ariaLabel.trim() === labelText) {
63
+ input.removeAttribute("aria-label");
64
+ }
65
+ const labelledBy = (input.getAttribute("aria-labelledby") || "").trim();
66
+ if (labelledBy) {
67
+ const ids = labelledBy.split(/\s+/);
68
+ const resolved = ids.map(rid => {
69
+ var _document$getElementB;
70
+ return (((_document$getElementB = document.getElementById(rid)) === null || _document$getElementB === void 0 ? void 0 : _document$getElementB.textContent) || "").trim();
71
+ }).filter(Boolean).join(" ");
72
+ if (resolved === labelText) {
73
+ input.removeAttribute("aria-labelledby");
74
+ }
75
+ }
76
+ });
77
+
28
78
  // action-button-toggle / login-button-toggle: submit/login action buttons can be
29
79
  // rendered with toggle-only ARIA that causes screen readers to announce them as
30
80
  // 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");
@@ -42,6 +42,17 @@ export const getLiveChatWidgetContextInitialState = props => {
42
42
  if (initialStateFromCache.appStates.isUserAuthenticated === undefined) {
43
43
  initialStateFromCache.appStates.isUserAuthenticated = false;
44
44
  }
45
+ // internal tracking: transient modal pane visibility must NOT survive a page
46
+ // reload. If `showConfirmationPane` / `showEmailTranscriptPane` are
47
+ // restored as `true`, the corresponding pane re-mounts and installs
48
+ // its focus trap (and sets sibling tabindex=-1). The user has no
49
+ // context for why the modal appeared and Tab from the host page can
50
+ // no longer traverse out of the widget. Always start cold for these
51
+ // ephemeral UI flags.
52
+ if (initialStateFromCache.uiStates) {
53
+ initialStateFromCache.uiStates.showConfirmationPane = false;
54
+ initialStateFromCache.uiStates.showEmailTranscriptPane = false;
55
+ }
45
56
  return initialStateFromCache;
46
57
  }
47
58
  const LiveChatWidgetContextInitialState = {
@@ -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";
@@ -4,7 +4,7 @@ export declare const customerUser: User;
4
4
  export declare const botUser: User;
5
5
  export declare const agentUser: User;
6
6
  export declare const postEchoActivity: (activityObserver: Subscriber<Activity> | undefined, activity: Message, user: User, delay?: number) => void;
7
- export declare const postBotMessageActivity: (activityObserver: Subscriber<Activity> | undefined, text: string, tags?: string, delay?: number) => void;
7
+ export declare const postBotMessageActivity: (activityObserver: Subscriber<Activity> | undefined, text: string, tags?: string, delay?: number, fromOverride?: Partial<User>) => void;
8
8
  export declare const postAgentMessageActivity: (activityObserver: Subscriber<Activity> | undefined, text: string, tags?: string, delay?: number) => void;
9
9
  export declare const postSystemMessageActivity: (activityObserver: Subscriber<Activity> | undefined, text: string, delay?: number) => void;
10
10
  export declare const postBotTypingActivity: (activityObserver: Subscriber<Activity> | undefined, delay?: number) => void;
@@ -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.1dd5564",
3
+ "version": "1.8.4-main.1f36474",
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,