@drivy/cobalt
Version:
Opinionated design system for Drivy's projects.
185 lines (182 loc) • 8.88 kB
JavaScript
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