@patternfly/chatbot 6.3.0-prerelease.22 → 6.3.0-prerelease.24

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 (33) hide show
  1. package/dist/cjs/FileDropZone/FileDropZone.d.ts +15 -1
  2. package/dist/cjs/FileDropZone/FileDropZone.js +7 -2
  3. package/dist/cjs/FileDropZone/FileDropZone.test.js +55 -0
  4. package/dist/cjs/MessageBar/AttachButton.d.ts +18 -1
  5. package/dist/cjs/MessageBar/AttachButton.js +4 -6
  6. package/dist/cjs/MessageBar/AttachButton.test.js +54 -0
  7. package/dist/cjs/MessageBar/MessageBar.d.ts +23 -7
  8. package/dist/cjs/MessageBar/MessageBar.js +2 -2
  9. package/dist/cjs/ResponseActions/ResponseActions.js +27 -2
  10. package/dist/cjs/ResponseActions/ResponseActions.test.js +60 -0
  11. package/dist/esm/FileDropZone/FileDropZone.d.ts +15 -1
  12. package/dist/esm/FileDropZone/FileDropZone.js +7 -2
  13. package/dist/esm/FileDropZone/FileDropZone.test.js +55 -0
  14. package/dist/esm/MessageBar/AttachButton.d.ts +18 -1
  15. package/dist/esm/MessageBar/AttachButton.js +4 -6
  16. package/dist/esm/MessageBar/AttachButton.test.js +54 -0
  17. package/dist/esm/MessageBar/MessageBar.d.ts +23 -7
  18. package/dist/esm/MessageBar/MessageBar.js +2 -2
  19. package/dist/esm/ResponseActions/ResponseActions.js +27 -2
  20. package/dist/esm/ResponseActions/ResponseActions.test.js +60 -0
  21. package/package.json +1 -1
  22. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithClickedResponseActions.tsx +25 -0
  23. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithCustomResponseActions.tsx +1 -0
  24. package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +17 -0
  25. package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachment.tsx +19 -1
  26. package/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachmentMenu.tsx +1 -0
  27. package/src/FileDropZone/FileDropZone.test.tsx +83 -0
  28. package/src/FileDropZone/FileDropZone.tsx +32 -2
  29. package/src/MessageBar/AttachButton.test.tsx +75 -0
  30. package/src/MessageBar/AttachButton.tsx +35 -2
  31. package/src/MessageBar/MessageBar.tsx +47 -7
  32. package/src/ResponseActions/ResponseActions.test.tsx +98 -1
  33. package/src/ResponseActions/ResponseActions.tsx +31 -2
@@ -7,7 +7,7 @@ import { forwardRef } from 'react';
7
7
 
8
8
  // Import PatternFly components
9
9
  import { Button, ButtonProps, DropEvent, Icon, Tooltip, TooltipProps } from '@patternfly/react-core';
10
- import { Accept, useDropzone } from 'react-dropzone';
10
+ import { Accept, DropzoneOptions, FileError, FileRejection, useDropzone } from 'react-dropzone';
11
11
  import { PaperclipIcon } from '@patternfly/react-icons/dist/esm/icons/paperclip-icon';
12
12
 
