UNPKG

botframework-webchat-component

Version:
1,026 lines (855 loc) 162 kB
"use strict"; function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _botframeworkWebchatApi = require("botframework-webchat-api"); var _reactScrollToBottom = require("react-scroll-to-bottom"); var _classnames = _interopRequireDefault(require("classnames")); var _propTypes = _interopRequireDefault(require("prop-types")); var _mathRandom = _interopRequireDefault(require("math-random")); var _react = _interopRequireWildcard(require("react")); var _BasicTypingIndicator = _interopRequireDefault(require("./BasicTypingIndicator")); var _Fade = _interopRequireDefault(require("./Utils/Fade")); var _FocusRedirector = _interopRequireDefault(require("./Utils/FocusRedirector")); var _getActivityUniqueId = _interopRequireDefault(require("./Utils/getActivityUniqueId")); var _getTabIndex = _interopRequireDefault(require("./Utils/TypeFocusSink/getTabIndex")); var _inputtableKey = _interopRequireDefault(require("./Utils/TypeFocusSink/inputtableKey")); var _intersectionOf = _interopRequireDefault(require("./Utils/intersectionOf")); var _isZeroOrPositive = _interopRequireDefault(require("./Utils/isZeroOrPositive")); var _removeInline = _interopRequireDefault(require("./Utils/removeInline")); var _ScreenReaderActivity = _interopRequireDefault(require("./ScreenReaderActivity")); var _ScreenReaderText = _interopRequireDefault(require("./ScreenReaderText")); var _ScrollToEndButton = _interopRequireDefault(require("./Activity/ScrollToEndButton")); var _Speak = _interopRequireDefault(require("./Activity/Speak")); var _tabbableElements = _interopRequireDefault(require("./Utils/tabbableElements")); var _useAcknowledgedActivity = _interopRequireDefault(require("./hooks/internal/useAcknowledgedActivity")); var _useDispatchScrollPosition = _interopRequireDefault(require("./hooks/internal/useDispatchScrollPosition")); var _useDispatchTranscriptFocus = _interopRequireDefault(require("./hooks/internal/useDispatchTranscriptFocus")); var _useFocus = _interopRequireDefault(require("./hooks/useFocus")); var _useMemoize = _interopRequireDefault(require("./hooks/internal/useMemoize")); var _useRegisterFocusTranscript = _interopRequireDefault(require("./hooks/internal/useRegisterFocusTranscript")); var _useRegisterScrollRelative = _interopRequireDefault(require("./hooks/internal/useRegisterScrollRelative")); var _useRegisterScrollTo = _interopRequireDefault(require("./hooks/internal/useRegisterScrollTo")); var _useRegisterScrollToEnd = _interopRequireDefault(require("./hooks/internal/useRegisterScrollToEnd")); var _useStyleSet5 = _interopRequireDefault(require("./hooks/useStyleSet")); var _useStyleToEmotionObject = _interopRequireDefault(require("./hooks/internal/useStyleToEmotionObject")); var _useUniqueId = _interopRequireDefault(require("./hooks/internal/useUniqueId")); function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (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; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } 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."); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } var useActivities = _botframeworkWebchatApi.hooks.useActivities, useCreateActivityRenderer = _botframeworkWebchatApi.hooks.useCreateActivityRenderer, useCreateActivityStatusRenderer = _botframeworkWebchatApi.hooks.useCreateActivityStatusRenderer, useCreateAvatarRenderer = _botframeworkWebchatApi.hooks.useCreateAvatarRenderer, useDirection = _botframeworkWebchatApi.hooks.useDirection, useGroupActivities = _botframeworkWebchatApi.hooks.useGroupActivities, useLocalizer = _botframeworkWebchatApi.hooks.useLocalizer, useStyleOptions = _botframeworkWebchatApi.hooks.useStyleOptions; var ROOT_STYLE = { '&.webchat__basic-transcript': { display: 'flex', flexDirection: 'column', overflow: 'hidden', // Make sure to set "position: relative" here to form another stacking context for the scroll-to-end button. // Stacking context help isolating elements that use "z-index" from global pollution. // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context position: 'relative', '& .webchat__basic-transcript__filler': { flex: 1 }, '& .webchat__basic-transcript__scrollable': { display: 'flex', flexDirection: 'column', overflowX: 'hidden', WebkitOverflowScrolling: 'touch' }, '& .webchat__basic-transcript__transcript': { listStyleType: 'none' } } }; function validateAllActivitiesTagged(activities, bins) { return activities.every(function (activity) { return bins.some(function (bin) { return bin.includes(activity); }); }); } var InternalTranscript = function InternalTranscript(_ref) { var activityElementsRef = _ref.activityElementsRef, className = _ref.className; var _useStyleSet = (0, _useStyleSet5.default)(), _useStyleSet2 = _slicedToArray(_useStyleSet, 1), basicTranscriptStyleSet = _useStyleSet2[0].basicTranscript; var _useStyleOptions = useStyleOptions(), _useStyleOptions2 = _slicedToArray(_useStyleOptions, 1), _useStyleOptions2$ = _useStyleOptions2[0], bubbleFromUserNubOffset = _useStyleOptions2$.bubbleFromUserNubOffset, bubbleNubOffset = _useStyleOptions2$.bubbleNubOffset, groupTimestamp = _useStyleOptions2$.groupTimestamp, internalLiveRegionFadeAfter = _useStyleOptions2$.internalLiveRegionFadeAfter, showAvatarInGroup = _useStyleOptions2$.showAvatarInGroup; var _useState = (0, _react.useState)(), _useState2 = _slicedToArray(_useState, 2), focusedActivityKey = _useState2[0], setFocusedActivityKey = _useState2[1]; var _useActivities = useActivities(), _useActivities2 = _slicedToArray(_useActivities, 1), activities = _useActivities2[0]; var _useDirection = useDirection(), _useDirection2 = _slicedToArray(_useDirection, 1), direction = _useDirection2[0]; var createActivityRenderer = useCreateActivityRenderer(); var createActivityStatusRenderer = useCreateActivityStatusRenderer(); var createAvatarRenderer = useCreateAvatarRenderer(); var focus = (0, _useFocus.default)(); var groupActivities = useGroupActivities(); var localize = useLocalizer(); var rootClassName = (0, _useStyleToEmotionObject.default)()(ROOT_STYLE) + ''; var rootElementRef = (0, _react.useRef)(); var terminatorRef = (0, _react.useRef)(); var activityInteractiveAlt = localize('ACTIVITY_INTERACTIVE_LABEL_ALT'); var terminatorText = localize('TRANSCRIPT_TERMINATOR_TEXT'); var transcriptAriaLabel = localize('TRANSCRIPT_ARIA_LABEL_ALT'); var transcriptRoleDescription = localize('TRANSCRIPT_ARIA_ROLE_ALT'); var hideAllTimestamps = groupTimestamp === false; // Gets renderer for every activity. // Activities that are not visible will return a falsy renderer. // Converted from createActivityRenderer({ activity, nextVisibleActivity }) to createActivityRenderer(activity, nextVisibleActivity). // This is for the memoization function to cache the arguments. Memoizer can only cache literal arguments. var createActivityRendererWithLiteralArgs = (0, _react.useCallback)(function (activity, nextVisibleActivity) { return createActivityRenderer({ activity: activity, nextVisibleActivity: nextVisibleActivity }); }, [createActivityRenderer]); // Create a memoized context of the createActivityRenderer function. var activitiesWithRenderer = (0, _useMemoize.default)(createActivityRendererWithLiteralArgs, function (createActivityRendererWithLiteralArgsMemoized) { // All calls to createActivityRendererWithLiteralArgsMemoized() in this function will be memoized (LRU = 1). // In the next render cycle, calls to createActivityRendererWithLiteralArgsMemoized() might return the memoized result instead. // This is an improvement to React useMemo(), because it only allows 1 memoization. // useMemoize() allows any number of memoization. var activitiesWithRenderer = []; var nextVisibleActivity; for (var index = activities.length - 1; index >= 0; index--) { var activity = activities[index]; var renderActivity = createActivityRendererWithLiteralArgsMemoized(activity, nextVisibleActivity); if (renderActivity) { activitiesWithRenderer.splice(0, 0, { activity: activity, renderActivity: renderActivity }); nextVisibleActivity = activity; } } return activitiesWithRenderer; }, [activities]); var visibleActivities = (0, _react.useMemo)(function () { return activitiesWithRenderer.map(function (_ref2) { var activity = _ref2.activity; return activity; }); }, [activitiesWithRenderer]); // Tag activities based on types. // The default implementation tag into 2 types: sender and status. var _useMemo = (0, _react.useMemo)(function () { var _groupActivities = groupActivities({ activities: visibleActivities }), activitiesGroupBySender = _groupActivities.sender, activitiesGroupByStatus = _groupActivities.status; if (!validateAllActivitiesTagged(visibleActivities, activitiesGroupBySender)) { console.warn('botframework-webchat: Not every activities are grouped in the "sender" property. Please fix "groupActivitiesMiddleware" and group every activities.'); } if (!validateAllActivitiesTagged(visibleActivities, activitiesGroupByStatus)) { console.warn('botframework-webchat: Not every activities are grouped in the "status" property. Please fix "groupActivitiesMiddleware" and group every activities.'); } return { activitiesGroupBySender: activitiesGroupBySender, activitiesGroupByStatus: activitiesGroupByStatus }; }, [groupActivities, visibleActivities]), activitiesGroupBySender = _useMemo.activitiesGroupBySender, activitiesGroupByStatus = _useMemo.activitiesGroupByStatus; // Create a tree of activities with 2 dimensions: sender, followed by status. var activityTree = (0, _react.useMemo)(function () { var visibleActivitiesPendingGrouping = _toConsumableArray(visibleActivities); var activityTree = []; var _loop = function _loop() { var _visibleActivitiesPen = _slicedToArray(visibleActivitiesPendingGrouping, 1), activity = _visibleActivitiesPen[0]; var senderTree = []; var activitiesWithSameSender = activitiesGroupBySender.find(function (activities) { return activities.includes(activity); }); activityTree.push(senderTree); activitiesWithSameSender.forEach(function (activity) { var activitiesWithSameStatus = activitiesGroupByStatus.find(function (activities) { return activities.includes(activity); }); var activitiesWithSameSenderAndStatus = (0, _intersectionOf.default)(visibleActivitiesPendingGrouping, activitiesWithSameSender, activitiesWithSameStatus); if (activitiesWithSameSenderAndStatus.length) { senderTree.push(activitiesWithSameSenderAndStatus); _removeInline.default.apply(void 0, [visibleActivitiesPendingGrouping].concat(_toConsumableArray(activitiesWithSameSenderAndStatus))); } }); }; while (visibleActivitiesPendingGrouping.length) { _loop(); } // Assertion: All activities in visibleActivities, must be assigned to the activityTree if (!visibleActivities.every(function (activity) { return activityTree.some(function (activitiesWithSameSender) { return activitiesWithSameSender.some(function (activitiesWithSameSenderAndStatus) { return activitiesWithSameSenderAndStatus.includes(activity); }); }); })) { console.warn('botframework-webchat internal: Not all visible activities are grouped in the activityTree.', { visibleActivities: visibleActivities, activityTree: activityTree }); } return activityTree; }, [activitiesGroupBySender, activitiesGroupByStatus, visibleActivities]); // Flatten the tree back into an array with information related to rendering. var renderingElements = (0, _react.useMemo)(function () { var renderingElements = []; var topSideBotNub = (0, _isZeroOrPositive.default)(bubbleNubOffset); var topSideUserNub = (0, _isZeroOrPositive.default)(bubbleFromUserNubOffset); activityTree.forEach(function (activitiesWithSameSender) { var _activitiesWithSameSe = _slicedToArray(activitiesWithSameSender, 1), _activitiesWithSameSe2 = _slicedToArray(_activitiesWithSameSe[0], 1), firstActivity = _activitiesWithSameSe2[0]; var renderAvatar = createAvatarRenderer({ activity: firstActivity }); activitiesWithSameSender.forEach(function (activitiesWithSameSenderAndStatus, indexWithinSenderGroup) { var firstInSenderGroup = !indexWithinSenderGroup; var lastInSenderGroup = indexWithinSenderGroup === activitiesWithSameSender.length - 1; activitiesWithSameSenderAndStatus.forEach(function (activity, indexWithinSenderAndStatusGroup) { // We only show the timestamp at the end of the sender group. But we always show the "Send failed, retry" prompt. var renderActivityStatus = createActivityStatusRenderer({ activity: activity }); var firstInSenderAndStatusGroup = !indexWithinSenderAndStatusGroup; var lastInSenderAndStatusGroup = indexWithinSenderAndStatusGroup === activitiesWithSameSenderAndStatus.length - 1; var _activitiesWithRender = activitiesWithRenderer.find(function (entry) { return entry.activity === activity; }), renderActivity = _activitiesWithRender.renderActivity; var key = (0, _getActivityUniqueId.default)(activity) || renderingElements.length; var _activity$channelData = activity.channelData; _activity$channelData = _activity$channelData === void 0 ? {} : _activity$channelData; var _activity$channelData2 = _activity$channelData.messageBack; _activity$channelData2 = _activity$channelData2 === void 0 ? {} : _activity$channelData2; var messageBackDisplayText = _activity$channelData2.displayText, role = activity.from.role, text = activity.text; var topSideNub = role === 'user' ? topSideUserNub : topSideBotNub; var showCallout; // Depends on different "showAvatarInGroup" setting, we will show the avatar in different positions. if (showAvatarInGroup === 'sender') { if (topSideNub) { showCallout = firstInSenderGroup && firstInSenderAndStatusGroup; } else { showCallout = lastInSenderGroup && lastInSenderAndStatusGroup; } } else if (showAvatarInGroup === 'status') { if (topSideNub) { showCallout = firstInSenderAndStatusGroup; } else { showCallout = lastInSenderAndStatusGroup; } } else { showCallout = true; } var focusActivity = function focusActivity() { setFocusedActivityKey((0, _getActivityUniqueId.default)(activity)); // IE11 need to manually focus on the transcript. var rootElement = rootElementRef.current; rootElement && rootElement.focus(); }; renderingElements.push({ activity: activity, // After the element is mounted, set it to activityElementsRef. callbackRef: function callbackRef(activityElement) { var entry = activityElementsRef.current.find(function (_ref3) { var activityID = _ref3.activityID; return activityID === activity.id; }); if (entry) { entry.element = activityElement; } }, // Calling this function will put the focus on the transcript and the activity. focusActivity: focusActivity, // When a child of the activity receives focus, notify the transcript to set the aria-activedescendant to this activity. handleFocus: function handleFocus() { setFocusedActivityKey((0, _getActivityUniqueId.default)(activity)); }, handleKeyDown: function handleKeyDown(event) { if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); setFocusedActivityKey((0, _getActivityUniqueId.default)(activity)); var current = rootElementRef.current; current && current.focus(); } }, // For accessibility: when the user press up/down arrow keys, we put a visual focus indicator around the focused activity. // We should do the same for mouse, that is why we have the click handler here. // We are doing it in event capture phase to prevent other components from stopping event propagation to us. handleMouseDownCapture: function handleMouseDownCapture(_ref4) { var target = _ref4.target; var tabIndex = (0, _getTabIndex.default)(target); if (typeof tabIndex !== 'number' || tabIndex < 0 || target.getAttribute('aria-disabled') === 'true') { focusActivity(); } }, // "hideTimestamp" is a render-time parameter for renderActivityStatus(). // If true, it will hide the timestamp, but it will continue to show the // retry prompt. And show the screen reader version of the timestamp. hideTimestamp: hideAllTimestamps || indexWithinSenderAndStatusGroup !== activitiesWithSameSenderAndStatus.length - 1, key: key, // When "liveRegionKey" changes, it will show up in the live region momentarily. liveRegionKey: key + '|' + (messageBackDisplayText || text), renderActivity: renderActivity, renderActivityStatus: renderActivityStatus, renderAvatar: renderAvatar, role: role, // TODO: [P2] #2858 We should use core/definitions/speakingActivity for this predicate instead shouldSpeak: activity.channelData && activity.channelData.speak, showCallout: showCallout }); }); }); }); var activityElements = activityElementsRef.current; // Update activityElementRef with new sets of activity, while retaining the existing referencing element if exists. activityElementsRef.current = renderingElements.map(function (_ref5) { var activity = _ref5.activity, id = _ref5.activity.id, elementId = _ref5.elementId, key = _ref5.key; var existingEntry = activityElements.find(function (entry) { return entry.key === key; }); return { activity: activity, activityID: id, ariaLabelID: existingEntry ? existingEntry.ariaLabelID : "webchat__basic-transcript__activity-label-".concat((0, _mathRandom.default)().toString(36).substr(2, 5)), element: existingEntry && existingEntry.element, elementId: elementId, key: key }; }); // There must be one focused (a.k.a. aria-activedescendant) designated. We default it to the last one. if (!renderingElements.find(function (_ref6) { var focused = _ref6.focused; return focused; })) { var lastElement = renderingElements[renderingElements.length - 1]; if (lastElement) { lastElement.focused = true; } } return renderingElements; }, [activitiesWithRenderer, activityElementsRef, activityTree, bubbleFromUserNubOffset, bubbleNubOffset, createActivityStatusRenderer, createAvatarRenderer, hideAllTimestamps, rootElementRef, showAvatarInGroup]); var renderingActivities = (0, _react.useMemo)(function () { return renderingElements.map(function (_ref7) { var activity = _ref7.activity; return activity; }); }, [renderingElements]); var scrollToBottomScrollTo = (0, _reactScrollToBottom.useScrollTo)(); var scrollToBottomScrollToEnd = (0, _reactScrollToBottom.useScrollToEnd)(); var scrollTo = (0, _react.useCallback)(function (position) { var _ref8 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, _ref8$behavior = _ref8.behavior, behavior = _ref8$behavior === void 0 ? 'auto' : _ref8$behavior; if (!position) { throw new Error('botframework-webchat: First argument passed to "useScrollTo" must be a ScrollPosition object.'); } var activityID = position.activityID, scrollTop = position.scrollTop; if (typeof scrollTop !== 'undefined') { scrollToBottomScrollTo(scrollTop, { behavior: behavior }); } else if (typeof activityID !== 'undefined') { var rootElement = rootElementRef.current; var _ref9 = activityElementsRef.current.find(function (entry) { return entry.activityID === activityID; }) || {}, activityElement = _ref9.element; var scrollableElement = rootElement.querySelector('.webchat__basic-transcript__scrollable'); if (scrollableElement && activityElement) { var _activityElement$getC = activityElement.getClientRects(), _activityElement$getC2 = _slicedToArray(_activityElement$getC, 1), _activityElement$getC3 = _activityElement$getC2[0], activityElementHeight = _activityElement$getC3.height, activityElementY = _activityElement$getC3.y; var _scrollableElement$ge = scrollableElement.getClientRects(), _scrollableElement$ge2 = _slicedToArray(_scrollableElement$ge, 1), scrollableHeight = _scrollableElement$ge2[0].height; var activityElementOffsetTop = activityElementY + scrollableElement.scrollTop; var _scrollTop = Math.min(activityElementOffsetTop, activityElementOffsetTop - scrollableHeight + activityElementHeight); scrollToBottomScrollTo(_scrollTop, { behavior: behavior }); } } }, [activityElementsRef, rootElementRef, scrollToBottomScrollTo]); var scrollRelative = (0, _react.useCallback)(function (direction) { var _ref10 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, displacement = _ref10.displacement; var rootElement = rootElementRef.current; if (!rootElement) { return; } var scrollable = rootElement.querySelector('.webchat__basic-transcript__scrollable'); var nextScrollTop; if (typeof displacement === 'number') { nextScrollTop = scrollable.scrollTop + (direction === 'down' ? 1 : -1) * displacement; } else { nextScrollTop = scrollable.scrollTop + (direction === 'down' ? 1 : -1) * scrollable.offsetHeight; } scrollTo({ scrollTop: Math.max(0, Math.min(scrollable.scrollHeight - scrollable.offsetHeight, nextScrollTop)) }, { behavior: 'smooth' }); }, [rootElementRef, scrollTo]); // Since there could be multiple instances of <BasicTranscript> inside the <Composer>, when the developer calls `scrollXXX`, we need to call it on all instances. // We call `useRegisterScrollXXX` to register a callback function, the `useScrollXXX` will multiplex the call into each instance of <BasicTranscript>. (0, _useRegisterScrollTo.default)(scrollTo); (0, _useRegisterScrollToEnd.default)(scrollToBottomScrollToEnd); (0, _useRegisterScrollRelative.default)(scrollRelative); var dispatchScrollPosition = (0, _useDispatchScrollPosition.default)(); var patchedDispatchScrollPosition = (0, _react.useMemo)(function () { if (!dispatchScrollPosition) { return; } return function (_ref11) { var scrollTop = _ref11.scrollTop; var rootElement = rootElementRef.current; if (!rootElement) { return; } var scrollableElement = rootElement.querySelector('.webchat__basic-transcript__scrollable'); var _scrollableElement$ge3 = scrollableElement.getClientRects(), _scrollableElement$ge4 = _slicedToArray(_scrollableElement$ge3, 1), _scrollableElement$ge5 = _scrollableElement$ge4[0]; _scrollableElement$ge5 = _scrollableElement$ge5 === void 0 ? {} : _scrollableElement$ge5; var offsetHeight = _scrollableElement$ge5.height; // Find the activity just above scroll view bottom. // If the scroll view is already on top, get the first activity. var entry = scrollableElement.scrollTop ? _toConsumableArray(activityElementsRef.current).reverse().find(function (_ref12) { var element = _ref12.element; if (!element) { return false; } var _element$getClientRec = element.getClientRects(), _element$getClientRec2 = _slicedToArray(_element$getClientRec, 1), _element$getClientRec3 = _element$getClientRec2[0]; _element$getClientRec3 = _element$getClientRec3 === void 0 ? {} : _element$getClientRec3; var y = _element$getClientRec3.y; return y < offsetHeight; }) : activityElementsRef.current[0]; var _ref13 = entry || {}, activityID = _ref13.activityID; dispatchScrollPosition(_objectSpread(_objectSpread({}, activityID ? { activityID: activityID } : {}), {}, { scrollTop: scrollTop })); }; }, [activityElementsRef, dispatchScrollPosition, rootElementRef]); (0, _reactScrollToBottom.useObserveScrollPosition)(patchedDispatchScrollPosition); var _useAcknowledgedActiv = (0, _useAcknowledgedActivity.default)(), _useAcknowledgedActiv2 = _slicedToArray(_useAcknowledgedActiv, 1), lastInteractedActivity = _useAcknowledgedActiv2[0]; var indexOfLastInteractedActivity = activities.indexOf(lastInteractedActivity); // Create a new ID for aria-activedescendant every time the active descendant change. // In our design, the transcript will only have 1 focused activity and it has an ID. Other blurred activities will not have ID assigned. // This help with performance. // But browser usually do noop if the value of aria-activedescendant doesn't change. // That means, if we assign the same ID to another element, browser will do noop. // We need to generate a new ID so the browser see there is a change in aria-activedescendant value and perform accordingly. var activeDescendantElementId = (0, _react.useMemo)(function () { return focusedActivityKey && "webchat__basic-transcript__active-descendant-".concat((0, _mathRandom.default)().toString(36).substr(2, 5)); }, [focusedActivityKey]); var scrollActiveDescendantIntoView = (0, _react.useCallback)(function () { var activeDescendant = activeDescendantElementId && document.getElementById(activeDescendantElementId); // Don't scroll active descendant into view if the focus is already inside it. // Otherwise, given the focus is on the send box, clicking on any <input> inside the Adaptive Cards may cause the view to move. // This UX is not desirable because click should not cause scroll. if (activeDescendant && !activeDescendant.contains(document.activeElement)) { // Checks if scrollIntoView support options or not. // - https://github.com/Modernizr/Modernizr/issues/1568#issuecomment-419457972 // - https://stackoverflow.com/questions/46919627/is-it-possible-to-test-for-scrollintoview-browser-compatibility if ('scrollBehavior' in document.documentElement.style) { activeDescendant.scrollIntoView({ block: 'nearest' }); } else { // This is for browser that does not support options passed to scrollIntoView(), possibly IE11. var scrollableElement = rootElementRef.current.querySelector('.webchat__basic-transcript__scrollable'); var scrollTopAtTopSide = activeDescendant.offsetTop; var scrollTopAtBottomSide = activeDescendant.offsetTop + activeDescendant.offsetHeight; if (scrollTopAtTopSide < scrollableElement.scrollTop) { scrollableElement.scrollTop = scrollTopAtTopSide; } else if (scrollTopAtBottomSide > scrollableElement.scrollTop + scrollableElement.offsetHeight) { scrollableElement.scrollTop = scrollTopAtBottomSide - scrollableElement.offsetHeight; } } } }, [activeDescendantElementId, rootElementRef]); var handleTranscriptFocus = (0, _react.useCallback)(function (event) { var currentTarget = event.currentTarget, target = event.target; // When focus is placed on the transcript, scroll active descendant into the view. currentTarget === target && scrollActiveDescendantIntoView(); }, [scrollActiveDescendantIntoView]); // After new aria-activedescendant is assigned, we will need to scroll it into view. // User agent will scroll automatically for focusing element, but not for aria-activedescendant. // We need to do the scrolling manually. (0, _react.useEffect)(function () { return scrollActiveDescendantIntoView(); }, [scrollActiveDescendantIntoView]); // If any activities has changed, reset the active descendant. (0, _react.useEffect)(function () { return setFocusedActivityKey(undefined); }, [activities, setFocusedActivityKey]); var focusRelativeActivity = (0, _react.useCallback)(function (delta) { if (isNaN(delta) || !renderingElements.length) { return setFocusedActivityKey(undefined); } var index = renderingElements.findIndex(function (_ref14) { var key = _ref14.key; return key === focusedActivityKey; }); var nextIndex = ~index ? Math.max(0, Math.min(renderingElements.length - 1, index + delta)) : renderingElements.length - 1; var nextFocusedActivity = renderingElements[nextIndex]; setFocusedActivityKey(nextFocusedActivity.key); rootElementRef.current && rootElementRef.current.focus(); }, [focusedActivityKey, renderingElements, rootElementRef, setFocusedActivityKey]); var handleTranscriptKeyDown = (0, _react.useCallback)(function (event) { var target = event.target; var fromEndOfTranscriptIndicator = target === terminatorRef.current; var fromTranscript = target === event.currentTarget; if (!fromEndOfTranscriptIndicator && !fromTranscript) { return; } var handled = true; switch (event.key) { case 'ArrowDown': focusRelativeActivity(fromEndOfTranscriptIndicator ? 0 : 1); break; case 'ArrowUp': focusRelativeActivity(fromEndOfTranscriptIndicator ? 0 : -1); break; case 'End': focusRelativeActivity(Infinity); break; case 'Enter': if (!fromEndOfTranscriptIndicator) { var focusedActivityEntry = renderingElements.find(function (_ref15) { var key = _ref15.key; return key === focusedActivityKey; }); if (focusedActivityEntry) { var _ref16 = activityElementsRef.current.find(function (_ref17) { var activity = _ref17.activity; return activity === focusedActivityEntry.activity; }) || {}, focusedActivityElement = _ref16.element; if (focusedActivityElement) { var _tabbableElements$fil = (0, _tabbableElements.default)(focusedActivityElement).filter(function (_ref18) { var className = _ref18.className; return className !== 'webchat__basic-transcript__activity-sentinel'; }), _tabbableElements$fil2 = _slicedToArray(_tabbableElements$fil, 1), firstTabbableElement = _tabbableElements$fil2[0]; firstTabbableElement && firstTabbableElement.focus(); } } } break; case 'Escape': focus('sendBoxWithoutKeyboard'); break; case 'Home': focusRelativeActivity(-Infinity); break; default: handled = false; break; } if (handled) { event.preventDefault(); // If a custom HTML control wants to handle up/down arrow, we will prevent them from listening to this event to prevent bugs due to handling arrow keys twice. event.stopPropagation(); } }, [focusedActivityKey, activityElementsRef, focusRelativeActivity, focus, terminatorRef, renderingElements]); var labelId = (0, _useUniqueId.default)('webchat__basic-transcript__label'); // If SHIFT-TAB from "End of transcript" indicator, if focusedActivityKey is not set (or no longer exists), set it the the bottommost activity. var setBottommostFocusedActivityKeyIfNeeded = (0, _react.useCallback)(function () { if (!~renderingElements.findIndex(function (_ref19) { var key = _ref19.key; return key === focusedActivityKey; })) { var _ref20 = renderingElements[renderingElements.length - 1] || {}, lastActivityKey = _ref20.key; setFocusedActivityKey(lastActivityKey); } }, [focusedActivityKey, renderingElements, setFocusedActivityKey]); var handleTranscriptKeyDownCapture = (0, _react.useCallback)(function (event) { var altKey = event.altKey, ctrlKey = event.ctrlKey, key = event.key, metaKey = event.metaKey, target = event.target; if (altKey || ctrlKey && key !== 'v' || metaKey || !(0, _inputtableKey.default)(key) && key !== 'Backspace') { // Ignore if one of the utility key (except SHIFT) is pressed // E.g. CTRL-C on a link in one of the message should not jump to chat box // E.g. "A" or "Backspace" should jump to chat box return; } // Send keystrokes to send box if we are focusing on the transcript or terminator. if (target === event.currentTarget || target === terminatorRef.current) { event.stopPropagation(); focus('sendBox'); } }, [focus]); var focusTranscriptCallback = (0, _react.useCallback)(function () { return rootElementRef.current && rootElementRef.current.focus(); }, [rootElementRef]); (0, _useRegisterFocusTranscript.default)(focusTranscriptCallback); var handleFocusActivity = (0, _react.useCallback)(function (key) { setFocusedActivityKey(key); rootElementRef.current && rootElementRef.current.focus(); }, [setFocusedActivityKey]); // When the focusing activity has changed, dispatch an event to observers of "useObserveTranscriptFocus". var dispatchTranscriptFocus = (0, _useDispatchTranscriptFocus.default)(); var focusedActivity = (0, _react.useMemo)(function () { var _ref21 = renderingElements.find(function (_ref22) { var key = _ref22.key; return key === focusedActivityKey; }) || {}, activity = _ref21.activity; return activity; }, [focusedActivityKey, renderingElements]); (0, _react.useMemo)(function () { return dispatchTranscriptFocus && dispatchTranscriptFocus({ activity: focusedActivity }); }, [dispatchTranscriptFocus, focusedActivity]); // This is required by IE11. // When the user clicks on and empty space (a.k.a. filler) in an empty transcript, IE11 says the focus is on the <div className="filler">, // despite the fact there are no "tabIndex" attributes set on the filler. // We need to artificially send the focus back to the transcript. var handleFocusFiller = (0, _react.useCallback)(function () { var current = rootElementRef.current; current && current.focus(); }, [rootElementRef]); return /*#__PURE__*/_react.default.createElement("div", { "aria-activedescendant": focusedActivityKey ? activeDescendantElementId : undefined, "aria-labelledby": labelId, className: (0, _classnames.default)('webchat__basic-transcript', basicTranscriptStyleSet + '', rootClassName, (className || '') + ''), dir: direction, onFocus: handleTranscriptFocus, onKeyDown: handleTranscriptKeyDown, onKeyDownCapture: handleTranscriptKeyDownCapture, ref: rootElementRef // "aria-activedescendant" will only works with a number of roles and it must be explicitly set. // https://www.w3.org/TR/wai-aria/#aria-activedescendant , role: "group" // For up/down arrow key navigation across activities, this component must be included in the tab sequence. // Otherwise, "aria-activedescendant" will not be narrated when the user press up/down arrow keys. // https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_focus_activedescendant , tabIndex: 0 }, /*#__PURE__*/_react.default.createElement(_ScreenReaderText.default, { id: labelId, text: transcriptAriaLabel }), /*#__PURE__*/_react.default.createElement("section", { "aria-atomic": false, "aria-live": "polite", "aria-relevant": "additions", "aria-roledescription": transcriptRoleDescription, role: "log" }, renderingElements.map(function (_ref23) { var activity = _ref23.activity, liveRegionKey = _ref23.liveRegionKey; return /*#__PURE__*/_react.default.createElement(_Fade.default, { fadeAfter: internalLiveRegionFadeAfter, key: liveRegionKey }, function () { return /*#__PURE__*/_react.default.createElement(_ScreenReaderActivity.default, { activity: activity }); }); })), /*#__PURE__*/_react.default.createElement(InternalTranscriptScrollable, { activities: renderingActivities, onFocusActivity: handleFocusActivity, onFocusFiller: handleFocusFiller, terminatorRef: terminatorRef }, renderingElements.map(function (_ref24, index) { var activity = _ref24.activity, callbackRef = _ref24.callbackRef, focusActivity = _ref24.focusActivity, handleFocus = _ref24.handleFocus, handleKeyDown = _ref24.handleKeyDown, handleMouseDownCapture = _ref24.handleMouseDownCapture, hideTimestamp = _ref24.hideTimestamp, key = _ref24.key, renderActivity = _ref24.renderActivity, renderActivityStatus = _ref24.renderActivityStatus, renderAvatar = _ref24.renderAvatar, role = _ref24.role, shouldSpeak = _ref24.shouldSpeak, showCallout = _ref24.showCallout; var _ref25 = activityElementsRef.current.find(function (entry) { return entry.activity === activity; }) || {}, ariaLabelID = _ref25.ariaLabelID, element = _ref25.element; var activeDescendant = focusedActivityKey === key; var isContentInteractive = !!(element ? (0, _tabbableElements.default)(element.querySelector('.webchat__basic-transcript__activity-box')).length : 0); return /*#__PURE__*/_react.default.createElement("li", { "aria-labelledby": ariaLabelID, className: (0, _classnames.default)('webchat__basic-transcript__activity', { 'webchat__basic-transcript__activity--acknowledged': index <= indexOfLastInteractedActivity, 'webchat__basic-transcript__activity--from-bot': role !== 'user', 'webchat__basic-transcript__activity--from-user': role === 'user' }) // Set "id" for valid for accessibility. /* eslint-disable-next-line react/forbid-dom-props */ , id: activeDescendant ? activeDescendantElementId : undefined, key: key, onFocus: handleFocus, onKeyDown: handleKeyDown, onMouseDownCapture: handleMouseDownCapture, ref: callbackRef }, /*#__PURE__*/_react.default.createElement(_ScreenReaderActivity.default, { activity: activity, id: ariaLabelID, renderAttachments: false }, !!isContentInteractive && /*#__PURE__*/_react.default.createElement("p", null, activityInteractiveAlt)), /*#__PURE__*/_react.default.createElement(_FocusRedirector.default, { className: "webchat__basic-transcript__activity-sentinel", onFocus: focusActivity, redirectRef: rootElementRef }), /*#__PURE__*/_react.default.createElement("div", { className: "webchat__basic-transcript__activity-box" }, renderActivity({ hideTimestamp: hideTimestamp, renderActivityStatus: renderActivityStatus, renderAvatar: renderAvatar, showCallout: showCallout })), shouldSpeak && /*#__PURE__*/_react.default.createElement(_Speak.default, { activity: activity }), /*#__PURE__*/_react.default.createElement(_FocusRedirector.default, { className: "webchat__basic-transcript__activity-sentinel", onFocus: focusActivity, redirectRef: rootElementRef }), /*#__PURE__*/_react.default.createElement("div", { className: (0, _classnames.default)('webchat__basic-transcript__activity-indicator', { 'webchat__basic-transcript__activity-indicator--first': !index, 'webchat__basic-transcript__activity-indicator--focus': activeDescendant }) })); })), !!renderingElements.length && /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_FocusRedirector.default, { className: "webchat__basic-transcript__sentinel", onFocus: setBottommostFocusedActivityKeyIfNeeded, redirectRef: rootElementRef }), /*#__PURE__*/_react.default.createElement("div", { className: "webchat__basic-transcript__terminator", ref: terminatorRef, tabIndex: 0 }, /*#__PURE__*/_react.default.createElement("div", { className: "webchat__basic-transcript__terminator-body" }, /*#__PURE__*/_react.default.createElement("div", { className: "webchat__basic-transcript__terminator-text" }, terminatorText)))), /*#__PURE__*/_react.default.createElement("div", { className: "webchat__basic-transcript__focus-indicator" })); }; InternalTranscript.defaultProps = { className: '' }; InternalTranscript.propTypes = { activityElementsRef: _propTypes.default.shape({ current: _propTypes.default.array.isRequired }).isRequired, className: _propTypes.default.string }; var InternalScreenReaderTranscript = function InternalScreenReaderTranscript(_ref26) { var renderingElements = _ref26.renderingElements; var localize = useLocalizer(); var _useStyleOptions3 = useStyleOptions(), _useStyleOptions4 = _slicedToArray(_useStyleOptions3, 1), internalLiveRegionFadeAfter = _useStyleOptions4[0]; var transcriptRoleDescription = localize('TRANSCRIPT_ARIA_ROLE_ALT'); return /*#__PURE__*/_react.default.createElement("section", { "aria-atomic": false, "aria-live": "polite", "aria-relevant": "additions", "aria-roledescription": transcriptRoleDescription, role: "log" }, renderingElements.map(function (_ref27) { var activity = _ref27.activity, liveRegionKey = _ref27.liveRegionKey; return /*#__PURE__*/_react.default.createElement(_Fade.default, { fadeAfter: internalLiveRegionFadeAfter, key: liveRegionKey }, function () { return /*#__PURE__*/_react.default.createElement(_ScreenReaderActivity.default, { activity: activity }); }); })); }; InternalScreenReaderTranscript.propTypes = { renderingElements: _propTypes.default.arrayOf(_propTypes.default.shape({ activity: _propTypes.default.any, liveRegionKey: _propTypes.default.string })).isRequired }; // Separating high-frequency hooks to improve performance. var InternalTranscriptScrollable = function InternalTranscriptScrollable(_ref28) { var activities = _ref28.activities, children = _ref28.children, onFocusActivity = _ref28.onFocusActivity, onFocusFiller = _ref28.onFocusFiller, terminatorRef = _ref28.terminatorRef; var _useStyleSet3 = (0, _useStyleSet5.default)(), _useStyleSet4 = _slicedToArray(_useStyleSet3, 1), activitiesStyleSet = _useStyleSet4[0].activities; var _useStyleOptions5 = useStyleOptions(), _useStyleOptions6 = _slicedToArray(_useStyleOptions5, 1), hideScrollToEndButton = _useStyleOptions6[0].hideScrollToEndButton; var _useAnimatingToEnd = (0, _reactScrollToBottom.useAnimatingToEnd)(), _useAnimatingToEnd2 = _slicedToArray(_useAnimatingToEnd, 1), animatingToEnd = _useAnimatingToEnd2[0]; var _useSticky = (0, _reactScrollToBottom.useSticky)(), _useSticky2 = _slicedToArray(_useSticky, 1), sticky = _useSticky2[0]; var lastVisibleActivityId = (0, _getActivityUniqueId.default)(activities[activities.length - 1] || {}); // Activity ID of the last visible activity in the list. var localize = useLocalizer(); var scrollToEndButtonRef = (0, _react.useRef)(); var lastReadActivityIdRef = (0, _react.useRef)(lastVisibleActivityId); var transcriptRoleDescription = localize('TRANSCRIPT_ARIA_ROLE_ALT'); var allActivitiesRead = lastVisibleActivityId === lastReadActivityIdRef.current; var handleScrollToEndButtonClick = (0, _react.useCallback)(function () { // After the "New message" button is clicked, focus on the first unread activity. var index = activities.findIndex(function (_ref29) { var id = _ref29.id; return id === lastReadActivityIdRef.current; }); if (~index) { var firstUnreadActivity = activities[index + 1]; if (firstUnreadActivity) { return onFocusActivity((0, _getActivityUniqueId.default)(firstUnreadActivity)); } } var current = terminatorRef.current; current && current.focus(); }, [activities, lastReadActivityIdRef, onFocusActivity, terminatorRef]); if (sticky) { // If it is sticky, the user is at the bottom of the transcript, everything is read. // So mark the activity ID as read. lastReadActivityIdRef.current = lastVisibleActivityId; } // Finds where we should render the "New messages" button, in in