Skip to content

Commit

Permalink
Merge pull request #405 from rebeccaalpert/panel
Browse files Browse the repository at this point in the history
feat(ChatbotConversationHistoryNav): Make it resizable
  • Loading branch information
nicolethoen authored Jan 15, 2025
2 parents ef74c6b + d3bb0a0 commit 38e1d83
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from 'react';
import { ChatbotDisplayMode } from '@patternfly/chatbot/dist/dynamic/Chatbot';
import ChatbotConversationHistoryNav, {
Conversation
} from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
import { Checkbox } from '@patternfly/react-core';

const initialConversations: { [key: string]: Conversation[] } = {
Today: [{ id: '1', text: 'Red Hat products and services' }],
'This month': [
{
id: '2',
text: 'Enterprise Linux installation and setup'
},
{ id: '3', text: 'Troubleshoot system crash' }
],
March: [
{ id: '4', text: 'Ansible security and updates' },
{ id: '5', text: 'Red Hat certification' },
{ id: '6', text: 'Lightspeed user documentation' }
],
February: [
{ id: '7', text: 'Crashing pod assistance' },
{ id: '8', text: 'OpenShift AI pipelines' },
{ id: '9', text: 'Updating subscription plan' },
{ id: '10', text: 'Red Hat licensing options' }
],
January: [
{ id: '11', text: 'RHEL system performance' },
{ id: '12', text: 'Manage user accounts' }
]
};

