@microsoft/omnichannel-chat-widget 1.8.4-main.f6a1732 → 1.8.4-main.f8ae4f8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cjs/common/Constants.js +0 -1
- package/lib/cjs/common/telemetry/TelemetryManager.js +6 -1
- package/lib/cjs/common/telemetry/sanitizeSasInPayload.js +82 -0
- package/lib/cjs/components/citationpanestateful/CitationPaneStateful.js +7 -1
- package/lib/cjs/components/confirmationpanestateful/ConfirmationPaneStateful.js +11 -1
- package/lib/cjs/components/emailtranscriptpanestateful/EmailTranscriptPaneStateful.js +5 -1
- package/lib/cjs/components/livechatwidget/common/createMarkdown.js +4 -2
- package/lib/cjs/components/livechatwidget/common/initWebChatComposer.js +16 -12
- 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 +68 -2
- package/lib/cjs/components/prechatsurveypanestateful/common/defaultStyles/defaultGeneralPreChatSurveyPaneStyleProps.js +1 -2
- package/lib/cjs/components/webchatcontainerstateful/WebChatContainerStateful.js +47 -17
- package/lib/cjs/components/webchatcontainerstateful/common/DesignerChatAdapter.js +10 -0
- package/lib/cjs/components/webchatcontainerstateful/common/defaultProps/defaultWebChatContainerStatefulProps.js +2 -1
- package/lib/cjs/components/webchatcontainerstateful/common/utils/chatAdapterUtils.js +3 -1
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +50 -0
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +4 -8
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/localizedStringsBotInitialsMiddleware.js +43 -5
- package/lib/cjs/contexts/common/LiveChatWidgetContextInitialState.js +11 -0
- package/lib/esm/common/Constants.js +0 -1
- package/lib/esm/common/telemetry/TelemetryManager.js +6 -1
- package/lib/esm/common/telemetry/sanitizeSasInPayload.js +75 -0
- package/lib/esm/components/citationpanestateful/CitationPaneStateful.js +7 -1
- package/lib/esm/components/confirmationpanestateful/ConfirmationPaneStateful.js +11 -1
- package/lib/esm/components/emailtranscriptpanestateful/EmailTranscriptPaneStateful.js +5 -1
- package/lib/esm/components/livechatwidget/common/createMarkdown.js +4 -2
- package/lib/esm/components/livechatwidget/common/initWebChatComposer.js +16 -12
- 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 +69 -3
- package/lib/esm/components/prechatsurveypanestateful/common/defaultStyles/defaultGeneralPreChatSurveyPaneStyleProps.js +1 -2
- package/lib/esm/components/webchatcontainerstateful/WebChatContainerStateful.js +48 -18
- package/lib/esm/components/webchatcontainerstateful/common/DesignerChatAdapter.js +10 -0
- package/lib/esm/components/webchatcontainerstateful/common/defaultProps/defaultWebChatContainerStatefulProps.js +2 -1
- package/lib/esm/components/webchatcontainerstateful/common/utils/chatAdapterUtils.js +3 -1
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +50 -0
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware.js +5 -9
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/localizedStringsBotInitialsMiddleware.js +43 -5
- package/lib/esm/contexts/common/LiveChatWidgetContextInitialState.js +11 -0
- package/lib/types/common/Constants.d.ts +0 -1
- package/lib/types/common/telemetry/sanitizeSasInPayload.d.ts +24 -0
- package/lib/types/components/webchatcontainerstateful/common/utils/chatAdapterUtils.d.ts +1 -1
- package/lib/types/components/webchatcontainerstateful/interfaces/IWebChatContainerStatefulProps.d.ts +7 -0
- package/package.json +5 -5
|
@@ -158,7 +158,6 @@ let HtmlIdNames = /*#__PURE__*/_createClass(function HtmlIdNames() {
|
|
|
158
158
|
});
|
|
159
159
|
exports.HtmlIdNames = HtmlIdNames;
|
|
160
160
|
_defineProperty(HtmlIdNames, "MSLiveChatWidget", "MSLiveChatWidget");
|
|
161
|
-
_defineProperty(HtmlIdNames, "fileSentAnnouncementRegionId", "ms_lcw_file_sent_announcement");
|
|
162
161
|
let HtmlClassNames = /*#__PURE__*/_createClass(function HtmlClassNames() {
|
|
163
162
|
_classCallCheck(this, HtmlClassNames);
|
|
164
163
|
});
|
|
@@ -11,6 +11,7 @@ var _appInsightsLogger = require("./loggers/appInsightsLogger");
|
|
|
11
11
|
var _ariaTelemetryLogger = require("./loggers/ariaTelemetryLogger");
|
|
12
12
|
var _consoleLogger = require("./loggers/consoleLogger");
|
|
13
13
|
var _defaultAriaConfig = require("./defaultConfigs/defaultAriaConfig");
|
|
14
|
+
var _sanitizeSasInPayload = require("./sanitizeSasInPayload");
|
|
14
15
|
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
|
|
15
16
|
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
|
|
16
17
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
@@ -81,11 +82,15 @@ const RegisterLoggers = () => {
|
|
|
81
82
|
};
|
|
82
83
|
};
|
|
83
84
|
const logTelemetry = telemetryEvent => {
|
|
85
|
+
// Redact Azure SAS credentials in any URL embedded in the payload (e.g.
|
|
86
|
+
// customer-supplied blob asset URLs in widget customization props) before
|
|
87
|
+
// it fans out to console, Aria, AppInsights, or any custom logger.
|
|
88
|
+
const sanitizedPayload = (0, _sanitizeSasInPayload.sanitizeSasInPayload)(telemetryEvent === null || telemetryEvent === void 0 ? void 0 : telemetryEvent.payload);
|
|
84
89
|
loggers.map(logger => {
|
|
85
90
|
var _payload, _telemetryInput$paylo;
|
|
86
91
|
const logLevel = telemetryEvent.logLevel ?? _TelemetryConstants.LogLevel.INFO;
|
|
87
92
|
const scenarioType = ((_payload = telemetryEvent.payload) === null || _payload === void 0 ? void 0 : _payload.scenarioType) ?? _TelemetryConstants.ScenarioType.UNDEFINED;
|
|
88
|
-
const telemetryInput = parseInput(
|
|
93
|
+
const telemetryInput = parseInput(sanitizedPayload, scenarioType);
|
|
89
94
|
telemetryInput.telemetryInfo = {
|
|
90
95
|
telemetryInfo: _TelemetryHelper.TelemetryHelper.buildTelemetryEvent(logLevel, telemetryInput)
|
|
91
96
|
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.sanitizeSasInPayload = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Redacts the values of Azure Shared Access Signature (SAS) query parameters in any
|
|
9
|
+
* URL embedded in a telemetry payload, so customer-supplied SAS URLs (e.g. blob asset
|
|
10
|
+
* URLs in widget customization props) cannot leak credentials through Aria/AppInsights.
|
|
11
|
+
*
|
|
12
|
+
* Trigger: any URL whose query string contains `sig=`. When detected, every well-known
|
|
13
|
+
* SAS parameter value in that query is replaced with `[REDACTED]`. Other URLs and other
|
|
14
|
+
* substrings of the payload pass through unchanged.
|
|
15
|
+
*
|
|
16
|
+
* Coverage of SAS parameter names:
|
|
17
|
+
* - Service/Blob SAS: sig, sv, se, st, sp, spr, sr, sip
|
|
18
|
+
* - Account SAS: ss, srt
|
|
19
|
+
* - User-delegation key SAS: skoid, sktid, skt, ske, sks, skv, saoid, suoid, sce, sdd
|
|
20
|
+
* - Response header overrides: rscc, rscd, rsce, rscl, rsct
|
|
21
|
+
*
|
|
22
|
+
* Reference: https://learn.microsoft.com/en-us/rest/api/storageservices/create-service-sas
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const SAS_PARAM_NAMES = new Set(["sig", "sv", "se", "st", "sp", "spr", "sr", "sip", "ss", "srt", "skoid", "sktid", "skt", "ske", "sks", "skv", "saoid", "suoid", "sce", "sdd", "rscc", "rscd", "rsce", "rscl", "rsct"]);
|
|
26
|
+
const REDACTED_VALUE = "[REDACTED]";
|
|
27
|
+
|
|
28
|
+
// Matches any http(s) URL substring up to whitespace / closing quote / angle-bracket / paren.
|
|
29
|
+
const URL_REGEX = /\bhttps?:\/\/[^\s"'<>)]+/gi;
|
|
30
|
+
const redactQueryIfSas = url => {
|
|
31
|
+
const queryIdx = url.indexOf("?");
|
|
32
|
+
if (queryIdx < 0) {
|
|
33
|
+
return url;
|
|
34
|
+
}
|
|
35
|
+
// Separate the fragment (#...) before processing the query — fragments come
|
|
36
|
+
// after the query string in URL syntax and must not be folded into the last
|
|
37
|
+
// query parameter's value when we split on `&`.
|
|
38
|
+
const afterQuestionMark = url.slice(queryIdx + 1);
|
|
39
|
+
const fragIdx = afterQuestionMark.indexOf("#");
|
|
40
|
+
const query = fragIdx < 0 ? afterQuestionMark : afterQuestionMark.slice(0, fragIdx);
|
|
41
|
+
const fragment = fragIdx < 0 ? "" : afterQuestionMark.slice(fragIdx); // includes the leading '#'
|
|
42
|
+
if (!/(^|&)sig=/i.test(query)) {
|
|
43
|
+
// No SAS signature present — leave the URL alone so legitimate non-SAS query
|
|
44
|
+
// strings (e.g. ?utm_source=...) are not mangled.
|
|
45
|
+
return url;
|
|
46
|
+
}
|
|
47
|
+
const base = url.slice(0, queryIdx);
|
|
48
|
+
const sanitizedQuery = query.split("&").map(pair => {
|
|
49
|
+
const eqIdx = pair.indexOf("=");
|
|
50
|
+
if (eqIdx < 0) {
|
|
51
|
+
return pair;
|
|
52
|
+
}
|
|
53
|
+
const name = pair.slice(0, eqIdx).toLowerCase();
|
|
54
|
+
return SAS_PARAM_NAMES.has(name) ? `${pair.slice(0, eqIdx)}=${REDACTED_VALUE}` : pair;
|
|
55
|
+
}).join("&");
|
|
56
|
+
return `${base}?${sanitizedQuery}${fragment}`;
|
|
57
|
+
};
|
|
58
|
+
const sanitizeString = s => s.replace(URL_REGEX, redactQueryIfSas);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Deep-walk a value and redact SAS credentials in any string it contains.
|
|
62
|
+
* Returns a new object/array (originals are not mutated); non-string leaves are
|
|
63
|
+
* passed through. Tree-shaped, JSON-like values only — not designed for class
|
|
64
|
+
* instances, prototype-bound objects, or cyclic graphs.
|
|
65
|
+
*/
|
|
66
|
+
const sanitizeSasInPayload = value => {
|
|
67
|
+
if (typeof value === "string") {
|
|
68
|
+
return sanitizeString(value);
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
return value.map(sanitizeSasInPayload);
|
|
72
|
+
}
|
|
73
|
+
if (value !== null && typeof value === "object") {
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const [k, v] of Object.entries(value)) {
|
|
76
|
+
out[k] = sanitizeSasInPayload(v);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
};
|
|
82
|
+
exports.sanitizeSasInPayload = sanitizeSasInPayload;
|
|
@@ -62,7 +62,13 @@ const CitationPaneStateful = props => {
|
|
|
62
62
|
Event: _TelemetryConstants.TelemetryEvent.UXCitationPaneCompleted,
|
|
63
63
|
ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
|
|
64
64
|
});
|
|
65
|
-
return
|
|
65
|
+
return () => {
|
|
66
|
+
// internal tracking: restore parent tab indices on unmount so focus
|
|
67
|
+
// can escape the widget even if the pane is dismissed by an
|
|
68
|
+
// external state change rather than a user button click.
|
|
69
|
+
(0, _utils.setTabIndices)(elements, initialTabIndexMap, true);
|
|
70
|
+
cleanup();
|
|
71
|
+
};
|
|
66
72
|
}, []);
|
|
67
73
|
|
|
68
74
|
// Retry focus once pane is actually visible (isReady) in case initial attempt occurred while wrapper was visibility:hidden
|
|
@@ -100,7 +100,17 @@ const ConfirmationPaneStateful = props => {
|
|
|
100
100
|
Event: _TelemetryConstants.TelemetryEvent.UXConfirmationPaneCompleted,
|
|
101
101
|
ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
|
|
102
102
|
});
|
|
103
|
-
return
|
|
103
|
+
return () => {
|
|
104
|
+
// internal tracking: when the confirmation pane unmounts (page reload,
|
|
105
|
+
// parent state change, or any path that bypasses onConfirm /
|
|
106
|
+
// onCancel) the tab indices that were forced to -1 on sibling
|
|
107
|
+
// focusable elements must be restored, otherwise Tab order
|
|
108
|
+
// remains broken and focus can be trapped inside the widget.
|
|
109
|
+
// setTabIndices is a no-op when initialTabIndexMap is already
|
|
110
|
+
// empty (onConfirm / onCancel already restored).
|
|
111
|
+
(0, _utils.setTabIndices)(elements, initialTabIndexMap, true);
|
|
112
|
+
cleanup();
|
|
113
|
+
};
|
|
104
114
|
}, []);
|
|
105
115
|
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_DimLayer.DimLayer, {
|
|
106
116
|
brightness: (controlProps === null || controlProps === void 0 ? void 0 : controlProps.brightnessValueOnDim) ?? "0.2"
|
|
@@ -136,7 +136,11 @@ const EmailTranscriptPaneStateful = props => {
|
|
|
136
136
|
Event: _TelemetryConstants.TelemetryEvent.UXEmailTranscriptPaneCompleted,
|
|
137
137
|
ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
|
|
138
138
|
});
|
|
139
|
-
return
|
|
139
|
+
return () => {
|
|
140
|
+
// internal tracking: restore parent tab indices on unmount.
|
|
141
|
+
(0, _utils.setTabIndices)(elements, initialTabIndexMap, true);
|
|
142
|
+
cleanup();
|
|
143
|
+
};
|
|
140
144
|
}, [initialEmail]);
|
|
141
145
|
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_DimLayer.DimLayer, {
|
|
142
146
|
brightness: (controlProps === null || controlProps === void 0 ? void 0 : controlProps.brightnessValueOnDim) ?? "0.2"
|
|
@@ -173,14 +173,16 @@ const createMarkdown = (disableMarkdownMessageFormatting, disableNewLineMarkdown
|
|
|
173
173
|
|
|
174
174
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
175
175
|
md.render = function (text, env) {
|
|
176
|
+
const safeEnv = env ?? {};
|
|
176
177
|
const processedText = preprocessText(text);
|
|
177
|
-
return md.renderer.render(md.parse(processedText,
|
|
178
|
+
return md.renderer.render(md.parse(processedText, safeEnv), md.options, safeEnv);
|
|
178
179
|
};
|
|
179
180
|
|
|
180
181
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
181
182
|
md.renderInline = function (text, env) {
|
|
183
|
+
const safeEnv = env ?? {};
|
|
182
184
|
const processedText = preprocessText(text);
|
|
183
|
-
return md.renderer.render(md.parseInline(processedText,
|
|
185
|
+
return md.renderer.render(md.parseInline(processedText, safeEnv), md.options, safeEnv);
|
|
184
186
|
};
|
|
185
187
|
});
|
|
186
188
|
|
|
@@ -304,24 +304,28 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
|
|
|
304
304
|
const conversationId = (state === null || state === void 0 ? void 0 : (_state$domainStates2 = state.domainStates) === null || _state$domainStates2 === void 0 ? void 0 : (_state$domainStates2$ = _state$domainStates2.chatToken) === null || _state$domainStates2$ === void 0 ? void 0 : _state$domainStates2$.chatId) || "unknown";
|
|
305
305
|
_TelemetryHelper.TelemetryHelper.logActionEvent(_TelemetryConstants.LogLevel.INFO, {
|
|
306
306
|
Event: _TelemetryConstants.TelemetryEvent.HTMLSanitized,
|
|
307
|
-
Description:
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
307
|
+
Description: JSON.stringify({
|
|
308
|
+
message: "HTML content would be sanitized by stricter allowlist (monitor-only)",
|
|
309
|
+
removedTags: uniqueTags,
|
|
310
|
+
removedAttributes: uniqueAttrs,
|
|
311
|
+
phase: "Monitor",
|
|
312
|
+
organizationId: orgId,
|
|
313
|
+
conversationId: conversationId
|
|
314
|
+
}),
|
|
315
|
+
ElapsedTimeInMilliseconds: executionTimeMs
|
|
316
316
|
});
|
|
317
317
|
|
|
318
318
|
// Log to console in development for debugging
|
|
319
319
|
if (process.env.NODE_ENV === "development") {
|
|
320
|
-
console.warn("[Monitor] Stricter HTML sanitization
|
|
320
|
+
console.warn("[Monitor] Stricter HTML sanitization telemetry:", {
|
|
321
|
+
description: JSON.parse(JSON.stringify({
|
|
322
|
+
message: "HTML content would be sanitized by stricter allowlist (monitor-only)",
|
|
323
|
+
removedTags: uniqueTags,
|
|
324
|
+
removedAttributes: uniqueAttrs,
|
|
325
|
+
phase: "Monitor"
|
|
326
|
+
})),
|
|
321
327
|
orgId,
|
|
322
328
|
conversationId,
|
|
323
|
-
removedTags: uniqueTags,
|
|
324
|
-
removedAttributes: uniqueAttrs,
|
|
325
329
|
executionTimeMs
|
|
326
330
|
});
|
|
327
331
|
}
|
|
@@ -16,13 +16,22 @@ function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "functio
|
|
|
16
16
|
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
|
17
17
|
let uiTimer;
|
|
18
18
|
const PostChatLoadingPaneStateful = props => {
|
|
19
|
-
var _props$styleProps;
|
|
19
|
+
var _props$controlProps, _props$styleProps;
|
|
20
|
+
const defaultSubtitleText = "Please take a moment to give us feedback about your chat experience. We are loading the survey for you now.";
|
|
21
|
+
const subtitleText = ((_props$controlProps = props.controlProps) === null || _props$controlProps === void 0 ? void 0 : _props$controlProps.subtitleText) ?? defaultSubtitleText;
|
|
22
|
+
const [announcedSubtitleText, setAnnouncedSubtitleText] = (0, _react.useState)("");
|
|
20
23
|
(0, _react.useEffect)(() => {
|
|
21
24
|
uiTimer = (0, _utils.createTimer)();
|
|
22
25
|
_TelemetryHelper.TelemetryHelper.logLoadingEvent(_TelemetryConstants.LogLevel.INFO, {
|
|
23
26
|
Event: _TelemetryConstants.TelemetryEvent.UXPostChatLoadingPaneStart
|
|
24
27
|
});
|
|
25
28
|
}, []);
|
|
29
|
+
(0, _react.useEffect)(() => {
|
|
30
|
+
const timeout = window.setTimeout(() => {
|
|
31
|
+
setAnnouncedSubtitleText(subtitleText);
|
|
32
|
+
}, 0);
|
|
33
|
+
return () => window.clearTimeout(timeout);
|
|
34
|
+
}, [subtitleText]);
|
|
26
35
|
const [state] = (0, _useChatContextStore.default)();
|
|
27
36
|
const generalStyleProps = Object.assign({}, _defaultgeneralPostChatLoadingPaneStyleProps.defaultGeneralPostChatLoadingPaneStyleProps, (_props$styleProps = props.styleProps) === null || _props$styleProps === void 0 ? void 0 : _props$styleProps.generalStyleProps);
|
|
28
37
|
const styleProps = {
|
|
@@ -36,8 +45,11 @@ const PostChatLoadingPaneStateful = props => {
|
|
|
36
45
|
hideTitle: true,
|
|
37
46
|
hideSpinner: true,
|
|
38
47
|
hideSpinnerText: true,
|
|
39
|
-
|
|
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,36 @@ 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
|
+
style: {
|
|
226
|
+
width: "100%",
|
|
227
|
+
height: "100%"
|
|
228
|
+
}
|
|
229
|
+
}, /*#__PURE__*/_react.default.createElement("div", {
|
|
230
|
+
role: "status",
|
|
231
|
+
"aria-live": "polite",
|
|
232
|
+
"aria-atomic": "true",
|
|
233
|
+
style: {
|
|
234
|
+
position: "absolute",
|
|
235
|
+
width: "1px",
|
|
236
|
+
height: "1px",
|
|
237
|
+
margin: "-1px",
|
|
238
|
+
padding: 0,
|
|
239
|
+
border: 0,
|
|
240
|
+
clip: "rect(0, 0, 0, 0)",
|
|
241
|
+
overflow: "hidden",
|
|
242
|
+
whiteSpace: "nowrap"
|
|
243
|
+
}
|
|
244
|
+
}, preChatFocusAnnouncement), /*#__PURE__*/_react.default.createElement(_omnichannelChatComponents.PreChatSurveyPane, {
|
|
179
245
|
controlProps: controlProps,
|
|
180
246
|
styleProps: styleProps
|
|
181
|
-
});
|
|
247
|
+
}));
|
|
182
248
|
};
|
|
183
249
|
exports.PreChatSurveyPaneStateful = PreChatSurveyPaneStateful;
|
|
184
250
|
var _default = PreChatSurveyPaneStateful;
|
|
@@ -12,7 +12,6 @@ const defaultGeneralPreChatSurveyPaneStyleProps = {
|
|
|
12
12
|
borderColor: "#F1F1F1",
|
|
13
13
|
overflowY: "auto",
|
|
14
14
|
height: "inherit",
|
|
15
|
-
width: "inherit"
|
|
16
|
-
overscrollBehavior: "none"
|
|
15
|
+
width: "inherit"
|
|
17
16
|
};
|
|
18
17
|
exports.defaultGeneralPreChatSurveyPaneStyleProps = defaultGeneralPreChatSurveyPaneStyleProps;
|
|
@@ -232,6 +232,48 @@ const WebChatContainerStateful = props => {
|
|
|
232
232
|
chatHistoryElement.setAttribute(_Constants.HtmlAttributeNames.ariaLabel, webChatContainerProps.webChatHistoryMobileAccessibilityLabel);
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
|
+
|
|
236
|
+
// internal tracking: WebChats BasicToaster renders a `role="log"` container
|
|
237
|
+
// with no accessible name. When the toaster is empty (typical idle
|
|
238
|
+
// state) screen readers traversing the page announce it as "blank".
|
|
239
|
+
// Inject an aria-label so the region has a meaningful name; the
|
|
240
|
+
// consumer can override via the new
|
|
241
|
+
// `webChatNotificationRegionAccessibilityLabel` prop. The toaster
|
|
242
|
+
// may not be rendered yet when this effect first runs, so observe
|
|
243
|
+
// for it via a MutationObserver scoped to the widget root (or, if
|
|
244
|
+
// not yet mounted, the widget container's parent) so we never fire
|
|
245
|
+
// on host-page mutations outside the widget subtree.
|
|
246
|
+
const toasterLabel = (webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.webChatNotificationRegionAccessibilityLabel) ?? _defaultWebChatContainerStatefulProps.defaultWebChatContainerStatefulProps.webChatNotificationRegionAccessibilityLabel ?? "Chat notifications";
|
|
247
|
+
const labelToaster = root => {
|
|
248
|
+
const toaster = root.querySelector(".webchat__toaster[role='log']");
|
|
249
|
+
if (toaster && !toaster.getAttribute(_Constants.HtmlAttributeNames.ariaLabel)) {
|
|
250
|
+
toaster.setAttribute(_Constants.HtmlAttributeNames.ariaLabel, toasterLabel);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
return false;
|
|
254
|
+
};
|
|
255
|
+
// Prefer the widget root; if it is not yet in the DOM, fall back to
|
|
256
|
+
// `document` for the *initial* lookup only, then observe the most
|
|
257
|
+
// specific available scope (widget root if present, otherwise the
|
|
258
|
+
// widget's parent / document.body as a last resort). The observer
|
|
259
|
+
// re-resolves the widget root on every callback so we tighten scope
|
|
260
|
+
// as soon as it mounts.
|
|
261
|
+
let toasterObserver;
|
|
262
|
+
const resolveScope = () => document.getElementById("ms_lcw_webchat_root") ?? document.body;
|
|
263
|
+
if (!labelToaster(resolveScope())) {
|
|
264
|
+
toasterObserver = new MutationObserver(() => {
|
|
265
|
+
const scope = resolveScope();
|
|
266
|
+
if (labelToaster(scope)) {
|
|
267
|
+
var _toasterObserver;
|
|
268
|
+
(_toasterObserver = toasterObserver) === null || _toasterObserver === void 0 ? void 0 : _toasterObserver.disconnect();
|
|
269
|
+
toasterObserver = undefined;
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
toasterObserver.observe(resolveScope(), {
|
|
273
|
+
childList: true,
|
|
274
|
+
subtree: true
|
|
275
|
+
});
|
|
276
|
+
}
|
|
235
277
|
dispatch({
|
|
236
278
|
type: _LiveChatWidgetActionType.LiveChatWidgetActionType.SET_RENDERING_MIDDLEWARE_PROPS,
|
|
237
279
|
payload: webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.renderingMiddlewareProps
|
|
@@ -256,6 +298,10 @@ const WebChatContainerStateful = props => {
|
|
|
256
298
|
}
|
|
257
299
|
}
|
|
258
300
|
}
|
|
301
|
+
return () => {
|
|
302
|
+
var _toasterObserver2;
|
|
303
|
+
(_toasterObserver2 = toasterObserver) === null || _toasterObserver2 === void 0 ? void 0 : _toasterObserver2.disconnect();
|
|
304
|
+
};
|
|
259
305
|
}, []);
|
|
260
306
|
(0, _react2.useEffect)(() => {
|
|
261
307
|
var _props$webChatContain6, _props$webChatContain7;
|
|
@@ -496,23 +542,7 @@ const WebChatContainerStateful = props => {
|
|
|
496
542
|
height: "100%",
|
|
497
543
|
width: "100%"
|
|
498
544
|
}
|
|
499
|
-
}, shouldLoadPersistentHistoryMessages && /*#__PURE__*/_react2.default.createElement(_WebChatEventSubscribers.default, null), /*#__PURE__*/_react2.default.createElement(BasicWebChat, null))), /*#__PURE__*/_react2.default.createElement(
|
|
500
|
-
id: _Constants.HtmlIdNames.fileSentAnnouncementRegionId,
|
|
501
|
-
role: "alert",
|
|
502
|
-
"aria-live": "assertive",
|
|
503
|
-
"aria-atomic": "true",
|
|
504
|
-
style: {
|
|
505
|
-
position: "absolute",
|
|
506
|
-
width: "1px",
|
|
507
|
-
height: "1px",
|
|
508
|
-
padding: "0",
|
|
509
|
-
margin: "-1px",
|
|
510
|
-
overflow: "hidden",
|
|
511
|
-
clip: "rect(0, 0, 0, 0)",
|
|
512
|
-
whiteSpace: "nowrap",
|
|
513
|
-
border: "0"
|
|
514
|
-
}
|
|
515
|
-
}), citationPaneOpen && /*#__PURE__*/_react2.default.createElement(_CitationPaneStateful.default, {
|
|
545
|
+
}, shouldLoadPersistentHistoryMessages && /*#__PURE__*/_react2.default.createElement(_WebChatEventSubscribers.default, null), /*#__PURE__*/_react2.default.createElement(BasicWebChat, null))), citationPaneOpen && /*#__PURE__*/_react2.default.createElement(_CitationPaneStateful.default, {
|
|
516
546
|
id: ((_props$citationPanePr = props.citationPaneProps) === null || _props$citationPanePr === void 0 ? void 0 : _props$citationPanePr.id) || _Constants.HtmlAttributeNames.ocwCitationPaneClassName,
|
|
517
547
|
title: ((_props$citationPanePr2 = props.citationPaneProps) === null || _props$citationPanePr2 === void 0 ? void 0 : _props$citationPanePr2.title) || _Constants.HtmlAttributeNames.ocwCitationPaneTitle,
|
|
518
548
|
contentHtml: citationPaneText,
|
|
@@ -59,6 +59,16 @@ let DesignerChatAdapter = /*#__PURE__*/function (_MockAdapter) {
|
|
|
59
59
|
if (msg.text) {
|
|
60
60
|
if (msg.suggestedActions) {
|
|
61
61
|
(0, _chatAdapterUtils.postAgentSuggestedActionsActivity)(this.activityObserver, msg.text, msg.suggestedActions, index * 100);
|
|
62
|
+
} else if (msg.from && msg.from.role) {
|
|
63
|
+
// Route caller-supplied `from` through the shared
|
|
64
|
+
// postBotMessageActivity helper so the activity shape
|
|
65
|
+
// (id, channelData.tags, timestamp, type) matches every
|
|
66
|
+
// other designer-mode path and downstream middleware
|
|
67
|
+
// (localizedStringsBotInitialsMiddleware, telemetry,
|
|
68
|
+
// attachment plumbing) behaves identically. The override
|
|
69
|
+
// narrows only the fields the caller cares about (e.g.
|
|
70
|
+
// from.name / from.role) — base botUser fields remain.
|
|
71
|
+
(0, _chatAdapterUtils.postBotMessageActivity)(this.activityObserver, msg.text, undefined, index * 100, msg.from);
|
|
62
72
|
} else {
|
|
63
73
|
(0, _chatAdapterUtils.postBotMessageActivity)(this.activityObserver, msg.text, undefined, index * 100);
|
|
64
74
|
}
|
|
@@ -20,6 +20,7 @@ const defaultWebChatContainerStatefulProps = {
|
|
|
20
20
|
honorsTargetInHTMLLinks: false,
|
|
21
21
|
hyperlinkTextOverride: false,
|
|
22
22
|
directLine: new _mockadapter.default(),
|
|
23
|
-
adaptiveCardStyles: _defaultAdaptiveCardStyles.defaultAdaptiveCardStyles
|
|
23
|
+
adaptiveCardStyles: _defaultAdaptiveCardStyles.defaultAdaptiveCardStyles,
|
|
24
|
+
webChatNotificationRegionAccessibilityLabel: "Chat notifications"
|
|
24
25
|
};
|
|
25
26
|
exports.defaultWebChatContainerStatefulProps = defaultWebChatContainerStatefulProps;
|
|
@@ -44,11 +44,13 @@ exports.postEchoActivity = postEchoActivity;
|
|
|
44
44
|
const postBotMessageActivity = function (activityObserver, text) {
|
|
45
45
|
let tags = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "";
|
|
46
46
|
let delay = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1000;
|
|
47
|
+
let fromOverride = arguments.length > 4 ? arguments[4] : undefined;
|
|
47
48
|
setTimeout(() => {
|
|
48
49
|
activityObserver === null || activityObserver === void 0 ? void 0 : activityObserver.next({
|
|
49
50
|
id: (0, _omnichannelChatSdk.uuidv4)(),
|
|
50
51
|
from: {
|
|
51
|
-
...botUser
|
|
52
|
+
...botUser,
|
|
53
|
+
...(fromOverride ?? {})
|
|
52
54
|
},
|
|
53
55
|
text,
|
|
54
56
|
type: "message",
|
|
@@ -32,6 +32,56 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
|
|
|
32
32
|
const container = containerRef.current;
|
|
33
33
|
if (!container) return;
|
|
34
34
|
|
|
35
|
+
// dropdown-double-label: compact Input.ChoiceSet renders as a native
|
|
36
|
+
// <select> with both aria-labelledby and a sibling visible <label for>.
|
|
37
|
+
// Only remove aria-labelledby when it points solely at that same visible
|
|
38
|
+
// label; preserve composite labels that include required/error/context text.
|
|
39
|
+
const compactSelects = container.querySelectorAll(".ac-input-container select.ac-input.ac-multichoiceInput[aria-labelledby]");
|
|
40
|
+
compactSelects.forEach(select => {
|
|
41
|
+
const id = select.id;
|
|
42
|
+
if (!id) return;
|
|
43
|
+
const visibleLabel = container.querySelector(`label[for="${CSS.escape(id)}"]:not([aria-hidden='true'])`);
|
|
44
|
+
const labelledBy = (select.getAttribute("aria-labelledby") || "").trim().split(/\s+/);
|
|
45
|
+
if (visibleLabel !== null && visibleLabel !== void 0 && visibleLabel.id && labelledBy.length === 1 && labelledBy[0] === visibleLabel.id) {
|
|
46
|
+
select.removeAttribute("aria-labelledby");
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// internal tracking: non-radio inputs (Input.Text including
|
|
51
|
+
// isMultiline=true which renders as <textarea>, Input.Date,
|
|
52
|
+
// Input.Number, Input.Toggle, Input.ChoiceSet multi-select
|
|
53
|
+
// checkbox) rendered by adaptivecards can have THREE accessible-
|
|
54
|
+
// name sources resolving to the same string: a visible
|
|
55
|
+
// <label for>, aria-label, AND aria-labelledby. TalkBack walks
|
|
56
|
+
// each accessibility node independently and reads the duplicated
|
|
57
|
+
// name. When a visible <label for> exists and names the control,
|
|
58
|
+
// strip the redundant aria-label / aria-labelledby on the input
|
|
59
|
+
// so only one announceable source remains.
|
|
60
|
+
const labelledInputs = container.querySelectorAll(".ac-input-container input.ac-input[id]:not([type='radio']), .ac-input-container textarea.ac-input[id]");
|
|
61
|
+
labelledInputs.forEach(input => {
|
|
62
|
+
const id = input.id;
|
|
63
|
+
if (!id) return;
|
|
64
|
+
const visibleLabel = container.querySelector(`label[for="${CSS.escape(id)}"]:not([aria-hidden='true'])`);
|
|
65
|
+
if (!visibleLabel) return;
|
|
66
|
+
const labelText = (visibleLabel.textContent || "").trim();
|
|
67
|
+
if (!labelText) return;
|
|
68
|
+
const ariaLabel = input.getAttribute("aria-label");
|
|
69
|
+
if (ariaLabel && ariaLabel.trim() === labelText) {
|
|
70
|
+
input.removeAttribute("aria-label");
|
|
71
|
+
}
|
|
72
|
+
const labelledBy = (input.getAttribute("aria-labelledby") || "").trim();
|
|
73
|
+
if (labelledBy) {
|
|
74
|
+
const ids = labelledBy.split(/\s+/);
|
|
75
|
+
const resolved = ids.map(rid => {
|
|
76
|
+
var _document$getElementB;
|
|
77
|
+
return (((_document$getElementB = document.getElementById(rid)) === null || _document$getElementB === void 0 ? void 0 : _document$getElementB.textContent) || "").trim();
|
|
78
|
+
}).filter(Boolean).join(" ");
|
|
79
|
+
if (resolved === labelText) {
|
|
80
|
+
input.removeAttribute("aria-labelledby");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
35
85
|
// action-button-toggle / login-button-toggle: submit/login action buttons can be
|
|
36
86
|
// rendered with toggle-only ARIA that causes screen readers to announce them as
|
|
37
87
|
// switches instead of plain push buttons. Preserve Action.ToggleVisibility buttons,
|
|
@@ -18,14 +18,10 @@ var _WebChatActionType = require("../../enums/WebChatActionType");
|
|
|
18
18
|
|
|
19
19
|
const MBtoBRatio = 1000000;
|
|
20
20
|
const announceFileSent = message => {
|
|
21
|
-
// TalkBack on Android WebView reliably announces newly *appended*
|
|
22
|
-
// nodes but often misses text-content updates on existing
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
const region = document.getElementById(_Constants.HtmlIdNames.fileSentAnnouncementRegionId);
|
|
26
|
-
if (region) {
|
|
27
|
-
region.textContent = "";
|
|
28
|
-
}
|
|
21
|
+
// TalkBack on Android WebView reliably announces newly *appended*
|
|
22
|
+
// role="alert" nodes but often misses text-content updates on existing
|
|
23
|
+
// nodes. Wait 500ms for the send-box focus shift to settle, then inject
|
|
24
|
+
// a fresh alert element appended to document.body and remove it after 3s.
|
|
29
25
|
setTimeout(() => {
|
|
30
26
|
const el = document.createElement("div");
|
|
31
27
|
el.setAttribute("role", "alert");
|