Skip to content

Commit 40999a1

Browse files
authored
feat(FileUpload) Add new beta File Upload component (#3865)
* feat(FileUpload) Add new beta File Upload component * Bump patternfly versions to 2.68.3
1 parent 2aac12f commit 40999a1

File tree

25 files changed

+837
-17
lines changed

25 files changed

+837
-17
lines changed

.eslintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"env": {
33
"browser": true,
4-
"node": true
4+
"node": true,
5+
"es6": true
56
},
67
"plugins": [
78
"@typescript-eslint",

packages/react-catalog-view-extension/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"develop": "yarn build:babel:esm --skip-initial-build --watch --verbose --source-maps"
4141
},
4242
"dependencies": {
43-
"@patternfly/patternfly": "2.66.0",
43+
"@patternfly/patternfly": "2.68.3",
4444
"@patternfly/react-core": "^3.145.1",
4545
"@patternfly/react-styles": "^3.7.7",
4646
"classnames": "^2.2.5",

packages/react-charts/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
},
3030
"homepage": "https://github.com/patternfly/patternfly-react#readme",
3131
"dependencies": {
32-
"@patternfly/patternfly": "2.66.0",
32+
"@patternfly/patternfly": "2.68.3",
3333
"@patternfly/react-styles": "^3.7.7",
3434
"@patternfly/react-tokens": "^2.8.7",
3535
"hoist-non-react-statics": "^3.3.0",

packages/react-core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"emotion": "^9.2.9",
4747
"exenv": "^1.2.2",
4848
"focus-trap-react": "^4.0.1",
49+
"react-dropzone": "9.0.0",
4950
"tippy.js": "5.1.2"
5051
},
5152
"devDependencies": {
@@ -57,7 +58,7 @@
5758
"@babel/plugin-transform-typescript": "^7.0.0",
5859
"@babel/preset-env": "^7.0.0",
5960
"@babel/preset-react": "^7.0.0",
60-
"@patternfly/patternfly": "2.66.0",
61+
"@patternfly/patternfly": "2.68.3",
6162
"@types/exenv": "^1.2.0",
6263
"@types/jest": "^24.0.11",
6364
"@types/react": "^16.4.0",
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import * as React from 'react';
2+
import Dropzone, { DropzoneProps, DropFileEventHandler } from 'react-dropzone';
3+
import { Omit } from '../../helpers';
4+
import { FileUploadField, FileUploadFieldProps } from './FileUploadField';
5+
import { readFile, fileReaderType } from '../../helpers/fileUtils';
6+
7+
export interface FileUploadProps
8+
extends Omit<
9+
FileUploadFieldProps,
10+
'children' | 'onBrowseButtonClick' | 'onClearButtonClick' | 'isDragActive' | 'containerRef'
11+
> {
12+
/** Unique id for the TextArea, also used to generate ids for accessible labels. */
13+
id: string;
14+
/** What type of file. Determines what is is passed to `onChange` and expected by `value`
15+
* (a string for 'text' and 'dataURL', or a File object otherwise. */
16+
type?: 'text' | 'dataURL';
17+
/** Value of the file's contents
18+
* (string if text file, File object otherwise) */
19+
value?: string | File;
20+
/** Value to be shown in the read-only filename field. */
21+
filename?: string;
22+
/** A callback for when the file contents change. */
23+
onChange?: (
24+
value: string | File,
25+
filename: string,
26+
event:
27+
| React.DragEvent<HTMLElement> // User dragged/dropped a file
28+
| React.ChangeEvent<HTMLTextAreaElement> // User typed in the TextArea
29+
| React.MouseEvent<HTMLButtonElement, MouseEvent> // User clicked Clear button
30+
) => void;
31+
/** Additional classes added to the FileUpload container element. */
32+
className?: string;
33+
/** Flag to show if the field is disabled. */
34+
isDisabled?: boolean;
35+
/** Flag to show if the field is read only. */
36+
isReadOnly?: boolean;
37+
/** Flag to show if a file is being loaded. */
38+
isLoading?: boolean;
39+
/** Aria-valuetext for the loading spinner */
40+
spinnerAriaValueText?: string;
41+
/** Flag to show if the field is required. */
42+
isRequired?: boolean;
43+
/* Value to indicate if the field is modified to show that validation state.
44+
* If set to success, field will be modified to indicate valid state.
45+
* If set to error, field will be modified to indicate error state.
46+
*/
47+
validated?: 'success' | 'error' | 'default';
48+
/** Aria-label for the TextArea. */
49+
'aria-label'?: string;
50+
/** Placeholder string to display in the empty filename field */
51+
filenamePlaceholder?: string;
52+
/** Aria-label for the read-only filename field */
53+
filenameAriaLabel?: string;
54+
/** Text for the Browse button */
55+
browseButtonText?: string;
56+
/** Text for the Clear button */
57+
clearButtonText?: string;
58+
/** Flag to hide the built-in preview of the file (where available).
59+
* If true, you can use children to render an alternate preview. */
60+
hideDefaultPreview?: boolean;
61+
/** Flag to allow editing of a text file's contents after it is selected from disk */
62+
allowEditingUploadedText?: boolean;
63+
/** Additional children to render after (or instead of) the file preview. */
64+
children?: React.ReactNode;
65+
66+
// Props available in FileUpload but not FileUploadField:
67+
68+
/** A callback for when a selected file starts loading */
69+
onReadStarted?: (fileHandle: File) => void;
70+
/** A callback for when a selected file finishes loading */
71+
onReadFinished?: (fileHandle: File) => void;
72+
/** A callback for when the FileReader API fails */
73+
onReadFailed?: (error: DOMException, fileHandle: File) => void;
74+
/** Optional extra props to customize react-dropzone. */
75+
dropzoneProps?: DropzoneProps;
76+
}
77+
78+
export const FileUpload: React.FunctionComponent<FileUploadProps> = ({
79+
id,
80+
type,
81+
value = type === fileReaderType.text || type === fileReaderType.dataURL ? '' : null,
82+
filename = '',
83+
children = null,
84+
onChange = () => {},
85+
onReadStarted = () => {},
86+
onReadFinished = () => {},
87+
onReadFailed = () => {},
88+
dropzoneProps = {},
89+
...props
90+
}: FileUploadProps) => {
91+
const onDropAccepted: DropFileEventHandler = (acceptedFiles: File[], event: React.DragEvent<HTMLElement>) => {
92+
if (acceptedFiles.length > 0) {
93+
const fileHandle = acceptedFiles[0];
94+
if (type === fileReaderType.text || type === fileReaderType.dataURL) {
95+
onChange('', fileHandle.name, event); // Show the filename while reading
96+
onReadStarted(fileHandle);
97+
readFile(fileHandle, type as fileReaderType)
98+
.then(data => {
99+
onReadFinished(fileHandle);
100+
onChange(data as string, fileHandle.name, event);
101+
})
102+
.catch((error: DOMException) => {
103+
onReadFailed(error, fileHandle);
104+
onReadFinished(fileHandle);
105+
onChange('', '', event); // Clear the filename field on a failure
106+
});
107+
} else {
108+
onChange(fileHandle, fileHandle.name, event);
109+
}
110+
}
111+
dropzoneProps.onDropAccepted && dropzoneProps.onDropAccepted(acceptedFiles, event);
112+
};
113+
114+
const onDropRejected: DropFileEventHandler = (rejectedFiles: File[], event: React.DragEvent<HTMLElement>) => {
115+
if (rejectedFiles.length > 0) {
116+
onChange('', rejectedFiles[0].name, event);
117+
}
118+
dropzoneProps.onDropRejected && dropzoneProps.onDropRejected(rejectedFiles, event);
119+
};
120+
121+
const onClearButtonClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
122+
onChange('', '', event);
123+
};
124+
125+
return (
126+
<Dropzone multiple={false} {...dropzoneProps} onDropAccepted={onDropAccepted} onDropRejected={onDropRejected}>
127+
{({ getRootProps, getInputProps, isDragActive, open }) => (
128+
<FileUploadField
129+
{...getRootProps({
130+
...props,
131+
refKey: 'containerRef',
132+
onClick: event => event.preventDefault() // Prevents clicking TextArea from opening file dialog
133+
})}
134+
tabIndex={null} // Omit the unwanted tabIndex from react-dropzone's getRootProps
135+
id={id}
136+
type={type}
137+
filename={filename}
138+
value={value}
139+
onChange={onChange}
140+
isDragActive={isDragActive}
141+
onBrowseButtonClick={open}
142+
onClearButtonClick={onClearButtonClick}
143+
>
144+
<input {...getInputProps()} /* hidden, necessary for react-dropzone */ />
145+
{children}
146+
</FileUploadField>
147+
)}
148+
</Dropzone>
149+
);
150+
};
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import * as React from 'react';
2+
import styles from '@patternfly/react-styles/css/components/FileUpload/file-upload';
3+
import { css } from '@patternfly/react-styles';
4+
import { Omit } from '../../helpers';
5+
import { InputGroup } from '../InputGroup';
6+
import { TextInput } from '../TextInput';
7+
import { Button, ButtonVariant } from '../Button';
8+
import { TextArea, TextAreResizeOrientation } from '../TextArea';
9+
import { Spinner, spinnerSize } from '../Spinner';
10+
import { fileReaderType } from '../../helpers/fileUtils';
11+
12+
export interface FileUploadFieldProps extends Omit<React.HTMLProps<HTMLDivElement>, 'value' | 'onChange'> {
13+
/** Unique id for the TextArea, also used to generate ids for accessible labels */
14+
id: string;
15+
/** What type of file. Determines what is is expected by `value`
16+
* (a string for 'text' and 'dataURL', or a File object otherwise). */
17+
type?: 'text' | 'dataURL';
18+
/** Value of the file's contents
19+
* (string if text file, File object otherwise) */
20+
value?: string | File;
21+
/** Value to be shown in the read-only filename field. */
22+
filename?: string;
23+
/** A callback for when the TextArea value changes. */
24+
onChange?: (
25+
value: string,
26+
filename: string,
27+
event:
28+
| React.ChangeEvent<HTMLTextAreaElement> // User typed in the TextArea
29+
| React.MouseEvent<HTMLButtonElement, MouseEvent> // User clicked Clear button
30+
) => void;
31+
/** Additional classes added to the FileUploadField container element. */
32+
className?: string;
33+
/** Flag to show if the field is disabled. */
34+
isDisabled?: boolean;
35+
/** Flag to show if the field is read only. */
36+
isReadOnly?: boolean;
37+
/** Flag to show if a file is being loaded. */
38+
isLoading?: boolean;
39+
/** Aria-valuetext for the loading spinner */
40+
spinnerAriaValueText?: string;
41+
/** Flag to show if the field is required. */
42+
isRequired?: boolean;
43+
/* Value to indicate if the field is modified to show that validation state.
44+
* If set to success, field will be modified to indicate valid state.
45+
* If set to error, field will be modified to indicate error state.
46+
*/
47+
validated?: 'success' | 'error' | 'default';
48+
/** Aria-label for the TextArea. */
49+
'aria-label'?: string;
50+
/** Placeholder string to display in the empty filename field */
51+
filenamePlaceholder?: string;
52+
/** Aria-label for the read-only filename field */
53+
filenameAriaLabel?: string;
54+
/** Text for the Browse button */
55+
browseButtonText?: string;
56+
/** Text for the Clear button */
57+
clearButtonText?: string;
58+
/** Flag to disable the Clear button */
59+
isClearButtonDisabled?: boolean;
60+
/** Flag to hide the built-in preview of the file (where available).
61+
* If true, you can use children to render an alternate preview. */
62+
hideDefaultPreview?: boolean;
63+
/** Flag to allow editing of a text file's contents after it is selected from disk */
64+
allowEditingUploadedText?: boolean;
65+
/** Additional children to render after (or instead of) the file preview. */
66+
children?: React.ReactNode;
67+
68+
// Props available in FileUploadField but not FileUpload:
69+
70+
/** A callback for when the Browse button is clicked. */
71+
onBrowseButtonClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
72+
/** A callback for when the Clear button is clicked. */
73+
onClearButtonClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
74+
/** Flag to show if a file is being dragged over the field */
75+
isDragActive?: boolean;
76+
/** A reference object to attach to the FileUploadField container element. */
77+
containerRef?: React.Ref<HTMLDivElement>;
78+
}
79+
80+
export const FileUploadField: React.FunctionComponent<FileUploadFieldProps> = ({
81+
id,
82+
type,
83+
value = '',
84+
filename = '',
85+
onChange = () => {},
86+
onBrowseButtonClick = () => {},
87+
onClearButtonClick = () => {},
88+
className = '',
89+
isDisabled = false,
90+
isReadOnly = false,
91+
isLoading = false,
92+
spinnerAriaValueText,
93+
isRequired = false,
94+
isDragActive = false,
95+
validated = 'default' as 'success' | 'error' | 'default',
96+
'aria-label': ariaLabel = 'File upload',
97+
filenamePlaceholder = 'Drag a file here or browse to upload',
98+
filenameAriaLabel = filename ? 'Read only filename' : filenamePlaceholder,
99+
browseButtonText = 'Browse...',
100+
clearButtonText = 'Clear',
101+
isClearButtonDisabled = !filename && !value,
102+
containerRef = null as React.Ref<HTMLDivElement>,
103+
allowEditingUploadedText = false,
104+
hideDefaultPreview = false,
105+
children = null,
106+
...props
107+
}: FileUploadFieldProps) => {
108+
const onTextAreaChange = (newValue: string, event: React.ChangeEvent<HTMLTextAreaElement>) => {
109+
onChange(newValue, filename, event);
110+
};
111+
112+
return (
113+
<div
114+
className={css(
115+
styles.fileUpload,
116+
isDragActive && styles.modifiers.dragHover,
117+
isLoading && styles.modifiers.loading,
118+
className
119+
)}
120+
ref={containerRef}
121+
{...props}
122+
>
123+
<div className={styles.fileUploadFileSelect}>
124+
<InputGroup>
125+
<TextInput
126+
isReadOnly // Always read-only regardless of isReadOnly prop (which is just for the TextArea)
127+
isDisabled={isDisabled}
128+
id={`${id}-filename`}
129+
name={`${id}-filename`}
130+
aria-label={filenameAriaLabel}
131+
placeholder={filenamePlaceholder}
132+
aria-describedby={`${id}-browse-button`}
133+
value={filename}
134+
/>
135+
<Button
136+
id={`${id}-browse-button`}
137+
variant={ButtonVariant.control}
138+
onClick={onBrowseButtonClick}
139+
isDisabled={isDisabled}
140+
>
141+
{browseButtonText}
142+
</Button>
143+
<Button
144+
variant={ButtonVariant.control}
145+
isDisabled={isDisabled || isClearButtonDisabled}
146+
onClick={onClearButtonClick}
147+
>
148+
{clearButtonText}
149+
</Button>
150+
</InputGroup>
151+
</div>
152+
<div className={styles.fileUploadFileDetails}>
153+
{!hideDefaultPreview && type === fileReaderType.text && (
154+
<TextArea
155+
readOnly={isReadOnly || (!!filename && !allowEditingUploadedText)}
156+
disabled={isDisabled}
157+
isRequired={isRequired}
158+
resizeOrientation={TextAreResizeOrientation.vertical}
159+
validated={validated}
160+
id={id}
161+
name={id}
162+
aria-label={ariaLabel}
163+
value={value as string}
164+
onChange={onTextAreaChange}
165+
/>
166+
)}
167+
{isLoading && (
168+
<div className={styles.fileUploadFileDetailsSpinner}>
169+
<Spinner size={spinnerSize.lg} aria-valuetext={spinnerAriaValueText} />
170+
</div>
171+
)}
172+
</div>
173+
{children}
174+
</div>
175+
);
176+
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { FileUpload } from '../FileUpload';
2+
import * as React from 'react';
3+
import { shallow } from 'enzyme';
4+
5+
test('simple fileupload', () => {
6+
const changeHandler = jest.fn();
7+
const readStartedHandler = jest.fn();
8+
const readFinishedHandler = jest.fn();
9+
10+
const view = shallow(<FileUpload
11+
id="simple-text-file"
12+
type="text"
13+
value={''}
14+
filename={''}
15+
onChange={changeHandler}
16+
onReadStarted={readStartedHandler}
17+
onReadFinished={readFinishedHandler}
18+
isLoading={false}
19+
/>);
20+
expect(view).toMatchSnapshot();
21+
});
22+

0 commit comments

Comments
 (0)