Skip to content

Commit 3e4fe3b

Browse files
authored
Merge pull request #789 from thatblindgeye/iss779_messageBarLayouts
feat(MessageBar): added custom attach and additional actions
2 parents b5769b4 + 4d73980 commit 3e4fe3b

File tree

5 files changed

+333
-53
lines changed

5 files changed

+333
-53
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { useState, FunctionComponent, ReactNode } from 'react';
2+
import { MessageBar } from '@patternfly/chatbot/dist/dynamic/MessageBar';
3+
import {
4+
Divider,
5+
DropdownItem,
6+
DropdownList,
7+
Label,
8+
MenuToggle,
9+
Select,
10+
SelectList,
11+
SelectOption
12+
} from '@patternfly/react-core';
13+
import { PlusIcon, ClipboardIcon, CodeIcon, UploadIcon } from '@patternfly/react-icons';
14+
import { useDropzone } from 'react-dropzone';
15+
16+
export const ChatbotMessageBarCustomActionsExample: FunctionComponent = () => {
17+
const [isFirstMenuOpen, setIsFirstMenuOpen] = useState<boolean>(false);
18+
const [isSecondMenuOpen, setIsSecondMenuOpen] = useState<boolean>(false);
19+
const [isModelSelectOpen, setIsModelSelectOpen] = useState<boolean>(false);
20+
const [selectedModel, setSelectedModel] = useState<string>('GPT-4');
21+
const [showCanvasLabel, setShowCanvasLabel] = useState<boolean>(true);
22+
23+
const handleSend = (message: string | number) => alert(message);
24+
25+
const { open, getInputProps } = useDropzone({
26+
multiple: true,
27+
// eslint-disable-next-line no-console
28+
onDropAccepted: () => console.log('fileUploaded')
29+
});
30+
31+
const onFirstMenuToggle = () => {
32+
setIsFirstMenuOpen(!isFirstMenuOpen);
33+
};
34+
35+
const onSecondMenuToggle = () => {
36+
setIsSecondMenuOpen(!isSecondMenuOpen);
37+
};
38+
39+
const onModelSelect = (
40+
_event: React.MouseEvent<Element, MouseEvent> | undefined,
41+
value: string | number | undefined
42+
) => {
43+
setSelectedModel(value as string);
44+
setIsModelSelectOpen(false);
45+
};
46+
47+
const firstMenuItems: ReactNode = (
48+
<DropdownList>
49+
<DropdownItem value="Logs" id="logs" icon={<ClipboardIcon />}>
50+
Logs
51+
</DropdownItem>
52+
<DropdownItem value="YAML - Status" id="yaml-status" icon={<CodeIcon />}>
53+
YAML - Status
54+
</DropdownItem>
55+
<DropdownItem value="YAML - All contents" id="yaml-all" icon={<CodeIcon />}>
56+
YAML - All contents
57+
</DropdownItem>
58+
<Divider key="divider" />
59+
<DropdownItem value="Upload from computer" id="upload" icon={<UploadIcon />} onClick={open}>
60+
Upload from computer
61+
</DropdownItem>
62+
</DropdownList>
63+
);
64+
65+
const secondMenuItems: ReactNode = (
66+
<DropdownList>
67+
<DropdownItem value="canvas" id="canvas">
68+
{showCanvasLabel ? 'Disable' : 'Enable'} Canvas
69+
</DropdownItem>
70+
<Divider key="divider-1" />
71+
<DropdownItem value="Logs" id="logs" icon={<ClipboardIcon />}>
72+
Logs
73+
</DropdownItem>
74+
<DropdownItem value="YAML - Status" id="yaml-status" icon={<CodeIcon />}>
75+
YAML - Status
76+
</DropdownItem>
77+
<DropdownItem value="YAML - All contents" id="yaml-all" icon={<CodeIcon />}>
78+
YAML - All contents
79+
</DropdownItem>
80+
<Divider key="divider-2" />
81+
<DropdownItem value="Upload from computer" id="upload" icon={<UploadIcon />} onClick={open}>
82+
Upload from computer
83+
</DropdownItem>
84+
</DropdownList>
85+
);
86+
87+
const modelOptions = ['GPT-4', 'GPT-3.5', 'Claude', 'Llama 2'];
88+
89+
return (
90+
<>
91+
{/* This is required for react-dropzone to work in Safari and Firefox */}
92+
<input {...getInputProps()} hidden />
93+
<div style={{ marginBottom: '1rem' }}>
94+
<h4 style={{ marginBottom: '0.5rem' }}>Custom attach menu with a PlusIcon at the start</h4>
95+
<MessageBar
96+
onSendMessage={handleSend}
97+
attachButtonPosition="start"
98+
attachMenuProps={{
99+
isAttachMenuOpen: isFirstMenuOpen,
100+
setIsAttachMenuOpen: setIsFirstMenuOpen,
101+
attachMenuItems: firstMenuItems,
102+
onAttachMenuSelect: (_ev, value) => {
103+
// eslint-disable-next-line no-console
104+
console.log('selected', value);
105+
setIsFirstMenuOpen(false);
106+
},
107+
attachMenuInputPlaceholder: 'Search options...',
108+
onAttachMenuToggleClick: onFirstMenuToggle,
109+
onAttachMenuOnOpenChangeKeys: ['Escape', 'Tab']
110+
}}
111+
buttonProps={{
112+
attach: {
113+
icon: <PlusIcon />,
114+
tooltipContent: 'Message actions',
115+
'aria-label': 'Message actions'
116+
}
117+
}}
118+
/>
119+
</div>
120+
121+
<div>
122+
<h4 style={{ marginBottom: '0.5rem' }}>Custom attach menu with additional actions</h4>
123+
<MessageBar
124+
onSendMessage={handleSend}
125+
attachButtonPosition="start"
126+
attachMenuProps={{
127+
isAttachMenuOpen: isSecondMenuOpen,
128+
setIsAttachMenuOpen: setIsSecondMenuOpen,
129+
attachMenuItems: secondMenuItems,
130+
onAttachMenuOnOpenChangeKeys: ['Escape', 'Tab'],
131+
onAttachMenuSelect: (_ev, value) => {
132+
// eslint-disable-next-line no-console
133+
console.log('selected', value);
134+
if (value === 'canvas') {
135+
setShowCanvasLabel(!showCanvasLabel);
136+
}
137+
setIsSecondMenuOpen(false);
138+
},
139+
onAttachMenuToggleClick: onSecondMenuToggle
140+
}}
141+
buttonProps={{
142+
attach: {
143+
icon: <PlusIcon />,
144+
tooltipContent: 'Message actions',
145+
'aria-label': 'Message actions'
146+
}
147+
}}
148+
additionalActions={
149+
<>
150+
<Select
151+
isOpen={isModelSelectOpen}
152+
selected={selectedModel}
153+
shouldFocusToggleOnSelect
154+
onSelect={onModelSelect}
155+
onOpenChange={(isOpen) => setIsModelSelectOpen(isOpen)}
156+
toggle={(toggleRef) => (
157+
<MenuToggle
158+
ref={toggleRef}
159+
variant="plainText"
160+
onClick={() => setIsModelSelectOpen(!isModelSelectOpen)}
161+
isExpanded={isModelSelectOpen}
162+
aria-label={`${selectedModel}, Select a model`}
163+
style={{
164+
minWidth: '120px'
165+
}}
166+
>
167+
{selectedModel}
168+
</MenuToggle>
169+
)}
170+
>
171+
<SelectList>
172+
{modelOptions.map((option) => (
173+
<SelectOption key={option} value={option}>
174+
{option}
175+
</SelectOption>
176+
))}
177+
</SelectList>
178+
</Select>
179+
{showCanvasLabel && (
180+
<Label closeBtnAriaLabel="Remove Canvas mode" onClose={() => setShowCanvasLabel(false)}>
181+
Canvas
182+
</Label>
183+
)}
184+
</>
185+
}
186+
/>
187+
</div>
188+
</>
189+
);
190+
};

