UNPKG

@react-ui-org/react-ui

Version:

React UI is a themeable UI library for React apps.

337 lines (309 loc) 9.76 kB
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;