@patternfly/chatbot 6.3.0-prerelease.18 → 6.3.0-prerelease.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +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
5
  export interface FileDropZoneProps {
5
6
  /** Content displayed when the drop zone is not currently in use */
6
7
  children?: React.ReactNode;
@@ -10,6 +11,12 @@ export interface FileDropZoneProps {
10
11
  infoText?: string;
11
12
  /** When files are dropped or uploaded this callback will be called with all accepted files */
12
13
  onFileDrop: (event: DropEvent, data: File[]) => void;
14
+ /** Specifies the file types accepted by the attachment upload component.
15
+ * Files that don't match the accepted types will be disabled in the file picker.
16
+ * For example,
17
+ * allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
18
+ **/
19
+ allowedFileTypes?: Accept;
13
20
  /** Display mode for the Chatbot parent; this influences the styles applied */
14
21
  displayMode?: ChatbotDisplayMode;
15
22
  }
@@ -17,9 +17,9 @@ 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, displayMode = Chatbot_1.ChatbotDisplayMode.default } = _a, props = __rest(_a, ["children", "className", "infoText", "onFileDrop", "displayMode"]);
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"]);
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({ 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: () => 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 }));
24
24
  };
25
25
  exports.default = FileDropZone;
@@ -1,4 +1,13 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
13
  };
@@ -7,6 +16,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
7
16
  const react_1 = require("@testing-library/react");
8
17
  require("@testing-library/jest-dom");
9
18
  const FileDropZone_1 = __importDefault(require("./FileDropZone"));
19
+ const user_event_1 = __importDefault(require("@testing-library/user-event"));
10
20
  describe('FileDropZone', () => {
11
21
  it('should render file drop zone', () => {
12
22
  const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { onFileDrop: jest.fn() }));
@@ -16,4 +26,20 @@ describe('FileDropZone', () => {
16
26
  (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { onFileDrop: jest.fn(), children: "Hi" }));
17
27
  expect(react_1.screen.getByText('Hi')).toBeTruthy();
18
28
  });
29
+ it('should call onFileDrop when file type is accepted', () => __awaiter(void 0, void 0, void 0, function* () {
30
+ const onFileDrop = jest.fn();
31
+ const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { "data-testid": "input", allowedFileTypes: { 'text/plain': ['.txt'] }, onFileDrop: onFileDrop }));
32
+ const file = new File(['Test'], 'example.text', { type: 'text/plain' });
33
+ const fileInput = container.querySelector('input[type="file"]');
34
+ yield user_event_1.default.upload(fileInput, file);
35
+ expect(onFileDrop).toHaveBeenCalled();
36
+ }));
37
+ it('should not call onFileDrop when file type is not accepted', () => __awaiter(void 0, void 0, void 0, function* () {
38
+ const onFileDrop = jest.fn();
39
+ const { container } = (0, react_1.render)((0, jsx_runtime_1.jsx)(FileDropZone_1.default, { "data-testid": "input", allowedFileTypes: { 'text/plain': ['.txt'] }, onFileDrop: onFileDrop }));
40
+ const file = new File(['[]'], 'example.json', { type: 'application/json' });
41
+ const fileInput = container.querySelector('input[type="file"]');
42
+ yield user_event_1.default.upload(fileInput, file);
43
+ expect(onFileDrop).not.toHaveBeenCalled();
44
+ }));
19
45
  });
@@ -1,9 +1,16 @@
1
1
  import { ButtonProps, DropEvent, TooltipProps } from '@patternfly/react-core';
