@microsoft/omnichannel-chat-widget 1.8.4-main.9edfa7a → 1.8.4-main.a37560d

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 (18) hide show
  1. package/lib/cjs/components/livechatwidget/common/createMarkdown.js +96 -5
  2. package/lib/cjs/components/postchatloadingpanestateful/PostChatLoadingPaneStateful.js +15 -3
  3. package/lib/cjs/components/postchatsurveypanestateful/PostChatSurveyPaneStateful.js +1 -0
  4. package/lib/cjs/components/prechatsurveypanestateful/PreChatSurveyPaneStateful.js +64 -2
  5. package/lib/cjs/components/webchatcontainerstateful/WebChatContainerStateful.js +51 -0
  6. package/lib/cjs/components/webchatcontainerstateful/common/utils/citationA11y.js +195 -0
  7. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +15 -0
  8. package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/localizedStringsBotInitialsMiddleware.js +43 -5
  9. package/lib/esm/components/livechatwidget/common/createMarkdown.js +96 -5
  10. package/lib/esm/components/postchatloadingpanestateful/PostChatLoadingPaneStateful.js +16 -4
  11. package/lib/esm/components/postchatsurveypanestateful/PostChatSurveyPaneStateful.js +1 -0
  12. package/lib/esm/components/prechatsurveypanestateful/PreChatSurveyPaneStateful.js +65 -3
  13. package/lib/esm/components/webchatcontainerstateful/WebChatContainerStateful.js +51 -0
  14. package/lib/esm/components/webchatcontainerstateful/common/utils/citationA11y.js +188 -0
  15. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +15 -0
  16. package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/localizedStringsBotInitialsMiddleware.js +43 -5
  17. package/lib/types/components/webchatcontainerstateful/common/utils/citationA11y.d.ts +1 -0
  18. package/package.json +3 -3
@@ -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
 
