@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.
- package/lib/cjs/components/livechatwidget/common/createMarkdown.js +96 -5
- package/lib/cjs/components/postchatloadingpanestateful/PostChatLoadingPaneStateful.js +15 -3
- package/lib/cjs/components/postchatsurveypanestateful/PostChatSurveyPaneStateful.js +1 -0
- package/lib/cjs/components/prechatsurveypanestateful/PreChatSurveyPaneStateful.js +64 -2
- package/lib/cjs/components/webchatcontainerstateful/WebChatContainerStateful.js +51 -0
- package/lib/cjs/components/webchatcontainerstateful/common/utils/citationA11y.js +195 -0
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +15 -0
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/localizedStringsBotInitialsMiddleware.js +43 -5
- package/lib/esm/components/livechatwidget/common/createMarkdown.js +96 -5
- package/lib/esm/components/postchatloadingpanestateful/PostChatLoadingPaneStateful.js +16 -4
- package/lib/esm/components/postchatsurveypanestateful/PostChatSurveyPaneStateful.js +1 -0
- package/lib/esm/components/prechatsurveypanestateful/PreChatSurveyPaneStateful.js +65 -3
- package/lib/esm/components/webchatcontainerstateful/WebChatContainerStateful.js +51 -0
- package/lib/esm/components/webchatcontainerstateful/common/utils/citationA11y.js +188 -0
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +15 -0
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/localizedStringsBotInitialsMiddleware.js +43 -5
- package/lib/types/components/webchatcontainerstateful/common/utils/citationA11y.d.ts +1 -0
- 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
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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$
|
|
58
|
+
var _activity$channelData2, _activity$channelData3, _activity$tags;
|
|
33
59
|
const agentName = activity.from.name.trim();
|
|
34
|
-
const isSystemMessage = agentName === "__agent__" || agentName.startsWith("__") || ((_activity$
|
|
35
|
-
if (
|
|
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
|
|
79
|
+
var _externalInitialsUpda2;
|
|
42
80
|
currentAgentInitials = newInitials;
|
|
43
81
|
// Notify external React context if provided
|
|
44
|
-
(
|
|
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
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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$
|
|
52
|
+
var _activity$channelData2, _activity$channelData3, _activity$tags;
|
|
27
53
|
const agentName = activity.from.name.trim();
|
|
28
|
-
const isSystemMessage = agentName === "__agent__" || agentName.startsWith("__") || ((_activity$
|
|
29
|
-
if (
|
|
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
|
|
73
|
+
var _externalInitialsUpda2;
|
|
36
74
|
currentAgentInitials = newInitials;
|
|
37
75
|
// Notify external React context if provided
|
|
38
|
-
(
|
|
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.
|
|
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": "^
|
|
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": "
|
|
175
|
+
"**/swiper": "12.1.2"
|
|
176
176
|
},
|
|
177
177
|
"jest": {
|
|
178
178
|
"verbose": true,
|