@patternfly/chatbot 6.8.0-prerelease.2 → 6.8.0-prerelease.4

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.
Files changed (90) hide show
  1. package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.js +2 -5
  2. package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +3 -3
  3. package/dist/cjs/ChatbotHeader/ChatbotHeaderCloseButton.js +1 -1
  4. package/dist/cjs/ChatbotHeader/ChatbotHeaderNewChatButton.js +2 -2
  5. package/dist/cjs/MarkdownContent/MarkdownContent.js +15 -1
  6. package/dist/cjs/Message/CodeBlockMessage/CodeBlockMessage.js +2 -2
  7. package/dist/cjs/Message/LinkMessage/LinkMessage.js +5 -1
  8. package/dist/cjs/Message/Message.test.js +3 -1
  9. package/dist/cjs/Message/QuickStarts/QuickStartTileDescription.js +2 -5
  10. package/dist/cjs/MessageBox/JumpButton.test.js +2 -2
  11. package/dist/cjs/MessageBox/MessageBox.js +79 -28
  12. package/dist/cjs/MessageBox/MessageBox.test.d.ts +1 -1
  13. package/dist/cjs/MessageBox/MessageBox.test.js +59 -0
  14. package/dist/cjs/ResponseActions/ResponseActionButton.test.js +6 -6
  15. package/dist/cjs/ResponseActions/ResponseActions.test.js +6 -7
  16. package/dist/cjs/index.d.ts +0 -2
  17. package/dist/cjs/index.js +1 -4
  18. package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.js +2 -2
  19. package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +4 -4
  20. package/dist/esm/ChatbotHeader/ChatbotHeaderCloseButton.js +2 -2
  21. package/dist/esm/ChatbotHeader/ChatbotHeaderNewChatButton.js +2 -2
  22. package/dist/esm/MarkdownContent/MarkdownContent.js +15 -1
  23. package/dist/esm/Message/CodeBlockMessage/CodeBlockMessage.js +2 -2
  24. package/dist/esm/Message/LinkMessage/LinkMessage.js +6 -2
  25. package/dist/esm/Message/Message.test.js +3 -1
  26. package/dist/esm/Message/QuickStarts/QuickStartTileDescription.js +2 -2
  27. package/dist/esm/MessageBox/JumpButton.test.js +2 -2
  28. package/dist/esm/MessageBox/MessageBox.js +80 -29
  29. package/dist/esm/MessageBox/MessageBox.test.d.ts +1 -1
  30. package/dist/esm/MessageBox/MessageBox.test.js +59 -0
  31. package/dist/esm/ResponseActions/ResponseActionButton.test.js +7 -7
  32. package/dist/esm/ResponseActions/ResponseActions.test.js +7 -8
  33. package/dist/esm/index.d.ts +0 -2
  34. package/dist/esm/index.js +0 -2
  35. package/dist/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +2 -2
  37. package/patternfly-a11y.config.js +2 -1
  38. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomResponseActions.tsx +3 -4
  39. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithMarkdownToolResponse.tsx +3 -3
  40. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithQuickResponses.tsx +3 -3
  41. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolResponse.tsx +5 -5
  42. package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +1 -1
  43. package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessageWithExtraContent.tsx +7 -31
  44. package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderBasic.tsx +4 -6
  45. package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawer.tsx +3 -3
  46. package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerResizable.tsx +2 -2
  47. package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithPin.tsx +2 -2
  48. package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotHeaderDrawerWithSearchActions.tsx +3 -3
  49. package/patternfly-docs/content/extensions/chatbot/examples/UI/CompactSettings.tsx +10 -5
  50. package/patternfly-docs/content/extensions/chatbot/examples/UI/Settings.tsx +10 -5
  51. package/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +1 -15
  52. package/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md +1 -3
  53. package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +2 -5
  54. package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.tsx +4 -7
  55. package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachment.tsx +4 -6
  56. package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotCompact.tsx +4 -7
  57. package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotDisplayMode.tsx +4 -7
  58. package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotScrolling.tsx +4 -8
  59. package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotTranscripts.tsx +4 -7
  60. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.tsx +2 -3
  61. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx +4 -4
  62. package/src/ChatbotHeader/ChatbotHeaderCloseButton.tsx +2 -2
  63. package/src/ChatbotHeader/ChatbotHeaderNewChatButton.tsx +2 -3
  64. package/src/MarkdownContent/MarkdownContent.tsx +14 -0
  65. package/src/Message/CodeBlockMessage/CodeBlockMessage.tsx +2 -2
  66. package/src/Message/LinkMessage/LinkMessage.tsx +21 -2
  67. package/src/Message/Message.test.tsx +3 -1
  68. package/src/Message/QuickStarts/QuickStartTileDescription.tsx +2 -2
  69. package/src/MessageBox/JumpButton.test.tsx +1 -1
  70. package/src/MessageBox/MessageBox.test.tsx +91 -0
  71. package/src/MessageBox/MessageBox.tsx +137 -40
  72. package/src/ResponseActions/ResponseActionButton.test.tsx +7 -7
  73. package/src/ResponseActions/ResponseActions.test.tsx +7 -8
  74. package/dist/cjs/LoadingMessage/LoadingMessage.d.ts +0 -6
  75. package/dist/cjs/LoadingMessage/LoadingMessage.js +0 -12
  76. package/dist/cjs/LoadingMessage/LoadingMessage.test.d.ts +0 -1
  77. package/dist/cjs/LoadingMessage/LoadingMessage.test.js +0 -20
  78. package/dist/cjs/LoadingMessage/index.d.ts +0 -2
  79. package/dist/cjs/LoadingMessage/index.js +0 -23
  80. package/dist/dynamic/LoadingMessage/package.json +0 -1
  81. package/dist/esm/LoadingMessage/LoadingMessage.d.ts +0 -6
  82. package/dist/esm/LoadingMessage/LoadingMessage.js +0 -5
  83. package/dist/esm/LoadingMessage/LoadingMessage.test.d.ts +0 -1
  84. package/dist/esm/LoadingMessage/LoadingMessage.test.js +0 -15
  85. package/dist/esm/LoadingMessage/index.d.ts +0 -2
  86. package/dist/esm/LoadingMessage/index.js +0 -2
  87. package/src/LoadingMessage/LoadingMessage.test.tsx +0 -15
  88. package/src/LoadingMessage/LoadingMessage.tsx +0 -27
  89. package/src/LoadingMessage/__snapshots__/LoadingMessage.test.tsx.snap +0 -64
  90. package/src/LoadingMessage/index.ts +0 -3
package/dist/cjs/index.js CHANGED
@@ -18,7 +18,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
18
18
  return (mod && mod.__esModule) ? mod : { "default": mod };
