From 398a4aff4c953d34cc568b8b8fd947b879fc9eb5 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Tue, 12 Aug 2025 16:13:51 -0400 Subject: [PATCH 1/2] fix(ConversationHistoryNav): Allow support for favorites, tooltips, etc. Revert back to menu implementation and ensure props passage down to appropriate Menu components to enable behavior. --- ...hatbotConversationHistoryDropdown.test.tsx | 12 +- .../ChatbotConversationHistoryDropdown.tsx | 1 + .../ChatbotConversationHistoryNav.scss | 64 ++++----- .../ChatbotConversationHistoryNav.test.tsx | 50 ++++--- .../ChatbotConversationHistoryNav.tsx | 133 ++++++++++-------- 5 files changed, 140 insertions(+), 120 deletions(-) diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.test.tsx b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.test.tsx index 13bc117ab..64c4cd7ac 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.test.tsx +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.test.tsx @@ -14,13 +14,13 @@ describe('ChatbotConversationHistoryDropdown', () => { it('should render the dropdown', () => { render(); - expect(screen.queryByRole('button', { name: /Conversation options/i })).toBeInTheDocument(); + expect(screen.queryByRole('menuitem', { name: /Conversation options/i })).toBeInTheDocument(); }); it('should display the dropdown menuItems', () => { render(); - const toggle = screen.queryByRole('button', { name: /Conversation options/i })!; + const toggle = screen.queryByRole('menuitem', { name: /Conversation options/i })!; expect(toggle).toBeInTheDocument(); fireEvent.click(toggle); @@ -33,7 +33,7 @@ describe('ChatbotConversationHistoryDropdown', () => { it('should invoke onSelect callback when menuitem is clicked', () => { render(); - const toggle = screen.queryByRole('button', { name: /Conversation options/i })!; + const toggle = screen.queryByRole('menuitem', { name: /Conversation options/i })!; fireEvent.click(toggle); fireEvent.click(screen.getByText('Rename')); @@ -42,7 +42,7 @@ describe('ChatbotConversationHistoryDropdown', () => { it('should toggle the dropdown when menuitem is clicked', () => { render(); - const toggle = screen.queryByRole('button', { name: /Conversation options/i })!; + const toggle = screen.queryByRole('menuitem', { name: /Conversation options/i })!; fireEvent.click(toggle); fireEvent.click(screen.getByText('Delete')); @@ -53,7 +53,7 @@ describe('ChatbotConversationHistoryDropdown', () => { it('should close the dropdown when user clicks outside', () => { render(); - const toggle = screen.queryByRole('button', { name: /Conversation options/i })!; + const toggle = screen.queryByRole('menuitem', { name: /Conversation options/i })!; fireEvent.click(toggle); expect(screen.queryByText('Delete')).toBeInTheDocument(); @@ -64,7 +64,7 @@ describe('ChatbotConversationHistoryDropdown', () => { it('should show the tooltip when the user hovers over the toggle button', async () => { render(); - const toggle = screen.queryByRole('button', { name: /Actions dropdown/i })!; + const toggle = screen.queryByRole('menuitem', { name: /Actions dropdown/i })!; fireEvent( toggle, diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.tsx b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.tsx index 68d1e5c9e..f4388f5ff 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.tsx +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.tsx @@ -48,6 +48,7 @@ export const ChatbotConversationHistoryDropdown: FunctionComponent setIsOpen(!isOpen)} id={id} + role="menuitem" > diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss index 510908960..c5ce6c31d 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss @@ -42,61 +42,53 @@ } // Drawer menu // ---------------------------------------------------------------------------- - .pf-chatbot__conversation-list { - --pf-v6-c-list--Gap: var(--pf-t--global--spacer--xs); - - margin-block-start: var(--pf-t--global--spacer--md); - margin-block-end: var(--pf-t--global--spacer--md); + .pf-v6-c-menu { + --pf-v6-c-menu--PaddingBlockStart: 0; + --pf-v6-c-menu--BackgroundColor: var(--pf-t--global--background--color--floating--default); + overflow: initial; + position: relative; + } + .pf-v6-c-menu__item-main { + --pf-v6-c-menu__item-main--ColumnGap: var(--pf-t--global--spacer--md); } - .pf-chatbot__conversation-list-header { + .pf-chatbot__menu-item-header > .pf-v6-c-menu__group-title { color: var(--pf-t--global--text--color--subtle); font-weight: var(--pf-t--global--font--weight--body--bold); font-size: var(--pf-t--global--icon--size--font--sm); - padding-inline-start: var(--pf-t--global--spacer--sm); - padding-inline-end: var(--pf-t--global--spacer--sm); + --pf-v6-c-menu__group-title--PaddingInlineStart: var(--pf-t--global--spacer--sm); + --pf-v6-c-menu__group-title--PaddingInlineEnd: var(--pf-t--global--spacer--sm); position: -webkit-sticky; position: sticky; top: 0; background-color: var(--pf-t--global--background--color--floating--default); z-index: var(--pf-t--global--z-index--md); } - .pf-chatbot__conversation-list-item { - & > span { - display: flex; - column-gap: var(--pf-t--global--spacer--sm); - } - - & .pf-chatbot__conversation-history-item { - --pf-v6-c-button--JustifyContent: flex-start; - --pf-v6-c-button--FontSize: var(--pf-t--global--font--size--body--lg); - --pf-v6-c-button--m-link--Color: var(--pf-t--global--text--color--regular); - --pf-v6-c-button--m-link__icon--Color: var(--pf-t--global--icon--color--regular); - --pf-v6-c-button--m-link--hover--Color: var(--pf-t--global--text--color--regular--hover); - --pf-v6-c-button--m-link--hover__icon--Color: var(--pf-t--global--icon--color--regular); - --pf-v6-c-button--m-link--m-clicked--Color: var(--pf-t--global--text--color--regular--clicked); - --pf-v6-c-button--m-link--m-clicked__icon--Color: var(--pf-t--global--icon--color--regular); - column-gap: var(--pf-t--global--spacer--md); - flex-basis: 100%; - - & .pf-v6-c-button__text { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } + .pf-chatbot__menu-item { + --pf-v6-c-menu__item--PaddingInlineStart: var(--pf-t--global--spacer--sm); + --pf-v6-c-menu__item--PaddingInlineEnd: var(--pf-t--global--spacer--sm); + padding-block-start: var(--pf-t--global--spacer--xs); + padding-block-end: var(--pf-t--global--spacer--xs); + color: var(--pf-t--global--text--color--regular); + font-size: var(--pf-t--global--font--size--body--lg); + font-weight: var(--pf-t--global--font--weight--body--default); + border-radius: var(--pf-t--global--border--radius--small); + } + // allows focus state to have border radius + .pf-v6-c-menu__list-item.pf-chatbot__menu-item { + overflow: hidden; } .pf-chatbot__history-actions { transform: rotate(90deg); } - .pf-chatbot__conversation-list-item--active { + .pf-chatbot__menu-item--active { background-color: var(--pf-t--global--background--color--action--plain--clicked); } - button.pf-chatbot__conversation-list-item--active { + button.pf-chatbot__menu-item--active { background-color: initial; } } @@ -259,8 +251,8 @@ } } - .pf-chatbot__conversation-history-item { - --pf-v6-c-button--FontSize: var(--pf-t--global--font--size--body--md); + .pf-chatbot__menu-item { + font-size: var(--pf-t--global--font--size--body--md); } .pf-v6-c-drawer__head { diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx index db19f3d49..f1c21c6ce 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx @@ -492,36 +492,36 @@ describe('ChatbotConversationHistoryNav', () => { expect(iconElement).toBeInTheDocument(); }); - it('Passes listTitleProps to Title', () => { + it('Passes menuProps to Menu', () => { render( ); - expect(screen.getByRole('heading', { name: /Today/i })).toHaveClass('test'); + + expect(screen.getByRole('menu').parentElement?.parentElement).toHaveClass('test'); }); - it('Overrides list title heading level when titleProps.headingLevel is passed', () => { + it('Passes menuContentProps to MenuContent', () => { render( ); - expect(screen.queryByRole('heading', { name: /Today/i, level: 4 })).not.toBeInTheDocument(); - expect(screen.getByRole('heading', { name: /Today/i, level: 2 })).toBeInTheDocument(); + expect(screen.getByRole('menu').parentElement).toHaveClass('test'); }); - it('Passes listProps to List when conversations is an array', () => { + it('Passes menuListProps to MenuList when conversations is an array', () => { render( { displayMode={ChatbotDisplayMode.fullscreen} setIsDrawerOpen={jest.fn()} conversations={initialConversations} - listProps={{ className: 'test' }} + menuListProps={{ className: 'test' }} + /> + ); + expect(screen.getByRole('menu')).toHaveClass('test'); + }); + + it('Passes menuListProps to MenuList when conversations is an object', () => { + render( + ); - expect(screen.getByRole('list')).toHaveClass('test'); + expect(screen.getByRole('menu')).toHaveClass('test'); }); - it('Passes listProps to List when conversations is an object', () => { + it('Passes menuGroupProps to MenuGroup when conversations is an object', () => { render( { displayMode={ChatbotDisplayMode.fullscreen} setIsDrawerOpen={jest.fn()} conversations={{ Today: initialConversations }} - listProps={{ Today: { className: 'test' } }} + menuGroupProps={{ Today: { className: 'test' } }} /> ); - expect(screen.getByRole('list')).toHaveClass('test'); + expect(screen.getByRole('menu').parentElement).toHaveClass('test'); }); - it('Passes listItemProps to ListItem', () => { + it('Passes additionalProps to MenuItem', () => { render( ); - expect(screen.getByRole('listitem')).toHaveClass('test'); + expect(screen.getByRole('menuitem')).toHaveClass('test'); }); it('should be able to spread search input props when searchInputProps is passed', () => { diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx index cd8dbab41..0fa7cc840 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx @@ -2,7 +2,6 @@ // Chatbot Header - Chatbot Conversation History Nav // ============================================================================ import type { KeyboardEvent, FunctionComponent } from 'react'; - import { useRef, Fragment } from 'react'; // Import PatternFly components @@ -19,9 +18,6 @@ import { DrawerCloseButton, DrawerContentBody, SearchInput, - List, - ListItem, - ListItemProps, Title, DrawerPanelContentProps, DrawerContentProps, @@ -32,10 +28,18 @@ import { DrawerPanelBodyProps, SkeletonProps, Icon, - MenuProps, // Remove in next breaking change + MenuProps, TitleProps, - ListProps, - SearchInputProps + MenuListProps, + SearchInputProps, + MenuList, + MenuGroup, + MenuItem, + Menu, + MenuContent, + MenuItemProps, + MenuGroupProps, + MenuContentProps } from '@patternfly/react-core'; import { OutlinedClockIcon, OutlinedCommentAltIcon, PenToSquareIcon } from '@patternfly/react-icons'; @@ -61,10 +65,8 @@ export interface Conversation { label?: string; /** Callback for when user selects item. */ onSelect?: (event?: React.MouseEvent, value?: string | number) => void; - /** Additional props passed to conversation button item */ - additionalProps?: ButtonProps; - /** Additional props passed to conversation list item */ - listItemProps?: Omit; + /** Additional props passed to menu item */ + additionalProps?: MenuItemProps; /** Custom dropdown ID to ensure uniqueness across demo instances */ dropdownId?: string; } @@ -83,10 +85,10 @@ export interface ChatbotConversationHistoryNavProps extends DrawerProps { conversations: Conversation[] | { [key: string]: Conversation[] }; /** Additional button props for new chat button. */ newChatButtonProps?: ButtonProps; - /** Additional props applied to all conversation list headers */ - listTitleProps?: Partial; - /** Additional props applied to conversation list. If conversations is an object, you should pass an object of ListProps for each group. */ - listProps?: ListProps | { [key: string]: ListProps }; + /** Additional props applied to conversation menu group. If conversations is an object, you should pass an object of MenuGroupProps for each group. */ + menuGroupProps?: MenuGroupProps | { [key: string]: MenuGroupProps }; + /** Additional props applied to conversation list. If conversations is an object, you should pass an object of MenuListProps for each group. */ + menuListProps?: Omit | { [key: string]: Omit }; /** Text shown in blue button */ newChatButtonText?: string; /** Callback function for when blue button is clicked. Omit to hide blue "new chat button" */ @@ -107,7 +109,7 @@ export interface ChatbotConversationHistoryNavProps extends DrawerProps { reverseButtonOrder?: boolean; /** Custom test id for the drawer actions */ drawerActionsTestId?: string; - /** @deprecated Additional props applied to list container */ + /** Additional props applied to menu */ menuProps?: MenuProps; /** Additional props applied to panel */ drawerPanelContentProps?: DrawerPanelContentProps; @@ -143,6 +145,8 @@ export interface ChatbotConversationHistoryNavProps extends DrawerProps { navTitleProps?: Partial; /** Visually hidden text that gets announced by assistive technologies. Should be used to convey the result count when the search input value changes. */ searchInputScreenReaderText?: string; + /** Additional props passed to MenuContent */ + menuContentProps?: Omit; } export const ChatbotConversationHistoryNav: FunctionComponent = ({ @@ -152,8 +156,7 @@ export const ChatbotConversationHistoryNav: FunctionComponent, searchInputScreenReaderText, + menuProps, + menuGroupProps, + menuContentProps, ...props }: ChatbotConversationHistoryNavProps) => { const drawerRef = useRef(null); @@ -190,61 +196,64 @@ export const ChatbotConversationHistoryNav: FunctionComponent + item && typeof item === 'object' && 'id' in item && 'text' in item; + const getNavItem = (conversation: Conversation) => ( - })} + /* eslint-disable indent */ + {...(conversation.menuItems + ? { + actions: ( + + ) + } + : {})} + {...conversation.additionalProps} > - <> - - {conversation.menuItems && ( - - )} - - + {conversation.text} + ); const buildConversations = () => { if (Array.isArray(conversations)) { return ( - - {conversations.map((conversation) => ( - {getNavItem(conversation)} - ))} - + + {conversations.map((conversation) => { + if (isConversation(conversation)) { + return {getNavItem(conversation)}; + } else { + return conversation; + } + })} + ); } else { return ( -
+ <> {Object.keys(conversations).map((navGroup) => ( -
- - {navGroup} - - - {conversations[navGroup].map((conversation) => ( + + + {conversations[navGroup].map((conversation: Conversation) => ( {getNavItem(conversation)} ))} - -
+ + ))} -
+ ); } }; @@ -264,7 +273,11 @@ export const ChatbotConversationHistoryNav: FunctionComponent; } - return <>{buildConversations()}; + return ( + + {buildConversations()} + + ); }; const renderDrawerContent = () => ( From 77704d7d7b55ee83b86ab4ce3188ee0d2b362158 Mon Sep 17 00:00:00 2001 From: Rebecca Alpert Date: Tue, 19 Aug 2025 15:11:10 -0400 Subject: [PATCH 2/2] Address feedback --- .../ChatbotConversationHistoryNav.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx index 0fa7cc840..453299644 100644 --- a/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx +++ b/packages/module/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx @@ -244,6 +244,7 @@ export const ChatbotConversationHistoryNav: FunctionComponent