UNPKG

@drivy/cobalt

Version:

Opinionated design system for Drivy's projects.

185 lines (182 loc) 8.88 kB
import React, { useRef, useState, useEffect } from 'react'; import cx from 'classnames'; import '../Icon/index.js'; import { validateFile } from '../utils/validateFile.js'; import Popover from '../Popover/index.js'; import Modal from '../Modal/index.js'; import useBreakpoint from '../../hooks/useBreakpoint.js'; import BinIcon from '../Icon/__generated__/BinIcon.js'; import PlusIcon from '../Icon/__generated__/PlusIcon.js'; import ContextualWarningCircleFilledIcon from '../Icon/__generated__/ContextualWarningCircleFilledIcon.js'; import LoadingIcon from '../Icon/__generated__/LoadingIcon.js'; const ACCEPTED_MAX_SIZE_MB = 10; const ACCEPTED_PHOTOS_TYPES = ["jpg", "jpeg", "png", "gif"]; const ERROR_DISPLAY_TIME = 6000; const ACCEPTED_TYPES_LOCALE_STRING = ACCEPTED_PHOTOS_TYPES.join(", "); const preventEventDefaults = (e) => { e.preventDefault && e.preventDefault(); e.stopPropagation && e.stopPropagation(); }; const isEnterOrSpaceKey = (event) => event.key === "Enter" || event.key === " "; const PhotoDropzone = ({ className, description, deleteContent, errorContent, onDropped, onPhotoDelete, deleteContentMode = "popover", initialImageUrl = "", }) => { const { isMobile } = useBreakpoint(); const fileInputRef = useRef(null); const deleteButtonRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [isLoading, setIsLoading] = useState(false); const [isErrored, setIsErrored] = useState(false); const [imagePreviewUrl, setImagePreviewUrl] = useState(initialImageUrl); const [displayDeletion, setDisplayDeletion] = useState(false); const [showDeletePopover, setShowDeletePopover] = useState(false); const [isImageLoaded, setIsImageLoaded] = useState(false); const openDeletePopover = (event) => { event && preventEventDefaults(event); setShowDeletePopover(true); }; const closeDeletePopover = (event) => { event && preventEventDefaults(event); setShowDeletePopover(false); }; const onDropzoneMouseEnter = (event) => { event && preventEventDefaults(event); if (imagePreviewUrl) setDisplayDeletion(true); }; const onDropzoneMouseLeave = (event) => { event && preventEventDefaults(event); if (displayDeletion && !showDeletePopover) setDisplayDeletion(false); }; const onDropzoneClick = (event) => { var _a; if (isMobile && showDeletePopover) return; event && preventEventDefaults(event); if (isLoading) return; if (isErrored) setIsErrored(false); if (imagePreviewUrl) { if (showDeletePopover) closeDeletePopover(); return setDisplayDeletion(!displayDeletion); } (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }; const onDropzoneKey = (event) => { if (showDeletePopover) return; if (isEnterOrSpaceKey(event)) { onDropzoneClick(); } }; const onDeleteKey = (event) => { if (isEnterOrSpaceKey(event)) { openDeletePopover(event); } }; const onDelete = () => { if (fileInputRef.current) fileInputRef.current.value = ""; closeDeletePopover(); setImagePreviewUrl(""); onPhotoDelete && onPhotoDelete(); }; const onDragEnter = (event) => { preventEventDefaults(event); if (isErrored) setIsErrored(false); !imagePreviewUrl && setIsDragging(true); }; const onDragLeave = (event) => { preventEventDefaults(event); setIsDragging(false); }; const processFile = async (fileToProcess) => { setIsDragging(false); setIsErrored(false); setIsLoading(true); const isValidFile = await validateFile(fileToProcess, (fileToValidate, extension) => { const maxFileSize = ACCEPTED_MAX_SIZE_MB * 1000 * 1000; const acceptedFileTypes = ACCEPTED_PHOTOS_TYPES; if (fileToValidate.size > maxFileSize) { return false; } else if (!extension || !acceptedFileTypes.includes(extension)) { return false; } else { return true; } }); if (isValidFile) { const imageSrc = URL.createObjectURL(fileToProcess); setImagePreviewUrl(imageSrc); await onDropped(fileToProcess); setIsLoading(false); } else { setIsLoading(false); setIsErrored(true); setTimeout(() => { setIsErrored(false); }, ERROR_DISPLAY_TIME); } }; const onFileInputChanged = (event) => { var _a; if ((_a = event.target.files) === null || _a === void 0 ? void 0 : _a.length) { processFile(event.target.files[0]); } }; const onDrop = (event) => { var _a, _b; preventEventDefaults(event); if (((_b = (_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) === null || _b === void 0 ? void 0 : _b.length) && !imagePreviewUrl) { processFile(event.dataTransfer.files[0]); } }; useEffect(() => { setIsImageLoaded(false); }, [imagePreviewUrl]); const onDeleteButtonClick = showDeletePopover ? closeDeletePopover : openDeletePopover; return (React.createElement("div", { tabIndex: 0, className: cx("cobalt-photo-dropzone", className, { "cobalt-photo-dropzone--filled": imagePreviewUrl, "cobalt-photo-dropzone--dragging": isDragging, "cobalt-photo-dropzone--loading": isLoading, "cobalt-photo-dropzone--errored": isErrored, "cobalt-photo-dropzone--imageVisible": isImageLoaded, }), onMouseEnter: onDropzoneMouseEnter, onMouseLeave: onDropzoneMouseLeave, onDragEnter: onDragEnter, onDragLeave: onDragLeave, // Not on click because we also use mouseEnter listener onMouseUp: onDropzoneClick, onTouchEnd: onDropzoneClick, onKeyUp: onDropzoneKey, onDrop: onDrop, // Need to reset those listeners to avoid default browser behaviour onDragStart: preventEventDefaults, onDragEnd: preventEventDefaults, onDragOver: preventEventDefaults }, isErrored && (React.createElement("div", { className: "cobalt-photo-dropzone__description" }, React.createElement(ContextualWarningCircleFilledIcon, { color: "error" }), errorContent(ACCEPTED_TYPES_LOCALE_STRING, ACCEPTED_MAX_SIZE_MB))), isLoading && (React.createElement("div", { className: "cobalt-photo-dropzone__description" }, React.createElement(LoadingIcon, null))), !isLoading && !isErrored && (imagePreviewUrl ? (React.createElement(React.Fragment, null, React.createElement("img", { className: "cobalt-photo-dropzone__preview", src: imagePreviewUrl, onLoad: () => { setIsImageLoaded(true); } }), React.createElement("div", null, React.createElement("button", { className: cx("cobalt-photo-dropzone__delete-button", { "cobalt-photo-dropzone__delete-button--triggered": displayDeletion, }), ref: deleteButtonRef, // Must follow the click listeners on the dropzone, // in order to have the correct events bubbling onTouchEnd: onDeleteButtonClick, onMouseUp: onDeleteButtonClick, onKeyUp: onDeleteKey }, React.createElement(BinIcon, { color: "onSurface" })), deleteContentMode === "modal" && (React.createElement(Modal, { isOpen: showDeletePopover, "aria-label": "delete", bodySpacing: false }, deleteContent(onDelete, closeDeletePopover))), deleteContentMode === "popover" && (React.createElement(Popover, { targetRef: deleteButtonRef, isOpen: showDeletePopover, close: closeDeletePopover, placement: "left-start", distance: 12, bodySpacing: false, arrow: true }, deleteContent(onDelete, closeDeletePopover)))))) : (React.createElement("div", { className: "cobalt-photo-dropzone__description cobalt-photo-dropzone__description--strong" }, description && React.createElement("div", null, description), React.createElement(PlusIcon, null)))), React.createElement("input", { ref: fileInputRef, className: "cobalt-photo-dropzone__hidden-input", type: "file", onChange: onFileInputChanged, accept: ACCEPTED_PHOTOS_TYPES.map((ext) => `.${ext}`).join(","), multiple: false }))); }; export { PhotoDropzone as default }; //# sourceMappingURL=index.js.map