Skip to content

Commit

Permalink
refactor: file picker component
Browse files Browse the repository at this point in the history
  • Loading branch information
Haofu Zhu committed Sep 9, 2020
1 parent 5a95fbc commit 8f1f580
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 151 deletions.
254 changes: 147 additions & 107 deletions src/components/FilePicker/index.jsx
Original file line number Diff line number Diff line change
@@ -1,129 +1,169 @@
import classNames from 'classnames';
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import React, { useState, useRef } from 'react';
import Button from 'react-bootstrap/lib/Button';
import './styles.scss';

const baseClass = 'filepicker-component';
const baseClass = 'aui--filepicker-component';

class FilePickerComponent extends React.PureComponent {
static propTypes = {
/**
* determines if the filePicker is disabled
*/
disabled: PropTypes.bool,
/**
* data-test-selector of the filePicker
*/
dts: PropTypes.string,
/**
* determines what file types the user can pick from the file input dialog box
*/
filter: PropTypes.string,
/**
* determines if the filePicker is highlighted or not
*/
isHighlighted: PropTypes.bool,
/**
* the label to be displayed
*/
label: PropTypes.string,
/**
* function called when onRemove event is fired
*/
onRemove: PropTypes.func,
/**
* function called when onSelect event is fired
*/
onSelect: PropTypes.func.isRequired,
/**
* determines the placeholder when no date is selected
*/
placeholder: PropTypes.string,
};

static defaultProps = {
isHighlighted: false,
label: 'Select',
placeholder: 'No file selected',
disabled: false,
};

constructor(props) {
super(props);

this.fileInput = React.createRef();
}
const FilePickerComponent = ({
disabled,
dts,
filter,
isHighlighted,
label,
onRemove,
onSelect,
onClick,
onChange,
value,
placeholder,
}) => {
const [isFileSelected, setIsFileSelected] = useState(false);
const [fileName, setFileName] = useState('');
const fileInput = useRef();

state = {
isFileSelected: false,
fileName: '',
};
const selectFile = event => {
if (!isFileSelected) {
const file = event.target.files[0];
setIsFileSelected(true);
if (_.isFunction(onChange)) {
onChange(file.name);
}
setFileName(file.name);

onChange = changeEvent => {
if (!this.state.isFileSelected) {
this.setState({ isFileSelected: true, fileName: changeEvent.target.files[0].name });
this.props.onSelect(changeEvent.target.files[0]);
onSelect(file);
}
};

onUploadBtnClick = () => {
this.fileInput.current.click();
const onUploadBtnClick = () => {
fileInput.current.click();
};

removeFile = () => {
if (this.state.isFileSelected) {
this.fileInput.current.value = null;
this.setState({ isFileSelected: false, fileName: '' });
if (this.props.onRemove) {
this.props.onRemove();
const removeFile = () => {
if (isFileSelected || !_.isEmpty(value)) {
fileInput.current.value = null;
setIsFileSelected(false);
if (_.isFunction(onChange)) {
onChange('');
}
setFileName('');

if (_.isFunction(onRemove)) {
onRemove();
}
}
};

render() {
const mainClass = classNames({ [`${baseClass}-highlight`]: this.props.isHighlighted }, baseClass, 'input-group');
const { isFileSelected, fileName } = this.state;
const clickFile = () => {
if (_.isFunction(onClick)) {
onClick();
}
};

if (value && !onChange)
console.warn(
'Failed prop type: You have provided a `value` prop to FilePicker Component without an `onChange` handler. This will render a read-only field.'
);

return (
<div data-testid="file-picker-wrapper" className={mainClass}>
<input
data-testid="file-picker-form-control"
className="form-control"
type="text"
disabled
placeholder={this.props.placeholder}
readOnly="readonly"
value={fileName}
title={fileName}
/>
<div className="input-group-btn">
{isFileSelected ? (
<Button data-testid="file-picker-remove-button" className="remove-file" onClick={this.removeFile}>
×
</Button>
) : null}
<Button
data-testid="file-picker-input-button"
className="btn-inverse"
onClick={this.onUploadBtnClick}
disabled={this.props.disabled || isFileSelected}
>
<span data-testid="file-picker-input-button-label">{this.props.label}</span>
<input
data-testid="file-picker-input-button-input"
className="file-input"
ref={this.fileInput}
type="file"
onChange={this.onChange}
accept={this.props.filter}
data-test-selector={this.props.dts}
/>
const hasFileName = _.isString(value) ? value !== '' : isFileSelected;

return (
<div
className={classNames({ [`${baseClass}-highlight`]: isHighlighted }, baseClass, 'input-group')}
data-test-selector={dts}
data-testid="file-picker-wrapper"
>
<input
data-testid="file-picker-form-control"
className={classNames('form-control', { clickable: _.isFunction(onClick) })}
type="text"
readOnly
placeholder={placeholder}
value={value || fileName}
title={value || fileName}
onClick={clickFile}
/>
<div className="input-group-btn">
{hasFileName ? (
<Button data-testid="file-picker-remove-button" className="remove-file" onClick={removeFile}>
×
</Button>
</div>
) : null}
<Button
data-testid="file-picker-input-button"
className="btn-inverse select-file"
onClick={onUploadBtnClick}
disabled={disabled || hasFileName}
>
<span data-testid="file-picker-input-button-label">{label}</span>
<input
data-testid="file-picker-input-button-input"
className="file-input"
ref={fileInput}
type="file"
onChange={selectFile}
accept={filter}
/>
</Button>
</div>
);
}
}
</div>
);
};

FilePickerComponent.propTypes = {
/**
* determines if the filePicker is disabled
*/
disabled: PropTypes.bool,
/**
* data-test-selector of the filePicker
*/
dts: PropTypes.string,
/**
* determines what file types the user can pick from the file input dialog box
*/
filter: PropTypes.string,
/**
* determines if the filePicker is highlighted or not
*/
isHighlighted: PropTypes.bool,
/**
* label on button
*/
label: PropTypes.string,
/**
* function called when onRemove event is fired
*/
onRemove: PropTypes.func,
/**
* function called when onSelect event is fired
*/
onSelect: PropTypes.func.isRequired,
/**
* function called when user click the file name
*/
onClick: PropTypes.func,
/**
* function called when the file name changes
*/
onChange: PropTypes.func,
/**
* file name on input
*/
value: PropTypes.string,
/**
* determines the placeholder when no date is selected
*/
placeholder: PropTypes.string,
};

FilePickerComponent.defaultProps = {
isHighlighted: false,
label: 'Select',
placeholder: 'No file selected',
disabled: false,
};

export default FilePickerComponent;
65 changes: 64 additions & 1 deletion src/components/FilePicker/index.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ afterEach(cleanup);
describe('<FilePicker />', () => {
it('should render with defaults', () => {
const { getByTestId } = render(<FilePickerComponent onSelect={jest.fn()} />);
expect(getByTestId('file-picker-wrapper')).toHaveClass('filepicker-component input-group');
expect(getByTestId('file-picker-wrapper')).toHaveClass('aui--filepicker-component input-group');

expect(getByTestId('file-picker-form-control')).toHaveClass('form-control');
expect(getByTestId('file-picker-form-control')).toHaveAttribute('placeholder', 'No file selected');
Expand Down Expand Up @@ -95,4 +95,67 @@ describe('<FilePicker />', () => {
expect(getByTestId('file-picker-form-control')).toHaveAttribute('title', '');
expect(onRemove).toHaveBeenCalledTimes(1);
});

it('should show value as file name, show remove button and disable input button if value is provided', () => {
const { getByTestId, queryAllByTestId } = render(
<FilePickerComponent onSelect={jest.fn()} value="custom_file_name" onChange={jest.fn()} />
);
expect(getByTestId('file-picker-form-control')).toHaveAttribute('value', 'custom_file_name');
expect(getByTestId('file-picker-form-control')).toHaveAttribute('title', 'custom_file_name');
expect(queryAllByTestId('file-picker-remove-button')).toHaveLength(1);
expect(getByTestId('file-picker-input-button')).toBeDisabled();
});

it('should remove file name, hide remove button and enable input button if value is set to empty string', () => {
const { getByTestId, queryAllByTestId, rerender } = render(
<FilePickerComponent onSelect={jest.fn()} value="custom_file_name" onChange={jest.fn()} />
);
expect(getByTestId('file-picker-form-control')).toHaveAttribute('value', 'custom_file_name');

rerender(<FilePickerComponent onSelect={jest.fn()} value="" />);
expect(getByTestId('file-picker-form-control')).toHaveAttribute('value', '');
expect(queryAllByTestId('file-picker-remove-button')).toHaveLength(0);
expect(getByTestId('file-picker-input-button')).toBeEnabled();
});

it('should show warning if value is provided but onChange is not provided', () => {
console.warn = jest.fn();

render(<FilePickerComponent onSelect={jest.fn()} value="custom_file_name" />);

expect(console.warn).toHaveBeenCalledWith(
'Failed prop type: You have provided a `value` prop to FilePicker Component without an `onChange` handler. This will render a read-only field.'
);
});

it('should call `onChange` with the file name when a file is selected', () => {
const onChange = jest.fn();
const { getByTestId } = render(
<FilePickerComponent onSelect={jest.fn()} value="custom_file_name" onChange={onChange} />
);
fireEvent.change(getByTestId('file-picker-input-button-input'), {
target: { files: [{ name: 'selected_file_name' }] },
});
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith('selected_file_name');
});

it('should call `onChange` with the empty string when a file is removed', () => {
const onChange = jest.fn();
const { getByTestId } = render(
<FilePickerComponent onSelect={jest.fn()} value="custom_file_name" onChange={onChange} />
);
fireEvent.click(getByTestId('file-picker-remove-button'));
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith('');
});

it('should call `onClick` when file name is clicked', () => {
const onClick = jest.fn();
const { getByTestId } = render(
<FilePickerComponent onSelect={jest.fn()} onClick={onClick} value="custom_file_name" onChange={jest.fn()} />
);
fireEvent.click(getByTestId('file-picker-form-control'));
expect(onClick).toHaveBeenCalledTimes(1);
});
});
20 changes: 14 additions & 6 deletions src/components/FilePicker/styles.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@import '~styles/color';

.filepicker-component {
.aui--filepicker-component {
&-highlight {
border: 1px solid $color-border-error;
border-radius: 2px;
Expand All @@ -15,15 +15,23 @@
margin-right: 0;
}

.select-file.btn {
box-shadow: none;
padding-bottom: 4px;
z-index: 3;
}

.file-input {
display: none;
}
}

.has-error {
.filepicker-component {
.form-control {
border: 0; // Error style is applied to .filepicker-component-highlight
.form-control {
border: 0;
cursor: not-allowed;

&.clickable {
cursor: pointer;
}
}
}

Loading

0 comments on commit 8f1f580

Please sign in to comment.