packages/module/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,11 @@ import { MessageBar } from '@patternfly/chatbot/dist/dynamic/MessageBar';
7070
import SourceDetailsMenuItem from '@patternfly/chatbot/dist/dynamic/SourceDetailsMenuItem';
7171
import { ChatbotModal } from '@patternfly/chatbot/dist/dynamic/ChatbotModal';
7272
import SettingsForm from '@patternfly/chatbot/dist/dynamic/Settings';
73-
import { BellIcon, CalendarAltIcon, ClipboardIcon, CodeIcon, ThumbtackIcon, UploadIcon } from '@patternfly/react-icons';
73+
import { BellIcon, CalendarAltIcon, ClipboardIcon, CodeIcon, PlusIcon, ThumbtackIcon, UploadIcon } from '@patternfly/react-icons';
7474
import { useDropzone } from 'react-dropzone';
7575

7676
import ChatbotConversationHistoryNav from '@patternfly/chatbot/dist/dynamic/ChatbotConversationHistoryNav';
77-
import { Button, DropdownItem, DropdownList, Checkbox, MenuToggle, Select, SelectList, SelectOption } from '@patternfly/react-core';
77+
import { Button, Label, DropdownItem, DropdownList, Checkbox, MenuToggle, Select, SelectList, SelectOption } from '@patternfly/react-core';
7878

