@patternfly/chatbot 2.2.0-prerelease.11 → 2.2.0-prerelease.13

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 (93) hide show
  1. package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.js +3 -1
  2. package/dist/cjs/ChatbotHeader/ChatbotHeaderCloseButton.js +3 -1
  3. package/dist/cjs/ChatbotHeader/ChatbotHeaderMenu.js +3 -1
  4. package/dist/cjs/ChatbotHeader/ChatbotHeaderOptionsDropdown.js +3 -1
  5. package/dist/cjs/ChatbotHeader/ChatbotHeaderSelectorDropdown.js +3 -1
  6. package/dist/cjs/ChatbotToggle/ChatbotToggle.js +3 -1
  7. package/dist/cjs/Message/Message.d.ts +12 -1
  8. package/dist/cjs/Message/Message.js +11 -6
  9. package/dist/cjs/Message/QuickResponse/QuickResponse.d.ts +3 -1
  10. package/dist/cjs/Message/QuickResponse/QuickResponse.js +2 -1
  11. package/dist/cjs/Message/UserFeedback/CloseButton.d.ts +10 -0
  12. package/dist/cjs/Message/UserFeedback/CloseButton.js +14 -0
  13. package/dist/cjs/Message/UserFeedback/UserFeedback.d.ts +39 -0
  14. package/dist/cjs/Message/UserFeedback/UserFeedback.js +55 -0
  15. package/dist/cjs/Message/UserFeedback/UserFeedback.test.d.ts +1 -0
  16. package/dist/cjs/Message/UserFeedback/UserFeedback.test.js +146 -0
  17. package/dist/cjs/Message/UserFeedback/UserFeedbackComplete.d.ts +42 -0
  18. package/dist/cjs/Message/UserFeedback/UserFeedbackComplete.js +117 -0
  19. package/dist/cjs/Message/UserFeedback/UserFeedbackComplete.test.d.ts +1 -0
  20. package/dist/cjs/Message/UserFeedback/UserFeedbackComplete.test.js +249 -0
  21. package/dist/cjs/MessageBar/AttachButton.js +3 -1
  22. package/dist/cjs/MessageBar/SendButton.js +3 -1
  23. package/dist/cjs/MessageBar/StopButton.js +3 -1
  24. package/dist/cjs/ResponseActions/ResponseActionButton.d.ts +4 -1
  25. package/dist/cjs/ResponseActions/ResponseActionButton.js +21 -6
  26. package/dist/cjs/ResponseActions/ResponseActions.d.ts +8 -2
  27. package/dist/cjs/ResponseActions/ResponseActions.js +7 -7
  28. package/dist/css/main.css +69 -11
  29. package/dist/css/main.css.map +1 -1
  30. package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.js +3 -1
  31. package/dist/esm/ChatbotHeader/ChatbotHeaderCloseButton.js +3 -1
  32. package/dist/esm/ChatbotHeader/ChatbotHeaderMenu.js +3 -1
  33. package/dist/esm/ChatbotHeader/ChatbotHeaderOptionsDropdown.js +3 -1
  34. package/dist/esm/ChatbotHeader/ChatbotHeaderSelectorDropdown.js +3 -1
  35. package/dist/esm/ChatbotToggle/ChatbotToggle.js +3 -1
  36. package/dist/esm/Message/Message.d.ts +12 -1
  37. package/dist/esm/Message/Message.js +8 -3
  38. package/dist/esm/Message/QuickResponse/QuickResponse.d.ts +3 -1
  39. package/dist/esm/Message/QuickResponse/QuickResponse.js +2 -1
  40. package/dist/esm/Message/UserFeedback/CloseButton.d.ts +10 -0
  41. package/dist/esm/Message/UserFeedback/CloseButton.js +9 -0
  42. package/dist/esm/Message/UserFeedback/UserFeedback.d.ts +39 -0
  43. package/dist/esm/Message/UserFeedback/UserFeedback.js +50 -0
  44. package/dist/esm/Message/UserFeedback/UserFeedback.test.d.ts +1 -0
  45. package/dist/esm/Message/UserFeedback/UserFeedback.test.js +141 -0
  46. package/dist/esm/Message/UserFeedback/UserFeedbackComplete.d.ts +42 -0
  47. package/dist/esm/Message/UserFeedback/UserFeedbackComplete.js +112 -0
  48. package/dist/esm/Message/UserFeedback/UserFeedbackComplete.test.d.ts +1 -0
  49. package/dist/esm/Message/UserFeedback/UserFeedbackComplete.test.js +244 -0
  50. package/dist/esm/MessageBar/AttachButton.js +3 -1
  51. package/dist/esm/MessageBar/SendButton.js +3 -1
  52. package/dist/esm/MessageBar/StopButton.js +3 -1
  53. package/dist/esm/ResponseActions/ResponseActionButton.d.ts +4 -1
  54. package/dist/esm/ResponseActions/ResponseActionButton.js +18 -3
  55. package/dist/esm/ResponseActions/ResponseActions.d.ts +8 -2
  56. package/dist/esm/ResponseActions/ResponseActions.js +7 -7
  57. package/dist/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +1 -1
  59. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithFeedback.tsx +71 -0
  60. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithFeedbackTimeout.tsx +27 -0
  61. package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +37 -7
  62. package/patternfly-docs/content/extensions/chatbot/examples/UI/UI.md +3 -6
  63. package/patternfly-docs/content/extensions/chatbot/examples/demos/AttachmentDemos.md +14 -0
  64. package/patternfly-docs/content/extensions/chatbot/examples/demos/Feedback.tsx +104 -0
  65. package/src/AttachMenu/AttachMenu.scss +1 -1
  66. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryDropdown.tsx +7 -1
  67. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss +8 -1
  68. package/src/ChatbotHeader/ChatbotHeaderCloseButton.tsx +7 -1
  69. package/src/ChatbotHeader/ChatbotHeaderMenu.tsx +7 -1
  70. package/src/ChatbotHeader/ChatbotHeaderOptionsDropdown.tsx +8 -1
  71. package/src/ChatbotHeader/ChatbotHeaderSelectorDropdown.tsx +8 -1
  72. package/src/ChatbotModal/ChatbotModal.scss +1 -1
  73. package/src/ChatbotToggle/ChatbotToggle.tsx +6 -1
  74. package/src/CodeModal/CodeModal.scss +1 -1
  75. package/src/FileDetails/FileDetails.scss +1 -1
  76. package/src/Message/CodeBlockMessage/CodeBlockMessage.scss +1 -1
  77. package/src/Message/Message.scss +1 -1
  78. package/src/Message/Message.tsx +24 -2
  79. package/src/Message/QuickResponse/QuickResponse.tsx +6 -2
  80. package/src/Message/UserFeedback/CloseButton.tsx +21 -0
  81. package/src/Message/UserFeedback/UserFeedback.scss +53 -0
  82. package/src/Message/UserFeedback/UserFeedback.test.tsx +257 -0
  83. package/src/Message/UserFeedback/UserFeedback.tsx +132 -0
  84. package/src/Message/UserFeedback/UserFeedbackComplete.test.tsx +255 -0
  85. package/src/Message/UserFeedback/UserFeedbackComplete.tsx +211 -0
  86. package/src/MessageBar/AttachButton.tsx +2 -0
  87. package/src/MessageBar/SendButton.tsx +2 -0
  88. package/src/MessageBar/StopButton.tsx +2 -0
  89. package/src/ResponseActions/ResponseActionButton.tsx +14 -2
  90. package/src/ResponseActions/ResponseActions.tsx +26 -2
  91. package/src/Settings/Settings.scss +2 -2
  92. package/src/SourceDetailsMenuItem/SourceDetailsMenuItem.scss +1 -1
  93. package/src/main.scss +1 -0
