diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx index 3f596eb..9477b99 100644 --- a/src/AjaxUploader.tsx +++ b/src/AjaxUploader.tsx @@ -1,9 +1,10 @@ /* eslint react/no-is-mounted:0,react/sort-comp:0,react/prop-types:0 */ -import { clsx } from 'clsx'; import pickAttrs from '@rc-component/util/lib/pickAttrs'; +import { clsx } from 'clsx'; import React, { Component } from 'react'; import attrAccept from './attr-accept'; import type { + AcceptConfig, BeforeUploadFileType, RcFile, UploadProgressEvent, @@ -30,12 +31,36 @@ class AjaxUploader extends Component { private _isMounted: boolean; - onChange = (e: React.ChangeEvent) => { + private filterFile = (file: RcFile | File, force = false) => { const { accept, directory } = this.props; + + let filterFn: Exclude; + let acceptFormat: string | undefined; + + if (typeof accept === 'string') { + acceptFormat = accept; + } else { + const { filter, format } = accept || {}; + + acceptFormat = format; + if (filter === 'native') { + filterFn = () => true; + } else { + filterFn = filter; + } + } + + const mergedFilter = + filterFn || + (directory || force + ? (currentFile: RcFile) => attrAccept(currentFile, acceptFormat) + : () => true); + return mergedFilter(file as RcFile); + }; + + onChange = (e: React.ChangeEvent) => { const { files } = e.target; - const acceptedFiles = [...files].filter( - (file: RcFile) => !directory || attrAccept(file, accept), - ); + const acceptedFiles = [...files].filter(file => this.filterFile(file)); this.uploadFiles(acceptedFiles); this.reset(); }; @@ -67,7 +92,7 @@ class AjaxUploader extends Component { }; onDataTransferFiles = async (dataTransfer: DataTransfer, existFileCallback?: () => void) => { - const { multiple, accept, directory } = this.props; + const { multiple, directory } = this.props; const items: DataTransferItem[] = [...(dataTransfer.items || [])]; let files: File[] = [...(dataTransfer.files || [])]; @@ -77,12 +102,10 @@ class AjaxUploader extends Component { } if (directory) { - files = await traverseFileTree(Array.prototype.slice.call(items), (_file: RcFile) => - attrAccept(_file, this.props.accept), - ); + files = await traverseFileTree(Array.prototype.slice.call(items), this.filterFile); this.uploadFiles(files); } else { - let acceptFiles = [...files].filter((file: RcFile) => attrAccept(file, accept)); + let acceptFiles = [...files].filter(file => this.filterFile(file, true)); if (multiple === false) { acceptFiles = files.slice(0, 1); @@ -322,17 +345,20 @@ class AjaxUploader extends Component { capture, children, directory, - folder, openFileDialogOnClick, onMouseEnter, onMouseLeave, hasControlInside, ...otherProps } = this.props; + + // Extract accept format for input element + const acceptFormat = typeof accept === 'string' ? accept : accept?.format; const cls = clsx(prefixCls, { [`${prefixCls}-disabled`]: disabled, [className]: className }); // because input don't have directory/webkitdirectory type declaration - const dirProps: any = - directory || folder ? { directory: 'directory', webkitdirectory: 'webkitdirectory' } : {}; + const dirProps: any = directory + ? { directory: 'directory', webkitdirectory: 'webkitdirectory' } + : {}; const events = disabled ? {} : { @@ -361,7 +387,7 @@ class AjaxUploader extends Component { key={this.state.uid} style={{ display: 'none', ...styles.input }} className={classNames.input} - accept={accept} + accept={acceptFormat} {...dirProps} multiple={multiple} onChange={this.onChange} diff --git a/src/interface.tsx b/src/interface.tsx index f2906e8..27f28b5 100644 --- a/src/interface.tsx +++ b/src/interface.tsx @@ -4,8 +4,13 @@ export type BeforeUploadFileType = File | Blob | boolean | string; export type Action = string | ((file: RcFile) => string | PromiseLike); +export type AcceptConfig = { + format: string; + filter?: 'native' | ((file: RcFile) => boolean); +}; + export interface UploadProps - extends Omit, 'onError' | 'onProgress'> { + extends Omit, 'onError' | 'onProgress' | 'accept'> { name?: string; style?: React.CSSProperties; className?: string; @@ -13,12 +18,10 @@ export interface UploadProps component?: React.ComponentType | string; action?: Action; method?: UploadRequestMethod; - /** @deprecated Please use `folder` instead */ directory?: boolean; - folder?: boolean; data?: Record | ((file: RcFile | string | Blob) => Record); headers?: UploadRequestHeader; - accept?: string; + accept?: string | AcceptConfig; multiple?: boolean; onBatchStart?: ( fileList: { file: RcFile; parsedFile: Exclude }[], diff --git a/tests/uploader.spec.tsx b/tests/uploader.spec.tsx index c688368..5b3040c 100644 --- a/tests/uploader.spec.tsx +++ b/tests/uploader.spec.tsx @@ -1,5 +1,5 @@ -import { fireEvent, render } from '@testing-library/react'; import { resetWarned } from '@rc-component/util/lib/warning'; +import { fireEvent, render } from '@testing-library/react'; import React from 'react'; import sinon from 'sinon'; import { format } from 'util'; @@ -1038,18 +1038,71 @@ describe('uploader', () => { directory: false, }, ); + }); - it('should trigger beforeUpload when uploading non-accepted files in folder mode', () => { - const beforeUpload = jest.fn(); - const { container } = render(); + describe('AcceptConfig', () => { + let uploader: ReturnType; + const handlers: UploadProps = {}; - fireEvent.change(container.querySelector('input')!, { - target: { - files: [new File([], 'bamboo.png'), new File([], 'light.jpg')], - }, + const props: UploadProps = { + action: '/test', + data: { a: 1, b: 2 }, + directory: true, // Enable format filtering + onStart(file) { + if (handlers.onStart) { + handlers.onStart(file); + } + }, + }; + + function testAcceptConfig(desc: string, accept: any, files: object[], expectCallTimes: number) { + it(desc, done => { + uploader = render(); + const input = uploader.container.querySelector('input')!; + fireEvent.change(input, { target: { files } }); + const mockStart = jest.fn(); + handlers.onStart = mockStart; + + setTimeout(() => { + expect(mockStart.mock.calls.length).toBe(expectCallTimes); + done(); + }, 100); }); - expect(beforeUpload).toHaveBeenCalledTimes(2); - }); + } + + testAcceptConfig( + 'should work with format only', + { format: '.png' }, + [{ name: 'test.png' }, { name: 'test.jpg' }], + 1, + ); + + testAcceptConfig( + 'should work with filter: native', + { format: '.png', filter: 'native' }, + [{ name: 'test.png' }, { name: 'test.jpg' }], + 2, // native filter bypasses format check + ); + + testAcceptConfig( + 'should work with custom filter function', + { + format: '.png', + filter: (file: any) => file.name.includes('custom'), + }, + [{ name: 'custom.jpg' }, { name: 'test.png' }], + 1, // only custom.jpg passes custom filter + ); + + testAcceptConfig( + 'should work with MIME type format', + { format: 'image/*' }, + [ + { name: 'test.png', type: 'image/png' }, + { name: 'doc.txt', type: 'text/plain' }, + ], + 1, // only image file passes + ); }); describe('transform file before request', () => {