Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"env": {
"browser": true,
"node": true
"node": true,
"es6": true
},
"plugins": [
"@typescript-eslint",
Expand Down
2 changes: 1 addition & 1 deletion packages/react-catalog-view-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"develop": "yarn build:babel:esm --skip-initial-build --watch --verbose --source-maps"
},
"dependencies": {
"@patternfly/patternfly": "2.66.0",
"@patternfly/patternfly": "2.68.3",
"@patternfly/react-core": "^3.145.1",
"@patternfly/react-styles": "^3.7.7",
"classnames": "^2.2.5",
Expand Down
2 changes: 1 addition & 1 deletion packages/react-charts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
},
"homepage": "https://github.com/patternfly/patternfly-react#readme",
"dependencies": {
"@patternfly/patternfly": "2.66.0",
"@patternfly/patternfly": "2.68.3",
"@patternfly/react-styles": "^3.7.7",
"@patternfly/react-tokens": "^2.8.7",
"hoist-non-react-statics": "^3.3.0",
Expand Down
3 changes: 2 additions & 1 deletion packages/react-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"emotion": "^9.2.9",
"exenv": "^1.2.2",
"focus-trap-react": "^4.0.1",
"react-dropzone": "9.0.0",
"tippy.js": "5.1.2"
},
"devDependencies": {
Expand All @@ -57,7 +58,7 @@
"@babel/plugin-transform-typescript": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@patternfly/patternfly": "2.66.0",
"@patternfly/patternfly": "2.68.3",
"@types/exenv": "^1.2.0",
"@types/jest": "^24.0.11",
"@types/react": "^16.4.0",
Expand Down
150 changes: 150 additions & 0 deletions packages/react-core/src/components/FileUpload/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import * as React from 'react';
import Dropzone, { DropzoneProps, DropFileEventHandler } from 'react-dropzone';
import { Omit } from '../../helpers';
import { FileUploadField, FileUploadFieldProps } from './FileUploadField';
import { readFile, fileReaderType } from '../../helpers/fileUtils';

export interface FileUploadProps
extends Omit<
FileUploadFieldProps,
'children' | 'onBrowseButtonClick' | 'onClearButtonClick' | 'isDragActive' | 'containerRef'
> {
/** Unique id for the TextArea, also used to generate ids for accessible labels. */
id: string;
/** What type of file. Determines what is is passed to `onChange` and expected by `value`
* (a string for 'text' and 'dataURL', or a File object otherwise. */
type?: 'text' | 'dataURL';
/** Value of the file's contents
* (string if text file, File object otherwise) */
value?: string | File;
/** Value to be shown in the read-only filename field. */
filename?: string;
/** A callback for when the file contents change. */
onChange?: (
value: string | File,
filename: string,
event:
| React.DragEvent<HTMLElement> // User dragged/dropped a file
| React.ChangeEvent<HTMLTextAreaElement> // User typed in the TextArea
| React.MouseEvent<HTMLButtonElement, MouseEvent> // User clicked Clear button
) => void;
/** Additional classes added to the FileUpload container element. */
className?: string;
/** Flag to show if the field is disabled. */
isDisabled?: boolean;
/** Flag to show if the field is read only. */
isReadOnly?: boolean;
/** Flag to show if a file is being loaded. */
isLoading?: boolean;
/** Aria-valuetext for the loading spinner */
spinnerAriaValueText?: string;
/** Flag to show if the field is required. */
isRequired?: boolean;
/* Value to indicate if the field is modified to show that validation state.
* If set to success, field will be modified to indicate valid state.
* If set to error, field will be modified to indicate error state.
*/
validated?: 'success' | 'error' | 'default';
/** Aria-label for the TextArea. */
'aria-label'?: string;
/** Placeholder string to display in the empty filename field */
filenamePlaceholder?: string;
/** Aria-label for the read-only filename field */
filenameAriaLabel?: string;
/** Text for the Browse button */
browseButtonText?: string;
/** Text for the Clear button */
clearButtonText?: string;
/** Flag to hide the built-in preview of the file (where available).
* If true, you can use children to render an alternate preview. */
hideDefaultPreview?: boolean;
/** Flag to allow editing of a text file's contents after it is selected from disk */
allowEditingUploadedText?: boolean;
/** Additional children to render after (or instead of) the file preview. */
children?: React.ReactNode;

// Props available in FileUpload but not FileUploadField:

/** A callback for when a selected file starts loading */
onReadStarted?: (fileHandle: File) => void;
/** A callback for when a selected file finishes loading */
onReadFinished?: (fileHandle: File) => void;
/** A callback for when the FileReader API fails */
onReadFailed?: (error: DOMException, fileHandle: File) => void;
/** Optional extra props to customize react-dropzone. */
dropzoneProps?: DropzoneProps;
}

export const FileUpload: React.FunctionComponent<FileUploadProps> = ({
id,
type,
value = type === fileReaderType.text || type === fileReaderType.dataURL ? '' : null,
filename = '',
children = null,
onChange = () => {},
onReadStarted = () => {},
onReadFinished = () => {},
onReadFailed = () => {},
dropzoneProps = {},
...props
}: FileUploadProps) => {
const onDropAccepted: DropFileEventHandler = (acceptedFiles: File[], event: React.DragEvent<HTMLElement>) => {
if (acceptedFiles.length > 0) {
const fileHandle = acceptedFiles[0];
if (type === fileReaderType.text || type === fileReaderType.dataURL) {
onChange('', fileHandle.name, event); // Show the filename while reading
onReadStarted(fileHandle);
readFile(fileHandle, type as fileReaderType)
.then(data => {
onReadFinished(fileHandle);
onChange(data as string, fileHandle.name, event);
})
.catch((error: DOMException) => {
onReadFailed(error, fileHandle);
onReadFinished(fileHandle);
onChange('', '', event); // Clear the filename field on a failure
});
} else {
onChange(fileHandle, fileHandle.name, event);
}
}
dropzoneProps.onDropAccepted && dropzoneProps.onDropAccepted(acceptedFiles, event);
};

const onDropRejected: DropFileEventHandler = (rejectedFiles: File[], event: React.DragEvent<HTMLElement>) => {
if (rejectedFiles.length > 0) {
onChange('', rejectedFiles[0].name, event);
}
dropzoneProps.onDropRejected && dropzoneProps.onDropRejected(rejectedFiles, event);
};

const onClearButtonClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
onChange('', '', event);
};

return (
<Dropzone multiple={false} {...dropzoneProps} onDropAccepted={onDropAccepted} onDropRejected={onDropRejected}>
{({ getRootProps, getInputProps, isDragActive, open }) => (
<FileUploadField
{...getRootProps({
...props,
refKey: 'containerRef',
onClick: event => event.preventDefault() // Prevents clicking TextArea from opening file dialog
})}
tabIndex={null} // Omit the unwanted tabIndex from react-dropzone's getRootProps
id={id}
type={type}
filename={filename}
value={value}
onChange={onChange}
isDragActive={isDragActive}
onBrowseButtonClick={open}
onClearButtonClick={onClearButtonClick}
>
<input {...getInputProps()} /* hidden, necessary for react-dropzone */ />
{children}
</FileUploadField>
)}
</Dropzone>
);
};
176 changes: 176 additions & 0 deletions packages/react-core/src/components/FileUpload/FileUploadField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import * as React from 'react';
import styles from '@patternfly/react-styles/css/components/FileUpload/file-upload';
import { css } from '@patternfly/react-styles';
import { Omit } from '../../helpers';
import { InputGroup } from '../InputGroup';
import { TextInput } from '../TextInput';
import { Button, ButtonVariant } from '../Button';
import { TextArea, TextAreResizeOrientation } from '../TextArea';
import { Spinner, spinnerSize } from '../Spinner';
import { fileReaderType } from '../../helpers/fileUtils';

