-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor: file picker component #1072
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(); | ||
Comment on lines
+48
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not quite sure about this. Triggering multiple handlers here seems to be a little bit confusing to me. the usage should be either // handle remove with onChange('')
handleChange = (value) => {
if(value==='') {/* handle remove */}
} or // setValue('') with onRemove()
handleRemove = () => setValue('') |
||
} | ||
} | ||
}; | ||
|
||
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} | ||
/> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @superpower1 @minhe86 can you put There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will put dts to the wrapper div instead |
||
<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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't exactly recollect the discussion, however, didn't we decide to cut down the handler functions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we were planning to retain the |
||
/** | ||
* 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should probably provide the actual file for the
onChange
instead of just the name and allow the external component to put the file's name into the value. In those scenarios they would also not need to useonSelect
since they have the power to do whatever and they also have the file itself to manage rather than it being hidden in the component.