@microsoft/omnichannel-chat-widget 1.8.4-main.1dd5564 → 1.8.4-main.1f36474
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/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/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/contexts/common/LiveChatWidgetContextInitialState.js +11 -0
- package/lib/esm/common/Constants.js +0 -1
- 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/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/contexts/common/LiveChatWidgetContextInitialState.js +11 -0
- package/lib/types/common/Constants.d.ts +0 -1
- 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 +4 -4
|
@@ -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
|
});
|
|
@@ -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"
|
|
@@ -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");
|
|
@@ -48,6 +48,17 @@ const getLiveChatWidgetContextInitialState = props => {
|
|
|
48
48
|
if (initialStateFromCache.appStates.isUserAuthenticated === undefined) {
|
|
49
49
|
initialStateFromCache.appStates.isUserAuthenticated = false;
|
|
50
50
|
}
|
|
51
|
+
// internal tracking: transient modal pane visibility must NOT survive a page
|
|
52
|
+
// reload. If `showConfirmationPane` / `showEmailTranscriptPane` are
|
|
53
|
+
// restored as `true`, the corresponding pane re-mounts and installs
|
|
54
|
+
// its focus trap (and sets sibling tabindex=-1). The user has no
|
|
55
|
+
// context for why the modal appeared and Tab from the host page can
|
|
56
|
+
// no longer traverse out of the widget. Always start cold for these
|
|
57
|
+
// ephemeral UI flags.
|
|
58
|
+
if (initialStateFromCache.uiStates) {
|
|
59
|
+
initialStateFromCache.uiStates.showConfirmationPane = false;
|
|
60
|
+
initialStateFromCache.uiStates.showEmailTranscriptPane = false;
|
|
61
|
+
}
|
|
51
62
|
return initialStateFromCache;
|
|
52
63
|
}
|
|
53
64
|
const LiveChatWidgetContextInitialState = {
|
|
@@ -149,7 +149,6 @@ export let HtmlIdNames = /*#__PURE__*/_createClass(function HtmlIdNames() {
|
|
|
149
149
|
_classCallCheck(this, HtmlIdNames);
|
|
150
150
|
});
|
|
151
151
|
_defineProperty(HtmlIdNames, "MSLiveChatWidget", "MSLiveChatWidget");
|
|
152
|
-
_defineProperty(HtmlIdNames, "fileSentAnnouncementRegionId", "ms_lcw_file_sent_announcement");
|
|
153
152
|
export let HtmlClassNames = /*#__PURE__*/_createClass(function HtmlClassNames() {
|
|
154
153
|
_classCallCheck(this, HtmlClassNames);
|
|
155
154
|
});
|
|
@@ -53,7 +53,13 @@ export const CitationPaneStateful = props => {
|
|
|
53
53
|
Event: TelemetryEvent.UXCitationPaneCompleted,
|
|
54
54
|
ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
|
|
55
55
|
});
|
|
56
|
-
return
|
|
56
|
+
return () => {
|
|
57
|
+
// internal tracking: restore parent tab indices on unmount so focus
|
|
58
|
+
// can escape the widget even if the pane is dismissed by an
|
|
59
|
+
// external state change rather than a user button click.
|
|
60
|
+
setTabIndices(elements, initialTabIndexMap, true);
|
|
61
|
+
cleanup();
|
|
62
|
+
};
|
|
57
63
|
}, []);
|
|
58
64
|
|
|
59
65
|
// Retry focus once pane is actually visible (isReady) in case initial attempt occurred while wrapper was visibility:hidden
|
|
@@ -91,7 +91,17 @@ export const ConfirmationPaneStateful = props => {
|
|
|
91
91
|
Event: TelemetryEvent.UXConfirmationPaneCompleted,
|
|
92
92
|
ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
|
|
93
93
|
});
|
|
94
|
-
return
|
|
94
|
+
return () => {
|
|
95
|
+
// internal tracking: when the confirmation pane unmounts (page reload,
|
|
96
|
+
// parent state change, or any path that bypasses onConfirm /
|
|
97
|
+
// onCancel) the tab indices that were forced to -1 on sibling
|
|
98
|
+
// focusable elements must be restored, otherwise Tab order
|
|
99
|
+
// remains broken and focus can be trapped inside the widget.
|
|
100
|
+
// setTabIndices is a no-op when initialTabIndexMap is already
|
|
101
|
+
// empty (onConfirm / onCancel already restored).
|
|
102
|
+
setTabIndices(elements, initialTabIndexMap, true);
|
|
103
|
+
cleanup();
|
|
104
|
+
};
|
|
95
105
|
}, []);
|
|
96
106
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DimLayer, {
|
|
97
107
|
brightness: (controlProps === null || controlProps === void 0 ? void 0 : controlProps.brightnessValueOnDim) ?? "0.2"
|
|
@@ -127,7 +127,11 @@ export const EmailTranscriptPaneStateful = props => {
|
|
|
127
127
|
Event: TelemetryEvent.UXEmailTranscriptPaneCompleted,
|
|
128
128
|
ElapsedTimeInMilliseconds: uiTimer.milliSecondsElapsed
|
|
129
129
|
});
|
|
130
|
-
return
|
|
130
|
+
return () => {
|
|
131
|
+
// internal tracking: restore parent tab indices on unmount.
|
|
132
|
+
setTabIndices(elements, initialTabIndexMap, true);
|
|
133
|
+
cleanup();
|
|
134
|
+
};
|
|
131
135
|
}, [initialEmail]);
|
|
132
136
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DimLayer, {
|
|
133
137
|
brightness: (controlProps === null || controlProps === void 0 ? void 0 : controlProps.brightnessValueOnDim) ?? "0.2"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Constants, HtmlAttributeNames, HtmlClassNames
|
|
1
|
+
import { Constants, HtmlAttributeNames, HtmlClassNames } from "../../common/Constants";
|
|
2
2
|
import { Stack } from "@fluentui/react";
|
|
3
3
|
import { LogLevel, TelemetryEvent } from "../../common/telemetry/TelemetryConstants";
|
|
4
4
|
import React, { useEffect, useRef, useState } from "react";
|
|
@@ -223,6 +223,48 @@ export const WebChatContainerStateful = props => {
|
|
|
223
223
|
chatHistoryElement.setAttribute(HtmlAttributeNames.ariaLabel, webChatContainerProps.webChatHistoryMobileAccessibilityLabel);
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
|
+
|
|
227
|
+
// internal tracking: WebChats BasicToaster renders a `role="log"` container
|
|
228
|
+
// with no accessible name. When the toaster is empty (typical idle
|
|
229
|
+
// state) screen readers traversing the page announce it as "blank".
|
|
230
|
+
// Inject an aria-label so the region has a meaningful name; the
|
|
231
|
+
// consumer can override via the new
|
|
232
|
+
// `webChatNotificationRegionAccessibilityLabel` prop. The toaster
|
|
233
|
+
// may not be rendered yet when this effect first runs, so observe
|
|
234
|
+
// for it via a MutationObserver scoped to the widget root (or, if
|
|
235
|
+
// not yet mounted, the widget container's parent) so we never fire
|
|
236
|
+
// on host-page mutations outside the widget subtree.
|
|
237
|
+
const toasterLabel = (webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.webChatNotificationRegionAccessibilityLabel) ?? defaultWebChatContainerStatefulProps.webChatNotificationRegionAccessibilityLabel ?? "Chat notifications";
|
|
238
|
+
const labelToaster = root => {
|
|
239
|
+
const toaster = root.querySelector(".webchat__toaster[role='log']");
|
|
240
|
+
if (toaster && !toaster.getAttribute(HtmlAttributeNames.ariaLabel)) {
|
|
241
|
+
toaster.setAttribute(HtmlAttributeNames.ariaLabel, toasterLabel);
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
};
|
|
246
|
+
// Prefer the widget root; if it is not yet in the DOM, fall back to
|
|
247
|
+
// `document` for the *initial* lookup only, then observe the most
|
|
248
|
+
// specific available scope (widget root if present, otherwise the
|
|
249
|
+
// widget's parent / document.body as a last resort). The observer
|
|
250
|
+
// re-resolves the widget root on every callback so we tighten scope
|
|
251
|
+
// as soon as it mounts.
|
|
252
|
+
let toasterObserver;
|
|
253
|
+
const resolveScope = () => document.getElementById("ms_lcw_webchat_root") ?? document.body;
|
|
254
|
+
if (!labelToaster(resolveScope())) {
|
|
255
|
+
toasterObserver = new MutationObserver(() => {
|
|
256
|
+
const scope = resolveScope();
|
|
257
|
+
if (labelToaster(scope)) {
|
|
258
|
+
var _toasterObserver;
|
|
259
|
+
(_toasterObserver = toasterObserver) === null || _toasterObserver === void 0 ? void 0 : _toasterObserver.disconnect();
|
|
260
|
+
toasterObserver = undefined;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
toasterObserver.observe(resolveScope(), {
|
|
264
|
+
childList: true,
|
|
265
|
+
subtree: true
|
|
266
|
+
});
|
|
267
|
+
}
|
|
226
268
|
dispatch({
|
|
227
269
|
type: LiveChatWidgetActionType.SET_RENDERING_MIDDLEWARE_PROPS,
|
|
228
270
|
payload: webChatContainerProps === null || webChatContainerProps === void 0 ? void 0 : webChatContainerProps.renderingMiddlewareProps
|
|
@@ -247,6 +289,10 @@ export const WebChatContainerStateful = props => {
|
|
|
247
289
|
}
|
|
248
290
|
}
|
|
249
291
|
}
|
|
292
|
+
return () => {
|
|
293
|
+
var _toasterObserver2;
|
|
294
|
+
(_toasterObserver2 = toasterObserver) === null || _toasterObserver2 === void 0 ? void 0 : _toasterObserver2.disconnect();
|
|
295
|
+
};
|
|
250
296
|
}, []);
|
|
251
297
|
useEffect(() => {
|
|
252
298
|
var _props$webChatContain6, _props$webChatContain7;
|
|
@@ -487,23 +533,7 @@ export const WebChatContainerStateful = props => {
|
|
|
487
533
|
height: "100%",
|
|
488
534
|
width: "100%"
|
|
489
535
|
}
|
|
490
|
-
}, shouldLoadPersistentHistoryMessages && /*#__PURE__*/React.createElement(WebChatEventSubscribers, null), /*#__PURE__*/React.createElement(BasicWebChat, null))), /*#__PURE__*/React.createElement(
|
|
491
|
-
id: HtmlIdNames.fileSentAnnouncementRegionId,
|
|
492
|
-
role: "alert",
|
|
493
|
-
"aria-live": "assertive",
|
|
494
|
-
"aria-atomic": "true",
|
|
495
|
-
style: {
|
|
496
|
-
position: "absolute",
|
|
497
|
-
width: "1px",
|
|
498
|
-
height: "1px",
|
|
499
|
-
padding: "0",
|
|
500
|
-
margin: "-1px",
|
|
501
|
-
overflow: "hidden",
|
|
502
|
-
clip: "rect(0, 0, 0, 0)",
|
|
503
|
-
whiteSpace: "nowrap",
|
|
504
|
-
border: "0"
|
|
505
|
-
}
|
|
506
|
-
}), citationPaneOpen && /*#__PURE__*/React.createElement(CitationPaneStateful, {
|
|
536
|
+
}, shouldLoadPersistentHistoryMessages && /*#__PURE__*/React.createElement(WebChatEventSubscribers, null), /*#__PURE__*/React.createElement(BasicWebChat, null))), citationPaneOpen && /*#__PURE__*/React.createElement(CitationPaneStateful, {
|
|
507
537
|
id: ((_props$citationPanePr = props.citationPaneProps) === null || _props$citationPanePr === void 0 ? void 0 : _props$citationPanePr.id) || HtmlAttributeNames.ocwCitationPaneClassName,
|
|
508
538
|
title: ((_props$citationPanePr2 = props.citationPaneProps) === null || _props$citationPanePr2 === void 0 ? void 0 : _props$citationPanePr2.title) || HtmlAttributeNames.ocwCitationPaneTitle,
|
|
509
539
|
contentHtml: citationPaneText,
|
|
@@ -52,6 +52,16 @@ export let DesignerChatAdapter = /*#__PURE__*/function (_MockAdapter) {
|
|
|
52
52
|
if (msg.text) {
|
|
53
53
|
if (msg.suggestedActions) {
|
|
54
54
|
postAgentSuggestedActionsActivity(this.activityObserver, msg.text, msg.suggestedActions, index * 100);
|
|
55
|
+
} else if (msg.from && msg.from.role) {
|
|
56
|
+
// Route caller-supplied `from` through the shared
|
|
57
|
+
// postBotMessageActivity helper so the activity shape
|
|
58
|
+
// (id, channelData.tags, timestamp, type) matches every
|
|
59
|
+
// other designer-mode path and downstream middleware
|
|
60
|
+
// (localizedStringsBotInitialsMiddleware, telemetry,
|
|
61
|
+
// attachment plumbing) behaves identically. The override
|
|
62
|
+
// narrows only the fields the caller cares about (e.g.
|
|
63
|
+
// from.name / from.role) — base botUser fields remain.
|
|
64
|
+
postBotMessageActivity(this.activityObserver, msg.text, undefined, index * 100, msg.from);
|
|
55
65
|
} else {
|
|
56
66
|
postBotMessageActivity(this.activityObserver, msg.text, undefined, index * 100);
|
|
57
67
|
}
|
|
@@ -13,5 +13,6 @@ export const defaultWebChatContainerStatefulProps = {
|
|
|
13
13
|
honorsTargetInHTMLLinks: false,
|
|
14
14
|
hyperlinkTextOverride: false,
|
|
15
15
|
directLine: new MockAdapter(),
|
|
16
|
-
adaptiveCardStyles: defaultAdaptiveCardStyles
|
|
16
|
+
adaptiveCardStyles: defaultAdaptiveCardStyles,
|
|
17
|
+
webChatNotificationRegionAccessibilityLabel: "Chat notifications"
|
|
17
18
|
};
|
|
@@ -34,11 +34,13 @@ export const postEchoActivity = function (activityObserver, activity, user) {
|
|
|
34
34
|
export const postBotMessageActivity = function (activityObserver, text) {
|
|
35
35
|
let tags = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "";
|
|
36
36
|
let delay = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1000;
|
|
37
|
+
let fromOverride = arguments.length > 4 ? arguments[4] : undefined;
|
|
37
38
|
setTimeout(() => {
|
|
38
39
|
activityObserver === null || activityObserver === void 0 ? void 0 : activityObserver.next({
|
|
39
40
|
id: uuidv4(),
|
|
40
41
|
from: {
|
|
41
|
-
...botUser
|
|
42
|
+
...botUser,
|
|
43
|
+
...(fromOverride ?? {})
|
|
42
44
|
},
|
|
43
45
|
text,
|
|
44
46
|
type: "message",
|
|
@@ -25,6 +25,56 @@ const AdaptiveCardAccessibilityWrapper = _ref => {
|
|
|
25
25
|
const container = containerRef.current;
|
|
26
26
|
if (!container) return;
|
|
27
27
|
|
|
28
|
+
// dropdown-double-label: compact Input.ChoiceSet renders as a native
|
|
29
|
+
// <select> with both aria-labelledby and a sibling visible <label for>.
|
|
30
|
+
// Only remove aria-labelledby when it points solely at that same visible
|
|
31
|
+
// label; preserve composite labels that include required/error/context text.
|
|
32
|
+
const compactSelects = container.querySelectorAll(".ac-input-container select.ac-input.ac-multichoiceInput[aria-labelledby]");
|
|
33
|
+
compactSelects.forEach(select => {
|
|
34
|
+
const id = select.id;
|
|
35
|
+
if (!id) return;
|
|
36
|
+
const visibleLabel = container.querySelector(`label[for="${CSS.escape(id)}"]:not([aria-hidden='true'])`);
|
|
37
|
+
const labelledBy = (select.getAttribute("aria-labelledby") || "").trim().split(/\s+/);
|
|
38
|
+
if (visibleLabel !== null && visibleLabel !== void 0 && visibleLabel.id && labelledBy.length === 1 && labelledBy[0] === visibleLabel.id) {
|
|
39
|
+
select.removeAttribute("aria-labelledby");
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// internal tracking: non-radio inputs (Input.Text including
|
|
44
|
+
// isMultiline=true which renders as <textarea>, Input.Date,
|
|
45
|
+
// Input.Number, Input.Toggle, Input.ChoiceSet multi-select
|
|
46
|
+
// checkbox) rendered by adaptivecards can have THREE accessible-
|
|
47
|
+
// name sources resolving to the same string: a visible
|
|
48
|
+
// <label for>, aria-label, AND aria-labelledby. TalkBack walks
|
|
49
|
+
// each accessibility node independently and reads the duplicated
|
|
50
|
+
// name. When a visible <label for> exists and names the control,
|
|
51
|
+
// strip the redundant aria-label / aria-labelledby on the input
|
|
52
|
+
// so only one announceable source remains.
|
|
53
|
+
const labelledInputs = container.querySelectorAll(".ac-input-container input.ac-input[id]:not([type='radio']), .ac-input-container textarea.ac-input[id]");
|
|
54
|
+
labelledInputs.forEach(input => {
|
|
55
|
+
const id = input.id;
|
|
56
|
+
if (!id) return;
|
|
57
|
+
const visibleLabel = container.querySelector(`label[for="${CSS.escape(id)}"]:not([aria-hidden='true'])`);
|
|
58
|
+
if (!visibleLabel) return;
|
|
59
|
+
const labelText = (visibleLabel.textContent || "").trim();
|
|
60
|
+
if (!labelText) return;
|
|
61
|
+
const ariaLabel = input.getAttribute("aria-label");
|
|
62
|
+
if (ariaLabel && ariaLabel.trim() === labelText) {
|
|
63
|
+
input.removeAttribute("aria-label");
|
|
64
|
+
}
|
|
65
|
+
const labelledBy = (input.getAttribute("aria-labelledby") || "").trim();
|
|
66
|
+
if (labelledBy) {
|
|
67
|
+
const ids = labelledBy.split(/\s+/);
|
|
68
|
+
const resolved = ids.map(rid => {
|
|
69
|
+
var _document$getElementB;
|
|
70
|
+
return (((_document$getElementB = document.getElementById(rid)) === null || _document$getElementB === void 0 ? void 0 : _document$getElementB.textContent) || "").trim();
|
|
71
|
+
}).filter(Boolean).join(" ");
|
|
72
|
+
if (resolved === labelText) {
|
|
73
|
+
input.removeAttribute("aria-labelledby");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
28
78
|
// action-button-toggle / login-button-toggle: submit/login action buttons can be
|
|
29
79
|
// rendered with toggle-only ARIA that causes screen readers to announce them as
|
|
30
80
|
// switches instead of plain push buttons. Preserve Action.ToggleVisibility buttons,
|
|
@@ -5,21 +5,17 @@
|
|
|
5
5
|
******/
|
|
6
6
|
|
|
7
7
|
import { LogLevel, TelemetryEvent } from "../../../../../common/telemetry/TelemetryConstants";
|
|
8
|
-
import { AMSConstants
|
|
8
|
+
import { AMSConstants } from "../../../../../common/Constants";
|
|
9
9
|
import { NotificationHandler } from "../../notification/NotificationHandler";
|
|
10
10
|
import { NotificationScenarios } from "../../enums/NotificationScenarios";
|
|
11
11
|
import { TelemetryHelper } from "../../../../../common/telemetry/TelemetryHelper";
|
|
12
12
|
import { WebChatActionType } from "../../enums/WebChatActionType";
|
|
13
13
|
const MBtoBRatio = 1000000;
|
|
14
14
|
const announceFileSent = message => {
|
|
15
|
-
// TalkBack on Android WebView reliably announces newly *appended*
|
|
16
|
-
// nodes but often misses text-content updates on existing
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
const region = document.getElementById(HtmlIdNames.fileSentAnnouncementRegionId);
|
|
20
|
-
if (region) {
|
|
21
|
-
region.textContent = "";
|
|
22
|
-
}
|
|
15
|
+
// TalkBack on Android WebView reliably announces newly *appended*
|
|
16
|
+
// role="alert" nodes but often misses text-content updates on existing
|
|
17
|
+
// nodes. Wait 500ms for the send-box focus shift to settle, then inject
|
|
18
|
+
// a fresh alert element appended to document.body and remove it after 3s.
|
|
23
19
|
setTimeout(() => {
|
|
24
20
|
const el = document.createElement("div");
|
|
25
21
|
el.setAttribute("role", "alert");
|
|
@@ -42,6 +42,17 @@ export const getLiveChatWidgetContextInitialState = props => {
|
|
|
42
42
|
if (initialStateFromCache.appStates.isUserAuthenticated === undefined) {
|
|
43
43
|
initialStateFromCache.appStates.isUserAuthenticated = false;
|
|
44
44
|
}
|
|
45
|
+
// internal tracking: transient modal pane visibility must NOT survive a page
|
|
46
|
+
// reload. If `showConfirmationPane` / `showEmailTranscriptPane` are
|
|
47
|
+
// restored as `true`, the corresponding pane re-mounts and installs
|
|
48
|
+
// its focus trap (and sets sibling tabindex=-1). The user has no
|
|
49
|
+
// context for why the modal appeared and Tab from the host page can
|
|
50
|
+
// no longer traverse out of the widget. Always start cold for these
|
|
51
|
+
// ephemeral UI flags.
|
|
52
|
+
if (initialStateFromCache.uiStates) {
|
|
53
|
+
initialStateFromCache.uiStates.showConfirmationPane = false;
|
|
54
|
+
initialStateFromCache.uiStates.showEmailTranscriptPane = false;
|
|
55
|
+
}
|
|
45
56
|
return initialStateFromCache;
|
|
46
57
|
}
|
|
47
58
|
const LiveChatWidgetContextInitialState = {
|
|
@@ -119,7 +119,6 @@ export declare const Regex: {
|
|
|
119
119
|
};
|
|
120
120
|
export declare class HtmlIdNames {
|
|
121
121
|
static readonly MSLiveChatWidget = "MSLiveChatWidget";
|
|
122
|
-
static readonly fileSentAnnouncementRegionId = "ms_lcw_file_sent_announcement";
|
|
123
122
|
}
|
|
124
123
|
export declare class HtmlClassNames {
|
|
125
124
|
static readonly webChatBannerCloseButton = "webchat__toast__dismissButton";
|
|
@@ -4,7 +4,7 @@ export declare const customerUser: User;
|
|
|
4
4
|
export declare const botUser: User;
|
|
5
5
|
export declare const agentUser: User;
|
|
6
6
|
export declare const postEchoActivity: (activityObserver: Subscriber<Activity> | undefined, activity: Message, user: User, delay?: number) => void;
|
|
7
|
-
export declare const postBotMessageActivity: (activityObserver: Subscriber<Activity> | undefined, text: string, tags?: string, delay?: number) => void;
|
|
7
|
+
export declare const postBotMessageActivity: (activityObserver: Subscriber<Activity> | undefined, text: string, tags?: string, delay?: number, fromOverride?: Partial<User>) => void;
|
|
8
8
|
export declare const postAgentMessageActivity: (activityObserver: Subscriber<Activity> | undefined, text: string, tags?: string, delay?: number) => void;
|
|
9
9
|
export declare const postSystemMessageActivity: (activityObserver: Subscriber<Activity> | undefined, text: string, delay?: number) => void;
|
|
10
10
|
export declare const postBotTypingActivity: (activityObserver: Subscriber<Activity> | undefined, delay?: number) => void;
|
package/lib/types/components/webchatcontainerstateful/interfaces/IWebChatContainerStatefulProps.d.ts
CHANGED
|
@@ -25,4 +25,11 @@ export interface IWebChatContainerStatefulProps {
|
|
|
25
25
|
adaptiveCardStyles?: IAdaptiveCardStyles;
|
|
26
26
|
sendBoxTextBox?: ISendBox;
|
|
27
27
|
webChatHistoryMobileAccessibilityLabel?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Accessible name applied to WebChats notification toaster region
|
|
30
|
+
* (`role="log"`). When omitted, defaults to "Chat notifications" so
|
|
31
|
+
* screen readers don't announce the empty live region as "blank"
|
|
32
|
+
* (internal tracking).
|
|
33
|
+
*/
|
|
34
|
+
webChatNotificationRegionAccessibilityLabel?: string;
|
|
28
35
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microsoft/omnichannel-chat-widget",
|
|
3
|
-
"version": "1.8.4-main.
|
|
3
|
+
"version": "1.8.4-main.1f36474",
|
|
4
4
|
"description": "Microsoft Omnichannel Chat Widget",
|
|
5
5
|
"main": "lib/cjs/index.js",
|
|
6
6
|
"types": "lib/types/index.d.ts",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"react-test-renderer": "^18.3.1",
|
|
85
85
|
"rimraf": "^6.0.1",
|
|
86
86
|
"storybook-addon-playwright": "^4.9.2",
|
|
87
|
-
"swiper": "^
|
|
87
|
+
"swiper": "^12.1.2",
|
|
88
88
|
"terser-webpack-plugin": "^4.2.3",
|
|
89
89
|
"thread-loader": "^2.1.3",
|
|
90
90
|
"ts-loader": "^9.2.6",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"dependencies": {
|
|
96
96
|
"@azure/core-tracing": "^1.2.0",
|
|
97
97
|
"@microsoft/applicationinsights-web": "^3.3.6",
|
|
98
|
-
"@microsoft/omnichannel-chat-components": "1.1.17-main.
|
|
98
|
+
"@microsoft/omnichannel-chat-components": "1.1.17-main.f21df63",
|
|
99
99
|
"@microsoft/omnichannel-chat-sdk": "1.11.9-main.da8219b",
|
|
100
100
|
"@opentelemetry/api": "^1.9.0",
|
|
101
101
|
"abort-controller": "^3",
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
"**/lodash": "4.17.23",
|
|
173
173
|
"**/@babel/runtime-corejs3": "^7.29.0",
|
|
174
174
|
"**/brace-expansion": "2.0.3",
|
|
175
|
-
"**/swiper": "
|
|
175
|
+
"**/swiper": "12.1.2"
|
|
176
176
|
},
|
|
177
177
|
"jest": {
|
|
178
178
|
"verbose": true,
|