@patternfly/chatbot 2.1.0-prerelease.17 → 2.1.0-prerelease.19

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 (48) hide show
  1. package/dist/cjs/ChatbotToggle/ChatbotToggle.d.ts +7 -1
  2. package/dist/cjs/ChatbotToggle/ChatbotToggle.js +4 -4
  3. package/dist/cjs/ChatbotToggle/ChatbotToggle.test.d.ts +1 -0
  4. package/dist/cjs/ChatbotToggle/ChatbotToggle.test.js +60 -0
  5. package/dist/cjs/Message/Message.d.ts +5 -1
  6. package/dist/cjs/Message/Message.js +8 -2
  7. package/dist/cjs/Message/Message.test.js +100 -4
  8. package/dist/cjs/ResponseActions/ResponseActions.test.js +38 -0
  9. package/dist/css/main.css +19 -12
  10. package/dist/css/main.css.map +1 -1
  11. package/dist/esm/ChatbotToggle/ChatbotToggle.d.ts +7 -1
  12. package/dist/esm/ChatbotToggle/ChatbotToggle.js +4 -4
  13. package/dist/esm/ChatbotToggle/ChatbotToggle.test.d.ts +1 -0
  14. package/dist/esm/ChatbotToggle/ChatbotToggle.test.js +55 -0
  15. package/dist/esm/Message/Message.d.ts +5 -1
  16. package/dist/esm/Message/Message.js +8 -2
  17. package/dist/esm/Message/Message.test.js +100 -4
  18. package/dist/esm/ResponseActions/ResponseActions.test.js +38 -0
  19. package/dist/tsconfig.tsbuildinfo +1 -1
  20. package/package.json +5 -1
  21. package/patternfly-docs/content/extensions/chatbot/examples/Messages/BotMessage.tsx +8 -0
  22. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithAttachment.tsx +1 -1
  23. package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +8 -2
  24. package/patternfly-docs/content/extensions/chatbot/examples/Messages/PF-social-color-square.svg +19 -0
  25. package/patternfly-docs/content/extensions/chatbot/examples/Messages/PF-social-dark-square.svg +19 -0
  26. package/patternfly-docs/content/extensions/chatbot/examples/Messages/UserMessage.tsx +8 -1
  27. package/patternfly-docs/content/extensions/chatbot/examples/Messages/user_avatar.svg +18 -0
  28. package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotToggleBasic.tsx +1 -1
  29. package/patternfly-docs/content/extensions/chatbot/examples/UI/ChatbotWelcomeInteraction.tsx +3 -2
  30. package/patternfly-docs/content/extensions/chatbot/examples/UI/CustomClosedIcon.tsx +1 -1
  31. package/patternfly-docs/content/extensions/chatbot/examples/UI/SkipToContent.tsx +1 -1
  32. package/patternfly-docs/content/extensions/chatbot/examples/UI/SquareChatbotToggle.tsx +14 -0
  33. package/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +9 -1
  34. package/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md +1 -1
  35. package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +1 -1
  36. package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.tsx +6 -4
  37. package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachment.tsx +3 -2
  38. package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachmentMenu.tsx +4 -3
  39. package/patternfly-docs/content/extensions/chatbot/examples/demos/EmbeddedChatbot.tsx +5 -3
  40. package/src/ChatbotToggle/ChatbotToggle.scss +11 -5
  41. package/src/ChatbotToggle/ChatbotToggle.test.tsx +47 -0
  42. package/src/ChatbotToggle/ChatbotToggle.tsx +15 -6
  43. package/src/Message/Message.scss +15 -9
  44. package/src/Message/Message.test.tsx +141 -4
  45. package/src/Message/Message.tsx +28 -3
  46. package/src/MessageBox/MessageBox.scss +2 -2
  47. package/src/ResponseActions/ResponseActions.test.tsx +44 -0
  48. package/patternfly-docs/content/extensions/chatbot/examples/Messages/user_avatar.jpg +0 -0