@@ -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",
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.patchCitationAnchorsForA11y = void 0;
7
+ // Accessibility helper: Merges WebChat's citation card number badge and link
8
+ // text into a single focusable element.
9
+ //
10
+ // WebChat's LinkDefinitionItem renders an <a> containing a badge <div> (number)
11
+ // and a text <div> (title). On iOS VoiceOver, these block-level descendants are
12
+ // announced as two separate focusable links (e.g. "1, link" and
13
+ // "Assessment of eligibility..., link"). Setting a combined aria-label on the
14
+ // anchor and hiding inner descendants from the accessibility tree ensures the
15
+ // whole card is announced as one link.
16
+
17
+ const CITATION_ANCHOR_SELECTOR = "a.webchat__link-definitions__list-item-box";
18
+ const PATCHED_MARKER = "ocwCitationA11yPatched";
19
+ const STYLE_TAG_ID = "ocw-citation-a11y-styles";
20
+
21
+ // Global CSS injected once per document. Neutralizes iOS Safari's per-inner-
22
+ // element tap highlight / selection / focus rectangles that otherwise make
23
+ // the "1" badge and the link text appear as separate selectable areas even
24
+ // though they are inside a single <a>.
25
+ // Strategy:
26
+ // - Make the anchor a block-level positioning parent.
27
+ // - Add a transparent ::after pseudo-element that covers the entire anchor.
28
+ // iOS Safari hit-tests that overlay as a SINGLE region, so the tap
29
+ // highlight / selection rectangle always covers the whole card instead
30
+ // of just the "1" badge or just the link text.
31
+ // - Make every descendant transparent to pointer / focus, so they can never
32
+ // become a standalone tap target.
33
+ const STYLE_RULES = `
34
+ a.webchat__link-definitions__list-item-box {
35
+ position: relative;
36
+ display: block;
37
+ isolation: isolate;
38
+ -webkit-tap-highlight-color: rgba(0, 120, 212, 0.15);
39
+ -webkit-touch-callout: none;
40
+ cursor: pointer;
41
+ }
42
+ a.webchat__link-definitions__list-item-box::after {
43
+ content: "";
44
+ position: absolute;
45
+ inset: 0;
46
+ z-index: 2;
47
+ background: transparent;
48
+ pointer-events: auto;
49
+ -webkit-tap-highlight-color: rgba(0, 120, 212, 0.15);
50
+ }
51
+ a.webchat__link-definitions__list-item-box * {
52
+ pointer-events: none;
53
+ -webkit-tap-highlight-color: transparent;
54
+ -webkit-touch-callout: none;
55
+ outline: none;
56
+ text-decoration: inherit;
57
+ cursor: inherit;
58
+ }
59
+ `;
60
+ const ensureStyleInjected = doc => {
61
+ if (!doc || typeof doc.getElementById !== "function" || doc.getElementById(STYLE_TAG_ID)) {
62
+ return;
63
+ }
64
+ const style = doc.createElement("style");
65
+ style.id = STYLE_TAG_ID;
66
+ style.textContent = STYLE_RULES;
67
+ (doc.head || doc.documentElement).appendChild(style);
68
+ };
69
+ const patchCitationAnchorsForA11y = root => {
70
+ var _defaultView;
71
+ if (!root || typeof root.querySelectorAll !== "function") {
72
+ return;
73
+ }
74
+
75
+ // Inject global CSS once per document. This covers every citation anchor
76
+ // regardless of whether our per-node patch has run on it yet.
77
+ const rootNode = root;
78
+ const doc = rootNode.ownerDocument ?? ((_defaultView = root.defaultView) === null || _defaultView === void 0 ? void 0 : _defaultView.document) ?? root ?? (typeof document !== "undefined" ? document : null);
79
+ if (doc && typeof doc.createElement === "function") {
80
+ ensureStyleInjected(doc);
81
+ }
82
+
83
+ // Also include the root itself when it matches — calls from MutationObserver
84
+ // pass the anchor element directly, which querySelectorAll wouldn't return.
85
+ const matched = new Set();
86
+ if (typeof root.matches === "function" && root.matches(CITATION_ANCHOR_SELECTOR)) {
87
+ matched.add(root);
88
+ }
89
+ root.querySelectorAll(CITATION_ANCHOR_SELECTOR).forEach(n => {
90
+ matched.add(n);
91
+ });
92
+ matched.forEach(anchor => {
93
+ var _badge$textContent, _textEl$textContent;
94
+ const badge = anchor.querySelector(".webchat__link-definitions__badge");
95
+ const textEl = anchor.querySelector(".webchat__link-definitions__list-item-text");
96
+ const identifier = (badge === null || badge === void 0 ? void 0 : (_badge$textContent = badge.textContent) === null || _badge$textContent === void 0 ? void 0 : _badge$textContent.trim()) ?? "";
97
+ // Prefer the explicit text element. For plain (non-card) citation anchors
98
+ // (no badge), fall back to the title or the anchor's own text. When a
99
+ // badge is present but no text element, avoid using anchor.textContent
100
+ // since it would duplicate the identifier.
101
+ let text = (textEl === null || textEl === void 0 ? void 0 : (_textEl$textContent = textEl.textContent) === null || _textEl$textContent === void 0 ? void 0 : _textEl$textContent.trim()) ?? "";
102
+ if (!text && !badge) {
103
+ var _anchor$getAttribute, _anchor$textContent;
104
+ text = ((_anchor$getAttribute = anchor.getAttribute("title")) === null || _anchor$getAttribute === void 0 ? void 0 : _anchor$getAttribute.trim()) || ((_anchor$textContent = anchor.textContent) === null || _anchor$textContent === void 0 ? void 0 : _anchor$textContent.trim()) || "";
105
+ }
106
+ if (!identifier && !text) {
107
+ return;
108
+ }
109
+ const combinedLabel = identifier && text ? `${identifier}, ${text}` : identifier || text;
110
+
111
+ // Always (re-)apply aria-label if absent. Setting it again with the same
112
+ // value when it already matches is a no-op for the DOM; this keeps the
113
+ // patch idempotent so React re-renders that strip the attribute heal.
114
+ const existingLabel = anchor.getAttribute("aria-label");
115
+ if (!existingLabel) {
116
+ anchor.setAttribute("aria-label", combinedLabel);
117
+ }
118
+
119
+ // Explicitly mark the anchor as a single link so iOS VoiceOver treats
120
+ // the whole card as one element.
121
+ if (!anchor.getAttribute("role")) {
122
+ anchor.setAttribute("role", "link");
123
+ }
124
+
125
+ // Nuclear a11y lockdown of EVERY descendant of the anchor (not just
126
+ // known class names, because WebChat can render additional nodes).
127
+ // iOS VoiceOver is known to split focus on block-level descendants
128
+ // inside an <a>. Applying all of the following guarantees only the
129
+ // outer <a> is a selectable a11y / focus target:
130
+ // - aria-hidden="true" removes element from the a11y tree
131
+ // - role="presentation" strips any implicit semantic role
132
+ // - inert removes element from focus + interaction
133
+ // - tabindex="-1" defensive (covers browsers without inert)
134
+ // - title removed iOS VoiceOver in WKWebView still surfaces
135
+ // elements with a `title` attribute as a
136
+ // separately swipeable item even when
137
+ // aria-hidden=true. WebChat's ItemBody sets
138
+ // title={text} on the inner link-text div,
139
+ // which is the root cause of the "two
140
+ // separate links" announcement on iOS.
141
+ // We move the title to a data-* attribute
142
+ // so any tooltip-like tooling can still
143
+ // read it, but iOS VO ignores it.
144
+ anchor.querySelectorAll("*").forEach(el => {
145
+ const inner = el;
146
+ if (inner.getAttribute("aria-hidden") !== "true") {
147
+ inner.setAttribute("aria-hidden", "true");
148
+ }
149
+ if (inner.getAttribute("role") !== "presentation") {
150
+ inner.setAttribute("role", "presentation");
151
+ }
152
+ if (inner.getAttribute("tabindex") !== "-1") {
153
+ inner.setAttribute("tabindex", "-1");
154
+ }
155
+ if (!inner.hasAttribute("inert")) {
156
+ inner.setAttribute("inert", "");
157
+ }
158
+ const innerTitle = inner.getAttribute("title");
159
+ if (innerTitle !== null) {
160
+ inner.removeAttribute("title");
161
+ if (!inner.dataset.ocwOriginalTitle) {
162
+ inner.dataset.ocwOriginalTitle = innerTitle;
163
+ }
164
+ }
165
+ if (inner.style.pointerEvents !== "none") {
166
+ inner.style.pointerEvents = "none";
167
+ }
168
+ });
169
+
170
+ // DOM-level full-bleed overlay. Inline styles are used so this works
171
+ // even when the widget renders inside a shadow DOM (where an
172
+ // injected <style> tag in document.head cannot reach). The overlay
173
+ // becomes the ONLY visible hit-test target for iOS Safari, so tapping
174
+ // anywhere on the card highlights the entire card instead of just the
175
+ // "1" badge or just the link text.
176
+ const anchorStyle = anchor.style;
177
+ if (!anchorStyle.position) {
178
+ anchorStyle.position = "relative";
179
+ }
180
+ if (!anchorStyle.display || anchorStyle.display === "inline") {
181
+ anchorStyle.display = "block";
182
+ }
183
+ const ownerDoc = anchor.ownerDocument;
184
+ const existingOverlay = anchor.querySelector(":scope > [data-ocw-citation-overlay='true']");
185
+ if (!existingOverlay && ownerDoc && typeof ownerDoc.createElement === "function") {
186
+ const overlay = ownerDoc.createElement("span");
187
+ overlay.setAttribute("aria-hidden", "true");
188
+ overlay.setAttribute("data-ocw-citation-overlay", "true");
189
+ overlay.style.cssText = ["position:absolute", "top:0", "left:0", "right:0", "bottom:0", "z-index:2", "background:transparent", "pointer-events:auto", "-webkit-tap-highlight-color:rgba(0,120,212,0.15)", "-webkit-touch-callout:none", "-webkit-user-select:none", "user-select:none", "cursor:pointer"].join(";");
190
+ anchor.appendChild(overlay);
191
+ }
192
+ anchor.dataset[PATCHED_MARKER] = "true";
193
+ });
194
+ };
195
+ exports.patchCitationAnchorsForA11y = patchCitationAnchorsForA11y;
@@ -32,6 +32,21 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
32
32
  const container = containerRef.current;
33
33
  if (!container) return;
34
34
 
35
+ // action-button-toggle / login-button-toggle: submit/login action buttons can be
36
+ // rendered with toggle-only ARIA that causes screen readers to announce them as
37
+ // switches instead of plain push buttons. Preserve Action.ToggleVisibility buttons,
38
+ // which own aria-controls and legitimately expose a pressed state.
39
+ const actionButtons = container.querySelectorAll(".ac-actionSet button.ac-pushButton:not([aria-controls])");
40
+ actionButtons.forEach(btn => {
41
+ if (btn.hasAttribute("aria-pressed")) {
42
+ btn.removeAttribute("aria-pressed");
43
+ }
44
+ const role = btn.getAttribute("role");
45
+ if (role === "switch") {
46
+ btn.removeAttribute("role");
47
+ }
48
+ });
49
+
35
50
  // Find all radio inputs inside the adaptive card
36
51
  const radios = container.querySelectorAll("input[type='radio']");
37
52
  if (radios.length === 0) return;
@@ -16,6 +16,32 @@ let currentAgentName = _defaultWebChatStyles.defaultWebChatStyles.botAvatarIniti
16
16
 
17
17
  // Optional external updater (React context dispatch wrapper) set at runtime
18
18
  let externalInitialsUpdater;
