@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
@@ -1,7 +1,7 @@
1
1
  import { DropEvent } from '@patternfly/react-core';
2
2
  import type { FunctionComponent } from 'react';
3
3
  import { ChatbotDisplayMode } from '../Chatbot';
4
- import { Accept } from 'react-dropzone/.';
4
+ import { Accept, FileError, FileRejection } from 'react-dropzone/.';
5
5
  export interface FileDropZoneProps {
6
6
  /** Content displayed when the drop zone is not currently in use */
7
7
  children?: React.ReactNode;
@@ -19,6 +19,20 @@ export interface FileDropZoneProps {
19
19
  allowedFileTypes?: Accept;
20
20
  /** Display mode for the Chatbot parent; this influences the styles applied */
21
21
  displayMode?: ChatbotDisplayMode;
22
+ /** Minimum file size allowed */
23
+ minSize?: number;
24
+ /** Max file size allowed */
25
+ maxSize?: number;
26
+ /** Max number of files allowed */
27
+ maxFiles?: number;
28
+ /** Whether attachments are disabled */
29
+ isAttachmentDisabled?: boolean;
30
+ /** Callback when file(s) are attached */
31
+ onAttach?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
32
+ /** Callback function for AttachButton when an attachment fails */
33
+ onAttachRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
34
+ /** Validator for files; see https://react-dropzone.js.org/#!/Custom%20validation for more information */
35
+ validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
22
36
  }
23
37
  declare const FileDropZone: FunctionComponent<FileDropZoneProps>;
24
38
  export default FileDropZone;
@@ -17,9 +17,14 @@ const react_1 = require("react");
17
17
  const Chatbot_1 = require("../Chatbot");
18
18
  const react_icons_1 = require("@patternfly/react-icons");
19
19
  const FileDropZone = (_a) => {
20
- var { children, className, infoText = 'Maximum file size is 25 MB', onFileDrop, allowedFileTypes, displayMode = Chatbot_1.ChatbotDisplayMode.default } = _a, props = __rest(_a, ["children", "className", "infoText", "onFileDrop", "allowedFileTypes", "displayMode"]);
20
+ var { children, className, infoText = 'Maximum file size is 25 MB', onFileDrop, allowedFileTypes, minSize, maxSize, maxFiles, isAttachmentDisabled, onAttach, onAttachRejected, validator, displayMode = Chatbot_1.ChatbotDisplayMode.default } = _a, props = __rest(_a, ["children", "className", "infoText", "onFileDrop", "allowedFileTypes", "minSize", "maxSize", "maxFiles", "isAttachmentDisabled", "onAttach", "onAttachRejected", "validator", "displayMode"]);
21
21
  const [showDropZone, setShowDropZone] = (0, react_1.useState)(false);
22
22
  const renderDropZone = () => ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsx)(react_core_1.MultipleFileUploadMain, { titleIcon: (0, jsx_runtime_1.jsx)(react_icons_1.UploadIcon, {}), titleText: "Drag and drop your file here", infoText: infoText, isUploadButtonHidden: true }) }));
23
- return ((0, jsx_runtime_1.jsx)(react_core_1.MultipleFileUpload, { dropzoneProps: Object.assign({ accept: allowedFileTypes, onDrop: () => setShowDropZone(false) }, props), onDragEnter: () => setShowDropZone(true), onDragLeave: () => setShowDropZone(false), onFileDrop: onFileDrop, className: `pf-chatbot__dropzone pf-chatbot__dropzone--${displayMode} pf-chatbot__dropzone--${showDropZone ? 'visible' : 'invisible'} ${className ? className : ''}`, children: showDropZone ? renderDropZone() : children }));
23
+ return ((0, jsx_runtime_1.jsx)(react_core_1.MultipleFileUpload, { dropzoneProps: Object.assign({ accept: allowedFileTypes, onDrop: (acceptedFiles, fileRejections, event) => {
24
+ setShowDropZone(false);
25
+ onAttach && onAttach(acceptedFiles, fileRejections, event);
26
+ }, minSize,
27
+ maxSize,
28
+ maxFiles, disabled: isAttachmentDisabled, onDropRejected: onAttachRejected, validator }, props), onDragEnter: () => setShowDropZone(true), onDragLeave: () => setShowDropZone(false), onFileDrop: onFileDrop, className: `pf-chatbot__dropzone pf-chatbot__dropzone--${displayMode} pf-chatbot__dropzone--${showDropZone ? 'visible' : 'invisible'} ${className ? className : ''}`, children: showDropZone ? renderDropZone() : children }));
24
29
  };
25
30
  exports.default = FileDropZone;
@@ -42,4 +42,59 @@ describe('FileDropZone', () => {
42
42
  yield user_event_1.default.upload(fileInput, file);
43
43
  expect(onFileDrop).not.toHaveBeenCalled();
44
44
  }));