19
19
  };
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.tracking = exports.ToolResponse = exports.ToolCall = exports.TermsOfUse = exports.SourcesCardBase = exports.SourcesCard = exports.SourceDetailsMenuItem = exports.Settings = exports.ResponseActions = exports.PreviewAttachment = exports.Onboarding = exports.MessageDivider = exports.MessageBox = exports.MessageBar = exports.Message = exports.MarkdownContent = exports.LoadingMessage = exports.ImagePreview = exports.FilePreview = exports.FileDropZone = exports.FileDetailsLabel = exports.FileDetails = exports.DeepThinking = exports.Compare = exports.CodeModal = exports.ChatbotWelcomePrompt = exports.ChatbotToggle = exports.ChatbotPopover = exports.ChatbotModal = exports.ChatbotHeader = exports.ChatbotFooter = exports.ChatbotConversationHistoryNav = exports.ChatbotContent = exports.ChatbotAlert = exports.Chatbot = exports.AttachMenu = exports.AttachmentEdit = void 0;
21
+ exports.tracking = exports.ToolResponse = exports.ToolCall = exports.TermsOfUse = exports.SourcesCardBase = exports.SourcesCard = exports.SourceDetailsMenuItem = exports.Settings = exports.ResponseActions = exports.PreviewAttachment = exports.Onboarding = exports.MessageDivider = exports.MessageBox = exports.MessageBar = exports.Message = exports.MarkdownContent = exports.ImagePreview = exports.FilePreview = exports.FileDropZone = exports.FileDetailsLabel = exports.FileDetails = exports.DeepThinking = exports.Compare = exports.CodeModal = exports.ChatbotWelcomePrompt = exports.ChatbotToggle = exports.ChatbotPopover = exports.ChatbotModal = exports.ChatbotHeader = exports.ChatbotFooter = exports.ChatbotConversationHistoryNav = exports.ChatbotContent = exports.ChatbotAlert = exports.Chatbot = exports.AttachMenu = exports.AttachmentEdit = void 0;
22
22
  var AttachmentEdit_1 = require("./AttachmentEdit");
23
23
  Object.defineProperty(exports, "AttachmentEdit", { enumerable: true, get: function () { return __importDefault(AttachmentEdit_1).default; } });
24
24
  __exportStar(require("./AttachmentEdit"), exports);
@@ -79,9 +79,6 @@ __exportStar(require("./FilePreview"), exports);
79
79
  var ImagePreview_1 = require("./ImagePreview");
80
80
  Object.defineProperty(exports, "ImagePreview", { enumerable: true, get: function () { return __importDefault(ImagePreview_1).default; } });
81
81
  __exportStar(require("./ImagePreview"), exports);
82
- var LoadingMessage_1 = require("./LoadingMessage");
83
- Object.defineProperty(exports, "LoadingMessage", { enumerable: true, get: function () { return __importDefault(LoadingMessage_1).default; } });
84
- __exportStar(require("./LoadingMessage"), exports);
85
82
  var MarkdownContent_1 = require("./MarkdownContent");
86
83
  Object.defineProperty(exports, "MarkdownContent", { enumerable: true, get: function () { return __importDefault(MarkdownContent_1).default; } });
87
84
  __exportStar(require("./MarkdownContent"), exports);
@@ -2,12 +2,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  // Import PatternFly components
4
4
  import { Tooltip, MenuToggle, Dropdown } from '@patternfly/react-core';