@@ -6,13 +6,11 @@
6
6
  inset-block-end: var(--pf-t--global--spacer--md);
7
7
  inset-inline-end: var(--pf-t--global--spacer--md);
8
8
  background-color: var(--pf-t--global--background--color--inverse--default);
9
- border-radius: var(--pf-t--global--border--radius--pill);
10
9
  --pf-v6-c-button__icon--Color: var(--pf-t--chatbot-toggle--color);
11
10
  padding: var(--pf-t--global--spacer--md);
12
- width: 3rem;
13
- height: 3rem;
14
11
 
15
- &:hover, &:focus {
12
+ &:hover,
13
+ &:focus {
16
14
  background-color: var(--pf-t--chatbot-toggle--background--hover);
17
15
  }
18
16
 
@@ -21,6 +19,14 @@
21
19
  }
22
20
 
23
21
  // Icon
24
- svg { width: var(--pf-t--global--spacer--lg); height: var(--pf-t--global--spacer--lg); }
22
+ svg {
23
+ width: var(--pf-t--global--spacer--lg);
24
+ height: var(--pf-t--global--spacer--lg);
25
+ }
25
26
  }
26
27
 
28
+ .pf-chatbot__button--round {
29
+ border-radius: var(--pf-t--global--border--radius--pill);
30
+ width: 3rem;
31
+ height: 3rem;
32
+ }
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import userEvent from '@testing-library/user-event';
5
+ import ChatbotToggle from './ChatbotToggle';
6
+
7
+ describe('ChatbotToggle', () => {
8
+ it('should render tooltipLabel correctly', async () => {
9
+ render(<ChatbotToggle tooltipLabel="Tooltip" />);
10
+ await userEvent.click(screen.getByRole('button', { name: /Tooltip toggle/i }));
11
+ expect(screen.getByRole('tooltip', { name: /Tooltip/i })).toBeTruthy();
12
+ });
13
+ it('should render toggleButtonLabel correctly', async () => {
14
+ render(<ChatbotToggle tooltipLabel="Chatbot" toggleButtonLabel="Button" />);
15
+ expect(screen.getByRole('button', { name: /Button/i })).toBeTruthy();
16
+ });
17
+ it('should call onToggleChatbot when clicked', async () => {
18
+ const spy = jest.fn();
19
+ render(<ChatbotToggle tooltipLabel="Chatbot" onToggleChatbot={spy} />);
20
+ await userEvent.click(screen.getByRole('button'));
21
+ expect(spy).toHaveBeenCalledTimes(1);
22
+ });
23
+ it('should handle isChatbotVisible correctly when true', () => {
24
+ render(<ChatbotToggle tooltipLabel="Chatbot" isChatbotVisible openIconTestId="Open" />);
25
+ expect(screen.getByRole('button')).toHaveClass('pf-chatbot__button');
26
+ expect(screen.getByRole('button')).toHaveClass('pf-chatbot__button--active');
27
+ expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true');
28
+ expect(screen.getByTestId('Open')).toBeTruthy();
29
+ });
30
+ it('should handle isChatbotVisible correctly when false', () => {
31
+ render(<ChatbotToggle tooltipLabel="Chatbot" isChatbotVisible={false} openIconTestId="Open" />);
32
+ expect(screen.getByRole('button')).toHaveClass('pf-chatbot__button');
33
+ expect(screen.getByRole('button')).not.toHaveClass('pf-chatbot__button--active');
34
+ expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false');
35
+ expect(screen.queryByTestId('Open')).toBeFalsy();
36
+ });
37
+ it('should handle isRound correctly', () => {
38
+ render(<ChatbotToggle tooltipLabel="Chatbot" isRound />);
39
+ expect(screen.getByRole('button')).toHaveClass('pf-chatbot__button');
40
+ expect(screen.getByRole('button')).toHaveClass('pf-chatbot__button--round');
41
+ });
42
+ it('should handle className correctly', () => {
43
+ render(<ChatbotToggle tooltipLabel="Chatbot" className="test" />);
44
+ expect(screen.getByRole('button')).toHaveClass('pf-chatbot__button');
45
+ expect(screen.getByRole('button')).toHaveClass('test');
46
+ });
47
+ });
@@ -7,7 +7,7 @@ import AngleDownIcon from '@patternfly/react-icons/dist/esm/icons/angle-down-ico
7
7
 
