@hackplan/polaris
Version:
Shopify’s product component library
300 lines (299 loc) • 12.1 kB
JavaScript
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);