@microsoft/omnichannel-chat-widget 1.8.4-main.bedd258 → 1.8.4-main.c3a5432

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 (42) hide show
  1. package/README.md +1 -0
  2. package/lib/cjs/common/Constants.js +0 -1
  3. package/lib/cjs/components/citationpanestateful/CitationPaneStateful.js +7 -1
  4. package/lib/cjs/components/confirmationpanestateful/ConfirmationPaneStateful.js +11 -1
  5. package/lib/cjs/components/emailtranscriptpanestateful/EmailTranscriptPaneStateful.js +5 -1
  6. package/lib/cjs/components/livechatwidget/common/createMarkdown.js +96 -5
  7. package/lib/cjs/components/livechatwidget/common/initWebChatComposer.js +16 -12
  8. package/lib/cjs/components/postchatloadingpanestateful/PostChatLoadingPaneStateful.js +15 -3
  9. package/lib/cjs/components/postchatsurveypanestateful/PostChatSurveyPaneStateful.js +1 -0
  10. package/lib/cjs/components/prechatsurveypanestateful/PreChatSurveyPaneStateful.js +64 -2
  11. package/lib/cjs/components/webchatcontainerstateful/WebChatContainerStateful.js +98 -17
  12. package/lib/cjs/components/webchatcontainerstateful/common/DesignerChatAdapter.js +10 -0
  13. package/lib/cjs/components/webchatcontainerstateful/common/defaultProps/defaultWebChatContainerStatefulProps.js +2 -1
  14. package/lib/cjs/components/webchatcontainerstateful/common/utils/chatAdapterUtils.js +3 -1
  15. package/lib/cjs/components/webchatcontainerstateful/common/utils/citationA11y.js +195 -0
  16. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +65 -0
  17. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +4 -8
  18. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/localizedStringsBotInitialsMiddleware.js +43 -5
  19. package/lib/cjs/contexts/common/LiveChatWidgetContextInitialState.js +11 -0
  20. package/lib/esm/common/Constants.js +0 -1
  21. package/lib/esm/components/citationpanestateful/CitationPaneStateful.js +7 -1
  22. package/lib/esm/components/confirmationpanestateful/ConfirmationPaneStateful.js +11 -1
  23. package/lib/esm/components/emailtranscriptpanestateful/EmailTranscriptPaneStateful.js +5 -1
  24. package/lib/esm/components/livechatwidget/common/createMarkdown.js +96 -5
  25. package/lib/esm/components/livechatwidget/common/initWebChatComposer.js +16 -12
  26. package/lib/esm/components/postchatloadingpanestateful/PostChatLoadingPaneStateful.js +16 -4
  27. package/lib/esm/components/postchatsurveypanestateful/PostChatSurveyPaneStateful.js +1 -0
  28. package/lib/esm/components/prechatsurveypanestateful/PreChatSurveyPaneStateful.js +65 -3
  29. package/lib/esm/components/webchatcontainerstateful/WebChatContainerStateful.js +99 -18
  30. package/lib/esm/components/webchatcontainerstateful/common/DesignerChatAdapter.js +10 -0
  31. package/lib/esm/components/webchatcontainerstateful/common/defaultProps/defaultWebChatContainerStatefulProps.js +2 -1
  32. package/lib/esm/components/webchatcontainerstateful/common/utils/chatAdapterUtils.js +3 -1
  33. package/lib/esm/components/webchatcontainerstateful/common/utils/citationA11y.js +188 -0
  34. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +65 -0
  35. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +5 -9
  36. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/localizedStringsBotInitialsMiddleware.js +43 -5
  37. package/lib/esm/contexts/common/LiveChatWidgetContextInitialState.js +11 -0
  38. package/lib/types/common/Constants.d.ts +0 -1
  39. package/lib/types/components/webchatcontainerstateful/common/utils/chatAdapterUtils.d.ts +1 -1
  40. package/lib/types/components/webchatcontainerstateful/common/utils/citationA11y.d.ts +1 -0
  41. package/lib/types/components/webchatcontainerstateful/interfaces/IWebChatContainerStatefulProps.d.ts +7 -0
  42. package/package.json +24 -6
