@microsoft/omnichannel-chat-widget 2.0.1-main.6ca1911 → 2.0.1-main.74b9796
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/initWebChatComposer.js +31 -7
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachmentForScreenReaderMiddleware.js +159 -0
- package/lib/esm/components/livechatwidget/common/initWebChatComposer.js +28 -6
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachmentForScreenReaderMiddleware.js +153 -0
- package/lib/types/components/livechatwidget/common/initWebChatComposer.d.ts +8 -0
- package/lib/types/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachmentForScreenReaderMiddleware.d.ts +11 -0
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.initWebChatComposer = void 0;
|
|
6
|
+
exports.initWebChatComposer = exports.createScreenReaderMarkdownRenderer = void 0;
|
|
7
7
|
var _Constants = require("../../../common/Constants");
|
|
8
8
|
var _TelemetryConstants = require("../../../common/telemetry/TelemetryConstants");
|
|
9
9
|
var _utils = require("../../../common/utils");
|
|
@@ -20,6 +20,7 @@ var _attachmentSentAnnouncementMiddleware = _interopRequireDefault(require("../.
|
|
|
20
20
|
var _channelDataMiddleware = _interopRequireDefault(require("../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/channelDataMiddleware"));
|
|
21
21
|
var _activityMiddleware = require("../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityMiddleware");
|
|
22
22
|
var _activityStatusMiddleware = require("../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityStatusMiddleware");
|
|
23
|
+
var _attachmentForScreenReaderMiddleware = require("../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachmentForScreenReaderMiddleware");
|
|
23
24
|
var _attachmentMiddleware = require("../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachmentMiddleware");
|
|
24
25
|
var _attachmentUploadValidatorMiddleware = _interopRequireDefault(require("../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware"));
|
|
25
26
|
var _avatarMiddleware = require("../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/avatarMiddleware");
|
|
@@ -48,7 +49,34 @@ var _htmlTextMiddleware = _interopRequireDefault(require("../../webchatcontainer
|
|
|
48
49
|
var _preProcessingMiddleware = _interopRequireDefault(require("../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/preProcessingMiddleware"));
|
|
49
50
|
var _sanitizationMiddleware = _interopRequireDefault(require("../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/sanitizationMiddleware"));
|
|
50
51
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
52
|
+
const markdownSanitizeConfig = {
|
|
53
|
+
FORBID_TAGS: ["form", "button", "script", "div", "input"],
|
|
54
|
+
FORBID_ATTR: ["action"],
|
|
55
|
+
ADD_ATTR: ["target"]
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Renders Markdown for the screen reader text of Adaptive Cards, from the same pieces as the visible card:
|
|
60
|
+
* the host renderMarkdown when set, the hyperlink text override, and the same sanitization.
|
|
61
|
+
* Markdown is always formatted, also when the widget shows Markdown unformatted, so that screen readers do not read the syntax.
|
|
62
|
+
* Styles are removed with their content because the card does not show them.
|
|
63
|
+
*/
|
|
64
|
+
const createScreenReaderMarkdownRenderer = webChatContainerProps => {
|
|
65
|
+
var _webChatContainerProp;
|
|
66
|
+
const hostRenderMarkdown = webChatContainerProps === null || webChatContainerProps === void 0 || (_webChatContainerProp = webChatContainerProps.webChatProps) === null || _webChatContainerProp === void 0 ? void 0 : _webChatContainerProp.renderMarkdown;
|
|
67
|
+
const markdown = (0, _createMarkdown.createMarkdown)(false, (webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.disableNewLineMarkdownSupport) ?? _defaultWebChatContainerStatefulProps.defaultWebChatContainerStatefulProps.disableNewLineMarkdownSupport, webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.opensMarkdownLinksInSameTab);
|
|
68
|
+
const hyperlinkTextOverrideRenderer = new _HyperlinkTextOverrideRenderer.default((webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.hyperlinkTextOverride) ?? _defaultWebChatContainerStatefulProps.defaultWebChatContainerStatefulProps.hyperlinkTextOverride);
|
|
69
|
+
return text => {
|
|
70
|
+
const html = hostRenderMarkdown ? hostRenderMarkdown(text) : markdown.render(text);
|
|
71
|
+
return _dompurify.default.sanitize(hyperlinkTextOverrideRenderer.render(html), {
|
|
72
|
+
...markdownSanitizeConfig,
|
|
73
|
+
FORBID_TAGS: [...markdownSanitizeConfig.FORBID_TAGS, "style"]
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
51
78
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
79
|
+
exports.createScreenReaderMarkdownRenderer = createScreenReaderMarkdownRenderer;
|
|
52
80
|
const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) => {
|
|
53
81
|
var _props$webChatContain, _props$webChatContain2, _props$webChatContain3, _props$webChatContain4, _props$webChatContain5, _props$webChatContain6, _state$domainStates$l4, _props$webChatContain0, _state$domainStates$r, _state$domainStates$r2, _props$webChatContain1, _state$domainStates$r3, _props$webChatContain10, _state$domainStates$l5, _props$webChatContain11, _props$webChatContain12, _state$domainStates$r4, _state$domainStates$r5, _props$webChatContain13, _defaultWebChatContai, _props$webChatContain14, _defaultWebChatContai2, _props$webChatContain15, _props$webChatContain16, _props$webChatContain17;
|
|
54
82
|
// Add a hook to make all links open a new window
|
|
@@ -154,12 +182,7 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
|
|
|
154
182
|
});
|
|
155
183
|
|
|
156
184
|
// EXISTING sanitization (continues to work as before)
|
|
157
|
-
|
|
158
|
-
FORBID_TAGS: ["form", "button", "script", "div", "input"],
|
|
159
|
-
FORBID_ATTR: ["action"],
|
|
160
|
-
ADD_ATTR: ["target"]
|
|
161
|
-
};
|
|
162
|
-
text = _dompurify.default.sanitize(text, config);
|
|
185
|
+
text = _dompurify.default.sanitize(text, markdownSanitizeConfig);
|
|
163
186
|
|
|
164
187
|
// MONITOR-ONLY: Test what the stricter allowlist would remove (Phase 1)
|
|
165
188
|
// This does NOT modify the text, only logs telemetry
|
|
@@ -363,6 +386,7 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
|
|
|
363
386
|
store: webChatStore,
|
|
364
387
|
activityMiddleware: (_props$webChatContain0 = props.webChatContainerProps) !== null && _props$webChatContain0 !== void 0 && (_props$webChatContain0 = _props$webChatContain0.renderingMiddlewareProps) !== null && _props$webChatContain0 !== void 0 && _props$webChatContain0.disableActivityMiddleware ? undefined : (0, _activityMiddleware.createActivityMiddleware)(renderMarkdown, (_state$domainStates$r = state.domainStates.renderingMiddlewareProps) === null || _state$domainStates$r === void 0 ? void 0 : _state$domainStates$r.systemMessageStyleProps, (_state$domainStates$r2 = state.domainStates.renderingMiddlewareProps) === null || _state$domainStates$r2 === void 0 ? void 0 : _state$domainStates$r2.userMessageStyleProps, localizedTexts),
|
|
365
388
|
attachmentMiddleware: (_props$webChatContain1 = props.webChatContainerProps) !== null && _props$webChatContain1 !== void 0 && (_props$webChatContain1 = _props$webChatContain1.renderingMiddlewareProps) !== null && _props$webChatContain1 !== void 0 && _props$webChatContain1.disableAttachmentMiddleware ? undefined : (0, _attachmentMiddleware.createAttachmentMiddleware)(((_state$domainStates$r3 = state.domainStates.renderingMiddlewareProps) === null || _state$domainStates$r3 === void 0 || (_state$domainStates$r3 = _state$domainStates$r3.attachmentProps) === null || _state$domainStates$r3 === void 0 ? void 0 : _state$domainStates$r3.enableInlinePlaying) ?? _defaultAttachmentProps.defaultAttachmentProps.enableInlinePlaying),
|
|
389
|
+
attachmentForScreenReaderMiddleware: (0, _attachmentForScreenReaderMiddleware.createAttachmentForScreenReaderMiddleware)(createScreenReaderMarkdownRenderer(props.webChatContainerProps)),
|
|
366
390
|
activityStatusMiddleware: (_props$webChatContain10 = props.webChatContainerProps) !== null && _props$webChatContain10 !== void 0 && (_props$webChatContain10 = _props$webChatContain10.renderingMiddlewareProps) !== null && _props$webChatContain10 !== void 0 && _props$webChatContain10.disableActivityStatusMiddleware ? undefined : (0, _activityStatusMiddleware.createActivityStatusMiddleware)((0, _omnichannelChatSdk.getLocaleStringFromId)((_state$domainStates$l5 = state.domainStates.liveChatConfig) === null || _state$domainStates$l5 === void 0 || (_state$domainStates$l5 = _state$domainStates$l5.ChatWidgetLanguage) === null || _state$domainStates$l5 === void 0 ? void 0 : _state$domainStates$l5.msdyn_localeid)),
|
|
367
391
|
toastMiddleware: (_props$webChatContain11 = props.webChatContainerProps) !== null && _props$webChatContain11 !== void 0 && (_props$webChatContain11 = _props$webChatContain11.renderingMiddlewareProps) !== null && _props$webChatContain11 !== void 0 && _props$webChatContain11.disableToastMiddleware ? undefined : (0, _toastMiddleware.createToastMiddleware)(props.notificationPaneProps, endChat),
|
|
368
392
|
renderMarkdown,
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.getAdaptiveCardSpeak = exports.createAttachmentForScreenReaderMiddleware = void 0;
|
|
7
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
8
|
+
var _Constants = require("../../../../../common/Constants");
|
|
9
|
+
var _botframeworkWebchat = require("botframework-webchat");
|
|
10
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
11
|
+
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
|
|
12
|
+
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
13
|
+
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
14
|
+
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
15
|
+
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
16
|
+
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } /* eslint-disable @typescript-eslint/no-explicit-any */ /******
|
|
17
|
+
* AttachmentForScreenReaderMiddleware
|
|
18
|
+
*
|
|
19
|
+
* Web Chat reads an Adaptive Card to screen readers only from the card's `speak` property.
|
|
20
|
+
* A card without `speak` is announced as "A card" with no content.
|
|
21
|
+
* This middleware sets `speak` from the visible text of the card, then lets Web Chat render it as usual.
|
|
22
|
+
******/
|
|
23
|
+
const useAdaptiveCardsPackage = _botframeworkWebchat.hooks.useAdaptiveCardsPackage,
|
|
24
|
+
useStyleOptions = _botframeworkWebchat.hooks.useStyleOptions;
|
|
25
|
+
const ADAPTIVE_CARD_CONTENT_TYPE = "application/vnd.microsoft.card.adaptive";
|
|
26
|
+
const HTML_BLOCK_END = /<\/(p|li|h[1-6]|div|tr|td|th|blockquote|pre)>|<br\s*\/?>/gi;
|
|
27
|
+
// The "opens in a new window" icon of each link is left out: it describes the link target, not the content of the message.
|
|
28
|
+
const htmlToText = html => {
|
|
29
|
+
const body = new DOMParser().parseFromString(html.replace(HTML_BLOCK_END, "$& "), "text/html").body;
|
|
30
|
+
body.querySelectorAll(`script, style, template, img.${_Constants.Constants.OpenLinkIconCssClass}`).forEach(element => element.remove());
|
|
31
|
+
body.querySelectorAll("img").forEach(image => image.replaceWith(` ${image.getAttribute("alt") ?? ""} `));
|
|
32
|
+
return (body.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Renders the element as the visible card does: Adaptive Cards formats {{DATE()}} and {{TIME()}}, then the Markdown is rendered.
|
|
36
|
+
// Web Chat sets the static Markdown handler before each visible render, and this restores the previous handler after rendering.
|
|
37
|
+
const getRenderedText = (element, AdaptiveCard, renderMarkdownAsHTML) => {
|
|
38
|
+
const previousOnProcessMarkdown = AdaptiveCard.onProcessMarkdown;
|
|
39
|
+
try {
|
|
40
|
+
AdaptiveCard.onProcessMarkdown = (text, result) => {
|
|
41
|
+
result.outputHtml = renderMarkdownAsHTML(text);
|
|
42
|
+
result.didProcess = true;
|
|
43
|
+
};
|
|
44
|
+
const renderedElement = element.render();
|
|
45
|
+
return renderedElement ? htmlToText(renderedElement.outerHTML) : "";
|
|
46
|
+
} catch {
|
|
47
|
+
var _element$asString;
|
|
48
|
+
return ((_element$asString = element.asString) === null || _element$asString === void 0 ? void 0 : _element$asString.call(element)) ?? "";
|
|
49
|
+
} finally {
|
|
50
|
+
AdaptiveCard.onProcessMarkdown = previousOnProcessMarkdown;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Collects the visible text of the card in document order, after the parser has applied element fallbacks.
|
|
55
|
+
// Inputs and actions are not collected because Web Chat already lists them after the card text.
|
|
56
|
+
const getAdaptiveCardSpeak = (content, adaptiveCardsPackage, renderMarkdownAsHTML, adaptiveCardsParserMaxVersion) => {
|
|
57
|
+
const AdaptiveCard = adaptiveCardsPackage.AdaptiveCard,
|
|
58
|
+
FactSet = adaptiveCardsPackage.FactSet,
|
|
59
|
+
Image = adaptiveCardsPackage.Image,
|
|
60
|
+
RichTextBlock = adaptiveCardsPackage.RichTextBlock,
|
|
61
|
+
SerializationContext = adaptiveCardsPackage.SerializationContext,
|
|
62
|
+
TextBlock = adaptiveCardsPackage.TextBlock,
|
|
63
|
+
Version = adaptiveCardsPackage.Version;
|
|
64
|
+
const maxVersion = adaptiveCardsParserMaxVersion ? Version.parse(adaptiveCardsParserMaxVersion, new SerializationContext()) : undefined;
|
|
65
|
+
const card = new AdaptiveCard();
|
|
66
|
+
const texts = [];
|
|
67
|
+
card.parse(content, new SerializationContext(maxVersion !== null && maxVersion !== void 0 && maxVersion.isValid ? maxVersion : undefined));
|
|
68
|
+
const getFactText = (factSet, text) => {
|
|
69
|
+
const textBlock = new TextBlock(text);
|
|
70
|
+
textBlock.setParent(factSet);
|
|
71
|
+
return getRenderedText(textBlock, AdaptiveCard, renderMarkdownAsHTML);
|
|
72
|
+
};
|
|
73
|
+
const walk = element => {
|
|
74
|
+
if (!(element !== null && element !== void 0 && element.isVisible)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (element instanceof TextBlock || element instanceof RichTextBlock) {
|
|
78
|
+
texts.push(getRenderedText(element, AdaptiveCard, renderMarkdownAsHTML));
|
|
79
|
+
} else if (element instanceof FactSet) {
|
|
80
|
+
element.facts.forEach(_ref => {
|
|
81
|
+
let name = _ref.name,
|
|
82
|
+
value = _ref.value;
|
|
83
|
+
texts.push([name, value].map(text => text ? getFactText(element, text) : "").filter(Boolean).join(": "));
|
|
84
|
+
});
|
|
85
|
+
} else if (element instanceof Image) {
|
|
86
|
+
texts.push(element.altText);
|
|
87
|
+
}
|
|
88
|
+
if (element.getItemAt && element.getItemCount) {
|
|
89
|
+
for (let index = 0, count = element.getItemCount(); index < count; index++) {
|
|
90
|
+
walk(element.getItemAt(index));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
walk(card);
|
|
95
|
+
return texts.filter(text => typeof text === "string" && text).join(" ").replace(/\s+/g, " ").trim();
|
|
96
|
+
};
|
|
97
|
+
exports.getAdaptiveCardSpeak = getAdaptiveCardSpeak;
|
|
98
|
+
const AdaptiveCardForScreenReader = _ref2 => {
|
|
99
|
+
let args = _ref2.args,
|
|
100
|
+
next = _ref2.next,
|
|
101
|
+
renderMarkdownAsHTML = _ref2.renderMarkdownAsHTML;
|
|
102
|
+
const _useAdaptiveCardsPack = useAdaptiveCardsPackage(),
|
|
103
|
+
_useAdaptiveCardsPack2 = _slicedToArray(_useAdaptiveCardsPack, 1),
|
|
104
|
+
adaptiveCardsPackage = _useAdaptiveCardsPack2[0];
|
|
105
|
+
const _useStyleOptions = useStyleOptions(),
|
|
106
|
+
_useStyleOptions2 = _slicedToArray(_useStyleOptions, 1),
|
|
107
|
+
adaptiveCardsParserMaxVersion = _useStyleOptions2[0].adaptiveCardsParserMaxVersion;
|
|
108
|
+
const _args = _slicedToArray(args, 1),
|
|
109
|
+
options = _args[0];
|
|
110
|
+
const attachment = options.attachment;
|
|
111
|
+
|
|
112
|
+
// The widget creates a new Markdown renderer on each render with the same settings, so the text is computed once for each attachment.
|
|
113
|
+
const contentWithSpeak = (0, _react.useMemo)(() => {
|
|
114
|
+
try {
|
|
115
|
+
const content = typeof attachment.content === "string" ? JSON.parse(attachment.content) : attachment.content;
|
|
116
|
+
if (!content || typeof content !== "object" || content.speak) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
const speak = getAdaptiveCardSpeak(content, adaptiveCardsPackage, renderMarkdownAsHTML, adaptiveCardsParserMaxVersion);
|
|
120
|
+
return speak ? {
|
|
121
|
+
...content,
|
|
122
|
+
speak
|
|
123
|
+
} : undefined;
|
|
124
|
+
} catch {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}, [adaptiveCardsPackage, adaptiveCardsParserMaxVersion, attachment]);
|
|
128
|
+
const render = contentWithSpeak ? next({
|
|
129
|
+
...options,
|
|
130
|
+
attachment: {
|
|
131
|
+
...attachment,
|
|
132
|
+
content: contentWithSpeak
|
|
133
|
+
}
|
|
134
|
+
}, ...args.slice(1)) : next(...args);
|
|
135
|
+
return typeof render === "function" ? render() : null;
|
|
136
|
+
};
|
|
137
|
+
const createAttachmentForScreenReaderMiddleware = renderMarkdownAsHTML => {
|
|
138
|
+
const attachmentForScreenReaderMiddleware = () => next => function () {
|
|
139
|
+
var _args$;
|
|
140
|
+
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
141
|
+
args[_key] = arguments[_key];
|
|
142
|
+
}
|
|
143
|
+
const attachment = (_args$ = args[0]) === null || _args$ === void 0 ? void 0 : _args$.attachment;
|
|
144
|
+
|
|
145
|
+
// Web Chat renders nothing for a card without content, so it is left to Web Chat.
|
|
146
|
+
if ((attachment === null || attachment === void 0 ? void 0 : attachment.contentType) !== ADAPTIVE_CARD_CONTENT_TYPE || !attachment.content) {
|
|
147
|
+
return next(...args);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// eslint-disable-next-line react/display-name
|
|
151
|
+
return () => /*#__PURE__*/_react.default.createElement(AdaptiveCardForScreenReader, {
|
|
152
|
+
args: args,
|
|
153
|
+
next: next,
|
|
154
|
+
renderMarkdownAsHTML: renderMarkdownAsHTML
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
return attachmentForScreenReaderMiddleware;
|
|
158
|
+
};
|
|
159
|
+
exports.createAttachmentForScreenReaderMiddleware = createAttachmentForScreenReaderMiddleware;
|
|
@@ -15,6 +15,7 @@ import createAttachmentSentAnnouncementMiddleware from "../../webchatcontainerst
|
|
|
15
15
|
import channelDataMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/channelDataMiddleware";
|
|
16
16
|
import { createActivityMiddleware } from "../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityMiddleware";
|
|
17
17
|
import { createActivityStatusMiddleware } from "../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityStatusMiddleware";
|
|
18
|
+
import { createAttachmentForScreenReaderMiddleware } from "../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachmentForScreenReaderMiddleware";
|
|
18
19
|
import { createAttachmentMiddleware } from "../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachmentMiddleware";
|
|
19
20
|
import createAttachmentUploadValidatorMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentUploadValidatorMiddleware";
|
|
20
21
|
import { createAvatarMiddleware } from "../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/avatarMiddleware";
|
|
@@ -42,6 +43,31 @@ import htmlPlayerMiddleware from "../../webchatcontainerstateful/webchatcontroll
|
|
|
42
43
|
import htmlTextMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/htmlTextMiddleware";
|
|
43
44
|
import preProcessingMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/preProcessingMiddleware";
|
|
44
45
|
import sanitizationMiddleware from "../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/sanitizationMiddleware";
|
|
46
|
+
const markdownSanitizeConfig = {
|
|
47
|
+
FORBID_TAGS: ["form", "button", "script", "div", "input"],
|
|
48
|
+
FORBID_ATTR: ["action"],
|
|
49
|
+
ADD_ATTR: ["target"]
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Renders Markdown for the screen reader text of Adaptive Cards, from the same pieces as the visible card:
|
|
54
|
+
* the host renderMarkdown when set, the hyperlink text override, and the same sanitization.
|
|
55
|
+
* Markdown is always formatted, also when the widget shows Markdown unformatted, so that screen readers do not read the syntax.
|
|
56
|
+
* Styles are removed with their content because the card does not show them.
|
|
57
|
+
*/
|
|
58
|
+
export const createScreenReaderMarkdownRenderer = webChatContainerProps => {
|
|
59
|
+
var _webChatContainerProp;
|
|
60
|
+
const hostRenderMarkdown = webChatContainerProps === null || webChatContainerProps === void 0 || (_webChatContainerProp = webChatContainerProps.webChatProps) === null || _webChatContainerProp === void 0 ? void 0 : _webChatContainerProp.renderMarkdown;
|
|
61
|
+
const markdown = createMarkdown(false, (webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.disableNewLineMarkdownSupport) ?? defaultWebChatContainerStatefulProps.disableNewLineMarkdownSupport, webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.opensMarkdownLinksInSameTab);
|
|
62
|
+
const hyperlinkTextOverrideRenderer = new HyperlinkTextOverrideRenderer((webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.hyperlinkTextOverride) ?? defaultWebChatContainerStatefulProps.hyperlinkTextOverride);
|
|
63
|
+
return text => {
|
|
64
|
+
const html = hostRenderMarkdown ? hostRenderMarkdown(text) : markdown.render(text);
|
|
65
|
+
return DOMPurify.sanitize(hyperlinkTextOverrideRenderer.render(html), {
|
|
66
|
+
...markdownSanitizeConfig,
|
|
67
|
+
FORBID_TAGS: [...markdownSanitizeConfig.FORBID_TAGS, "style"]
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
};
|
|
45
71
|
|
|
46
72
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
73
|
export const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) => {
|
|
@@ -149,12 +175,7 @@ export const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endCh
|
|
|
149
175
|
});
|
|
150
176
|
|
|
151
177
|
// EXISTING sanitization (continues to work as before)
|
|
152
|
-
|
|
153
|
-
FORBID_TAGS: ["form", "button", "script", "div", "input"],
|
|
154
|
-
FORBID_ATTR: ["action"],
|
|
155
|
-
ADD_ATTR: ["target"]
|
|
156
|
-
};
|
|
157
|
-
text = DOMPurify.sanitize(text, config);
|
|
178
|
+
text = DOMPurify.sanitize(text, markdownSanitizeConfig);
|
|
158
179
|
|
|
159
180
|
// MONITOR-ONLY: Test what the stricter allowlist would remove (Phase 1)
|
|
160
181
|
// This does NOT modify the text, only logs telemetry
|
|
@@ -358,6 +379,7 @@ export const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endCh
|
|
|
358
379
|
store: webChatStore,
|
|
359
380
|
activityMiddleware: (_props$webChatContain0 = props.webChatContainerProps) !== null && _props$webChatContain0 !== void 0 && (_props$webChatContain0 = _props$webChatContain0.renderingMiddlewareProps) !== null && _props$webChatContain0 !== void 0 && _props$webChatContain0.disableActivityMiddleware ? undefined : createActivityMiddleware(renderMarkdown, (_state$domainStates$r = state.domainStates.renderingMiddlewareProps) === null || _state$domainStates$r === void 0 ? void 0 : _state$domainStates$r.systemMessageStyleProps, (_state$domainStates$r2 = state.domainStates.renderingMiddlewareProps) === null || _state$domainStates$r2 === void 0 ? void 0 : _state$domainStates$r2.userMessageStyleProps, localizedTexts),
|
|
360
381
|
attachmentMiddleware: (_props$webChatContain1 = props.webChatContainerProps) !== null && _props$webChatContain1 !== void 0 && (_props$webChatContain1 = _props$webChatContain1.renderingMiddlewareProps) !== null && _props$webChatContain1 !== void 0 && _props$webChatContain1.disableAttachmentMiddleware ? undefined : createAttachmentMiddleware(((_state$domainStates$r3 = state.domainStates.renderingMiddlewareProps) === null || _state$domainStates$r3 === void 0 || (_state$domainStates$r3 = _state$domainStates$r3.attachmentProps) === null || _state$domainStates$r3 === void 0 ? void 0 : _state$domainStates$r3.enableInlinePlaying) ?? defaultAttachmentProps.enableInlinePlaying),
|
|
382
|
+
attachmentForScreenReaderMiddleware: createAttachmentForScreenReaderMiddleware(createScreenReaderMarkdownRenderer(props.webChatContainerProps)),
|
|
361
383
|
activityStatusMiddleware: (_props$webChatContain10 = props.webChatContainerProps) !== null && _props$webChatContain10 !== void 0 && (_props$webChatContain10 = _props$webChatContain10.renderingMiddlewareProps) !== null && _props$webChatContain10 !== void 0 && _props$webChatContain10.disableActivityStatusMiddleware ? undefined : createActivityStatusMiddleware(getLocaleStringFromId((_state$domainStates$l5 = state.domainStates.liveChatConfig) === null || _state$domainStates$l5 === void 0 || (_state$domainStates$l5 = _state$domainStates$l5.ChatWidgetLanguage) === null || _state$domainStates$l5 === void 0 ? void 0 : _state$domainStates$l5.msdyn_localeid)),
|
|
362
384
|
toastMiddleware: (_props$webChatContain11 = props.webChatContainerProps) !== null && _props$webChatContain11 !== void 0 && (_props$webChatContain11 = _props$webChatContain11.renderingMiddlewareProps) !== null && _props$webChatContain11 !== void 0 && _props$webChatContain11.disableToastMiddleware ? undefined : createToastMiddleware(props.notificationPaneProps, endChat),
|
|
363
385
|
renderMarkdown,
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
|
|
2
|
+
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
3
|
+
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
4
|
+
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
5
|
+
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
6
|
+
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
8
|
+
/******
|
|
9
|
+
* AttachmentForScreenReaderMiddleware
|
|
10
|
+
*
|
|
11
|
+
* Web Chat reads an Adaptive Card to screen readers only from the card's `speak` property.
|
|
12
|
+
* A card without `speak` is announced as "A card" with no content.
|
|
13
|
+
* This middleware sets `speak` from the visible text of the card, then lets Web Chat render it as usual.
|
|
14
|
+
******/
|
|
15
|
+
|
|
16
|
+
import React, { useMemo } from "react";
|
|
17
|
+
import { Constants } from "../../../../../common/Constants";
|
|
18
|
+
import { hooks } from "botframework-webchat";
|
|
19
|
+
const useAdaptiveCardsPackage = hooks.useAdaptiveCardsPackage,
|
|
20
|
+
useStyleOptions = hooks.useStyleOptions;
|
|
21
|
+
const ADAPTIVE_CARD_CONTENT_TYPE = "application/vnd.microsoft.card.adaptive";
|
|
22
|
+
const HTML_BLOCK_END = /<\/(p|li|h[1-6]|div|tr|td|th|blockquote|pre)>|<br\s*\/?>/gi;
|
|
23
|
+
// The "opens in a new window" icon of each link is left out: it describes the link target, not the content of the message.
|
|
24
|
+
const htmlToText = html => {
|
|
25
|
+
const body = new DOMParser().parseFromString(html.replace(HTML_BLOCK_END, "$& "), "text/html").body;
|
|
26
|
+
body.querySelectorAll(`script, style, template, img.${Constants.OpenLinkIconCssClass}`).forEach(element => element.remove());
|
|
27
|
+
body.querySelectorAll("img").forEach(image => image.replaceWith(` ${image.getAttribute("alt") ?? ""} `));
|
|
28
|
+
return (body.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Renders the element as the visible card does: Adaptive Cards formats {{DATE()}} and {{TIME()}}, then the Markdown is rendered.
|
|
32
|
+
// Web Chat sets the static Markdown handler before each visible render, and this restores the previous handler after rendering.
|
|
33
|
+
const getRenderedText = (element, AdaptiveCard, renderMarkdownAsHTML) => {
|
|
34
|
+
const previousOnProcessMarkdown = AdaptiveCard.onProcessMarkdown;
|
|
35
|
+
try {
|
|
36
|
+
AdaptiveCard.onProcessMarkdown = (text, result) => {
|
|
37
|
+
result.outputHtml = renderMarkdownAsHTML(text);
|
|
38
|
+
result.didProcess = true;
|
|
39
|
+
};
|
|
40
|
+
const renderedElement = element.render();
|
|
41
|
+
return renderedElement ? htmlToText(renderedElement.outerHTML) : "";
|
|
42
|
+
} catch {
|
|
43
|
+
var _element$asString;
|
|
44
|
+
return ((_element$asString = element.asString) === null || _element$asString === void 0 ? void 0 : _element$asString.call(element)) ?? "";
|
|
45
|
+
} finally {
|
|
46
|
+
AdaptiveCard.onProcessMarkdown = previousOnProcessMarkdown;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Collects the visible text of the card in document order, after the parser has applied element fallbacks.
|
|
51
|
+
// Inputs and actions are not collected because Web Chat already lists them after the card text.
|
|
52
|
+
export const getAdaptiveCardSpeak = (content, adaptiveCardsPackage, renderMarkdownAsHTML, adaptiveCardsParserMaxVersion) => {
|
|
53
|
+
const AdaptiveCard = adaptiveCardsPackage.AdaptiveCard,
|
|
54
|
+
FactSet = adaptiveCardsPackage.FactSet,
|
|
55
|
+
Image = adaptiveCardsPackage.Image,
|
|
56
|
+
RichTextBlock = adaptiveCardsPackage.RichTextBlock,
|
|
57
|
+
SerializationContext = adaptiveCardsPackage.SerializationContext,
|
|
58
|
+
TextBlock = adaptiveCardsPackage.TextBlock,
|
|
59
|
+
Version = adaptiveCardsPackage.Version;
|
|
60
|
+
const maxVersion = adaptiveCardsParserMaxVersion ? Version.parse(adaptiveCardsParserMaxVersion, new SerializationContext()) : undefined;
|
|
61
|
+
const card = new AdaptiveCard();
|
|
62
|
+
const texts = [];
|
|
63
|
+
card.parse(content, new SerializationContext(maxVersion !== null && maxVersion !== void 0 && maxVersion.isValid ? maxVersion : undefined));
|
|
64
|
+
const getFactText = (factSet, text) => {
|
|
65
|
+
const textBlock = new TextBlock(text);
|
|
66
|
+
textBlock.setParent(factSet);
|
|
67
|
+
return getRenderedText(textBlock, AdaptiveCard, renderMarkdownAsHTML);
|
|
68
|
+
};
|
|
69
|
+
const walk = element => {
|
|
70
|
+
if (!(element !== null && element !== void 0 && element.isVisible)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (element instanceof TextBlock || element instanceof RichTextBlock) {
|
|
74
|
+
texts.push(getRenderedText(element, AdaptiveCard, renderMarkdownAsHTML));
|
|
75
|
+
} else if (element instanceof FactSet) {
|
|
76
|
+
element.facts.forEach(_ref => {
|
|
77
|
+
let name = _ref.name,
|
|
78
|
+
value = _ref.value;
|
|
79
|
+
texts.push([name, value].map(text => text ? getFactText(element, text) : "").filter(Boolean).join(": "));
|
|
80
|
+
});
|
|
81
|
+
} else if (element instanceof Image) {
|
|
82
|
+
texts.push(element.altText);
|
|
83
|
+
}
|
|
84
|
+
if (element.getItemAt && element.getItemCount) {
|
|
85
|
+
for (let index = 0, count = element.getItemCount(); index < count; index++) {
|
|
86
|
+
walk(element.getItemAt(index));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
walk(card);
|
|
91
|
+
return texts.filter(text => typeof text === "string" && text).join(" ").replace(/\s+/g, " ").trim();
|
|
92
|
+
};
|
|
93
|
+
const AdaptiveCardForScreenReader = _ref2 => {
|
|
94
|
+
let args = _ref2.args,
|
|
95
|
+
next = _ref2.next,
|
|
96
|
+
renderMarkdownAsHTML = _ref2.renderMarkdownAsHTML;
|
|
97
|
+
const _useAdaptiveCardsPack = useAdaptiveCardsPackage(),
|
|
98
|
+
_useAdaptiveCardsPack2 = _slicedToArray(_useAdaptiveCardsPack, 1),
|
|
99
|
+
adaptiveCardsPackage = _useAdaptiveCardsPack2[0];
|
|
100
|
+
const _useStyleOptions = useStyleOptions(),
|
|
101
|
+
_useStyleOptions2 = _slicedToArray(_useStyleOptions, 1),
|
|
102
|
+
adaptiveCardsParserMaxVersion = _useStyleOptions2[0].adaptiveCardsParserMaxVersion;
|
|
103
|
+
const _args = _slicedToArray(args, 1),
|
|
104
|
+
options = _args[0];
|
|
105
|
+
const attachment = options.attachment;
|
|
106
|
+
|
|
107
|
+
// The widget creates a new Markdown renderer on each render with the same settings, so the text is computed once for each attachment.
|
|
108
|
+
const contentWithSpeak = useMemo(() => {
|
|
109
|
+
try {
|
|
110
|
+
const content = typeof attachment.content === "string" ? JSON.parse(attachment.content) : attachment.content;
|
|
111
|
+
if (!content || typeof content !== "object" || content.speak) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
const speak = getAdaptiveCardSpeak(content, adaptiveCardsPackage, renderMarkdownAsHTML, adaptiveCardsParserMaxVersion);
|
|
115
|
+
return speak ? {
|
|
116
|
+
...content,
|
|
117
|
+
speak
|
|
118
|
+
} : undefined;
|
|
119
|
+
} catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}, [adaptiveCardsPackage, adaptiveCardsParserMaxVersion, attachment]);
|
|
123
|
+
const render = contentWithSpeak ? next({
|
|
124
|
+
...options,
|
|
125
|
+
attachment: {
|
|
126
|
+
...attachment,
|
|
127
|
+
content: contentWithSpeak
|
|
128
|
+
}
|
|
129
|
+
}, ...args.slice(1)) : next(...args);
|
|
130
|
+
return typeof render === "function" ? render() : null;
|
|
131
|
+
};
|
|
132
|
+
export const createAttachmentForScreenReaderMiddleware = renderMarkdownAsHTML => {
|
|
133
|
+
const attachmentForScreenReaderMiddleware = () => next => function () {
|
|
134
|
+
var _args$;
|
|
135
|
+
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
136
|
+
args[_key] = arguments[_key];
|
|
137
|
+
}
|
|
138
|
+
const attachment = (_args$ = args[0]) === null || _args$ === void 0 ? void 0 : _args$.attachment;
|
|
139
|
+
|
|
140
|
+
// Web Chat renders nothing for a card without content, so it is left to Web Chat.
|
|
141
|
+
if ((attachment === null || attachment === void 0 ? void 0 : attachment.contentType) !== ADAPTIVE_CARD_CONTENT_TYPE || !attachment.content) {
|
|
142
|
+
return next(...args);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// eslint-disable-next-line react/display-name
|
|
146
|
+
return () => /*#__PURE__*/React.createElement(AdaptiveCardForScreenReader, {
|
|
147
|
+
args: args,
|
|
148
|
+
next: next,
|
|
149
|
+
renderMarkdownAsHTML: renderMarkdownAsHTML
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
return attachmentForScreenReaderMiddleware;
|
|
153
|
+
};
|
|
@@ -4,4 +4,12 @@ import { ILiveChatWidgetAction } from "../../../contexts/common/ILiveChatWidgetA
|
|
|
4
4
|
import { ILiveChatWidgetContext } from "../../../contexts/common/ILiveChatWidgetContext";
|
|
5
5
|
import { ILiveChatWidgetProps } from "../interfaces/ILiveChatWidgetProps";
|
|
6
6
|
import { IWebChatProps } from "../../webchatcontainerstateful/interfaces/IWebChatProps";
|
|
7
|
+
import { IWebChatContainerStatefulProps } from "../../webchatcontainerstateful/interfaces/IWebChatContainerStatefulProps";
|
|
8
|
+
/**
|
|
9
|
+
* Renders Markdown for the screen reader text of Adaptive Cards, from the same pieces as the visible card:
|
|
10
|
+
* the host renderMarkdown when set, the hyperlink text override, and the same sanitization.
|
|
11
|
+
* Markdown is always formatted, also when the widget shows Markdown unformatted, so that screen readers do not read the syntax.
|
|
12
|
+
* Styles are removed with their content because the card does not show them.
|
|
13
|
+
*/
|
|
14
|
+
export declare const createScreenReaderMarkdownRenderer: (webChatContainerProps?: IWebChatContainerStatefulProps) => (text: string) => string;
|
|
7
15
|
export declare const initWebChatComposer: (props: ILiveChatWidgetProps, state: ILiveChatWidgetContext, dispatch: Dispatch<ILiveChatWidgetAction>, facadeChatSDK: FacadeChatSDK, endChat: any) => IWebChatProps;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/******
|
|
2
|
+
* AttachmentForScreenReaderMiddleware
|
|
3
|
+
*
|
|
4
|
+
* Web Chat reads an Adaptive Card to screen readers only from the card's `speak` property.
|
|
5
|
+
* A card without `speak` is announced as "A card" with no content.
|
|
6
|
+
* This middleware sets `speak` from the visible text of the card, then lets Web Chat render it as usual.
|
|
7
|
+
******/
|
|
8
|
+
type RenderMarkdownAsHTML = (markdown: string) => string;
|
|
9
|
+
export declare const getAdaptiveCardSpeak: (content: any, adaptiveCardsPackage: any, renderMarkdownAsHTML: RenderMarkdownAsHTML, adaptiveCardsParserMaxVersion?: string) => string;
|
|
10
|
+
export declare const createAttachmentForScreenReaderMiddleware: (renderMarkdownAsHTML: RenderMarkdownAsHTML) => () => (next: any) => (...args: any[]) => any;
|
|
11
|
+
export {};
|