@@ -65,7 +65,7 @@
65
65
  .pf-v6-c-label {
66
66
  --pf-v6-c-label--m-outline--BorderColor: var(--pf-t--chatbot-message--meta--label--color);
67
67
  --pf-v6-c-label--FontSize: var(--pf-t--global--font--size--xs);
68
- font-weight: 500;
68
+ font-weight: var(--pf-t--global--font--weight--body--bold);
69
69
 
70
70
  .pf-v6-c-label__content {
71
71
  --pf-v6-c-label--Color: var(--pf-t--chatbot-message--meta--label--color);
@@ -19,6 +19,8 @@ import OrderedListMessage from './ListMessage/OrderedListMessage';
19
19
  import QuickStartTile from './QuickStarts/QuickStartTile';
20
20
  import { QuickStart, QuickstartAction } from './QuickStarts/types';
21
21
  import QuickResponse from './QuickResponse/QuickResponse';
22
+ import UserFeedback, { UserFeedbackProps } from './UserFeedback/UserFeedback';
23
+ import UserFeedbackComplete, { UserFeedbackCompleteProps } from './UserFeedback/UserFeedbackComplete';
22
24
 
23
25
  export interface MessageAttachment {
24
26
  /** Name of file attached to the message */
@@ -74,6 +76,10 @@ export interface MessageProps extends Omit<React.HTMLProps<HTMLDivElement>, 'rol
74
76
  quickResponses?: QuickResponse[];
75
77
  /** Props for quick responses container */
76
78
  quickResponseContainerProps?: Omit<LabelGroupProps, 'ref'>;
79
+ /** Props for user feedback card */
80
+ userFeedbackForm?: Omit<UserFeedbackProps, 'ref'>;
81
+ /** Props for user feedback response */
82
+ userFeedbackComplete?: Omit<UserFeedbackCompleteProps, 'ref'>;
77
83
  /** Whether avatar is round */
78
84
  hasRoundAvatar?: boolean;
79
85
  /** Any additional props applied to the avatar, for additional customization */
@@ -91,9 +97,13 @@ export interface MessageProps extends Omit<React.HTMLProps<HTMLDivElement>, 'rol
91
97
  onClick?: () => void;
92
98
  action?: QuickstartAction;
93
99
  };
100
+ /** Turns the container into a live region so that changes to content within the Message, such as appending a feedback card, are reliably announced to assistive technology. */
101
+ isLiveRegion?: boolean;
102
+ /** Ref applied to message */
103
+ innerRef?: React.Ref<HTMLDivElement>;
94
104
  }
95
105
 
96
- export const Message: React.FunctionComponent<MessageProps> = ({
106
+ export const MessageBase: React.FunctionComponent<MessageProps> = ({
97
107
  role,
98
108
  content,
99
109
  name,
@@ -111,6 +121,10 @@ export const Message: React.FunctionComponent<MessageProps> = ({
111
121
  hasRoundAvatar = true,
112
122
  avatarProps,
113
123
  quickStarts,
124
+ userFeedbackForm,
125
+ userFeedbackComplete,
126
+ isLiveRegion = true,
127
+ innerRef,
114
128
  ...props
115
129
  }: MessageProps) => {
116
130
  let avatarClassName;
@@ -122,11 +136,13 @@ export const Message: React.FunctionComponent<MessageProps> = ({
122
136
  // Keep timestamps consistent between Timestamp component and aria-label
123
137
  const date = new Date();
124
138
  const dateString = timestamp ?? `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
125
-
126
139
  return (
127
140
  <section
128
141
  aria-label={`Message from ${role} - ${dateString}`}
129
142
  className={`pf-chatbot__message pf-chatbot__message--${role}`}
143
+ aria-live={isLiveRegion ? 'polite' : undefined}
144
+ aria-atomic={isLiveRegion ? false : undefined}
145
+ ref={innerRef}
130
146
  {...props}
131
147
  >
132
148
  {/* We are using an empty alt tag intentionally in order to reduce noise on screen readers */}
@@ -181,6 +197,8 @@ export const Message: React.FunctionComponent<MessageProps> = ({
181
197
  />
182
198
  )}
183
199
  {!isLoading && actions && <ResponseActions actions={actions} />}
200
+ {userFeedbackForm && <UserFeedback {...userFeedbackForm} timestamp={dateString} />}
201
+ {userFeedbackComplete && <UserFeedbackComplete {...userFeedbackComplete} timestamp={dateString} />}
184
202
  {!isLoading && quickResponses && (
185
203
  <QuickResponse
186
204
  quickResponses={quickResponses}
@@ -212,4 +230,8 @@ export const Message: React.FunctionComponent<MessageProps> = ({
212
230
  );
213
231
  };
214
232
 
233
+ const Message = React.forwardRef((props: MessageProps, ref: React.Ref<HTMLDivElement>) => (
234
+ <MessageBase innerRef={ref} {...props} />
235
+ ));
236
+
215
237
  export default Message;
@@ -5,7 +5,7 @@ import { CheckIcon } from '@patternfly/react-icons';
5
5
  export interface QuickResponse extends Omit<LabelProps, 'children'> {
6
6
  content: string;
7
7
  id: string;
8
- onClick: () => void;
8
+ onClick?: () => void;
9
9
  }
10
10
 
11
11
  export interface QuickResponseProps {
@@ -13,17 +13,21 @@ export interface QuickResponseProps {
13
13
  quickResponses: QuickResponse[];
14
14
  /** Props for quick responses container */
15
15
  quickResponseContainerProps?: Omit<LabelGroupProps, 'ref'>;
16
+ /** Callback when a response is clicked; used in feedback cards */
17
+ onSelect?: (id: string) => void;
16
18
  }
17
19
 
18
20
  export const QuickResponse: React.FunctionComponent<QuickResponseProps> = ({
19
21
  quickResponses,
20
- quickResponseContainerProps = { numLabels: 5 }
22
+ quickResponseContainerProps = { numLabels: 5 },
23
+ onSelect
21
24
  }: QuickResponseProps) => {
22
25
  const [selectedQuickResponse, setSelectedQuickResponse] = React.useState<string>();
23
26
 
24
27
  const handleQuickResponseClick = (id: string, onClick?: () => void) => {
25
28
  setSelectedQuickResponse(id);
26
29
  onClick && onClick();
30
+ onSelect && onSelect(id);
27
31
  };
28
32
  return (
29
33
  <LabelGroup
@@ -0,0 +1,21 @@
1
+ // ============================================================================
2
+ // Chatbot Main - Messages - Close Button
3
+ // ============================================================================
4
+ import React from 'react';
5
+
6
+ // Import PatternFly components
7
+ import { Button, ButtonProps } from '@patternfly/react-core';
8
+ import { CloseIcon } from '@patternfly/react-icons';
9
+
10
+ export interface CloseButtonProps extends ButtonProps {
11
+ /** Callback function for when close button is clicked */
12
+ onClose?: () => void;
13
+ /** Aria-label for button */
14
+ ariaLabel?: string;
15
+ }
16
+
17
+ const CloseButton: React.FunctionComponent<CloseButtonProps> = ({ onClose, ariaLabel }: CloseButtonProps) => (
18
+ <Button variant="plain" onClick={onClose} icon={<CloseIcon />} aria-label={ariaLabel} />
19
+ );
20
+
21
+ export default CloseButton;
@@ -0,0 +1,53 @@
1
+ // shared
2
+ .pf-chatbot__feedback-card {
3
+ box-shadow: var(--pf-t--global--box-shadow--sm);
4
+ --pf-v6-c-card--BorderWidth: 0;
5
+ max-width: 27.5rem; // fixme address mobile vs desktop
6
+ }
7
+
8
+ // complete card
9
+ .pf-chatbot__feedback-complete-body {
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: var(--pf-t--global--spacer--lg);
13
+ align-items: center;
14
+ }
15
+ .pf-chatbot__feedback-complete-text {
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ flex-direction: column;
20
+ gap: var(--pf-t--global--spacer--sm);
21
+ --pf-v6-c-card--first-child--PaddingBlockStart: 0;
22
+ --pf-v6-c-card__title--not--last-child--PaddingBlockEnd: 0;
23
+ }
24
+ .pf-chatbot__feedback-complete-image {
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ }
29
+ .pf-chatbot__feedback-complete-body {
30
+ text-align: center;
31
+ }
32
+ .pf-chatbot__feedback-complete-title {
33
+ font-family: var(--pf-t--global--font--family--heading);
34
+ font-size: var(--pf-t--global--font--size--lg);
35
+ font-weight: var(--pf-t--global--font--weight--body--bold);
36
+ line-height: var(--pf-t--global--font--line-height--heading);
37
+ }
38
+
39
+ // feedback card
40
+ .pf-chatbot__feedback-card-title {
41
+ font-family: var(--pf-t--global--font--family--heading);
42
+ font-size: var(--pf-t--global--font--size--md);
43
+ font-weight: var(--pf-t--global--font--weight--body--bold);
44
+ line-height: var(--pf-t--global--font--line-height--heading);
45
+ }
46
+
47
+ .pf-chatbot__feedback-card-form {
48
+ --pf-v6-c-form__group--m-action--MarginBlockStart: 0;
49
+ }
50
+
51
+ .pf-chatbot__feedback-card-optional {
52
+ font-weight: initial;
53
+ }
@@ -0,0 +1,257 @@
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 UserFeedback from './UserFeedback';
6
+
7
+ const MOCK_RESPONSES = [
8
+ { id: '1', content: 'Helpful information', onClick: () => alert('Clicked helpful information') },
9
+ { id: '2', content: 'Easy to understand', onClick: () => alert('Clicked easy to understand') },
10
+ { id: '3', content: 'Resolved my issue', onClick: () => alert('Clicked resolved my issue') }
11
+ ];
12
+
13
+ describe('UserFeedback', () => {
14
+ it('should render correctly', () => {
15
+ render(<UserFeedback onClose={jest.fn} onSubmit={jest.fn} quickResponses={MOCK_RESPONSES} timestamp="12/12/12" />);
16
+ expect(screen.getByRole('heading', { name: /Why did you choose this rating?/i })).toBeTruthy();
17
+ expect(screen.getByRole('list', { name: 'Quick feedback for message received at 12/12/12' })).toBeTruthy();
18
+ expect(screen.getByRole('button', { name: /Helpful information/i })).toBeTruthy();
19
+ expect(screen.getByRole('button', { name: /Easy to understand/i })).toBeTruthy();
20
+ expect(screen.getByRole('button', { name: /Resolved my issue/i })).toBeTruthy();
21
+ expect(screen.getByRole('button', { name: /Submit/i })).toBeTruthy();
22
+ expect(screen.getByRole('button', { name: 'Close feedback for message received at 12/12/12' })).toBeTruthy();
23
+ expect(screen.getByRole('button', { name: /Cancel/i })).toBeTruthy();
24
+ expect(screen.queryByRole('textbox', { name: /Provide optional additional feedback/i })).toBeFalsy();
25
+ });
26
+ it('should render different title correctly', () => {
27
+ render(
28
+ <UserFeedback
29
+ timestamp="12/12/12"
30
+ onClose={jest.fn}
31
+ onSubmit={jest.fn}
32
+ quickResponses={MOCK_RESPONSES}
33
+ title="Thanks! Why?"
34
+ />
35
+ );
36
+ expect(screen.getByText('Thanks! Why?')).toBeTruthy();
37
+ });
38
+ it('should render different submit button text correctly', () => {
39
+ render(
40
+ <UserFeedback
41
+ timestamp="12/12/12"
42
+ onClose={jest.fn}
43
+ onSubmit={jest.fn}
44
+ quickResponses={MOCK_RESPONSES}
45
+ submitWord="Give feedback"
46
+ />
47
+ );
48
+ expect(screen.getByRole('button', { name: /Give feedback/i })).toBeTruthy();
49
+ });
50
+ it('should render text area correctly', () => {
51
+ render(
52
+ <UserFeedback
53
+ timestamp="12/12/12"
54
+ onClose={jest.fn}
55
+ onSubmit={jest.fn}
56
+ quickResponses={MOCK_RESPONSES}
57
+ hasTextArea
58
+ />
59
+ );
60
+ expect(screen.getByRole('textbox', { name: /Provide optional additional feedback/i })).toBeTruthy();
61
+ });
62
+ it('should call onTextAreaChange correctly', async () => {
63
+ const spy = jest.fn();
64
+ render(
65
+ <UserFeedback
66
+ timestamp="12/12/12"
67
+ onClose={jest.fn}
68
+ onSubmit={jest.fn}
69
+ quickResponses={MOCK_RESPONSES}
70
+ hasTextArea
71
+ onTextAreaChange={spy}
72
+ />
73
+ );
74
+ const textbox = screen.getByRole('textbox', { name: /Provide optional additional feedback/i });
75
+ await userEvent.type(textbox, 'test');
76
+ expect(spy).toHaveBeenCalledTimes(4);
77
+ });
78
+ it('should render different placeholder correctly', () => {
79
+ render(
80
+ <UserFeedback
81
+ timestamp="12/12/12"
82
+ onClose={jest.fn}
83
+ onSubmit={jest.fn}
84
+ quickResponses={MOCK_RESPONSES}
85
+ hasTextArea
86
+ textAreaPlaceholder="Provide any other information"
87
+ />
88
+ );
89
+ expect(screen.getByRole('textbox', { name: /Provide optional additional feedback/i })).toHaveAttribute(
90
+ 'placeholder',
91
+ 'Provide any other information'
92
+ );
93
+ });
94
+ it('should render different text area label correctly', () => {
95
+ render(
96
+ <UserFeedback
97
+ timestamp="12/12/12"
98
+ onClose={jest.fn}
99
+ onSubmit={jest.fn}
100
+ quickResponses={MOCK_RESPONSES}
101
+ hasTextArea
102
+ textAreaAriaLabel="Provide more details"
103
+ />
104
+ );
105
+ expect(screen.getByRole('textbox', { name: /Provide more details/i })).toBeTruthy();
106
+ });
107
+ it('should handle onClose correctly when close button is clicked', async () => {
108
+ const spy = jest.fn();
109
+ render(<UserFeedback onSubmit={jest.fn} quickResponses={MOCK_RESPONSES} onClose={spy} timestamp="12/12/12" />);
110
+ const closeButton = screen.getByRole('button', { name: 'Close feedback for message received at 12/12/12' });
111
+ expect(closeButton).toBeTruthy();
112
+ await userEvent.click(closeButton);
113
+ expect(spy).toHaveBeenCalledTimes(1);
114
+ });
115
+ it('should be able to change close button aria label', () => {
116
+ const spy = jest.fn();
117
+ render(
118
+ <UserFeedback
119
+ timestamp="12/12/12"
120
+ onSubmit={jest.fn}
121
+ quickResponses={MOCK_RESPONSES}
122
+ onClose={spy}
123
+ closeButtonAriaLabel="Ima button"
124
+ />
125
+ );
126
+ expect(screen.getByRole('button', { name: /Ima button/i })).toBeTruthy();
127
+ });
128
+ it('should handle onClose correctly when cancel button is clicked', async () => {
129
+ const spy = jest.fn();
130
+ render(<UserFeedback onSubmit={jest.fn} quickResponses={MOCK_RESPONSES} onClose={spy} timestamp="12/12/12" />);
131
+ const cancelButton = screen.getByRole('button', { name: 'Cancel' });
132
+ expect(cancelButton).toBeTruthy();
133
+ await userEvent.click(cancelButton);
134
+ expect(spy).toHaveBeenCalledTimes(1);
135
+ });
136
+ it('should change cancel word correctly', () => {
137
+ render(
138
+ <UserFeedback
139
+ onSubmit={jest.fn}
140
+ quickResponses={MOCK_RESPONSES}
141
+ onClose={jest.fn}
142
+ cancelWord="Exit"
143
+ timestamp="12/12/12"
144
+ />
145
+ );
146
+ expect(screen.getByRole('button', { name: 'Exit' })).toBeTruthy();
147
+ });
148
+ it('should handle className', async () => {
149
+ render(
150
+ <UserFeedback
151
+ timestamp="12/12/12"
152
+ onClose={jest.fn}
153
+ onSubmit={jest.fn}
154
+ quickResponses={MOCK_RESPONSES}
155
+ className="test"
156
+ data-testid="card"
157
+ />
158
+ );
159
+ expect(screen.getByTestId('card')).toHaveClass('test');
160
+ });
161
+ it('should apply id', async () => {
162
+ render(
163
+ <UserFeedback
164
+ timestamp="12/12/12"
165
+ onClose={jest.fn}
166
+ onSubmit={jest.fn}
167
+ quickResponses={MOCK_RESPONSES}
168
+ id="test"
169
+ data-testid="card"
170
+ />
171
+ );
172
+ expect(screen.getByTestId('card').parentElement).toHaveAttribute('id', 'test');
173
+ });
174
+ it('should handle submit correctly when nothing is selected', async () => {
175
+ const spy = jest.fn();
176
+ render(<UserFeedback timestamp="12/12/12" onClose={jest.fn} onSubmit={spy} quickResponses={MOCK_RESPONSES} />);
177
+ await userEvent.click(screen.getByRole('button', { name: /Submit/i }));
178
+ expect(spy).toHaveBeenCalledTimes(1);
179
+ expect(spy).toHaveBeenCalledWith(undefined, '');
180
+ });
181
+ it('should handle submit correctly when item is selected', async () => {
182
+ const spy = jest.fn();
183
+ render(<UserFeedback timestamp="12/12/12" onClose={jest.fn} onSubmit={spy} quickResponses={MOCK_RESPONSES} />);
184
+ await userEvent.click(screen.getByRole('button', { name: /Easy to understand/i }));
185
+ await userEvent.click(screen.getByRole('button', { name: /Submit/i }));
186
+ expect(spy).toHaveBeenCalledTimes(1);
187
+ expect(spy).toHaveBeenCalledWith('2', '');
188
+ });
189
+ it('should handle submit correctly when there is just text input', async () => {
190
+ const spy = jest.fn();
191
+ render(
192
+ <UserFeedback timestamp="12/12/12" onClose={jest.fn} onSubmit={spy} quickResponses={MOCK_RESPONSES} hasTextArea />
193
+ );
194
+ await userEvent.type(
195
+ screen.getByRole('textbox', { name: /Provide optional additional feedback/i }),
196
+ 'What a great experience!'
197
+ );
198
+ await userEvent.click(screen.getByRole('button', { name: /Submit/i }));
199
+ expect(spy).toHaveBeenCalledTimes(1);
200
+ expect(spy).toHaveBeenCalledWith(undefined, 'What a great experience!');
201
+ });
202
+ it('should handle submit correctly when item is selected and there is text input', async () => {
203
+ const spy = jest.fn();
204
+ render(
205
+ <UserFeedback timestamp="12/12/12" onClose={jest.fn} onSubmit={spy} quickResponses={MOCK_RESPONSES} hasTextArea />
206
+ );
207
+ await userEvent.click(screen.getByRole('button', { name: /Easy to understand/i }));
208
+ await userEvent.type(
209
+ screen.getByRole('textbox', { name: /Provide optional additional feedback/i }),
210
+ 'What a great experience!'
211
+ );
212
+ await userEvent.click(screen.getByRole('button', { name: /Submit/i }));
213
+ expect(spy).toHaveBeenCalledTimes(1);
214
+ expect(spy).toHaveBeenCalledWith('2', 'What a great experience!');
215
+ });
216
+ it('should default title heading level to h1', () => {
217
+ render(<UserFeedback timestamp="12/12/12" onClose={jest.fn} onSubmit={jest.fn} quickResponses={MOCK_RESPONSES} />);
218
+ expect(screen.getByRole('heading', { level: 1, name: /Why did you choose this rating?/i })).toBeTruthy();
219
+ });
220
+ it('should be able to change title heading level', () => {
221
+ render(
222
+ <UserFeedback
223
+ timestamp="12/12/12"
224
+ onClose={jest.fn}
225
+ onSubmit={jest.fn}
226
+ quickResponses={MOCK_RESPONSES}
227
+ headingLevel="h6"
228
+ />
229
+ );
230
+ expect(screen.getByRole('heading', { level: 6, name: /Why did you choose this rating?/i })).toBeTruthy();
231
+ });
232
+ it('should focus on load by default', () => {
233
+ render(
234
+ <UserFeedback
235
+ timestamp="12/12/12"
236
+ onClose={jest.fn}
237
+ onSubmit={jest.fn}
238
+ quickResponses={MOCK_RESPONSES}
239
+ data-testid="card"
240
+ />
241
+ );
242
+ expect(screen.getByTestId('card').parentElement).toHaveFocus();
243
+ });
244
+ it('should not focus on load if focusOnLoad = false', () => {
245
+ render(
246
+ <UserFeedback
247
+ timestamp="12/12/12"
248
+ onClose={jest.fn}
249
+ onSubmit={jest.fn}
250
+ quickResponses={MOCK_RESPONSES}
251
+ data-testid="card"
252
+ focusOnLoad={false}
253
+ />
254
+ );
255
+ expect(screen.getByTestId('card').parentElement).not.toHaveFocus();
256
+ });
257
+ });
@@ -0,0 +1,132 @@
1
+ // ============================================================================
2
+ // Chatbot Main - Messages - Feedback Card
3
+ // ============================================================================
4
+ import React from 'react';
5
+
6
+ // Import PatternFly components
7
+ import {
8
+ ActionGroup,
9
+ Button,
10
+ Card,
11
+ CardBody,
12
+ CardHeader,
13
+ CardProps,
14
+ Form,
15
+ LabelGroupProps,
16
+ OUIAProps,
17
+ TextArea
18
+ } from '@patternfly/react-core';
19
+ import QuickResponse from '../QuickResponse/QuickResponse';
20
+ import CloseButton from './CloseButton';
21
+
22
+ export interface UserFeedbackProps extends Omit<CardProps, 'onSubmit'>, OUIAProps {
23
+ /** Additional classes for the pagination navigation container. */
24
+ className?: string;
25
+ /** Quick responses a user can select */
26
+ quickResponses?: QuickResponse[];
27
+ /** Props for quick responses container */
28
+ quickResponseContainerProps?: Omit<LabelGroupProps, 'ref'>;
29
+ /** Whether form includes text area */
30
+ hasTextArea?: boolean;
31
+ /** Placeholder of text area */
32
+ textAreaPlaceholder?: string;
33
+ /** Aria label for text area */
34
+ textAreaAriaLabel?: string;
35
+ /** Callback function for when text area changes */
36
+ onTextAreaChange?: (event: React.ChangeEvent<HTMLTextAreaElement>, value: string) => void;
37
+ /** Callback function for when form is submitted */
38
+ onSubmit: (selectedResponse?: string, additionalFeedback?: string) => void;
39
+ /** Callback function for when close button is clicked */
40
+ onClose: () => void;
41
+ /** Aria label for close button */
42
+ closeButtonAriaLabel?: string;
43
+ /** Label for the English word "Submit." */
44
+ submitWord?: string;
45
+ /** Label for the English word "Cancel." */
46
+ cancelWord?: string;
47
+ /** Uniquely identifies the card. */
48
+ id?: string;
49
+ /** The heading level to use, default is h1 */
50
+ headingLevel?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
51
+ /** Whether to focus card on load */
52
+ focusOnLoad?: boolean;
53
+ /** Timestamp passed in by Message for more context in aria announcements */
54
+ timestamp?: string;
55
+ }
56
+
57
+ const UserFeedback: React.FunctionComponent<UserFeedbackProps> = ({
58
+ className,
59
+ timestamp,
60
+ title = 'Why did you choose this rating?',
61
+ hasTextArea,
62
+ textAreaAriaLabel = `Provide optional additional feedback for message received at ${timestamp}`,
63
+ textAreaPlaceholder = 'Provide optional additional feedback',
64
+ onTextAreaChange,
65
+ submitWord = 'Submit',
66
+ quickResponses,
67
+ quickResponseContainerProps = { 'aria-label': `Quick feedback for message received at ${timestamp}` },
68
+ onSubmit,
69
+ onClose,
70
+ closeButtonAriaLabel = `Close feedback for message received at ${timestamp}`,
71
+ id,
72
+ headingLevel: HeadingLevel = 'h1',
73
+ focusOnLoad = true,
74
+ cancelWord = 'Cancel',
75
+ ...props
76
+ }: UserFeedbackProps) => {
77
+ const [selectedResponse, setSelectedResponse] = React.useState<string>();
78
+ const [value, setValue] = React.useState('');
79
+ const divRef = React.useRef<HTMLDivElement>(null);
80
+
81
+ React.useEffect(() => {
82
+ if (focusOnLoad) {
83
+ divRef.current?.focus();
84
+ }
85
+ }, []);
86
+
87
+ return (
88
+ /* card does not have ref forwarding; hence wrapper div */
89
+ <div ref={divRef} id={id} tabIndex={0} aria-label={title}>
90
+ <Card className={`pf-chatbot__feedback-card ${className ? className : ''}`} {...props}>
91
+ <CardHeader
92
+ actions={{
93
+ actions: <CloseButton onClose={onClose} ariaLabel={closeButtonAriaLabel} />
94
+ }}
95
+ >
96
+ <HeadingLevel className="pf-chatbot__feedback-card-title">{title}</HeadingLevel>
97
+ </CardHeader>
98
+ <CardBody>
99
+ <Form className="pf-chatbot__feedback-card-form">
100
+ {quickResponses && (
101
+ <QuickResponse
102
+ quickResponses={quickResponses}
103
+ quickResponseContainerProps={quickResponseContainerProps}
104
+ onSelect={(id) => setSelectedResponse(id)}
105
+ />
106
+ )}
107
+ {hasTextArea && (
108
+ <TextArea
109
+ value={value}
110
+ onChange={(_event, value) => {
111
+ setValue(value);
112
+ onTextAreaChange && onTextAreaChange(_event, value);
113
+ }}
114
+ placeholder={textAreaPlaceholder}
115
+ aria-label={textAreaAriaLabel}
116
+ resizeOrientation="vertical"
117
+ />
118
+ )}
119
+ <ActionGroup>
120
+ <Button onClick={() => onSubmit(selectedResponse, value)}>{submitWord}</Button>
121
+ <Button variant="link" onClick={onClose}>
122
+ {cancelWord}
123
+ </Button>
124
+ </ActionGroup>
125
+ </Form>
126
+ </CardBody>
127
+ </Card>
128
+ </div>
129
+ );
130
+ };
131
+
132
+ export default UserFeedback;