UNPKG

@wfp/ui

Version:
455 lines (403 loc) 11.4 kB
/** * Copyright IBM Corp. 2016, 2018 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ /* eslint react/no-multi-comp: "off" */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import settings from '../../globals/js/settings'; /* import { Close16, WarningFilled16, CheckmarkFilled16, } from '@carbon/icons-react'; */ import { iconCheckmarkGlyph, iconCloseGlyph, iconWarningGlyph, } from '@wfp/icons'; import Icon from '../Icon'; import Loading from '../Loading'; import uid from '../../tools/uniqueId'; import { ButtonTypes } from '../../prop-types/types'; const { prefix } = settings; export class FileUploaderButton extends Component { state = {}; static propTypes = { /** * Provide a custom className to be applied to the container node */ className: PropTypes.string, /** * Specify whether you want to disable any updates to the FileUploaderButton * label */ disableLabelChanges: PropTypes.bool, /** * Provide a unique id for the underlying <input> node */ id: PropTypes.string, /** * Provide the label text to be read by screen readers when interacting with * this control */ labelText: PropTypes.node, /** * Specify whether you want the component to list the files that have been * submitted to be uploaded */ listFiles: PropTypes.bool, /** * Specify if the component should accept multiple files to upload */ multiple: PropTypes.bool, /** * Provide a name for the underlying <input> node */ name: PropTypes.string, /** * Provide an optional `onChange` hook that is called each time the <input> * value changes */ onChange: PropTypes.func, /** * Provide an optional `onClick` hook that is called each time the button is * clicked */ onClick: PropTypes.func, /** * Provide an accessibility role for the <FileUploaderButton> */ role: PropTypes.string, /** * Provide a custom tabIndex value for the <FileUploaderButton> */ tabIndex: PropTypes.number, /** * Specify the type of underlying button */ buttonKind: ButtonTypes.buttonKind, /** * Specify the types of files that this input should be able to receive */ accept: PropTypes.arrayOf(PropTypes.string), /** * Specify whether file input is disabled */ disabled: PropTypes.bool, }; static defaultProps = { tabIndex: 0, disableLabelChanges: false, labelText: 'Add file', buttonKind: 'primary', multiple: false, onChange: () => {}, onClick: () => {}, accept: [], disabled: false, role: 'button', }; static getDerivedStateFromProps({ labelText }, state) { const { prevLabelText } = state; return prevLabelText === labelText ? null : { labelText, prevLabelText: labelText, }; } handleChange = (evt) => { const files = evt.target.files; const length = evt.target.files.length; if (files && !this.props.disableLabelChanges) { if (length > 1) { this.setState({ labelText: `${length} files` }); } else if (length === 1) { this.setState({ labelText: files[0].name }); } } this.props.onChange(evt); }; render() { const { className, disableLabelChanges, // eslint-disable-line labelText, // eslint-disable-line multiple, role, tabIndex, buttonKind, accept, name, disabled, ...other } = this.props; const classes = classNames(`${prefix}--btn`, className, { [`${prefix}--btn--${buttonKind}`]: buttonKind, [`${prefix}--btn--disabled`]: disabled, }); this.uid = this.props.id || uid(); return ( <> <label tabIndex={disabled ? -1 : tabIndex || 0} aria-disabled={disabled} className={classes} onKeyDown={(evt) => { if (evt.which === 13 || evt.which === 32) { this.input.click(); } }} htmlFor={this.uid} {...other} > <span role={role}>{this.state.labelText}</span> </label> <input className={`${prefix}--visually-hidden`} ref={(input) => (this.input = input)} id={this.uid} disabled={disabled} type="file" tabIndex="-1" multiple={multiple} accept={accept} name={name} onChange={this.handleChange} onClick={(evt) => { evt.target.value = null; }} /> </> ); } } export function Filename({ iconDescription, status, invalid, ...other }) { switch (status) { case 'uploading': return ( <Loading description={iconDescription} withOverlay={false} small /> ); case 'edit': return ( <> {invalid && ( <Icon icon={iconWarningGlyph} className={`${prefix}--file-invalid`} /> )} <Icon icon={iconCloseGlyph} className={`${prefix}--file-close`} aria-label={iconDescription} {...other} /> {/*<Close16 className={`${prefix}--file-close`} aria-label={iconDescription} {...other}> {iconDescription && <title>{iconDescription}</title>} </Close16>*/} </> ); case 'complete': return ( <Icon icon={iconCheckmarkGlyph} className={`${prefix}--file-complete`} aria-label={iconDescription} {...other} /> ); default: return null; } } Filename.propTypes = { /** * Provide a description of the SVG icon to denote file upload status */ iconDescription: PropTypes.string, /** * Status of the file upload */ status: PropTypes.oneOf(['edit', 'complete', 'uploading']), /** * Provide a custom tabIndex value for the <Filename> */ tabIndex: PropTypes.string, }; Filename.defaultProps = { iconDescription: 'Uploading file', status: 'uploading', tabIndex: '0', }; export default class FileUploader extends Component { static propTypes = { /** * Provide a description for the complete/close icon that can be read by screen readers */ iconDescription: PropTypes.string, /** * Provide the label text to be read by screen readers when interacting with * the <FileUploaderButton> */ buttonLabel: PropTypes.string, /** * Specify the type of the <FileUploaderButton> */ //buttonKind: PropTypes.oneOf(buttonKinds), /** * Specify the status of the File Upload */ //filenameStatus: PropTypes.oneOf(['edit', 'complete', 'uploading']) // .isRequired, /** * Specify the description text of this <FileUploader> */ labelDescription: PropTypes.string, /** * Specify the title text of this <FileUploader> */ labelTitle: PropTypes.string, /** * Specify if the component should accept multiple files to upload */ multiple: PropTypes.bool, /** * Provide a name for the underlying <input> node */ name: PropTypes.string, /** * Provide an optional `onChange` hook that is called each time the input is * changed */ onChange: PropTypes.func, /** * Provide an optional `onClick` hook that is called each time the button is * clicked */ onClick: PropTypes.func, /** * Provide a custom className to be applied to the container node */ className: PropTypes.string, /** * Specify the types of files that this input should be able to receive */ //accept: PropTypes.arrayOf(PropTypes.string), }; static defaultProps = { iconDescription: 'Provide icon description', filenameStatus: 'uploading', buttonLabel: '', buttonKind: 'primary', multiple: false, onClick: () => {}, accept: [], }; state = { filenames: [], }; nodes = []; static getDerivedStateFromProps({ filenameStatus }, state) { const { prevFilenameStatus } = state; return prevFilenameStatus === filenameStatus ? null : { filenameStatus, prevFilenameStatus: filenameStatus, }; } handleChange = (evt) => { evt.stopPropagation(); this.setState({ filenames: this.state.filenames.concat( Array.prototype.map.call(evt.target.files, (file) => file.name) ), }); if (this.props.onChange) { this.props.onChange(evt); } }; handleClick = (evt, index) => { const filteredArray = this.state.filenames.filter( (filename) => filename !== this.nodes[index].innerText.trim() ); this.setState({ filenames: filteredArray }); this.props.onClick(evt); }; clearFiles = () => { // A clearFiles function that resets filenames and can be referenced using a ref by the parent. this.setState({ filenames: [] }); }; render() { const { iconDescription, buttonLabel, buttonKind, filenameStatus, labelDescription, labelTitle, className, multiple, accept, name, ...other } = this.props; const classes = classNames({ [`${prefix}--form-item`]: true, [className]: className, }); return ( <div className={classes} {...other}> <strong className={`${prefix}--file--label`}>{labelTitle}</strong> <p className={`${prefix}--label-description`}>{labelDescription}</p> <FileUploaderButton labelText={buttonLabel} multiple={multiple} buttonKind={buttonKind} onChange={this.handleChange} disableLabelChanges accept={accept} name={name} /> <div className={`${prefix}--file-container`}> {this.state.filenames.length === 0 ? null : this.state.filenames.map((name, index) => ( <span key={index} className={`${prefix}--file__selected-file`} ref={(node) => (this.nodes[index] = node)} // eslint-disable-line {...other} > <p className={`${prefix}--file-filename`}>{name}</p> <span className={`${prefix}--file__state-container`}> <Filename iconDescription={iconDescription} status={filenameStatus} onKeyDown={(evt) => { if (evt.which === 13 || evt.which === 32) { this.handleClick(evt, index); } }} onClick={(evt) => { if (filenameStatus === 'edit') { this.handleClick(evt, index); } }} /> </span> </span> ))} </div> </div> ); } }