export interface FileUploadFieldProps extends Omit<React.HTMLProps<HTMLDivElement>, 'value' | 'onChange'> {
/** Unique id for the TextArea, also used to generate ids for accessible labels */
id: string;
/** What type of file. Determines what is is expected by `value`
* (a string for 'text' and 'dataURL', or a File object otherwise). */
type?: 'text' | 'dataURL';
/** Value of the file's contents
* (string if text file, File object otherwise) */
value?: string | File;
/** Value to be shown in the read-only filename field. */
filename?: string;
/** A callback for when the TextArea value changes. */
onChange?: (
value: string,
filename: string,
event:
| React.ChangeEvent<HTMLTextAreaElement> // User typed in the TextArea
| React.MouseEvent<HTMLButtonElement, MouseEvent> // User clicked Clear button
) => void;
/** Additional classes added to the FileUploadField container element. */
className?: string;
/** Flag to show if the field is disabled. */
isDisabled?: boolean;
/** Flag to show if the field is read only. */
isReadOnly?: boolean;
/** Flag to show if a file is being loaded. */
isLoading?: boolean;
/** Aria-valuetext for the loading spinner */
spinnerAriaValueText?: string;
/** Flag to show if the field is required. */
isRequired?: boolean;
/* Value to indicate if the field is modified to show that validation state.
* If set to success, field will be modified to indicate valid state.
* If set to error, field will be modified to indicate error state.
*/
validated?: 'success' | 'error' | 'default';
/** Aria-label for the TextArea. */
'aria-label'?: string;
/** Placeholder string to display in the empty filename field */
filenamePlaceholder?: string;
/** Aria-label for the read-only filename field */
filenameAriaLabel?: string;
/** Text for the Browse button */
browseButtonText?: string;
/** Text for the Clear button */
clearButtonText?: string;
/** Flag to disable the Clear button */
isClearButtonDisabled?: boolean;
/** Flag to hide the built-in preview of the file (where available).
* If true, you can use children to render an alternate preview. */
hideDefaultPreview?: boolean;
/** Flag to allow editing of a text file's contents after it is selected from disk */
allowEditingUploadedText?: boolean;
/** Additional children to render after (or instead of) the file preview. */
children?: React.ReactNode;

// Props available in FileUploadField but not FileUpload:

/** A callback for when the Browse button is clicked. */
onBrowseButtonClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
/** A callback for when the Clear button is clicked. */
onClearButtonClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
/** Flag to show if a file is being dragged over the field */
isDragActive?: boolean;
/** A reference object to attach to the FileUploadField container element. */
containerRef?: React.Ref<HTMLDivElement>;
}

