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';