@microsoft/omnichannel-chat-widget 1.8.4-main.f6a1732 → 1.8.4-main.f8ae4f8

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 (45) hide show
  1. package/lib/cjs/common/Constants.js +0 -1
  2. package/lib/cjs/common/telemetry/TelemetryManager.js +6 -1
  3. package/lib/cjs/common/telemetry/sanitizeSasInPayload.js +82 -0
  4. package/lib/cjs/components/citationpanestateful/CitationPaneStateful.js +7 -1
  5. package/lib/cjs/components/confirmationpanestateful/ConfirmationPaneStateful.js +11 -1
  6. package/lib/cjs/components/emailtranscriptpanestateful/EmailTranscriptPaneStateful.js +5 -1
  7. package/lib/cjs/components/livechatwidget/common/createMarkdown.js +4 -2
  8. package/lib/cjs/components/livechatwidget/common/initWebChatComposer.js +16 -12
  9. package/lib/cjs/components/postchatloadingpanestateful/PostChatLoadingPaneStateful.js +15 -3
  10. package/lib/cjs/components/postchatsurveypanestateful/PostChatSurveyPaneStateful.js +1 -0
  11. package/lib/cjs/components/prechatsurveypanestateful/PreChatSurveyPaneStateful.js +68 -2
  12. package/lib/cjs/components/prechatsurveypanestateful/common/defaultStyles/defaultGeneralPreChatSurveyPaneStyleProps.js +1 -2
  13. package/lib/cjs/components/webchatcontainerstateful/WebChatContainerStateful.js +47 -17
  14. package/lib/cjs/components/webchatcontainerstateful/common/DesignerChatAdapter.js +10 -0
  15. package/lib/cjs/components/webchatcontainerstateful/common/defaultProps/defaultWebChatContainerStatefulProps.js +2 -1
  16. package/lib/cjs/components/webchatcontainerstateful/common/utils/chatAdapterUtils.js +3 -1
  17. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +50 -0
  18. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +4 -8
  19. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/localizedStringsBotInitialsMiddleware.js +43 -5
  20. package/lib/cjs/contexts/common/LiveChatWidgetContextInitialState.js +11 -0
  21. package/lib/esm/common/Constants.js +0 -1
  22. package/lib/esm/common/telemetry/TelemetryManager.js +6 -1
  23. package/lib/esm/common/telemetry/sanitizeSasInPayload.js +75 -0
  24. package/lib/esm/components/citationpanestateful/CitationPaneStateful.js +7 -1
  25. package/lib/esm/components/confirmationpanestateful/ConfirmationPaneStateful.js +11 -1
  26. package/lib/esm/components/emailtranscriptpanestateful/EmailTranscriptPaneStateful.js +5 -1
  27. package/lib/esm/components/livechatwidget/common/createMarkdown.js +4 -2
  28. package/lib/esm/components/livechatwidget/common/initWebChatComposer.js +16 -12
  29. package/lib/esm/components/postchatloadingpanestateful/PostChatLoadingPaneStateful.js +16 -4
  30. package/lib/esm/components/postchatsurveypanestateful/PostChatSurveyPaneStateful.js +1 -0
  31. package/lib/esm/components/prechatsurveypanestateful/PreChatSurveyPaneStateful.js +69 -3
  32. package/lib/esm/components/prechatsurveypanestateful/common/defaultStyles/defaultGeneralPreChatSurveyPaneStyleProps.js +1 -2
  33. package/lib/esm/components/webchatcontainerstateful/WebChatContainerStateful.js +48 -18
  34. package/lib/esm/components/webchatcontainerstateful/common/DesignerChatAdapter.js +10 -0
  35. package/lib/esm/components/webchatcontainerstateful/common/defaultProps/defaultWebChatContainerStatefulProps.js +2 -1
  36. package/lib/esm/components/webchatcontainerstateful/common/utils/chatAdapterUtils.js +3 -1
  37. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +50 -0
  38. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +5 -9
  39. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/localizedStringsBotInitialsMiddleware.js +43 -5
  40. package/lib/esm/contexts/common/LiveChatWidgetContextInitialState.js +11 -0
  41. package/lib/types/common/Constants.d.ts +0 -1
  42. package/lib/types/common/telemetry/sanitizeSasInPayload.d.ts +24 -0
  43. package/lib/types/components/webchatcontainerstateful/common/utils/chatAdapterUtils.d.ts +1 -1
  44. package/lib/types/components/webchatcontainerstateful/interfaces/IWebChatContainerStatefulProps.d.ts +7 -0
  45. package/package.json +5 -5
