diff --git a/src/components/FilePicker/index.jsx b/src/components/FilePicker/index.jsx index 151f9bb2d..4c0b44ad7 100644 --- a/src/components/FilePicker/index.jsx +++ b/src/components/FilePicker/index.jsx @@ -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 ( -
- -
- {isFileSelected ? ( - - ) : null} - -
+ ) : null} +
- ); - } -} + + ); +}; + +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; diff --git a/src/components/FilePicker/index.spec.jsx b/src/components/FilePicker/index.spec.jsx index 27eb49652..8667a2632 100644 --- a/src/components/FilePicker/index.spec.jsx +++ b/src/components/FilePicker/index.spec.jsx @@ -7,8 +7,9 @@ afterEach(cleanup); describe('', () => { it('should render with defaults', () => { - const { getByTestId } = render(); - expect(getByTestId('file-picker-wrapper')).toHaveClass('filepicker-component input-group'); + const { getByTestId } = render(); + expect(getByTestId('file-picker-wrapper')).toHaveClass('aui--filepicker-component input-group'); + expect(getByTestId('file-picker-wrapper')).toHaveAttribute('data-test-selector', 'test-file-picker-input'); expect(getByTestId('file-picker-form-control')).toHaveClass('form-control'); expect(getByTestId('file-picker-form-control')).toHaveAttribute('placeholder', 'No file selected'); @@ -20,9 +21,7 @@ describe('', () => { it('should show remove button and call `onSelect` when file selected', () => { const onSelect = jest.fn(); - const { getByTestId, queryAllByTestId } = render( - - ); + const { getByTestId, queryAllByTestId } = render(); expect(getByTestId('file-picker-form-control')).toHaveAttribute('title', ''); expect(getByTestId('file-picker-form-control')).toHaveAttribute('placeholder', 'No file selected'); @@ -30,11 +29,6 @@ describe('', () => { expect(queryAllByTestId('file-picker-remove-button')).toHaveLength(0); expect(getByTestId('file-picker-input-button')).toBeEnabled(); - expect(getByTestId('file-picker-input-button-input')).toHaveAttribute( - 'data-test-selector', - 'test-file-picker-input' - ); - fireEvent.change(getByTestId('file-picker-input-button-input'), { target: { files: [{ name: 'selected file' }] } }); expect(onSelect).toHaveBeenCalledTimes(1); expect(onSelect).toHaveBeenCalledWith({ name: 'selected file' }); @@ -95,4 +89,67 @@ describe('', () => { 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( + + ); + 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( + + ); + expect(getByTestId('file-picker-form-control')).toHaveAttribute('value', 'custom_file_name'); + + rerender(); + 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(); + + 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( + + ); + 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( + + ); + 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( + + ); + fireEvent.click(getByTestId('file-picker-form-control')); + expect(onClick).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/components/FilePicker/styles.scss b/src/components/FilePicker/styles.scss index 5ec8d77b4..f4906f333 100644 --- a/src/components/FilePicker/styles.scss +++ b/src/components/FilePicker/styles.scss @@ -1,6 +1,6 @@ @import '~styles/color'; -.filepicker-component { +.aui--filepicker-component { &-highlight { border: 1px solid $color-border-error; border-radius: 2px; @@ -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; } } } + diff --git a/src/components/RichTextEditor/index.jsx b/src/components/RichTextEditor/index.jsx index 68885d442..20c4542b0 100644 --- a/src/components/RichTextEditor/index.jsx +++ b/src/components/RichTextEditor/index.jsx @@ -8,7 +8,7 @@ import InlineStyleButtons from './InlineStyleButtons'; import BlockStyleButtons from './BlockStyleButtons'; import './styles.scss'; -const RichTextEditor = ({ className, value, initialValue, onChange, placeholder }) => { +const RichTextEditor = ({ className, value, initialValue, onChange, placeholder, dts }) => { const editor = React.createRef(null); const focusEditor = () => editor.current.focus(); @@ -40,7 +40,7 @@ const RichTextEditor = ({ className, value, initialValue, onChange, placeholder }; return ( -
+
draft-js editor state */ value: PropTypes.instanceOf(EditorState), onChange: PropTypes.func, + /** + * data-test-selector of the rich text editor + */ + dts: PropTypes.string, }; RichTextEditor.defaultProps = { diff --git a/www/containers/props.json b/www/containers/props.json index f9b9e7d10..c8d8008fc 100644 --- a/www/containers/props.json +++ b/www/containers/props.json @@ -1332,34 +1332,7 @@ { "description": "", "displayName": "FilePickerComponent", - "methods": [ - { - "name": "onChange", - "docblock": null, - "modifiers": [], - "params": [ - { - "name": "changeEvent", - "type": null - } - ], - "returns": null - }, - { - "name": "onUploadBtnClick", - "docblock": null, - "modifiers": [], - "params": [], - "returns": null - }, - { - "name": "removeFile", - "docblock": null, - "modifiers": [], - "params": [], - "returns": null - } - ], + "methods": [], "props": { "disabled": { "type": { @@ -1402,7 +1375,7 @@ "name": "string" }, "required": false, - "description": "the label to be displayed", + "description": "label on button", "defaultValue": { "value": "'Select'", "computed": false @@ -1422,6 +1395,27 @@ "required": true, "description": "function called when onSelect event is fired" }, + "onClick": { + "type": { + "name": "func" + }, + "required": false, + "description": "function called when user click the file name" + }, + "onChange": { + "type": { + "name": "func" + }, + "required": false, + "description": "function called when the file name changes" + }, + "value": { + "type": { + "name": "string" + }, + "required": false, + "description": "file name on input" + }, "placeholder": { "type": { "name": "string" @@ -3480,6 +3474,13 @@ }, "required": false, "description": "" + }, + "dts": { + "type": { + "name": "string" + }, + "required": false, + "description": "data-test-selector of the rich text editor" } } } diff --git a/www/examples/FilePicker.mdx b/www/examples/FilePicker.mdx index 117eca991..22d5381fd 100644 --- a/www/examples/FilePicker.mdx +++ b/www/examples/FilePicker.mdx @@ -4,12 +4,20 @@ import DesignNotes from '../containers/DesignNotes.jsx'; ## File Picker ```jsx live=true -class FilePickerExample extends React.PureComponent { - render() { - const onSelect = _.noop; - return ; - } -} +const FilePickerExample = () => { + const onSelect = _.noop; + const [fileName, setFileName] = React.useState(''); + + return ( + <> + Uncontrolled: + +
+ Controlled: + setFileName(fileName)} /> + + ); +}; render(