@patternfly/chatbot 2.2.0-prerelease.4 → 2.2.0-prerelease.5
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.
- package/dist/cjs/ResponseActions/ResponseActionButton.d.ts +6 -0
- package/dist/cjs/ResponseActions/ResponseActionButton.js +10 -2
- package/dist/cjs/ResponseActions/ResponseActionButton.test.d.ts +1 -0
- package/dist/cjs/ResponseActions/ResponseActionButton.test.js +54 -0
- package/dist/cjs/ResponseActions/ResponseActions.d.ts +4 -0
- package/dist/cjs/ResponseActions/ResponseActions.js +26 -9
- package/dist/cjs/ResponseActions/ResponseActions.test.js +79 -5
- package/dist/css/main.css +11 -4
- package/dist/css/main.css.map +1 -1
- package/dist/esm/ResponseActions/ResponseActionButton.d.ts +6 -0
- package/dist/esm/ResponseActions/ResponseActionButton.js +10 -2
- package/dist/esm/ResponseActions/ResponseActionButton.test.d.ts +1 -0
- package/dist/esm/ResponseActions/ResponseActionButton.test.js +49 -0
- package/dist/esm/ResponseActions/ResponseActions.d.ts +4 -0
- package/dist/esm/ResponseActions/ResponseActions.js +26 -9
- package/dist/esm/ResponseActions/ResponseActions.test.js +79 -5
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomResponseActions.tsx +4 -0
- package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +13 -2
- package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +2 -2
- package/src/ResponseActions/ResponseActionButton.test.tsx +52 -0
- package/src/ResponseActions/ResponseActionButton.tsx +46 -27
- package/src/ResponseActions/ResponseActions.scss +10 -8
- package/src/ResponseActions/ResponseActions.test.tsx +103 -5
- package/src/ResponseActions/ResponseActions.tsx +54 -7
@@ -13,25 +13,30 @@ import '@testing-library/jest-dom';
|
|
13
13
|
import ResponseActions from './ResponseActions';
|
14
14
|
import userEvent from '@testing-library/user-event';
|
15
15
|
import { DownloadIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons';
|
16
|
+
import Message from '../Message';
|
16
17
|
const ALL_ACTIONS = [
|
17
|
-
{ type: 'positive', label: 'Good response' },
|
18
|
-
{ type: 'negative', label: 'Bad response' },
|
19
|
-
{ type: 'copy', label: 'Copy' },
|
20
|
-
{ type: 'share', label: 'Share' },
|
21
|
-
{ type: 'listen', label: 'Listen' }
|
18
|
+
{ type: 'positive', label: 'Good response', clickedLabel: 'Response recorded' },
|
19
|
+
{ type: 'negative', label: 'Bad response', clickedLabel: 'Response recorded' },
|
20
|
+
{ type: 'copy', label: 'Copy', clickedLabel: 'Copied' },
|
21
|
+
{ type: 'share', label: 'Share', clickedLabel: 'Shared' },
|
22
|
+
{ type: 'listen', label: 'Listen', clickedLabel: 'Listening' }
|
22
23
|
];
|
23
24
|
const CUSTOM_ACTIONS = [
|
24
25
|
{
|
25
26
|
regenerate: {
|
26
27
|
ariaLabel: 'Regenerate',
|
28
|
+
clickedAriaLabel: 'Regenerated',
|
27
29
|
onClick: jest.fn(),
|
28
30
|
tooltipContent: 'Regenerate',
|
31
|
+
clickedTooltipContent: 'Regenerated',
|
29
32
|
icon: React.createElement(RedoIcon, null)
|
30
33
|
},
|
31
34
|
download: {
|
32
35
|
ariaLabel: 'Download',
|
36
|
+
clickedAriaLabel: 'Downloaded',
|
33
37
|
onClick: jest.fn(),
|
34
38
|
tooltipContent: 'Download',
|
39
|
+
clickedTooltipContent: 'Downloaded',
|
35
40
|
icon: React.createElement(DownloadIcon, null)
|
36
41
|
},
|
37
42
|
info: {
|
@@ -43,6 +48,59 @@ const CUSTOM_ACTIONS = [
|
|
43
48
|
}
|
44
49
|
];
|
45
50
|
describe('ResponseActions', () => {
|
51
|
+
afterEach(() => {
|
52
|
+
jest.clearAllMocks();
|
53
|
+
});
|
54
|
+
it('should handle click within group of buttons correctly', () => __awaiter(void 0, void 0, void 0, function* () {
|
55
|
+
render(React.createElement(ResponseActions, { actions: {
|
56
|
+
positive: { onClick: jest.fn() },
|
57
|
+
negative: { onClick: jest.fn() },
|
58
|
+
copy: { onClick: jest.fn() },
|
59
|
+
share: { onClick: jest.fn() },
|
60
|
+
listen: { onClick: jest.fn() }
|
61
|
+
} }));
|
62
|
+
const goodBtn = screen.getByRole('button', { name: 'Good response' });
|
63
|
+
const badBtn = screen.getByRole('button', { name: 'Bad response' });
|
64
|
+
const copyBtn = screen.getByRole('button', { name: 'Copy' });
|
65
|
+
const shareBtn = screen.getByRole('button', { name: 'Share' });
|
66
|
+
const listenBtn = screen.getByRole('button', { name: 'Listen' });
|
67
|
+
const buttons = [goodBtn, badBtn, copyBtn, shareBtn, listenBtn];
|
68
|
+
buttons.forEach((button) => {
|
69
|
+
expect(button).toBeTruthy();
|
70
|
+
});
|
71
|
+
yield userEvent.click(goodBtn);
|
72
|
+
expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
|
73
|
+
let unclickedButtons = buttons.filter((button) => button !== goodBtn);
|
74
|
+
unclickedButtons.forEach((button) => {
|
75
|
+
expect(button).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
76
|
+
});
|
77
|
+
yield userEvent.click(badBtn);
|
78
|
+
expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
|
79
|
+
unclickedButtons = buttons.filter((button) => button !== badBtn);
|
80
|
+
unclickedButtons.forEach((button) => {
|
81
|
+
expect(button).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
82
|
+
});
|
83
|
+
}));
|
84
|
+
it('should handle click outside of group of buttons correctly', () => __awaiter(void 0, void 0, void 0, function* () {
|
85
|
+
// using message just so we have something outside the group that's rendered
|
86
|
+
render(React.createElement(Message, { name: "Bot", role: "bot", avatar: "", content: "Example with all prebuilt actions", actions: {
|
87
|
+
positive: {},
|
88
|
+
negative: {}
|
89
|
+
} }));
|
90
|
+
const goodBtn = screen.getByRole('button', { name: 'Good response' });
|
91
|
+
const badBtn = screen.getByRole('button', { name: 'Bad response' });
|
92
|
+
expect(goodBtn).toBeTruthy();
|
93
|
+
expect(badBtn).toBeTruthy();
|
94
|
+
yield userEvent.click(goodBtn);
|
95
|
+
expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
|
96
|
+
expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
97
|
+
yield userEvent.click(badBtn);
|
98
|
+
expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass('pf-chatbot__button--response-action-clicked');
|
99
|
+
expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
100
|
+
yield userEvent.click(screen.getByText('Example with all prebuilt actions'));
|
101
|
+
expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
102
|
+
expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
103
|
+
}));
|
46
104
|
it('should render buttons correctly', () => {
|
47
105
|
ALL_ACTIONS.forEach(({ type, label }) => {
|
48
106
|
render(React.createElement(ResponseActions, { actions: { [type]: { onClick: jest.fn() } } }));
|
@@ -57,6 +115,22 @@ describe('ResponseActions', () => {
|
|
57
115
|
expect(spy).toHaveBeenCalledTimes(1);
|
58
116
|
}));
|
59
117
|
}));
|
118
|
+
it('should swap clicked and non-clicked aria labels on click', () => __awaiter(void 0, void 0, void 0, function* () {
|
119
|
+
ALL_ACTIONS.forEach((_a) => __awaiter(void 0, [_a], void 0, function* ({ type, label, clickedLabel }) {
|
120
|
+
render(React.createElement(ResponseActions, { actions: { [type]: { onClick: jest.fn() } } }));
|
121
|
+
expect(screen.getByRole('button', { name: label })).toBeTruthy();
|
122
|
+
yield userEvent.click(screen.getByRole('button', { name: label }));
|
123
|
+
expect(screen.getByRole('button', { name: clickedLabel })).toBeTruthy();
|
124
|
+
}));
|
125
|
+
}));
|
126
|
+
it('should swap clicked and non-clicked tooltips on click', () => __awaiter(void 0, void 0, void 0, function* () {
|
127
|
+
ALL_ACTIONS.forEach((_a) => __awaiter(void 0, [_a], void 0, function* ({ type, label, clickedLabel }) {
|
128
|
+
render(React.createElement(ResponseActions, { actions: { [type]: { onClick: jest.fn() } } }));
|
129
|
+
expect(screen.getByRole('button', { name: label })).toBeTruthy();
|
130
|
+
yield userEvent.click(screen.getByRole('button', { name: label }));
|
131
|
+
expect(screen.getByRole('tooltip', { name: clickedLabel })).toBeTruthy();
|
132
|
+
}));
|
133
|
+
}));
|
60
134
|
it('should be able to change aria labels', () => {
|
61
135
|
const actions = [
|
62
136
|
{ type: 'positive', ariaLabel: 'Thumbs up' },
|
@@ -1 +1 @@
|
|
1
|
-
{"root":["../src/index.ts","../src/AttachMenu/AttachMenu.tsx","../src/AttachMenu/index.ts","../src/AttachmentEdit/AttachmentEdit.tsx","../src/AttachmentEdit/index.ts","../src/Chatbot/Chatbot.tsx","../src/Chatbot/index.ts","../src/ChatbotAlert/ChatbotAlert.tsx","../src/ChatbotAlert/index.ts","../src/ChatbotContent/ChatbotContent.tsx","../src/ChatbotContent/index.ts","../src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.test.tsx","../src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.tsx","../src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx","../src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx","../src/ChatbotConversationHistoryNav/index.ts","../src/ChatbotFooter/ChatbotFooter.tsx","../src/ChatbotFooter/ChatbotFootnote.tsx","../src/ChatbotFooter/index.ts","../src/ChatbotHeader/ChatbotHeader.tsx","../src/ChatbotHeader/ChatbotHeaderActions.tsx","../src/ChatbotHeader/ChatbotHeaderMain.tsx","../src/ChatbotHeader/ChatbotHeaderMenu.tsx","../src/ChatbotHeader/ChatbotHeaderOptionsDropdown.tsx","../src/ChatbotHeader/ChatbotHeaderSelectorDropdown.tsx","../src/ChatbotHeader/ChatbotHeaderTitle.tsx","../src/ChatbotHeader/index.ts","../src/ChatbotModal/ChatbotModal.tsx","../src/ChatbotModal/index.ts","../src/ChatbotPopover/ChatbotPopover.tsx","../src/ChatbotPopover/index.ts","../src/ChatbotToggle/ChatbotToggle.test.tsx","../src/ChatbotToggle/ChatbotToggle.tsx","../src/ChatbotToggle/index.ts","../src/ChatbotWelcomePrompt/ChatbotWelcomePrompt.test.tsx","../src/ChatbotWelcomePrompt/ChatbotWelcomePrompt.tsx","../src/ChatbotWelcomePrompt/index.ts","../src/CodeModal/CodeModal.tsx","../src/CodeModal/index.ts","../src/FileDetails/FileDetails.test.tsx","../src/FileDetails/FileDetails.tsx","../src/FileDetails/index.ts","../src/FileDetailsLabel/FileDetailsLabel.test.tsx","../src/FileDetailsLabel/FileDetailsLabel.tsx","../src/FileDetailsLabel/index.ts","../src/FileDropZone/FileDropZone.test.tsx","../src/FileDropZone/FileDropZone.tsx","../src/FileDropZone/index.ts","../src/LoadingMessage/LoadingMessage.test.tsx","../src/LoadingMessage/LoadingMessage.tsx","../src/LoadingMessage/index.ts","../src/Message/Message.test.tsx","../src/Message/Message.tsx","../src/Message/MessageLoading.tsx","../src/Message/index.ts","../src/Message/CodeBlockMessage/CodeBlockMessage.tsx","../src/Message/ListMessage/ListItemMessage.tsx","../src/Message/ListMessage/OrderedListMessage.tsx","../src/Message/ListMessage/UnorderedListMessage.tsx","../src/Message/TextMessage/TextMessage.tsx","../src/MessageBar/AttachButton.test.tsx","../src/MessageBar/AttachButton.tsx","../src/MessageBar/MessageBar.test.tsx","../src/MessageBar/MessageBar.tsx","../src/MessageBar/MicrophoneButton.tsx","../src/MessageBar/SendButton.test.tsx","../src/MessageBar/SendButton.tsx","../src/MessageBar/StopButton.test.tsx","../src/MessageBar/StopButton.tsx","../src/MessageBar/index.ts","../src/MessageBox/JumpButton.test.tsx","../src/MessageBox/JumpButton.tsx","../src/MessageBox/MessageBox.tsx","../src/MessageBox/index.ts","../src/PreviewAttachment/PreviewAttachment.tsx","../src/PreviewAttachment/index.ts","../src/ResponseActions/ResponseActionButton.tsx","../src/ResponseActions/ResponseActions.test.tsx","../src/ResponseActions/ResponseActions.tsx","../src/ResponseActions/index.ts","../src/SourceDetailsMenuItem/SourceDetailsMenuItem.tsx","../src/SourceDetailsMenuItem/index.ts","../src/SourcesCard/SourcesCard.test.tsx","../src/SourcesCard/SourcesCard.tsx","../src/SourcesCard/index.ts","../src/TermsOfUse/TermsOfUse.test.tsx","../src/TermsOfUse/TermsOfUse.tsx","../src/TermsOfUse/index.ts"],"version":"5.6.3"}
|
1
|
+
{"root":["../src/index.ts","../src/AttachMenu/AttachMenu.tsx","../src/AttachMenu/index.ts","../src/AttachmentEdit/AttachmentEdit.tsx","../src/AttachmentEdit/index.ts","../src/Chatbot/Chatbot.tsx","../src/Chatbot/index.ts","../src/ChatbotAlert/ChatbotAlert.tsx","../src/ChatbotAlert/index.ts","../src/ChatbotContent/ChatbotContent.tsx","../src/ChatbotContent/index.ts","../src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.test.tsx","../src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.tsx","../src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.test.tsx","../src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx","../src/ChatbotConversationHistoryNav/index.ts","../src/ChatbotFooter/ChatbotFooter.tsx","../src/ChatbotFooter/ChatbotFootnote.tsx","../src/ChatbotFooter/index.ts","../src/ChatbotHeader/ChatbotHeader.tsx","../src/ChatbotHeader/ChatbotHeaderActions.tsx","../src/ChatbotHeader/ChatbotHeaderMain.tsx","../src/ChatbotHeader/ChatbotHeaderMenu.tsx","../src/ChatbotHeader/ChatbotHeaderOptionsDropdown.tsx","../src/ChatbotHeader/ChatbotHeaderSelectorDropdown.tsx","../src/ChatbotHeader/ChatbotHeaderTitle.tsx","../src/ChatbotHeader/index.ts","../src/ChatbotModal/ChatbotModal.tsx","../src/ChatbotModal/index.ts","../src/ChatbotPopover/ChatbotPopover.tsx","../src/ChatbotPopover/index.ts","../src/ChatbotToggle/ChatbotToggle.test.tsx","../src/ChatbotToggle/ChatbotToggle.tsx","../src/ChatbotToggle/index.ts","../src/ChatbotWelcomePrompt/ChatbotWelcomePrompt.test.tsx","../src/ChatbotWelcomePrompt/ChatbotWelcomePrompt.tsx","../src/ChatbotWelcomePrompt/index.ts","../src/CodeModal/CodeModal.tsx","../src/CodeModal/index.ts","../src/FileDetails/FileDetails.test.tsx","../src/FileDetails/FileDetails.tsx","../src/FileDetails/index.ts","../src/FileDetailsLabel/FileDetailsLabel.test.tsx","../src/FileDetailsLabel/FileDetailsLabel.tsx","../src/FileDetailsLabel/index.ts","../src/FileDropZone/FileDropZone.test.tsx","../src/FileDropZone/FileDropZone.tsx","../src/FileDropZone/index.ts","../src/LoadingMessage/LoadingMessage.test.tsx","../src/LoadingMessage/LoadingMessage.tsx","../src/LoadingMessage/index.ts","../src/Message/Message.test.tsx","../src/Message/Message.tsx","../src/Message/MessageLoading.tsx","../src/Message/index.ts","../src/Message/CodeBlockMessage/CodeBlockMessage.tsx","../src/Message/ListMessage/ListItemMessage.tsx","../src/Message/ListMessage/OrderedListMessage.tsx","../src/Message/ListMessage/UnorderedListMessage.tsx","../src/Message/TextMessage/TextMessage.tsx","../src/MessageBar/AttachButton.test.tsx","../src/MessageBar/AttachButton.tsx","../src/MessageBar/MessageBar.test.tsx","../src/MessageBar/MessageBar.tsx","../src/MessageBar/MicrophoneButton.tsx","../src/MessageBar/SendButton.test.tsx","../src/MessageBar/SendButton.tsx","../src/MessageBar/StopButton.test.tsx","../src/MessageBar/StopButton.tsx","../src/MessageBar/index.ts","../src/MessageBox/JumpButton.test.tsx","../src/MessageBox/JumpButton.tsx","../src/MessageBox/MessageBox.tsx","../src/MessageBox/index.ts","../src/PreviewAttachment/PreviewAttachment.tsx","../src/PreviewAttachment/index.ts","../src/ResponseActions/ResponseActionButton.test.tsx","../src/ResponseActions/ResponseActionButton.tsx","../src/ResponseActions/ResponseActions.test.tsx","../src/ResponseActions/ResponseActions.tsx","../src/ResponseActions/index.ts","../src/SourceDetailsMenuItem/SourceDetailsMenuItem.tsx","../src/SourceDetailsMenuItem/index.ts","../src/SourcesCard/SourcesCard.test.tsx","../src/SourcesCard/SourcesCard.tsx","../src/SourcesCard/index.ts","../src/TermsOfUse/TermsOfUse.test.tsx","../src/TermsOfUse/TermsOfUse.tsx","../src/TermsOfUse/index.ts"],"version":"5.6.3"}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@patternfly/chatbot",
|
3
|
-
"version": "2.2.0-prerelease.
|
3
|
+
"version": "2.2.0-prerelease.5",
|
4
4
|
"description": "This library provides React components based on PatternFly 6 that can be used to build chatbots.",
|
5
5
|
"main": "dist/cjs/index.js",
|
6
6
|
"module": "dist/esm/index.js",
|
@@ -15,16 +15,20 @@ export const CustomActionExample: React.FunctionComponent = () => (
|
|
15
15
|
actions={{
|
16
16
|
regenerate: {
|
17
17
|
ariaLabel: 'Regenerate',
|
18
|
+
clickedAriaLabel: 'Regenerated',
|
18
19
|
// eslint-disable-next-line no-console
|
19
20
|
onClick: () => console.log('Clicked regenerate'),
|
20
21
|
tooltipContent: 'Regenerate',
|
22
|
+
clickedTooltipContent: 'Regenerated',
|
21
23
|
icon: <RedoIcon />
|
22
24
|
},
|
23
25
|
download: {
|
24
26
|
ariaLabel: 'Download',
|
27
|
+
clickedAriaLabel: 'Downloaded',
|
25
28
|
// eslint-disable-next-line no-console
|
26
29
|
onClick: () => console.log('Clicked download'),
|
27
30
|
tooltipContent: 'Download',
|
31
|
+
clickedTooltipContent: 'Downloaded',
|
28
32
|
icon: <DownloadIcon />
|
29
33
|
},
|
30
34
|
info: {
|
@@ -63,7 +63,7 @@ You can further customize the avatar by applying an additional class or passing
|
|
63
63
|
|
64
64
|
```
|
65
65
|
|
66
|
-
###
|
66
|
+
### Message actions
|
67
67
|
|
68
68
|
You can add actions to a message, to allow users to interact with the message content. These actions can include:
|
69
69
|
|
@@ -79,7 +79,18 @@ You can add actions to a message, to allow users to interact with the message co
|
|
79
79
|
|
80
80
|
### Custom message actions
|
81
81
|
|
82
|
-
Beyond the standard message actions (
|
82
|
+
Beyond the standard message actions (good response, bad response, copy, share, or listen), you can add custom actions to a bot message by passing an `actions` object to the `<Message>` component. This object can contain the following customizations:
|
83
|
+
|
84
|
+
- `ariaLabel`
|
85
|
+
- `onClick`
|
86
|
+
- `className`
|
87
|
+
- `isDisabled`
|
88
|
+
- `tooltipContent`
|
89
|
+
- `tooltipContent`
|
90
|
+
- `tooltipProps`
|
91
|
+
- `icon`
|
92
|
+
|
93
|
+
You can apply a `clickedAriaLabel` and `clickedTooltipContent` once a button is clicked. If either of these props are omitted, their values will default to the `ariaLabel` or `tooltipContent` supplied.
|
83
94
|
|
84
95
|
```js file="./MessageWithCustomResponseActions.tsx"
|
85
96
|
|
@@ -66,7 +66,7 @@ This demo displays a basic ChatBot, which includes:
|
|
66
66
|
4. [`<ChatbotContent>` and `<MessageBox>`](/patternfly-ai/chatbot/ui#content-and-message-box) with:
|
67
67
|
|
68
68
|
- A `<ChatbotWelcomePrompt>`
|
69
|
-
- An initial [user `<Message>`](/patternfly-ai/chatbot/messages#user-messages) and an initial bot message with [message actions.](/patternfly-ai/chatbot/messages#
|
69
|
+
- An initial [user `<Message>`](/patternfly-ai/chatbot/messages#user-messages) and an initial bot message with [message actions.](/patternfly-ai/chatbot/messages#message-actions)
|
70
70
|
- Logic for enabling auto-scrolling to the most recent message whenever a new message is sent or received using a `scrollToBottomRef`
|
71
71
|
|
72
72
|
5. A [`<ChatbotFooter>`](/patternfly-ai/chatbot/ui#footer) with a [`<ChatbotFootNote>`](/patternfly-ai/chatbot/ui#footnote-with-popover) and a `<MessageBar>` that contains the abilities of:
|
@@ -92,7 +92,7 @@ This demo displays an embedded ChatBot. Embedded ChatBots are meant to be placed
|
|
92
92
|
3. A [`<ChatbotHeader>`](/patternfly-ai/chatbot/ui#header) with all built sub-components laid out, including a `<ChatbotHeaderTitle>`
|
93
93
|
4. [`<ChatbotContent>` and `<MessageBox>`](/patternfly-ai/chatbot/ui#content-and-message-box) with:
|
94
94
|
- A `<ChatbotWelcomePrompt>`
|
95
|
-
- An initial [user `<Message>`](/patternfly-ai/chatbot/messages#user-messages) and an initial bot message with [message actions.](/patternfly-ai/chatbot/messages/#
|
95
|
+
- An initial [user `<Message>`](/patternfly-ai/chatbot/messages#user-messages) and an initial bot message with [message actions.](/patternfly-ai/chatbot/messages/#message-actions)
|
96
96
|
- Logic for enabling auto-scrolling to the most recent message whenever a new message is sent or received using a `scrollToBottomRef`
|
97
97
|
5. A [`<ChatbotFooter>`](/patternfly-ai/chatbot/ui#footer) with a [`<ChatbotFootNote>`](/patternfly-ai/chatbot/ui#footnote-with-popover) and a `<MessageBar>` that contains the abilities of:
|
98
98
|
- [Speech to text.](/patternfly-ai/chatbot/ui#message-bar-with-speech-recognition-and-file-attachment)
|
@@ -0,0 +1,52 @@
|
|
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 { DownloadIcon } from '@patternfly/react-icons';
|
6
|
+
import ResponseActionButton from './ResponseActionButton';
|
7
|
+
|
8
|
+
describe('ResponseActionButton', () => {
|
9
|
+
it('renders aria-label correctly if not clicked', () => {
|
10
|
+
render(<ResponseActionButton icon={<DownloadIcon />} ariaLabel="Download" clickedAriaLabel="Downloaded" />);
|
11
|
+
expect(screen.getByRole('button', { name: 'Download' })).toBeTruthy();
|
12
|
+
});
|
13
|
+
it('renders aria-label correctly if clicked', () => {
|
14
|
+
render(
|
15
|
+
<ResponseActionButton icon={<DownloadIcon />} ariaLabel="Download" clickedAriaLabel="Downloaded" isClicked />
|
16
|
+
);
|
17
|
+
expect(screen.getByRole('button', { name: 'Downloaded' })).toBeTruthy();
|
18
|
+
});
|
19
|
+
it('renders tooltip correctly if not clicked', async () => {
|
20
|
+
render(
|
21
|
+
<ResponseActionButton icon={<DownloadIcon />} tooltipContent="Download" clickedTooltipContent="Downloaded" />
|
22
|
+
);
|
23
|
+
expect(screen.getByRole('button', { name: 'Download' })).toBeTruthy();
|
24
|
+
// clicking here just triggers the tooltip; in this button, the logic is divorced from whether it is actually clicked
|
25
|
+
await userEvent.click(screen.getByRole('button', { name: 'Download' }));
|
26
|
+
expect(screen.getByRole('tooltip', { name: 'Download' })).toBeTruthy();
|
27
|
+
});
|
28
|
+
it('renders tooltip correctly if clicked', async () => {
|
29
|
+
render(
|
30
|
+
<ResponseActionButton
|
31
|
+
icon={<DownloadIcon />}
|
32
|
+
tooltipContent="Download"
|
33
|
+
clickedTooltipContent="Downloaded"
|
34
|
+
isClicked
|
35
|
+
/>
|
36
|
+
);
|
37
|
+
expect(screen.getByRole('button', { name: 'Downloaded' })).toBeTruthy();
|
38
|
+
// clicking here just triggers the tooltip; in this button, the logic is divorced from whether it is actually clicked
|
39
|
+
await userEvent.click(screen.getByRole('button', { name: 'Downloaded' }));
|
40
|
+
expect(screen.getByRole('tooltip', { name: 'Downloaded' })).toBeTruthy();
|
41
|
+
});
|
42
|
+
it('if clicked variant for tooltip is not supplied, it uses the default', async () => {
|
43
|
+
render(<ResponseActionButton icon={<DownloadIcon />} tooltipContent="Download" isClicked />);
|
44
|
+
// clicking here just triggers the tooltip; in this button, the logic is divorced from whether it is actually clicked
|
45
|
+
await userEvent.click(screen.getByRole('button', { name: 'Download' }));
|
46
|
+
expect(screen.getByRole('button', { name: 'Download' })).toBeTruthy();
|
47
|
+
});
|
48
|
+
it('if clicked variant for aria label is not supplied, it uses the default', async () => {
|
49
|
+
render(<ResponseActionButton icon={<DownloadIcon />} ariaLabel="Download" isClicked />);
|
50
|
+
expect(screen.getByRole('button', { name: 'Download' })).toBeTruthy();
|
51
|
+
});
|
52
|
+
});
|
@@ -4,6 +4,8 @@ import { Button, Icon, Tooltip, TooltipProps } from '@patternfly/react-core';
|
|
4
4
|
export interface ResponseActionButtonProps {
|
5
5
|
/** Aria-label for the button. Defaults to the value of the tooltipContent if none provided */
|
6
6
|
ariaLabel?: string;
|
7
|
+
/** Aria-label for the button, shown when the button is clicked. Defaults to the value of ariaLabel or tooltipContent if not provided. */
|
8
|
+
clickedAriaLabel?: string;
|
7
9
|
/** Icon for the button */
|
8
10
|
icon: React.ReactNode;
|
9
11
|
/** On-click handler for the button */
|
@@ -14,43 +16,60 @@ export interface ResponseActionButtonProps {
|
|
14
16
|
isDisabled?: boolean;
|
15
17
|
/** Content shown in the tooltip */
|
16
18
|
tooltipContent?: string;
|
19
|
+
/** Content shown in the tooltip when the button is clicked. Defaults to the value of tooltipContent if not provided. */
|
20
|
+
clickedTooltipContent?: string;
|
17
21
|
/** Props to control the PF Tooltip component */
|
18
22
|
tooltipProps?: TooltipProps;
|
23
|
+
/** Whether button is in clicked state */
|
24
|
+
isClicked?: boolean;
|
19
25
|
}
|
20
26
|
|
21
27
|
export const ResponseActionButton: React.FunctionComponent<ResponseActionButtonProps> = ({
|
22
28
|
ariaLabel,
|
29
|
+
clickedAriaLabel = ariaLabel,
|
23
30
|
className,
|
24
31
|
icon,
|
25
32
|
isDisabled,
|
26
33
|
onClick,
|
27
34
|
tooltipContent,
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
<
|
41
|
-
|
42
|
-
|
43
|
-
aria-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
}
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
)
|
35
|
+
clickedTooltipContent = tooltipContent,
|
36
|
+
tooltipProps,
|
37
|
+
isClicked = false
|
38
|
+
}) => {
|
39
|
+
const generateAriaLabel = () => {
|
40
|
+
if (ariaLabel) {
|
41
|
+
return isClicked ? clickedAriaLabel : ariaLabel;
|
42
|
+
}
|
43
|
+
return isClicked ? clickedTooltipContent : tooltipContent;
|
44
|
+
};
|
45
|
+
|
46
|
+
return (
|
47
|
+
<Tooltip
|
48
|
+
id={`pf-chatbot__tooltip-response-action-${tooltipContent}`}
|
49
|
+
content={isClicked ? clickedTooltipContent : tooltipContent}
|
50
|
+
aria-live="polite"
|
51
|
+
position="bottom"
|
52
|
+
entryDelay={tooltipProps?.entryDelay || 0}
|
53
|
+
exitDelay={tooltipProps?.exitDelay || 0}
|
54
|
+
distance={tooltipProps?.distance || 8}
|
55
|
+
animationDuration={tooltipProps?.animationDuration || 0}
|
56
|
+
{...tooltipProps}
|
57
|
+
>
|
58
|
+
<Button
|
59
|
+
variant="plain"
|
60
|
+
className={`pf-chatbot__button--response-action ${isClicked ? 'pf-chatbot__button--response-action-clicked' : ''} ${className ?? ''}`}
|
61
|
+
aria-label={generateAriaLabel()}
|
62
|
+
icon={
|
63
|
+
<Icon isInline size="lg">
|
64
|
+
{icon}
|
65
|
+
</Icon>
|
66
|
+
}
|
67
|
+
isDisabled={isDisabled}
|
68
|
+
onClick={onClick}
|
69
|
+
size="sm"
|
70
|
+
></Button>
|
71
|
+
</Tooltip>
|
72
|
+
);
|
73
|
+
};
|
55
74
|
|
56
75
|
export default ResponseActionButton;
|
@@ -4,6 +4,7 @@
|
|
4
4
|
grid-template-columns: repeat(auto-fit, minmax(0, max-content));
|
5
5
|
|
6
6
|
.pf-v6-c-button {
|
7
|
+
--pf-v6-c-button__icon--Color: var(--pf-t--global--icon--color--subtle);
|
7
8
|
border-radius: var(--pf-t--global--border--radius--pill);
|
8
9
|
width: 2.3125rem;
|
9
10
|
height: 2.3125rem;
|
@@ -11,16 +12,17 @@
|
|
11
12
|
align-items: center;
|
12
13
|
justify-content: center;
|
13
14
|
|
14
|
-
|
15
|
-
|
15
|
+
&:hover {
|
16
|
+
--pf-v6-c-button__icon--Color: var(--pf-t--global--icon--color--subtle);
|
16
17
|
}
|
17
|
-
|
18
|
-
// Interactive states
|
19
|
-
&:hover,
|
20
18
|
&:focus {
|
21
|
-
|
22
|
-
|
23
|
-
}
|
19
|
+
--pf-v6-c-button--hover--BackgroundColor: var(--pf-t--global--background--color--action--plain--alt--clicked);
|
20
|
+
--pf-v6-c-button__icon--Color: var(--pf-t--global--icon--color--regular);
|
24
21
|
}
|
25
22
|
}
|
26
23
|
}
|
24
|
+
|
25
|
+
.pf-v6-c-button.pf-chatbot__button--response-action-clicked {
|
26
|
+
--pf-v6-c-button--m-plain--BackgroundColor: var(--pf-t--global--background--color--action--plain--alt--clicked);
|
27
|
+
--pf-v6-c-button__icon--Color: var(--pf-t--global--icon--color--regular);
|
28
|
+
}
|
@@ -4,27 +4,32 @@ import '@testing-library/jest-dom';
|
|
4
4
|
import ResponseActions from './ResponseActions';
|
5
5
|
import userEvent from '@testing-library/user-event';
|
6
6
|
import { DownloadIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons';
|
7
|
+
import Message from '../Message';
|
7
8
|
|
8
9
|
const ALL_ACTIONS = [
|
9
|
-
{ type: 'positive', label: 'Good response' },
|
10
|
-
{ type: 'negative', label: 'Bad response' },
|
11
|
-
{ type: 'copy', label: 'Copy' },
|
12
|
-
{ type: 'share', label: 'Share' },
|
13
|
-
{ type: 'listen', label: 'Listen' }
|
10
|
+
{ type: 'positive', label: 'Good response', clickedLabel: 'Response recorded' },
|
11
|
+
{ type: 'negative', label: 'Bad response', clickedLabel: 'Response recorded' },
|
12
|
+
{ type: 'copy', label: 'Copy', clickedLabel: 'Copied' },
|
13
|
+
{ type: 'share', label: 'Share', clickedLabel: 'Shared' },
|
14
|
+
{ type: 'listen', label: 'Listen', clickedLabel: 'Listening' }
|
14
15
|
];
|
15
16
|
|
16
17
|
const CUSTOM_ACTIONS = [
|
17
18
|
{
|
18
19
|
regenerate: {
|
19
20
|
ariaLabel: 'Regenerate',
|
21
|
+
clickedAriaLabel: 'Regenerated',
|
20
22
|
onClick: jest.fn(),
|
21
23
|
tooltipContent: 'Regenerate',
|
24
|
+
clickedTooltipContent: 'Regenerated',
|
22
25
|
icon: <RedoIcon />
|
23
26
|
},
|
24
27
|
download: {
|
25
28
|
ariaLabel: 'Download',
|
29
|
+
clickedAriaLabel: 'Downloaded',
|
26
30
|
onClick: jest.fn(),
|
27
31
|
tooltipContent: 'Download',
|
32
|
+
clickedTooltipContent: 'Downloaded',
|
28
33
|
icon: <DownloadIcon />
|
29
34
|
},
|
30
35
|
info: {
|
@@ -37,6 +42,81 @@ const CUSTOM_ACTIONS = [
|
|
37
42
|
];
|
38
43
|
|
39
44
|
describe('ResponseActions', () => {
|
45
|
+
afterEach(() => {
|
46
|
+
jest.clearAllMocks();
|
47
|
+
});
|
48
|
+
it('should handle click within group of buttons correctly', async () => {
|
49
|
+
render(
|
50
|
+
<ResponseActions
|
51
|
+
actions={{
|
52
|
+
positive: { onClick: jest.fn() },
|
53
|
+
negative: { onClick: jest.fn() },
|
54
|
+
copy: { onClick: jest.fn() },
|
55
|
+
share: { onClick: jest.fn() },
|
56
|
+
listen: { onClick: jest.fn() }
|
57
|
+
}}
|
58
|
+
/>
|
59
|
+
);
|
60
|
+
const goodBtn = screen.getByRole('button', { name: 'Good response' });
|
61
|
+
const badBtn = screen.getByRole('button', { name: 'Bad response' });
|
62
|
+
const copyBtn = screen.getByRole('button', { name: 'Copy' });
|
63
|
+
const shareBtn = screen.getByRole('button', { name: 'Share' });
|
64
|
+
const listenBtn = screen.getByRole('button', { name: 'Listen' });
|
65
|
+
const buttons = [goodBtn, badBtn, copyBtn, shareBtn, listenBtn];
|
66
|
+
buttons.forEach((button) => {
|
67
|
+
expect(button).toBeTruthy();
|
68
|
+
});
|
69
|
+
await userEvent.click(goodBtn);
|
70
|
+
expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass(
|
71
|
+
'pf-chatbot__button--response-action-clicked'
|
72
|
+
);
|
73
|
+
let unclickedButtons = buttons.filter((button) => button !== goodBtn);
|
74
|
+
unclickedButtons.forEach((button) => {
|
75
|
+
expect(button).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
76
|
+
});
|
77
|
+
await userEvent.click(badBtn);
|
78
|
+
expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass(
|
79
|
+
'pf-chatbot__button--response-action-clicked'
|
80
|
+
);
|
81
|
+
unclickedButtons = buttons.filter((button) => button !== badBtn);
|
82
|
+
unclickedButtons.forEach((button) => {
|
83
|
+
expect(button).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
84
|
+
});
|
85
|
+
});
|
86
|
+
it('should handle click outside of group of buttons correctly', async () => {
|
87
|
+
// using message just so we have something outside the group that's rendered
|
88
|
+
render(
|
89
|
+
<Message
|
90
|
+
name="Bot"
|
91
|
+
role="bot"
|
92
|
+
avatar=""
|
93
|
+
content="Example with all prebuilt actions"
|
94
|
+
actions={{
|
95
|
+
positive: {},
|
96
|
+
negative: {}
|
97
|
+
}}
|
98
|
+
/>
|
99
|
+
);
|
100
|
+
const goodBtn = screen.getByRole('button', { name: 'Good response' });
|
101
|
+
const badBtn = screen.getByRole('button', { name: 'Bad response' });
|
102
|
+
expect(goodBtn).toBeTruthy();
|
103
|
+
expect(badBtn).toBeTruthy();
|
104
|
+
|
105
|
+
await userEvent.click(goodBtn);
|
106
|
+
expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass(
|
107
|
+
'pf-chatbot__button--response-action-clicked'
|
108
|
+
);
|
109
|
+
expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
110
|
+
|
111
|
+
await userEvent.click(badBtn);
|
112
|
+
expect(screen.getByRole('button', { name: 'Response recorded' })).toHaveClass(
|
113
|
+
'pf-chatbot__button--response-action-clicked'
|
114
|
+
);
|
115
|
+
expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
116
|
+
await userEvent.click(screen.getByText('Example with all prebuilt actions'));
|
117
|
+
expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
118
|
+
expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
|
119
|
+
});
|
40
120
|
it('should render buttons correctly', () => {
|
41
121
|
ALL_ACTIONS.forEach(({ type, label }) => {
|
42
122
|
render(<ResponseActions actions={{ [type]: { onClick: jest.fn() } }} />);
|
@@ -53,6 +133,24 @@ describe('ResponseActions', () => {
|
|
53
133
|
});
|
54
134
|
});
|
55
135
|
|
136
|
+
it('should swap clicked and non-clicked aria labels on click', async () => {
|
137
|
+
ALL_ACTIONS.forEach(async ({ type, label, clickedLabel }) => {
|
138
|
+
render(<ResponseActions actions={{ [type]: { onClick: jest.fn() } }} />);
|
139
|
+
expect(screen.getByRole('button', { name: label })).toBeTruthy();
|
140
|
+
await userEvent.click(screen.getByRole('button', { name: label }));
|
141
|
+
expect(screen.getByRole('button', { name: clickedLabel })).toBeTruthy();
|
142
|
+
});
|
143
|
+
});
|
144
|
+
|
145
|
+
it('should swap clicked and non-clicked tooltips on click', async () => {
|
146
|
+
ALL_ACTIONS.forEach(async ({ type, label, clickedLabel }) => {
|
147
|
+
render(<ResponseActions actions={{ [type]: { onClick: jest.fn() } }} />);
|
148
|
+
expect(screen.getByRole('button', { name: label })).toBeTruthy();
|
149
|
+
await userEvent.click(screen.getByRole('button', { name: label }));
|
150
|
+
expect(screen.getByRole('tooltip', { name: clickedLabel })).toBeTruthy();
|
151
|
+
});
|
152
|
+
});
|
153
|
+
|
56
154
|
it('should be able to change aria labels', () => {
|
57
155
|
const actions = [
|
58
156
|
{ type: 'positive', ariaLabel: 'Thumbs up' },
|