@@ -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
  });
@@ -11,6 +11,7 @@ var _appInsightsLogger = require("./loggers/appInsightsLogger");
11
11
  var _ariaTelemetryLogger = require("./loggers/ariaTelemetryLogger");
12
12
  var _consoleLogger = require("./loggers/consoleLogger");
13
13
  var _defaultAriaConfig = require("./defaultConfigs/defaultAriaConfig");
14
+ var _sanitizeSasInPayload = require("./sanitizeSasInPayload");
14
15
  function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
15
16
  function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
16
17
  function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
@@ -81,11 +82,15 @@ const RegisterLoggers = () => {
81
82
  };
82
83
  };
83
84
  const logTelemetry = telemetryEvent => {
85
+ // Redact Azure SAS credentials in any URL embedded in the payload (e.g.
86
+ // customer-supplied blob asset URLs in widget customization props) before
87
+ // it fans out to console, Aria, AppInsights, or any custom logger.
88
+ const sanitizedPayload = (0, _sanitizeSasInPayload.sanitizeSasInPayload)(telemetryEvent === null || telemetryEvent === void 0 ? void 0 : telemetryEvent.payload);
84
89
  loggers.map(logger => {
85
90
  var _payload, _telemetryInput$paylo;
86
91
  const logLevel = telemetryEvent.logLevel ?? _TelemetryConstants.LogLevel.INFO;
87
92
  const scenarioType = ((_payload = telemetryEvent.payload) === null || _payload === void 0 ? void 0 : _payload.scenarioType) ?? _TelemetryConstants.ScenarioType.UNDEFINED;
88
- const telemetryInput = parseInput(telemetryEvent === null || telemetryEvent === void 0 ? void 0 : telemetryEvent.payload, scenarioType);
93
+ const telemetryInput = parseInput(sanitizedPayload, scenarioType);
89
94
  telemetryInput.telemetryInfo = {
90
95
  telemetryInfo: _TelemetryHelper.TelemetryHelper.buildTelemetryEvent(logLevel, telemetryInput)
91
96
  };
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.sanitizeSasInPayload = void 0;
7
+ /**
8
+ * Redacts the values of Azure Shared Access Signature (SAS) query parameters in any
9
+ * URL embedded in a telemetry payload, so customer-supplied SAS URLs (e.g. blob asset
10
+ * URLs in widget customization props) cannot leak credentials through Aria/AppInsights.
11
+ *
12
+ * Trigger: any URL whose query string contains `sig=`. When detected, every well-known
13
+ * SAS parameter value in that query is replaced with `[REDACTED]`. Other URLs and other
14
+ * substrings of the payload pass through unchanged.
15
+ *
16
+ * Coverage of SAS parameter names:
17
+ * - Service/Blob SAS: sig, sv, se, st, sp, spr, sr, sip
18
+ * - Account SAS: ss, srt
19
+ * - User-delegation key SAS: skoid, sktid, skt, ske, sks, skv, saoid, suoid, sce, sdd
20
+ * - Response header overrides: rscc, rscd, rsce, rscl, rsct
21
+ *
22
+ * Reference: https://learn.microsoft.com/en-us/rest/api/storageservices/create-service-sas
23
+ */
24
+
25
+ const SAS_PARAM_NAMES = new Set(["sig", "sv", "se", "st", "sp", "spr", "sr", "sip", "ss", "srt", "skoid", "sktid", "skt", "ske", "sks", "skv", "saoid", "suoid", "sce", "sdd", "rscc", "rscd", "rsce", "rscl", "rsct"]);
26
+ const REDACTED_VALUE = "[REDACTED]";
27
+
28
+ // Matches any http(s) URL substring up to whitespace / closing quote / angle-bracket / paren.
29
+ const URL_REGEX = /\bhttps?:\/\/[^\s"'<>)]+/gi;
30
+ const redactQueryIfSas = url => {
31
+ const queryIdx = url.indexOf("?");
32
+ if (queryIdx < 0) {
33
+ return url;
34
+ }
35
+ // Separate the fragment (#...) before processing the query — fragments come
36
+ // after the query string in URL syntax and must not be folded into the last
37
+ // query parameter's value when we split on `&`.
38
+ const afterQuestionMark = url.slice(queryIdx + 1);
39
+ const fragIdx = afterQuestionMark.indexOf("#");
40
+ const query = fragIdx < 0 ? afterQuestionMark : afterQuestionMark.slice(0, fragIdx);
41
+ const fragment = fragIdx < 0 ? "" : afterQuestionMark.slice(fragIdx); // includes the leading '#'
42
+ if (!/(^|&)sig=/i.test(query)) {
43
+ // No SAS signature present — leave the URL alone so legitimate non-SAS query
44
+ // strings (e.g. ?utm_source=...) are not mangled.
45
+ return url;
46
+ }
47
+ const base = url.slice(0, queryIdx);
48
+ const sanitizedQuery = query.split("&").map(pair => {
49
+ const eqIdx = pair.indexOf("=");
50
+ if (eqIdx < 0) {
51
+ return pair;
52
+ }
53
+ const name = pair.slice(0, eqIdx).toLowerCase();
54
+ return SAS_PARAM_NAMES.has(name) ? `${pair.slice(0, eqIdx)}=${REDACTED_VALUE}` : pair;
55
+ }).join("&");
56
+ return `${base}?${sanitizedQuery}${fragment}`;
57
+ };
58
+ const sanitizeString = s => s.replace(URL_REGEX, redactQueryIfSas);
59
+
60
+ /**
61
+ * Deep-walk a value and redact SAS credentials in any string it contains.
62
+ * Returns a new object/array (originals are not mutated); non-string leaves are
63
+ * passed through. Tree-shaped, JSON-like values only — not designed for class
64
+ * instances, prototype-bound objects, or cyclic graphs.
65
+ */
66
+ const sanitizeSasInPayload = value => {
67
+ if (typeof value === "string") {
68
+ return sanitizeString(value);
69
+ }
70
+ if (Array.isArray(value)) {
71
+ return value.map(sanitizeSasInPayload);
72
+ }
73
+ if (value !== null && typeof value === "object") {
74
+ const out = {};
75
+ for (const [k, v] of Object.entries(value)) {
76
+ out[k] = sanitizeSasInPayload(v);
77
+ }
78
+ return out;
79
+ }
80
+ return value;
81
+ };
82
+ exports.sanitizeSasInPayload = sanitizeSasInPayload;
@@ -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"
@@ -173,14 +173,16 @@ const createMarkdown = (disableMarkdownMessageFormatting, disableNewLineMarkdown
173
173
 
174
174
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
175
175
  md.render = function (text, env) {
176
+ const safeEnv = env ?? {};
176
177
  const processedText = preprocessText(text);
177
- return md.renderer.render(md.parse(processedText, env), md.options, env);
178
+ return md.renderer.render(md.parse(processedText, safeEnv), md.options, safeEnv);
178
179
  };
179
180
 
180
181
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
181
182
  md.renderInline = function (text, env) {
183
+ const safeEnv = env ?? {};
182
184
  const processedText = preprocessText(text);
183
- return md.renderer.render(md.parseInline(processedText, env), md.options, env);
185
+ return md.renderer.render(md.parseInline(processedText, safeEnv), md.options, safeEnv);
184
186
  };
185
187
  });
186
188
 
@@ -304,24 +304,28 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
304
304
  const conversationId = (state === null || state === void 0 ? void 0 : (_state$domainStates2 = state.domainStates) === null || _state$domainStates2 === void 0 ? void 0 : (_state$domainStates2$ = _state$domainStates2.chatToken) === null || _state$domainStates2$ === void 0 ? void 0 : _state$domainStates2$.chatId) || "unknown";
305
305
  _TelemetryHelper.TelemetryHelper.logActionEvent(_TelemetryConstants.LogLevel.INFO, {
306
306
  Event: _TelemetryConstants.TelemetryEvent.HTMLSanitized,
307
- Description: "HTML content would be sanitized by stricter allowlist (monitor-only)",
308
- ElapsedTimeInMilliseconds: executionTimeMs,
309
- CustomProperties: {
310
- OrganizationId: orgId,
311
- ConversationId: conversationId,
312
- RemovedTags: uniqueTags.join(", "),
313
- RemovedAttributes: uniqueAttrs.join(", "),
314
- Phase: "Monitor"
315
- }
307
+ Description: JSON.stringify({
308
+ message: "HTML content would be sanitized by stricter allowlist (monitor-only)",
309
+ removedTags: uniqueTags,
310
+ removedAttributes: uniqueAttrs,
311
+ phase: "Monitor",
312
+ organizationId: orgId,
313
+ conversationId: conversationId
314
+ }),
315
+ ElapsedTimeInMilliseconds: executionTimeMs
316
316
  });
317
317
 
318
318
  // Log to console in development for debugging
319
319
  if (process.env.NODE_ENV === "development") {
320
- console.warn("[Monitor] Stricter HTML sanitization would remove:", {
320
+ console.warn("[Monitor] Stricter HTML sanitization telemetry:", {
321
+ description: JSON.parse(JSON.stringify({
322
+ message: "HTML content would be sanitized by stricter allowlist (monitor-only)",
323
+ removedTags: uniqueTags,
324
+ removedAttributes: uniqueAttrs,
325
+ phase: "Monitor"
326
+ })),
321
327
  orgId,
322
328
  conversationId,
323
- removedTags: uniqueTags,
324
- removedAttributes: uniqueAttrs,
325
329
  executionTimeMs
326
330
  });
327
331
  }
@@ -16,13 +16,22 @@ function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "functio
16
16
  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; }
17
17
  let uiTimer;
18
18
  const PostChatLoadingPaneStateful = props => {
19
- var _props$styleProps;
19
+ var _props$controlProps, _props$styleProps;
20
+ const defaultSubtitleText = "Please take a moment to give us feedback about your chat experience. We are loading the survey for you now.";
21
+ const subtitleText = ((_props$controlProps = props.controlProps) === null || _props$controlProps === void 0 ? void 0 : _props$controlProps.subtitleText) ?? defaultSubtitleText;
22
+ const [announcedSubtitleText, setAnnouncedSubtitleText] = (0, _react.useState)("");
20
23
  (0, _react.useEffect)(() => {
21
24
  uiTimer = (0, _utils.createTimer)();
22
25
  _TelemetryHelper.TelemetryHelper.logLoadingEvent(_TelemetryConstants.LogLevel.INFO, {
23
26
  Event: _TelemetryConstants.TelemetryEvent.UXPostChatLoadingPaneStart
24
27
  });
25
28
  }, []);
29
+ (0, _react.useEffect)(() => {
30
+ const timeout = window.setTimeout(() => {
31
+ setAnnouncedSubtitleText(subtitleText);
32
+ }, 0);
33
+ return () => window.clearTimeout(timeout);
34
+ }, [subtitleText]);
26
35
  const [state] = (0, _useChatContextStore.default)();
27
36
  const generalStyleProps = Object.assign({}, _defaultgeneralPostChatLoadingPaneStyleProps.defaultGeneralPostChatLoadingPaneStyleProps, (_props$styleProps = props.styleProps) === null || _props$styleProps === void 0 ? void 0 : _props$styleProps.generalStyleProps);
28
37
  const styleProps = {
@@ -36,8 +45,11 @@ const PostChatLoadingPaneStateful = props => {
36
45
  hideTitle: true,
37
46
  hideSpinner: true,
38
47
  hideSpinnerText: true,
39
- subtitleText: "Please take a moment to give us feedback about your chat experience. We are loading the survey for you now.",
40
- ...props.controlProps
48
+ ...props.controlProps,
49
+ role: "status",
50
+ "aria-live": "polite",
51
+ "aria-atomic": "true",
52
+ subtitleText: announcedSubtitleText
41
53
  };
42
54
 
43
55
  // Move focus to the first button
@@ -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,36 @@ 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
+ style: {
226
+ width: "100%",
227
+ height: "100%"
228
+ }
229
+ }, /*#__PURE__*/_react.default.createElement("div", {
230
+ role: "status",
231
+ "aria-live": "polite",
232
+ "aria-atomic": "true",
233
+ style: {
234
+ position: "absolute",
235
+ width: "1px",
236
+ height: "1px",
237
+ margin: "-1px",
238
+ padding: 0,
239
+ border: 0,
240
+ clip: "rect(0, 0, 0, 0)",
241
+ overflow: "hidden",
242
+ whiteSpace: "nowrap"
243
+ }
244
+ }, preChatFocusAnnouncement), /*#__PURE__*/_react.default.createElement(_omnichannelChatComponents.PreChatSurveyPane, {
179
245
  controlProps: controlProps,
180
246
  styleProps: styleProps
181
- });
247
+ }));
182
248
  };
183
249
  exports.PreChatSurveyPaneStateful = PreChatSurveyPaneStateful;
184
250
  var _default = PreChatSurveyPaneStateful;
@@ -12,7 +12,6 @@ const defaultGeneralPreChatSurveyPaneStyleProps = {
12
12
  borderColor: "#F1F1F1",
13
13
  overflowY: "auto",
14
14
  height: "inherit",
15
- width: "inherit",
16
- overscrollBehavior: "none"
15
+ width: "inherit"
17
16
  };
18
17
  exports.defaultGeneralPreChatSurveyPaneStyleProps = defaultGeneralPreChatSurveyPaneStyleProps;
@@ -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");