45
+ it('should respect minSize restriction', () => __awaiter(void 0, void 0, void 0, function* () {
46
+ const onAttachRejected = jest.fn();
47
+ const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { onFileDrop: jest.fn(), minSize: 1000, onAttachRejected: onAttachRejected }));
48
+ const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
49
+ const fileInput = container.querySelector('input[type="file"]');
50
+ yield user_event_1.default.upload(fileInput, file);
51
+ expect(onAttachRejected).toHaveBeenCalled();
52
+ }));
53
+ it('should respect maxSize restriction', () => __awaiter(void 0, void 0, void 0, function* () {
54
+ const onAttachRejected = jest.fn();
55
+ const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { onFileDrop: jest.fn(), maxSize: 100, onAttachRejected: onAttachRejected }));
56
+ const largeContent = 'x'.repeat(200);
57
+ const file = new File([largeContent], 'example.txt', { type: 'text/plain' });
58
+ const fileInput = container.querySelector('input[type="file"]');
59
+ yield user_event_1.default.upload(fileInput, file);
60
+ expect(onAttachRejected).toHaveBeenCalled();
61
+ }));
62
+ it('should respect maxFiles restriction', () => __awaiter(void 0, void 0, void 0, function* () {
63
+ const onAttachRejected = jest.fn();
64
+ const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { onFileDrop: jest.fn(), maxFiles: 1, onAttachRejected: onAttachRejected }));
65
+ const files = [
66
+ new File(['Test1'], 'example1.txt', { type: 'text/plain' }),
67
+ new File(['Test2'], 'example2.txt', { type: 'text/plain' })
68
+ ];
69
+ const fileInput = container.querySelector('input[type="file"]');
70
+ yield user_event_1.default.upload(fileInput, files);
71
+ expect(onAttachRejected).toHaveBeenCalled();
72
+ }));
73
+ it('should be disabled when isAttachmentDisabled is true', () => __awaiter(void 0, void 0, void 0, function* () {
74
+ const onFileDrop = jest.fn();
75
+ const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { onFileDrop: onFileDrop, isAttachmentDisabled: true }));
76
+ const file = new File(['Test'], 'example.text', { type: 'text/plain' });
77
+ const fileInput = container.querySelector('input[type="file"]');
78
+ yield user_event_1.default.upload(fileInput, file);
79
+ expect(onFileDrop).not.toHaveBeenCalled();
80
+ }));
81
+ it('should call onAttach when files are attached', () => __awaiter(void 0, void 0, void 0, function* () {
82
+ const onAttach = jest.fn();
83
+ const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { onFileDrop: jest.fn(), onAttach: onAttach }));
84
+ const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
85
+ const fileInput = container.querySelector('input[type="file"]');
86
+ yield user_event_1.default.upload(fileInput, file);
87
+ expect(onAttach).toHaveBeenCalled();
88
+ }));
89
+ it('should use custom validator when provided', () => __awaiter(void 0, void 0, void 0, function* () {
90
+ const validator = jest.fn().mockReturnValue({ message: 'Custom error' });
91
+ const onAttachRejected = jest.fn();
92
+ const onFileDrop = jest.fn();
93
+ const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { onFileDrop: onFileDrop, validator: validator, onAttachRejected: onAttachRejected }));
94
+ const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
95
+ const fileInput = container.querySelector('input[type="file"]');
96
+ yield user_event_1.default.upload(fileInput, file);
97
+ expect(validator).toHaveBeenCalledWith(file);
98
+ expect(onAttachRejected).toHaveBeenCalled();
99
+ }));
45
100
  });
@@ -1,5 +1,5 @@
1
1
  import { ButtonProps, DropEvent, TooltipProps } from '@patternfly/react-core';
2
- import { Accept } from 'react-dropzone';
2
+ import { Accept, DropzoneOptions, FileError, FileRejection } from 'react-dropzone';
3
3
  export interface AttachButtonProps extends ButtonProps {
4
4
  /** Callback for when button is clicked */
5
5
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
@@ -23,6 +23,23 @@ export interface AttachButtonProps extends ButtonProps {
23
23
  tooltipContent?: string;
24
24
  /** Test id applied to input */
25
25
  inputTestId?: string;
26
+ /** Whether button is compact */
26
27
  isCompact?: boolean;
28
+ /** Minimum file size allowed */
29
+ minSize?: number;
30
+ /** Max file size allowed */
31
+ maxSize?: number;
32
+ /** Max number of files allowed */
33
+ maxFiles?: number;
34
+ /** Whether attachments are disabled */
35
+ isAttachmentDisabled?: boolean;
36
+ /** Callback when file(s) are attached */
37
+ onAttach?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
38
+ /** Callback function for AttachButton when an attachment fails */
39
+ onAttachRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
40
+ /** Validator for files; see https://react-dropzone.js.org/#!/Custom%20validation for more information */
41
+ validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
42
+ /** Additional props passed to react-dropzone */
43
+ dropzoneProps?: DropzoneOptions;
27
44
  }
28
45
  export declare const AttachButton: import("react").ForwardRefExoticComponent<AttachButtonProps & import("react").RefAttributes<any>>;
@@ -19,12 +19,10 @@ const react_core_1 = require("@patternfly/react-core");
19
19
  const react_dropzone_1 = require("react-dropzone");
20
20
  const paperclip_icon_1 = require("@patternfly/react-icons/dist/esm/icons/paperclip-icon");
21
21
  const AttachButtonBase = (_a) => {
22
- var { onAttachAccepted, onClick, isDisabled, className, tooltipProps, innerRef, tooltipContent = 'Attach', inputTestId, isCompact, allowedFileTypes } = _a, props = __rest(_a, ["onAttachAccepted", "onClick", "isDisabled", "className", "tooltipProps", "innerRef", "tooltipContent", "inputTestId", "isCompact", "allowedFileTypes"]);
23
- const { open, getInputProps } = (0, react_dropzone_1.useDropzone)({
24
- multiple: true,
25
- onDropAccepted: onAttachAccepted,
26
- accept: allowedFileTypes
27
- });
22
+ var { onAttachAccepted, onClick, isDisabled, className, tooltipProps, innerRef, tooltipContent = 'Attach', inputTestId, isCompact, allowedFileTypes, minSize, maxSize, maxFiles, isAttachmentDisabled, onAttach, onAttachRejected, validator, dropzoneProps } = _a, props = __rest(_a, ["onAttachAccepted", "onClick", "isDisabled", "className", "tooltipProps", "innerRef", "tooltipContent", "inputTestId", "isCompact", "allowedFileTypes", "minSize", "maxSize", "maxFiles", "isAttachmentDisabled", "onAttach", "onAttachRejected", "validator", "dropzoneProps"]);
23
+ const { open, getInputProps } = (0, react_dropzone_1.useDropzone)(Object.assign({ multiple: true, onDropAccepted: onAttachAccepted, accept: allowedFileTypes, minSize,
24
+ maxSize,
25
+ maxFiles, disabled: isAttachmentDisabled, onDrop: onAttach, onDropRejected: onAttachRejected, validator }, dropzoneProps));
28
26
  return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("input", Object.assign({ "data-testid": inputTestId }, getInputProps(), { hidden: true })), (0, jsx_runtime_1.jsx)(react_core_1.Tooltip, Object.assign({ id: "pf-chatbot__tooltip--attach", content: tooltipContent, position: "top", entryDelay: (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.entryDelay) || 0, exitDelay: (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.exitDelay) || 0, distance: (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.distance) || 8, animationDuration: (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.animationDuration) || 0,
29
27
  // prevents VO announcements of both aria label and tooltip
30
28
  aria: "none" }, tooltipProps, { children: (0, jsx_runtime_1.jsx)(react_core_1.Button, Object.assign({ variant: "plain", ref: innerRef, className: `pf-chatbot__button--attach ${isCompact ? 'pf-m-compact' : ''} ${className !== null && className !== void 0 ? className : ''}`, "aria-label": props['aria-label'] || 'Attach', isDisabled: isDisabled, onClick: onClick !== null && onClick !== void 0 ? onClick : open, icon: (0, jsx_runtime_1.jsx)(react_core_1.Icon, { iconSize: isCompact ? 'lg' : 'xl', isInline: true, children: (0, jsx_runtime_1.jsx)(paperclip_icon_1.PaperclipIcon, {}) }), size: isCompact ? 'sm' : undefined }, props)) }))] }));
