UNPKG

@hackplan/polaris

Version:

Shopify’s product component library

300 lines (299 loc) 12.1 kB
import React from 'react'; import { createUniqueIDFactory } from '@shopify/javascript-utilities/other'; import debounce from 'lodash/debounce'; import { addEventListener, removeEventListener, } from '@shopify/javascript-utilities/events'; import { DragDropMajorMonotone, CircleAlertMajorMonotone, } from '@shopify/polaris-icons'; import { classNames } from '../../utilities/css'; import capitalize from '../../utilities/capitalize'; import Icon from '../Icon'; import Stack from '../Stack'; import Caption from '../Caption'; import DisplayText from '../DisplayText'; import VisuallyHidden from '../VisuallyHidden'; import Labelled from '../Labelled'; import { withAppProvider } from '../AppProvider'; import { FileUpload } from './components'; import DropZoneContext from './context'; import { fileAccepted, getDataTransferFiles } from './utils'; import styles from './DropZone.scss'; const getUniqueID = createUniqueIDFactory('DropZone'); export class DropZone extends React.Component { constructor(props) { super(props); this.node = React.createRef(); this.dragTargets = []; this.fileInputNode = React.createRef(); this.adjustSize = debounce(() => { if (!this.node.current) { return; } let size = 'extraLarge'; const width = this.node.current.getBoundingClientRect().width; if (width < 100) { size = 'small'; } else if (width < 160) { size = 'medium'; } else if (width < 300) { size = 'large'; } this.setState({ size }); }, 50, { trailing: true }); this.triggerFileDialog = () => { this.open(); if (this.props.onFileDialogClose) { this.props.onFileDialogClose(); } }; this.open = () => { if (!this.fileInputNode.current) { return; } this.fileInputNode.current.click(); }; this.getValidatedFiles = (files) => { const { accept, allowMultiple, customValidator } = this.props; const acceptedFiles = []; const rejectedFiles = []; Array.from(files).forEach((file) => { if (!fileAccepted(file, accept) || (customValidator && !customValidator(file))) { rejectedFiles.push(file); } else { acceptedFiles.push(file); } }); if (!allowMultiple) { acceptedFiles.splice(1, acceptedFiles.length); rejectedFiles.push(...acceptedFiles.slice(1)); } return { files, acceptedFiles, rejectedFiles, }; }; this.handleClick = (event) => { const { numFiles } = this.state; const { onClick, disabled, allowMultiple } = this.props; if (disabled || (!allowMultiple && numFiles > 0)) { return; } return onClick ? onClick(event) : this.open(); }; this.handleDrop = (event) => { event.preventDefault(); event.stopPropagation(); const { disabled, onDrop, onDropAccepted, onDropRejected, allowMultiple, } = this.props; const { numFiles } = this.state; if (disabled || (!allowMultiple && numFiles > 0)) { return; } const fileList = getDataTransferFiles(event); const { files, acceptedFiles, rejectedFiles } = this.getValidatedFiles(fileList); this.dragTargets = []; this.setState((prev) => ({ dragging: false, error: rejectedFiles.length > 0, numFiles: prev.numFiles + acceptedFiles.length, })); if (onDrop) { onDrop(files, acceptedFiles, rejectedFiles); } if (onDropAccepted && acceptedFiles.length) { onDropAccepted(acceptedFiles); } if (onDropRejected && rejectedFiles.length) { onDropRejected(rejectedFiles); } event.target.value = ''; }; this.handleDragEnter = (event) => { event.preventDefault(); event.stopPropagation(); const { dragging, numFiles } = this.state; const { disabled, onDragEnter, allowMultiple } = this.props; if (disabled || (!allowMultiple && numFiles > 0)) { return; } const fileList = getDataTransferFiles(event); if (event.target && this.dragTargets.indexOf(event.target) === -1) { this.dragTargets.push(event.target); } if (dragging) { return false; } const { rejectedFiles } = this.getValidatedFiles(fileList); this.setState({ dragging: true, error: rejectedFiles.length > 0 }); if (onDragEnter) { onDragEnter(); } }; this.handleDragOver = (event) => { event.preventDefault(); event.stopPropagation(); const { numFiles } = this.state; const { disabled, onDragOver, allowMultiple } = this.props; if (disabled || (!allowMultiple && numFiles > 0)) { return; } if (onDragOver) { onDragOver(); } return false; }; this.handleDragLeave = (event) => { event.preventDefault(); const { numFiles } = this.state; const { disabled, onDragLeave, allowMultiple } = this.props; if (disabled || (!allowMultiple && numFiles > 0)) { return; } this.dragTargets = this.dragTargets.filter((el) => { return el !== event.target && this.dropNode && this.dropNode.contains(el); }); if (this.dragTargets.length > 0) { return; } this.setState({ dragging: false, error: false }); if (onDragLeave) { onDragLeave(); } }; const { polaris: { intl: { translate }, }, type, } = props; const suffix = capitalize(type); this.state = { type, id: props.id || getUniqueID(), size: 'extraLarge', dragging: false, error: false, overlayText: translate(`Polaris.DropZone.overlayText${suffix}`), errorOverlayText: translate(`Polaris.DropZone.errorOverlayText${suffix}`), numFiles: 0, }; } static getDerivedStateFromProps(nextProps, prevState) { const { id, error, type, overlayText, errorOverlayText } = prevState; const newState = {}; if (nextProps.id != null && id !== nextProps.id) { newState.id = nextProps.id || id; } if (nextProps.error != null && error !== nextProps.error) { newState.error = nextProps.error; } if (nextProps.type != null && type !== nextProps.type) { newState.type = nextProps.type; } if (nextProps.overlayText != null && overlayText !== nextProps.overlayText) { newState.overlayText = nextProps.overlayText; } if (nextProps.errorOverlayText != null && errorOverlayText !== nextProps.errorOverlayText) { newState.errorOverlayText = nextProps.errorOverlayText; } return Object.keys(newState).length ? newState : null; } get dropNode() { return this.props.dropOnPage ? document : this.node.current; } render() { const { id, dragging, error, size, type, overlayText, errorOverlayText, } = this.state; const { label, labelAction, labelHidden, children, disabled, outline, accept, active, overlay, allowMultiple, polaris: { intl }, } = this.props; const inputAttributes = { id, accept, disabled, type: 'file', multiple: allowMultiple, ref: this.fileInputNode, onChange: this.handleDrop, autoComplete: 'off', }; const classes = classNames(styles.DropZone, outline && styles.hasOutline, (active || dragging) && styles.isDragging, error && styles.hasError, size && size === 'extraLarge' && styles.sizeExtraLarge, size && size === 'large' && styles.sizeLarge, size && size === 'medium' && styles.sizeMedium, size && size === 'small' && styles.sizeSmall); const dragOverlay = (active || dragging) && !error && overlay ? (<div className={styles.Overlay}> <Stack vertical spacing="tight"> <Icon source={DragDropMajorMonotone} color="indigo"/> {size === 'extraLarge' && (<DisplayText size="small" element="p"> {overlayText} </DisplayText>)} {(size === 'medium' || size === 'large') && (<Caption>{overlayText}</Caption>)} </Stack> </div>) : null; const dragErrorOverlay = dragging && error ? (<div className={styles.Overlay}> <Stack vertical spacing="tight"> <Icon source={CircleAlertMajorMonotone} color="red"/> {size === 'extraLarge' && (<DisplayText size="small" element="p"> {errorOverlayText} </DisplayText>)} {(size === 'medium' || size === 'large') && (<Caption>{errorOverlayText}</Caption>)} </Stack> </div>) : null; const labelValue = label ? label : intl.translate('Polaris.DropZone.FileUpload.label'); const labelHiddenValue = label ? labelHidden : true; const context = { size, type: type || 'file', }; return (<DropZoneContext.Provider value={context}> <Labelled id={id} label={labelValue} action={labelAction} labelHidden={labelHiddenValue}> <div ref={this.node} className={classes} aria-disabled={disabled} onClick={this.handleClick} onDragStart={handleDragStart}> {dragOverlay} {dragErrorOverlay} <div className={styles.Container}>{children}</div> <VisuallyHidden> <input {...inputAttributes}/> </VisuallyHidden> </div> </Labelled> </DropZoneContext.Provider>); } componentDidMount() { this.dragTargets = []; this.setState({ error: this.props.error }); if (!this.dropNode) { return; } addEventListener(this.dropNode, 'drop', this.handleDrop); addEventListener(this.dropNode, 'dragover', this.handleDragOver); addEventListener(this.dropNode, 'dragenter', this.handleDragEnter); addEventListener(this.dropNode, 'dragleave', this.handleDragLeave); addEventListener(window, 'resize', this.adjustSize); this.adjustSize(); if (this.props.openFileDialog) { this.triggerFileDialog(); } } componentWillUnmount() { if (!this.dropNode) { return; } removeEventListener(this.dropNode, 'drop', this.handleDrop); removeEventListener(this.dropNode, 'dragover', this.handleDragOver); removeEventListener(this.dropNode, 'dragenter', this.handleDragEnter); removeEventListener(this.dropNode, 'dragleave', this.handleDragLeave); removeEventListener(window, 'resize', this.adjustSize); } componentDidUpdate() { if (this.props.openFileDialog) { this.triggerFileDialog(); } } } DropZone.FileUpload = FileUpload; DropZone.defaultProps = { type: 'file', outline: true, overlay: true, allowMultiple: true, }; function handleDragStart(event) { event.preventDefault(); event.stopPropagation(); } export default withAppProvider()(DropZone);