@microsoft/omnichannel-chat-widget 1.8.4-main.882d8f1 → 1.8.4-main.9284e84
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/telemetry/TelemetryConstants.js +1 -0
- package/lib/cjs/common/utils.js +56 -15
- package/lib/cjs/components/citationpanestateful/CitationPaneStateful.js +2 -1
- package/lib/cjs/components/confirmationpanestateful/ConfirmationPaneStateful.js +2 -1
- package/lib/cjs/components/emailtranscriptpanestateful/EmailTranscriptPaneStateful.js +2 -1
- package/lib/cjs/components/livechatwidget/common/endChat.js +4 -0
- package/lib/cjs/components/livechatwidget/common/initWebChatComposer.js +182 -1
- package/lib/cjs/components/livechatwidget/livechatwidgetstateful/LiveChatWidgetStateful.js +14 -2
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityMiddleware.js +2 -0
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachmentMiddleware.js +2 -1
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +138 -0
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.js +81 -0
- package/lib/cjs/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/localizedStringsBotInitialsMiddleware.js +9 -3
- package/lib/esm/common/telemetry/TelemetryConstants.js +1 -0
- package/lib/esm/common/utils.js +53 -13
- package/lib/esm/components/citationpanestateful/CitationPaneStateful.js +2 -1
- package/lib/esm/components/confirmationpanestateful/ConfirmationPaneStateful.js +2 -1
- package/lib/esm/components/emailtranscriptpanestateful/EmailTranscriptPaneStateful.js +2 -1
- package/lib/esm/components/livechatwidget/common/endChat.js +4 -0
- package/lib/esm/components/livechatwidget/common/initWebChatComposer.js +182 -1
- package/lib/esm/components/livechatwidget/livechatwidgetstateful/LiveChatWidgetStateful.js +15 -3
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityMiddleware.js +2 -0
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachmentMiddleware.js +2 -1
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.js +130 -0
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.js +74 -0
- package/lib/esm/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/localizedStringsBotInitialsMiddleware.js +9 -3
- package/lib/types/common/telemetry/TelemetryConstants.d.ts +1 -0
- package/lib/types/common/utils.d.ts +2 -1
- package/lib/types/components/webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/attachments/AdaptiveCardAccessibilityWrapper.d.ts +18 -0
- package/lib/types/components/webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware.d.ts +12 -0
- package/package.json +3 -2
|
@@ -202,6 +202,7 @@ exports.TelemetryEvent = TelemetryEvent;
|
|
|
202
202
|
TelemetryEvent["QueueOverflowEvent"] = "QueueOverflowEvent";
|
|
203
203
|
TelemetryEvent["ProcessingHTMLTextMiddlewareFailed"] = "ProcessingHTMLTextMiddlewareFailed";
|
|
204
204
|
TelemetryEvent["ProcessingSanitizationMiddlewareFailed"] = "ProcessingSanitizationMiddlewareFailed";
|
|
205
|
+
TelemetryEvent["HTMLSanitized"] = "HTMLSanitized";
|
|
205
206
|
TelemetryEvent["FormatTagsMiddlewareJSONStringifyFailed"] = "FormatTagsMiddlewareJSONStringifyFailed";
|
|
206
207
|
TelemetryEvent["AttachmentUploadValidatorMiddlewareFailed"] = "AttachmentUploadValidatorMiddlewareFailed";
|
|
207
208
|
TelemetryEvent["CitationMiddlewareFailed"] = "CitationMiddlewareFailed";
|
package/lib/cjs/common/utils.js
CHANGED
|
@@ -9,7 +9,7 @@ exports.getWidgetEndChatEventName = exports.getWidgetCacheIdfromProps = exports.
|
|
|
9
9
|
exports.isEndConversationDueToOverflowActivity = isEndConversationDueToOverflowActivity;
|
|
10
10
|
exports.parseAdaptiveCardPayload = exports.newGuid = exports.isValidCustomEvent = exports.isUndefinedOrEmpty = exports.isThisSessionPopout = exports.isNullOrUndefined = exports.isNullOrEmptyString = void 0;
|
|
11
11
|
exports.parseBooleanFromConfig = parseBooleanFromConfig;
|
|
12
|
-
exports.setTabIndices = exports.setOcUserAgent = exports.setFocusOnSendBox = exports.setFocusOnElement = exports.preventFocusToMoveOutOfElement = exports.parseLowerCaseString = void 0;
|
|
12
|
+
exports.setTabIndices = exports.setOcUserAgent = exports.setFocusOnSendBox = exports.setFocusOnElement = exports.setAriaHiddenForSiblings = exports.preventFocusToMoveOutOfElement = exports.parseLowerCaseString = void 0;
|
|
13
13
|
var _Constants = require("./Constants");
|
|
14
14
|
var _TelemetryConstants = require("./telemetry/TelemetryConstants");
|
|
15
15
|
var _omnichannelChatComponents = require("@microsoft/omnichannel-chat-components");
|
|
@@ -85,28 +85,69 @@ exports.findAllFocusableElement = findAllFocusableElement;
|
|
|
85
85
|
const preventFocusToMoveOutOfElement = elementId => {
|
|
86
86
|
const container = document.getElementById(elementId);
|
|
87
87
|
if (!container) {
|
|
88
|
-
return;
|
|
88
|
+
return () => {/* no-op */};
|
|
89
89
|
}
|
|
90
90
|
const focusableElements = findAllFocusableElement(container);
|
|
91
91
|
if (!focusableElements) {
|
|
92
|
-
return;
|
|
92
|
+
return () => {/* no-op */};
|
|
93
93
|
}
|
|
94
94
|
const firstFocusableElement = focusableElements[0];
|
|
95
95
|
const lastFocusableElement = focusableElements[focusableElements.length - 1];
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
96
|
+
const cleanups = [];
|
|
97
|
+
if (firstFocusableElement === lastFocusableElement) {
|
|
98
|
+
const handler = e => {
|
|
99
|
+
if (e.key === _KeyCodes.KeyCodes.TAB && !e.shiftKey) {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
firstFocusableElement.focus();
|
|
102
|
+
} else if (e.key === _KeyCodes.KeyCodes.TAB && e.shiftKey) {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
firstFocusableElement.focus();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
firstFocusableElement.addEventListener("keydown", handler);
|
|
108
|
+
cleanups.push(() => firstFocusableElement.removeEventListener("keydown", handler));
|
|
109
|
+
} else {
|
|
110
|
+
const firstHandler = e => {
|
|
111
|
+
if (e.shiftKey && e.key === _KeyCodes.KeyCodes.TAB) {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
lastFocusableElement === null || lastFocusableElement === void 0 ? void 0 : lastFocusableElement.focus();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
firstFocusableElement.addEventListener("keydown", firstHandler);
|
|
117
|
+
cleanups.push(() => firstFocusableElement.removeEventListener("keydown", firstHandler));
|
|
118
|
+
const lastHandler = e => {
|
|
119
|
+
if (!e.shiftKey && e.key === _KeyCodes.KeyCodes.TAB) {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
firstFocusableElement === null || firstFocusableElement === void 0 ? void 0 : firstFocusableElement.focus();
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
lastFocusableElement.addEventListener("keydown", lastHandler);
|
|
125
|
+
cleanups.push(() => lastFocusableElement.removeEventListener("keydown", lastHandler));
|
|
126
|
+
}
|
|
127
|
+
return () => cleanups.forEach(fn => fn());
|
|
108
128
|
};
|
|
109
129
|
exports.preventFocusToMoveOutOfElement = preventFocusToMoveOutOfElement;
|
|
130
|
+
const setAriaHiddenForSiblings = (elementId, shouldHide, stateMap) => {
|
|
131
|
+
const element = document.getElementById(elementId);
|
|
132
|
+
if (!(element !== null && element !== void 0 && element.parentElement)) return;
|
|
133
|
+
Array.from(element.parentElement.children).forEach(sibling => {
|
|
134
|
+
if (sibling !== element) {
|
|
135
|
+
if (shouldHide) {
|
|
136
|
+
stateMap.set(sibling, sibling.getAttribute("aria-hidden"));
|
|
137
|
+
sibling.setAttribute("aria-hidden", "true");
|
|
138
|
+
} else if (stateMap.has(sibling)) {
|
|
139
|
+
const original = stateMap.get(sibling);
|
|
140
|
+
if (original === null) {
|
|
141
|
+
sibling.removeAttribute("aria-hidden");
|
|
142
|
+
} else {
|
|
143
|
+
sibling.setAttribute("aria-hidden", original);
|
|
144
|
+
}
|
|
145
|
+
stateMap.delete(sibling);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
exports.setAriaHiddenForSiblings = setAriaHiddenForSiblings;
|
|
110
151
|
const setFocusOnSendBox = () => {
|
|
111
152
|
const sendBoxSelector = "textarea[data-id=\"webchat-sendbox-input\"]";
|
|
112
153
|
setFocusOnElement(sendBoxSelector);
|
|
@@ -44,7 +44,7 @@ const CitationPaneStateful = props => {
|
|
|
44
44
|
|
|
45
45
|
// Initial focus pattern (mirrors ConfirmationPaneStateful): focus first focusable element (will re-attempt after visibility becomes true)
|
|
46
46
|
(0, _react.useEffect)(() => {
|
|
47
|
-
(0, _utils.preventFocusToMoveOutOfElement)(controlId);
|
|
47
|
+
const cleanup = (0, _utils.preventFocusToMoveOutOfElement)(controlId);
|
|
48
48
|
const focusableElements = (0, _utils.findAllFocusableElement)(`#${controlId}`);
|
|
49
49
|
requestAnimationFrame(() => {
|
|
50
50
|
if (focusableElements && focusableElements.length > 0 && focusableElements[0]) {
|
|
@@ -62,6 +62,7 @@ const CitationPaneStateful = props => {
|
|
|
62
62
|
Event: _TelemetryConstants.TelemetryEvent.UXCitationPaneCompleted,
|
|
63
63
|
ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
|
|
64
64
|
});
|
|
65
|
+
return cleanup;
|
|
65
66
|
}, []);
|
|
66
67
|
|
|
67
68
|
// Retry focus once pane is actually visible (isReady) in case initial attempt occurred while wrapper was visibility:hidden
|
|
@@ -82,7 +82,7 @@ const ConfirmationPaneStateful = props => {
|
|
|
82
82
|
|
|
83
83
|
// Move focus to the first button
|
|
84
84
|
(0, _react.useEffect)(() => {
|
|
85
|
-
(0, _utils.preventFocusToMoveOutOfElement)(controlProps.id);
|
|
85
|
+
const cleanup = (0, _utils.preventFocusToMoveOutOfElement)(controlProps.id);
|
|
86
86
|
const focusableElements = (0, _utils.findAllFocusableElement)(`#${controlProps.id}`);
|
|
87
87
|
requestAnimationFrame(() => {
|
|
88
88
|
if (focusableElements && focusableElements.length > 0 && focusableElements[0]) {
|
|
@@ -100,6 +100,7 @@ const ConfirmationPaneStateful = props => {
|
|
|
100
100
|
Event: _TelemetryConstants.TelemetryEvent.UXConfirmationPaneCompleted,
|
|
101
101
|
ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
|
|
102
102
|
});
|
|
103
|
+
return cleanup;
|
|
103
104
|
}, []);
|
|
104
105
|
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_DimLayer.DimLayer, {
|
|
105
106
|
brightness: (controlProps === null || controlProps === void 0 ? void 0 : controlProps.brightnessValueOnDim) ?? "0.2"
|
|
@@ -103,7 +103,7 @@ const EmailTranscriptPaneStateful = props => {
|
|
|
103
103
|
|
|
104
104
|
// Move focus to the first button
|
|
105
105
|
(0, _react.useEffect)(() => {
|
|
106
|
-
(0, _utils.preventFocusToMoveOutOfElement)(controlProps.id);
|
|
106
|
+
const cleanup = (0, _utils.preventFocusToMoveOutOfElement)(controlProps.id);
|
|
107
107
|
const focusableElements = (0, _utils.findAllFocusableElement)(`#${controlProps.id}`);
|
|
108
108
|
if (focusableElements) {
|
|
109
109
|
focusableElements[0].focus();
|
|
@@ -118,6 +118,7 @@ const EmailTranscriptPaneStateful = props => {
|
|
|
118
118
|
Event: _TelemetryConstants.TelemetryEvent.UXEmailTranscriptPaneCompleted,
|
|
119
119
|
ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
|
|
120
120
|
});
|
|
121
|
+
return cleanup;
|
|
121
122
|
}, [initialEmail]);
|
|
122
123
|
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_DimLayer.DimLayer, {
|
|
123
124
|
brightness: (controlProps === null || controlProps === void 0 ? void 0 : controlProps.brightnessValueOnDim) ?? "0.2"
|
|
@@ -346,6 +346,10 @@ const closeChatStateCleanUp = dispatch => {
|
|
|
346
346
|
type: _LiveChatWidgetActionType.LiveChatWidgetActionType.SET_CITATIONS,
|
|
347
347
|
payload: {}
|
|
348
348
|
});
|
|
349
|
+
dispatch({
|
|
350
|
+
type: _LiveChatWidgetActionType.LiveChatWidgetActionType.SET_SHOW_EMAIL_TRANSCRIPT_PANE,
|
|
351
|
+
payload: false
|
|
352
|
+
});
|
|
349
353
|
|
|
350
354
|
// Dismiss the chat disconnect notification banner if it was shown
|
|
351
355
|
_NotificationHandler.NotificationHandler.dismissNotification(_NotificationScenarios.NotificationScenarios.ChatDisconnect);
|
|
@@ -16,6 +16,7 @@ var _LiveChatWidgetActionType = require("../../../contexts/common/LiveChatWidget
|
|
|
16
16
|
var _TelemetryHelper = require("../../../common/telemetry/TelemetryHelper");
|
|
17
17
|
var _WebChatStoreLoader = require("../../webchatcontainerstateful/webchatcontroller/WebChatStoreLoader");
|
|
18
18
|
var _attachmentProcessingMiddleware = _interopRequireDefault(require("../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentProcessingMiddleware"));
|
|
19
|
+
var _attachmentSentAnnouncementMiddleware = _interopRequireDefault(require("../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/attachmentSentAnnouncementMiddleware"));
|
|
19
20
|
var _channelDataMiddleware = _interopRequireDefault(require("../../webchatcontainerstateful/webchatcontroller/middlewares/storemiddlewares/channelDataMiddleware"));
|
|
20
21
|
var _activityMiddleware = require("../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityMiddleware");
|
|
21
22
|
var _activityStatusMiddleware = require("../../webchatcontainerstateful/webchatcontroller/middlewares/renderingmiddlewares/activityStatusMiddleware");
|
|
@@ -125,7 +126,7 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
|
|
|
125
126
|
};
|
|
126
127
|
webChatStore = (0, _botframeworkWebchat.createStore)({},
|
|
127
128
|
//initial state
|
|
128
|
-
_preProcessingMiddleware.default, _attachmentProcessingMiddleware.default, (0, _attachmentUploadValidatorMiddleware.default)((_state$domainStates$l = state.domainStates.liveChatConfig) === null || _state$domainStates$l === void 0 ? void 0 : _state$domainStates$l.allowedFileExtensions, (_state$domainStates$l2 = state.domainStates.liveChatConfig) === null || _state$domainStates$l2 === void 0 ? void 0 : _state$domainStates$l2.maxUploadFileSize, localizedTexts), (0, _customEventMiddleware.default)(_omnichannelChatComponents.BroadcastService), (0, _queueOverflowHandlerMiddleware.createQueueOverflowMiddleware)(state, dispatch), (0, _channelDataMiddleware.default)(addConversationalSurveyTagsCallback), (0, _conversationEndMiddleware.default)(conversationEndCallback, startConversationalSurveyCallback, endConversationalSurveyCallback), (0, _dataMaskingMiddleware.default)((_state$domainStates$l3 = state.domainStates.liveChatConfig) === null || _state$domainStates$l3 === void 0 ? void 0 : _state$domainStates$l3.DataMaskingInfo), _messageTimestampMiddleware.createMessageTimeStampMiddleware, _messageSequenceIdOverrideMiddleware.createMessageSequenceIdOverrideMiddleware, (0, _citationsMiddleware.createCitationsMiddleware)(state, dispatch), _gifUploadMiddleware.default, _htmlPlayerMiddleware.default, (0, _htmlTextMiddleware.default)(honorsTargetInHTMLLinks), (0, _maxMessageSizeValidator.default)(localizedTexts), _sanitizationMiddleware.default, (0, _callActionMiddleware.default)(),
|
|
129
|
+
_preProcessingMiddleware.default, _attachmentProcessingMiddleware.default, (0, _attachmentUploadValidatorMiddleware.default)((_state$domainStates$l = state.domainStates.liveChatConfig) === null || _state$domainStates$l === void 0 ? void 0 : _state$domainStates$l.allowedFileExtensions, (_state$domainStates$l2 = state.domainStates.liveChatConfig) === null || _state$domainStates$l2 === void 0 ? void 0 : _state$domainStates$l2.maxUploadFileSize, localizedTexts), _attachmentSentAnnouncementMiddleware.default, (0, _customEventMiddleware.default)(_omnichannelChatComponents.BroadcastService), (0, _queueOverflowHandlerMiddleware.createQueueOverflowMiddleware)(state, dispatch), (0, _channelDataMiddleware.default)(addConversationalSurveyTagsCallback), (0, _conversationEndMiddleware.default)(conversationEndCallback, startConversationalSurveyCallback, endConversationalSurveyCallback), (0, _dataMaskingMiddleware.default)((_state$domainStates$l3 = state.domainStates.liveChatConfig) === null || _state$domainStates$l3 === void 0 ? void 0 : _state$domainStates$l3.DataMaskingInfo), _messageTimestampMiddleware.createMessageTimeStampMiddleware, _messageSequenceIdOverrideMiddleware.createMessageSequenceIdOverrideMiddleware, (0, _citationsMiddleware.createCitationsMiddleware)(state, dispatch), _gifUploadMiddleware.default, _htmlPlayerMiddleware.default, (0, _htmlTextMiddleware.default)(honorsTargetInHTMLLinks), (0, _maxMessageSizeValidator.default)(localizedTexts), _sanitizationMiddleware.default, (0, _callActionMiddleware.default)(),
|
|
129
130
|
// Pass a callback so middleware can push initials into React context for reactivity
|
|
130
131
|
(0, _localizedStringsBotInitialsMiddleware.localizedStringsBotInitialsMiddleware)(initials => {
|
|
131
132
|
dispatch({
|
|
@@ -151,14 +152,194 @@ const initWebChatComposer = (props, state, dispatch, facadeChatSDK, endChat) =>
|
|
|
151
152
|
markdownRenderers.forEach(renderer => {
|
|
152
153
|
text = renderer.render(text);
|
|
153
154
|
});
|
|
155
|
+
|
|
156
|
+
// EXISTING sanitization (continues to work as before)
|
|
154
157
|
const config = {
|
|
155
158
|
FORBID_TAGS: ["form", "button", "script", "div", "input"],
|
|
156
159
|
FORBID_ATTR: ["action"],
|
|
157
160
|
ADD_ATTR: ["target"]
|
|
158
161
|
};
|
|
159
162
|
text = _dompurify.default.sanitize(text, config);
|
|
163
|
+
|
|
164
|
+
// MONITOR-ONLY: Test what the stricter allowlist would remove (Phase 1)
|
|
165
|
+
// This does NOT modify the text, only logs telemetry
|
|
166
|
+
// Run during browser idle time to avoid blocking message flow and adding latency
|
|
167
|
+
const textToMonitor = text; // Capture current text value
|
|
168
|
+
|
|
169
|
+
// Schedule monitoring to run during browser idle time
|
|
170
|
+
const scheduleMonitoring = () => {
|
|
171
|
+
try {
|
|
172
|
+
monitorStrictSanitization(textToMonitor, state);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
// Silently catch errors to prevent blocking message flow
|
|
175
|
+
if (process.env.NODE_ENV === "development") {
|
|
176
|
+
console.error("[Monitor] HTML sanitization monitoring failed:", error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Use requestIdleCallback for truly idle execution, fallback to setTimeout for older browsers
|
|
182
|
+
if (typeof window !== "undefined" && "requestIdleCallback" in window) {
|
|
183
|
+
window.requestIdleCallback(scheduleMonitoring);
|
|
184
|
+
} else {
|
|
185
|
+
setTimeout(scheduleMonitoring, 0);
|
|
186
|
+
}
|
|
160
187
|
return text;
|
|
161
188
|
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Monitor-only sanitization (Phase 1: Gather telemetry)
|
|
192
|
+
* Tests what a stricter allowlist-based sanitization would remove
|
|
193
|
+
* WITHOUT actually removing it. Logs telemetry for analysis.
|
|
194
|
+
*
|
|
195
|
+
* IMPORTANT: This function is wrapped in try-catch and runs asynchronously
|
|
196
|
+
* to ensure failures don't block message flow or add latency.
|
|
197
|
+
*
|
|
198
|
+
* ISOLATION: Uses a separate DOMPurify instance to completely isolate
|
|
199
|
+
* monitoring hooks from other sanitization paths (e.g., postDomPurifyActivities).
|
|
200
|
+
* The instance is garbage collected after use, no cleanup needed.
|
|
201
|
+
*
|
|
202
|
+
* @param html - The HTML text that was already sanitized with existing config
|
|
203
|
+
* @param state - Widget state containing orgId and chatId
|
|
204
|
+
*/
|
|
205
|
+
const monitorStrictSanitization = (html, state) => {
|
|
206
|
+
// Early exit for empty content
|
|
207
|
+
if (!html) return;
|
|
208
|
+
|
|
209
|
+
// Track execution time for performance monitoring
|
|
210
|
+
const startTime = performance.now();
|
|
211
|
+
try {
|
|
212
|
+
// Create a separate DOMPurify instance for monitoring
|
|
213
|
+
// This completely isolates monitoring from other sanitization paths
|
|
214
|
+
const monitorDOMPurify = (0, _dompurify.default)(window);
|
|
215
|
+
|
|
216
|
+
// Strict allowlist configuration (proposed new rules)
|
|
217
|
+
// Note: DOMPurify blocks event handlers (onclick, onerror, etc.) by default
|
|
218
|
+
const strictConfig = {
|
|
219
|
+
ALLOWED_TAGS: ["b", "strong",
|
|
220
|
+
// Bold text
|
|
221
|
+
"i", "em", "u",
|
|
222
|
+
// Italic, emphasis, underline
|
|
223
|
+
"br", "p",
|
|
224
|
+
// Line breaks and paragraphs
|
|
225
|
+
"ul", "ol", "li",
|
|
226
|
+
// Lists
|
|
227
|
+
"a" // Links (with restricted attributes)
|
|
228
|
+
],
|
|
229
|
+
|
|
230
|
+
ALLOWED_ATTR: ["href",
|
|
231
|
+
// For links (will be restricted to http/https)
|
|
232
|
+
"target",
|
|
233
|
+
// For link behavior
|
|
234
|
+
"rel" // For security (noopener, noreferrer)
|
|
235
|
+
],
|
|
236
|
+
|
|
237
|
+
FORBID_TAGS: ["img", "video", "audio",
|
|
238
|
+
// Media (tracking beacons)
|
|
239
|
+
"iframe", "object", "embed",
|
|
240
|
+
// Embedded content
|
|
241
|
+
"script", "style",
|
|
242
|
+
// Script and styling
|
|
243
|
+
"form", "input", "textarea", "button",
|
|
244
|
+
// Form elements
|
|
245
|
+
"link", "meta", "base",
|
|
246
|
+
// Document metadata
|
|
247
|
+
"div", "span" // Layout elements
|
|
248
|
+
],
|
|
249
|
+
|
|
250
|
+
FORBID_ATTR: ["style",
|
|
251
|
+
// Inline CSS
|
|
252
|
+
"action" // Form action attribute (event handlers blocked by default)
|
|
253
|
+
],
|
|
254
|
+
|
|
255
|
+
ALLOWED_URI_REGEXP: /^https?:/i,
|
|
256
|
+
ALLOW_DATA_ATTR: false,
|
|
257
|
+
ALLOW_UNKNOWN_PROTOCOLS: false
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Track what would be removed
|
|
261
|
+
const removedTags = [];
|
|
262
|
+
const removedAttributes = [];
|
|
263
|
+
|
|
264
|
+
// Add hooks to the isolated monitoring instance
|
|
265
|
+
monitorDOMPurify.addHook("uponSanitizeElement", (node, data) => {
|
|
266
|
+
try {
|
|
267
|
+
const tagName = data.tagName.toLowerCase();
|
|
268
|
+
// Filter out "body" tag which is DOMPurify's internal wrapper
|
|
269
|
+
if (node.nodeType === 1 && !strictConfig.ALLOWED_TAGS.includes(tagName) && tagName !== "body") {
|
|
270
|
+
removedTags.push(tagName);
|
|
271
|
+
}
|
|
272
|
+
} catch (hookError) {
|
|
273
|
+
// Silently ignore hook errors
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
monitorDOMPurify.addHook("uponSanitizeAttribute", (node, data) => {
|
|
277
|
+
try {
|
|
278
|
+
const attrName = data.attrName.toLowerCase();
|
|
279
|
+
if (!strictConfig.ALLOWED_ATTR.includes(attrName) && attrName !== "class" && attrName !== "id") {
|
|
280
|
+
removedAttributes.push(attrName);
|
|
281
|
+
}
|
|
282
|
+
} catch (hookError) {
|
|
283
|
+
// Silently ignore hook errors
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Run sanitization on the isolated instance (we discard the result)
|
|
288
|
+
// No cleanup needed - the instance will be garbage collected with its hooks
|
|
289
|
+
monitorDOMPurify.sanitize(html, strictConfig);
|
|
290
|
+
|
|
291
|
+
// Log telemetry if content would be affected by strict rules
|
|
292
|
+
if (removedTags.length > 0 || removedAttributes.length > 0) {
|
|
293
|
+
try {
|
|
294
|
+
var _state$domainStates, _state$domainStates$t, _state$domainStates2, _state$domainStates2$;
|
|
295
|
+
const uniqueTags = [...new Set(removedTags)];
|
|
296
|
+
const uniqueAttrs = [...new Set(removedAttributes)];
|
|
297
|
+
|
|
298
|
+
// Calculate execution time
|
|
299
|
+
const endTime = performance.now();
|
|
300
|
+
const executionTimeMs = Math.round((endTime - startTime) * 100) / 100; // Round to 2 decimal places
|
|
301
|
+
|
|
302
|
+
// Get context for telemetry (with safe fallbacks)
|
|
303
|
+
const orgId = (state === null || state === void 0 ? void 0 : (_state$domainStates = state.domainStates) === null || _state$domainStates === void 0 ? void 0 : (_state$domainStates$t = _state$domainStates.telemetryInternalData) === null || _state$domainStates$t === void 0 ? void 0 : _state$domainStates$t.orgId) || "unknown";
|
|
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
|
+
_TelemetryHelper.TelemetryHelper.logActionEvent(_TelemetryConstants.LogLevel.INFO, {
|
|
306
|
+
Event: _TelemetryConstants.TelemetryEvent.HTMLSanitized,
|
|
307
|
+
Description: "HTML content would be sanitized by stricter allowlist (monitor-only)",
|
|
308
|
+
ElapsedTimeInMilliseconds: executionTimeMs,
|
|
309
|
+
CustomProperties: {
|
|
310
|
+
OrganizationId: orgId,
|
|
311
|
+
ConversationId: conversationId,
|
|
312
|
+
RemovedTags: uniqueTags.join(", "),
|
|
313
|
+
RemovedAttributes: uniqueAttrs.join(", "),
|
|
314
|
+
Phase: "Monitor"
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Log to console in development for debugging
|
|
319
|
+
if (process.env.NODE_ENV === "development") {
|
|
320
|
+
console.warn("[Monitor] Stricter HTML sanitization would remove:", {
|
|
321
|
+
orgId,
|
|
322
|
+
conversationId,
|
|
323
|
+
removedTags: uniqueTags,
|
|
324
|
+
removedAttributes: uniqueAttrs,
|
|
325
|
+
executionTimeMs
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
} catch (telemetryError) {
|
|
329
|
+
// Silently ignore telemetry errors to prevent blocking
|
|
330
|
+
if (process.env.NODE_ENV === "development") {
|
|
331
|
+
console.error("[Monitor] Telemetry logging failed:", telemetryError);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} catch (error) {
|
|
336
|
+
// Catch-all for any unexpected errors
|
|
337
|
+
// Silently fail to ensure monitoring never blocks message flow
|
|
338
|
+
if (process.env.NODE_ENV === "development") {
|
|
339
|
+
console.error("[Monitor] Monitoring failed:", error);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
};
|
|
162
343
|
function postDomPurifyActivities() {
|
|
163
344
|
_dompurify.default.addHook("afterSanitizeAttributes", function (node) {
|
|
164
345
|
const target = node.getAttribute(_Constants.Constants.Target);
|
|
@@ -66,7 +66,7 @@ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj &&
|
|
|
66
66
|
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
67
67
|
let uiTimer;
|
|
68
68
|
const LiveChatWidgetStateful = props => {
|
|
69
|
-
var _props$webChatContain, _props$webChatContain2, _props$webChatContain3, _props$webChatContain4, _props$webChatContain5, _props$webChatContain6, _props$webChatContain7, _props$webChatContain8, _props$webChatContain9, _props$styleProps, _props$webChatContain10, _props$webChatContain11, _props$controlProps, _props$controlProps3, _state$appStates7, _props$webChatContain15, _state$appStates8, _props$webChatContain17, _props$webChatContain18, _props$controlProps12, _props$draggableChatW, _props$draggableChatW2, _props$draggableChatW3, _props$draggableChatW4, _props$draggableChatW5, _livechatProps$webCha, _livechatProps$styleP, _livechatProps$contro, _livechatProps$contro2, _livechatProps$compon, _livechatProps$contro3, _livechatProps$compon2, _livechatProps$contro4, _livechatProps$compon3, _livechatProps$contro5, _livechatProps$compon4, _livechatProps$contro6, _livechatProps$compon5, _livechatProps$contro7, _livechatProps$compon6, _livechatProps$contro8, _livechatProps$compon7, _livechatProps$contro9, _livechatProps$compon8, _livechatProps$contro10, _livechatProps$contro11, _livechatProps$compon9, _livechatProps$contro12, _livechatProps$compon10, _livechatProps$contro13, _livechatProps$compon11, _livechatProps$compon12, _livechatProps$compon13;
|
|
69
|
+
var _props$webChatContain, _props$webChatContain2, _props$webChatContain3, _props$webChatContain4, _props$webChatContain5, _props$webChatContain6, _props$webChatContain7, _props$webChatContain8, _props$webChatContain9, _props$styleProps, _props$webChatContain10, _props$webChatContain11, _props$controlProps, _props$controlProps3, _state$appStates7, _props$webChatContain15, _state$appStates8, _props$webChatContain17, _props$webChatContain18, _props$controlProps12, _props$draggableChatW, _props$draggableChatW2, _props$draggableChatW3, _props$draggableChatW4, _props$draggableChatW5, _livechatProps$webCha, _livechatProps$styleP, _props$headerProps, _props$headerProps$co, _props$headerProps$co2, _livechatProps$contro, _livechatProps$contro2, _livechatProps$compon, _livechatProps$contro3, _livechatProps$compon2, _livechatProps$contro4, _livechatProps$compon3, _livechatProps$contro5, _livechatProps$compon4, _livechatProps$contro6, _livechatProps$compon5, _livechatProps$contro7, _livechatProps$compon6, _livechatProps$contro8, _livechatProps$compon7, _livechatProps$contro9, _livechatProps$compon8, _livechatProps$contro10, _livechatProps$contro11, _livechatProps$compon9, _livechatProps$contro12, _livechatProps$compon10, _livechatProps$contro13, _livechatProps$compon11, _livechatProps$compon12, _livechatProps$compon13;
|
|
70
70
|
(0, _react2.useEffect)(() => {
|
|
71
71
|
uiTimer = (0, _utils.createTimer)();
|
|
72
72
|
_TelemetryHelper.TelemetryHelper.logLoadingEventToAllTelemetry(_TelemetryConstants.LogLevel.INFO, {
|
|
@@ -1029,6 +1029,15 @@ const LiveChatWidgetStateful = props => {
|
|
|
1029
1029
|
}));
|
|
1030
1030
|
}
|
|
1031
1031
|
}, [state.domainStates.botAvatarInitials]);
|
|
1032
|
+
const isWidgetOpen = !state.appStates.isMinimized && state.appStates.conversationState !== _ConversationState.ConversationState.Closed;
|
|
1033
|
+
const siblingAriaHiddenMapRef = (0, _react2.useRef)(new Map());
|
|
1034
|
+
(0, _react2.useEffect)(() => {
|
|
1035
|
+
const map = siblingAriaHiddenMapRef.current;
|
|
1036
|
+
(0, _utils.setAriaHiddenForSiblings)(widgetElementId, isWidgetOpen, map);
|
|
1037
|
+
return () => {
|
|
1038
|
+
(0, _utils.setAriaHiddenForSiblings)(widgetElementId, false, map);
|
|
1039
|
+
};
|
|
1040
|
+
}, [isWidgetOpen]);
|
|
1032
1041
|
|
|
1033
1042
|
// WebChat's Composer can only be rendered if a directLine object is defined
|
|
1034
1043
|
return directLine && /*#__PURE__*/_react2.default.createElement(_react2.default.Fragment, null, /*#__PURE__*/_react2.default.createElement("style", null, `
|
|
@@ -1152,7 +1161,10 @@ const LiveChatWidgetStateful = props => {
|
|
|
1152
1161
|
}), /*#__PURE__*/_react2.default.createElement(_react.Stack, {
|
|
1153
1162
|
id: widgetElementId,
|
|
1154
1163
|
styles: generalStyles,
|
|
1155
|
-
className: (_livechatProps$styleP = livechatProps.styleProps) === null || _livechatProps$styleP === void 0 ? void 0 : _livechatProps$styleP.className
|
|
1164
|
+
className: (_livechatProps$styleP = livechatProps.styleProps) === null || _livechatProps$styleP === void 0 ? void 0 : _livechatProps$styleP.className,
|
|
1165
|
+
role: isWidgetOpen ? "dialog" : undefined,
|
|
1166
|
+
"aria-modal": isWidgetOpen ? true : undefined,
|
|
1167
|
+
"aria-label": isWidgetOpen ? ((_props$headerProps = props.headerProps) === null || _props$headerProps === void 0 ? void 0 : (_props$headerProps$co = _props$headerProps.controlProps) === null || _props$headerProps$co === void 0 ? void 0 : (_props$headerProps$co2 = _props$headerProps$co.headerTitleProps) === null || _props$headerProps$co2 === void 0 ? void 0 : _props$headerProps$co2.text) ?? "Live Chat" : undefined
|
|
1156
1168
|
}, !((_livechatProps$contro = livechatProps.controlProps) !== null && _livechatProps$contro !== void 0 && _livechatProps$contro.hideChatButton) && !((_livechatProps$contro2 = livechatProps.controlProps) !== null && _livechatProps$contro2 !== void 0 && _livechatProps$contro2.hideStartChatButton) && (0, _componentController.shouldShowChatButton)(state) && ((0, _omnichannelChatComponents.decodeComponentString)((_livechatProps$compon = livechatProps.componentOverrides) === null || _livechatProps$compon === void 0 ? void 0 : _livechatProps$compon.chatButton) || /*#__PURE__*/_react2.default.createElement(_ChatButtonStateful.default, {
|
|
1157
1169
|
buttonProps: livechatProps.chatButtonProps,
|
|
1158
1170
|
outOfOfficeButtonProps: livechatProps.outOfOfficeChatButtonProps,
|
|
@@ -65,6 +65,8 @@ const handleSystemMessage = (next, args, card, renderMarkdown, systemMessageStyl
|
|
|
65
65
|
originalSystemMessageTexts.set(activityKey, card.activity.text);
|
|
66
66
|
}
|
|
67
67
|
const sourceText = (activityKey && originalSystemMessageTexts.get(activityKey)) ?? card.activity.text;
|
|
68
|
+
// renderMarkdown sanitizes the HTML using DOMPurify with allowlist-based configuration
|
|
69
|
+
// See initWebChatComposer.ts for sanitization details
|
|
68
70
|
const renderedHtml = renderMarkdown(sourceText);
|
|
69
71
|
// eslint-disable-next-line react/display-name
|
|
70
72
|
return () => /*#__PURE__*/_react.default.createElement("div", {
|
|
@@ -7,6 +7,7 @@ exports.createAttachmentMiddleware = void 0;
|
|
|
7
7
|
var _Constants = require("../../../../../common/Constants");
|
|
8
8
|
var _react = _interopRequireDefault(require("react"));
|
|
9
9
|
var _FileAttachmentIconManager = require("../../../common/utils/FileAttachmentIconManager");
|
|
10
|
+
var _AdaptiveCardAccessibilityWrapper = _interopRequireDefault(require("./attachments/AdaptiveCardAccessibilityWrapper"));
|
|
10
11
|
var _Attachment = _interopRequireDefault(require("./attachments/Attachment"));
|
|
11
12
|
var _TelemetryConstants = require("../../../../../common/telemetry/TelemetryConstants");
|
|
12
13
|
var _omnichannelChatComponents = require("@microsoft/omnichannel-chat-components");
|
|
@@ -120,7 +121,7 @@ const createAttachmentMiddleware = enableInlinePlaying => {
|
|
|
120
121
|
...((_state$domainStates$r3 = state.domainStates.renderingMiddlewareProps) === null || _state$domainStates$r3 === void 0 ? void 0 : _state$domainStates$r3.attachmentAdaptiveCardStyles)
|
|
121
122
|
};
|
|
122
123
|
if (type === _Constants.WebChatMiddlewareConstants.adaptiveCard || _Constants.Constants.supportedAdaptiveCardContentTypes.indexOf(contentType) >= 0) {
|
|
123
|
-
return /*#__PURE__*/_react.default.createElement(
|
|
124
|
+
return /*#__PURE__*/_react.default.createElement(_AdaptiveCardAccessibilityWrapper.default, {
|
|
124
125
|
id: attachmentId,
|
|
125
126
|
style: atttachmentAdaptiveCardStyles
|
|
126
127
|
}, next(...args));
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
8
|
+
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); }
|
|
9
|
+
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; }
|
|
10
|
+
/******
|
|
11
|
+
* AdaptiveCardAccessibilityWrapper
|
|
12
|
+
*
|
|
13
|
+
* The `adaptivecards` renderer does not set `aria-setsize` or `aria-posinset`
|
|
14
|
+
* on radio buttons rendered by Input.ChoiceSet (style: "expanded"). Without
|
|
15
|
+
* these attributes, screen readers (TalkBack, VoiceOver, NVDA) cannot announce
|
|
16
|
+
* the correct option count (e.g. "1 of 2") and may say "1 of 1" for every item.
|
|
17
|
+
*
|
|
18
|
+
* This wrapper observes the rendered card via MutationObserver and patches each
|
|
19
|
+
* radio group with the correct ARIA attributes after the card finishes rendering.
|
|
20
|
+
******/
|
|
21
|
+
|
|
22
|
+
const AdaptiveCardAccessibilityWrapper = _ref => {
|
|
23
|
+
let {
|
|
24
|
+
id,
|
|
25
|
+
style,
|
|
26
|
+
children
|
|
27
|
+
} = _ref;
|
|
28
|
+
const containerRef = (0, _react.useRef)(null);
|
|
29
|
+
(0, _react.useEffect)(() => {
|
|
30
|
+
if (!containerRef.current) return;
|
|
31
|
+
const patchRadioGroups = () => {
|
|
32
|
+
const container = containerRef.current;
|
|
33
|
+
if (!container) return;
|
|
34
|
+
|
|
35
|
+
// Find all radio inputs inside the adaptive card
|
|
36
|
+
const radios = container.querySelectorAll("input[type='radio']");
|
|
37
|
+
if (radios.length === 0) return;
|
|
38
|
+
|
|
39
|
+
// Group radios by their name attribute (each ChoiceSet has a unique name)
|
|
40
|
+
const groups = new Map();
|
|
41
|
+
radios.forEach(radio => {
|
|
42
|
+
var _groups$get;
|
|
43
|
+
const name = radio.getAttribute("name") || "__default";
|
|
44
|
+
if (!groups.has(name)) {
|
|
45
|
+
groups.set(name, []);
|
|
46
|
+
}
|
|
47
|
+
(_groups$get = groups.get(name)) === null || _groups$get === void 0 ? void 0 : _groups$get.push(radio);
|
|
48
|
+
});
|
|
49
|
+
groups.forEach(radioInputs => {
|
|
50
|
+
const groupSize = radioInputs.length;
|
|
51
|
+
radioInputs.forEach((radio, index) => {
|
|
52
|
+
radio.setAttribute("aria-setsize", String(groupSize));
|
|
53
|
+
radio.setAttribute("aria-posinset", String(index + 1));
|
|
54
|
+
|
|
55
|
+
// Ensure the parent container has radiogroup role
|
|
56
|
+
const parent = radio.closest("[role='radiogroup']") || radio.closest(".ac-input-container");
|
|
57
|
+
if (parent && !parent.getAttribute("role")) {
|
|
58
|
+
parent.setAttribute("role", "radiogroup");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The adaptive card renders each choice as:
|
|
62
|
+
// <div style="display:flex"> ← flex wrapper
|
|
63
|
+
// <input type="radio">
|
|
64
|
+
// <div style="width:6px"></div> ← spacer
|
|
65
|
+
// <label><div><p>Text</p></div></label>
|
|
66
|
+
// </div>
|
|
67
|
+
//
|
|
68
|
+
// TalkBack focuses on the <label> text node (just "Standard")
|
|
69
|
+
// instead of the <input>, so it never announces "radio button" or count.
|
|
70
|
+
//
|
|
71
|
+
// Fix: hide the label and spacer from the accessibility tree so TalkBack
|
|
72
|
+
// lands directly on the <input> which already has aria-label,
|
|
73
|
+
// aria-setsize and aria-posinset. The input's aria-labelledby still
|
|
74
|
+
// resolves the accessible name from the hidden label per ARIA spec.
|
|
75
|
+
const flexWrapper = radio.parentElement;
|
|
76
|
+
if (flexWrapper) {
|
|
77
|
+
var _label$textContent;
|
|
78
|
+
// Hide label sibling — only set if not already applied to
|
|
79
|
+
// avoid re-triggering the attributeFilter observer in a loop
|
|
80
|
+
const label = flexWrapper.querySelector("label");
|
|
81
|
+
if (label && label.getAttribute("aria-hidden") !== "true") {
|
|
82
|
+
label.setAttribute("aria-hidden", "true");
|
|
83
|
+
}
|
|
84
|
+
// Hide spacer siblings (any non-input, non-label divs)
|
|
85
|
+
Array.from(flexWrapper.children).forEach(child => {
|
|
86
|
+
const tag = child.tagName.toLowerCase();
|
|
87
|
+
if (tag !== "input" && tag !== "label" && child.getAttribute("aria-hidden") !== "true") {
|
|
88
|
+
child.setAttribute("aria-hidden", "true");
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// TalkBack reads content-desc from the Android accessibility node,
|
|
93
|
+
// which Chrome populates from aria-label. However, aria-labelledby
|
|
94
|
+
// (set by the adaptive card renderer) takes precedence over aria-label.
|
|
95
|
+
// Since we hide the label (aria-hidden=true), Chrome can no longer
|
|
96
|
+
// resolve aria-labelledby and returns empty content-desc.
|
|
97
|
+
//
|
|
98
|
+
// Fix: remove aria-labelledby so Chrome falls back to aria-label,
|
|
99
|
+
// then encode the position count into aria-label so TalkBack announces
|
|
100
|
+
// e.g. "Standard, 1 of 2, radio button, not checked".
|
|
101
|
+
if (radio.hasAttribute("aria-labelledby")) {
|
|
102
|
+
radio.removeAttribute("aria-labelledby");
|
|
103
|
+
}
|
|
104
|
+
const labelText = (label === null || label === void 0 ? void 0 : (_label$textContent = label.textContent) === null || _label$textContent === void 0 ? void 0 : _label$textContent.trim()) || radio.getAttribute("aria-label") || "";
|
|
105
|
+
const expectedLabel = `${labelText}, ${index + 1} of ${groupSize}`;
|
|
106
|
+
if (radio.getAttribute("aria-label") !== expectedLabel) {
|
|
107
|
+
radio.setAttribute("aria-label", expectedLabel);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Use MutationObserver to patch after WebChat finishes rendering the card.
|
|
115
|
+
// Also watch attribute changes on aria-hidden so we can re-apply if React
|
|
116
|
+
// reconciliation removes it from labels.
|
|
117
|
+
const observer = new MutationObserver(() => {
|
|
118
|
+
patchRadioGroups();
|
|
119
|
+
});
|
|
120
|
+
observer.observe(containerRef.current, {
|
|
121
|
+
childList: true,
|
|
122
|
+
subtree: true,
|
|
123
|
+
attributes: true,
|
|
124
|
+
attributeFilter: ["aria-hidden"]
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Also run immediately in case the card is already rendered
|
|
128
|
+
patchRadioGroups();
|
|
129
|
+
return () => observer.disconnect();
|
|
130
|
+
}, []);
|
|
131
|
+
return /*#__PURE__*/_react.default.createElement("div", {
|
|
132
|
+
id: id,
|
|
133
|
+
style: style,
|
|
134
|
+
ref: containerRef
|
|
135
|
+
}, children);
|
|
136
|
+
};
|
|
137
|
+
var _default = AdaptiveCardAccessibilityWrapper;
|
|
138
|
+
exports.default = _default;
|