@@ -91,4 +91,58 @@ describe('Attach button', () => {
91
91
  yield user_event_1.default.upload(input, file);
92
92
  expect(onAttachAccepted).not.toHaveBeenCalled();
93
93
  }));
94
+ it('should respect minSize restriction', () => __awaiter(void 0, void 0, void 0, function* () {
95
+ const onAttachRejected = jest.fn();
96
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", minSize: 1000, onAttachRejected: onAttachRejected }));
97
+ const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
98
+ const input = react_1.screen.getByTestId('input');
99
+ yield user_event_1.default.upload(input, file);
100
+ expect(onAttachRejected).toHaveBeenCalled();
101
+ }));
102
+ it('should respect maxSize restriction', () => __awaiter(void 0, void 0, void 0, function* () {
103
+ const onAttachRejected = jest.fn();
104
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", maxSize: 100, onAttachRejected: onAttachRejected }));
105
+ const largeContent = 'x'.repeat(200);
106
+ const file = new File([largeContent], 'example.txt', { type: 'text/plain' });
107
+ const input = react_1.screen.getByTestId('input');
108
+ yield user_event_1.default.upload(input, file);
109
+ expect(onAttachRejected).toHaveBeenCalled();
110
+ }));
111
+ it('should respect maxFiles restriction', () => __awaiter(void 0, void 0, void 0, function* () {
112
+ const onAttachRejected = jest.fn();
113
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", maxFiles: 1, onAttachRejected: onAttachRejected }));
114
+ const files = [
115
+ new File(['Test1'], 'example1.txt', { type: 'text/plain' }),
116
+ new File(['Test2'], 'example2.txt', { type: 'text/plain' })
117
+ ];
118
+ const input = react_1.screen.getByTestId('input');
119
+ yield user_event_1.default.upload(input, files);
120
+ expect(onAttachRejected).toHaveBeenCalled();
121
+ }));
122
+ it('should be disabled when isAttachmentDisabled is true', () => __awaiter(void 0, void 0, void 0, function* () {
123
+ const onFileDrop = jest.fn();
124
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", isAttachmentDisabled: true }));
125
+ const file = new File(['Test'], 'example.text', { type: 'text/plain' });
126
+ const input = react_1.screen.getByTestId('input');
127
+ yield user_event_1.default.upload(input, file);
128
+ expect(onFileDrop).not.toHaveBeenCalled();
129
+ }));
130
+ it('should call onAttach when files are attached', () => __awaiter(void 0, void 0, void 0, function* () {
131
+ const onAttach = jest.fn();
132
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", onAttach: onAttach }));
133
+ const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
134
+ const input = react_1.screen.getByTestId('input');
135
+ yield user_event_1.default.upload(input, file);
136
+ expect(onAttach).toHaveBeenCalled();
137
+ }));
138
+ it('should use custom validator when provided', () => __awaiter(void 0, void 0, void 0, function* () {
139
+ const validator = jest.fn().mockReturnValue({ message: 'Custom error' });
140
+ const onAttachRejected = jest.fn();
141
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", validator: validator, onAttachRejected: onAttachRejected }));
142
+ const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
143
+ const input = react_1.screen.getByTestId('input');
144
+ yield user_event_1.default.upload(input, file);
145
+ expect(validator).toHaveBeenCalledWith(file);
146
+ expect(onAttachRejected).toHaveBeenCalled();
147
+ }));
94
148
  });
@@ -1,5 +1,5 @@
1
1
  import type { FunctionComponent } from 'react';
2
- import { Accept } from 'react-dropzone/.';
2
+ import { Accept, DropzoneOptions, FileError, FileRejection } from 'react-dropzone/.';
3
3
  import { ButtonProps, DropEvent, TextAreaProps, TooltipProps } from '@patternfly/react-core';
4
4
  import { ChatbotDisplayMode } from '../Chatbot';