7979
import OutlinedWindowRestoreIcon from '@patternfly/react-icons/dist/esm/icons/outlined-window-restore-icon';
8080
import ExpandIcon from '@patternfly/react-icons/dist/esm/icons/expand-icon';
@@ -291,6 +291,19 @@ Attachments can also be added to the ChatBot via [drag and drop.](/extensions/ch
291291

292292
```
293293

294+
### Message bar with custom attach menu and additional actions
295+
296+
You can move the attach button to the start of the message bar and customize it with a different icon. To include additional actions in the message bar you can also use the `additionalActions` prop.
297+
298+
This example shows two message bar variations:
299+
300+
1. A message bar with a custom attach menu where a `PlusIcon` is positioned at the start
301+
2. The same custom attach menu with additional actions, including a model selector menu and a dismissable "Canvas" label
302+
303+
```js file="./ChatbotMessageBarCustomActions.tsx"
304+
305+
```
306+
294307
### Footer with message bar and footnote
295308

296309
A simple footer with a message bar and footnote would have this code structure:

packages/module/src/MessageBar/MessageBar.scss

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@
6666
padding-block-start: var(--pf-t--global--spacer--xs);
6767
padding-block-end: var(--pf-t--global--spacer--xs);
6868
gap: var(--pf-t--global--spacer--gap--action-to-action--plain);
69+
70+
&.pf-m-grouped {
71+
flex-basis: 100%;
72+
justify-content: space-between;
73+
}
74+
}
75+
76+
&-actions-group {
77+
display: flex;
78+
padding-block-start: var(--pf-t--global--spacer--xs);
79+
padding-block-end: var(--pf-t--global--spacer--xs);
80+
gap: var(--pf-t--global--spacer--gap--action-to-action--plain);
81+
align-items: center;
6982
}
7083

7184
&-input {
@@ -150,7 +163,8 @@
150163
}
151164

152165
.pf-m-compact {
153-
.pf-chatbot__message-bar-actions {
166+
.pf-chatbot__message-bar-actions,
167+
.pf-chatbot__message-bar-actions-group {
154168
padding-block-start: var(--pf-t--global--spacer--sm);
155169
padding-block-end: var(--pf-t--global--spacer--sm);
156170
}

packages/module/src/MessageBar/MessageBar.test.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,4 +488,31 @@ describe('Message bar', () => {
488488

489489
expect(screen.getByRole('textbox').closest('.pf-chatbot__message-bar')).toHaveClass('pf-v6-m-thinking');
490490
});
491+
492+
it('Renders with flex-basis of auto by default', () => {
493+
render(<MessageBar onSendMessage={jest.fn} />);
494+
495+
expect(screen.getByRole('textbox').closest('.pf-chatbot__message-bar-input')).toHaveAttribute(
496+
'style',
497+
'flex-basis: auto;'
498+
);
499+
});
500+
501+
it('Renders with flex-basis of 100% when forceMultilineLayout is true', () => {
502+
render(<MessageBar forceMultilineLayout onSendMessage={jest.fn} />);
503+
504+
expect(screen.getByRole('textbox').closest('.pf-chatbot__message-bar-input')).toHaveAttribute(
505+
'style',
506+
'flex-basis: 100%;'
507+
);
508+
});
509+
510+
it('Renders with flex-basis of 100% when additionalActions is truthy', () => {
511+
render(<MessageBar additionalActions="actions" onSendMessage={jest.fn} />);
512+
513+
expect(screen.getByRole('textbox').closest('.pf-chatbot__message-bar-input')).toHaveAttribute(
514+
'style',
515+
'flex-basis: 100%;'
516+
);
517+
});
491518
});

0 commit comments

Comments
 (0)