Skip to content

Commit 475bcea

Browse files
fix(MessageBar/FileDropZone): Allow specific file types in attachment upload (#564)
Also adjusted child component attach button. Menu implementation demo was left as-is since react-dropzone is fully user-configurable there.
1 parent 06c496d commit 475bcea

File tree

6 files changed

+113
-3
lines changed

6 files changed

+113
-3
lines changed

packages/module/patternfly-docs/content/extensions/chatbot/examples/demos/ChatbotAttachment.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,16 @@ export const BasicDemo: FunctionComponent = () => {
218218
</ChatbotHeaderOptionsDropdown>
219219
</ChatbotHeaderActions>
220220
</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+
>
222231
<ChatbotContent>
223232
<MessageBox>
224233
{showAlert && (

packages/module/src/FileDropZone/FileDropZone.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { render, screen } from '@testing-library/react';
22
import '@testing-library/jest-dom';
33
import FileDropZone from './FileDropZone';
4+
import userEvent from '@testing-library/user-event';
45

56
describe('FileDropZone', () => {
67
it('should render file drop zone', () => {
@@ -11,4 +12,32 @@ describe('FileDropZone', () => {
1112
render(<FileDropZone onFileDrop={jest.fn()}>Hi</FileDropZone>);
1213
expect(screen.getByText('Hi')).toBeTruthy();
1314
});
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+
});
1443
});

packages/module/src/FileDropZone/FileDropZone.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { FunctionComponent } from 'react';
33
import { useState } from 'react';
44
import { ChatbotDisplayMode } from '../Chatbot';
55
import { UploadIcon } from '@patternfly/react-icons';
6+
import { Accept } from 'react-dropzone/.';
67

78
export interface FileDropZoneProps {
89
/** Content displayed when the drop zone is not currently in use */
@@ -13,6 +14,12 @@ export interface FileDropZoneProps {
1314
infoText?: string;
1415
/** When files are dropped or uploaded this callback will be called with all accepted files */
1516
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;
1623
/** Display mode for the Chatbot parent; this influences the styles applied */
1724
displayMode?: ChatbotDisplayMode;
1825
}
@@ -22,6 +29,7 @@ const FileDropZone: FunctionComponent<FileDropZoneProps> = ({
2229
className,
2330
infoText = 'Maximum file size is 25 MB',
2431
onFileDrop,
32+
allowedFileTypes,
2533
displayMode = ChatbotDisplayMode.default,
2634
...props
2735
}: FileDropZoneProps) => {
@@ -41,6 +49,7 @@ const FileDropZone: FunctionComponent<FileDropZoneProps> = ({
4149
return (
4250
<MultipleFileUpload
4351
dropzoneProps={{
52+
accept: allowedFileTypes,
4453
onDrop: () => setShowDropZone(false),
4554
...props
4655
}}

packages/module/src/MessageBar/AttachButton.test.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,49 @@ describe('Attach button', () => {
5353
render(<AttachButton isCompact data-testid="button" />);
5454
expect(screen.getByTestId('button')).toHaveClass('pf-m-compact');
5555
});
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+
});
56101
});

packages/module/src/MessageBar/AttachButton.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,20 @@ import { forwardRef } from 'react';
77

88
// Import PatternFly components
99
import { Button, ButtonProps, DropEvent, Icon, Tooltip, TooltipProps } from '@patternfly/react-core';
10-
import { useDropzone } from 'react-dropzone';
10+
import { Accept, useDropzone } from 'react-dropzone';
1111
import { PaperclipIcon } from '@patternfly/react-icons/dist/esm/icons/paperclip-icon';
1212

1313
export interface AttachButtonProps extends ButtonProps {
1414
/** Callback for when button is clicked */
1515
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
1616
/** Callback function for AttachButton when an attachment is made */
1717
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;
1824
/** Class name for AttachButton */
1925
className?: string;
2026
/** Props to control if the AttachButton should be disabled */
@@ -40,11 +46,13 @@ const AttachButtonBase: FunctionComponent<AttachButtonProps> = ({
4046
tooltipContent = 'Attach',
4147
inputTestId,
4248
isCompact,
49+
allowedFileTypes,
4350
...props
4451
}: AttachButtonProps) => {
4552
const { open, getInputProps } = useDropzone({
4653
multiple: true,
47-
onDropAccepted: onAttachAccepted
54+
onDropAccepted: onAttachAccepted,
55+
accept: allowedFileTypes
4856
});
4957

5058
return (

packages/module/src/MessageBar/MessageBar.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ChangeEvent, FunctionComponent, KeyboardEvent as ReactKeyboardEvent } from 'react';
22
import { useCallback, useEffect, useRef, useState } from 'react';
3+
import { Accept } from 'react-dropzone/.';
34
import { ButtonProps, DropEvent, TextArea, TextAreaProps, TooltipProps } from '@patternfly/react-core';
45

56
// Import Chatbot components
@@ -78,6 +79,12 @@ export interface MessageBarProps extends TextAreaProps {
7879
/** Display mode of chatbot, if you want to message bar to resize when the display mode changes */
7980
displayMode?: ChatbotDisplayMode;
8081
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;
8188
}
8289

8390
export const MessageBar: FunctionComponent<MessageBarProps> = ({
@@ -98,6 +105,7 @@ export const MessageBar: FunctionComponent<MessageBarProps> = ({
98105
displayMode,
99106
value,
100107
isCompact = false,
108+
allowedFileTypes,
101109
...props
102110
}: MessageBarProps) => {
103111
// Text Input
@@ -295,6 +303,7 @@ export const MessageBar: FunctionComponent<MessageBarProps> = ({
295303
tooltipContent={buttonProps?.attach?.tooltipContent}
296304
isCompact={isCompact}
297305
tooltipProps={buttonProps?.attach?.tooltipProps}
306+
allowedFileTypes={allowedFileTypes}
298307
{...buttonProps?.attach?.props}
299308
/>
300309
)}
@@ -306,6 +315,7 @@ export const MessageBar: FunctionComponent<MessageBarProps> = ({
306315
inputTestId={buttonProps?.attach?.inputTestId}
307316
isCompact={isCompact}
308317
tooltipProps={buttonProps?.attach?.tooltipProps}
318+
allowedFileTypes={allowedFileTypes}
309319
{...buttonProps?.attach?.props}
310320
/>
311321
)}

0 commit comments

Comments
 (0)