@microsoft/omnichannel-chat-widget 1.8.4-main.882d8f1 → 1.8.4-main.9284e84

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 (31) hide show
  1. package/lib/cjs/common/telemetry/TelemetryConstants.js +1 -0
  2. package/lib/cjs/common/utils.js +56 -15
  3. package/lib/cjs/components/citationpanestateful/CitationPaneStateful.js +2 -1
  4. package/lib/cjs/components/confirmationpanestateful/ConfirmationPaneStateful.js +2 -1
  5. package/lib/cjs/components/emailtranscriptpanestateful/EmailTranscriptPaneStateful.js +2 -1
  6. package/lib/cjs/components/livechatwidget/common/endChat.js +4 -0
  7. package/lib/cjs/components/livechatwidget/common/initWebChatComposer.js +182 -1
  8. package/lib/cjs/components/livechatwidget/livechatwidgetstateful/LiveChatWidgetStateful.js +14 -2
  9. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityMiddleware.js +2 -0
  10. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachmentMiddleware.js +2 -1
  11. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +138 -0
  12. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.js +81 -0
  13. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/localizedStringsBotInitialsMiddleware.js +9 -3
  14. package/lib/esm/common/telemetry/TelemetryConstants.js +1 -0
  15. package/lib/esm/common/utils.js +53 -13
  16. package/lib/esm/components/citationpanestateful/CitationPaneStateful.js +2 -1
  17. package/lib/esm/components/confirmationpanestateful/ConfirmationPaneStateful.js +2 -1
  18. package/lib/esm/components/emailtranscriptpanestateful/EmailTranscriptPaneStateful.js +2 -1
  19. package/lib/esm/components/livechatwidget/common/endChat.js +4 -0
  20. package/lib/esm/components/livechatwidget/common/initWebChatComposer.js +182 -1
  21. package/lib/esm/components/livechatwidget/livechatwidgetstateful/LiveChatWidgetStateful.js +15 -3
  22. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityMiddleware.js +2 -0
  23. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachmentMiddleware.js +2 -1
  24. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +130 -0
  25. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.js +74 -0
  26. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/localizedStringsBotInitialsMiddleware.js +9 -3
  27. package/lib/types/common/telemetry/TelemetryConstants.d.ts +1 -0
  28. package/lib/types/common/utils.d.ts +2 -1
  29. package/lib/types/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.d.ts +18 -0
  30. package/lib/types/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.d.ts +12 -0
  31. package/package.json +3 -2
@@ -202,6 +202,7 @@ exports.TelemetryEvent = TelemetryEvent;
202
202
  TelemetryEvent["QueueOverflowEvent"] = "QueueOverflowEvent";
203
203
  TelemetryEvent["ProcessingHTMLTextMiddlewareFailed"] = "ProcessingHTMLTextMiddlewareFailed";
204
204
  TelemetryEvent["ProcessingSanitizationMiddlewareFailed"] = "ProcessingSanitizationMiddlewareFailed";
205
+ TelemetryEvent["HTMLSanitized"] = "HTMLSanitized";
205
206
  TelemetryEvent["FormatTagsMiddlewareJSONStringifyFailed"] = "FormatTagsMiddlewareJSONStringifyFailed";
206
207
  TelemetryEvent["AttachmentUploadValidatorMiddlewareFailed"] = "AttachmentUploadValidatorMiddlewareFailed";
207
208
  TelemetryEvent["CitationMiddlewareFailed"] = "CitationMiddlewareFailed";
@@ -9,7 +9,7 @@ exports.getWidgetEndChatEventName = exports.getWidgetCacheIdfromProps = exports.
9
9
  exports.isEndConversationDueToOverflowActivity = isEndConversationDueToOverflowActivity;
10
10
  exports.parseAdaptiveCardPayload = exports.newGuid = exports.isValidCustomEvent = exports.isUndefinedOrEmpty = exports.isThisSessionPopout = exports.isNullOrUndefined = exports.isNullOrEmptyString = void 0;
11
11
  exports.parseBooleanFromConfig = parseBooleanFromConfig;
12
- exports.setTabIndices = exports.setOcUserAgent = exports.setFocusOnSendBox = exports.setFocusOnElement = exports.preventFocusToMoveOutOfElement = exports.parseLowerCaseString = void 0;
12
+ exports.setTabIndices = exports.setOcUserAgent = exports.setFocusOnSendBox = exports.setFocusOnElement = exports.setAriaHiddenForSiblings = exports.preventFocusToMoveOutOfElement = exports.parseLowerCaseString = void 0;
13
13
  var _Constants = require("./Constants");
14
14
  var _TelemetryConstants = require("./telemetry/TelemetryConstants");
15
15
  var _omnichannelChatComponents = require("@microsoft/omnichannel-chat-components");