package/README.md CHANGED
@@ -311,6 +311,7 @@ BroadcastService.getMessageByEventName("CloseChat").subscribe(async (msg) => {
311
311
  [Create LiveChatWidget with Webpack5 and TypeScript](https://github.com/microsoft/omnichannel-chat-widget/blob/main/docs/BuildingUsingWebpack5.md)\
312
312
  [Omnichannel Features](https://github.com/microsoft/omnichannel-chat-widget/blob/main/docs/Features.md)\
313
313
  [How to Add Visual Regression Tests](https://github.com/microsoft/omnichannel-chat-widget/blob/main/docs/VisualRegressionTestingGuide.md)\
314
+ [Accessibility Tooling](https://github.com/microsoft/omnichannel-chat-widget/blob/main/docs/accessibility/README.md)\
314
315
  [Security](https://github.com/microsoft/omnichannel-chat-widget/blob/main/SECURITY.md)\
315
316
  [Third Party Cookie Support](https://github.com/microsoft/omnichannel-chat-widget/blob/main/docs/Tpc.md)\
316
317
  [Storybook](https://microsoft.github.io/omnichannel-chat-widget/docs/storybook/)
@@ -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"
@@ -42,10 +42,8 @@ const createMarkdown = (disableMarkdownMessageFormatting, disableNewLineMarkdown
42
42
 
43
43
  markdown.disable(["strikethrough"]);
44
44
 
45
- // Custom plugin to fix numbered list continuity
45
+ // Custom plugin to fix numbered list continuity and merge adjacent markdown links with the same href.
46
46
  markdown.use(function (md) {
47
- const originalRender = md.render.bind(md);
48
- const originalRenderInline = md.renderInline.bind(md);
49
47
  function preprocessText(text) {
50
48
  // Handle numbered lists that come with double line breaks (knowledge article format)
51
49
  // This ensures proper continuous numbering instead of separate lists
@@ -80,16 +78,109 @@ const createMarkdown = (disableMarkdownMessageFormatting, disableNewLineMarkdown
80
78
  return result;
81
79
  }
82
80
 
81
+ // Accessibility fix: when a bot emits content like "[1.](url) [View details](url)",
82
+ // markdown-it renders two sibling links that screen readers announce as two separate
83
+ // focusable links. Merge consecutive markdown link tokens with identical attributes so
84
+ // the number and label form one combined focusable link without discarding metadata.
85
+ md.core.ruler.after("inline", "merge_adjacent_same_href_links", function (state) {
86
+ const sortedAttrs = token => (token.attrs || []).map(attr => `${attr[0]}=${attr[1]}`).sort();
87
+ const hasSameAttributes = (first, second) => {
88
+ const firstAttrs = sortedAttrs(first);
89
+ const secondAttrs = sortedAttrs(second);
90
+ return firstAttrs.length === secondAttrs.length && firstAttrs.every((attr, index) => attr === secondAttrs[index]);
91
+ };
92
+ const collectLink = (children, index) => {
93
+ var _open$attrGet;
94
+ const open = children[index];
95
+ if (!open || open.type !== "link_open") {
96
+ return undefined;
97
+ }
98
+ const href = (_open$attrGet = open.attrGet) === null || _open$attrGet === void 0 ? void 0 : _open$attrGet.call(open, "href");
99
+ if (!href) {
100
+ return undefined;
101
+ }
102
+ let depth = 0;
103
+ for (let currentIndex = index; currentIndex < children.length; currentIndex++) {
104
+ const token = children[currentIndex];
105
+ if (token.type === "link_open") {
106
+ depth++;
107
+ } else if (token.type === "link_close") {
108
+ depth--;
109
+ if (depth === 0) {
110
+ return {
111
+ closeIndex: currentIndex,
112
+ href,
113
+ open
114
+ };
115
+ }
116
+ }
117
+ }
118
+ return undefined;
119
+ };
120
+ const getNextAdjacentLink = (children, startIndex) => {
121
+ const separatorTokens = [];
122
+ let index = startIndex;
123
+ while (index < children.length && children[index].type === "text" && /^\s*$/.test(children[index].content || "")) {
124
+ separatorTokens.push(children[index]);
125
+ index++;
126
+ }
127
+ const link = collectLink(children, index);
128
+ if (!link) {
129
+ return undefined;
130
+ }
131
+ return {
132
+ ...link,
133
+ openIndex: index,
134
+ separatorTokens
135
+ };
136
+ };
137
+ state.tokens.forEach(blockToken => {
138
+ if (blockToken.type !== "inline" || !blockToken.children) {
139
+ return;
140
+ }
141
+ const mergedChildren = [];
142
+ const children = blockToken.children;
143
+ let index = 0;
144
+ while (index < children.length) {
145
+ const firstLink = collectLink(children, index);
146
+ if (!firstLink) {
147
+ mergedChildren.push(children[index]);
148
+ index++;
149
+ continue;
150
+ }
151
+ const linkTokens = [firstLink.open, ...children.slice(index + 1, firstLink.closeIndex)];
152
+ let nextIndex = firstLink.closeIndex + 1;
153
+ let mergedAnyLink = false;
154
+ let nextLink = getNextAdjacentLink(children, nextIndex);
155
+ while (nextLink && nextLink.href === firstLink.href && hasSameAttributes(firstLink.open, nextLink.open)) {
156
+ linkTokens.push(...nextLink.separatorTokens);
157
+ linkTokens.push(...children.slice(nextLink.openIndex + 1, nextLink.closeIndex));
158
+ nextIndex = nextLink.closeIndex + 1;
159
+ mergedAnyLink = true;
160
+ nextLink = getNextAdjacentLink(children, nextIndex);
161
+ }
162
+ if (mergedAnyLink) {
163
+ mergedChildren.push(...linkTokens, children[firstLink.closeIndex]);
164
+ index = nextIndex;
165
+ } else {
166
+ mergedChildren.push(children[index]);
167
+ index++;
168
+ }
169
+ }
170
+ blockToken.children = mergedChildren;
171
+ });
172
+ });
173
+
83
174
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
175
  md.render = function (text, env) {
85
176
  const processedText = preprocessText(text);
86
- return originalRender(processedText, env);
177
+ return md.renderer.render(md.parse(processedText, env), md.options, env);
87
178
  };
88
179
 
89
180
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
90
181
  md.renderInline = function (text, env) {
91
182
  const processedText = preprocessText(text);
92
- return originalRenderInline(processedText, env);
183
+ return md.renderer.render(md.parseInline(processedText, env), md.options, env);
93
184
  };
94
185
  });
95
186
 
@@ -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,32 @@ const PreChatSurveyPaneStateful = props => {
175
215
  }
176
216
  }
177
217
  }, [state.appStates.isMinimized]);
178
- return /*#__PURE__*/_react.default.createElement(_omnichannelChatComponents.PreChatSurveyPane, {
218
+ (0, _react.useEffect)(() => () => {
219
+ if (liveRegionUpdateTimeout.current) {
220
+ window.clearTimeout(liveRegionUpdateTimeout.current);
221
+ }
222
+ }, []);
223
+ return /*#__PURE__*/_react.default.createElement("div", {
224
+ onFocusCapture: announceFocusedElement
225
+ }, /*#__PURE__*/_react.default.createElement("div", {
226
+ role: "status",
227
+ "aria-live": "polite",
228
+ "aria-atomic": "true",
229
+ style: {
230
+ position: "absolute",
231
+ width: "1px",
232
+ height: "1px",
233
+ margin: "-1px",
234
+ padding: 0,
235
+ border: 0,
236
+ clip: "rect(0, 0, 0, 0)",
237
+ overflow: "hidden",
238
+ whiteSpace: "nowrap"
239
+ }
240
+ }, preChatFocusAnnouncement), /*#__PURE__*/_react.default.createElement(_omnichannelChatComponents.PreChatSurveyPane, {
179
241
  controlProps: controlProps,
180
242
  styleProps: styleProps
181
- });
243
+ }));
182
244
  };
183
245
  exports.PreChatSurveyPaneStateful = PreChatSurveyPaneStateful;
184
246
  var _default = PreChatSurveyPaneStateful;
@@ -31,6 +31,7 @@ var _liveChatConfigUtils = require("../livechatwidget/common/liveChatConfigUtils
31
31
  var _ = require("../..");
32
32
  var _useFacadeChatSDKStore = _interopRequireDefault(require("../../hooks/useFacadeChatSDKStore"));
33
33
  var _usePersistentChatHistory = _interopRequireDefault(require("./hooks/usePersistentChatHistory"));
34
+ var _citationA11y = require("./common/utils/citationA11y");
34
35
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
35
36
  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); }
36
37
  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; }
@@ -155,6 +156,56 @@ const WebChatContainerStateful = props => {
155
156
  document.addEventListener("click", clickHandler);
156
157
  return () => document.removeEventListener("click", clickHandler);
157
158
  }, [state]);
159
+
160
+ // Accessibility: Merge citation card's number badge and link text into a single
161
+ // focusable element. WebChat's LinkDefinitionItem renders an <a> containing a
162
+ // badge <div> (the number, e.g. "1") and a text <div> (the title). On iOS
163
+ // VoiceOver and Android TalkBack, those block-level descendants are otherwise
164
+ // announced as two separate focusable links. The patch sets a combined
165
+ // aria-label on the anchor and hides every descendant from the a11y tree so
166
+ // the whole card is announced as one link.
167
+ (0, _react2.useEffect)(() => {
168
+ const webChatRoot = document.getElementById("ms_lcw_webchat_root");
169
+ if (!webChatRoot) {
170
+ return;
171
+ }
172
+ (0, _citationA11y.patchCitationAnchorsForA11y)(webChatRoot);
173
+ const observer = new MutationObserver(mutations => {
174
+ for (const m of mutations) {
175
+ if (m.type === "childList") {
176
+ m.addedNodes.forEach(node => {
177
+ if (node.nodeType === Node.ELEMENT_NODE) {
178
+ var _parentElement, _parentElement$closes;
179
+ (0, _citationA11y.patchCitationAnchorsForA11y)(node);
180
+ // A child added inside an existing anchor (e.g. React
181
+ // mounting OpenInNewWindowIcon after the anchor) means
182
+ // the parent anchor needs to re-walk its descendants.
183
+ const ancestor = (_parentElement = node.parentElement) === null || _parentElement === void 0 ? void 0 : (_parentElement$closes = _parentElement.closest) === null || _parentElement$closes === void 0 ? void 0 : _parentElement$closes.call(_parentElement, "a.webchat__link-definitions__list-item-box");
184
+ if (ancestor) {
185
+ (0, _citationA11y.patchCitationAnchorsForA11y)(ancestor);
186
+ }
187
+ }
188
+ });
189
+ } else if (m.type === "attributes" && m.target.nodeType === Node.ELEMENT_NODE) {
190
+ var _target$closest;
191
+ // React may strip our aria-hidden during a re-render; reapply
192
+ // by re-patching the closest matching anchor ancestor.
193
+ const target = m.target;
194
+ const ancestor = (_target$closest = target.closest) === null || _target$closest === void 0 ? void 0 : _target$closest.call(target, "a.webchat__link-definitions__list-item-box");
195
+ if (ancestor) {
196
+ (0, _citationA11y.patchCitationAnchorsForA11y)(ancestor);
197
+ }
198
+ }
199
+ }
200
+ });
201
+ observer.observe(webChatRoot, {
202
+ childList: true,
203
+ subtree: true,
204
+ attributes: true,
205
+ attributeFilter: ["aria-hidden", "role", "tabindex", "inert"]
206
+ });
207
+ return () => observer.disconnect();
208
+ }, []);
158
209
  const minimizedStyles = state.appStates.isMinimized ? shouldLoadPersistentHistoryMessages ? {
159
210
  visibility: "hidden",
160
211
  position: "absolute",
@@ -181,6 +232,48 @@ const WebChatContainerStateful = props => {
181
232
  chatHistoryElement.setAttribute(_Constants.HtmlAttributeNames.ariaLabel, webChatContainerProps.webChatHistoryMobileAccessibilityLabel);
182
233
  }
183
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
+ }
184
277
  dispatch({
185
278
  type: _LiveChatWidgetActionType.LiveChatWidgetActionType.SET_RENDERING_MIDDLEWARE_PROPS,
186
279
  payload: webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.renderingMiddlewareProps
@@ -205,6 +298,10 @@ const WebChatContainerStateful = props => {
205
298
  }
206
299
  }
207
300
  }
301
+ return () => {
302
+ var _toasterObserver2;
303
+ (_toasterObserver2 = toasterObserver) === null || _toasterObserver2 === void 0 ? void 0 : _toasterObserver2.disconnect();
304
+ };
208
305
  }, []);
209
306
  (0, _react2.useEffect)(() => {
210
307
  var _props$webChatContain6, _props$webChatContain7;
@@ -445,23 +542,7 @@ const WebChatContainerStateful = props => {
445
542
  height: "100%",
446
543
  width: "100%"
447
544
  }
448
- }, shouldLoadPersistentHistoryMessages && /*#__PURE__*/_react2.default.createElement(_WebChatEventSubscribers.default, null), /*#__PURE__*/_react2.default.createElement(BasicWebChat, null))), /*#__PURE__*/_react2.default.createElement("div", {
449
- id: _Constants.HtmlIdNames.fileSentAnnouncementRegionId,
450
- role: "alert",
451
- "aria-live": "assertive",
452
- "aria-atomic": "true",
453
- style: {
454
- position: "absolute",
455
- width: "1px",
456
- height: "1px",
457
- padding: "0",
458
- margin: "-1px",
459
- overflow: "hidden",
460
- clip: "rect(0, 0, 0, 0)",
461
- whiteSpace: "nowrap",
462
- border: "0"
463
- }
464
- }), 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, {
465
546
  id: ((_props$citationPanePr = props.citationPaneProps) === null || _props$citationPanePr === void 0 ? void 0 : _props$citationPanePr.id) || _Constants.HtmlAttributeNames.ocwCitationPaneClassName,
466
547
  title: ((_props$citationPanePr2 = props.citationPaneProps) === null || _props$citationPanePr2 === void 0 ? void 0 : _props$citationPanePr2.title) || _Constants.HtmlAttributeNames.ocwCitationPaneTitle,
467
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",