diff --git a/packages/react-core/package.json b/packages/react-core/package.json index 02128e66d64..e67b56c8089 100644 --- a/packages/react-core/package.json +++ b/packages/react-core/package.json @@ -38,7 +38,7 @@ "@patternfly/react-styles": "^4.19.3", "@patternfly/react-tokens": "^4.21.3", "focus-trap": "6.2.2", - "react-dropzone": "9.0.0", + "react-dropzone": "10.2.2", "tippy.js": "5.1.2", "tslib": "^2.0.0" }, diff --git a/packages/react-core/src/components/FileUpload/FileUpload.tsx b/packages/react-core/src/components/FileUpload/FileUpload.tsx index 5d0ac7d2652..fcdaca64640 100644 --- a/packages/react-core/src/components/FileUpload/FileUpload.tsx +++ b/packages/react-core/src/components/FileUpload/FileUpload.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import Dropzone, { DropzoneProps, DropFileEventHandler } from 'react-dropzone'; +import Dropzone, { DropzoneProps, DropEvent, DropzoneInputProps } from 'react-dropzone'; import { FileUploadField, FileUploadFieldProps } from './FileUploadField'; import { readFile, fileReaderType } from '../../helpers/fileUtils'; - +import { fromEvent } from 'file-selector'; export interface FileUploadProps extends Omit< FileUploadFieldProps, @@ -18,15 +18,19 @@ export interface FileUploadProps value?: string | File; /** Value to be shown in the read-only filename field. */ filename?: string; - /** A callback for when the file contents change. */ + /** *(deprecated)* A callback for when the file contents change. Please instead use onFileInputChange, onTextChange, onDataChange, onClearClick individually. */ onChange?: ( value: string | File, filename: string, event: + | React.MouseEvent // Clear button was clicked | React.DragEvent // User dragged/dropped a file - | React.ChangeEvent // User typed in the TextArea - | React.MouseEvent // User clicked Clear button + | React.ChangeEvent // User typed in the TextArea + | DragEvent + | Event ) => void; + /** Change event emitted from the hidden \ field associated with the component */ + onFileInputChange?: (event: React.ChangeEvent | DropEvent, file: File) => void; /** Callback for clicking on the FileUploadField text area. By default, prevents a click in the text area from opening file dialog. */ onClick?: (event: React.MouseEvent) => void; /** Additional classes added to the FileUpload container element. */ @@ -74,6 +78,12 @@ export interface FileUploadProps onReadFailed?: (error: DOMException, fileHandle: File) => void; /** Optional extra props to customize react-dropzone. */ dropzoneProps?: DropzoneProps; + /** Clear button was clicked */ + onClearClick?: React.MouseEventHandler; + /** Text area text changed */ + onTextChange?: (text: string) => void; + /** On data changed - if type='text' or type='dataURL' and file was loaded it will call this method */ + onDataChange?: (data: string) => void; } export const FileUpload: React.FunctionComponent = ({ @@ -83,16 +93,23 @@ export const FileUpload: React.FunctionComponent = ({ filename = '', children = null, onChange = () => {}, + onFileInputChange = null, onReadStarted = () => {}, onReadFinished = () => {}, onReadFailed = () => {}, + onClearClick, onClick = event => event.preventDefault(), + onTextChange, + onDataChange, dropzoneProps = {}, ...props }: FileUploadProps) => { - const onDropAccepted: DropFileEventHandler = (acceptedFiles: File[], event: React.DragEvent) => { + const onDropAccepted = (acceptedFiles: File[], event: DropEvent) => { if (acceptedFiles.length > 0) { const fileHandle = acceptedFiles[0]; + if (event.type === 'drop') { + onFileInputChange?.(event, fileHandle); + } if (type === fileReaderType.text || type === fileReaderType.dataURL) { onChange('', fileHandle.name, event); // Show the filename while reading onReadStarted(fileHandle); @@ -100,11 +117,13 @@ export const FileUpload: React.FunctionComponent = ({ .then(data => { onReadFinished(fileHandle); onChange(data as string, fileHandle.name, event); + onDataChange?.(data as string); }) .catch((error: DOMException) => { onReadFailed(error, fileHandle); onReadFinished(fileHandle); onChange('', '', event); // Clear the filename field on a failure + onDataChange?.(''); }); } else { onChange(fileHandle, fileHandle.name, event); @@ -113,41 +132,69 @@ export const FileUpload: React.FunctionComponent = ({ dropzoneProps.onDropAccepted && dropzoneProps.onDropAccepted(acceptedFiles, event); }; - const onDropRejected: DropFileEventHandler = (rejectedFiles: File[], event: React.DragEvent) => { + const onDropRejected = (rejectedFiles: File[], event: DropEvent) => { if (rejectedFiles.length > 0) { onChange('', rejectedFiles[0].name, event); } dropzoneProps.onDropRejected && dropzoneProps.onDropRejected(rejectedFiles, event); }; + const fileInputRef = React.useRef(); + const setFileValue = (filename: string) => { + fileInputRef.current.value = filename; + }; + const onClearButtonClick = (event: React.MouseEvent) => { onChange('', '', event); + onClearClick?.(event); + setFileValue(null); }; return ( - - {({ getRootProps, getInputProps, isDragActive, open }) => ( - event.preventDefault() - })} - 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} - onTextAreaClick={onClick} - > - - {children} - - )} + + {({ getRootProps, getInputProps, isDragActive, open }) => { + const oldInputProps = getInputProps(); + const inputProps: DropzoneInputProps = { + ...oldInputProps, + onChange: async (e: React.ChangeEvent) => { + oldInputProps.onChange?.(e); + const files = await fromEvent(e.nativeEvent); + if (files.length === 1) { + onFileInputChange?.(e, files[0] as File); + } + } + }; + + return ( + event.preventDefault() + })} + 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} + onTextAreaClick={onClick} + onTextChange={onTextChange} + > + + {children} + + ); + }} ); }; diff --git a/packages/react-core/src/components/FileUpload/FileUploadField.tsx b/packages/react-core/src/components/FileUpload/FileUploadField.tsx index 8b6331c5891..bb624fe6882 100644 --- a/packages/react-core/src/components/FileUpload/FileUploadField.tsx +++ b/packages/react-core/src/components/FileUpload/FileUploadField.tsx @@ -76,6 +76,8 @@ export interface FileUploadFieldProps extends Omit; + /** Text area text changed */ + onTextChange?: (text: string) => void; } export const FileUploadField: React.FunctionComponent = ({ @@ -87,6 +89,7 @@ export const FileUploadField: React.FunctionComponent = ({ onBrowseButtonClick = () => {}, onClearButtonClick = () => {}, onTextAreaClick, + onTextChange, className = '', isDisabled = false, isReadOnly = false, @@ -105,10 +108,12 @@ export const FileUploadField: React.FunctionComponent = ({ allowEditingUploadedText = false, hideDefaultPreview = false, children = null, + ...props }: FileUploadFieldProps) => { const onTextAreaChange = (newValue: string, event: React.ChangeEvent) => { onChange(newValue, filename, event); + onTextChange?.(newValue); }; return (
- + `; diff --git a/packages/react-core/src/components/FileUpload/examples/FileUpload.md b/packages/react-core/src/components/FileUpload/examples/FileUpload.md index d05f9e4b522..b3617347388 100644 --- a/packages/react-core/src/components/FileUpload/examples/FileUpload.md +++ b/packages/react-core/src/components/FileUpload/examples/FileUpload.md @@ -9,13 +9,15 @@ import FileUploadIcon from '@patternfly/react-icons/dist/esm/icons/file-upload-i ## Examples -The basic `FileUpload` component can accept a file via browse or drag-and-drop, and behaves like a standard form field with its `value` and `onChange` props. The `type` prop determines how the `FileUpload` component behaves upon accepting a file, what type of value it passes to its `onChange` prop, and what type it expects for its `value` prop. +The basic `FileUpload` component can accept a file via browse or drag-and-drop, and behaves like a standard form field with its `value` and `onFileInputChange` event that is similar to `` prop. The `type` prop determines how the `FileUpload` component behaves upon accepting a file, what type of value it passes to its `onDataChange` event. ### Text files -If `type="text"` is passed (and `hideDefaultPreview` is not), a `TextArea` preview will be rendered underneath the filename bar. When a file is selected, its contents will be read into memory and passed to the `onChange` prop as a string (along with its filename). Typing/pasting text in the box will also call `onChange` with a string, and a string value is expected for the `value` prop. +If `type="text"` is passed (and `hideDefaultPreview` is not), a `TextArea` preview will be rendered underneath the filename bar. When a file is selected, its contents will be read into memory and passed to the `onDataChange` event as a string. Every filename change is passed to `onFileInputChange` same as it would do with the `` element. +Pressing _Clear_ button triggers `onClearClick` event. ### Simple text file + ```js import React from 'react'; import { FileUpload } from '@patternfly/react-core'; @@ -24,7 +26,9 @@ class SimpleTextFileUpload extends React.Component { constructor(props) { super(props); this.state = { value: '', filename: '', isLoading: false }; - this.handleFileChange = (value, filename, event) => this.setState({ value, filename }); + this.handleFileInputChange = (event, file) => this.setState({ filename: file.name }); + this.handleDataChange = value => this.setState({ value }); + this.handleClear = event => this.setState({ filename: '', value: '' }); this.handleFileReadStarted = fileHandle => this.setState({ isLoading: true }); this.handleFileReadFinished = fileHandle => this.setState({ isLoading: false }); } @@ -38,9 +42,11 @@ class SimpleTextFileUpload extends React.Component { value={value} filename={filename} filenamePlaceholder="Drag and drop a file or upload one" - onChange={this.handleFileChange} + onFileInputChange={this.handleFileInputChange} + onDataChange={this.handleDataChange} onReadStarted={this.handleFileReadStarted} onReadFinished={this.handleFileReadFinished} + onClearClick={this.handleClear} isLoading={isLoading} browseButtonText="Upload" /> @@ -49,9 +55,11 @@ class SimpleTextFileUpload extends React.Component { } ``` -A user can always type instead of selecting a file, but by default, once a user selects a text file from their disk they are not allowed to edit it (to prevent unintended changes to a format-sensitive file). This behavior can be changed with the `allowEditingUploadedText` prop: +A user can always type instead of selecting a file, but by default, once a user selects a text file from their disk they are not allowed to edit it (to prevent unintended changes to a format-sensitive file). This behavior can be changed with the `allowEditingUploadedText` prop. +Typing/pasting text in the box will call `onTextChange` with a string, and a string value is expected for the `value` prop. : ### Text file with edits allowed + ```js import React from 'react'; import { FileUpload } from '@patternfly/react-core'; @@ -60,7 +68,9 @@ class TextFileWithEditsAllowed extends React.Component { constructor(props) { super(props); this.state = { value: '', filename: '', isLoading: false }; - this.handleFileChange = (value, filename, event) => this.setState({ value, filename }); + this.handleFileInputChange = (event, file) => this.setState({ filename: file.name }); + this.handleTextOrDataChange = value => this.setState({ value }); + this.handleClear = event => this.setState({ filename: '', value: '' }); this.handleFileReadStarted = fileHandle => this.setState({ isLoading: true }); this.handleFileReadFinished = fileHandle => this.setState({ isLoading: false }); } @@ -74,7 +84,10 @@ class TextFileWithEditsAllowed extends React.Component { value={value} filename={filename} filenamePlaceholder="Drag and drop a file or upload one" - onChange={this.handleFileChange} + onFileInputChange={this.handleFileInputChange} + onDataChange={this.handleTextOrDataChange} + onClearClick={this.handleClear} + onTextChange={this.handleTextOrDataChange} onReadStarted={this.handleFileReadStarted} onReadFinished={this.handleFileReadFinished} isLoading={isLoading} @@ -95,6 +108,7 @@ Any [props accepted by `react-dropzone`'s `Dropzone` component](https://react-dr Restricting file sizes and types in this way is for user convenience only, and it cannot prevent a malicious user from submitting anything to your server. As with any user input, your application should also validate, sanitize and/or reject restricted files on the server side. ### Text file with restrictions + ```js import React from 'react'; import { FileUpload, Form, FormGroup } from '@patternfly/react-core'; @@ -103,9 +117,9 @@ class TextFileUploadWithRestrictions extends React.Component { constructor(props) { super(props); this.state = { value: '', filename: '', isLoading: false, isRejected: false }; - this.handleFileChange = (value, filename, event) => { - this.setState({ value, filename, isRejected: false }); - }; + this.handleFileInputChange = (event, file) => this.setState({ filename: file.name }); + this.handleTextOrDataChange = value => this.setState({ value }); + this.handleClear = event => this.setState({ filename: '', value: '', isRejected: false }); this.handleFileRejected = (rejectedFiles, event) => this.setState({ isRejected: true }); this.handleFileReadStarted = fileHandle => this.setState({ isLoading: true }); this.handleFileReadFinished = fileHandle => this.setState({ isLoading: false }); @@ -127,7 +141,10 @@ class TextFileUploadWithRestrictions extends React.Component { value={value} filename={filename} filenamePlaceholder="Drag and drop a file or upload one" - onChange={this.handleFileChange} + onFileInputChange={this.handleFileInputChange} + onDataChange={this.handleTextOrDataChange} + onTextChange={this.handleTextOrDataChange} + onClearClick={this.handleClear} onReadStarted={this.handleFileReadStarted} onReadFinished={this.handleFileReadFinished} isLoading={isLoading} @@ -148,9 +165,10 @@ class TextFileUploadWithRestrictions extends React.Component { ### Other file types -If no `type` prop is specified, the component will not read files directly. When a file is selected, a [`File` object](https://developer.mozilla.org/en-US/docs/Web/API/File) will be passed to `onChange` and your application will be responsible for reading from it (e.g. by using the [FileReader API](https://developer.mozilla.org/en-US/docs/Web/API/FileReader) or attaching it to a [FormData object](https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects)). A `File` object will also be expected for the `value` prop instead of a string, and no preview of the file contents will be shown by default. The `onReadStarted` and `onReadFinished` callbacks will also not be called since the component is not reading the file. +If no `type` prop is specified, the component will not read files directly. When a file is selected, a [`File` object](https://developer.mozilla.org/en-US/docs/Web/API/File) will be passed as a second argument to `onFileInputChange` and your application will be responsible for reading from it (e.g. by using the [FileReader API](https://developer.mozilla.org/en-US/docs/Web/API/FileReader) or attaching it to a [FormData object](https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects)). A `File` object will also be expected for the `value` prop instead of a string, and no preview of the file contents will be shown by default. The `onReadStarted` and `onReadFinished` callbacks will also not be called since the component is not reading the file. ### Simple file of any format + ```js import React from 'react'; import { FileUpload } from '@patternfly/react-core'; @@ -159,12 +177,25 @@ class SimpleFileUpload extends React.Component { constructor(props) { super(props); this.state = { value: null, filename: '' }; - this.handleFileChange = (value, filename, event) => this.setState({ value, filename }); + this.handleFileInputChange = (event, file) => { + this.setState({ filename: file.name }); + }; + this.handleClear = event => this.setState({ filename: '', value: '' }); } render() { const { value, filename } = this.state; - return ; + return ( + + ); } } ``` @@ -174,6 +205,7 @@ class SimpleFileUpload extends React.Component { Regardless of `type`, the preview area (the TextArea, or any future implementations of default previews for other types) can be removed by passing `hideDefaultPreview`, and a custom one can be rendered by passing `children`. ### Custom file preview + ```js import React from 'react'; import { FileUpload } from '@patternfly/react-core'; @@ -183,7 +215,10 @@ class CustomPreviewFileUpload extends React.Component { constructor(props) { super(props); this.state = { value: null, filename: '' }; - this.handleFileChange = (value, filename, event) => this.setState({ value, filename }); + this.handleFileInputChange = (event, file) => { + this.setState({ value: file, filename: file.name }); + }; + this.handleClear = event => this.setState({ filename: '', value: '' }); } render() { @@ -194,7 +229,8 @@ class CustomPreviewFileUpload extends React.Component { value={value} filename={filename} filenamePlaceholder="Drag and drop a file or upload one" - onChange={this.handleFileChange} + onFileInputChange={this.handleFileInputChange} + onClearClick={this.handleClear} hideDefaultPreview browseButtonText="Upload" > @@ -216,6 +252,7 @@ class CustomPreviewFileUpload extends React.Component { Note that the `isLoading` prop is styled to position the spinner dead center above the entire component, so it should not be used with `hideDefaultPreview` unless a custom empty-state preview is provided via `children`. The below example prevents `isLoading` and `hideDefaultPreview` from being used at the same time. You can always provide your own spinner as part of the `children`! ### Custom file upload + ```js import React from 'react'; import { FileUploadField, Checkbox } from '@patternfly/react-core'; @@ -274,7 +311,7 @@ class CustomFileUpload extends React.Component { type="text" value={value} filename={filename ? 'example-filename.txt' : ''} - onChange={this.handleTextAreaChange} + onTextChange={this.handleTextAreaChange} filenamePlaceholder="Do something custom with this!" onBrowseButtonClick={() => alert('Browse button clicked!')} onClearButtonClick={() => alert('Clear button clicked!')} diff --git a/packages/react-integration/demo-app-ts/src/components/demos/FileUploadDemo/FileUploadDemo.tsx b/packages/react-integration/demo-app-ts/src/components/demos/FileUploadDemo/FileUploadDemo.tsx index d836cf023cb..1f933eae632 100644 --- a/packages/react-integration/demo-app-ts/src/components/demos/FileUploadDemo/FileUploadDemo.tsx +++ b/packages/react-integration/demo-app-ts/src/components/demos/FileUploadDemo/FileUploadDemo.tsx @@ -6,8 +6,9 @@ export class FileUploadDemo extends React.Component { state = { value: '', filename: '', isLoading: false }; /* eslint-disable-next-line no-console */ - handleClick = (evt: React.MouseEvent) => console.log('clicked', evt.target); - handleFileChange = (value: string | File, filename: string) => this.setState({ value, filename }); + handleFileInputChange = (event: React.ChangeEvent, file: File) => + this.setState({ value: file, filename: file.name }); + handleDataChange = (value: string) => this.setState({ value }); /* eslint-disable @typescript-eslint/no-unused-vars */ handleFileReadStarted = (fileHandle: File) => this.setState({ isLoading: true }); handleFileReadFinished = (fileHandle: File) => this.setState({ isLoading: false }); @@ -21,10 +22,10 @@ export class FileUploadDemo extends React.Component { type="text" value={value} filename={filename} - onChange={this.handleFileChange} + onFileInputChange={this.handleFileInputChange} + onDataChange={this.handleDataChange} onReadStarted={this.handleFileReadStarted} onReadFinished={this.handleFileReadFinished} - onClick={this.handleClick} isLoading={isLoading} /> ); diff --git a/yarn.lock b/yarn.lock index 65d504b81d2..7a37a6bd6fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4585,6 +4585,11 @@ attr-accept@^1.1.3: dependencies: core-js "^2.5.0" +attr-accept@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" + integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== + autoprefixer@^9.8.6: version "9.8.6" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f" @@ -8147,6 +8152,13 @@ file-saver@^1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8" +file-selector@^0.1.12: + version "0.1.19" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.19.tgz#8ecc9d069a6f544f2e4a096b64a8052e70ec8abf" + integrity sha512-kCWw3+Aai8Uox+5tHCNgMFaUdgidxvMnLWO6fM5sZ0hA2wlHP5/DHGF0ECe84BiB95qdJbKNEJhWKVDvMN+JDQ== + dependencies: + tslib "^2.0.1" + file-selector@^0.1.8: version "0.1.12" resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.12.tgz#fe726547be219a787a9dcc640575a04a032b1fd0" @@ -13641,6 +13653,15 @@ react-dom@^17.0.0: object-assign "^4.1.1" scheduler "^0.20.1" +react-dropzone@10.2.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-10.2.2.tgz#67b4db7459589a42c3b891a82eaf9ade7650b815" + integrity sha512-U5EKckXVt6IrEyhMMsgmHQiWTGLudhajPPG77KFSvgsMqNEHSyGpqWvOMc5+DhEah/vH4E1n+J5weBNLd5VtyA== + dependencies: + attr-accept "^2.0.0" + file-selector "^0.1.12" + prop-types "^15.7.2" + react-dropzone@9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-9.0.0.tgz#4f5223cdcb4d3bd8a66e3298c4041eb0c75c4634"