From 71486d7ae87fc4e78ee61d7b607ccd30461fe2ba Mon Sep 17 00:00:00 2001 From: David Ellingsworth Date: Fri, 12 Apr 2019 16:25:05 -0400 Subject: [PATCH 1/6] fix(CustomInput[type=file]): add CustomFileInput component to display the selected files in the label. This fixes #1460. --- src/CustomFileInput.js | 112 ++++++++++++++++++++++++++++++ src/CustomInput.js | 6 +- src/__tests__/CustomInput.spec.js | 4 +- src/index.js | 1 + 4 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 src/CustomFileInput.js diff --git a/src/CustomFileInput.js b/src/CustomFileInput.js new file mode 100644 index 000000000..248011c25 --- /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`, + bsSize ? `custom-file-${bsSize}` : false, + ), + cssModule + ); + + const validationClassNames = mapToCssModules( + classNames( + invalid && 'is-invalid', + valid && 'is-valid', + ), + cssModule + ); + + const labelHtmlFor = htmlFor || attributes.id; + const {files} = this.state; + + return ( +
+ + +
+ ); + } +} + +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..e0f3ef361 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', () => { 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'; From 2114d1e4ccfa84bccb749e24426f766d53d5b172 Mon Sep 17 00:00:00 2001 From: David Ellingsworth Date: Tue, 16 Apr 2019 17:38:58 -0400 Subject: [PATCH 2/6] fix(CustomFileInput): Remove bsSize class as it does not apply --- src/CustomFileInput.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CustomFileInput.js b/src/CustomFileInput.js index 248011c25..d81a906ba 100644 --- a/src/CustomFileInput.js +++ b/src/CustomFileInput.js @@ -82,7 +82,6 @@ class CustomFileInput extends React.Component { classNames( className, `custom-file`, - bsSize ? `custom-file-${bsSize}` : false, ), cssModule ); From def734dc7fb8a0719f16f587be72d2d1486fd54b Mon Sep 17 00:00:00 2001 From: David Ellingsworth Date: Tue, 16 Apr 2019 17:51:00 -0400 Subject: [PATCH 3/6] test(CustomFileInput): ensure onChange calls prop.onChange if it exists --- src/__tests__/CustomInput.spec.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/__tests__/CustomInput.spec.js b/src/__tests__/CustomInput.spec.js index e0f3ef361..e851bdcad 100644 --- a/src/__tests__/CustomInput.spec.js +++ b/src/__tests__/CustomInput.spec.js @@ -264,6 +264,16 @@ 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(); + }); + }); }); describe('CustomRange', () => { From 2f4b6b6e9e71dc9ec7dd8f3b84e2cec62191d57f Mon Sep 17 00:00:00 2001 From: David Ellingsworth Date: Tue, 16 Apr 2019 17:52:11 -0400 Subject: [PATCH 4/6] test(CustomFileInput): removes fakepath from file name --- src/__tests__/CustomInput.spec.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/__tests__/CustomInput.spec.js b/src/__tests__/CustomInput.spec.js index e851bdcad..d9ed75e41 100644 --- a/src/__tests__/CustomInput.spec.js +++ b/src/__tests__/CustomInput.spec.js @@ -274,6 +274,18 @@ describe('Custom Inputs', () => { 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'); + }); }); describe('CustomRange', () => { From 39f498d41bcada9196245e7a82882db414136c6f Mon Sep 17 00:00:00 2001 From: David Ellingsworth Date: Tue, 16 Apr 2019 17:53:06 -0400 Subject: [PATCH 5/6] test(CustomFileInput): lists multiple files when supported --- src/__tests__/CustomInput.spec.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/__tests__/CustomInput.spec.js b/src/__tests__/CustomInput.spec.js index d9ed75e41..7eec0ce97 100644 --- a/src/__tests__/CustomInput.spec.js +++ b/src/__tests__/CustomInput.spec.js @@ -286,6 +286,23 @@ describe('Custom Inputs', () => { 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', () => { From ab21fbfd2f123c9ea91297308576fa258c834567 Mon Sep 17 00:00:00 2001 From: David Ellingsworth Date: Thu, 22 Aug 2019 00:54:59 -0400 Subject: [PATCH 6/6] fix(CustomFileInput): include children in render --- src/CustomFileInput.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CustomFileInput.js b/src/CustomFileInput.js index d81a906ba..6ff4d0c55 100644 --- a/src/CustomFileInput.js +++ b/src/CustomFileInput.js @@ -85,7 +85,7 @@ class CustomFileInput extends React.Component { ), cssModule ); - + const validationClassNames = mapToCssModules( classNames( invalid && 'is-invalid', @@ -101,6 +101,7 @@ class CustomFileInput extends React.Component {
+ {children}
); }