diff --git a/.eslintrc.json b/.eslintrc.json index dce5c439bfe..2700f274705 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,8 @@ { "env": { "browser": true, - "node": true + "node": true, + "es6": true }, "plugins": [ "@typescript-eslint", diff --git a/packages/react-catalog-view-extension/package.json b/packages/react-catalog-view-extension/package.json index a4e08b2075d..cacec3ff67e 100644 --- a/packages/react-catalog-view-extension/package.json +++ b/packages/react-catalog-view-extension/package.json @@ -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", diff --git a/packages/react-charts/package.json b/packages/react-charts/package.json index 7cf7bf7d629..39beaeb4da4 100644 --- a/packages/react-charts/package.json +++ b/packages/react-charts/package.json @@ -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", diff --git a/packages/react-core/package.json b/packages/react-core/package.json index 880617576fc..039cdea5440 100644 --- a/packages/react-core/package.json +++ b/packages/react-core/package.json @@ -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": { @@ -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", diff --git a/packages/react-core/src/components/FileUpload/FileUpload.tsx b/packages/react-core/src/components/FileUpload/FileUpload.tsx new file mode 100644 index 00000000000..be5dd00a61d --- /dev/null +++ b/packages/react-core/src/components/FileUpload/FileUpload.tsx @@ -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 // User dragged/dropped a file + | React.ChangeEvent // User typed in the TextArea + | React.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 = ({ + 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) => { + 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) => { + if (rejectedFiles.length > 0) { + onChange('', rejectedFiles[0].name, event); + } + dropzoneProps.onDropRejected && dropzoneProps.onDropRejected(rejectedFiles, event); + }; + + const onClearButtonClick = (event: React.MouseEvent) => { + onChange('', '', event); + }; + + return ( + + {({ getRootProps, getInputProps, isDragActive, open }) => ( + 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} + > + + {children} + + )} + + ); +}; diff --git a/packages/react-core/src/components/FileUpload/FileUploadField.tsx b/packages/react-core/src/components/FileUpload/FileUploadField.tsx new file mode 100644 index 00000000000..77b354003da --- /dev/null +++ b/packages/react-core/src/components/FileUpload/FileUploadField.tsx @@ -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, '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 // User typed in the TextArea + | React.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) => void; + /** A callback for when the Clear button is clicked. */ + onClearButtonClick?: (event: React.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; +} + +export const FileUploadField: React.FunctionComponent = ({ + 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, + allowEditingUploadedText = false, + hideDefaultPreview = false, + children = null, + ...props +}: FileUploadFieldProps) => { + const onTextAreaChange = (newValue: string, event: React.ChangeEvent) => { + onChange(newValue, filename, event); + }; + + return ( +
+
+ + + + + +
+
+ {!hideDefaultPreview && type === fileReaderType.text && ( +