5
- import EllipsisIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
5
+ import { RhUiEllipsisVerticalFillIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-ellipsis-vertical-fill-icon';
6
6
  export const ChatbotConversationHistoryDropdown = ({ menuItems, menuClassName, onSelect, label = 'Conversation options', 'aria-label': ariaLabel, id }) => {
7
7
  const [isOpen, setIsOpen] = useState(false);
8
8
  const toggle = (toggleRef) => (_jsx(Tooltip, { className: "pf-chatbot__tooltip", content: label, position: "bottom",
9
9
  // prevents VO announcements of both aria label and tooltip
10
- aria: "none", children: _jsx(MenuToggle, { className: "pf-chatbot__history-actions", variant: "plain", "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : label, ref: toggleRef, isExpanded: isOpen, onClick: () => setIsOpen(!isOpen), id: id, role: "menuitem", children: _jsx(EllipsisIcon, {}) }) }));
10
+ aria: "none", children: _jsx(MenuToggle, { className: "pf-chatbot__history-actions", variant: "plain", "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : label, ref: toggleRef, isExpanded: isOpen, onClick: () => setIsOpen(!isOpen), id: id, role: "menuitem", children: _jsx(RhUiEllipsisVerticalFillIcon, {}) }) }));
11
11
  return (_jsx(Dropdown, { className: `pf-chatbot__selections ${menuClassName !== null && menuClassName !== void 0 ? menuClassName : ''}`, isOpen: isOpen, onSelect: (props) => {
12
12
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(props);
13
13
  setIsOpen((prev) => !prev);
@@ -13,13 +13,13 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
13
13
  import { useRef, Fragment } from 'react';
14
14
  // Import PatternFly components
15
15
  import { Button, Divider, Drawer, DrawerPanelContent, DrawerContent, DrawerPanelBody, DrawerHead, DrawerActions, DrawerCloseButton, DrawerContentBody, InputGroup, InputGroupItem, SearchInput, Title, Icon, MenuList, MenuGroup, MenuItem, Menu, MenuContent } from '@patternfly/react-core';
16
- import { OutlinedClockIcon, OutlinedCommentAltIcon, PenToSquareIcon } from '@patternfly/react-icons';
16
+ import { RhUiClockIcon, RhUiCommentIcon, RhUiEditFillIcon } from '@patternfly/react-icons';
17
17
  import { ChatbotDisplayMode } from '../Chatbot/Chatbot';
18
18
  import ConversationHistoryDropdown from './ChatbotConversationHistoryDropdown';
19
19
  import LoadingState from './LoadingState';
20
20
  import HistoryEmptyState from './EmptyState';
21
21
  export const ChatbotConversationHistoryNav = (_a) => {
22
- var { onDrawerToggle, isDrawerOpen, setIsDrawerOpen, activeItemId, onSelectActiveItem, conversations, menuListProps, newChatButtonText = 'New chat', drawerContent, onNewChat, newChatButtonProps, searchInputPlaceholder = 'Search previous conversations...', searchInputAriaLabel = 'Search previous conversations', searchInputProps, handleTextInputChange, displayMode, reverseButtonOrder = false, drawerActionsTestId = 'chatbot-nav-drawer-actions', drawerPanelContentProps, drawerContentProps, drawerContentBodyProps, drawerHeadProps, drawerActionsProps, drawerCloseButtonProps, drawerPanelBodyProps, hasDrawerHeadDivider, isLoading, loadingState, errorState, emptyState, noResultsState, isCompact, title = 'Chat history', navTitleProps, navTitleIcon = _jsx(OutlinedClockIcon, {}), searchInputScreenReaderText, searchActionStart, searchActionEnd, searchToolbar, menuProps, menuGroupProps, menuContentProps } = _a, props = __rest(_a, ["onDrawerToggle", "isDrawerOpen", "setIsDrawerOpen", "activeItemId", "onSelectActiveItem", "conversations", "menuListProps", "newChatButtonText", "drawerContent", "onNewChat", "newChatButtonProps", "searchInputPlaceholder", "searchInputAriaLabel", "searchInputProps", "handleTextInputChange", "displayMode", "reverseButtonOrder", "drawerActionsTestId", "drawerPanelContentProps", "drawerContentProps", "drawerContentBodyProps", "drawerHeadProps", "drawerActionsProps", "drawerCloseButtonProps", "drawerPanelBodyProps", "hasDrawerHeadDivider", "isLoading", "loadingState", "errorState", "emptyState", "noResultsState", "isCompact", "title", "navTitleProps", "navTitleIcon", "searchInputScreenReaderText", "searchActionStart", "searchActionEnd", "searchToolbar", "menuProps", "menuGroupProps", "menuContentProps"]);
22
+ var { onDrawerToggle, isDrawerOpen, setIsDrawerOpen, activeItemId, onSelectActiveItem, conversations, menuListProps, newChatButtonText = 'New chat', drawerContent, onNewChat, newChatButtonProps, searchInputPlaceholder = 'Search previous conversations...', searchInputAriaLabel = 'Search previous conversations', searchInputProps, handleTextInputChange, displayMode, reverseButtonOrder = false, drawerActionsTestId = 'chatbot-nav-drawer-actions', drawerPanelContentProps, drawerContentProps, drawerContentBodyProps, drawerHeadProps, drawerActionsProps, drawerCloseButtonProps, drawerPanelBodyProps, hasDrawerHeadDivider, isLoading, loadingState, errorState, emptyState, noResultsState, isCompact, title = 'Chat history', navTitleProps, navTitleIcon = _jsx(RhUiClockIcon, {}), searchInputScreenReaderText, searchActionStart, searchActionEnd, searchToolbar, menuProps, menuGroupProps, menuContentProps } = _a, props = __rest(_a, ["onDrawerToggle", "isDrawerOpen", "setIsDrawerOpen", "activeItemId", "onSelectActiveItem", "conversations", "menuListProps", "newChatButtonText", "drawerContent", "onNewChat", "newChatButtonProps", "searchInputPlaceholder", "searchInputAriaLabel", "searchInputProps", "handleTextInputChange", "displayMode", "reverseButtonOrder", "drawerActionsTestId", "drawerPanelContentProps", "drawerContentProps", "drawerContentBodyProps", "drawerHeadProps", "drawerActionsProps", "drawerCloseButtonProps", "drawerPanelBodyProps", "hasDrawerHeadDivider", "isLoading", "loadingState", "errorState", "emptyState", "noResultsState", "isCompact", "title", "navTitleProps", "navTitleIcon", "searchInputScreenReaderText", "searchActionStart", "searchActionEnd", "searchToolbar", "menuProps", "menuGroupProps", "menuContentProps"]);
23
23
  const drawerRef = useRef(null);
24
24
  const onExpand = () => {
25
25
  drawerRef.current && drawerRef.current.focus();
@@ -27,7 +27,7 @@ export const ChatbotConversationHistoryNav = (_a) => {
27
27
  const isConversation = (item) => item && typeof item === 'object' && 'id' in item && 'text' in item;
28
28
  const getNavItem = (conversation) => {
29
29
  var _a;
30
- return (_jsx(MenuItem, Object.assign({ className: `pf-chatbot__menu-item ${activeItemId && activeItemId === conversation.id ? 'pf-chatbot__menu-item--active' : ''}`, itemId: conversation.id }, (conversation.noIcon ? {} : { icon: (_a = conversation.icon) !== null && _a !== void 0 ? _a : _jsx(OutlinedCommentAltIcon, {}) }), (conversation.menuItems
30
+ return (_jsx(MenuItem, Object.assign({ className: `pf-chatbot__menu-item ${activeItemId && activeItemId === conversation.id ? 'pf-chatbot__menu-item--active' : ''}`, itemId: conversation.id }, (conversation.noIcon ? {} : { icon: (_a = conversation.icon) !== null && _a !== void 0 ? _a : _jsx(RhUiCommentIcon, {}) }), (conversation.menuItems
31
31
  ? {
32
32
  actions: (_jsx(ConversationHistoryDropdown, { menuClassName: conversation.menuClassName, onSelect: conversation.onSelect, menuItems: conversation.menuItems, label: conversation.label }))
33
33
  }
@@ -72,7 +72,7 @@ export const ChatbotConversationHistoryNav = (_a) => {
72
72
  return searchActionStart || searchActionEnd ? (_jsx("div", { className: "pf-chatbot__history-search-actions", children: _jsxs(InputGroup, { children: [searchActionStart && _jsx(InputGroupItem, { children: searchActionStart }), searchInputContainer && _jsx(InputGroupItem, { isFill: true, children: searchInputContainer }), searchActionEnd && _jsx(InputGroupItem, { children: searchActionEnd })] }) })) : (searchInputContainer);
73
73
  };
74
74
  const renderPanelContent = () => {
75
- const drawer = (_jsxs(_Fragment, { children: [_jsx(DrawerHead, Object.assign({}, drawerHeadProps, { children: _jsxs(DrawerActions, Object.assign({ "data-testid": drawerActionsTestId, className: reverseButtonOrder ? 'pf-v6-c-drawer__actions--reversed' : '' }, drawerActionsProps, { children: [_jsx(DrawerCloseButton, Object.assign({ onClick: onDrawerToggle }, drawerCloseButtonProps)), onNewChat && (_jsx(Button, Object.assign({ size: isCompact ? 'sm' : undefined, onClick: onNewChat, icon: _jsx(PenToSquareIcon, {}) }, newChatButtonProps, { children: newChatButtonText })))] })) })), hasDrawerHeadDivider && _jsx(Divider, { className: "pf-chatbot__heading-divider" }), _jsxs("div", { className: "pf-chatbot__heading-container", children: [_jsxs("div", { className: "pf-chatbot__title-container", children: [_jsx(Icon, { size: "lg", className: "pf-chatbot__title-icon", children: navTitleIcon }), _jsx(Title, Object.assign({ className: "pf-chatbot__title", headingLevel: "h2" }, navTitleProps, { children: title }))] }), !isLoading && renderSearchAndActions()] }), isLoading ? _jsx(LoadingState, Object.assign({}, loadingState)) : renderDrawerContent()] }));
75
+ const drawer = (_jsxs(_Fragment, { children: [_jsx(DrawerHead, Object.assign({}, drawerHeadProps, { children: _jsxs(DrawerActions, Object.assign({ "data-testid": drawerActionsTestId, className: reverseButtonOrder ? 'pf-v6-c-drawer__actions--reversed' : '' }, drawerActionsProps, { children: [_jsx(DrawerCloseButton, Object.assign({ onClick: onDrawerToggle }, drawerCloseButtonProps)), onNewChat && (_jsx(Button, Object.assign({ size: isCompact ? 'sm' : undefined, onClick: onNewChat, icon: _jsx(RhUiEditFillIcon, {}) }, newChatButtonProps, { children: newChatButtonText })))] })) })), hasDrawerHeadDivider && _jsx(Divider, { className: "pf-chatbot__heading-divider" }), _jsxs("div", { className: "pf-chatbot__heading-container", children: [_jsxs("div", { className: "pf-chatbot__title-container", children: [_jsx(Icon, { size: "lg", className: "pf-chatbot__title-icon", children: navTitleIcon }), _jsx(Title, Object.assign({ className: "pf-chatbot__title", headingLevel: "h2" }, navTitleProps, { children: title }))] }), !isLoading && renderSearchAndActions()] }), isLoading ? _jsx(LoadingState, Object.assign({}, loadingState)) : renderDrawerContent()] }));
76
76
  return (_jsx(DrawerPanelContent, Object.assign({ "aria-live": "polite", focusTrap: { enabled: true }, defaultSize: "384px" }, drawerPanelContentProps, { children: drawer })));
77
77
  };
78
78
  // An onKeyDown property must be passed to the Drawer component to handle closing
@@ -12,11 +12,11 @@ var __rest = (this && this.__rest) || function (s, e) {
12
12
  import { jsx as _jsx } from "react/jsx-runtime";
13
13
  import { forwardRef } from 'react';
14
14
  import { Button, Icon, Tooltip } from '@patternfly/react-core';
15
- import { CloseIcon } from '@patternfly/react-icons';
15
+ import { RhMicronsCloseIcon } from '@patternfly/react-icons';
16
16
  const ChatbotHeaderCloseButtonBase = (_a) => {
17
17
  var { className, onClick, tooltipProps, menuAriaLabel = 'Close', innerRef, tooltipContent = 'Close', isCompact } = _a, props = __rest(_a, ["className", "onClick", "tooltipProps", "menuAriaLabel", "innerRef", "tooltipContent", "isCompact"]);
18
18
  return (_jsx("div", { className: `pf-chatbot__menu${className ? ` ${className}` : ''}`, children: _jsx(Tooltip, Object.assign({ content: tooltipContent, position: "bottom",
19
19
  // prevents VO announcements of both aria label and tooltip
20
- aria: "none" }, tooltipProps, { children: _jsx(Button, Object.assign({ className: `pf-chatbot__button--toggle-menu ${isCompact ? 'pf-m-compact' : ''}`, variant: "plain", onClick: onClick, "aria-label": menuAriaLabel, ref: innerRef, icon: _jsx(Icon, { size: isCompact ? 'lg' : 'xl', isInline: true, children: _jsx(CloseIcon, {}) }), size: isCompact ? 'sm' : undefined }, props)) })) }));
20
+ aria: "none" }, tooltipProps, { children: _jsx(Button, Object.assign({ className: `pf-chatbot__button--toggle-menu ${isCompact ? 'pf-m-compact' : ''}`, variant: "plain", onClick: onClick, "aria-label": menuAriaLabel, ref: innerRef, icon: _jsx(Icon, { size: isCompact ? 'lg' : 'xl', isInline: true, children: _jsx(RhMicronsCloseIcon, {}) }), size: isCompact ? 'sm' : undefined }, props)) })) }));
21
21
  };
22
22
  export const ChatbotHeaderCloseButton = forwardRef((props, ref) => (_jsx(ChatbotHeaderCloseButtonBase, Object.assign({ innerRef: ref }, props))));
@@ -12,11 +12,11 @@ var __rest = (this && this.__rest) || function (s, e) {
12
12
  import { jsx as _jsx } from "react/jsx-runtime";
13
13
  import { forwardRef } from 'react';
14
14
  import { Button, Icon, Tooltip } from '@patternfly/react-core';
15
- import { PenToSquareIcon } from '@patternfly/react-icons/dist/esm/icons/pen-to-square-icon';
15
+ import { RhUiEditFillIcon } from '@patternfly/react-icons/dist/esm/icons/rh-ui-edit-fill-icon';
16
16
  const ChatbotHeaderNewChatButtonBase = (_a) => {
17
17
  var { className, onClick, tooltipProps, menuAriaLabel = 'New chat', innerRef, tooltipContent = 'New chat', isCompact } = _a, props = __rest(_a, ["className", "onClick", "tooltipProps", "menuAriaLabel", "innerRef", "tooltipContent", "isCompact"]);
18
18
  return (_jsx("div", { className: `pf-chatbot__menu${className ? ` ${className}` : ''}`, children: _jsx(Tooltip, Object.assign({ content: tooltipContent, position: "bottom",
19
19
  // prevents VO announcements of both aria label and tooltip
20
- aria: "none" }, tooltipProps, { children: _jsx(Button, Object.assign({ className: `pf-chatbot__button--toggle-menu ${isCompact ? 'pf-m-compact' : ''}`, variant: "plain", onClick: onClick, "aria-label": menuAriaLabel, ref: innerRef, icon: _jsx(Icon, { size: isCompact ? 'lg' : 'xl', isInline: true, children: _jsx(PenToSquareIcon, {}) }), size: isCompact ? 'sm' : undefined }, props)) })) }));
20
+ aria: "none" }, tooltipProps, { children: _jsx(Button, Object.assign({ className: `pf-chatbot__button--toggle-menu ${isCompact ? 'pf-m-compact' : ''}`, variant: "plain", onClick: onClick, "aria-label": menuAriaLabel, ref: innerRef, icon: _jsx(Icon, { size: isCompact ? 'lg' : 'xl', isInline: true, children: _jsx(RhUiEditFillIcon, {}) }), size: isCompact ? 'sm' : undefined }, props)) })) }));
21
21
  };
22
22
  export const ChatbotHeaderNewChatButton = forwardRef((props, ref) => (_jsx(ChatbotHeaderNewChatButtonBase, Object.assign({ innerRef: ref }, props))));
@@ -169,6 +169,20 @@ export const MarkdownContent = ({ content, isMarkdownDisabled, codeBlockProps, t
169
169
  }
170
170
  }, remarkPlugins: [[remarkGfm, Object.assign({}, remarkGfmProps)], ...additionalRemarkPlugins], rehypePlugins: rehypePlugins }, reactMarkdownProps, { remarkRehypeOptions: Object.assign({
171
171
  // removes sr-only class from footnote labels applied by default
172
- footnoteLabelProperties: { className: [''] } }, reactMarkdownProps === null || reactMarkdownProps === void 0 ? void 0 : reactMarkdownProps.remarkRehypeOptions), disallowedElements: disallowedElements, children: content })));
172
+ footnoteLabelProperties: { className: [''] },
173
+ // omit default ↩ text; backref icon is rendered in LinkMessage
174
+ footnoteBackContent: (_referenceIndex, rereferenceIndex) => {
175
+ if (rereferenceIndex > 1) {
176
+ return [
177
+ {
178
+ type: 'element',
179
+ tagName: 'sup',
180
+ properties: {},
181
+ children: [{ type: 'text', value: String(rereferenceIndex) }]
182
+ }
183
+ ];
184
+ }
185
+ return [];
186
+ } }, reactMarkdownProps === null || reactMarkdownProps === void 0 ? void 0 : reactMarkdownProps.remarkRehypeOptions), disallowedElements: disallowedElements, children: content })));
173
187
  };
174
188
  export default MarkdownContent;
@@ -17,8 +17,8 @@ import { useState, useRef, useCallback, useEffect } from 'react';
17
17
  // Import PatternFly components
18
18
  import { CodeBlock, CodeBlockAction, CodeBlockCode, Button, Tooltip, ExpandableSection, ExpandableSectionToggle, ExpandableSectionVariant, getUniqueId } from '@patternfly/react-core';
19
19
  import { CheckIcon } from '@patternfly/react-icons/dist/esm/icons/check-icon';
20
- import { CopyIcon } from '@patternfly/react-icons/dist/esm/icons/copy-icon';
21
20
  import { css } from '@patternfly/react-styles';
21
+ import { RhUiCopyFillIcon } from '@patternfly/react-icons';
22
22
  const DEFAULT_EXPANDED_TEXT = 'Show less';
23
23
  const DEFAULT_COLLAPSED_TEXT = 'Show more';
24
24
  const CodeBlockMessage = (_a) => {
@@ -73,7 +73,7 @@ const CodeBlockMessage = (_a) => {
73
73
  return (_jsx("code", Object.assign({}, props, { className: `pf-chatbot__message-inline-code ${isPrimary ? 'pf-m-primary' : ''}`, children: children })));
74
74
  }
75
75
  // Setup code block header
76
- const actions = (_jsxs(_Fragment, { children: [_jsxs(CodeBlockAction, { className: "pf-chatbot__message-code-block-default-action", children: [language && _jsx("div", { className: "pf-chatbot__message-code-block-language", children: language }), _jsx(Button, { ref: buttonRef, "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : 'Copy code', variant: "plain", className: "pf-chatbot__button--copy", onClick: (event) => handleCopy(event, children), children: copied ? _jsx(CheckIcon, {}) : _jsx(CopyIcon, {}) }), _jsx(Tooltip, { id: tooltipID, content: "Copy", position: "top", triggerRef: buttonRef })] }), customActions] }));
76
+ const actions = (_jsxs(_Fragment, { children: [_jsxs(CodeBlockAction, { className: "pf-chatbot__message-code-block-default-action", children: [language && _jsx("div", { className: "pf-chatbot__message-code-block-language", children: language }), _jsx(Button, { ref: buttonRef, "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : 'Copy code', variant: "plain", className: "pf-chatbot__button--copy", onClick: (event) => handleCopy(event, children), children: copied ? _jsx(CheckIcon, {}) : _jsx(RhUiCopyFillIcon, {}) }), _jsx(Tooltip, { id: tooltipID, content: "Copy", position: "top", triggerRef: buttonRef })] }), customActions] }));
77
77
  return (_jsx("div", { className: css('pf-chatbot__message-code-block', shouldRetainStyles && 'pf-m-markdown'), ref: codeBlockRef, children: _jsxs(CodeBlock, { actions: actions, children: [_jsx(CodeBlockCode, { children: _jsx(_Fragment, { children: isExpandable ? (_jsx(ExpandableSection, Object.assign({ variant: ExpandableSectionVariant.truncate, isExpanded: isExpanded, isDetached: true, toggleId: toggleId, contentId: contentId }, expandableSectionProps, { children: children }))) : (children) }) }), isExpandable && (_jsx(ExpandableSectionToggle, Object.assign({ isExpanded: isExpanded, onToggle: onToggle, direction: "up", toggleId: toggleId, contentId: contentId, hasTruncatedContent: true, className: "pf-chatbot__message-code-toggle" }, expandableSectionToggleProps, { children: isExpanded ? finalExpandedText : finalCollapsedText })))] }) }));
78
78
  };
79
79
  export default CodeBlockMessage;
@@ -14,15 +14,19 @@ import { jsx as _jsx } from "react/jsx-runtime";
14
14
  // Chatbot Main - Message - Content - Link
15
15
  // ============================================================================
16
16
  import { Button } from '@patternfly/react-core';
17
- import { ExternalLinkSquareAltIcon } from '@patternfly/react-icons';
17
+ import { RhMicronsExternalLinkIcon, RhUiBackwardsIcon } from '@patternfly/react-icons';
18
18
  import { css } from '@patternfly/react-styles';
19
+ const isFootnoteBackref = (className) => className === null || className === void 0 ? void 0 : className.includes('data-footnote-backref');
19
20
  const LinkMessage = (_a) => {
20
21
  var { children, target, href, id, shouldRetainStyles } = _a, props = __rest(_a, ["children", "target", "href", "id", "shouldRetainStyles"]);
21
22
  if (target === '_blank') {
22
- return (_jsx(Button, Object.assign({ component: "a", variant: "link", href: href, icon: _jsx(ExternalLinkSquareAltIcon, {}), iconPosition: "end", isInline: true, target: target,
23
+ return (_jsx(Button, Object.assign({ component: "a", variant: "link", href: href, icon: _jsx(RhMicronsExternalLinkIcon, {}), iconPosition: "end", isInline: true, target: target,
23
24
  // need to explicitly call this out or id doesn't seem to get passed - required for footnotes
24
25
  id: id }, props, { className: css(shouldRetainStyles && 'pf-m-markdown', props === null || props === void 0 ? void 0 : props.className), children: children })));
25
26
  }
27
+ if (isFootnoteBackref(props.className)) {
28
+ return (_jsx(Button, Object.assign({ isInline: true, component: "a", href: href, variant: "link", id: id, icon: _jsx(RhUiBackwardsIcon, {}) }, props, { className: css(shouldRetainStyles && 'pf-m-markdown', props === null || props === void 0 ? void 0 : props.className), children: children })));
29
+ }
26
30
  return (
27
31
  // need to explicitly call this out or id doesn't seem to get passed - required for footnotes
28
32
  _jsx(Button, Object.assign({ isInline: true, component: "a", href: href, variant: "link", id: id }, props, { className: css(shouldRetainStyles && 'pf-m-markdown', props === null || props === void 0 ? void 0 : props.className), children: children })));
@@ -31,10 +31,12 @@ jest.mock('@patternfly/react-icons', () => ({
31
31
  RhUiVolumeUpFillIcon: () => _jsx("div", { children: "RhUiVolumeUpFillIcon" }),
32
32
  RhUiExportIcon: () => _jsx("div", { children: "RhUiExportIcon" }),
33
33
  RhUiExportFillIcon: () => _jsx("div", { children: "RhUiExportFillIcon" }),
34
+ RhMicronsInformationFillIcon: () => _jsx("div", { children: "RhMicronsInformationFillIcon" }),
34
35
  CheckIcon: () => _jsx("div", { children: "CheckIcon" }),
35
36
  CloseIcon: () => _jsx("div", { children: "CloseIcon" }),
36
37
  ExternalLinkSquareAltIcon: () => _jsx("div", { children: "ExternalLinkSquareAltIcon" }),
37
- TimesIcon: () => _jsx("div", { children: "TimesIcon" })
38
+ TimesIcon: () => _jsx("div", { children: "TimesIcon" }),
39
+ RhUiBackwardsIcon: () => _jsx("div", { children: "RhUiBackwardsIcon" })
38
40
  }));
39
41
  const ALL_ACTIONS = [
40
42
  { label: /Good response/i },
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useRef } from 'react';
3
3
  import { Button, Flex, pluralize, Popover } from '@patternfly/react-core';
4
- import InfoCircleIcon from '@patternfly/react-icons/dist/js/icons/info-circle-icon';
4
+ import { RhMicronsInformationFillIcon } from '@patternfly/react-icons';
5
5
  /** This function is a helper for pluralizing strings stolen from React.
6
6
  *
7
7
  * @param {number} i The quantity of the string you want to pluralize
@@ -22,7 +22,7 @@ const QuickStartTileDescription = ({ description, prerequisites, prerequisiteWor
22
22
  return (_jsxs(_Fragment, { children: [description, prereqs && prereqs.length > 0 && (_jsxs(Flex, { spaceItems: { default: 'spaceItemsSm' }, children: [_jsx("h5", { children: pluralize(prereqs.length, prerequisiteWord, prerequisiteWordPlural) }), _jsx(Button, { variant: "link", isInline: true, "data-testid": "qs-card-prereqs", ref: buttonRef, onClick: (e) => {
23
23
  e.preventDefault();
24
24
  e.stopPropagation();
25
- }, "aria-label": quickStartButtonAriaLabel, children: _jsx(InfoCircleIcon, {}) }), _jsx(Popover, { "aria-label": pluralizedPrereq, headerContent: pluralizedPrereq, triggerRef: buttonRef, bodyContent: _jsx("div", { children: _jsx("ul", { "aria-label": pluralizedPrereq, children: prereqs.map((prerequisite, index) => (
25
+ }, "aria-label": quickStartButtonAriaLabel, children: _jsx(RhMicronsInformationFillIcon, {}) }), _jsx(Popover, { "aria-label": pluralizedPrereq, headerContent: pluralizedPrereq, triggerRef: buttonRef, bodyContent: _jsx("div", { children: _jsx("ul", { "aria-label": pluralizedPrereq, children: prereqs.map((prerequisite, index) => (
26
26
  // eslint-disable-next-line react/no-array-index-key
27
27
  _jsx("li", { children: prerequisite }, index))) }) }) })] }))] }));
28
28
  };
@@ -27,8 +27,8 @@ describe('JumpButton', () => {
27
27
  yield userEvent.click(screen.getByRole('button', { name: /Back to bottom/i }));
28
28
  expect(spy).toHaveBeenCalledTimes(1);
29
29
  }));
30
- it('should be hidden if isHidden prop is used', () => __awaiter(void 0, void 0, void 0, function* () {
30
+ it('should be hidden if isHidden prop is used', () => {
31
31
  render(_jsx(JumpButton, { position: "bottom", onClick: jest.fn(), isHidden: true }));
32
32
  expect(screen.queryByRole('button', { name: /Back to bottom/i })).toBeFalsy();
33
- }));
33
+ });
34
34
  });
@@ -13,50 +13,79 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
13
13
  // ============================================================================
14
14
  // Chatbot Main - Messages
15
15
  // ============================================================================
16
- import { useState, useRef, useCallback, useEffect, forwardRef, useImperativeHandle } from 'react';
16
+ import { useState, useRef, useCallback, useLayoutEffect, forwardRef, useImperativeHandle } from 'react';
17
17
  import JumpButton from './JumpButton';
18
+ import { getResizeObserver } from '@patternfly/react-core';
19
+ const SCROLL_EDGE_DELTA = 100;
20
+ const getScrollMetrics = (element) => {
21
+ const { scrollTop, scrollHeight, clientHeight } = element;
22
+ const roundedScrollTop = Math.round(scrollTop);
23
+ const roundedClientHeight = Math.round(clientHeight);
24
+ const roundedScrollHeight = Math.round(scrollHeight);
25
+ const distanceFromBottom = roundedScrollHeight - roundedScrollTop - roundedClientHeight;
26
+ return {
27
+ roundedScrollTop,
28
+ roundedClientHeight,
29
+ roundedScrollHeight,
30
+ distanceFromBottom,
31
+ atTop: roundedScrollTop === 0,
32
+ nearBottom: distanceFromBottom <= SCROLL_EDGE_DELTA,
33
+ isOverflowing: roundedScrollHeight >= roundedClientHeight
34
+ };
35
+ };
36
+ const isAtBottomEdge = (metrics, enableSmartScroll, autoScroll, pauseAutoScroll, programmaticScroll = false) => metrics.nearBottom ||
37
+ programmaticScroll ||
38
+ (enableSmartScroll && autoScroll && !pauseAutoScroll && metrics.roundedScrollTop > 0);
39
+ const shouldFollowContentGrowth = (metrics, enableSmartScroll, autoScroll, pauseAutoScroll, programmaticScroll) => programmaticScroll ||
40
+ metrics.nearBottom ||
41
+ (enableSmartScroll && autoScroll && !pauseAutoScroll && metrics.roundedScrollTop > 0);
18
42
  export const MessageBox = forwardRef((_a, ref) => {
19
43
  var { announcement, ariaLabel = 'Scrollable message log', children, className, position = 'top', onScrollToTopClick, onScrollToBottomClick, enableSmartScroll = false, jumpButtonTopProps, jumpButtonBottomProps, jumpButtonBottomTooltipProps, jumpButtonTopTooltipProps } = _a, props = __rest(_a, ["announcement", "ariaLabel", "children", "className", "position", "onScrollToTopClick", "onScrollToBottomClick", "enableSmartScroll", "jumpButtonTopProps", "jumpButtonBottomProps", "jumpButtonBottomTooltipProps", "jumpButtonTopTooltipProps"]);
20
- const [atTop, setAtTop] = useState(false);
21
- const [atBottom, setAtBottom] = useState(true);
44
+ const [atTop, setAtTop] = useState(true);
45
+ const [atBottom, setAtBottom] = useState(false);
22
46
  const [isOverflowing, setIsOverflowing] = useState(false);
47
+ const [hasMeasuredScroll, setHasMeasuredScroll] = useState(false);
23
48
  const [autoScroll, setAutoScroll] = useState(true);
49
+ const autoScrollRef = useRef(autoScroll);
24
50
  const lastScrollTop = useRef(0);
25
51
  const animationFrame = useRef(null);
26
52
  const debounceTimeout = useRef(null);
27
53
  const pauseAutoScrollRef = useRef(false);
54
+ const programmaticScrollRef = useRef(false);
28
55
  const messageBoxRef = useRef(null);
29
56
  const scrollQueued = useRef(false);
30
57
  const resetUserScrollIntentTimeout = useRef();
58
+ autoScrollRef.current = autoScroll;
31
59
  // Configure handlers
32
60
  const handleScroll = useCallback(() => {
33
61
  const element = messageBoxRef.current;
34
62
  if (!element) {
35
63
  return;
36
64
  }
37
- const { scrollTop, scrollHeight, clientHeight } = element;
38
- const roundedScrollTop = Math.round(scrollTop);
39
- const roundedClientHeight = Math.round(clientHeight);
40
- const roundedScrollHeight = Math.round(scrollHeight);
41
- const distanceFromBottom = roundedScrollHeight - roundedScrollTop - roundedClientHeight;
65
+ const metrics = getScrollMetrics(element);
66
+ const { roundedScrollTop, distanceFromBottom } = metrics;
42
67
  const isScrollingDown = roundedScrollTop > lastScrollTop.current;
43
68
  const DELTA_UP = 10;
44
69
  const DELTA_DOWN = 50;
45
70
  const DEBOUNCE_DELAY = 200;
46
71
  const delta = isScrollingDown ? DELTA_DOWN : DELTA_UP;
47
72
  const isAtBottom = distanceFromBottom <= delta;
48
- setAtTop(roundedScrollTop === 0);
49
- setAtBottom(roundedScrollTop + roundedClientHeight >= roundedScrollHeight - 1); // rounding means it could be within a pixel of the bottom
73
+ setAtTop(metrics.atTop);
74
+ setAtBottom(isAtBottomEdge(metrics, enableSmartScroll, autoScrollRef.current, pauseAutoScrollRef.current, programmaticScrollRef.current));
75
+ setIsOverflowing(metrics.isOverflowing);
76
+ if (isAtBottom) {
77
+ programmaticScrollRef.current = false;
78
+ }
50
79
  if (!enableSmartScroll || scrollQueued.current) {
51
80
  return;
52
81
  }
53
- if (roundedScrollTop === 0) {
82
+ if (metrics.roundedScrollTop === 0) {
54
83
  pauseAutoScrollRef.current = false;
55
84
  }
56
85
  if (debounceTimeout.current) {
57
86
  clearTimeout(debounceTimeout.current);
58
87
  }
59
- if (!isAtBottom && !pauseAutoScrollRef.current) {
88
+ if (!isAtBottom && !metrics.nearBottom && !pauseAutoScrollRef.current && !programmaticScrollRef.current) {
60
89
  setAutoScroll(false);
61
90
  }
62
91
  // User is near bottom and scrolling down - debounce re-enabling auto-scroll
@@ -66,14 +95,7 @@ export const MessageBox = forwardRef((_a, ref) => {
66
95
  }, DEBOUNCE_DELAY);
67
96
  }
68
97
  lastScrollTop.current = roundedScrollTop;
69
- }, [messageBoxRef]);
70
- const checkOverflow = useCallback(() => {
71
- const element = messageBoxRef.current;
72
- if (element) {
73
- const { scrollHeight, clientHeight } = element;
74
- setIsOverflowing(scrollHeight >= clientHeight);
75
- }
76
- }, [messageBoxRef]);
98
+ }, [enableSmartScroll]);
77
99
  const resumeAutoScroll = useCallback(() => {
78
100
  if (!enableSmartScroll) {
79
101
  return;
@@ -125,6 +147,7 @@ export const MessageBox = forwardRef((_a, ref) => {
125
147
  return;
126
148
  }
127
149
  scrollQueued.current = true;
150
+ programmaticScrollRef.current = true;
128
151
  if (animationFrame.current) {
129
152
  cancelAnimationFrame(animationFrame.current);
130
153
  }
@@ -132,24 +155,51 @@ export const MessageBox = forwardRef((_a, ref) => {
132
155
  element.scrollTo({ top: element.scrollHeight, behavior });
133
156
  resumeAutoScroll();
134
157
  scrollQueued.current = false;
158
+ if (behavior === 'auto') {
159
+ programmaticScrollRef.current = false;
160
+ lastScrollTop.current = element.scrollTop;
161
+ }
135
162
  });
136
163
  onScrollToBottomClick && onScrollToBottomClick();
137
164
  }, [messageBoxRef, enableSmartScroll]);
138
- // Detect scroll position
139
- useEffect(() => {
165
+ const measureJumpButtonState = useCallback(() => {
166
+ const element = messageBoxRef.current;
167
+ if (!element) {
168
+ return;
169
+ }
170
+ const metrics = getScrollMetrics(element);
171
+ setAtTop(metrics.atTop);
172
+ setAtBottom(isAtBottomEdge(metrics, enableSmartScroll, autoScrollRef.current, pauseAutoScrollRef.current, programmaticScrollRef.current));
173
+ setIsOverflowing(metrics.isOverflowing);
174
+ setHasMeasuredScroll(true);
175
+ }, [enableSmartScroll]);
176
+ const handleContentResize = useCallback(() => {
177
+ const element = messageBoxRef.current;
178
+ if (element) {
179
+ const metrics = getScrollMetrics(element);
180
+ const shouldFollow = shouldFollowContentGrowth(metrics, enableSmartScroll, autoScrollRef.current, pauseAutoScrollRef.current, programmaticScrollRef.current);
181
+ if (shouldFollow && !scrollQueued.current) {
182
+ programmaticScrollRef.current = true;
183
+ element.scrollTop = element.scrollHeight - element.clientHeight;
184
+ lastScrollTop.current = element.scrollTop;
185
+ }
186
+ }
187
+ measureJumpButtonState();
188
+ }, [enableSmartScroll, measureJumpButtonState]);
189
+ // Detect scroll position before paint to avoid jump button flash on mount
190
+ useLayoutEffect(() => {
140
191
  const element = messageBoxRef.current;
141
192
  if (!element) {
142
193
  return;
143
194
  }
144
- // Listen for scroll events
195
+ measureJumpButtonState();
145
196
  element.addEventListener('scroll', handleScroll);
146
- // Check initial position and overflow
147
- handleScroll();
148
- checkOverflow();
197
+ const resizeObserver = getResizeObserver(element, handleContentResize);
149
198
  return () => {
150
199
  element.removeEventListener('scroll', handleScroll);
200
+ resizeObserver();
151
201
  };
152
- }, [checkOverflow, handleScroll, messageBoxRef]);
202
+ }, [handleContentResize, handleScroll, measureJumpButtonState]);
153
203
  useImperativeHandle(ref, () => {
154
204
  const node = messageBoxRef.current;
155
205
  // Attach custom methods to the element
@@ -170,12 +220,13 @@ export const MessageBox = forwardRef((_a, ref) => {
170
220
  }
171
221
  if (!isScrollingDown) {
172
222
  pauseAutoScrollRef.current = true;
223
+ programmaticScrollRef.current = false;
173
224
  clearTimeout(resetUserScrollIntentTimeout.current);
174
225
  return;
175
226
  }
176
227
  const { scrollTop, scrollHeight, clientHeight } = container;
177
228
  const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
178
- if (distanceFromBottom < 100) {
229
+ if (distanceFromBottom < SCROLL_EDGE_DELTA) {
179
230
  pauseAutoScrollRef.current = false;
180
231
  setAutoScroll(true);
181
232
  }
@@ -200,6 +251,6 @@ export const MessageBox = forwardRef((_a, ref) => {
200
251
  onTouchMove,
201
252
  onTouchEnd
202
253
  };
203
- return (_jsxs(_Fragment, { children: [_jsx(JumpButton, { position: "top", isHidden: isOverflowing && atTop, onClick: scrollToTop, jumpButtonProps: jumpButtonTopProps, jumpButtonTooltipProps: jumpButtonTopTooltipProps }), _jsxs("div", Object.assign({ role: "region", tabIndex: 0, "aria-label": ariaLabel, className: `pf-chatbot__messagebox ${position === 'bottom' ? 'pf-chatbot__messagebox--bottom' : ''} ${className !== null && className !== void 0 ? className : ''}`, ref: messageBoxRef }, props, (enableSmartScroll ? Object.assign({}, smartScrollHandlers) : {}), { children: [children, _jsx("div", { className: "pf-chatbot__messagebox-announcement pf-chatbot-m-hidden", "aria-live": "polite", children: announcement })] })), _jsx(JumpButton, { position: "bottom", isHidden: isOverflowing && atBottom, onClick: () => scrollToBottom({ resumeSmartScroll: true }), jumpButtonProps: jumpButtonBottomProps, jumpButtonTooltipProps: jumpButtonBottomTooltipProps })] }));
254
+ return (_jsxs(_Fragment, { children: [_jsx(JumpButton, { position: "top", isHidden: !hasMeasuredScroll || (isOverflowing && atTop), onClick: scrollToTop, jumpButtonProps: jumpButtonTopProps, jumpButtonTooltipProps: jumpButtonTopTooltipProps }), _jsx(JumpButton, { position: "bottom", isHidden: !hasMeasuredScroll || (isOverflowing && atBottom), onClick: () => scrollToBottom({ resumeSmartScroll: true }), jumpButtonProps: jumpButtonBottomProps, jumpButtonTooltipProps: jumpButtonBottomTooltipProps }), _jsxs("div", Object.assign({ role: "region", tabIndex: 0, "aria-label": ariaLabel, className: `pf-chatbot__messagebox ${position === 'bottom' ? 'pf-chatbot__messagebox--bottom' : ''} ${className !== null && className !== void 0 ? className : ''}`, ref: messageBoxRef }, props, (enableSmartScroll ? Object.assign({}, smartScrollHandlers) : {}), { children: [children, _jsx("div", { className: "pf-chatbot__messagebox-announcement pf-chatbot-m-hidden", "aria-live": "polite", children: announcement })] }))] }));
204
255
  });
205
256
  export default MessageBox;
@@ -1 +1 @@
1
- export {};
1
+ import '@testing-library/jest-dom';
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
11
11
  import { createRef } from 'react';
12
12
  import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
13
+ import '@testing-library/jest-dom';
13
14
  import { MessageBox } from './MessageBox';
14
15
  import userEvent from '@testing-library/user-event';
15
16
  describe('MessageBox', () => {
@@ -227,4 +228,62 @@ describe('MessageBox', () => {
227
228
  });
228
229
  expect((_b = ref.current) === null || _b === void 0 ? void 0 : _b.isSmartScrollActive()).toBe(true);
229
230
  }));
231
+ it('should keep auto-scroll active during programmatic scroll to bottom', () => __awaiter(void 0, void 0, void 0, function* () {
232
+ var _a, _b;
233
+ const ref = createRef();
234
+ render(_jsx(MessageBox, { ref: ref, enableSmartScroll: true, children: _jsx("div", { children: "Test message content" }) }));
235
+ const element = ref.current;
236
+ Object.defineProperty(element, 'scrollHeight', { configurable: true, value: 1000 });
237
+ Object.defineProperty(element, 'clientHeight', { configurable: true, value: 300 });
238
+ Object.defineProperty(element, 'scrollTop', { configurable: true, value: 0, writable: true });
239
+ act(() => {
240
+ var _a;
241
+ (_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollToBottom({ behavior: 'smooth' });
242
+ });
243
+ expect((_a = ref.current) === null || _a === void 0 ? void 0 : _a.isSmartScrollActive()).toBe(true);
244
+ act(() => {
245
+ element.scrollTop = 500;
246
+ element.dispatchEvent(new Event('scroll', { bubbles: true }));
247
+ });
248
+ expect((_b = ref.current) === null || _b === void 0 ? void 0 : _b.isSmartScrollActive()).toBe(true);
249
+ }));
250
+ it('should not scroll to bottom on initial layout when the message box is at the top', () => {
251
+ render(_jsx(MessageBox, { enableSmartScroll: true, children: _jsx("div", { children: "Tall message content" }) }));
252
+ const region = screen.getByRole('region');
253
+ Object.defineProperty(region, 'scrollHeight', { configurable: true, value: 1000 });
254
+ Object.defineProperty(region, 'clientHeight', { configurable: true, value: 300 });
255
+ Object.defineProperty(region, 'scrollTop', { configurable: true, value: 0, writable: true });
256
+ act(() => {
257
+ region.dispatchEvent(new Event('scroll'));
258
+ });
259
+ expect(region.scrollTop).toBe(0);
260
+ });
261
+ it('should show the bottom jump button at the top on initial load when overflowing', () => __awaiter(void 0, void 0, void 0, function* () {
262
+ render(_jsx(MessageBox, { enableSmartScroll: true, children: _jsx("div", { children: "Tall message content" }) }));
263
+ const region = screen.getByRole('region');
264
+ Object.defineProperty(region, 'scrollHeight', { configurable: true, value: 1000 });
265
+ Object.defineProperty(region, 'clientHeight', { configurable: true, value: 300 });
266
+ Object.defineProperty(region, 'scrollTop', { configurable: true, value: 0, writable: true });
267
+ act(() => {
268
+ region.dispatchEvent(new Event('scroll'));
269
+ });
270
+ yield waitFor(() => {
271
+ expect(screen.queryByRole('button', { name: /Back to top/i })).toBeFalsy();
272
+ expect(screen.getByRole('button', { name: /Back to bottom/i })).toBeTruthy();
273
+ });
274
+ }));
275
+ it('should hide the bottom jump button while smart scroll is actively following content', () => __awaiter(void 0, void 0, void 0, function* () {
276
+ render(_jsx(MessageBox, { enableSmartScroll: true, children: _jsx("div", { children: "Test message content" }) }));
277
+ const region = screen.getByRole('region');
278
+ Object.defineProperty(region, 'scrollHeight', { configurable: true, value: 1000 });
279
+ Object.defineProperty(region, 'clientHeight', { configurable: true, value: 300 });
280
+ Object.defineProperty(region, 'scrollTop', { configurable: true, value: 640, writable: true });
281
+ act(() => {
282
+ region.dispatchEvent(new Event('scroll'));
283
+ });
284
+ yield waitFor(() => {
285
+ expect(screen.getByRole('button', { name: /Back to top/i })).toBeTruthy();
286
+ expect(screen.queryByRole('button', { name: /Back to bottom/i })).toBeFalsy();
287
+ });
288
+ }));
230
289
  });
@@ -11,39 +11,39 @@ import { jsx as _jsx } from "react/jsx-runtime";
11
11
  import { render, screen } from '@testing-library/react';
12
12
  import '@testing-library/jest-dom';
13
13
  import userEvent from '@testing-library/user-event';
14
- import { DownloadIcon } from '@patternfly/react-icons';
14
+ import { RhUiExportIcon } from '@patternfly/react-icons';
15
15
  import ResponseActionButton from './ResponseActionButton';
16
16
  describe('ResponseActionButton', () => {
17
17
  it('renders aria-label correctly if not clicked', () => {
18
- render(_jsx(ResponseActionButton, { icon: _jsx(DownloadIcon, {}), ariaLabel: "Download", clickedAriaLabel: "Downloaded" }));
18
+ render(_jsx(ResponseActionButton, { icon: _jsx(RhUiExportIcon, {}), ariaLabel: "Download", clickedAriaLabel: "Downloaded" }));
19
19
  expect(screen.getByRole('button', { name: 'Download' })).toBeTruthy();
20
20
  });
21
21
  it('renders aria-label correctly if clicked', () => {
22
- render(_jsx(ResponseActionButton, { icon: _jsx(DownloadIcon, {}), ariaLabel: "Download", clickedAriaLabel: "Downloaded", isClicked: true }));
22
+ render(_jsx(ResponseActionButton, { icon: _jsx(RhUiExportIcon, {}), ariaLabel: "Download", clickedAriaLabel: "Downloaded", isClicked: true }));
23
23
  expect(screen.getByRole('button', { name: 'Downloaded' })).toBeTruthy();
24
24
  });
25
25
  it('renders tooltip correctly if not clicked', () => __awaiter(void 0, void 0, void 0, function* () {
26
- render(_jsx(ResponseActionButton, { icon: _jsx(DownloadIcon, {}), tooltipContent: "Download", clickedTooltipContent: "Downloaded" }));
26
+ render(_jsx(ResponseActionButton, { icon: _jsx(RhUiExportIcon, {}), tooltipContent: "Download", clickedTooltipContent: "Downloaded" }));
27
27
  expect(screen.getByRole('button', { name: 'Download' })).toBeTruthy();
28
28
  // clicking here just triggers the tooltip; in this button, the logic is divorced from whether it is actually clicked
29
29
  yield userEvent.click(screen.getByRole('button', { name: 'Download' }));
30
30
  expect(screen.getByRole('tooltip', { name: 'Download' })).toBeTruthy();
31
31
  }));
32
32
  it('renders tooltip correctly if clicked', () => __awaiter(void 0, void 0, void 0, function* () {
33
- render(_jsx(ResponseActionButton, { icon: _jsx(DownloadIcon, {}), tooltipContent: "Download", clickedTooltipContent: "Downloaded", isClicked: true }));
33
+ render(_jsx(ResponseActionButton, { icon: _jsx(RhUiExportIcon, {}), tooltipContent: "Download", clickedTooltipContent: "Downloaded", isClicked: true }));
34
34
  expect(screen.getByRole('button', { name: 'Downloaded' })).toBeTruthy();
35
35
  // clicking here just triggers the tooltip; in this button, the logic is divorced from whether it is actually clicked
36
36
  yield userEvent.click(screen.getByRole('button', { name: 'Downloaded' }));
37
37
  expect(screen.getByRole('tooltip', { name: 'Downloaded' })).toBeTruthy();
38
38
  }));
39
39
  it('if clicked variant for tooltip is not supplied, it uses the default', () => __awaiter(void 0, void 0, void 0, function* () {
40
- render(_jsx(ResponseActionButton, { icon: _jsx(DownloadIcon, {}), tooltipContent: "Download", isClicked: true }));
40
+ render(_jsx(ResponseActionButton, { icon: _jsx(RhUiExportIcon, {}), tooltipContent: "Download", isClicked: true }));
41
41
  // clicking here just triggers the tooltip; in this button, the logic is divorced from whether it is actually clicked
42
42
  yield userEvent.click(screen.getByRole('button', { name: 'Download' }));
43
43
  expect(screen.getByRole('button', { name: 'Download' })).toBeTruthy();
44
44
  }));
45
45
  it('if clicked variant for aria label is not supplied, it uses the default', () => __awaiter(void 0, void 0, void 0, function* () {
46
- render(_jsx(ResponseActionButton, { icon: _jsx(DownloadIcon, {}), ariaLabel: "Download", isClicked: true }));
46
+ render(_jsx(ResponseActionButton, { icon: _jsx(RhUiExportIcon, {}), ariaLabel: "Download", isClicked: true }));
47
47
  expect(screen.getByRole('button', { name: 'Download' })).toBeTruthy();
48
48
  }));
49
49
  });