2
+ import { Accept } from 'react-dropzone';
2
3
  export interface AttachButtonProps extends ButtonProps {
3
4
  /** Callback for when button is clicked */
4
5
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
5
6
  /** Callback function for AttachButton when an attachment is made */
6
7
  onAttachAccepted?: (data: File[], event: DropEvent) => void;
8
+ /** Specifies the file types accepted by the attachment upload component.
9
+ * Files that don't match the accepted types will be disabled in the file picker.
10
+ * For example,
11
+ * allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
12
+ **/
13
+ allowedFileTypes?: Accept;
7
14
  /** Class name for AttachButton */
8
15
  className?: string;
9
16
  /** Props to control if the AttachButton should be disabled */
@@ -19,10 +19,11 @@ 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 } = _a, props = __rest(_a, ["onAttachAccepted", "onClick", "isDisabled", "className", "tooltipProps", "innerRef", "tooltipContent", "inputTestId", "isCompact"]);
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
23
  const { open, getInputProps } = (0, react_dropzone_1.useDropzone)({
24
24
  multiple: true,
25
- onDropAccepted: onAttachAccepted
25
+ onDropAccepted: onAttachAccepted,
26
+ accept: allowedFileTypes
26
27
  });
27
28
  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,
28
29
  // prevents VO announcements of both aria label and tooltip
@@ -67,4 +67,28 @@ describe('Attach button', () => {
67
67
  (0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { isCompact: true, "data-testid": "button" }));
68
68
  expect(react_1.screen.getByTestId('button')).toHaveClass('pf-m-compact');
69
69
  });
70
+ it('should set correct accept attribute on file input', () => __awaiter(void 0, void 0, void 0, function* () {
71
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", allowedFileTypes: { 'text/plain': ['.txt'] } }));
72
+ yield user_event_1.default.click(react_1.screen.getByRole('button', { name: 'Attach' }));
73
+ const input = react_1.screen.getByTestId('input');
74
+ expect(input).toHaveAttribute('accept', 'text/plain,.txt');
75
+ }));
76
+ it('should call onAttachAccepted when file type is accepted', () => __awaiter(void 0, void 0, void 0, function* () {
77
+ const onAttachAccepted = jest.fn();
78
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", allowedFileTypes: { 'text/plain': ['.txt'] }, onAttachAccepted: onAttachAccepted }));
79
+ const file = new File(['hello'], 'example.txt', { type: 'text/plain' });
80
+ const input = react_1.screen.getByTestId('input');
81
+ yield user_event_1.default.upload(input, file);
82
+ expect(onAttachAccepted).toHaveBeenCalled();
83
+ const [attachedFile] = onAttachAccepted.mock.calls[0][0];
84
+ expect(attachedFile).toEqual(file);
85
+ }));
86
+ it('should not call onAttachAccepted when file type is not accepted', () => __awaiter(void 0, void 0, void 0, function* () {
87
+ const onAttachAccepted = jest.fn();
88
+ (0, react_1.render)((0, jsx_runtime_1.jsx)(AttachButton_1.AttachButton, { inputTestId: "input", allowedFileTypes: { 'text/plain': ['.txt'] }, onAttachAccepted: onAttachAccepted }));
89
+ const file = new File(['[]'], 'example.json', { type: 'application/json' });
90
+ const input = react_1.screen.getByTestId('input');
91
+ yield user_event_1.default.upload(input, file);
92
+ expect(onAttachAccepted).not.toHaveBeenCalled();
93
+ }));
70
94
  });
@@ -1,4 +1,5 @@
1
1
  import type { FunctionComponent } from 'react';
2
+ import { Accept } from 'react-dropzone/.';
2
3
  import { ButtonProps, DropEvent, TextAreaProps, TooltipProps } from '@patternfly/react-core';
3
4
  import { ChatbotDisplayMode } from '../Chatbot';
