@react-ui-org/react-ui
Version:
React UI is a themeable UI library for React apps.
337 lines (309 loc) • 9.76 kB
JSX
import PropTypes from 'prop-types';
import React, {
useCallback,
useEffect,
useContext,
useImperativeHandle,
useRef,
useState,
} from 'react';
import { withGlobalProps } from '../../providers/globalProps';
import { classNames } from '../../helpers/classNames';
import { transferProps } from '../../helpers/transferProps';
import { TranslationsContext } from '../../providers/translations';
import { getRootSizeClassName } from '../_helpers/getRootSizeClassName';
import { getRootValidationStateClassName } from '../_helpers/getRootValidationStateClassName';
import { resolveContextOrProp } from '../_helpers/resolveContextOrProp';
import { InputGroupContext } from '../InputGroup';
import { Text } from '../Text';
import { FormLayoutContext } from '../FormLayout';
import styles from './FileInputField.module.scss';
/* istanbul ignore file */
export const FileInputField = React.forwardRef((props, ref) => {
const {
disabled,
fullWidth,
helpText,
id,
isLabelVisible,
label,
layout,
multiple,
onFilesChanged,
required,
size,
validationState,
validationText,
...restProps
} = props;
const formLayoutContext = useContext(FormLayoutContext);
const inputGroupContext = useContext(InputGroupContext);
const translations = useContext(TranslationsContext);
const [selectedFileNames, setSelectedFileNames] = useState([]);
const [isDragging, setIsDragging] = useState(false);
const internalInputRef = useRef();
const handleReset = useCallback((event) => {
if (internalInputRef.current) {
internalInputRef.current.value = '';
}
setSelectedFileNames([]);
onFilesChanged([], event);
}, [onFilesChanged]);
// We need to have a reference to the input element to be able to call its methods,
// but at the same time we want to expose this reference to the parent component in
// case someone wants to call input methods from outside the component.
useImperativeHandle(
ref,
() => {
// The reason of extending object instead of using spread operator is that
// if it is transformed to the object, it changes the reference of the object
// and its prototype chain.
const inputEl = internalInputRef?.current ?? {};
inputEl.resetState = () => {
handleReset(null);
};
return inputEl;
},
[handleReset],
);
const handleFileChange = (files, event) => {
if (files.length === 0) {
setSelectedFileNames([]);
onFilesChanged([], event);
return;
}
// Mimic the native behavior of the `input` element: if multiple files are selected and the input
// does not accept multiple files, no files are processed.
if (files.length > 1 && !multiple) {
setSelectedFileNames([]);
onFilesChanged([], event);
return;
}
const fileNames = [];
[...files].forEach((file) => {
fileNames.push(file.name);
});
setSelectedFileNames(fileNames);
onFilesChanged(files, event);
};
const handleInputChange = (event) => {
handleFileChange(event.target.files, event);
};
const handleClick = () => {
internalInputRef?.current.click();
};
const handleDrop = (event) => {
event.preventDefault();
handleFileChange(event.dataTransfer.files, event);
setIsDragging(false);
};
const handleDragOver = (event) => {
if (!isDragging) {
setIsDragging(true);
}
event.preventDefault();
};
const handleDragLeave = () => {
if (isDragging) {
setIsDragging(false);
}
};
useEffect(() => {
const inputEl = internalInputRef.current;
if (!inputEl) {
return () => {};
}
const { form } = inputEl;
if (!form) {
return () => {};
}
form.addEventListener('reset', handleReset);
return () => {
form.removeEventListener('reset', handleReset);
};
}, [handleReset]);
return (
<div
className={classNames(
styles.root,
fullWidth && styles.isRootFullWidth,
formLayoutContext && styles.isRootInFormLayout,
resolveContextOrProp(formLayoutContext && formLayoutContext.layout, layout) === 'horizontal'
? styles.isRootLayoutHorizontal
: styles.isRootLayoutVertical,
resolveContextOrProp(inputGroupContext && inputGroupContext.disabled, disabled) && styles.isRootDisabled,
inputGroupContext && styles.isRootGrouped,
isDragging && styles.isRootDragging,
required && styles.isRootRequired,
getRootSizeClassName(
resolveContextOrProp(inputGroupContext && inputGroupContext.size, size),
styles,
),
getRootValidationStateClassName(validationState, styles),
)}
id={`${id}__root`}
onDragLeave={!disabled ? handleDragLeave : undefined}
onDragOver={!disabled ? handleDragOver : undefined}
onDrop={!disabled ? handleDrop : undefined}
>
<label
className={classNames(
styles.label,
(!isLabelVisible || inputGroupContext) && styles.isLabelHidden,
)}
htmlFor={id}
id={`${id}__labelText`}
>
{label}
</label>
<div className={styles.field}>
<div className={styles.inputContainer}>
<input
{...transferProps(restProps)}
className={styles.input}
disabled={resolveContextOrProp(inputGroupContext && inputGroupContext.disabled, disabled)}
id={id}
multiple={multiple}
onChange={handleInputChange}
ref={internalInputRef}
required={required}
tabIndex={-1}
type="file"
/>
<button
className={styles.dropZone}
disabled={resolveContextOrProp(inputGroupContext && inputGroupContext.disabled, disabled)}
onClick={handleClick}
type="button"
>
<Text lines={1}>
{isDragging && (
<span className={styles.dropFileHereText}>
{translations.FileInputField.dropFileHere}
</span>
)}
{!isDragging && !selectedFileNames.length && (
<>
<span className={styles.dropZoneLink}>{translations.FileInputField.browse}</span>
{' '}
{translations.FileInputField.drop}
</>
)}
{!isDragging && selectedFileNames.length === 1 && selectedFileNames[0]}
{!isDragging && selectedFileNames.length > 1 && (
<>
{selectedFileNames.length}
{' '}
{translations.FileInputField.filesSelected}
</>
)}
</Text>
</button>
</div>
{(helpText && !inputGroupContext) && (
<div
className={styles.helpText}
id={`${id}__helpText`}
>
{helpText}
</div>
)}
{(validationText && !inputGroupContext) && (
<div
className={styles.validationText}
id={`${id}__validationText`}
>
{validationText}
</div>
)}
</div>
</div>
);
});
FileInputField.defaultProps = {
disabled: false,
fullWidth: false,
helpText: undefined,
isLabelVisible: true,
layout: 'vertical',
multiple: false,
required: false,
size: 'medium',
validationState: undefined,
validationText: undefined,
};
FileInputField.propTypes = {
/**
* If `true`, the input will be disabled.
*/
disabled: PropTypes.bool,
/**
* If `true`, the field will span the full width of its parent.
*/
fullWidth: PropTypes.bool,
/**
* Optional help text.
*
* Help text is never rendered when the component is placed into `InputGroup`.
* If a help text is needed, it must be defined on the `InputGroup` component instead.
*/
helpText: PropTypes.node,
/**
* ID of the `<input>` HTML element.
*
* Also serves as base for ids of nested elements:
* * `<ID>__label`
* * `<ID>__labelText`
* * `<ID>__helpText`
* * `<ID>__validationText`
*/
id: PropTypes.string.isRequired,
/**
* If `false`, the label will be visually hidden (but remains accessible by assistive
* technologies).
*/
isLabelVisible: PropTypes.bool,
/**
* File input field label.
*/
label: PropTypes.node.isRequired,
/**
* Layout of the field.
*
* Ignored if the component is rendered within `FormLayout` component
* as the value is inherited in such case.
*
*/
layout: PropTypes.oneOf(['horizontal', 'vertical']),
/**
* If `true`, the input will accept multiple files.
*/
multiple: PropTypes.bool,
/**
* Callback fired when the value of the input changes.
*/
onFilesChanged: PropTypes.func.isRequired,
/**
* If `true`, the input will be required.
*/
required: PropTypes.bool,
/**
* Size of the field.
*
* Ignored if the component is rendered within `InputGroup` component as the value is inherited in such case.
*/
size: PropTypes.oneOf(['small', 'medium', 'large']),
/**
* Alter the field to provide feedback based on validation result.
*/
validationState: PropTypes.oneOf(['invalid', 'valid', 'warning']),
/**
* Validation message to be displayed.
*
* Validation text is never rendered when the component is placed into `InputGroup`.
* If a validation text is needed, it must be defined on the `InputGroup` component instead.
*/
validationText: PropTypes.node,
};
export const FileInputFieldWithGlobalProps = withGlobalProps(FileInputField, 'FileInputField');
export default FileInputFieldWithGlobalProps;