diff --git a/src/CustomFileInput.js b/src/CustomFileInput.js new file mode 100644 index 000000000..6ff4d0c55 --- /dev/null +++ b/src/CustomFileInput.js @@ -0,0 +1,112 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { mapToCssModules } from './utils'; + +const propTypes = { + className: PropTypes.string, + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + label: PropTypes.node, + valid: PropTypes.bool, + invalid: PropTypes.bool, + bsSize: PropTypes.string, + htmlFor: PropTypes.string, + cssModule: PropTypes.object, + onChange: PropTypes.func, + children: PropTypes.oneOfType([PropTypes.node, PropTypes.array, PropTypes.func]), + innerRef: PropTypes.oneOfType([ + PropTypes.object, + PropTypes.string, + PropTypes.func, + ]) +}; + +class CustomFileInput extends React.Component { + constructor(props) { + super(props); + + this.state = { + files:null, + }; + + this.onChange = this.onChange.bind(this); + } + + onChange(e) { + let input = e.target; + let {onChange} = this.props; + let files = this.getSelectedFiles(input); + + if (typeof(onChange) === 'function') { + onChange(...arguments); + } + + this.setState({files}) + } + + getSelectedFiles(input) { + let {multiple} = this.props; + + if (multiple && input.files) { + let files = [].slice.call(input.files); + + return files.map(file => file.name).join(', '); + } + + if (input.value.indexOf('fakepath') !== -1) { + let parts = input.value.split('\\'); + + return parts[parts.length - 1]; + } + + return input.value; + } + + render() { + const { + className, + label, + valid, + invalid, + cssModule, + children, + bsSize, + innerRef, + htmlFor, + type, + onChange, + ...attributes + } = this.props; + + const customClass = mapToCssModules( + classNames( + className, + `custom-file`, + ), + cssModule + ); + + const validationClassNames = mapToCssModules( + classNames( + invalid && 'is-invalid', + valid && 'is-valid', + ), + cssModule + ); + + const labelHtmlFor = htmlFor || attributes.id; + const {files} = this.state; + + return ( +
+ + + {children} +
+ ); + } +} + +CustomFileInput.propTypes = propTypes; + +export default CustomFileInput; diff --git a/src/CustomInput.js b/src/CustomInput.js index b48239d48..a8f97438b 100644 --- a/src/CustomInput.js +++ b/src/CustomInput.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { mapToCssModules } from './utils'; +import CustomFileInput from './CustomFileInput'; const propTypes = { className: PropTypes.string, @@ -58,10 +59,7 @@ function CustomInput(props) { if (type === 'file') { return ( -
- - -
+ ); } diff --git a/src/__tests__/CustomInput.spec.js b/src/__tests__/CustomInput.spec.js index 21666223d..7eec0ce97 100644 --- a/src/__tests__/CustomInput.spec.js +++ b/src/__tests__/CustomInput.spec.js @@ -212,8 +212,8 @@ describe('Custom Inputs', () => { describe('CustomFile', () => { it('should render children in the outer div', () => { - const file = shallow(); - expect(file.type()).toBe('div'); + const file = mount(); + expect(file.find('.custom-file').first().type()).toBe('div'); }); it('should add class is-invalid when invalid is true', () => { @@ -264,6 +264,45 @@ describe('Custom Inputs', () => { expect(ref.current).not.toBeNull(); expect(ref.current).toBeInstanceOf(HTMLInputElement); }); + + describe('onChange', () => { + it('calls props.onChange if it exists', () => { + const onChange = jest.fn(); + const file = mount(); + + file.find('input').hostNodes().simulate('change'); + expect(onChange).toHaveBeenCalled(); + }); + }); + + it('removes fakepath from file name', () => { + const file = mount(); + + file.find('input').hostNodes().simulate('change', { + target:{ + value:'C:\\fakepath\\test.txt' + } + }); + + expect(file.find('.custom-file-label').text()).toBe('test.txt'); + }); + + it('lists multiple files when supported', () => { + const file = mount(); + + file.find('input').hostNodes().simulate('change', { + target:{ + value:'C:\\fakepath\\file1.txt', + files:[ + {name:"file1.txt"}, + {name:'file2.txt'}, + {name:'file3.txt'}, + ] + } + }) + + expect(file.find('.custom-file-label').text()).toBe('file1.txt, file2.txt, file3.txt'); + }) }); describe('CustomRange', () => { diff --git a/src/index.js b/src/index.js index b089eedee..bf00ecc23 100644 --- a/src/index.js +++ b/src/index.js @@ -38,6 +38,7 @@ export CarouselCaption from './CarouselCaption'; export CardSubtitle from './CardSubtitle'; export CardText from './CardText'; export CardTitle from './CardTitle'; +export CustomFileInput from './CustomFileInput'; export CustomInput from './CustomInput'; export PopperContent from './PopperContent'; export PopperTargetHelper from './PopperTargetHelper';