8
8
  export interface ChatbotToggleProps extends ButtonProps {
9
9
  /** Contents of the tooltip applied to the toggle button */
10
- toolTipLabel?: React.ReactNode;
10
+ tooltipLabel: React.ReactNode;
11
11
  /** Props spread to the PF Tooltip component */
12
12
  tooltipProps?: Omit<TooltipProps, 'content'>;
13
13
  /** Flag indicating visibility of the chatbot appended to the toggle */
@@ -20,6 +20,12 @@ export interface ChatbotToggleProps extends ButtonProps {
20
20
  closedToggleIcon?: () => JSX.Element;
21
21
  /** Ref applied to toggle */
22
22
  innerRef?: React.Ref<HTMLButtonElement>;
23
+ /** Whether toggle is a circle */
24
+ isRound?: boolean;
25
+ /** Class name applied to toggle */
26
+ className?: string;
27
+ /** Test id applied to default open icon */
28
+ openIconTestId?: string;
23
29
  }
24
30
 
25
31
  const ChatIcon = () => (
@@ -42,25 +48,28 @@ const ChatIcon = () => (
42
48
  );
43
49
 
44
50
  const ChatbotToggleBase: React.FunctionComponent<ChatbotToggleProps> = ({
45
- toolTipLabel,
51
+ tooltipLabel,
46
52
  isChatbotVisible,
47
53
  onToggleChatbot,
48
54
  tooltipProps,
49
55
  toggleButtonLabel,
50
56
  closedToggleIcon: ClosedToggleIcon,
51
57
  innerRef,
58
+ isRound = true,
59
+ className,
60
+ openIconTestId,
52
61
  ...props
53
62
  }: ChatbotToggleProps) => {
54
63
  // Configure icon
55
64
  const closedIcon = ClosedToggleIcon ? <ClosedToggleIcon /> : <ChatIcon />;
56
- const icon = isChatbotVisible ? <AngleDownIcon /> : closedIcon;
65
+ const icon = isChatbotVisible ? <AngleDownIcon data-testid={openIconTestId} /> : closedIcon;
57
66
 
58
67
  return (
59
- <Tooltip content={toolTipLabel} {...tooltipProps}>
68
+ <Tooltip content={tooltipLabel} {...tooltipProps}>
60
69
  <Button
61
- className={`pf-chatbot__button ${isChatbotVisible ? 'pf-chatbot__button--active' : ''}`}
70
+ className={`pf-chatbot__button ${isChatbotVisible ? 'pf-chatbot__button--active' : ''} ${isRound ? 'pf-chatbot__button--round' : ''} ${className ? className : ''}`}
62
71
  variant="plain"
63
- aria-label={toggleButtonLabel || `${toolTipLabel} toggle`}
72
+ aria-label={toggleButtonLabel || `${tooltipLabel} toggle`}
64
73
  onClick={onToggleChatbot}
65
74
  aria-expanded={isChatbotVisible}
66
75
  icon={<Icon isInline>{icon}</Icon>}
@@ -15,23 +15,29 @@
15
15
  gap: var(--pf-t--global--spacer--lg);
16
16
  padding-bottom: var(--pf-t--global--spacer--2xl);
17
17
 
18
- // Name
19
- // --------------------------------------------------------------------------
20
- .pf-v6-c-truncate {
21
- --pf-v6-c-truncate--MinWidth: 0ch;
22
- --pf-v6-c-truncate__start--MinWidth: 0ch;
23
- }
24
18
  // Avatar
25
19
  // --------------------------------------------------------------------------
26
- .pf-v6-c-avatar {
27
- --pf-v6-c-avatar--Width: 3rem;
28
- --pf-v6-c-avatar--Height: 3rem;
20
+ &-avatar.pf-v6-c-avatar {
21
+ --pf-v6-c-avatar--BorderRadius: 0;
29
22
  position: sticky;
30
23
  top: var(--pf-t--global--spacer--md);
31
24
  object-fit: cover;
32
25
  pointer-events: none; // prevent dragging - interferes with FileDropZone
33
26
  }
34
27
 
28
+ &-avatar.pf-chatbot__message-avatar--round.pf-v6-c-avatar {
29
+ --pf-v6-c-avatar--Width: 3rem;
30
+ --pf-v6-c-avatar--Height: 3rem;
31
+ --pf-v6-c-avatar--BorderRadius: var(--pf-t--global--border--radius--pill);
32
+ }
33
+
34
+ // Name
35
+ // --------------------------------------------------------------------------
36
+ .pf-v6-c-truncate {
37
+ --pf-v6-c-truncate--MinWidth: 0ch;
38
+ --pf-v6-c-truncate__start--MinWidth: 0ch;
39
+ }
40
+
35
41
  // Contents
36
42
  // --------------------------------------------------------------------------
37
43
  &-contents {
@@ -68,7 +68,14 @@ describe('Message', () => {
68
68
  expect(screen.getByText('User')).toBeTruthy();
69
69
  expect(screen.getByText('Hi')).toBeTruthy();
70
70
  const date = new Date();
71
- expect(screen.getByText(`${date.toLocaleDateString()}, ${date.toLocaleTimeString()}`)).toBeTruthy();
71
+ const formattedDate = date.toLocaleDateString();
72
+ expect(
73
+ screen.getByText((content, element) => {
74
+ const hasText = content.includes(formattedDate);
75
+ const isVisible = element?.tagName.toLowerCase() !== 'script' && element?.tagName.toLowerCase() !== 'style';
76
+ return hasText && isVisible;
77
+ })
78
+ ).toBeInTheDocument();
72
79
  expect(screen.queryByText('Loading message')).toBeFalsy();
73
80
  expect(screen.getByRole('img')).toHaveAttribute('src', './img');
74
81
  });
@@ -78,7 +85,14 @@ describe('Message', () => {
78
85
  expect(screen.getByText('AI')).toBeTruthy();
79
86
  expect(screen.getByText('Hi')).toBeTruthy();
80
87
  const date = new Date();
81
- expect(screen.getByText(`${date.toLocaleDateString()}, ${date.toLocaleTimeString()}`)).toBeTruthy();
88
+ const formattedDate = date.toLocaleDateString();
89
+ expect(
90
+ screen.getByText((content, element) => {
91
+ const hasText = content.includes(formattedDate);
92
+ const isVisible = element?.tagName.toLowerCase() !== 'script' && element?.tagName.toLowerCase() !== 'style';
93
+ return hasText && isVisible;
94
+ })
95
+ ).toBeInTheDocument();
82
96
  });
83
97
  it('should render avatar correctly', () => {
84
98
  render(<Message avatar="./testImg" role="bot" name="Bot" content="Hi" />);
@@ -98,7 +112,14 @@ describe('Message', () => {
98
112
  expect(screen.getByText('Hi')).toBeTruthy();
99
113
  expect(screen.getByText('2 hours ago')).toBeTruthy();
100
114
  const date = new Date();
101
- expect(screen.queryByText(`${date.toLocaleDateString()}, ${date.toLocaleTimeString()}`)).toBeFalsy();
115
+ const formattedDate = date.toLocaleDateString();
116
+ expect(
117
+ screen.queryByText((content, element) => {
118
+ const hasText = content.includes(formattedDate);
119
+ const isVisible = element?.tagName.toLowerCase() !== 'script' && element?.tagName.toLowerCase() !== 'style';
120
+ return hasText && isVisible;
121
+ })
122
+ ).not.toBeInTheDocument();
102
123
  });
103
124
  it('should render attachments', () => {
104
125
  render(<Message avatar="./img" role="user" content="Hi" attachments={[{ name: 'testAttachment' }]} />);
@@ -132,7 +153,14 @@ describe('Message', () => {
132
153
  expect(screen.getByText('AI')).toBeTruthy();
133
154
  expect(screen.queryByText('Hi')).toBeFalsy();
134
155
  const date = new Date();
135
- expect(screen.getByText(`${date.toLocaleDateString()}, ${date.toLocaleTimeString()}`)).toBeTruthy();
156
+ const formattedDate = date.toLocaleDateString();
157
+ expect(
158
+ screen.getByText((content, element) => {
159
+ const hasText = content.includes(formattedDate);
160
+ const isVisible = element?.tagName.toLowerCase() !== 'script' && element?.tagName.toLowerCase() !== 'style';
161
+ return hasText && isVisible;
162
+ })
163
+ ).toBeInTheDocument();
136
164
  expect(screen.getByText('Loading message')).toBeTruthy();
137
165
  });
138
166
  it('should be able to show sources', async () => {
@@ -177,6 +205,83 @@ describe('Message', () => {
177
205
  expect(screen.getByText('Loading message')).toBeTruthy();
178
206
  expect(screen.queryByText('Getting started with Red Hat OpenShift')).toBeFalsy();
179
207
  });
208
+ it('should be able to show quick response', async () => {
209
+ const spy = jest.fn();
210
+ render(
211
+ <Message
212
+ avatar="./img"
213
+ role="bot"
214
+ name="Bot"
215
+ content="Hi"
216
+ quickResponses={[
217
+ {
218
+ id: '1',
219
+ content: 'Yes',
220
+ onClick: spy,
221
+ className: 'test'
222
+ }
223
+ ]}
224
+ />
225
+ );
226
+ const quickResponse = screen.getByRole('button', { name: /Yes/i });
227
+ expect(quickResponse).toBeTruthy();
228
+ await userEvent.click(quickResponse);
229
+ expect(spy).toHaveBeenCalledTimes(1);
230
+ });
231
+ it('should be able to show more than 1 quick response', async () => {
232
+ const spy = jest.fn();
233
+ render(
234
+ <Message
235
+ avatar="./img"
236
+ role="bot"
237
+ name="Bot"
238
+ content="Hi"
239
+ quickResponses={[
240
+ {
241
+ id: '1',
242
+ content: 'Yes',
243
+ onClick: spy
244
+ },
245
+ {
246
+ id: '2',
247
+ content: 'No',
248
+ onClick: spy
249
+ }
250
+ ]}
251
+ />
252
+ );
253
+ expect(screen.getByRole('button', { name: /Yes/i })).toBeTruthy();
254
+ expect(screen.getByRole('button', { name: /No/i })).toBeTruthy();
255
+ });
256
+ it('should be able to spread quickResponseContainerProps', async () => {
257
+ const spy = jest.fn();
258
+ render(
259
+ <Message
260
+ avatar="./img"
261
+ role="bot"
262
+ name="Bot"
263
+ content="Hi"
264
+ quickResponses={[
265
+ {
266
+ id: '1',
267
+ content: 'Yes',
268
+ onClick: spy
269
+ },
270
+ {
271
+ id: '2',
272
+ content: 'No',
273
+ onClick: spy
274
+ }
275
+ ]}
276
+ // this is a LabelGroup prop that changes the default number shown
277
+ // to be different than what we use in ChatBot
278
+ quickResponseContainerProps={{ numLabels: 1 }}
279
+ />
280
+ );
281
+ expect(screen.getByRole('button', { name: /Yes/i })).toBeTruthy();
282
+ expect(screen.queryByRole('button', { name: /No/i })).toBeFalsy();
283
+ expect(screen.getByRole('button', { name: /1 more/i }));
284
+ });
180
285
  it('should be able to show actions', async () => {
181
286
  render(
182
287
  <Message
@@ -278,4 +383,36 @@ describe('Message', () => {
278
383
  );
279
384
  expect(screen.getByRole('button', { name: 'test' })).toBeTruthy();
280
385
  });
386
+ it('should handle hasRoundAvatar correctly when it is true', () => {
387
+ render(<Message avatar="./img" role="user" name="User" content="Hi" hasRoundAvatar />);
388
+ expect(screen.getByRole('img')).toBeTruthy();
389
+ expect(screen.getByRole('img')).toHaveClass('pf-chatbot__message-avatar');
390
+ expect(screen.getByRole('img')).toHaveClass('pf-chatbot__message-avatar--round');
391
+ });
392
+ it('should handle hasRoundAvatar correctly when it is false', () => {
393
+ render(<Message avatar="./img" role="user" name="User" content="Hi" hasRoundAvatar={false} />);
394
+ expect(screen.getByRole('img')).toBeTruthy();
395
+ expect(screen.getByRole('img')).toHaveClass('pf-chatbot__message-avatar');
396
+ expect(screen.getByRole('img')).not.toHaveClass('pf-chatbot__message-avatar--round');
397
+ });
398
+ it('should handle avatarProps correctly by spreading it onto the Message Avatar', () => {
399
+ render(<Message avatar="./img" role="user" name="User" content="Hi" avatarProps={{ className: 'test' }} />);
400
+ expect(screen.getByRole('img')).toBeTruthy();
401
+ expect(screen.getByRole('img')).toHaveClass('test');
402
+ });
403
+ it('should handle avatarProps and hasRoundAvatar correctly', () => {
404
+ render(
405
+ <Message
406
+ avatar="./img"
407
+ role="user"
408
+ name="User"
409
+ content="Hi"
410
+ avatarProps={{ className: 'test' }}
411
+ hasRoundAvatar={false}
412
+ />
413
+ );
414
+ expect(screen.getByRole('img')).toBeTruthy();
415
+ expect(screen.getByRole('img')).toHaveClass('test');
416
+ expect(screen.getByRole('img')).toHaveClass('pf-chatbot__message-avatar');
417
+ });
281
418
  });
@@ -6,7 +6,16 @@ import React from 'react';
6
6
 
7
7
  import Markdown from 'react-markdown';
8
8
  import remarkGfm from 'remark-gfm';
9
- import { Avatar, Label, LabelGroup, LabelGroupProps, LabelProps, Timestamp, Truncate } from '@patternfly/react-core';
9
+ import {
10
+ Avatar,
11
+ AvatarProps,
12
+ Label,
13
+ LabelGroup,
14
+ LabelGroupProps,
15
+ LabelProps,
16
+ Timestamp,
17
+ Truncate
18
+ } from '@patternfly/react-core';
10
19
  import MessageLoading from './MessageLoading';
11
20
  import CodeBlockMessage from './CodeBlockMessage/CodeBlockMessage';
12
21
  import TextMessage from './TextMessage/TextMessage';
@@ -76,6 +85,10 @@ export interface MessageProps extends Omit<React.HTMLProps<HTMLDivElement>, 'rol
76
85
  quickResponses?: QuickResponse[];
77
86
  /** Props for quick responses container */
78
87
  quickResponseContainerProps?: Omit<LabelGroupProps, 'ref'>;
88
+ /** Whether avatar is round */
89
+ hasRoundAvatar?: boolean;
90
+ /** Any additional props applied to the avatar, for additional customization */
91
+ avatarProps?: Omit<AvatarProps, 'alt'>;
79
92
  }
80
93
 
81
94
  export const Message: React.FunctionComponent<MessageProps> = ({
@@ -93,12 +106,19 @@ export const Message: React.FunctionComponent<MessageProps> = ({
93
106
  quickResponses,
94
107
  quickResponseContainerProps = { numLabels: 5 },
95
108
  attachments,
109
+ hasRoundAvatar = true,
110
+ avatarProps,
96
111
  ...props
97
112
  }: MessageProps) => {
113
+ let avatarClassName;
114
+ if (avatarProps && 'className' in avatarProps) {
115
+ const { className, ...rest } = avatarProps;
116
+ avatarClassName = className;
117
+ avatarProps = { ...rest };
118
+ }
98
119
  // Keep timestamps consistent between Timestamp component and aria-label
99
120
  const date = new Date();
100
121
  const dateString = timestamp ?? `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
101
-
102
122
  return (
103
123
  <section
104
124
  aria-label={`Message from ${role} - ${dateString}`}
@@ -106,7 +126,12 @@ export const Message: React.FunctionComponent<MessageProps> = ({
106
126
  {...props}
107
127
  >
108
128
  {/* We are using an empty alt tag intentionally in order to reduce noise on screen readers */}
109
- <Avatar src={avatar} alt="" />
129
+ <Avatar
130
+ className={`pf-chatbot__message-avatar ${hasRoundAvatar ? 'pf-chatbot__message-avatar--round' : ''} ${avatarClassName ? avatarClassName : ''}`}
131
+ src={avatar}
132
+ alt=""
133
+ {...avatarProps}
134
+ />
110
135
  <div className="pf-chatbot__message-contents">
111
136
  <div className="pf-chatbot__message-meta">
112
137
  {name && (
@@ -9,8 +9,8 @@
9
9
  padding: var(--pf-t--global--spacer--lg);
10
10
  }
11
11
 
12
- .pf-chatbot__messagebox--bottom {
13
- justify-content: flex-end;
12
+ .pf-chatbot__messagebox--bottom > :first-child {
13
+ margin-top: auto !important;
14
14
  }
15
15
 
16
16
  // hide from view but not assistive technologies
@@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
4
  import ResponseActions from './ResponseActions';
5
5
  import userEvent from '@testing-library/user-event';
6
+ import { DownloadIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons';
6
7
 
7
8
  const ALL_ACTIONS = [
8
9
  { type: 'positive', label: 'Good response' },
@@ -12,6 +13,29 @@ const ALL_ACTIONS = [
12
13
  { type: 'listen', label: 'Listen' }
13
14
  ];
14
15
 
16
+ const CUSTOM_ACTIONS = [
17
+ {
18
+ regenerate: {
19
+ ariaLabel: 'Regenerate',
20
+ onClick: jest.fn(),
21
+ tooltipContent: 'Regenerate',
22
+ icon: <RedoIcon />
23
+ },
24
+ download: {
25
+ ariaLabel: 'Download',
26
+ onClick: jest.fn(),
27
+ tooltipContent: 'Download',
28
+ icon: <DownloadIcon />
29
+ },
30
+ info: {
31
+ ariaLabel: 'Info',
32
+ onClick: jest.fn(),
33
+ tooltipContent: 'Info',
34
+ icon: <InfoCircleIcon />
35
+ }
36
+ }
37
+ ];
38
+
15
39
  describe('ResponseActions', () => {
16
40
  it('should render buttons correctly', () => {
17
41
  ALL_ACTIONS.forEach(({ type, label }) => {
@@ -56,4 +80,24 @@ describe('ResponseActions', () => {
56
80
  expect(screen.getByRole('button', { name: label })).toHaveClass('test');
57
81
  });
58
82
  });
83
+
84
+ it('should be able to add custom actions', () => {
85
+ CUSTOM_ACTIONS.forEach((action) => {
86
+ const key = Object.keys(action)[0];
87
+ render(
88
+ <ResponseActions
89
+ actions={{
90
+ [key]: {
91
+ tooltipContent: action[key].tooltipContent,
92
+ onClick: action[key].onClick,
93
+ // doing this just because it's easier to test without a regex for the button name
94
+ ariaLabel: action[key].ariaLabel.toLowerCase(),
95
+ icon: action[key].icon
96
+ }
97
+ }}
98
+ />
99
+ );
100
+ expect(screen.getByRole('button', { name: key })).toBeTruthy();
101
+ });
102
+ });
59
103
  });