@@ -85,28 +85,69 @@ exports.findAllFocusableElement = findAllFocusableElement;
85
85
  const preventFocusToMoveOutOfElement = elementId => {
86
86
  const container = document.getElementById(elementId);
87
87
  if (!container) {
88
- return;
88
+ return () => {/* no-op */};
89
89
  }
90
90
  const focusableElements = findAllFocusableElement(container);
91
91
  if (!focusableElements) {
92
- return;
92
+ return () => {/* no-op */};
93
93
  }
94
94
  const firstFocusableElement = focusableElements[0];
95
95
  const lastFocusableElement = focusableElements[focusableElements.length - 1];
96
- firstFocusableElement.onkeydown = e => {
97
- if (e.shiftKey && e.key === _KeyCodes.KeyCodes.TAB) {
98
- e.preventDefault();
99
- lastFocusableElement === null || lastFocusableElement === void 0 ? void 0 : lastFocusableElement.focus();
100
- }
101
- };
102
- lastFocusableElement.onkeydown = e => {
103
- if (!e.shiftKey && e.key === _KeyCodes.KeyCodes.TAB) {
104
- e.preventDefault();
105
- firstFocusableElement === null || firstFocusableElement === void 0 ? void 0 : firstFocusableElement.focus();
106
- }
107
- };
96
+ const cleanups = [];
97
+ if (firstFocusableElement === lastFocusableElement) {
98
+ const handler = e => {
99
+ if (e.key === _KeyCodes.KeyCodes.TAB && !e.shiftKey) {
100
+ e.preventDefault();
101
+ firstFocusableElement.focus();
102
+ } else if (e.key === _KeyCodes.KeyCodes.TAB && e.shiftKey) {
103
+ e.preventDefault();
104
+ firstFocusableElement.focus();
105
+ }
106
+ };
107
+ firstFocusableElement.addEventListener("keydown", handler);
108
+ cleanups.push(() => firstFocusableElement.removeEventListener("keydown", handler));
109
+ } else {
110
+ const firstHandler = e => {
111
+ if (e.shiftKey && e.key === _KeyCodes.KeyCodes.TAB) {
112
+ e.preventDefault();
113
+ lastFocusableElement === null || lastFocusableElement === void 0 ? void 0 : lastFocusableElement.focus();
114
+ }
115
+ };
116
+ firstFocusableElement.addEventListener("keydown", firstHandler);
117
+ cleanups.push(() => firstFocusableElement.removeEventListener("keydown", firstHandler));
118
+ const lastHandler = e => {
119
+ if (!e.shiftKey && e.key === _KeyCodes.KeyCodes.TAB) {
120
+ e.preventDefault();
121
+ firstFocusableElement === null || firstFocusableElement === void 0 ? void 0 : firstFocusableElement.focus();
122
+ }
123
+ };
124
+ lastFocusableElement.addEventListener("keydown", lastHandler);
125
+ cleanups.push(() => lastFocusableElement.removeEventListener("keydown", lastHandler));
126
+ }
127
+ return () => cleanups.forEach(fn => fn());
108
128
  };
109
129
  exports.preventFocusToMoveOutOfElement = preventFocusToMoveOutOfElement;