19
+ const hasTransferTag = activity => {
20
+ var _activity$channelData;
21
+ const tags = [...(Array.isArray(activity === null || activity === void 0 ? void 0 : (_activity$channelData = activity.channelData) === null || _activity$channelData === void 0 ? void 0 : _activity$channelData.tags) ? activity.channelData.tags : []), ...(Array.isArray(activity === null || activity === void 0 ? void 0 : activity.tags) ? activity.tags : [])];
22
+ return tags.some(tag => typeof tag === "string" && tag.toLowerCase().includes("transfer"));
23
+ };
24
+ const isTransferSystemMessage = activity => {
25
+ const text = ((activity === null || activity === void 0 ? void 0 : activity.text) || "").toString();
26
+ return hasTransferTag(activity) || /\btransfer(?:red|ring)?\b/i.test(text);
27
+ };
28
+ const resetCachedAgentName = dispatch => {
29
+ if (currentAgentName !== _defaultWebChatStyles.defaultWebChatStyles.botAvatarInitials || currentAgentInitials !== _defaultWebChatStyles.defaultWebChatStyles.botAvatarInitials) {
30
+ var _externalInitialsUpda;
31
+ currentAgentName = _defaultWebChatStyles.defaultWebChatStyles.botAvatarInitials;
32
+ currentAgentInitials = _defaultWebChatStyles.defaultWebChatStyles.botAvatarInitials;
33
+ (_externalInitialsUpda = externalInitialsUpdater) === null || _externalInitialsUpda === void 0 ? void 0 : _externalInitialsUpda(currentAgentInitials || "");
34
+ _omnichannelChatComponents.BroadcastService.postMessage({
35
+ eventName: "BotAvatarInitialsUpdated",
36
+ payload: {
37
+ initials: currentAgentInitials
38
+ }
39
+ });
40
+ dispatch({
41
+ type: "__BOT_INITIALS_UPDATED__"
42
+ });
43
+ }
44
+ };
19
45
  const localizedStringsBotInitialsMiddleware = onInitialsChange => _ref => {
20
46
  let {
21
47
  dispatch
@@ -29,19 +55,31 @@ const localizedStringsBotInitialsMiddleware = onInitialsChange => _ref => {
29
55
  var _action$payload, _activity$from;
30
56
  const activity = (_action$payload = action.payload) === null || _action$payload === void 0 ? void 0 : _action$payload.activity;
31
57
  if (activity !== null && activity !== void 0 && (_activity$from = activity.from) !== null && _activity$from !== void 0 && _activity$from.name && activity.from.role !== _Constants.Constants.userMessageTag && activity.from.name !== _Constants.Constants.userMessageTag) {
32
- var _activity$channelData, _activity$channelData2, _activity$tags;
58
+ var _activity$channelData2, _activity$channelData3, _activity$tags;
33
59
  const agentName = activity.from.name.trim();
34
- const isSystemMessage = agentName === "__agent__" || agentName.startsWith("__") || ((_activity$channelData = activity.channelData) === null || _activity$channelData === void 0 ? void 0 : (_activity$channelData2 = _activity$channelData.tags) === null || _activity$channelData2 === void 0 ? void 0 : _activity$channelData2.includes(_Constants.Constants.systemMessageTag)) || ((_activity$tags = activity.tags) === null || _activity$tags === void 0 ? void 0 : _activity$tags.includes(_Constants.Constants.systemMessageTag));
35
- if (!isSystemMessage && agentName) {
60
+ const isSystemMessage = agentName === "__agent__" || agentName.startsWith("__") || ((_activity$channelData2 = activity.channelData) === null || _activity$channelData2 === void 0 ? void 0 : (_activity$channelData3 = _activity$channelData2.tags) === null || _activity$channelData3 === void 0 ? void 0 : _activity$channelData3.includes(_Constants.Constants.systemMessageTag)) || ((_activity$tags = activity.tags) === null || _activity$tags === void 0 ? void 0 : _activity$tags.includes(_Constants.Constants.systemMessageTag));
61
+ if (isSystemMessage) {
62
+ // transfer-stale-bot-name: when a transfer system message indicates the
63
+ // conversation has been routed to a different agent, the
64
+ // previously-cached agent name must NOT linger. Otherwise the
65
+ // next non-system activity (e.g. typing indicator, welcome
66
+ // message, or the new agent's first turn before their name
67
+ // is observed) is announced to screen readers as the OLD
68
+ // agent. Reset to defaults so the next observed name takes
69
+ // effect cleanly.
70
+ if (isTransferSystemMessage(activity)) {
71
+ resetCachedAgentName(dispatch);
72
+ }
73
+ } else if (agentName) {
36
74
  const newInitials = (0, _utils.getIconText)(agentName) || currentAgentInitials;
37
75
  const hasInitialsChanged = newInitials !== currentAgentInitials;
38
76
  const hasNameChanged = agentName !== currentAgentName;
39
77
  currentAgentName = agentName;
40
78
  if (hasInitialsChanged) {
41
- var _externalInitialsUpda;
79
+ var _externalInitialsUpda2;
42
80
  currentAgentInitials = newInitials;
43
81
  // Notify external React context if provided
44
- (_externalInitialsUpda = externalInitialsUpdater) === null || _externalInitialsUpda === void 0 ? void 0 : _externalInitialsUpda(currentAgentInitials || "");
82
+ (_externalInitialsUpda2 = externalInitialsUpdater) === null || _externalInitialsUpda2 === void 0 ? void 0 : _externalInitialsUpda2(currentAgentInitials || "");
45
83
  // Broadcast (optional) for multi-tab sync without forcing consumers
46
84
  _omnichannelChatComponents.BroadcastService.postMessage({
47
85
  eventName: "BotAvatarInitialsUpdated",
@@ -36,10 +36,8 @@ export const createMarkdown = (disableMarkdownMessageFormatting, disableNewLineM
36
36
 
37
37
  markdown.disable(["strikethrough"]);
38
38
 
39
- // Custom plugin to fix numbered list continuity
39
+ // Custom plugin to fix numbered list continuity and merge adjacent markdown links with the same href.
40
40
  markdown.use(function (md) {
41
- const originalRender = md.render.bind(md);
42
- const originalRenderInline = md.renderInline.bind(md);
43
41
  function preprocessText(text) {
44
42
  // Handle numbered lists that come with double line breaks (knowledge article format)
45
43
  // This ensures proper continuous numbering instead of separate lists
@@ -74,16 +72,109 @@ export const createMarkdown = (disableMarkdownMessageFormatting, disableNewLineM
74
72
  return result;
75
73
  }
76
74
 
75
+ // Accessibility fix: when a bot emits content like "[1.](url) [View details](url)",
76
+ // markdown-it renders two sibling links that screen readers announce as two separate
77
+ // focusable links. Merge consecutive markdown link tokens with identical attributes so
78
+ // the number and label form one combined focusable link without discarding metadata.
79
+ md.core.ruler.after("inline", "merge_adjacent_same_href_links", function (state) {
80
+ const sortedAttrs = token => (token.attrs || []).map(attr => `${attr[0]}=${attr[1]}`).sort();
81
+ const hasSameAttributes = (first, second) => {
82
+ const firstAttrs = sortedAttrs(first);
83
+ const secondAttrs = sortedAttrs(second);
84
+ return firstAttrs.length === secondAttrs.length && firstAttrs.every((attr, index) => attr === secondAttrs[index]);
85
+ };
86
+ const collectLink = (children, index) => {
87
+ var _open$attrGet;
88
+ const open = children[index];
89
+ if (!open || open.type !== "link_open") {
90
+ return undefined;
91
+ }
92
+ const href = (_open$attrGet = open.attrGet) === null || _open$attrGet === void 0 ? void 0 : _open$attrGet.call(open, "href");
93
+ if (!href) {
94
+ return undefined;
95
+ }
96
+ let depth = 0;
97
+ for (let currentIndex = index; currentIndex < children.length; currentIndex++) {
98
+ const token = children[currentIndex];
99
+ if (token.type === "link_open") {
100
+ depth++;
101
+ } else if (token.type === "link_close") {
102
+ depth--;
103
+ if (depth === 0) {
104
+ return {
105
+ closeIndex: currentIndex,
106
+ href,
107
+ open
108
+ };
109
+ }
110
+ }
111
+ }
112
+ return undefined;
113
+ };
114
+ const getNextAdjacentLink = (children, startIndex) => {
115
+ const separatorTokens = [];
116
+ let index = startIndex;
117
+ while (index < children.length && children[index].type === "text" && /^\s*$/.test(children[index].content || "")) {
118
+ separatorTokens.push(children[index]);
119
+ index++;
120
+ }
121
+ const link = collectLink(children, index);
122
+ if (!link) {
123
+ return undefined;
124
+ }
125
+ return {
126
+ ...link,
127
+ openIndex: index,
128
+ separatorTokens
129
+ };
130
+ };
131
+ state.tokens.forEach(blockToken => {
132
+ if (blockToken.type !== "inline" || !blockToken.children) {
133
+ return;
134
+ }
135
+ const mergedChildren = [];
136
+ const children = blockToken.children;
137
+ let index = 0;
138
+ while (index < children.length) {
139
+ const firstLink = collectLink(children, index);
140
+ if (!firstLink) {
141
+ mergedChildren.push(children[index]);
142
+ index++;
143
+ continue;
144
+ }
145
+ const linkTokens = [firstLink.open, ...children.slice(index + 1, firstLink.closeIndex)];
146
+ let nextIndex = firstLink.closeIndex + 1;
147
+ let mergedAnyLink = false;
148
+ let nextLink = getNextAdjacentLink(children, nextIndex);
149
+ while (nextLink && nextLink.href === firstLink.href && hasSameAttributes(firstLink.open, nextLink.open)) {
150
+ linkTokens.push(...nextLink.separatorTokens);
151
+ linkTokens.push(...children.slice(nextLink.openIndex + 1, nextLink.closeIndex));
152
+ nextIndex = nextLink.closeIndex + 1;
153
+ mergedAnyLink = true;
154
+ nextLink = getNextAdjacentLink(children, nextIndex);
155
+ }
156
+ if (mergedAnyLink) {
157
+ mergedChildren.push(...linkTokens, children[firstLink.closeIndex]);
158
+ index = nextIndex;
159
+ } else {
160
+ mergedChildren.push(children[index]);
161
+ index++;
162
+ }
163
+ }
164
+ blockToken.children = mergedChildren;
165
+ });
166
+ });
167
+
77
168
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
169
  md.render = function (text, env) {
79
170
  const processedText = preprocessText(text);
80
- return originalRender(processedText, env);
171
+ return md.renderer.render(md.parse(processedText, env), md.options, env);
81
172
  };
82
173
 
83
174
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
175
  md.renderInline = function (text, env) {
85
176
  const processedText = preprocessText(text);
86
- return originalRenderInline(processedText, env);
177
+ return md.renderer.render(md.parseInline(processedText, env), md.options, env);
87
178
  };
88
179
  });
89
180
 
@@ -1,5 +1,5 @@
1
1
  import { LogLevel, TelemetryEvent } from "../../common/telemetry/TelemetryConstants";
2
- import React, { useEffect } from "react";
2
+ import React, { useEffect, useState } from "react";
3
3
  import { createTimer, findAllFocusableElement } from "../../common/utils";
4
4
  import { LoadingPane } from "@microsoft/omnichannel-chat-components";
5
5
  import { TelemetryHelper } from "../../common/telemetry/TelemetryHelper";
@@ -7,13 +7,22 @@ import { defaultGeneralPostChatLoadingPaneStyleProps } from "./common/defaultgen
7
7
  import useChatContextStore from "../../hooks/useChatContextStore";
8
8
  let uiTimer;
9
9
  export const PostChatLoadingPaneStateful = props => {
10
- var _props$styleProps;
10
+ var _props$controlProps, _props$styleProps;
11
+ const defaultSubtitleText = "Please take a moment to give us feedback about your chat experience. We are loading the survey for you now.";
12
+ const subtitleText = ((_props$controlProps = props.controlProps) === null || _props$controlProps === void 0 ? void 0 : _props$controlProps.subtitleText) ?? defaultSubtitleText;
13
+ const [announcedSubtitleText, setAnnouncedSubtitleText] = useState("");
11
14
  useEffect(() => {
12
15
  uiTimer = createTimer();
13
16
  TelemetryHelper.logLoadingEvent(LogLevel.INFO, {
14
17
  Event: TelemetryEvent.UXPostChatLoadingPaneStart
15
18
  });
16
19
  }, []);
20
+ useEffect(() => {
21
+ const timeout = window.setTimeout(() => {
22
+ setAnnouncedSubtitleText(subtitleText);
23
+ }, 0);
24
+ return () => window.clearTimeout(timeout);
25
+ }, [subtitleText]);
17
26
  const [state] = useChatContextStore();
18
27
  const generalStyleProps = Object.assign({}, defaultGeneralPostChatLoadingPaneStyleProps, (_props$styleProps = props.styleProps) === null || _props$styleProps === void 0 ? void 0 : _props$styleProps.generalStyleProps);
19
28
  const styleProps = {
@@ -27,8 +36,11 @@ export const PostChatLoadingPaneStateful = props => {
27
36
  hideTitle: true,
28
37
  hideSpinner: true,
29
38
  hideSpinnerText: true,
30
- subtitleText: "Please take a moment to give us feedback about your chat experience. We are loading the survey for you now.",
31
- ...props.controlProps
39
+ ...props.controlProps,
40
+ role: "status",
41
+ "aria-live": "polite",
42
+ "aria-atomic": "true",
43
+ subtitleText: announcedSubtitleText
32
44
  };
33
45
 
34
46
  // Move focus to the first button
@@ -58,6 +58,7 @@ export const PostChatSurveyPaneStateful = props => {
58
58
  };
59
59
  const controlProps = {
60
60
  id: "oc-lcw-postchatsurvey-pane",
61
+ title: "Post-chat survey",
61
62
  surveyURL: ((_props$controlProps = props.controlProps) === null || _props$controlProps === void 0 ? void 0 : _props$controlProps.surveyURL) ?? surveyInviteLink,
62
63
  ...props.controlProps
63
64
  };
@@ -1,6 +1,6 @@
1
1
  import { HtmlAttributeNames, Regex } from "../../common/Constants";
2
2
  import { ConversationStage, LogLevel, TelemetryEvent } from "../../common/telemetry/TelemetryConstants";
3
- import React, { useEffect } from "react";
3
+ import React, { useEffect, useRef, useState } from "react";
4
4
  import { createTimer, extractPreChatSurveyResponseValues, findAllFocusableElement, getStateFromCache, getWidgetCacheId, isUndefinedOrEmpty, parseAdaptiveCardPayload } from "../../common/utils";
5
5
  import { ConversationState } from "../../contexts/common/ConversationState";
6
6
  import { LiveChatWidgetActionType } from "../../contexts/common/LiveChatWidgetActionType";
@@ -11,6 +11,34 @@ import { defaultGeneralPreChatSurveyPaneStyleProps } from "./common/defaultStyle
11
11
  import { defaultPreChatSurveyLocalizedTexts } from "./common/defaultProps/defaultPreChatSurveyLocalizedTexts";
12
12
  import useChatContextStore from "../../hooks/useChatContextStore";
13
13
  let uiTimer;
14
+ const getFocusedElementAnnouncement = element => {
15
+ var _element$textContent;
16
+ const ariaLabel = element.getAttribute(HtmlAttributeNames.ariaLabel);
17
+ if (ariaLabel) {
18
+ return ariaLabel.trim();
19
+ }
20
+ const ariaLabelledBy = element.getAttribute(HtmlAttributeNames.ariaLabelledby);
21
+ if (ariaLabelledBy) {
22
+ const labelledByText = ariaLabelledBy.split(/\s+/).map(id => {
23
+ var _document$getElementB, _document$getElementB2;
24
+ return (_document$getElementB = document.getElementById(id)) === null || _document$getElementB === void 0 ? void 0 : (_document$getElementB2 = _document$getElementB.textContent) === null || _document$getElementB2 === void 0 ? void 0 : _document$getElementB2.trim();
25
+ }).filter(Boolean).join(" ");
26
+ if (labelledByText) {
27
+ return labelledByText;
28
+ }
29
+ }
30
+ if (element.id) {
31
+ const label = Array.from(document.querySelectorAll("label")).find(candidate => candidate.htmlFor === element.id || candidate.getAttribute("for") === element.id);
32
+ if (label !== null && label !== void 0 && label.textContent) {
33
+ return label.textContent.trim();
34
+ }
35
+ }
36
+ const parentLabel = element.closest("label");
37
+ if (parentLabel !== null && parentLabel !== void 0 && parentLabel.textContent) {
38
+ return parentLabel.textContent.trim();
39
+ }
40
+ return ((_element$textContent = element.textContent) === null || _element$textContent === void 0 ? void 0 : _element$textContent.trim()) ?? "";
41
+ };
14
42
 
15
43
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
44
  export const PreChatSurveyPaneStateful = props => {
@@ -33,6 +61,8 @@ export const PreChatSurveyPaneStateful = props => {
33
61
  surveyProps,
34
62
  initStartChat
35
63
  } = props;
64
+ const [preChatFocusAnnouncement, setPreChatFocusAnnouncement] = useState("");
65
+ const liveRegionUpdateTimeout = useRef();
36
66
  const generalStyleProps = Object.assign({}, defaultGeneralPreChatSurveyPaneStyleProps, surveyProps === null || surveyProps === void 0 ? void 0 : (_surveyProps$stylePro = surveyProps.styleProps) === null || _surveyProps$stylePro === void 0 ? void 0 : _surveyProps$stylePro.generalStyleProps, {
37
67
  display: state.appStates.isMinimized ? "none" : ""
38
68
  });
@@ -119,6 +149,16 @@ export const PreChatSurveyPaneStateful = props => {
119
149
  ...(surveyProps === null || surveyProps === void 0 ? void 0 : surveyProps.styleProps),
120
150
  generalStyleProps: generalStyleProps
121
151
  };
152
+ const announceFocusedElement = event => {
153
+ if (liveRegionUpdateTimeout.current) {
154
+ window.clearTimeout(liveRegionUpdateTimeout.current);
155
+ }
156
+ const announcement = getFocusedElementAnnouncement(event.target);
157
+ setPreChatFocusAnnouncement("");
158
+ liveRegionUpdateTimeout.current = window.setTimeout(() => {
159
+ setPreChatFocusAnnouncement(announcement);
160
+ }, 0);
161
+ };
122
162
  useEffect(() => {
123
163
  // Set Aria-Label Attribute for Inputs
124
164
  const adaptiveCardElements = document.getElementsByClassName(HtmlAttributeNames.adaptiveCardClassName);
@@ -166,9 +206,31 @@ export const PreChatSurveyPaneStateful = props => {
166
206
  }
167
207
  }
168
208
  }, [state.appStates.isMinimized]);
169
- return /*#__PURE__*/React.createElement(PreChatSurveyPane, {
209
+ useEffect(() => () => {
210
+ if (liveRegionUpdateTimeout.current) {
211
+ window.clearTimeout(liveRegionUpdateTimeout.current);
212
+ }
213
+ }, []);
214
+ return /*#__PURE__*/React.createElement("div", {
215
+ onFocusCapture: announceFocusedElement
216
+ }, /*#__PURE__*/React.createElement("div", {
217
+ role: "status",
218
+ "aria-live": "polite",
219
+ "aria-atomic": "true",
220
+ style: {
221
+ position: "absolute",
222
+ width: "1px",
223
+ height: "1px",
224
+ margin: "-1px",
225
+ padding: 0,
226
+ border: 0,
227
+ clip: "rect(0, 0, 0, 0)",
228
+ overflow: "hidden",
229
+ whiteSpace: "nowrap"
230
+ }
231
+ }, preChatFocusAnnouncement), /*#__PURE__*/React.createElement(PreChatSurveyPane, {
170
232
  controlProps: controlProps,
171
233
  styleProps: styleProps
172
- });
234
+ }));
173
235
  };
174
236
  export default PreChatSurveyPaneStateful;
@@ -25,6 +25,7 @@ import { shouldLoadPersistentChatHistory } from "../livechatwidget/common/liveCh
25
25
  import { useChatContextStore } from "../..";
26
26
  import useFacadeSDKStore from "../../hooks/useFacadeChatSDKStore";
27
27
  import usePersistentChatHistory from "./hooks/usePersistentChatHistory";
28
+ import { patchCitationAnchorsForA11y } from "./common/utils/citationA11y";
28
29
  let uiTimer;
29
30
  const broadcastChannelMessageEvent = "message";
30
31
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -146,6 +147,56 @@ export const WebChatContainerStateful = props => {
146
147
  document.addEventListener("click", clickHandler);
147
148
  return () => document.removeEventListener("click", clickHandler);
148
149
  }, [state]);
150
+
151
+ // Accessibility: Merge citation card's number badge and link text into a single
152
+ // focusable element. WebChat's LinkDefinitionItem renders an <a> containing a
153
+ // badge <div> (the number, e.g. "1") and a text <div> (the title). On iOS
154
+ // VoiceOver and Android TalkBack, those block-level descendants are otherwise
155
+ // announced as two separate focusable links. The patch sets a combined
156
+ // aria-label on the anchor and hides every descendant from the a11y tree so
157
+ // the whole card is announced as one link.
158
+ useEffect(() => {
159
+ const webChatRoot = document.getElementById("ms_lcw_webchat_root");
160
+ if (!webChatRoot) {
161
+ return;
162
+ }
163
+ patchCitationAnchorsForA11y(webChatRoot);
164
+ const observer = new MutationObserver(mutations => {
165
+ for (const m of mutations) {
166
+ if (m.type === "childList") {
167
+ m.addedNodes.forEach(node => {
168
+ if (node.nodeType === Node.ELEMENT_NODE) {
169
+ var _parentElement, _parentElement$closes;
170
+ patchCitationAnchorsForA11y(node);
171
+ // A child added inside an existing anchor (e.g. React
172
+ // mounting OpenInNewWindowIcon after the anchor) means
173
+ // the parent anchor needs to re-walk its descendants.
174
+ 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");
175
+ if (ancestor) {
176
+ patchCitationAnchorsForA11y(ancestor);
177
+ }
178
+ }
179
+ });
180
+ } else if (m.type === "attributes" && m.target.nodeType === Node.ELEMENT_NODE) {
181
+ var _target$closest;
182
+ // React may strip our aria-hidden during a re-render; reapply
183
+ // by re-patching the closest matching anchor ancestor.
184
+ const target = m.target;
185
+ const ancestor = (_target$closest = target.closest) === null || _target$closest === void 0 ? void 0 : _target$closest.call(target, "a.webchat__link-definitions__list-item-box");
186
+ if (ancestor) {
187
+ patchCitationAnchorsForA11y(ancestor);
188
+ }
189
+ }
190
+ }
191
+ });
192
+ observer.observe(webChatRoot, {
193
+ childList: true,
194
+ subtree: true,
195
+ attributes: true,
196
+ attributeFilter: ["aria-hidden", "role", "tabindex", "inert"]
197
+ });
198
+ return () => observer.disconnect();
199
+ }, []);
149
200
  const minimizedStyles = state.appStates.isMinimized ? shouldLoadPersistentHistoryMessages ? {
150
201
  visibility: "hidden",
151
202
  position: "absolute",
@@ -0,0 +1,188 @@
1
+ // Accessibility helper: Merges WebChat's citation card number badge and link
2
+ // text into a single focusable element.
3
+ //
4
+ // WebChat's LinkDefinitionItem renders an <a> containing a badge <div> (number)
5
+ // and a text <div> (title). On iOS VoiceOver, these block-level descendants are
6
+ // announced as two separate focusable links (e.g. "1, link" and
7
+ // "Assessment of eligibility..., link"). Setting a combined aria-label on the
8
+ // anchor and hiding inner descendants from the accessibility tree ensures the
9
+ // whole card is announced as one link.
10
+
11
+ const CITATION_ANCHOR_SELECTOR = "a.webchat__link-definitions__list-item-box";
12
+ const PATCHED_MARKER = "ocwCitationA11yPatched";
13
+ const STYLE_TAG_ID = "ocw-citation-a11y-styles";
14
+
15
+ // Global CSS injected once per document. Neutralizes iOS Safari's per-inner-
16
+ // element tap highlight / selection / focus rectangles that otherwise make
17
+ // the "1" badge and the link text appear as separate selectable areas even
18
+ // though they are inside a single <a>.
19
+ // Strategy:
20
+ // - Make the anchor a block-level positioning parent.
21
+ // - Add a transparent ::after pseudo-element that covers the entire anchor.
22
+ // iOS Safari hit-tests that overlay as a SINGLE region, so the tap
23
+ // highlight / selection rectangle always covers the whole card instead
24
+ // of just the "1" badge or just the link text.
25
+ // - Make every descendant transparent to pointer / focus, so they can never
26
+ // become a standalone tap target.
27
+ const STYLE_RULES = `
28
+ a.webchat__link-definitions__list-item-box {
29
+ position: relative;
30
+ display: block;
31
+ isolation: isolate;
32
+ -webkit-tap-highlight-color: rgba(0, 120, 212, 0.15);
33
+ -webkit-touch-callout: none;
34
+ cursor: pointer;
35
+ }
36
+ a.webchat__link-definitions__list-item-box::after {
37
+ content: "";
38
+ position: absolute;
39
+ inset: 0;
40
+ z-index: 2;
41
+ background: transparent;
42
+ pointer-events: auto;
43
+ -webkit-tap-highlight-color: rgba(0, 120, 212, 0.15);
44
+ }
45
+ a.webchat__link-definitions__list-item-box * {
46
+ pointer-events: none;
47
+ -webkit-tap-highlight-color: transparent;
48
+ -webkit-touch-callout: none;
49
+ outline: none;
50
+ text-decoration: inherit;
51
+ cursor: inherit;
52
+ }
53
+ `;
54
+ const ensureStyleInjected = doc => {
55
+ if (!doc || typeof doc.getElementById !== "function" || doc.getElementById(STYLE_TAG_ID)) {
56
+ return;
57
+ }
58
+ const style = doc.createElement("style");
59
+ style.id = STYLE_TAG_ID;
60
+ style.textContent = STYLE_RULES;
61
+ (doc.head || doc.documentElement).appendChild(style);
62
+ };
63
+ export const patchCitationAnchorsForA11y = root => {
64
+ var _defaultView;
65
+ if (!root || typeof root.querySelectorAll !== "function") {
66
+ return;
67
+ }
68
+
69
+ // Inject global CSS once per document. This covers every citation anchor
70
+ // regardless of whether our per-node patch has run on it yet.
71
+ const rootNode = root;
72
+ const doc = rootNode.ownerDocument ?? ((_defaultView = root.defaultView) === null || _defaultView === void 0 ? void 0 : _defaultView.document) ?? root ?? (typeof document !== "undefined" ? document : null);
73
+ if (doc && typeof doc.createElement === "function") {
74
+ ensureStyleInjected(doc);
75
+ }
76
+
77
+ // Also include the root itself when it matches — calls from MutationObserver
78
+ // pass the anchor element directly, which querySelectorAll wouldn't return.
79
+ const matched = new Set();
80
+ if (typeof root.matches === "function" && root.matches(CITATION_ANCHOR_SELECTOR)) {
81
+ matched.add(root);
82
+ }
83
+ root.querySelectorAll(CITATION_ANCHOR_SELECTOR).forEach(n => {
84
+ matched.add(n);
85
+ });
86
+ matched.forEach(anchor => {
87
+ var _badge$textContent, _textEl$textContent;
88
+ const badge = anchor.querySelector(".webchat__link-definitions__badge");
89
+ const textEl = anchor.querySelector(".webchat__link-definitions__list-item-text");
90
+ const identifier = (badge === null || badge === void 0 ? void 0 : (_badge$textContent = badge.textContent) === null || _badge$textContent === void 0 ? void 0 : _badge$textContent.trim()) ?? "";
91
+ // Prefer the explicit text element. For plain (non-card) citation anchors
92
+ // (no badge), fall back to the title or the anchor's own text. When a
93
+ // badge is present but no text element, avoid using anchor.textContent
94
+ // since it would duplicate the identifier.
95
+ let text = (textEl === null || textEl === void 0 ? void 0 : (_textEl$textContent = textEl.textContent) === null || _textEl$textContent === void 0 ? void 0 : _textEl$textContent.trim()) ?? "";
96
+ if (!text && !badge) {
97
+ var _anchor$getAttribute, _anchor$textContent;
98
+ text = ((_anchor$getAttribute = anchor.getAttribute("title")) === null || _anchor$getAttribute === void 0 ? void 0 : _anchor$getAttribute.trim()) || ((_anchor$textContent = anchor.textContent) === null || _anchor$textContent === void 0 ? void 0 : _anchor$textContent.trim()) || "";
99
+ }
100
+ if (!identifier && !text) {
101
+ return;
102
+ }
103
+ const combinedLabel = identifier && text ? `${identifier}, ${text}` : identifier || text;
104
+
105
+ // Always (re-)apply aria-label if absent. Setting it again with the same
106
+ // value when it already matches is a no-op for the DOM; this keeps the
107
+ // patch idempotent so React re-renders that strip the attribute heal.
108
+ const existingLabel = anchor.getAttribute("aria-label");
109
+ if (!existingLabel) {
110
+ anchor.setAttribute("aria-label", combinedLabel);
111
+ }
112
+
113
+ // Explicitly mark the anchor as a single link so iOS VoiceOver treats
114
+ // the whole card as one element.
115
+ if (!anchor.getAttribute("role")) {
116
+ anchor.setAttribute("role", "link");
117
+ }
118
+
119
+ // Nuclear a11y lockdown of EVERY descendant of the anchor (not just
120
+ // known class names, because WebChat can render additional nodes).
121
+ // iOS VoiceOver is known to split focus on block-level descendants
122
+ // inside an <a>. Applying all of the following guarantees only the
123
+ // outer <a> is a selectable a11y / focus target:
124
+ // - aria-hidden="true" removes element from the a11y tree
125
+ // - role="presentation" strips any implicit semantic role
126
+ // - inert removes element from focus + interaction
127
+ // - tabindex="-1" defensive (covers browsers without inert)
128
+ // - title removed iOS VoiceOver in WKWebView still surfaces
129
+ // elements with a `title` attribute as a
130
+ // separately swipeable item even when
131
+ // aria-hidden=true. WebChat's ItemBody sets
132
+ // title={text} on the inner link-text div,
133
+ // which is the root cause of the "two
134
+ // separate links" announcement on iOS.
135
+ // We move the title to a data-* attribute
136
+ // so any tooltip-like tooling can still
137
+ // read it, but iOS VO ignores it.
138
+ anchor.querySelectorAll("*").forEach(el => {
139
+ const inner = el;
140
+ if (inner.getAttribute("aria-hidden") !== "true") {
141
+ inner.setAttribute("aria-hidden", "true");
142
+ }
143
+ if (inner.getAttribute("role") !== "presentation") {
144
+ inner.setAttribute("role", "presentation");
145
+ }
146
+ if (inner.getAttribute("tabindex") !== "-1") {
147
+ inner.setAttribute("tabindex", "-1");
148
+ }
149
+ if (!inner.hasAttribute("inert")) {
150
+ inner.setAttribute("inert", "");
151
+ }
152
+ const innerTitle = inner.getAttribute("title");
153
+ if (innerTitle !== null) {
154
+ inner.removeAttribute("title");
155
+ if (!inner.dataset.ocwOriginalTitle) {
156
+ inner.dataset.ocwOriginalTitle = innerTitle;
157
+ }
158
+ }
159
+ if (inner.style.pointerEvents !== "none") {
160
+ inner.style.pointerEvents = "none";
161
+ }
162
+ });
163
+
164
+ // DOM-level full-bleed overlay. Inline styles are used so this works
165
+ // even when the widget renders inside a shadow DOM (where an
166
+ // injected <style> tag in document.head cannot reach). The overlay
167
+ // becomes the ONLY visible hit-test target for iOS Safari, so tapping
168
+ // anywhere on the card highlights the entire card instead of just the
169
+ // "1" badge or just the link text.
170
+ const anchorStyle = anchor.style;
171
+ if (!anchorStyle.position) {
172
+ anchorStyle.position = "relative";
173
+ }
174
+ if (!anchorStyle.display || anchorStyle.display === "inline") {
175
+ anchorStyle.display = "block";
176
+ }
177
+ const ownerDoc = anchor.ownerDocument;
178
+ const existingOverlay = anchor.querySelector(":scope > [data-ocw-citation-overlay='true']");
179
+ if (!existingOverlay && ownerDoc && typeof ownerDoc.createElement === "function") {
180
+ const overlay = ownerDoc.createElement("span");
181
+ overlay.setAttribute("aria-hidden", "true");
182
+ overlay.setAttribute("data-ocw-citation-overlay", "true");
183
+ overlay.style.cssText = ["position:absolute", "top:0", "left:0", "right:0", "bottom:0", "z-index:2", "background:transparent", "pointer-events:auto", "-webkit-tap-highlight-color:rgba(0,120,212,0.15)", "-webkit-touch-callout:none", "-webkit-user-select:none", "user-select:none", "cursor:pointer"].join(";");
184
+ anchor.appendChild(overlay);
185
+ }
186
+ anchor.dataset[PATCHED_MARKER] = "true";
187
+ });
188
+ };
@@ -25,6 +25,21 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
25
25
  const container = containerRef.current;
26
26
  if (!container) return;
27
27
 
28
+ // action-button-toggle / login-button-toggle: submit/login action buttons can be
29
+ // rendered with toggle-only ARIA that causes screen readers to announce them as
30
+ // switches instead of plain push buttons. Preserve Action.ToggleVisibility buttons,
31
+ // which own aria-controls and legitimately expose a pressed state.
32
+ const actionButtons = container.querySelectorAll(".ac-actionSet button.ac-pushButton:not([aria-controls])");
33
+ actionButtons.forEach(btn => {
34
+ if (btn.hasAttribute("aria-pressed")) {
35
+ btn.removeAttribute("aria-pressed");
36
+ }
37
+ const role = btn.getAttribute("role");
38
+ if (role === "switch") {
39
+ btn.removeAttribute("role");
40
+ }
41
+ });
42
+
28
43
  // Find all radio inputs inside the adaptive card
29
44
  const radios = container.querySelectorAll("input[type='radio']");
30
45
  if (radios.length === 0) return;
@@ -10,6 +10,32 @@ let currentAgentName = defaultWebChatStyles.botAvatarInitials;
10
10
 
11
11
  // Optional external updater (React context dispatch wrapper) set at runtime
12
12
  let externalInitialsUpdater;
13
+ const hasTransferTag = activity => {
14
+ var _activity$channelData;
15
+ const tags = [...(Array.isArray(activity === null || activity === void 0 ? void 0 : (_activity$channelData = activity.channelData) === null || _activity$channelData === void 0 ? void 0 : _activity$channelData.tags) ? activity.channelData.tags : []), ...(Array.isArray(activity === null || activity === void 0 ? void 0 : activity.tags) ? activity.tags : [])];
16
+ return tags.some(tag => typeof tag === "string" && tag.toLowerCase().includes("transfer"));
17
+ };
18
+ const isTransferSystemMessage = activity => {
19
+ const text = ((activity === null || activity === void 0 ? void 0 : activity.text) || "").toString();
20
+ return hasTransferTag(activity) || /\btransfer(?:red|ring)?\b/i.test(text);
21
+ };
22
+ const resetCachedAgentName = dispatch => {
23
+ if (currentAgentName !== defaultWebChatStyles.botAvatarInitials || currentAgentInitials !== defaultWebChatStyles.botAvatarInitials) {
24
+ var _externalInitialsUpda;
25
+ currentAgentName = defaultWebChatStyles.botAvatarInitials;
26
+ currentAgentInitials = defaultWebChatStyles.botAvatarInitials;
27
+ (_externalInitialsUpda = externalInitialsUpdater) === null || _externalInitialsUpda === void 0 ? void 0 : _externalInitialsUpda(currentAgentInitials || "");
28
+ BroadcastService.postMessage({
29
+ eventName: "BotAvatarInitialsUpdated",
30
+ payload: {
31
+ initials: currentAgentInitials
32
+ }
33
+ });
34
+ dispatch({
35
+ type: "__BOT_INITIALS_UPDATED__"
36
+ });
37
+ }
38
+ };
13
39
  export const localizedStringsBotInitialsMiddleware = onInitialsChange => _ref => {
14
40
  let {
15
41
  dispatch
@@ -23,19 +49,31 @@ export const localizedStringsBotInitialsMiddleware = onInitialsChange => _ref =>
23
49
  var _action$payload, _activity$from;
24
50
  const activity = (_action$payload = action.payload) === null || _action$payload === void 0 ? void 0 : _action$payload.activity;
25
51
  if (activity !== null && activity !== void 0 && (_activity$from = activity.from) !== null && _activity$from !== void 0 && _activity$from.name && activity.from.role !== Constants.userMessageTag && activity.from.name !== Constants.userMessageTag) {
26
- var _activity$channelData, _activity$channelData2, _activity$tags;
52
+ var _activity$channelData2, _activity$channelData3, _activity$tags;
27
53
  const agentName = activity.from.name.trim();
28
- const isSystemMessage = agentName === "__agent__" || agentName.startsWith("__") || ((_activity$channelData = activity.channelData) === null || _activity$channelData === void 0 ? void 0 : (_activity$channelData2 = _activity$channelData.tags) === null || _activity$channelData2 === void 0 ? void 0 : _activity$channelData2.includes(Constants.systemMessageTag)) || ((_activity$tags = activity.tags) === null || _activity$tags === void 0 ? void 0 : _activity$tags.includes(Constants.systemMessageTag));
29
- if (!isSystemMessage && agentName) {
54
+ const isSystemMessage = agentName === "__agent__" || agentName.startsWith("__") || ((_activity$channelData2 = activity.channelData) === null || _activity$channelData2 === void 0 ? void 0 : (_activity$channelData3 = _activity$channelData2.tags) === null || _activity$channelData3 === void 0 ? void 0 : _activity$channelData3.includes(Constants.systemMessageTag)) || ((_activity$tags = activity.tags) === null || _activity$tags === void 0 ? void 0 : _activity$tags.includes(Constants.systemMessageTag));
55
+ if (isSystemMessage) {
56
+ // transfer-stale-bot-name: when a transfer system message indicates the
57
+ // conversation has been routed to a different agent, the
58
+ // previously-cached agent name must NOT linger. Otherwise the
59
+ // next non-system activity (e.g. typing indicator, welcome
60
+ // message, or the new agent's first turn before their name
61
+ // is observed) is announced to screen readers as the OLD
62
+ // agent. Reset to defaults so the next observed name takes
63
+ // effect cleanly.
64
+ if (isTransferSystemMessage(activity)) {
65
+ resetCachedAgentName(dispatch);
66
+ }
67
+ } else if (agentName) {
30
68
  const newInitials = getIconText(agentName) || currentAgentInitials;
31
69
  const hasInitialsChanged = newInitials !== currentAgentInitials;
32
70
  const hasNameChanged = agentName !== currentAgentName;
33
71
  currentAgentName = agentName;
34
72
  if (hasInitialsChanged) {
35
- var _externalInitialsUpda;
73
+ var _externalInitialsUpda2;
36
74
  currentAgentInitials = newInitials;
37
75
  // Notify external React context if provided
38
- (_externalInitialsUpda = externalInitialsUpdater) === null || _externalInitialsUpda === void 0 ? void 0 : _externalInitialsUpda(currentAgentInitials || "");
76
+ (_externalInitialsUpda2 = externalInitialsUpdater) === null || _externalInitialsUpda2 === void 0 ? void 0 : _externalInitialsUpda2(currentAgentInitials || "");
39
77
  // Broadcast (optional) for multi-tab sync without forcing consumers
40
78
  BroadcastService.postMessage({
41
79
  eventName: "BotAvatarInitialsUpdated",
@@ -0,0 +1 @@
1
+ export declare const patchCitationAnchorsForA11y: (root: ParentNode) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/omnichannel-chat-widget",
3
- "version": "1.8.4-main.9edfa7a",
3
+ "version": "1.8.4-main.a37560d",
4
4
  "description": "Microsoft Omnichannel Chat Widget",
5
5
  "main": "lib/cjs/index.js",
6
6
  "types": "lib/types/index.d.ts",
@@ -84,7 +84,7 @@
84
84
  "react-test-renderer": "^18.3.1",
85
85
  "rimraf": "^6.0.1",
86
86
  "storybook-addon-playwright": "^4.9.2",
87
- "swiper": "^9.0.5",
87
+ "swiper": "^12.1.2",
88
88
  "terser-webpack-plugin": "^4.2.3",
89
89
  "thread-loader": "^2.1.3",
90
90
  "ts-loader": "^9.2.6",
@@ -172,7 +172,7 @@
172
172
  "**/lodash": "4.17.23",
173
173
  "**/@babel/runtime-corejs3": "^7.29.0",
174
174
  "**/brace-expansion": "2.0.3",
175
- "**/swiper": "9.4.1"
175
+ "**/swiper": "12.1.2"
176
176
  },
177
177
  "jest": {
178
178
  "verbose": true,