UNPKG

@shopify/polaris

Version:

Shopify’s product component library

300 lines (276 loc) • 10.9 kB
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 };