13
13
  export interface AttachButtonProps extends ButtonProps {
@@ -33,7 +33,24 @@ export interface AttachButtonProps extends ButtonProps {
33
33
  tooltipContent?: string;
34
34
  /** Test id applied to input */
35
35
  inputTestId?: string;
36
+ /** Whether button is compact */
36
37
  isCompact?: boolean;
38
+ /** Minimum file size allowed */
39
+ minSize?: number;
40
+ /** Max file size allowed */
41
+ maxSize?: number;
42
+ /** Max number of files allowed */
43
+ maxFiles?: number;
44
+ /** Whether attachments are disabled */
45
+ isAttachmentDisabled?: boolean;
46
+ /** Callback when file(s) are attached */
47
+ onAttach?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
48
+ /** Callback function for AttachButton when an attachment fails */
49
+ onAttachRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
50
+ /** Validator for files; see https://react-dropzone.js.org/#!/Custom%20validation for more information */
51
+ validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
52
+ /** Additional props passed to react-dropzone */
53
+ dropzoneProps?: DropzoneOptions;
37
54
  }
38
55
 
39
56
  const AttachButtonBase: FunctionComponent<AttachButtonProps> = ({
@@ -47,12 +64,28 @@ const AttachButtonBase: FunctionComponent<AttachButtonProps> = ({
47
64
  inputTestId,
48
65
  isCompact,
49
66
  allowedFileTypes,
67
+ minSize,
68
+ maxSize,
69
+ maxFiles,
70
+ isAttachmentDisabled,
71
+ onAttach,
72
+ onAttachRejected,
73
+ validator,
74
+ dropzoneProps,
50
75
  ...props
51
76
  }: AttachButtonProps) => {
52
77
  const { open, getInputProps } = useDropzone({
53
78
  multiple: true,
54
79
  onDropAccepted: onAttachAccepted,
55
- accept: allowedFileTypes
80
+ accept: allowedFileTypes,
81
+ minSize,
82
+ maxSize,
83
+ maxFiles,
84
+ disabled: isAttachmentDisabled,
85
+ onDrop: onAttach,
86
+ onDropRejected: onAttachRejected,
87
+ validator,
88
+ ...dropzoneProps
56
89
  });
57
90
 
58
91
  return (
@@ -1,6 +1,6 @@
1
1
  import type { ChangeEvent, FunctionComponent, KeyboardEvent as ReactKeyboardEvent, Ref } from 'react';
2
2
  import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
3
- import { Accept } from 'react-dropzone/.';
3
+ import { Accept, DropzoneOptions, FileError, FileRejection } from 'react-dropzone/.';
4
4
  import { ButtonProps, DropEvent, TextArea, TextAreaProps, TooltipProps } from '@patternfly/react-core';
5
5
 
6
6
  // Import Chatbot components
@@ -53,6 +53,28 @@ export interface MessageBarProps extends Omit<TextAreaProps, 'innerRef'> {
53
53
  handleStopButton?: (event: React.MouseEvent<HTMLButtonElement>) => void;
54
54
  /** Callback function for when attach button is used to upload a file */
55
55
  handleAttach?: (data: File[], event: DropEvent) => void;
56
+ /** Specifies the file types accepted by the attachment upload component.
57
+ * Files that don't match the accepted types will be disabled in the file picker.
58
+ * For example,
59
+ * allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
60
+ **/
61
+ allowedFileTypes?: Accept;
62
+ /** Minimum file size allowed */
63
+ minSize?: number;
64
+ /** Max file size allowed */
65
+ maxSize?: number;
66
+ /** Max number of files allowed */
67
+ maxFiles?: number;
68
+ /** Whether attachments are disabled */
69
+ isAttachmentDisabled?: boolean;
70
+ /** Callback when file(s) are attached */
71
+ onAttach?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
72
+ /** Callback function for AttachButton when an attachment fails */
73
+ onAttachRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
74
+ /** Validator for files; see https://react-dropzone.js.org/#!/Custom%20validation for more information */
75
+ validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
76
+ /** Additional props passed to react-dropzone */
77
+ dropzoneProps?: DropzoneOptions;
56
78
  /** Props to enable a menu that opens when the Attach button is clicked, instead of the attachment window */
57
79
  attachMenuProps?: MessageBarWithAttachMenuProps;
58
80
  /** Flag to provide manual control over whether send button is disabled */
@@ -80,12 +102,6 @@ export interface MessageBarProps extends Omit<TextAreaProps, 'innerRef'> {
80
102
  displayMode?: ChatbotDisplayMode;
81
103
  /** Whether message bar is compact */
82
104
  isCompact?: boolean;
83
- /** Specifies the file types accepted by the attachment upload component.
84
- * Files that don't match the accepted types will be disabled in the file picker.
85
- * For example,
86
- * allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
87
- **/
88
- allowedFileTypes?: Accept;
89
105
  /** Ref applied to message bar textarea, for use with focus or other custom behaviors */
90
106
  innerRef?: React.Ref<HTMLTextAreaElement>;
91
107
  }
@@ -109,6 +125,14 @@ export const MessageBarBase: FunctionComponent<MessageBarProps> = ({
109
125
  value,
110
126
  isCompact = false,
111
127
  allowedFileTypes,
128
+ minSize,
129
+ maxSize,
130
+ maxFiles,
131
+ isAttachmentDisabled,
132
+ onAttach,
133
+ onAttachRejected,
134
+ validator,
135
+ dropzoneProps,
112
136
  innerRef,
113
137
  ...props
114
138
  }: MessageBarProps) => {
@@ -309,6 +333,14 @@ export const MessageBarBase: FunctionComponent<MessageBarProps> = ({
309
333
  isCompact={isCompact}
310
334
  tooltipProps={buttonProps?.attach?.tooltipProps}
311
335
  allowedFileTypes={allowedFileTypes}
336
+ minSize={minSize}
337
+ maxSize={maxSize}
338
+ maxFiles={maxFiles}
339
+ isAttachmentDisabled={isAttachmentDisabled}
340
+ onAttach={onAttach}
341
+ onAttachRejected={onAttachRejected}
342
+ validator={validator}
343
+ dropzoneProps={dropzoneProps}
312
344
  {...buttonProps?.attach?.props}
313
345
  />
314
346
  )}
@@ -321,6 +353,14 @@ export const MessageBarBase: FunctionComponent<MessageBarProps> = ({
321
353
  isCompact={isCompact}
322
354
  tooltipProps={buttonProps?.attach?.tooltipProps}
323
355
  allowedFileTypes={allowedFileTypes}
356
+ minSize={minSize}
357
+ maxSize={maxSize}
358
+ maxFiles={maxFiles}
359
+ isAttachmentDisabled={isAttachmentDisabled}
360
+ onAttach={onAttach}
361
+ onAttachRejected={onAttachRejected}
362
+ validator={validator}
363
+ dropzoneProps={dropzoneProps}
324
364
  {...buttonProps?.attach?.props}
325
365
  />
326
366
  )}
@@ -1,6 +1,6 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
  import '@testing-library/jest-dom';
3
- import ResponseActions from './ResponseActions';
3
+ import ResponseActions, { ActionProps } from './ResponseActions';
4
4
  import userEvent from '@testing-library/user-event';
5
5
  import { DownloadIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons';
6
6
  import Message from '../Message';
@@ -129,6 +129,103 @@ describe('ResponseActions', () => {
129
129
  expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
130
130
  expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
131
131
  });
132
+
133
+ it('should handle isClicked prop within group of buttons correctly', async () => {
134
+ render(
135
+ <ResponseActions
136
+ actions={
137
+ {
138
+ positive: { 'data-testid': 'positive-btn', onClick: jest.fn(), isClicked: true },
139
+ negative: { 'data-testid': 'negative-btn', onClick: jest.fn() }
140
+ } as Record<string, ActionProps>
141
+ }
142
+ />
143
+ );
144
+
145
+ expect(screen.getByTestId('positive-btn')).toHaveClass('pf-chatbot__button--response-action-clicked');
146
+ expect(screen.getByTestId('negative-btn')).not.toHaveClass('pf-chatbot__button--response-action-clicked');
147
+ });
148
+
149
+ it('should set "listen" button as active if its `isClicked` is true', async () => {
150
+ render(
151
+ <ResponseActions
152
+ actions={
153
+ {
154
+ positive: { 'data-testid': 'positive-btn', onClick: jest.fn(), isClicked: false },
155
+ negative: { 'data-testid': 'negative-btn', onClick: jest.fn(), isClicked: false },
156
+ listen: { 'data-testid': 'listen-btn', onClick: jest.fn(), isClicked: true }
157
+ } as Record<string, ActionProps>
158
+ }
159
+ />
160
+ );
161
+ expect(screen.getByTestId('listen-btn')).toHaveClass('pf-chatbot__button--response-action-clicked');
162
+
163
+ expect(screen.getByTestId('positive-btn')).not.toHaveClass('pf-chatbot__button--response-action-clicked');
164
+ expect(screen.getByTestId('negative-btn')).not.toHaveClass('pf-chatbot__button--response-action-clicked');
165
+ });
166
+
167
+ it('should prioritize "positive" when both "positive" and "negative" are set to clicked', async () => {
168
+ render(
169
+ <ResponseActions
170
+ actions={
171
+ {
172
+ positive: { 'data-testid': 'positive-btn', onClick: jest.fn(), isClicked: true },
173
+ negative: { 'data-testid': 'negative-btn', onClick: jest.fn(), isClicked: true }
174
+ } as Record<string, ActionProps>
175
+ }
176
+ />
177
+ );
178
+ // Positive button should take precendence
179
+ expect(screen.getByTestId('positive-btn')).toHaveClass('pf-chatbot__button--response-action-clicked');
180
+ expect(screen.getByTestId('negative-btn')).not.toHaveClass('pf-chatbot__button--response-action-clicked');
181
+ });
182
+
183
+ it('should set an additional action button as active if it is initially clicked and no predefined are clicked', async () => {
184
+ const [additionalActions] = CUSTOM_ACTIONS;
185
+ const customActions = {
186
+ positive: { 'data-testid': 'positive', onClick: jest.fn(), isClicked: false },
187
+ negative: { 'data-testid': 'negative', onClick: jest.fn(), isClicked: false },
188
+ ...Object.keys(additionalActions).reduce((acc, actionKey) => {
189
+ acc[actionKey] = {
190
+ ...additionalActions[actionKey],
191
+ 'data-testid': actionKey,
192
+ isClicked: actionKey === 'regenerate'
193
+ };
194
+ return acc;
195
+ }, {})
196
+ };
197
+ render(<ResponseActions actions={customActions} />);
198
+
199
+ Object.keys(customActions).forEach((actionKey) => {
200
+ if (actionKey === 'regenerate') {
201
+ expect(screen.getByTestId(actionKey)).toHaveClass('pf-chatbot__button--response-action-clicked');
202
+ } else {
203
+ // Other actions should not have clicked class
204
+ expect(screen.getByTestId(actionKey)).not.toHaveClass('pf-chatbot__button--response-action-clicked');
205
+ }
206
+ });
207
+ });
208
+
209
+ it('should activate the clicked button and deactivate any previously active button', async () => {
210
+ const actions = {
211
+ positive: { 'data-testid': 'positive', onClick: jest.fn(), isClicked: false },
212
+ negative: { 'data-testid': 'negative', onClick: jest.fn(), isClicked: true }
213
+ };
214
+ render(<ResponseActions actions={actions} />);
215
+
216
+ const negativeBtn = screen.getByTestId('negative');
217
+ const positiveBtn = screen.getByTestId('positive');
218
+ // negative button is initially clicked
219
+ expect(negativeBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
220
+ expect(positiveBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
221
+
222
+ await userEvent.click(positiveBtn);
223
+
224
+ // positive button should now have the clicked class
225
+ expect(positiveBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
226
+ expect(negativeBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
227
+ });
228
+
132
229
  it('should render buttons correctly', () => {
133
230
  ALL_ACTIONS.forEach(({ type, label }) => {
134
231
  render(<ResponseActions actions={{ [type]: { onClick: jest.fn() } }} />);
@@ -55,12 +55,40 @@ export interface ResponseActionProps {
55
55
 
56
56
  export const ResponseActions: FunctionComponent<ResponseActionProps> = ({ actions }) => {
57
57
  const [activeButton, setActiveButton] = useState<string>();
58
+ const [clickStatePersisted, setClickStatePersisted] = useState<boolean>(false);
59
+ useEffect(() => {
60
+ // Define the order of precedence for checking initial `isClicked`
61
+ const actionPrecedence = ['positive', 'negative', 'copy', 'share', 'download', 'listen'];
62
+ let initialActive: string | undefined;
63
+
64
+ // Check predefined actions first based on precedence
65
+ for (const actionName of actionPrecedence) {
66
+ const actionProp = actions[actionName as keyof typeof actions];
67
+ if (actionProp?.isClicked) {
68
+ initialActive = actionName;
69
+ break;
70
+ }
71
+ }
72
+ // If no predefined action was initially clicked, check additionalActions
73
+ if (!initialActive) {
74
+ const clickedActionName = Object.keys(additionalActions).find(
75
+ (actionName) => !actionPrecedence.includes(actionName) && additionalActions[actionName]?.isClicked
76
+ );
77
+ initialActive = clickedActionName;
78
+ }
79
+ if (initialActive) {
80
+ // Click state is explicitly controlled by consumer.
81
+ setClickStatePersisted(true);
82
+ }
83
+ setActiveButton(initialActive);
84
+ }, [actions]);
85
+
58
86
  const { positive, negative, copy, share, download, listen, ...additionalActions } = actions;
59
87
  const responseActions = useRef<HTMLDivElement>(null);
60
88
 
61
89
  useEffect(() => {
62
90
  const handleClickOutside = (e) => {
63
- if (responseActions.current && !responseActions.current.contains(e.target)) {
91
+ if (responseActions.current && !responseActions.current.contains(e.target) && !clickStatePersisted) {
64
92
  setActiveButton(undefined);
65
93
  }
66
94
  };
@@ -69,13 +97,14 @@ export const ResponseActions: FunctionComponent<ResponseActionProps> = ({ action
69
97
  return () => {
70
98
  window.removeEventListener('click', handleClickOutside);
71
99
  };
72
- }, []);
100
+ }, [clickStatePersisted]);
73
101
 
74
102
  const handleClick = (
75
103
  e: MouseEvent | MouseEvent<Element, MouseEvent> | KeyboardEvent,
76
104
  id: string,
77
105
  onClick?: (event: MouseEvent | MouseEvent<Element, MouseEvent> | KeyboardEvent) => void
78
106
  ) => {
107
+ setClickStatePersisted(false);
79
108
  setActiveButton(id);
80
109
  onClick && onClick(e);
81
110
  };