4
5
  export interface MessageBarWithAttachMenuProps {
@@ -79,6 +80,12 @@ export interface MessageBarProps extends TextAreaProps {
79
80
  /** Display mode of chatbot, if you want to message bar to resize when the display mode changes */
80
81
  displayMode?: ChatbotDisplayMode;
81
82
  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;
82
89
  }
83
90
  export declare const MessageBar: FunctionComponent<MessageBarProps>;
84
91
  export default MessageBar;
@@ -25,7 +25,7 @@ const AttachButton_1 = require("./AttachButton");
25
25
  const AttachMenu_1 = __importDefault(require("../AttachMenu"));
26
26
  const StopButton_1 = __importDefault(require("./StopButton"));
27
27
  const MessageBar = (_a) => {
28
- var { onSendMessage, className, alwayShowSendButton, placeholder = 'Send a message...', hasAttachButton = true, hasMicrophoneButton, listeningText = 'Listening', handleAttach, attachMenuProps, isSendButtonDisabled, handleStopButton, hasStopButton, buttonProps, onChange, displayMode, value, isCompact = false } = _a, props = __rest(_a, ["onSendMessage", "className", "alwayShowSendButton", "placeholder", "hasAttachButton", "hasMicrophoneButton", "listeningText", "handleAttach", "attachMenuProps", "isSendButtonDisabled", "handleStopButton", "hasStopButton", "buttonProps", "onChange", "displayMode", "value", "isCompact"]);
28
+ 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 } = _a, props = __rest(_a, ["onSendMessage", "className", "alwayShowSendButton", "placeholder", "hasAttachButton", "hasMicrophoneButton", "listeningText", "handleAttach", "attachMenuProps", "isSendButtonDisabled", "handleStopButton", "hasStopButton", "buttonProps", "onChange", "displayMode", "value", "isCompact", "allowedFileTypes"]);
29
29
  // Text Input
30
30
  // --------------------------------------------------------------------------
31
31
  const [message, setMessage] = (0, react_1.useState)(value !== null && value !== void 0 ? value : '');
@@ -180,7 +180,7 @@ const MessageBar = (_a) => {
180
180
  if (hasStopButton && handleStopButton) {
181
181
  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)));
182
182
  }
183
- 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 }, (_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 }, (_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)))] }));
183
+ 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)))] }));
184
184
  };
185
185
  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() })] }));
186
186
  if (attachMenuProps) {
@@ -1,6 +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
5
  export interface FileDropZoneProps {
5
6
  /** Content displayed when the drop zone is not currently in use */
6
7
  children?: React.ReactNode;
@@ -10,6 +11,12 @@ export interface FileDropZoneProps {
10
11
  infoText?: string;
11
12
  /** When files are dropped or uploaded this callback will be called with all accepted files */
12
13
  onFileDrop: (event: DropEvent, data: File[]) => void;
14
+ /** Specifies the file types accepted by the attachment upload component.
15
+ * Files that don't match the accepted types will be disabled in the file picker.
16
+ * For example,
17
+ * allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
18
+ **/
19
+ allowedFileTypes?: Accept;
13
20
  /** Display mode for the Chatbot parent; this influences the styles applied */
14
21
  displayMode?: ChatbotDisplayMode;
15
22
  }
@@ -15,9 +15,9 @@ 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, displayMode = ChatbotDisplayMode.default } = _a, props = __rest(_a, ["children", "className", "infoText", "onFileDrop", "displayMode"]);
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"]);
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({ 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: () => 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 }));
22
22
  };
23
23
  export default FileDropZone;
@@ -1,7 +1,17 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
1
10
  import { jsx as _jsx } from "react/jsx-runtime";
2
11
  import { render, screen } from '@testing-library/react';
3
12
  import '@testing-library/jest-dom';
4
13
  import FileDropZone from './FileDropZone';
