Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
fix(CustomInput[type=file]): add CustomFileInput component (#1461)
* fix(CustomInput[type=file]): add CustomFileInput component
to display the selected files in the label.

This fixes #1460.

* fix(CustomFileInput): Remove bsSize class as it does not apply

* test(CustomFileInput): ensure onChange calls prop.onChange if it exists

* test(CustomFileInput): removes fakepath from file name

* test(CustomFileInput): lists multiple files when supported

* fix(CustomFileInput): include children in render
  • Loading branch information
deAtog authored and TheSharpieOne committed Aug 22, 2019
1 parent c07e777 commit 25e2480
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 6 deletions.
112 changes: 112 additions & 0 deletions 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 (
<div className={customClass}>
<input type="file" {...attributes} ref={innerRef} className={classNames(validationClassNames, mapToCssModules('custom-file-input', cssModule))} onChange={this.onChange}/>
<label className={mapToCssModules('custom-file-label', cssModule)} htmlFor={labelHtmlFor}>{files || label || 'Choose file'}</label>
{children}
</div>
);
}
}

CustomFileInput.propTypes = propTypes;

export default CustomFileInput;
6 changes: 2 additions & 4 deletions src/CustomInput.js
Expand Up @@ -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,
Expand Down Expand Up @@ -58,10 +59,7 @@ function CustomInput(props) {

if (type === 'file') {
return (
<div className={customClass}>
<input {...attributes} ref={innerRef} className={classNames(validationClassNames, mapToCssModules('custom-file-input', cssModule))} />
<label className={mapToCssModules('custom-file-label', cssModule)} htmlFor={labelHtmlFor}>{label || 'Choose file'}</label>
</div>
<CustomFileInput {...props}/>
);
}

Expand Down
43 changes: 41 additions & 2 deletions src/__tests__/CustomInput.spec.js
Expand Up @@ -212,8 +212,8 @@ describe('Custom Inputs', () => {

describe('CustomFile', () => {
it('should render children in the outer div', () => {
const file = shallow(<CustomInput type="file" />);
expect(file.type()).toBe('div');
const file = mount(<CustomInput type="file" />);
expect(file.find('.custom-file').first().type()).toBe('div');
});

it('should add class is-invalid when invalid is true', () => {
Expand Down Expand Up @@ -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(<CustomInput type="file" onChange={onChange} />);

file.find('input').hostNodes().simulate('change');
expect(onChange).toHaveBeenCalled();
});
});

it('removes fakepath from file name', () => {
const file = mount(<CustomInput type="file" />);

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(<CustomInput type="file" multiple/>);

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', () => {
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Expand Up @@ -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';
Expand Down

0 comments on commit 25e2480

Please sign in to comment.