export const FileUploadField: React.FunctionComponent<FileUploadFieldProps> = ({
id,
type,
value = '',
filename = '',
onChange = () => {},
onBrowseButtonClick = () => {},
onClearButtonClick = () => {},
className = '',
isDisabled = false,
isReadOnly = false,
isLoading = false,
spinnerAriaValueText,
isRequired = false,
isDragActive = false,
validated = 'default' as 'success' | 'error' | 'default',
'aria-label': ariaLabel = 'File upload',
filenamePlaceholder = 'Drag a file here or browse to upload',
filenameAriaLabel = filename ? 'Read only filename' : filenamePlaceholder,
browseButtonText = 'Browse...',
clearButtonText = 'Clear',
isClearButtonDisabled = !filename && !value,
containerRef = null as React.Ref<HTMLDivElement>,
allowEditingUploadedText = false,
hideDefaultPreview = false,
children = null,
...props
}: FileUploadFieldProps) => {
const onTextAreaChange = (newValue: string, event: React.ChangeEvent<HTMLTextAreaElement>) => {
onChange(newValue, filename, event);
};

return (
<div
className={css(
styles.fileUpload,
isDragActive && styles.modifiers.dragHover,
isLoading && styles.modifiers.loading,
className
)}
ref={containerRef}
{...props}
>
<div className={styles.fileUploadFileSelect}>
<InputGroup>
<TextInput
isReadOnly // Always read-only regardless of isReadOnly prop (which is just for the TextArea)
isDisabled={isDisabled}
id={`${id}-filename`}
name={`${id}-filename`}
aria-label={filenameAriaLabel}
placeholder={filenamePlaceholder}
aria-describedby={`${id}-browse-button`}
value={filename}
/>
<Button
id={`${id}-browse-button`}
variant={ButtonVariant.control}
onClick={onBrowseButtonClick}
isDisabled={isDisabled}
>
{browseButtonText}
</Button>
<Button
variant={ButtonVariant.control}
isDisabled={isDisabled || isClearButtonDisabled}
onClick={onClearButtonClick}
>
{clearButtonText}
</Button>
</InputGroup>
</div>
<div className={styles.fileUploadFileDetails}>
{!hideDefaultPreview && type === fileReaderType.text && (
<TextArea
readOnly={isReadOnly || (!!filename && !allowEditingUploadedText)}
disabled={isDisabled}
isRequired={isRequired}
resizeOrientation={TextAreResizeOrientation.vertical}
validated={validated}
id={id}
name={id}
aria-label={ariaLabel}
value={value as string}
onChange={onTextAreaChange}
/>
)}
{isLoading && (
<div className={styles.fileUploadFileDetailsSpinner}>
<Spinner size={spinnerSize.lg} aria-valuetext={spinnerAriaValueText} />
</div>
)}
</div>
{children}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FileUpload } from '../FileUpload';
import * as React from 'react';
import { shallow } from 'enzyme';

test('simple fileupload', () => {
const changeHandler = jest.fn();
const readStartedHandler = jest.fn();
const readFinishedHandler = jest.fn();

const view = shallow(<FileUpload
id="simple-text-file"
type="text"
value={''}
filename={''}
onChange={changeHandler}
onReadStarted={readStartedHandler}
onReadFinished={readFinishedHandler}
isLoading={false}
/>);
expect(view).toMatchSnapshot();
});

Loading