14
+ import userEvent from '@testing-library/user-event';
5
15
  describe('FileDropZone', () => {
6
16
  it('should render file drop zone', () => {
7
17
  const { container } = render(_jsx(FileDropZone, { onFileDrop: jest.fn() }));
@@ -11,4 +21,20 @@ describe('FileDropZone', () => {
11
21
  render(_jsx(FileDropZone, { onFileDrop: jest.fn(), children: "Hi" }));
12
22
  expect(screen.getByText('Hi')).toBeTruthy();
13
23
  });
24
+ it('should call onFileDrop when file type is accepted', () => __awaiter(void 0, void 0, void 0, function* () {
25
+ const onFileDrop = jest.fn();
26
+ const { container } = render(_jsx(FileDropZone, { "data-testid": "input", allowedFileTypes: { 'text/plain': ['.txt'] }, onFileDrop: onFileDrop }));
27
+ const file = new File(['Test'], 'example.text', { type: 'text/plain' });
28
+ const fileInput = container.querySelector('input[type="file"]');
29
+ yield userEvent.upload(fileInput, file);
30
+ expect(onFileDrop).toHaveBeenCalled();
31
+ }));
32
+ it('should not call onFileDrop when file type is not accepted', () => __awaiter(void 0, void 0, void 0, function* () {
33
+ const onFileDrop = jest.fn();
34
+ const { container } = render(_jsx(FileDropZone, { "data-testid": "input", allowedFileTypes: { 'text/plain': ['.txt'] }, onFileDrop: onFileDrop }));
35
+ const file = new File(['[]'], 'example.json', { type: 'application/json' });
36
+ const fileInput = container.querySelector('input[type="file"]');
37
+ yield userEvent.upload(fileInput, file);
38
+ expect(onFileDrop).not.toHaveBeenCalled();
39
+ }));
14
40
  });
@@ -1,9 +1,16 @@
1
1
  import { ButtonProps, DropEvent, TooltipProps } from '@patternfly/react-core';
2
+ import { Accept } from 'react-dropzone';
2
3
  export interface AttachButtonProps extends ButtonProps {
3
4
  /** Callback for when button is clicked */
4
5
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
5
6
  /** Callback function for AttachButton when an attachment is made */
6
7
  onAttachAccepted?: (data: File[], event: DropEvent) => void;
8
+ /** Specifies the file types accepted by the attachment upload component.
9
+ * Files that don't match the accepted types will be disabled in the file picker.
10
+ * For example,
11
+ * allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
12
+ **/
13
+ allowedFileTypes?: Accept;
7
14
  /** Class name for AttachButton */
8
15
  className?: string;
9
16
  /** Props to control if the AttachButton should be disabled */
@@ -16,10 +16,11 @@ 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 } = _a, props = __rest(_a, ["onAttachAccepted", "onClick", "isDisabled", "className", "tooltipProps", "innerRef", "tooltipContent", "inputTestId", "isCompact"]);
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
20
  const { open, getInputProps } = useDropzone({
21
21
  multiple: true,
22
- onDropAccepted: onAttachAccepted
22
+ onDropAccepted: onAttachAccepted,
23
+ accept: allowedFileTypes
23
24
  });
24
25
  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,
25
26
  // prevents VO announcements of both aria label and tooltip
@@ -62,4 +62,28 @@ describe('Attach button', () => {
62
62
  render(_jsx(AttachButton, { isCompact: true, "data-testid": "button" }));
63
63
  expect(screen.getByTestId('button')).toHaveClass('pf-m-compact');
64
64
  });
65
+ it('should set correct accept attribute on file input', () => __awaiter(void 0, void 0, void 0, function* () {
66
+ render(_jsx(AttachButton, { inputTestId: "input", allowedFileTypes: { 'text/plain': ['.txt'] } }));
67
+ yield userEvent.click(screen.getByRole('button', { name: 'Attach' }));
68
+ const input = screen.getByTestId('input');
69
+ expect(input).toHaveAttribute('accept', 'text/plain,.txt');
70
+ }));
71
+ it('should call onAttachAccepted when file type is accepted', () => __awaiter(void 0, void 0, void 0, function* () {
72
+ const onAttachAccepted = jest.fn();
73
+ render(_jsx(AttachButton, { inputTestId: "input", allowedFileTypes: { 'text/plain': ['.txt'] }, onAttachAccepted: onAttachAccepted }));
74
+ const file = new File(['hello'], 'example.txt', { type: 'text/plain' });
75
+ const input = screen.getByTestId('input');
76
+ yield userEvent.upload(input, file);
77
+ expect(onAttachAccepted).toHaveBeenCalled();
78
+ const [attachedFile] = onAttachAccepted.mock.calls[0][0];
79
+ expect(attachedFile).toEqual(file);
80
+ }));
81
+ it('should not call onAttachAccepted when file type is not accepted', () => __awaiter(void 0, void 0, void 0, function* () {
82
+ const onAttachAccepted = jest.fn();
83
+ render(_jsx(AttachButton, { inputTestId: "input", allowedFileTypes: { 'text/plain': ['.txt'] }, onAttachAccepted: onAttachAccepted }));
84
+ const file = new File(['[]'], 'example.json', { type: 'application/json' });
85
+ const input = screen.getByTestId('input');
86
+ yield userEvent.upload(input, file);
87
+ expect(onAttachAccepted).not.toHaveBeenCalled();
88
+ }));
65
89
  });