130
+ const setAriaHiddenForSiblings = (elementId, shouldHide, stateMap) => {
131
+ const element = document.getElementById(elementId);
132
+ if (!(element !== null && element !== void 0 && element.parentElement)) return;
133
+ Array.from(element.parentElement.children).forEach(sibling => {
134
+ if (sibling !== element) {
135
+ if (shouldHide) {
136
+ stateMap.set(sibling, sibling.getAttribute("aria-hidden"));
137
+ sibling.setAttribute("aria-hidden", "true");
138
+ } else if (stateMap.has(sibling)) {
139
+ const original = stateMap.get(sibling);
140
+ if (original === null) {
141
+ sibling.removeAttribute("aria-hidden");
142
+ } else {
143
+ sibling.setAttribute("aria-hidden", original);
144
+ }
145
+ stateMap.delete(sibling);
146
+ }
147
+ }
148
+ });
149
+ };
150
+ exports.setAriaHiddenForSiblings = setAriaHiddenForSiblings;
110
151
  const setFocusOnSendBox = () => {
111
152
  const sendBoxSelector = "textarea[data-id=\"webchat-sendbox-input\"]";
112
153
  setFocusOnElement(sendBoxSelector);
@@ -44,7 +44,7 @@ const CitationPaneStateful = props => {
44
44
 
45
45
  // Initial focus pattern (mirrors ConfirmationPaneStateful): focus first focusable element (will re-attempt after visibility becomes true)
46
46
  (0, _react.useEffect)(() => {
47
- (0, _utils.preventFocusToMoveOutOfElement)(controlId);
47
+ const cleanup = (0, _utils.preventFocusToMoveOutOfElement)(controlId);
48
48
  const focusableElements = (0, _utils.findAllFocusableElement)(`#${controlId}`);
49
49
  requestAnimationFrame(() => {
50
50
  if (focusableElements && focusableElements.length > 0 && focusableElements[0]) {
@@ -62,6 +62,7 @@ const CitationPaneStateful = props => {
62
62
  Event: _TelemetryConstants.TelemetryEvent.UXCitationPaneCompleted,
63
63
  ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
64
64
  });
65
+ return cleanup;
65
66
  }, []);
66
67
 
67
68
  // Retry focus once pane is actually visible (isReady) in case initial attempt occurred while wrapper was visibility:hidden
@@ -82,7 +82,7 @@ const ConfirmationPaneStateful = props => {
82
82
 
83
83
  // Move focus to the first button
84
84
  (0, _react.useEffect)(() => {
85
- (0, _utils.preventFocusToMoveOutOfElement)(controlProps.id);
85
+ const cleanup = (0, _utils.preventFocusToMoveOutOfElement)(controlProps.id);
86
86
  const focusableElements = (0, _utils.findAllFocusableElement)(`#${controlProps.id}`);
87
87
  requestAnimationFrame(() => {
88
88
  if (focusableElements && focusableElements.length > 0 && focusableElements[0]) {
@@ -100,6 +100,7 @@ const ConfirmationPaneStateful = props => {
100
100
  Event: _TelemetryConstants.TelemetryEvent.UXConfirmationPaneCompleted,
101
101
  ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
102
102
  });
103
+ return cleanup;
103
104
  }, []);
104
105
  return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_DimLayer.DimLayer, {
105
106
  brightness: (controlProps === null || controlProps === void 0 ? void 0 : controlProps.brightnessValueOnDim) ?? "0.2"
@@ -103,7 +103,7 @@ const EmailTranscriptPaneStateful = props => {
103
103
 
104
104
  // Move focus to the first button
105
105
  (0, _react.useEffect)(() => {
106
- (0, _utils.preventFocusToMoveOutOfElement)(controlProps.id);
106
+ const cleanup = (0, _utils.preventFocusToMoveOutOfElement)(controlProps.id);
107
107
  const focusableElements = (0, _utils.findAllFocusableElement)(`#${controlProps.id}`);
108
108
  if (focusableElements) {
109
109
  focusableElements[0].focus();
@@ -118,6 +118,7 @@ const EmailTranscriptPaneStateful = props => {
118
118
  Event: _TelemetryConstants.TelemetryEvent.UXEmailTranscriptPaneCompleted,
119
119
  ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
120
120
  });
121
+ return cleanup;
121
122
  }, [initialEmail]);
122
123
  return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_DimLayer.DimLayer, {
123
124
  brightness: (controlProps === null || controlProps === void 0 ? void 0 : controlProps.brightnessValueOnDim) ?? "0.2"
@@ -346,6 +346,10 @@ const closeChatStateCleanUp = dispatch => {
346
346
  type: _LiveChatWidgetActionType.LiveChatWidgetActionType.SET_CITATIONS,
347
347
  payload: {}
348
348
  });
349
+ dispatch({
350
+ type: _LiveChatWidgetActionType.LiveChatWidgetActionType.SET_SHOW_EMAIL_TRANSCRIPT_PANE,
351
+ payload: false
352
+ });
349
353
 
350
354
  // Dismiss the chat disconnect notification banner if it was shown
351
355
  _NotificationHandler.NotificationHandler.dismissNotification(_NotificationScenarios.NotificationScenarios.ChatDisconnect);
@@ -16,6 +16,7 @@ var _LiveChatWidgetActionType = require("../../../contexts/common/LiveChatWidget
16
16
  var _TelemetryHelper = require("../../../common/telemetry/TelemetryHelper");
17
17
  var _WebChatStoreLoader = require("../../webchatcontainerstateful/webchatcontroller/WebChatStoreLoader");
18
18
  var _attachmentProcessingMiddleware = _interopRequireDefault(require("../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentProcessingMiddleware"));
19
+ var _attachmentSentAnnouncementMiddleware = _interopRequireDefault(require("../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware"));
19
20
  var _channelDataMiddleware = _interopRequireDefault(require("../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/channelDataMiddleware"));
20
21
  var _activityMiddleware = require("../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityMiddleware");
21
22
  var _activityStatusMiddleware = require("../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityStatusMiddleware");
@@ -125,7 +126,7 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
125
126
  };
126
127
  webChatStore = (0, _botframeworkWebchat.createStore)({},
127
128
  //initial state
128
- _preProcessingMiddleware.default, _attachmentProcessingMiddleware.default, (0, _attachmentUploadValidatorMiddleware.default)((_state$domainStates$l = state.domainStates.liveChatConfig) === null || _state$domainStates$l === void 0 ? void 0 : _state$domainStates$l.allowedFileExtensions, (_state$domainStates$l2 = state.domainStates.liveChatConfig) === null || _state$domainStates$l2 === void 0 ? void 0 : _state$domainStates$l2.maxUploadFileSize, localizedTexts), (0, _customEventMiddleware.default)(_omnichannelChatComponents.BroadcastService), (0, _queueOverflowHandlerMiddleware.createQueueOverflowMiddleware)(state, dispatch), (0, _channelDataMiddleware.default)(addConversationalSurveyTagsCallback), (0, _conversationEndMiddleware.default)(conversationEndCallback, startConversationalSurveyCallback, endConversationalSurveyCallback), (0, _dataMaskingMiddleware.default)((_state$domainStates$l3 = state.domainStates.liveChatConfig) === null || _state$domainStates$l3 === void 0 ? void 0 : _state$domainStates$l3.DataMaskingInfo), _messageTimestampMiddleware.createMessageTimeStampMiddleware, _messageSequenceIdOverrideMiddleware.createMessageSequenceIdOverrideMiddleware, (0, _citationsMiddleware.createCitationsMiddleware)(state, dispatch), _gifUploadMiddleware.default, _htmlPlayerMiddleware.default, (0, _htmlTextMiddleware.default)(honorsTargetInHTMLLinks), (0, _maxMessageSizeValidator.default)(localizedTexts), _sanitizationMiddleware.default, (0, _callActionMiddleware.default)(),
129
+ _preProcessingMiddleware.default, _attachmentProcessingMiddleware.default, (0, _attachmentUploadValidatorMiddleware.default)((_state$domainStates$l = state.domainStates.liveChatConfig) === null || _state$domainStates$l === void 0 ? void 0 : _state$domainStates$l.allowedFileExtensions, (_state$domainStates$l2 = state.domainStates.liveChatConfig) === null || _state$domainStates$l2 === void 0 ? void 0 : _state$domainStates$l2.maxUploadFileSize, localizedTexts), _attachmentSentAnnouncementMiddleware.default, (0, _customEventMiddleware.default)(_omnichannelChatComponents.BroadcastService), (0, _queueOverflowHandlerMiddleware.createQueueOverflowMiddleware)(state, dispatch), (0, _channelDataMiddleware.default)(addConversationalSurveyTagsCallback), (0, _conversationEndMiddleware.default)(conversationEndCallback, startConversationalSurveyCallback, endConversationalSurveyCallback), (0, _dataMaskingMiddleware.default)((_state$domainStates$l3 = state.domainStates.liveChatConfig) === null || _state$domainStates$l3 === void 0 ? void 0 : _state$domainStates$l3.DataMaskingInfo), _messageTimestampMiddleware.createMessageTimeStampMiddleware, _messageSequenceIdOverrideMiddleware.createMessageSequenceIdOverrideMiddleware, (0, _citationsMiddleware.createCitationsMiddleware)(state, dispatch), _gifUploadMiddleware.default, _htmlPlayerMiddleware.default, (0, _htmlTextMiddleware.default)(honorsTargetInHTMLLinks), (0, _maxMessageSizeValidator.default)(localizedTexts), _sanitizationMiddleware.default, (0, _callActionMiddleware.default)(),
129
130
  // Pass a callback so middleware can push initials into React context for reactivity
130
131
  (0, _localizedStringsBotInitialsMiddleware.localizedStringsBotInitialsMiddleware)(initials => {
131
132
  dispatch({
@@ -151,14 +152,194 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
151
152
  markdownRenderers.forEach(renderer => {
152
153
  text = renderer.render(text);
153
154
  });
155
+
156
+ // EXISTING sanitization (continues to work as before)
154
157
  const config = {
155
158
  FORBID_TAGS: ["form", "button", "script", "div", "input"],
156
159
  FORBID_ATTR: ["action"],
157
160
  ADD_ATTR: ["target"]
158
161
  };
159
162
  text = _dompurify.default.sanitize(text, config);
163
+
164
+ // MONITOR-ONLY: Test what the stricter allowlist would remove (Phase 1)
165
+ // This does NOT modify the text, only logs telemetry
166
+ // Run during browser idle time to avoid blocking message flow and adding latency
167
+ const textToMonitor = text; // Capture current text value
168
+
169
+ // Schedule monitoring to run during browser idle time
170
+ const scheduleMonitoring = () => {
171
+ try {
172
+ monitorStrictSanitization(textToMonitor, state);
173
+ } catch (error) {
174
+ // Silently catch errors to prevent blocking message flow
175
+ if (process.env.NODE_ENV === "development") {
176
+ console.error("[Monitor] HTML sanitization monitoring failed:", error);
177
+ }
178
+ }
179
+ };
180
+
181
+ // Use requestIdleCallback for truly idle execution, fallback to setTimeout for older browsers
182
+ if (typeof window !== "undefined" && "requestIdleCallback" in window) {
183
+ window.requestIdleCallback(scheduleMonitoring);
184
+ } else {
185
+ setTimeout(scheduleMonitoring, 0);
186
+ }
160
187
  return text;
161
188
  };
189
+
190
+ /**
191
+ * Monitor-only sanitization (Phase 1: Gather telemetry)
192
+ * Tests what a stricter allowlist-based sanitization would remove
193
+ * WITHOUT actually removing it. Logs telemetry for analysis.
194
+ *
195
+ * IMPORTANT: This function is wrapped in try-catch and runs asynchronously
196
+ * to ensure failures don't block message flow or add latency.
197
+ *
198
+ * ISOLATION: Uses a separate DOMPurify instance to completely isolate
199
+ * monitoring hooks from other sanitization paths (e.g., postDomPurifyActivities).
200
+ * The instance is garbage collected after use, no cleanup needed.
201
+ *
202
+ * @param html - The HTML text that was already sanitized with existing config
203
+ * @param state - Widget state containing orgId and chatId
204
+ */
205
+ const monitorStrictSanitization = (html, state) => {
206
+ // Early exit for empty content
207
+ if (!html) return;
208
+
209
+ // Track execution time for performance monitoring
210
+ const startTime = performance.now();
211
+ try {
212
+ // Create a separate DOMPurify instance for monitoring
213
+ // This completely isolates monitoring from other sanitization paths
214
+ const monitorDOMPurify = (0, _dompurify.default)(window);
215
+
216
+ // Strict allowlist configuration (proposed new rules)
217
+ // Note: DOMPurify blocks event handlers (onclick, onerror, etc.) by default
218
+ const strictConfig = {
219
+ ALLOWED_TAGS: ["b", "strong",
220
+ // Bold text
221
+ "i", "em", "u",
222
+ // Italic, emphasis, underline
223
+ "br", "p",
224
+ // Line breaks and paragraphs
225
+ "ul", "ol", "li",
226
+ // Lists
227
+ "a" // Links (with restricted attributes)
228
+ ],
229
+
230
+ ALLOWED_ATTR: ["href",
231
+ // For links (will be restricted to http/https)
232
+ "target",
233
+ // For link behavior
234
+ "rel" // For security (noopener, noreferrer)
235
+ ],
236
+
237
+ FORBID_TAGS: ["img", "video", "audio",
238
+ // Media (tracking beacons)
239
+ "iframe", "object", "embed",
240
+ // Embedded content
241
+ "script", "style",
242
+ // Script and styling
243
+ "form", "input", "textarea", "button",
244
+ // Form elements
245
+ "link", "meta", "base",
246
+ // Document metadata
247
+ "div", "span" // Layout elements
248
+ ],
249
+
250
+ FORBID_ATTR: ["style",
251
+ // Inline CSS
252
+ "action" // Form action attribute (event handlers blocked by default)
253
+ ],
254
+
255
+ ALLOWED_URI_REGEXP: /^https?:/i,
256
+ ALLOW_DATA_ATTR: false,
257
+ ALLOW_UNKNOWN_PROTOCOLS: false
258
+ };
259
+
260
+ // Track what would be removed
261
+ const removedTags = [];
262
+ const removedAttributes = [];
263
+
264
+ // Add hooks to the isolated monitoring instance
265
+ monitorDOMPurify.addHook("uponSanitizeElement", (node, data) => {
266
+ try {
267
+ const tagName = data.tagName.toLowerCase();
268
+ // Filter out "body" tag which is DOMPurify's internal wrapper
269
+ if (node.nodeType === 1 && !strictConfig.ALLOWED_TAGS.includes(tagName) && tagName !== "body") {
270
+ removedTags.push(tagName);
271
+ }
272
+ } catch (hookError) {
273
+ // Silently ignore hook errors
274
+ }
275
+ });
276
+ monitorDOMPurify.addHook("uponSanitizeAttribute", (node, data) => {
277
+ try {
278
+ const attrName = data.attrName.toLowerCase();
279
+ if (!strictConfig.ALLOWED_ATTR.includes(attrName) && attrName !== "class" && attrName !== "id") {
280
+ removedAttributes.push(attrName);
281
+ }
282
+ } catch (hookError) {
283
+ // Silently ignore hook errors
284
+ }
285
+ });
286
+
287
+ // Run sanitization on the isolated instance (we discard the result)
288
+ // No cleanup needed - the instance will be garbage collected with its hooks
289
+ monitorDOMPurify.sanitize(html, strictConfig);
290
+
291
+ // Log telemetry if content would be affected by strict rules
292
+ if (removedTags.length > 0 || removedAttributes.length > 0) {
293
+ try {
294
+ var _state$domainStates, _state$domainStates$t, _state$domainStates2, _state$domainStates2$;
295
+ const uniqueTags = [...new Set(removedTags)];
296
+ const uniqueAttrs = [...new Set(removedAttributes)];
297
+
298
+ // Calculate execution time
299
+ const endTime = performance.now();
300
+ const executionTimeMs = Math.round((endTime - startTime) * 100) / 100; // Round to 2 decimal places
301
+
302
+ // Get context for telemetry (with safe fallbacks)
303
+ const orgId = (state === null || state === void 0 ? void 0 : (_state$domainStates = state.domainStates) === null || _state$domainStates === void 0 ? void 0 : (_state$domainStates$t = _state$domainStates.telemetryInternalData) === null || _state$domainStates$t === void 0 ? void 0 : _state$domainStates$t.orgId) || "unknown";
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
+ _TelemetryHelper.TelemetryHelper.logActionEvent(_TelemetryConstants.LogLevel.INFO, {
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
+ }
316
+ });
317
+
318
+ // Log to console in development for debugging
319
+ if (process.env.NODE_ENV === "development") {
320
+ console.warn("[Monitor] Stricter HTML sanitization would remove:", {
321
+ orgId,
322
+ conversationId,
323
+ removedTags: uniqueTags,
324
+ removedAttributes: uniqueAttrs,
325
+ executionTimeMs
326
+ });
327
+ }
328
+ } catch (telemetryError) {
329
+ // Silently ignore telemetry errors to prevent blocking
330
+ if (process.env.NODE_ENV === "development") {
331
+ console.error("[Monitor] Telemetry logging failed:", telemetryError);
332
+ }
333
+ }
334
+ }
335
+ } catch (error) {
336
+ // Catch-all for any unexpected errors
337
+ // Silently fail to ensure monitoring never blocks message flow
338
+ if (process.env.NODE_ENV === "development") {
339
+ console.error("[Monitor] Monitoring failed:", error);
340
+ }
341
+ }
342
+ };
162
343
  function postDomPurifyActivities() {
163
344
  _dompurify.default.addHook("afterSanitizeAttributes", function (node) {
164
345
  const target = node.getAttribute(_Constants.Constants.Target);
@@ -66,7 +66,7 @@ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj &&
66
66
  function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
67
67
  let uiTimer;
68
68
  const LiveChatWidgetStateful = props => {
69
- var _props$webChatContain, _props$webChatContain2, _props$webChatContain3, _props$webChatContain4, _props$webChatContain5, _props$webChatContain6, _props$webChatContain7, _props$webChatContain8, _props$webChatContain9, _props$styleProps, _props$webChatContain10, _props$webChatContain11, _props$controlProps, _props$controlProps3, _state$appStates7, _props$webChatContain15, _state$appStates8, _props$webChatContain17, _props$webChatContain18, _props$controlProps12, _props$draggableChatW, _props$draggableChatW2, _props$draggableChatW3, _props$draggableChatW4, _props$draggableChatW5, _livechatProps$webCha, _livechatProps$styleP, _livechatProps$contro, _livechatProps$contro2, _livechatProps$compon, _livechatProps$contro3, _livechatProps$compon2, _livechatProps$contro4, _livechatProps$compon3, _livechatProps$contro5, _livechatProps$compon4, _livechatProps$contro6, _livechatProps$compon5, _livechatProps$contro7, _livechatProps$compon6, _livechatProps$contro8, _livechatProps$compon7, _livechatProps$contro9, _livechatProps$compon8, _livechatProps$contro10, _livechatProps$contro11, _livechatProps$compon9, _livechatProps$contro12, _livechatProps$compon10, _livechatProps$contro13, _livechatProps$compon11, _livechatProps$compon12, _livechatProps$compon13;
69
+ var _props$webChatContain, _props$webChatContain2, _props$webChatContain3, _props$webChatContain4, _props$webChatContain5, _props$webChatContain6, _props$webChatContain7, _props$webChatContain8, _props$webChatContain9, _props$styleProps, _props$webChatContain10, _props$webChatContain11, _props$controlProps, _props$controlProps3, _state$appStates7, _props$webChatContain15, _state$appStates8, _props$webChatContain17, _props$webChatContain18, _props$controlProps12, _props$draggableChatW, _props$draggableChatW2, _props$draggableChatW3, _props$draggableChatW4, _props$draggableChatW5, _livechatProps$webCha, _livechatProps$styleP, _props$headerProps, _props$headerProps$co, _props$headerProps$co2, _livechatProps$contro, _livechatProps$contro2, _livechatProps$compon, _livechatProps$contro3, _livechatProps$compon2, _livechatProps$contro4, _livechatProps$compon3, _livechatProps$contro5, _livechatProps$compon4, _livechatProps$contro6, _livechatProps$compon5, _livechatProps$contro7, _livechatProps$compon6, _livechatProps$contro8, _livechatProps$compon7, _livechatProps$contro9, _livechatProps$compon8, _livechatProps$contro10, _livechatProps$contro11, _livechatProps$compon9, _livechatProps$contro12, _livechatProps$compon10, _livechatProps$contro13, _livechatProps$compon11, _livechatProps$compon12, _livechatProps$compon13;
70
70
  (0, _react2.useEffect)(() => {
71
71
  uiTimer = (0, _utils.createTimer)();
72
72
  _TelemetryHelper.TelemetryHelper.logLoadingEventToAllTelemetry(_TelemetryConstants.LogLevel.INFO, {
@@ -1029,6 +1029,15 @@ const LiveChatWidgetStateful = props => {
1029
1029
  }));
1030
1030
  }
1031
1031
  }, [state.domainStates.botAvatarInitials]);
1032
+ const isWidgetOpen = !state.appStates.isMinimized && state.appStates.conversationState !== _ConversationState.ConversationState.Closed;
1033
+ const siblingAriaHiddenMapRef = (0, _react2.useRef)(new Map());
1034
+ (0, _react2.useEffect)(() => {
1035
+ const map = siblingAriaHiddenMapRef.current;
1036
+ (0, _utils.setAriaHiddenForSiblings)(widgetElementId, isWidgetOpen, map);
1037
+ return () => {
1038
+ (0, _utils.setAriaHiddenForSiblings)(widgetElementId, false, map);
1039
+ };
1040
+ }, [isWidgetOpen]);
1032
1041
 
1033
1042
  // WebChat's Composer can only be rendered if a directLine object is defined
1034
1043
  return directLine && /*#__PURE__*/_react2.default.createElement(_react2.default.Fragment, null, /*#__PURE__*/_react2.default.createElement("style", null, `
@@ -1152,7 +1161,10 @@ const LiveChatWidgetStateful = props => {
1152
1161
  }), /*#__PURE__*/_react2.default.createElement(_react.Stack, {
1153
1162
  id: widgetElementId,
1154
1163
  styles: generalStyles,
1155
- className: (_livechatProps$styleP = livechatProps.styleProps) === null || _livechatProps$styleP === void 0 ? void 0 : _livechatProps$styleP.className
1164
+ className: (_livechatProps$styleP = livechatProps.styleProps) === null || _livechatProps$styleP === void 0 ? void 0 : _livechatProps$styleP.className,
1165
+ role: isWidgetOpen ? "dialog" : undefined,
1166
+ "aria-modal": isWidgetOpen ? true : undefined,
1167
+ "aria-label": isWidgetOpen ? ((_props$headerProps = props.headerProps) === null || _props$headerProps === void 0 ? void 0 : (_props$headerProps$co = _props$headerProps.controlProps) === null || _props$headerProps$co === void 0 ? void 0 : (_props$headerProps$co2 = _props$headerProps$co.headerTitleProps) === null || _props$headerProps$co2 === void 0 ? void 0 : _props$headerProps$co2.text) ?? "Live Chat" : undefined
1156
1168
  }, !((_livechatProps$contro = livechatProps.controlProps) !== null && _livechatProps$contro !== void 0 && _livechatProps$contro.hideChatButton) && !((_livechatProps$contro2 = livechatProps.controlProps) !== null && _livechatProps$contro2 !== void 0 && _livechatProps$contro2.hideStartChatButton) && (0, _componentController.shouldShowChatButton)(state) && ((0, _omnichannelChatComponents.decodeComponentString)((_livechatProps$compon = livechatProps.componentOverrides) === null || _livechatProps$compon === void 0 ? void 0 : _livechatProps$compon.chatButton) || /*#__PURE__*/_react2.default.createElement(_ChatButtonStateful.default, {
1157
1169
  buttonProps: livechatProps.chatButtonProps,
1158
1170
  outOfOfficeButtonProps: livechatProps.outOfOfficeChatButtonProps,
@@ -65,6 +65,8 @@ const handleSystemMessage = (next, args, card, renderMarkdown, systemMessageStyl
65
65
  originalSystemMessageTexts.set(activityKey, card.activity.text);
66
66
  }
67
67
  const sourceText = (activityKey && originalSystemMessageTexts.get(activityKey)) ?? card.activity.text;
68
+ // renderMarkdown sanitizes the HTML using DOMPurify with allowlist-based configuration
69
+ // See initWebChatComposer.ts for sanitization details
68
70
  const renderedHtml = renderMarkdown(sourceText);
69
71
  // eslint-disable-next-line react/display-name
70
72
  return () => /*#__PURE__*/_react.default.createElement("div", {
@@ -7,6 +7,7 @@ exports.createAttachmentMiddleware = void 0;
7
7
  var _Constants = require("../../../../../common/Constants");
8
8
  var _react = _interopRequireDefault(require("react"));
9
9
  var _FileAttachmentIconManager = require("../../../common/utils/FileAttachmentIconManager");
10
+ var _AdaptiveCardAccessibilityWrapper = _interopRequireDefault(require("./attachments/AdaptiveCardAccessibilityWrapper"));
10
11
  var _Attachment = _interopRequireDefault(require("./attachments/Attachment"));
11
12
  var _TelemetryConstants = require("../../../../../common/telemetry/TelemetryConstants");
12
13
  var _omnichannelChatComponents = require("@microsoft/omnichannel-chat-components");
@@ -120,7 +121,7 @@ const createAttachmentMiddleware = enableInlinePlaying => {
120
121
  ...((_state$domainStates$r3 = state.domainStates.renderingMiddlewareProps) === null || _state$domainStates$r3 === void 0 ? void 0 : _state$domainStates$r3.attachmentAdaptiveCardStyles)
121
122
  };
122
123
  if (type === _Constants.WebChatMiddlewareConstants.adaptiveCard || _Constants.Constants.supportedAdaptiveCardContentTypes.indexOf(contentType) >= 0) {
123
- return /*#__PURE__*/_react.default.createElement("div", {
124
+ return /*#__PURE__*/_react.default.createElement(_AdaptiveCardAccessibilityWrapper.default, {
124
125
  id: attachmentId,
125
126
  style: atttachmentAdaptiveCardStyles
126
127
  }, next(...args));
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = _interopRequireWildcard(require("react"));
8
+ 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); }
9
+ 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; }
10
+ /******
11
+ * AdaptiveCardAccessibilityWrapper
12
+ *
13
+ * The `adaptivecards` renderer does not set `aria-setsize` or `aria-posinset`
14
+ * on radio buttons rendered by Input.ChoiceSet (style: "expanded"). Without
15
+ * these attributes, screen readers (TalkBack, VoiceOver, NVDA) cannot announce
16
+ * the correct option count (e.g. "1 of 2") and may say "1 of 1" for every item.
17
+ *
18
+ * This wrapper observes the rendered card via MutationObserver and patches each
19
+ * radio group with the correct ARIA attributes after the card finishes rendering.
20
+ ******/
21
+
22
+ const AdaptiveCardAccessibilityWrapper = _ref => {
23
+ let {
24
+ id,
25
+ style,
26
+ children
27
+ } = _ref;
28
+ const containerRef = (0, _react.useRef)(null);
29
+ (0, _react.useEffect)(() => {
30
+ if (!containerRef.current) return;
31
+ const patchRadioGroups = () => {
32
+ const container = containerRef.current;
33
+ if (!container) return;
34
+
35
+ // Find all radio inputs inside the adaptive card
36
+ const radios = container.querySelectorAll("input[type='radio']");
37
+ if (radios.length === 0) return;
38
+
39
+ // Group radios by their name attribute (each ChoiceSet has a unique name)
40
+ const groups = new Map();
41
+ radios.forEach(radio => {
42
+ var _groups$get;
43
+ const name = radio.getAttribute("name") || "__default";
44
+ if (!groups.has(name)) {
45
+ groups.set(name, []);
46
+ }
47
+ (_groups$get = groups.get(name)) === null || _groups$get === void 0 ? void 0 : _groups$get.push(radio);
48
+ });
49
+ groups.forEach(radioInputs => {
50
+ const groupSize = radioInputs.length;
51
+ radioInputs.forEach((radio, index) => {
52
+ radio.setAttribute("aria-setsize", String(groupSize));
53
+ radio.setAttribute("aria-posinset", String(index + 1));
54
+
55
+ // Ensure the parent container has radiogroup role
56
+ const parent = radio.closest("[role='radiogroup']") || radio.closest(".ac-input-container");
57
+ if (parent && !parent.getAttribute("role")) {
58
+ parent.setAttribute("role", "radiogroup");
59
+ }
60
+
61
+ // The adaptive card renders each choice as:
62
+ // <div style="display:flex"> ← flex wrapper
63
+ // <input type="radio">
64
+ // <div style="width:6px"></div> ← spacer
65
+ // <label><div><p>Text</p></div></label>
66
+ // </div>
67
+ //
68
+ // TalkBack focuses on the <label> text node (just "Standard")
69
+ // instead of the <input>, so it never announces "radio button" or count.
70
+ //
71
+ // Fix: hide the label and spacer from the accessibility tree so TalkBack
72
+ // lands directly on the <input> which already has aria-label,
73
+ // aria-setsize and aria-posinset. The input's aria-labelledby still
74
+ // resolves the accessible name from the hidden label per ARIA spec.
75
+ const flexWrapper = radio.parentElement;
76
+ if (flexWrapper) {
77
+ var _label$textContent;
78
+ // Hide label sibling — only set if not already applied to
79
+ // avoid re-triggering the attributeFilter observer in a loop
80
+ const label = flexWrapper.querySelector("label");
81
+ if (label && label.getAttribute("aria-hidden") !== "true") {
82
+ label.setAttribute("aria-hidden", "true");
83
+ }
84
+ // Hide spacer siblings (any non-input, non-label divs)
85
+ Array.from(flexWrapper.children).forEach(child => {
86
+ const tag = child.tagName.toLowerCase();
87
+ if (tag !== "input" && tag !== "label" && child.getAttribute("aria-hidden") !== "true") {
88
+ child.setAttribute("aria-hidden", "true");
89
+ }
90
+ });
91
+
92
+ // TalkBack reads content-desc from the Android accessibility node,
93
+ // which Chrome populates from aria-label. However, aria-labelledby
94
+ // (set by the adaptive card renderer) takes precedence over aria-label.
95
+ // Since we hide the label (aria-hidden=true), Chrome can no longer
96
+ // resolve aria-labelledby and returns empty content-desc.
97
+ //
98
+ // Fix: remove aria-labelledby so Chrome falls back to aria-label,
99
+ // then encode the position count into aria-label so TalkBack announces
100
+ // e.g. "Standard, 1 of 2, radio button, not checked".
101
+ if (radio.hasAttribute("aria-labelledby")) {
102
+ radio.removeAttribute("aria-labelledby");
103
+ }
104
+ const labelText = (label === null || label === void 0 ? void 0 : (_label$textContent = label.textContent) === null || _label$textContent === void 0 ? void 0 : _label$textContent.trim()) || radio.getAttribute("aria-label") || "";
105
+ const expectedLabel = `${labelText}, ${index + 1} of ${groupSize}`;
106
+ if (radio.getAttribute("aria-label") !== expectedLabel) {
107
+ radio.setAttribute("aria-label", expectedLabel);
108
+ }
109
+ }
110
+ });
111
+ });
112
+ };
113
+
114
+ // Use MutationObserver to patch after WebChat finishes rendering the card.
115
+ // Also watch attribute changes on aria-hidden so we can re-apply if React
116
+ // reconciliation removes it from labels.
117
+ const observer = new MutationObserver(() => {
118
+ patchRadioGroups();
119
+ });
120
+ observer.observe(containerRef.current, {
121
+ childList: true,
122
+ subtree: true,
123
+ attributes: true,
124
+ attributeFilter: ["aria-hidden"]
125
+ });
126
+
127
+ // Also run immediately in case the card is already rendered
128
+ patchRadioGroups();
129
+ return () => observer.disconnect();
130
+ }, []);
131
+ return /*#__PURE__*/_react.default.createElement("div", {
132
+ id: id,
133
+ style: style,
134
+ ref: containerRef
135
+ }, children);
136
+ };
137
+ var _default = AdaptiveCardAccessibilityWrapper;
138
+ exports.default = _default;