@shopify/polaris
Version:
Shopify’s product component library
300 lines (276 loc) • 10.9 kB
JavaScript
import { objectWithoutProperties as _objectWithoutProperties } from '../../_virtual/_rollupPluginBabelHelpers.js';
import React$1, { useRef, useCallback, useState, useEffect, useMemo, Component, createRef } from 'react';
import { useFeatures } from '../../utilities/features/hooks.js';
import debounce$1 from 'lodash/debounce';
import { useUniqueId } from '../../utilities/unique-id/hooks.js';
import { useI18n } from '../../utilities/i18n/hooks.js';
import { isServer } from '../../utilities/target.js';
import { classNames, variationName } from '../../utilities/css.js';
import { CircleAlertMajor, DragDropMajor } from '@shopify/polaris-icons';
import { Icon as Icon$1 } from '../Icon/Icon.js';
import { VisuallyHidden as VisuallyHidden$1 } from '../VisuallyHidden/VisuallyHidden.js';
import { useToggle as useToggle$1 } from '../../utilities/use-toggle.js';
import { Stack as Stack$1 } from '../Stack/Stack.js';
import { Labelled as Labelled$1 } from '../Labelled/Labelled.js';
import { useComponentDidMount as useComponentDidMount$1 } from '../../utilities/use-component-did-mount.js';
import { Caption as Caption$1 } from '../Caption/Caption.js';
import { DisplayText as DisplayText$1 } from '../DisplayText/DisplayText.js';
import { capitalize as capitalize$1 } from '../../utilities/capitalize.js';
import { DropZoneContext } from './context.js';
import { FileUpload as FileUpload$1 } from './components/FileUpload/FileUpload.js';
import { fileAccepted, getDataTransferFiles } from './utils/index.js';
import styles from './DropZone.scss.js';
// TypeScript can't generate types that correctly infer the typing of
// subcomponents so explicitly state the subcomponents in the type definition.
// Letting this be implicit works in this project but fails in projects that use
// generated *.d.ts files.
var DropZone = function DropZone({
dropOnPage,
label,
labelAction,
labelHidden,
children,
disabled = false,
outline = true,
accept,
active,
overlay = true,
allowMultiple = true,
overlayText,
errorOverlayText,
id: idProp,
type = 'file',
onClick,
error,
openFileDialog,
onFileDialogClose,
customValidator,
onDrop,
onDropAccepted,
onDropRejected,
onDragEnter,
onDragOver,
onDragLeave
}) {
var {
newDesignLanguage
} = useFeatures();
var node = useRef(null);
var dragTargets = useRef([]); // eslint-disable-next-line react-hooks/exhaustive-deps
var adjustSize = useCallback(debounce$1(() => {
if (!node.current) {
return;
}
var size = 'extraLarge';
var width = node.current.getBoundingClientRect().width;
if (width < 100) {
size = 'small';
} else if (width < 160) {
size = 'medium';
} else if (width < 300) {
size = 'large';
}
setSize(size);
measuring && setMeasuring(false);
}, 50, {
trailing: true
}), []);
var [dragging, setDragging] = useState(false);
var [internalError, setInternalError] = useState(false);
var {
value: focused,
setTrue: handleFocus,
setFalse: handleBlur
} = useToggle$1(false);
var [size, setSize] = useState('extraLarge');
var [measuring, setMeasuring] = useState(true);
var i18n = useI18n();
var getValidatedFiles = useCallback(files => {
var acceptedFiles = [];
var rejectedFiles = [];
Array.from(files).forEach(file => {
!fileAccepted(file, accept) || customValidator && !customValidator(file) ? rejectedFiles.push(file) : acceptedFiles.push(file);
});
if (!allowMultiple) {
acceptedFiles.splice(1, acceptedFiles.length);
rejectedFiles.push(...acceptedFiles.slice(1));
}
return {
files,
acceptedFiles,
rejectedFiles
};
}, [accept, allowMultiple, customValidator]);
var handleDrop = useCallback(event => {
stopEvent(event);
if (disabled) return;
var fileList = getDataTransferFiles(event);
var {
files,
acceptedFiles,
rejectedFiles
} = getValidatedFiles(fileList);
dragTargets.current = [];
setDragging(false);
setInternalError(rejectedFiles.length > 0);
onDrop && onDrop(files, acceptedFiles, rejectedFiles);
onDropAccepted && acceptedFiles.length && onDropAccepted(acceptedFiles);
onDropRejected && rejectedFiles.length && onDropRejected(rejectedFiles);
event.target.value = '';
}, [disabled, getValidatedFiles, onDrop, onDropAccepted, onDropRejected]);
var handleDragEnter = useCallback(event => {
stopEvent(event);
if (disabled) return;
var fileList = getDataTransferFiles(event);
if (event.target && !dragTargets.current.includes(event.target)) {
dragTargets.current.push(event.target);
}
if (dragging) return;
var {
rejectedFiles
} = getValidatedFiles(fileList);
setDragging(true);
setInternalError(rejectedFiles.length > 0);
onDragEnter && onDragEnter();
}, [disabled, dragging, getValidatedFiles, onDragEnter]);
var handleDragOver = useCallback(event => {
stopEvent(event);
if (disabled) return;
onDragOver && onDragOver();
}, [disabled, onDragOver]);
var handleDragLeave = useCallback(event => {
event.preventDefault();
if (disabled) return;
dragTargets.current = dragTargets.current.filter(el => {
var compareNode = dropOnPage && !isServer ? document : node.current;
return el !== event.target && compareNode && compareNode.contains(el);
});
if (dragTargets.current.length > 0) return;
setDragging(false);
setInternalError(false);
onDragLeave && onDragLeave();
}, [dropOnPage, disabled, onDragLeave]);
useEffect(() => {
var dropNode = dropOnPage ? document : node.current;
if (!dropNode) return;
dropNode.addEventListener('drop', handleDrop);
dropNode.addEventListener('dragover', handleDragOver);
dropNode.addEventListener('dragenter', handleDragEnter);
dropNode.addEventListener('dragleave', handleDragLeave);
window.addEventListener('resize', adjustSize);
return () => {
dropNode.removeEventListener('drop', handleDrop);
dropNode.removeEventListener('dragover', handleDragOver);
dropNode.removeEventListener('dragenter', handleDragEnter);
dropNode.removeEventListener('dragleave', handleDragLeave);
window.removeEventListener('resize', adjustSize);
};
}, [dropOnPage, handleDrop, handleDragOver, handleDragEnter, handleDragLeave, adjustSize]);
useComponentDidMount$1(() => {
adjustSize();
});
var id = useUniqueId('DropZone', idProp);
var suffix = capitalize$1(type);
var overlayTextWithDefault = overlayText === undefined ? i18n.translate("Polaris.DropZone.overlayText".concat(suffix)) : overlayText;
var errorOverlayTextWithDefault = errorOverlayText === undefined ? i18n.translate("Polaris.DropZone.errorOverlayText".concat(suffix)) : errorOverlayText;
var inputAttributes = {
id,
accept,
disabled,
type: 'file',
multiple: allowMultiple,
onChange: handleDrop,
onFocus: handleFocus,
onBlur: handleBlur
};
var classes = classNames(styles.DropZone, outline && styles.hasOutline, focused && styles.focused, (active || dragging) && styles.isDragging, disabled && styles.isDisabled, newDesignLanguage && styles.newDesignLanguage, (internalError || error) && styles.hasError, styles[variationName('size', size)], measuring && styles.measuring);
var dragOverlay = (active || dragging) && (!internalError || !error) && overlay && overlayMarkup(DragDropMajor, 'indigo', overlayTextWithDefault);
var dragErrorOverlay = dragging && (internalError || error) && overlayMarkup(CircleAlertMajor, 'red', errorOverlayTextWithDefault);
var labelValue = label || i18n.translate('Polaris.DropZone.FileUpload.label');
var labelHiddenValue = label ? labelHidden : true;
var context = useMemo(() => ({
disabled,
focused,
size,
type: type || 'file',
measuring
}), [disabled, focused, measuring, size, type]);
return /*#__PURE__*/React$1.createElement(DropZoneContext.Provider, {
value: context
}, /*#__PURE__*/React$1.createElement(Labelled$1, {
id: id,
label: labelValue,
action: labelAction,
labelHidden: labelHiddenValue
}, /*#__PURE__*/React$1.createElement("div", {
ref: node,
className: classes,
"aria-disabled": disabled,
onClick: handleClick,
onDragStart: stopEvent
}, dragOverlay, dragErrorOverlay, /*#__PURE__*/React$1.createElement("div", {
className: styles.Container
}, children), /*#__PURE__*/React$1.createElement(VisuallyHidden$1, null, /*#__PURE__*/React$1.createElement(DropZoneInput, Object.assign({}, inputAttributes, {
openFileDialog: openFileDialog,
onFileDialogClose: onFileDialogClose
}))))));
function overlayMarkup(icon, color, text) {
var overlayClass = classNames(styles.Overlay, newDesignLanguage && styles.newDesignLanguage);
return /*#__PURE__*/React$1.createElement("div", {
className: overlayClass
}, /*#__PURE__*/React$1.createElement(Stack$1, {
vertical: true,
spacing: "tight"
}, /*#__PURE__*/React$1.createElement(Icon$1, {
source: icon,
color: color
}), size === 'extraLarge' && /*#__PURE__*/React$1.createElement(DisplayText$1, {
size: "small",
element: "p"
}, text), (size === 'medium' || size === 'large') && /*#__PURE__*/React$1.createElement(Caption$1, null, text)));
}
function open() {
var fileInputNode = node.current && node.current.querySelector("#".concat(id));
fileInputNode && fileInputNode instanceof HTMLElement && fileInputNode.click();
}
function handleClick(event) {
if (disabled) return;
return onClick ? onClick(event) : open();
}
};
function stopEvent(event) {
event.preventDefault();
event.stopPropagation();
}
DropZone.FileUpload = FileUpload$1;
// Due to security reasons, browsers do not allow file inputs to be opened artificially.
// For example `useEffect(() => { ref.click() })`. Oddly enough react class-based components bi-pass this.
class DropZoneInput extends Component {
constructor(...args) {
super(...args);
this.fileInputNode = /*#__PURE__*/createRef();
this.triggerFileDialog = () => {
this.open();
this.props.onFileDialogClose && this.props.onFileDialogClose();
};
this.open = () => {
if (!this.fileInputNode.current) return;
this.fileInputNode.current.click();
};
}
componentDidMount() {
this.props.openFileDialog && this.triggerFileDialog();
}
componentDidUpdate() {
this.props.openFileDialog && this.triggerFileDialog();
}
render() {
var _this$props = this.props,
inputProps = _objectWithoutProperties(_this$props, ["openFileDialog", "onFileDialogClose"]);
return /*#__PURE__*/React$1.createElement("input", Object.assign({}, inputProps, {
ref: this.fileInputNode,
autoComplete: "off"
}));
}
}
export { DropZone };