@@ -1,4 +1,5 @@
1
1
  import type { FunctionComponent } from 'react';
2
+ import { Accept } from 'react-dropzone/.';
2
3
  import { ButtonProps, DropEvent, TextAreaProps, TooltipProps } from '@patternfly/react-core';
3
4
  import { ChatbotDisplayMode } from '../Chatbot';
4
5
  export interface MessageBarWithAttachMenuProps {
@@ -79,6 +80,12 @@ export interface MessageBarProps extends TextAreaProps {
79
80
  /** Display mode of chatbot, if you want to message bar to resize when the display mode changes */
80
81
  displayMode?: ChatbotDisplayMode;
81
82
  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;
82
89
  }
83
90
  export declare const MessageBar: FunctionComponent<MessageBarProps>;
84
91
  export default MessageBar;
@@ -19,7 +19,7 @@ import { AttachButton } from './AttachButton';
19
19
  import AttachMenu from '../AttachMenu';
20
20
  import StopButton from './StopButton';
21
21
  export const MessageBar = (_a) => {
22
- var { onSendMessage, className, alwayShowSendButton, placeholder = 'Send a message...', hasAttachButton = true, hasMicrophoneButton, listeningText = 'Listening', handleAttach, attachMenuProps, isSendButtonDisabled, handleStopButton, hasStopButton, buttonProps, onChange, displayMode, value, isCompact = false } = _a, props = __rest(_a, ["onSendMessage", "className", "alwayShowSendButton", "placeholder", "hasAttachButton", "hasMicrophoneButton", "listeningText", "handleAttach", "attachMenuProps", "isSendButtonDisabled", "handleStopButton", "hasStopButton", "buttonProps", "onChange", "displayMode", "value", "isCompact"]);
22
+ 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 } = _a, props = __rest(_a, ["onSendMessage", "className", "alwayShowSendButton", "placeholder", "hasAttachButton", "hasMicrophoneButton", "listeningText", "handleAttach", "attachMenuProps", "isSendButtonDisabled", "handleStopButton", "hasStopButton", "buttonProps", "onChange", "displayMode", "value", "isCompact", "allowedFileTypes"]);
23
23
  // Text Input
24
24
  // --------------------------------------------------------------------------
25
25
  const [message, setMessage] = useState(value !== null && value !== void 0 ? value : '');
@@ -174,7 +174,7 @@ export const MessageBar = (_a) => {
174
174
  if (hasStopButton && handleStopButton) {
175
175
  return (_jsx(StopButton, 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)));
176
176
  }
177
- return (_jsxs(_Fragment, { children: [attachMenuProps && (_jsx(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 }, (_f = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _f === void 0 ? void 0 : _f.props))), !attachMenuProps && hasAttachButton && (_jsx(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 }, (_k = buttonProps === null || buttonProps === void 0 ? void 0 : buttonProps.attach) === null || _k === void 0 ? void 0 : _k.props))), hasMicrophoneButton && (_jsx(MicrophoneButton, 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) && (_jsx(SendButton, 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)))] }));
177
+ return (_jsxs(_Fragment, { children: [attachMenuProps && (_jsx(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 && (_jsx(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 && (_jsx(MicrophoneButton, 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) && (_jsx(SendButton, 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)))] }));
178
178
  };
179
179
  const messageBarContents = (_jsxs(_Fragment, { children: [_jsx("div", { className: `pf-chatbot__message-bar-input ${isCompact ? 'pf-m-compact' : ''}`, children: _jsx(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)) }), _jsx("div", { className: "pf-chatbot__message-bar-actions", children: renderButtons() })] }));
180
180
  if (attachMenuProps) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patternfly/chatbot",
3
- "version": "6.3.0-prerelease.18",
3
+ "version": "6.3.0-prerelease.19",
4
4
  "description": "This library provides React components based on PatternFly 6 that can be used to build chatbots.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -218,7 +218,16 @@ export const BasicDemo: FunctionComponent = () => {
218
218
  </ChatbotHeaderOptionsDropdown>
219
219
  </ChatbotHeaderActions>
220
220
  </ChatbotHeader>
221
- <FileDropZone onFileDrop={handleFileDrop} displayMode={displayMode}>
221
+ <FileDropZone
222
+ onFileDrop={handleFileDrop}
223
+ displayMode={displayMode}
224
+ infoText="Allowed file types are .json, .txt and .yaml and maximum file size is 25 MB."
225
+ allowedFileTypes={{
226
+ 'text/plain': ['.txt'],
227
+ 'application/json': ['.json'],
228
+ 'application/yaml': ['.yaml', '.yml']
229
+ }}
230
+ >
222
231
  <ChatbotContent>
223
232
  <MessageBox>
224
233
  {showAlert && (
@@ -1,6 +1,7 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
  import '@testing-library/jest-dom';
3
3
  import FileDropZone from './FileDropZone';
4
+ import userEvent from '@testing-library/user-event';
4
5
 
5
6
  describe('FileDropZone', () => {
6
7
  it('should render file drop zone', () => {
@@ -11,4 +12,32 @@ describe('FileDropZone', () => {
11
12
  render(<FileDropZone onFileDrop={jest.fn()}>Hi</FileDropZone>);
12
13
  expect(screen.getByText('Hi')).toBeTruthy();
13
14
  });
15
+
16
+ it('should call onFileDrop when file type is accepted', async () => {
17
+ const onFileDrop = jest.fn();
18
+ const { container } = render(
19
+ <FileDropZone data-testid="input" allowedFileTypes={{ 'text/plain': ['.txt'] }} onFileDrop={onFileDrop} />
20
+ );
21
+
22
+ const file = new File(['Test'], 'example.text', { type: 'text/plain' });
23
+ const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
24
+
25
+ await userEvent.upload(fileInput, file);
26
+
27
+ expect(onFileDrop).toHaveBeenCalled();
28
+ });
29
+
30
+ it('should not call onFileDrop when file type is not accepted', async () => {
31
+ const onFileDrop = jest.fn();
32
+ const { container } = render(
33
+ <FileDropZone data-testid="input" allowedFileTypes={{ 'text/plain': ['.txt'] }} onFileDrop={onFileDrop} />
34
+ );
35
+
36
+ const file = new File(['[]'], 'example.json', { type: 'application/json' });
37
+ const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
38
+
39
+ await userEvent.upload(fileInput, file);
40
+
41
+ expect(onFileDrop).not.toHaveBeenCalled();
42
+ });
14
43
  });
@@ -3,6 +3,7 @@ import type { FunctionComponent } from 'react';
3
3
  import { useState } from 'react';
4
4
  import { ChatbotDisplayMode } from '../Chatbot';
5
5
  import { UploadIcon } from '@patternfly/react-icons';
6
+ import { Accept } from 'react-dropzone/.';
6
7
 
7
8
  export interface FileDropZoneProps {
8
9
  /** Content displayed when the drop zone is not currently in use */
@@ -13,6 +14,12 @@ export interface FileDropZoneProps {
13
14
  infoText?: string;
14
15
  /** When files are dropped or uploaded this callback will be called with all accepted files */
15
16
  onFileDrop: (event: DropEvent, data: File[]) => void;
17
+ /** Specifies the file types accepted by the attachment upload component.
18
+ * Files that don't match the accepted types will be disabled in the file picker.
19
+ * For example,
20
+ * allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
21
+ **/
22
+ allowedFileTypes?: Accept;
16
23
  /** Display mode for the Chatbot parent; this influences the styles applied */
17
24
  displayMode?: ChatbotDisplayMode;
18
25
  }
@@ -22,6 +29,7 @@ const FileDropZone: FunctionComponent<FileDropZoneProps> = ({
22
29
  className,
23
30
  infoText = 'Maximum file size is 25 MB',
24
31
  onFileDrop,
32
+ allowedFileTypes,
25
33
  displayMode = ChatbotDisplayMode.default,
26
34
  ...props
27
35
  }: FileDropZoneProps) => {
@@ -41,6 +49,7 @@ const FileDropZone: FunctionComponent<FileDropZoneProps> = ({
41
49
  return (
42
50
  <MultipleFileUpload
43
51
  dropzoneProps={{
52
+ accept: allowedFileTypes,
44
53
  onDrop: () => setShowDropZone(false),
45
54
  ...props
46
55
  }}
@@ -53,4 +53,49 @@ describe('Attach button', () => {
53
53
  render(<AttachButton isCompact data-testid="button" />);
54
54
  expect(screen.getByTestId('button')).toHaveClass('pf-m-compact');
55
55
  });
56
+
57
+ it('should set correct accept attribute on file input', async () => {
58
+ render(<AttachButton inputTestId="input" allowedFileTypes={{ 'text/plain': ['.txt'] }} />);
59
+ await userEvent.click(screen.getByRole('button', { name: 'Attach' }));
60
+ const input = screen.getByTestId('input') as HTMLInputElement;
61
+ expect(input).toHaveAttribute('accept', 'text/plain,.txt');
62
+ });
63
+
64
+ it('should call onAttachAccepted when file type is accepted', async () => {
65
+ const onAttachAccepted = jest.fn();
66
+ render(
67
+ <AttachButton
68
+ inputTestId="input"
69
+ allowedFileTypes={{ 'text/plain': ['.txt'] }}
70
+ onAttachAccepted={onAttachAccepted}
71
+ />
72
+ );
73
+
74
+ const file = new File(['hello'], 'example.txt', { type: 'text/plain' });
75
+ const input = screen.getByTestId('input');
76
+
77
+ await userEvent.upload(input, file);
78
+
79
+ expect(onAttachAccepted).toHaveBeenCalled();
80
+ const [attachedFile] = onAttachAccepted.mock.calls[0][0];
81
+ expect(attachedFile).toEqual(file);
82
+ });
83
+
84
+ it('should not call onAttachAccepted when file type is not accepted', async () => {
85
+ const onAttachAccepted = jest.fn();
86
+ render(
87
+ <AttachButton
88
+ inputTestId="input"
89
+ allowedFileTypes={{ 'text/plain': ['.txt'] }}
90
+ onAttachAccepted={onAttachAccepted}
91
+ />
92
+ );
93
+
94
+ const file = new File(['[]'], 'example.json', { type: 'application/json' });
95
+ const input = screen.getByTestId('input');
96
+
97
+ await userEvent.upload(input, file);
98
+
99
+ expect(onAttachAccepted).not.toHaveBeenCalled();
100
+ });
56
101
  });
@@ -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 { useDropzone } from 'react-dropzone';
10
+ import { Accept, 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 {
@@ -15,6 +15,12 @@ export interface AttachButtonProps extends ButtonProps {
15
15
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
16
16
  /** Callback function for AttachButton when an attachment is made */
17
17
  onAttachAccepted?: (data: File[], event: DropEvent) => void;
18
+ /** Specifies the file types accepted by the attachment upload component.
19
+ * Files that don't match the accepted types will be disabled in the file picker.
20
+ * For example,
21
+ * allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
22
+ **/
23
+ allowedFileTypes?: Accept;
18
24
  /** Class name for AttachButton */
19
25
  className?: string;
20
26
  /** Props to control if the AttachButton should be disabled */
@@ -40,11 +46,13 @@ const AttachButtonBase: FunctionComponent<AttachButtonProps> = ({
40
46
  tooltipContent = 'Attach',
41
47
  inputTestId,
42
48
  isCompact,
49
+ allowedFileTypes,
43
50
  ...props
44
51
  }: AttachButtonProps) => {
45
52
  const { open, getInputProps } = useDropzone({
46
53
  multiple: true,
47
- onDropAccepted: onAttachAccepted
54
+ onDropAccepted: onAttachAccepted,
55
+ accept: allowedFileTypes
48
56
  });
49
57
 
50
58
  return (
@@ -1,5 +1,6 @@
1
1
  import type { ChangeEvent, FunctionComponent, KeyboardEvent as ReactKeyboardEvent } from 'react';
2
2
  import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { Accept } from 'react-dropzone/.';
3
4
  import { ButtonProps, DropEvent, TextArea, TextAreaProps, TooltipProps } from '@patternfly/react-core';
4
5
 
5
6
  // Import Chatbot components
@@ -78,6 +79,12 @@ export interface MessageBarProps extends TextAreaProps {
78
79
  /** Display mode of chatbot, if you want to message bar to resize when the display mode changes */
79
80
  displayMode?: ChatbotDisplayMode;
80
81
  isCompact?: boolean;
82
+ /** Specifies the file types accepted by the attachment upload component.
83
+ * Files that don't match the accepted types will be disabled in the file picker.
84
+ * For example,
85
+ * allowedFileTypes: { 'application/json': ['.json'], 'text/plain': ['.txt'] }
86
+ **/
87
+ allowedFileTypes?: Accept;
81
88
  }
82
89
 
83
90
  export const MessageBar: FunctionComponent<MessageBarProps> = ({
@@ -98,6 +105,7 @@ export const MessageBar: FunctionComponent<MessageBarProps> = ({
98
105
  displayMode,
99
106
  value,
100
107
  isCompact = false,
108
+ allowedFileTypes,
101
109
  ...props
102
110
  }: MessageBarProps) => {
103
111
  // Text Input
@@ -295,6 +303,7 @@ export const MessageBar: FunctionComponent<MessageBarProps> = ({
295
303
  tooltipContent={buttonProps?.attach?.tooltipContent}
296
304
  isCompact={isCompact}
297
305
  tooltipProps={buttonProps?.attach?.tooltipProps}
306
+ allowedFileTypes={allowedFileTypes}
298
307
  {...buttonProps?.attach?.props}
299
308
  />
300
309
  )}
@@ -306,6 +315,7 @@ export const MessageBar: FunctionComponent<MessageBarProps> = ({
306
315
  inputTestId={buttonProps?.attach?.inputTestId}
307
316
  isCompact={isCompact}
308
317
  tooltipProps={buttonProps?.attach?.tooltipProps}
318
+ allowedFileTypes={allowedFileTypes}
309
319
  {...buttonProps?.attach?.props}
310
320
  />
311
321
  )}