export const ChatbotHeaderDrawerResizableDemo: React.FunctionComponent = () => {
const [isOpen, setIsOpen] = React.useState(true);
const [conversations, setConversations] = React.useState<Conversation[] | { [key: string]: Conversation[] }>(
initialConversations
);
const displayMode = ChatbotDisplayMode.embedded;

const findMatchingItems = (targetValue: string) => {
let filteredConversations = Object.entries(initialConversations).reduce((acc, [key, items]) => {
const filteredItems = items.filter((item) => item.text.toLowerCase().includes(targetValue.toLowerCase()));
if (filteredItems.length > 0) {
acc[key] = filteredItems;
}
return acc;
}, {});

// append message if no items are found
if (Object.keys(filteredConversations).length === 0) {
filteredConversations = [{ id: '13', noIcon: true, text: 'No results found' }];
}
return filteredConversations;
};

return (
<>
<Checkbox
label="Display drawer"
isChecked={isOpen}
onChange={() => {
setIsOpen(!isOpen);
setConversations(initialConversations);
}}
id="drawer-visible"
name="drawer-visible"
/>
<ChatbotConversationHistoryNav
displayMode={displayMode}
onDrawerToggle={() => setIsOpen(!isOpen)}
isDrawerOpen={isOpen}
setIsDrawerOpen={setIsOpen}
// eslint-disable-next-line no-console
onSelectActiveItem={(e, selectedItem) => console.log(`Selected history item with id ${selectedItem}`)}
conversations={conversations}
onNewChat={() => {
setIsOpen(!isOpen);
}}
handleTextInputChange={(value: string) => {
if (value === '') {
setConversations(initialConversations);
}
// this is where you would perform search on the items in the drawer
// and update the state
const newConversations: { [key: string]: Conversation[] } = findMatchingItems(value);
setConversations(newConversations);
}}
drawerContent={<div>Drawer content</div>}
drawerPanelContentProps={{ isResizable: true, minSize: '200px' }}
/>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,16 @@ If you're showing a conversation that is already active, you can set the `active

```

### Resizable drawer

By default, the conversation history drawer has a fixed width (384px) and a focus trap. To provide users with more flexibility as they navigate their conversation history, or to better support embedded ChatBots on tablet-sized devices or smaller browser windows, you can instead make the drawer resizable. By default, even resizable drawers will still open to their full width on mobile devices.

In this example, the drawer can be resized up to the max size of the parent and resized down to 200px wide. To customize this behavior further (including width, style, and focus behavior) use PatternFly [`<Drawer>` props](/components/drawer#props), [`<DrawerPanelContent>` props](/components/drawer/#drawerpanelcontent), or any other drawer subcomponents.

```js file="./ChatbotHeaderDrawerResizable.tsx"

```

### Drawer with simple menu

The drawer can also be used to display a list of basic menu items.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,113 @@ describe('ChatbotConversationHistoryNav', () => {
expect(screen.queryByText('ChatBot documentation')).not.toBeInTheDocument();
});
});

it('should be resizable', () => {
render(
<ChatbotConversationHistoryNav
onDrawerToggle={onDrawerToggle}
isDrawerOpen={true}
displayMode={ChatbotDisplayMode.fullscreen}
setIsDrawerOpen={jest.fn()}
conversations={initialConversations}
drawerPanelContentProps={{ isResizable: true, minSize: '200px' }}
/>
);
expect(screen.getByRole('dialog', { name: /Resize/i })).toBeTruthy();
expect(screen.getByRole('separator', { name: /Resize/i })).toBeTruthy();
expect(screen.getByRole('dialog', { name: /Resize/i })).toHaveAttribute(
'style',
'--pf-v6-c-drawer__panel--md--FlexBasis: 384px; --pf-v6-c-drawer__panel--md--FlexBasis--min: 200px;'
);
});

it('should accept drawerContentProps', () => {
const { container } = render(
<ChatbotConversationHistoryNav
onDrawerToggle={onDrawerToggle}
isDrawerOpen={true}
displayMode={ChatbotDisplayMode.fullscreen}
setIsDrawerOpen={jest.fn()}
conversations={initialConversations}
drawerContentProps={{ className: 'test' }}
/>
);
const element = container.querySelector('.test');
expect(element).toBeInTheDocument();
});

it('should accept drawerContentBodyProps', () => {
const { container } = render(
<ChatbotConversationHistoryNav
onDrawerToggle={onDrawerToggle}
isDrawerOpen={true}
displayMode={ChatbotDisplayMode.fullscreen}
setIsDrawerOpen={jest.fn()}
conversations={initialConversations}
drawerContentBodyProps={{ className: 'test' }}
/>
);
const element = container.querySelector('.test');
expect(element).toBeInTheDocument();
});

it('should accept drawerHeadProps', () => {
const { container } = render(
<ChatbotConversationHistoryNav
onDrawerToggle={onDrawerToggle}
isDrawerOpen={true}
displayMode={ChatbotDisplayMode.fullscreen}
setIsDrawerOpen={jest.fn()}
conversations={initialConversations}
drawerHeadProps={{ className: 'test' }}
/>
);
const element = container.querySelector('.test');
expect(element).toBeInTheDocument();
});

it('should accept drawerActionsProps', () => {
const { container } = render(
<ChatbotConversationHistoryNav
onDrawerToggle={onDrawerToggle}
isDrawerOpen={true}
displayMode={ChatbotDisplayMode.fullscreen}
setIsDrawerOpen={jest.fn()}
conversations={initialConversations}
drawerActionsProps={{ className: 'test' }}
/>
);
const element = container.querySelector('.test');
expect(element).toBeInTheDocument();
});

it('should accept drawerCloseButtonProps', () => {
const { container } = render(
<ChatbotConversationHistoryNav
onDrawerToggle={onDrawerToggle}
isDrawerOpen={true}
displayMode={ChatbotDisplayMode.fullscreen}
setIsDrawerOpen={jest.fn()}
conversations={initialConversations}
drawerCloseButtonProps={{ className: 'test' }}
/>
);
const element = container.querySelector('.test');
expect(element).toBeInTheDocument();
});

it('should accept drawerPanelBodyProps', () => {
const { container } = render(
<ChatbotConversationHistoryNav
onDrawerToggle={onDrawerToggle}
isDrawerOpen={true}
displayMode={ChatbotDisplayMode.fullscreen}
setIsDrawerOpen={jest.fn()}
conversations={initialConversations}
drawerPanelBodyProps={{ className: 'test' }}
/>
);
const element = container.querySelector('.test');
expect(element).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ import {
MenuItem,
MenuContent,
MenuItemProps,
MenuProps
MenuProps,
DrawerPanelContentProps,
DrawerContentProps,
DrawerContentBodyProps,
DrawerHeadProps,
DrawerActionsProps,
DrawerCloseButtonProps,
DrawerPanelBodyProps
} from '@patternfly/react-core';

import { OutlinedCommentAltIcon } from '@patternfly/react-icons';
Expand Down Expand Up @@ -82,6 +89,20 @@ export interface ChatbotConversationHistoryNavProps extends DrawerProps {
drawerActionsTestId?: string;
/** Additional props applied to menu */
menuProps?: MenuProps;
/** Additional props applied to panel */
drawerPanelContentProps?: DrawerPanelContentProps;
/** Additional props applied to drawer content */
drawerContentProps?: Omit<DrawerContentProps, 'panelContent'>;
/** Additional props applied to drawer content body */
drawerContentBodyProps?: DrawerContentBodyProps;
/** Additional props applied to drawer head */
drawerHeadProps?: DrawerHeadProps;
/** Additional props applied to drawer actions */
drawerActionsProps?: DrawerActionsProps;
/** Additional props applied to drawer close button */
drawerCloseButtonProps?: DrawerCloseButtonProps;
/** Additional props appleid to drawer panel body */
drawerPanelBodyProps?: DrawerPanelBodyProps;
}

export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConversationHistoryNavProps> = ({
Expand All @@ -101,6 +122,13 @@ export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConve
reverseButtonOrder = false,
drawerActionsTestId = 'chatbot-nav-drawer-actions',
menuProps,
drawerPanelContentProps,
drawerContentProps,
drawerContentBodyProps,
drawerHeadProps,
drawerActionsProps,
drawerCloseButtonProps,
drawerPanelBodyProps,
...props
}: ChatbotConversationHistoryNavProps) => {
const drawerRef = React.useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -173,13 +201,14 @@ export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConve
);

const panelContent = (
<DrawerPanelContent focusTrap={{ enabled: true }} minSize="384px" maxSize="384px">
<DrawerHead>
<DrawerPanelContent focusTrap={{ enabled: true }} defaultSize="384px" {...drawerPanelContentProps}>
<DrawerHead {...drawerHeadProps}>
<DrawerActions
data-testid={drawerActionsTestId}
className={reverseButtonOrder ? 'pf-v6-c-drawer__actions--reversed' : ''}
{...drawerActionsProps}
>
<DrawerCloseButton onClick={onDrawerToggle} />
<DrawerCloseButton onClick={onDrawerToggle} {...drawerCloseButtonProps} />
{onNewChat && <Button onClick={onNewChat}>{newChatButtonText}</Button>}
</DrawerActions>
</DrawerHead>
Expand All @@ -192,7 +221,7 @@ export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConve
/>
</div>
)}
<DrawerPanelBody>{menuContent}</DrawerPanelBody>
<DrawerPanelBody {...drawerPanelBodyProps}>{menuContent}</DrawerPanelBody>
</DrawerPanelContent>
);

Expand All @@ -217,8 +246,8 @@ export const ChatbotConversationHistoryNav: React.FunctionComponent<ChatbotConve
isInline={displayMode === ChatbotDisplayMode.fullscreen || displayMode === ChatbotDisplayMode.embedded}
{...props}
>
<DrawerContent panelContent={panelContent}>
<DrawerContentBody>
<DrawerContent panelContent={panelContent} {...drawerContentProps}>
<DrawerContentBody {...drawerContentBodyProps}>
<>
<div
className={`${isDrawerOpen && (displayMode === ChatbotDisplayMode.default || displayMode === ChatbotDisplayMode.docked) ? 'pf-v6-c-backdrop pf-chatbot__drawer-backdrop' : undefined} `}
Expand Down

0 comments on commit 38e1d83

Please sign in to comment.