5
5
  export interface MessageBarWithAttachMenuProps {
@@ -43,6 +43,28 @@ export interface MessageBarProps extends Omit<TextAreaProps, 'innerRef'> {
43
43
  handleStopButton?: (event: React.MouseEvent<HTMLButtonElement>) => void;
44
44
  /** Callback function for when attach button is used to upload a file */
45
45
  handleAttach?: (data: File[], event: DropEvent) => void;
46
+ /** Specifies the file types accepted by the attachment upload component.
47
+ * Files that don't match the accepted types will be disabled in the file picker.
48
+ * For example,
49
+ * allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
50
+ **/
51
+ allowedFileTypes?: Accept;
52
+ /** Minimum file size allowed */
53
+ minSize?: number;
54
+ /** Max file size allowed */
55
+ maxSize?: number;
56
+ /** Max number of files allowed */
57
+ maxFiles?: number;
58
+ /** Whether attachments are disabled */
59
+ isAttachmentDisabled?: boolean;
60
+ /** Callback when file(s) are attached */
61
+ onAttach?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
62
+ /** Callback function for AttachButton when an attachment fails */
63
+ onAttachRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
64
+ /** Validator for files; see https://react-dropzone.js.org/#!/Custom%20validation for more information */
65
+ validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
66
+ /** Additional props passed to react-dropzone */
67
+ dropzoneProps?: DropzoneOptions;
46
68
  /** Props to enable a menu that opens when the Attach button is clicked, instead of the attachment window */
47
69
  attachMenuProps?: MessageBarWithAttachMenuProps;
48
70
  /** Flag to provide manual control over whether send button is disabled */
@@ -81,12 +103,6 @@ export interface MessageBarProps extends Omit<TextAreaProps, 'innerRef'> {
81
103
  displayMode?: ChatbotDisplayMode;
82
104
  /** Whether message bar is compact */
83
105
  isCompact?: boolean;
84
- /** Specifies the file types accepted by the attachment upload component.
85
- * Files that don't match the accepted types will be disabled in the file picker.
86
- * For example,
87
- * allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
88
- **/
89
- allowedFileTypes?: Accept;
90
106
  /** Ref applied to message bar textarea, for use with focus or other custom behaviors */
91
107
  innerRef?: React.Ref<HTMLTextAreaElement>;
92
108
  }
@@ -26,7 +26,7 @@ const AttachMenu_1 = __importDefault(require("../AttachMenu"));
26
26
  const StopButton_1 = __importDefault(require("./StopButton"));
27
27
  const MessageBarBase = (_a) => {
28
28
  var _b;
29
- var { onSendMessage, className, alwayShowSendButton, placeholder = 'Send a message...', hasAttachButton = true, hasMicrophoneButton, listeningText = 'Listening', handleAttach, attachMenuProps, isSendButtonDisabled, handleStopButton, hasStopButton, buttonProps, onChange, displayMode, value, isCompact = false, allowedFileTypes, innerRef } = _a, props = __rest(_a, ["onSendMessage", "className", "alwayShowSendButton", "placeholder", "hasAttachButton", "hasMicrophoneButton", "listeningText", "handleAttach", "attachMenuProps", "isSendButtonDisabled", "handleStopButton", "hasStopButton", "buttonProps", "onChange", "displayMode", "value", "isCompact", "allowedFileTypes", "innerRef"]);
29
+ var { onSendMessage, className, alwayShowSendButton, placeholder = 'Send a message...', hasAttachButton = true, hasMicrophoneButton, listeningText = 'Listening', handleAttach, attachMenuProps, isSendButtonDisabled, handleStopButton, hasStopButton, buttonProps, onChange, displayMode, value, isCompact = false, allowedFileTypes, minSize, maxSize, maxFiles, isAttachmentDisabled, onAttach, onAttachRejected, validator, dropzoneProps, innerRef } = _a, props = __rest(_a, ["onSendMessage", "className", "alwayShowSendButton", "placeholder", "hasAttachButton", "hasMicrophoneButton", "listeningText", "handleAttach", "attachMenuProps", "isSendButtonDisabled", "handleStopButton", "hasStopButton", "buttonProps", "onChange", "displayMode", "value", "isCompact", "allowedFileTypes", "minSize", "maxSize", "maxFiles", "isAttachmentDisabled", "onAttach", "onAttachRejected", "validator", "dropzoneProps", "innerRef"]);
30
30
  // Text Input
31
31
  // --------------------------------------------------------------------------
32
32
  const [message, setMessage] = (0, react_1.useState)(value !== null && value !== void 0 ? value : '');
@@ -182,7 +182,7 @@ const MessageBarBase = (_a) => {
182
182
  if (hasStopButton && handleStopButton) {
183
183
  return ((0, jsx_runtime_1.jsx)(StopButton_1.default, Object.assign({ onClick: handleStopButton, tooltipContent: (_a = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.stop) === null || _a === void 0 ? void 0 : _a.tooltipContent, isCompact: isCompact, tooltipProps: (_b = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.stop) === null || _b === void 0 ? void 0 : _b.tooltipProps }, (_c = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.stop) === null || _c === void 0 ? void 0 : _c.props)));
184
184
  }
185
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [attachMenuProps && ((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, Object.assign({ ref: attachButtonRef, onClick: handleAttachMenuToggle, isDisabled: isListeningMessage, tooltipContent: (_d = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _d === void 0 ? void 0 : _d.tooltipContent, isCompact: isCompact, tooltipProps: (_e = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _e === void 0 ? void 0 : _e.tooltipProps, allowedFileTypes: allowedFileTypes }, (_f = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _f === void 0 ? void 0 : _f.props))), !attachMenuProps && hasAttachButton && ((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, Object.assign({ onAttachAccepted: handleAttach, isDisabled: isListeningMessage, tooltipContent: (_g = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _g === void 0 ? void 0 : _g.tooltipContent, inputTestId: (_h = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _h === void 0 ? void 0 : _h.inputTestId, isCompact: isCompact, tooltipProps: (_j = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _j === void 0 ? void 0 : _j.tooltipProps, allowedFileTypes: allowedFileTypes }, (_k = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _k === void 0 ? void 0 : _k.props))), hasMicrophoneButton && ((0, jsx_runtime_1.jsx)(MicrophoneButton_1.default, Object.assign({ isListening: isListeningMessage, onIsListeningChange: setIsListeningMessage, onSpeechRecognition: handleSpeechRecognition, tooltipContent: (_l = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _l === void 0 ? void 0 : _l.tooltipContent, language: (_m = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _m === void 0 ? void 0 : _m.language, isCompact: isCompact, tooltipProps: (_o = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _o === void 0 ? void 0 : _o.tooltipProps }, (_p = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _p === void 0 ? void 0 : _p.props))), (alwayShowSendButton || message) && ((0, jsx_runtime_1.jsx)(SendButton_1.default, Object.assign({ value: message, onClick: () => handleSend(message), isDisabled: isSendButtonDisabled, tooltipContent: (_q = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _q === void 0 ? void 0 : _q.tooltipContent, isCompact: isCompact, tooltipProps: (_r = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _r === void 0 ? void 0 : _r.tooltipProps }, (_s = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _s === void 0 ? void 0 : _s.props)))] }));
185
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [attachMenuProps && ((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, Object.assign({ ref: attachButtonRef, onClick: handleAttachMenuToggle, isDisabled: isListeningMessage, tooltipContent: (_d = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _d === void 0 ? void 0 : _d.tooltipContent, isCompact: isCompact, tooltipProps: (_e = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _e === void 0 ? void 0 : _e.tooltipProps, allowedFileTypes: allowedFileTypes, minSize: minSize, maxSize: maxSize, maxFiles: maxFiles, isAttachmentDisabled: isAttachmentDisabled, onAttach: onAttach, onAttachRejected: onAttachRejected, validator: validator, dropzoneProps: dropzoneProps }, (_f = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _f === void 0 ? void 0 : _f.props))), !attachMenuProps && hasAttachButton && ((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, Object.assign({ onAttachAccepted: handleAttach, isDisabled: isListeningMessage, tooltipContent: (_g = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _g === void 0 ? void 0 : _g.tooltipContent, inputTestId: (_h = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _h === void 0 ? void 0 : _h.inputTestId, isCompact: isCompact, tooltipProps: (_j = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _j === void 0 ? void 0 : _j.tooltipProps, allowedFileTypes: allowedFileTypes, minSize: minSize, maxSize: maxSize, maxFiles: maxFiles, isAttachmentDisabled: isAttachmentDisabled, onAttach: onAttach, onAttachRejected: onAttachRejected, validator: validator, dropzoneProps: dropzoneProps }, (_k = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _k === void 0 ? void 0 : _k.props))), hasMicrophoneButton && ((0, jsx_runtime_1.jsx)(MicrophoneButton_1.default, Object.assign({ isListening: isListeningMessage, onIsListeningChange: setIsListeningMessage, onSpeechRecognition: handleSpeechRecognition, tooltipContent: (_l = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _l === void 0 ? void 0 : _l.tooltipContent, language: (_m = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _m === void 0 ? void 0 : _m.language, isCompact: isCompact, tooltipProps: (_o = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _o === void 0 ? void 0 : _o.tooltipProps }, (_p = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.microphone) === null || _p === void 0 ? void 0 : _p.props))), (alwayShowSendButton || message) && ((0, jsx_runtime_1.jsx)(SendButton_1.default, Object.assign({ value: message, onClick: () => handleSend(message), isDisabled: isSendButtonDisabled, tooltipContent: (_q = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _q === void 0 ? void 0 : _q.tooltipContent, isCompact: isCompact, tooltipProps: (_r = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _r === void 0 ? void 0 : _r.tooltipProps }, (_s = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.send) === null || _s === void 0 ? void 0 : _s.props)))] }));
186
186
  };
187
187
  const messageBarContents = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { className: `pf-chatbot__message-bar-input ${isCompact ? 'pf-m-compact' : ''}`, children: (0, jsx_runtime_1.jsx)(react_core_1.TextArea, Object.assign({ className: "pf-chatbot__message-textarea", value: message, onChange: handleChange, "aria-label": isListeningMessage ? listeningText : placeholder, placeholder: isListeningMessage ? listeningText : placeholder, ref: textareaRef, onKeyDown: handleKeyDown }, props)) }), (0, jsx_runtime_1.jsx)("div", { className: "pf-chatbot__message-bar-actions", children: renderButtons() })] }));
188
188
  if (attachMenuProps) {
@@ -23,11 +23,35 @@ const ResponseActionButton_1 = __importDefault(require("./ResponseActionButton")
23
23
  const ResponseActions = ({ actions }) => {
24
24
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
25
25
  const [activeButton, setActiveButton] = (0, react_2.useState)();
26
+ const [clickStatePersisted, setClickStatePersisted] = (0, react_2.useState)(false);
27
+ (0, react_2.useEffect)(() => {
28
+ // Define the order of precedence for checking initial `isClicked`
29
+ const actionPrecedence = ['positive', 'negative', 'copy', 'share', 'download', 'listen'];
30
+ let initialActive;
31
+ // Check predefined actions first based on precedence
32
+ for (const actionName of actionPrecedence) {
33
+ const actionProp = actions[actionName];
34
+ if (actionProp === null || actionProp === void 0 ? void 0 : actionProp.isClicked) {
35
+ initialActive = actionName;
36
+ break;
37
+ }
38
+ }
39
+ // If no predefined action was initially clicked, check additionalActions
40
+ if (!initialActive) {
41
+ const clickedActionName = Object.keys(additionalActions).find((actionName) => { var _a; return !actionPrecedence.includes(actionName) && ((_a = additionalActions[actionName]) === null || _a === void 0 ? void 0 : _a.isClicked); });
42
+ initialActive = clickedActionName;
43
+ }
44
+ if (initialActive) {
45
+ // Click state is explicitly controlled by consumer.
46
+ setClickStatePersisted(true);
47
+ }
48
+ setActiveButton(initialActive);
49
+ }, [actions]);
26
50
  const { positive, negative, copy, share, download, listen } = actions, additionalActions = __rest(actions, ["positive", "negative", "copy", "share", "download", "listen"]);
27
51
  const responseActions = (0, react_2.useRef)(null);
28
52
  (0, react_2.useEffect)(() => {
29
53
  const handleClickOutside = (e) => {
30
- if (responseActions.current && !responseActions.current.contains(e.target)) {
54
+ if (responseActions.current && !responseActions.current.contains(e.target) && !clickStatePersisted) {
31
55
  setActiveButton(undefined);
32
56
  }
33
57
  };
@@ -35,8 +59,9 @@ const ResponseActions = ({ actions }) => {
35
59
  return () => {
36
60
  window.removeEventListener('click', handleClickOutside);
37
61
  };
38
- }, []);
62
+ }, [clickStatePersisted]);
39
63
  const handleClick = (e, id, onClick) => {
64
+ setClickStatePersisted(false);
40
65
  setActiveButton(id);
41
66
  onClick && onClick(e);
42
67
  };
@@ -116,6 +116,66 @@ describe('ResponseActions', () => {
116
116
  expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
117
117
  expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
118
118
  }));
119
+ it('should handle isClicked prop within group of buttons correctly', () => __awaiter(void 0, void 0, void 0, function* () {
120
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: {
121
+ positive: { 'data-testid': 'positive-btn', onClick: jest.fn(), isClicked: true },
122
+ negative: { 'data-testid': 'negative-btn', onClick: jest.fn() }
123
+ } }));
124
+ expect(react_1.screen.getByTestId('positive-btn')).toHaveClass('pf-chatbot__button--response-action-clicked');
125
+ expect(react_1.screen.getByTestId('negative-btn')).not.toHaveClass('pf-chatbot__button--response-action-clicked');
126
+ }));
127
+ it('should set "listen" button as active if its `isClicked` is true', () => __awaiter(void 0, void 0, void 0, function* () {
128
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: {
129
+ positive: { 'data-testid': 'positive-btn', onClick: jest.fn(), isClicked: false },
130
+ negative: { 'data-testid': 'negative-btn', onClick: jest.fn(), isClicked: false },
131
+ listen: { 'data-testid': 'listen-btn', onClick: jest.fn(), isClicked: true }
132
+ } }));
133
+ expect(react_1.screen.getByTestId('listen-btn')).toHaveClass('pf-chatbot__button--response-action-clicked');
134
+ expect(react_1.screen.getByTestId('positive-btn')).not.toHaveClass('pf-chatbot__button--response-action-clicked');
135
+ expect(react_1.screen.getByTestId('negative-btn')).not.toHaveClass('pf-chatbot__button--response-action-clicked');
136
+ }));
137
+ it('should prioritize "positive" when both "positive" and "negative" are set to clicked', () => __awaiter(void 0, void 0, void 0, function* () {
138
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: {
139
+ positive: { 'data-testid': 'positive-btn', onClick: jest.fn(), isClicked: true },
140
+ negative: { 'data-testid': 'negative-btn', onClick: jest.fn(), isClicked: true }
141
+ } }));
142
+ // Positive button should take precendence
143
+ expect(react_1.screen.getByTestId('positive-btn')).toHaveClass('pf-chatbot__button--response-action-clicked');
144
+ expect(react_1.screen.getByTestId('negative-btn')).not.toHaveClass('pf-chatbot__button--response-action-clicked');
145
+ }));
146
+ it('should set an additional action button as active if it is initially clicked and no predefined are clicked', () => __awaiter(void 0, void 0, void 0, function* () {
147
+ const [additionalActions] = CUSTOM_ACTIONS;
148
+ const customActions = Object.assign({ positive: { 'data-testid': 'positive', onClick: jest.fn(), isClicked: false }, negative: { 'data-testid': 'negative', onClick: jest.fn(), isClicked: false } }, Object.keys(additionalActions).reduce((acc, actionKey) => {
149
+ acc[actionKey] = Object.assign(Object.assign({}, additionalActions[actionKey]), { 'data-testid': actionKey, isClicked: actionKey === 'regenerate' });
150
+ return acc;
151
+ }, {}));
152
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: customActions }));
153
+ Object.keys(customActions).forEach((actionKey) => {
154
+ if (actionKey === 'regenerate') {
155
+ expect(react_1.screen.getByTestId(actionKey)).toHaveClass('pf-chatbot__button--response-action-clicked');
156
+ }
157
+ else {
158
+ // Other actions should not have clicked class
159
+ expect(react_1.screen.getByTestId(actionKey)).not.toHaveClass('pf-chatbot__button--response-action-clicked');
160
+ }
161
+ });
162
+ }));
163
+ it('should activate the clicked button and deactivate any previously active button', () => __awaiter(void 0, void 0, void 0, function* () {
164
+ const actions = {
165
+ positive: { 'data-testid': 'positive', onClick: jest.fn(), isClicked: false },
166
+ negative: { 'data-testid': 'negative', onClick: jest.fn(), isClicked: true }
167
+ };
168
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: actions }));
169
+ const negativeBtn = react_1.screen.getByTestId('negative');
170
+ const positiveBtn = react_1.screen.getByTestId('positive');
171
+ // negative button is initially clicked
172
+ expect(negativeBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
173
+ expect(positiveBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
174
+ yield user_event_1.default.click(positiveBtn);
175
+ // positive button should now have the clicked class
176
+ expect(positiveBtn).toHaveClass('pf-chatbot__button--response-action-clicked');
177
+ expect(negativeBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
178
+ }));
119
179
  it('should render buttons correctly', () => {
120
180
  ALL_ACTIONS.forEach(({ type, label }) => {
121
181
  (0, react_1.render)((0, jsx_runtime_1.jsx)(ResponseActions_1.default, { actions: { [type]: { onClick: jest.fn() } } }));
@@ -1,7 +1,7 @@
1
1
  import { DropEvent } from '@patternfly/react-core';
2
2
  import type { FunctionComponent } from 'react';
3
3
  import { ChatbotDisplayMode } from '../Chatbot';
4
- import { Accept } from 'react-dropzone/.';
4
+ import { Accept, FileError, FileRejection } from 'react-dropzone/.';
5
5
  export interface FileDropZoneProps {
6
6
  /** Content displayed when the drop zone is not currently in use */
7
7
  children?: React.ReactNode;
@@ -19,6 +19,20 @@ export interface FileDropZoneProps {
19
19
  allowedFileTypes?: Accept;
20
20
  /** Display mode for the Chatbot parent; this influences the styles applied */
21
21
  displayMode?: ChatbotDisplayMode;
22
+ /** Minimum file size allowed */
23
+ minSize?: number;
24
+ /** Max file size allowed */
25
+ maxSize?: number;
26
+ /** Max number of files allowed */
27
+ maxFiles?: number;
28
+ /** Whether attachments are disabled */
29
+ isAttachmentDisabled?: boolean;
30
+ /** Callback when file(s) are attached */
31
+ onAttach?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
32
+ /** Callback function for AttachButton when an attachment fails */
33
+ onAttachRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
34
+ /** Validator for files; see https://react-dropzone.js.org/#!/Custom%20validation for more information */
35
+ validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
22
36
  }
23
37
  declare const FileDropZone: FunctionComponent<FileDropZoneProps>;
24
38
  export default FileDropZone;
@@ -15,9 +15,14 @@ import { useState } from 'react';
15
15
  import { ChatbotDisplayMode } from '../Chatbot';
16
16
  import { UploadIcon } from '@patternfly/react-icons';
17
17
  const FileDropZone = (_a) => {
18
- var { children, className, infoText = 'Maximum file size is 25 MB', onFileDrop, allowedFileTypes, displayMode = ChatbotDisplayMode.default } = _a, props = __rest(_a, ["children", "className", "infoText", "onFileDrop", "allowedFileTypes", "displayMode"]);
18
+ var { children, className, infoText = 'Maximum file size is 25 MB', onFileDrop, allowedFileTypes, minSize, maxSize, maxFiles, isAttachmentDisabled, onAttach, onAttachRejected, validator, displayMode = ChatbotDisplayMode.default } = _a, props = __rest(_a, ["children", "className", "infoText", "onFileDrop", "allowedFileTypes", "minSize", "maxSize", "maxFiles", "isAttachmentDisabled", "onAttach", "onAttachRejected", "validator", "displayMode"]);
19
19
  const [showDropZone, setShowDropZone] = useState(false);
20
20
  const renderDropZone = () => (_jsx(_Fragment, { children: _jsx(MultipleFileUploadMain, { titleIcon: _jsx(UploadIcon, {}), titleText: "Drag and drop your file here", infoText: infoText, isUploadButtonHidden: true }) }));
21
- return (_jsx(MultipleFileUpload, { dropzoneProps: Object.assign({ accept: allowedFileTypes, onDrop: () => setShowDropZone(false) }, props), onDragEnter: () => setShowDropZone(true), onDragLeave: () => setShowDropZone(false), onFileDrop: onFileDrop, className: `pf-chatbot__dropzone pf-chatbot__dropzone--${displayMode} pf-chatbot__dropzone--${showDropZone ? 'visible' : 'invisible'} ${className ? className : ''}`, children: showDropZone ? renderDropZone() : children }));
21
+ return (_jsx(MultipleFileUpload, { dropzoneProps: Object.assign({ accept: allowedFileTypes, onDrop: (acceptedFiles, fileRejections, event) => {
22
+ setShowDropZone(false);
23
+ onAttach && onAttach(acceptedFiles, fileRejections, event);
24
+ }, minSize,
25
+ maxSize,
26
+ maxFiles, disabled: isAttachmentDisabled, onDropRejected: onAttachRejected, validator }, props), onDragEnter: () => setShowDropZone(true), onDragLeave: () => setShowDropZone(false), onFileDrop: onFileDrop, className: `pf-chatbot__dropzone pf-chatbot__dropzone--${displayMode} pf-chatbot__dropzone--${showDropZone ? 'visible' : 'invisible'} ${className ? className : ''}`, children: showDropZone ? renderDropZone() : children }));
22
27
  };
23
28
  export default FileDropZone;
@@ -37,4 +37,59 @@ describe('FileDropZone', () => {
37
37
  yield userEvent.upload(fileInput, file);
38
38
  expect(onFileDrop).not.toHaveBeenCalled();
39
39
  }));
40
+ it('should respect minSize restriction', () => __awaiter(void 0, void 0, void 0, function* () {
41
+ const onAttachRejected = jest.fn();
42
+ const { container } = render(_jsx(FileDropZone, { onFileDrop: jest.fn(), minSize: 1000, onAttachRejected: onAttachRejected }));
43
+ const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
44
+ const fileInput = container.querySelector('input[type="file"]');
45
+ yield userEvent.upload(fileInput, file);
46
+ expect(onAttachRejected).toHaveBeenCalled();
47
+ }));
48
+ it('should respect maxSize restriction', () => __awaiter(void 0, void 0, void 0, function* () {
49
+ const onAttachRejected = jest.fn();
50
+ const { container } = render(_jsx(FileDropZone, { onFileDrop: jest.fn(), maxSize: 100, onAttachRejected: onAttachRejected }));
51
+ const largeContent = 'x'.repeat(200);
52
+ const file = new File([largeContent], 'example.txt', { type: 'text/plain' });
53
+ const fileInput = container.querySelector('input[type="file"]');
54
+ yield userEvent.upload(fileInput, file);
55
+ expect(onAttachRejected).toHaveBeenCalled();
56
+ }));
57
+ it('should respect maxFiles restriction', () => __awaiter(void 0, void 0, void 0, function* () {
58
+ const onAttachRejected = jest.fn();
59
+ const { container } = render(_jsx(FileDropZone, { onFileDrop: jest.fn(), maxFiles: 1, onAttachRejected: onAttachRejected }));
60
+ const files = [
61
+ new File(['Test1'], 'example1.txt', { type: 'text/plain' }),
62
+ new File(['Test2'], 'example2.txt', { type: 'text/plain' })
63
+ ];
64
+ const fileInput = container.querySelector('input[type="file"]');
65
+ yield userEvent.upload(fileInput, files);
66
+ expect(onAttachRejected).toHaveBeenCalled();
67
+ }));
68
+ it('should be disabled when isAttachmentDisabled is true', () => __awaiter(void 0, void 0, void 0, function* () {
69
+ const onFileDrop = jest.fn();
70
+ const { container } = render(_jsx(FileDropZone, { onFileDrop: onFileDrop, isAttachmentDisabled: true }));
71
+ const file = new File(['Test'], 'example.text', { type: 'text/plain' });
72
+ const fileInput = container.querySelector('input[type="file"]');
73
+ yield userEvent.upload(fileInput, file);
74
+ expect(onFileDrop).not.toHaveBeenCalled();
75
+ }));
76
+ it('should call onAttach when files are attached', () => __awaiter(void 0, void 0, void 0, function* () {
77
+ const onAttach = jest.fn();
78
+ const { container } = render(_jsx(FileDropZone, { onFileDrop: jest.fn(), onAttach: onAttach }));
79
+ const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
80
+ const fileInput = container.querySelector('input[type="file"]');
81
+ yield userEvent.upload(fileInput, file);
82
+ expect(onAttach).toHaveBeenCalled();
83
+ }));
84
+ it('should use custom validator when provided', () => __awaiter(void 0, void 0, void 0, function* () {
85
+ const validator = jest.fn().mockReturnValue({ message: 'Custom error' });
86
+ const onAttachRejected = jest.fn();
87
+ const onFileDrop = jest.fn();
88
+ const { container } = render(_jsx(FileDropZone, { onFileDrop: onFileDrop, validator: validator, onAttachRejected: onAttachRejected }));
89
+ const file = new File(['Test'], 'example.txt', { type: 'text/plain' });
90
+ const fileInput = container.querySelector('input[type="file"]');
91
+ yield userEvent.upload(fileInput, file);
92
+ expect(validator).toHaveBeenCalledWith(file);
93
+ expect(onAttachRejected).toHaveBeenCalled();
94
+ }));
40
95
  });
@@ -1,5 +1,5 @@
1
1
  import { ButtonProps, DropEvent, TooltipProps } from '@patternfly/react-core';
2
- import { Accept } from 'react-dropzone';
2
+ import { Accept, DropzoneOptions, FileError, FileRejection } from 'react-dropzone';
3
3
  export interface AttachButtonProps extends ButtonProps {
4
4
  /** Callback for when button is clicked */
5
5
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
@@ -23,6 +23,23 @@ export interface AttachButtonProps extends ButtonProps {
23
23
  tooltipContent?: string;
24
24
  /** Test id applied to input */
25
25
  inputTestId?: string;
26
+ /** Whether button is compact */
26
27
  isCompact?: boolean;
28
+ /** Minimum file size allowed */
29
+ minSize?: number;
30
+ /** Max file size allowed */
31
+ maxSize?: number;
32
+ /** Max number of files allowed */
33
+ maxFiles?: number;
34
+ /** Whether attachments are disabled */
35
+ isAttachmentDisabled?: boolean;
36
+ /** Callback when file(s) are attached */
37
+ onAttach?: <T extends File>(acceptedFiles: T[], fileRejections: FileRejection[], event: DropEvent) => void;
38
+ /** Callback function for AttachButton when an attachment fails */
39
+ onAttachRejected?: (fileRejections: FileRejection[], event: DropEvent) => void;
40
+ /** Validator for files; see https://react-dropzone.js.org/#!/Custom%20validation for more information */
41
+ validator?: <T extends File>(file: T) => FileError | readonly FileError[] | null;
42
+ /** Additional props passed to react-dropzone */
43
+ dropzoneProps?: DropzoneOptions;
27
44
  }
28
45
  export declare const AttachButton: import("react").ForwardRefExoticComponent<AttachButtonProps & import("react").RefAttributes<any>>;
@@ -16,12 +16,10 @@ import { Button, Icon, Tooltip } from '@patternfly/react-core';
16
16
  import { useDropzone } from 'react-dropzone';
17
17
  import { PaperclipIcon } from '@patternfly/react-icons/dist/esm/icons/paperclip-icon';
18
18
  const AttachButtonBase = (_a) => {
19
- var { onAttachAccepted, onClick, isDisabled, className, tooltipProps, innerRef, tooltipContent = 'Attach', inputTestId, isCompact, allowedFileTypes } = _a, props = __rest(_a, ["onAttachAccepted", "onClick", "isDisabled", "className", "tooltipProps", "innerRef", "tooltipContent", "inputTestId", "isCompact", "allowedFileTypes"]);
20
- const { open, getInputProps } = useDropzone({
21
- multiple: true,
22
- onDropAccepted: onAttachAccepted,
23
- accept: allowedFileTypes
24
- });
19
+ var { onAttachAccepted, onClick, isDisabled, className, tooltipProps, innerRef, tooltipContent = 'Attach', inputTestId, isCompact, allowedFileTypes, minSize, maxSize, maxFiles, isAttachmentDisabled, onAttach, onAttachRejected, validator, dropzoneProps } = _a, props = __rest(_a, ["onAttachAccepted", "onClick", "isDisabled", "className", "tooltipProps", "innerRef", "tooltipContent", "inputTestId", "isCompact", "allowedFileTypes", "minSize", "maxSize", "maxFiles", "isAttachmentDisabled", "onAttach", "onAttachRejected", "validator", "dropzoneProps"]);
20
+ const { open, getInputProps } = useDropzone(Object.assign({ multiple: true, onDropAccepted: onAttachAccepted, accept: allowedFileTypes, minSize,
21
+ maxSize,
22
+ maxFiles, disabled: isAttachmentDisabled, onDrop: onAttach, onDropRejected: onAttachRejected, validator }, dropzoneProps));
25
23
  return (_jsxs(_Fragment, { children: [_jsx("input", Object.assign({ "data-testid": inputTestId }, getInputProps(), { hidden: true })), _jsx(Tooltip, Object.assign({ id: "pf-chatbot__tooltip--attach", content: tooltipContent, position: "top", entryDelay: (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.entryDelay) || 0, exitDelay: (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.exitDelay) || 0, distance: (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.distance) || 8, animationDuration: (tooltipProps === null || tooltipProps === void 0 ? void 0 : tooltipProps.animationDuration) || 0,
26
24
  // prevents VO announcements of both aria label and tooltip
27
25
  aria: "none" }, tooltipProps, { children: _jsx(Button, Object.assign({ variant: "plain", ref: innerRef, className: `pf-chatbot__button--attach ${isCompact ? 'pf-m-compact' : ''} ${className !== null && className !== void 0 ? className : ''}`, "aria-label": props['aria-label'] || 'Attach', isDisabled: isDisabled, onClick: onClick !== null && onClick !== void 0 ? onClick : open, icon: _jsx(Icon, { iconSize: isCompact ? 'lg' : 'xl', isInline: true, children: _jsx(PaperclipIcon, {}) }), size: isCompact ? 'sm' : undefined }, props)) }))] }));